mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-09-01 08:04:07 +00:00
moving userconfig ownership to rust
This commit is contained in:
parent
6f16ee85d7
commit
bd65268dd2
143 changed files with 7556 additions and 9763 deletions
|
|
@ -1,3 +1,3 @@
|
|||
rust_input: crate::bridge
|
||||
rust_root: rust
|
||||
dart_output: lib/core
|
||||
dart_output: lib/core
|
||||
|
|
|
|||
|
|
@ -220,7 +220,11 @@ class _AppMainWidgetState extends State<AppMainWidget> {
|
|||
} else {
|
||||
// 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) {
|
||||
Log.info('Starting with proof of work calculation.');
|
||||
_proofOfWork = (
|
||||
|
|
|
|||
32
lib/core/api/proto/client/encrypted_content.dart
Normal file
32
lib/core/api/proto/client/encrypted_content.dart
Normal 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;
|
||||
}
|
||||
|
|
@ -5,15 +5,11 @@
|
|||
|
||||
import '../api/server/prekeys.dart';
|
||||
import '../frb_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 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 {
|
||||
stopped,
|
||||
|
|
@ -85,11 +81,10 @@ class PreparedOutgoingMessage {
|
|||
class RustApi {
|
||||
const RustApi();
|
||||
|
||||
static Future<ServerResultEmpty> addAdditionalUser({
|
||||
required PlatformInt64 userId,
|
||||
}) => RustLib.instance.api.crateBridgeApiRustApiAddAdditionalUser(
|
||||
userId: userId,
|
||||
);
|
||||
static Future<void> addAdditionalUser({required PlatformInt64 userId}) =>
|
||||
RustLib.instance.api.crateBridgeApiRustApiAddAdditionalUser(
|
||||
userId: userId,
|
||||
);
|
||||
|
||||
static Future<BigInt> allocateSequence() =>
|
||||
RustLib.instance.api.crateBridgeApiRustApiAllocateSequence();
|
||||
|
|
@ -97,15 +92,15 @@ class RustApi {
|
|||
static String apiBaseUrl({required String protocol}) =>
|
||||
RustLib.instance.api.crateBridgeApiRustApiApiBaseUrl(protocol: protocol);
|
||||
|
||||
static Future<ServerResultEmpty> changeUsername({required String username}) =>
|
||||
RustLib.instance.api.crateBridgeApiRustApiChangeUsername(
|
||||
username: username,
|
||||
);
|
||||
static Future<void> changeUsername({required String username}) => RustLib
|
||||
.instance
|
||||
.api
|
||||
.crateBridgeApiRustApiChangeUsername(username: username);
|
||||
|
||||
static Future<void> checkForDeletedUsernames() =>
|
||||
RustLib.instance.api.crateBridgeApiRustApiCheckForDeletedUsernames();
|
||||
|
||||
static Future<ServerResultVecU8> checkForPasswordlessNotification({
|
||||
static Future<Uint8List> checkForPasswordlessNotification({
|
||||
required String notificationId,
|
||||
required List<int> downloadAuthToken,
|
||||
required Int64List alreadyReceivedMessageIds,
|
||||
|
|
@ -119,11 +114,10 @@ class RustApi {
|
|||
static Future<void> close() =>
|
||||
RustLib.instance.api.crateBridgeApiRustApiClose();
|
||||
|
||||
static Future<ServerResultEmpty> confirmMemoriesUpload({
|
||||
required String mediaId,
|
||||
}) => RustLib.instance.api.crateBridgeApiRustApiConfirmMemoriesUpload(
|
||||
mediaId: mediaId,
|
||||
);
|
||||
static Future<void> confirmMemoriesUpload({required String mediaId}) =>
|
||||
RustLib.instance.api.crateBridgeApiRustApiConfirmMemoriesUpload(
|
||||
mediaId: mediaId,
|
||||
);
|
||||
|
||||
static Future<void> connect() =>
|
||||
RustLib.instance.api.crateBridgeApiRustApiConnect();
|
||||
|
|
@ -131,25 +125,31 @@ class RustApi {
|
|||
static Future<ApiConnectionState> connectionState() =>
|
||||
RustLib.instance.api.crateBridgeApiRustApiConnectionState();
|
||||
|
||||
static Future<ServerResultEmpty> deleteAccount() =>
|
||||
static Future<void> deleteAccount() =>
|
||||
RustLib.instance.api.crateBridgeApiRustApiDeleteAccount();
|
||||
|
||||
static Future<ServerResultEmpty> deleteMemory({required String mediaId}) =>
|
||||
static Future<void> deleteMemory({required String mediaId}) =>
|
||||
RustLib.instance.api.crateBridgeApiRustApiDeleteMemory(mediaId: mediaId);
|
||||
|
||||
static Future<ServerResultEmpty> disableMemoriesBackup() =>
|
||||
static Future<void> disableMemoriesBackup() =>
|
||||
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);
|
||||
|
||||
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() =>
|
||||
RustLib.instance.api.crateBridgeApiRustApiEvents();
|
||||
|
||||
static Future<ServerResultEmpty> forceIpaCheck() =>
|
||||
static Future<void> forceIpaCheck() =>
|
||||
RustLib.instance.api.crateBridgeApiRustApiForceIpaCheck();
|
||||
|
||||
static Future<ServerResultVecU8> getMemoriesUrl({
|
||||
static Future<Uint8List> getMemoriesUrl({
|
||||
required String mediaId,
|
||||
required bool thumbnail,
|
||||
}) => RustLib.instance.api.crateBridgeApiRustApiGetMemoriesUrl(
|
||||
|
|
@ -157,16 +157,16 @@ class RustApi {
|
|||
thumbnail: thumbnail,
|
||||
);
|
||||
|
||||
static Future<ServerResultVecU8> getMemoriesUsage() =>
|
||||
static Future<Uint8List> getMemoriesUsage() =>
|
||||
RustLib.instance.api.crateBridgeApiRustApiGetMemoriesUsage();
|
||||
|
||||
static Future<Uint8List> getPlanBalance() =>
|
||||
RustLib.instance.api.crateBridgeApiRustApiGetPlanBalance();
|
||||
|
||||
static Future<ServerResultVecU8> getProofOfWork() =>
|
||||
static Future<Uint8List> getProofOfWork() =>
|
||||
RustLib.instance.api.crateBridgeApiRustApiGetProofOfWork();
|
||||
|
||||
static Future<ServerResultVecU8> getServerKeyForPasswordlessRecovery({
|
||||
static Future<Uint8List> getServerKeyForPasswordlessRecovery({
|
||||
required PlatformInt64 userId,
|
||||
required List<int> serverKeyProtection,
|
||||
Uint8List? pinUnlockToken,
|
||||
|
|
@ -181,14 +181,13 @@ class RustApi {
|
|||
email: email,
|
||||
);
|
||||
|
||||
static Future<ServerResultVecU8> getUserById({
|
||||
required PlatformInt64 userId,
|
||||
}) => RustLib.instance.api.crateBridgeApiRustApiGetUserById(userId: userId);
|
||||
static Future<Uint8List> getUserById({required PlatformInt64 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);
|
||||
|
||||
static Future<ServerResultI64> getUserIdFromUsername({
|
||||
static Future<PlatformInt64> getUserIdFromUsername({
|
||||
required String username,
|
||||
}) => RustLib.instance.api.crateBridgeApiRustApiGetUserIdFromUsername(
|
||||
username: username,
|
||||
|
|
@ -230,7 +229,7 @@ class RustApi {
|
|||
quoteMessageId: quoteMessageId,
|
||||
);
|
||||
|
||||
static Future<Uint8List> ipaPurchase({
|
||||
static Future<void> ipaPurchase({
|
||||
required String productId,
|
||||
required String source,
|
||||
required String verificationData,
|
||||
|
|
@ -240,10 +239,8 @@ class RustApi {
|
|||
verificationData: verificationData,
|
||||
);
|
||||
|
||||
static Future<Uint8List> loadPlanBalance({required bool useCache}) => RustLib
|
||||
.instance
|
||||
.api
|
||||
.crateBridgeApiRustApiLoadPlanBalance(useCache: useCache);
|
||||
static Future<Uint8List> loadPlanBalance() =>
|
||||
RustLib.instance.api.crateBridgeApiRustApiLoadPlanBalance();
|
||||
|
||||
static Future<void> notifyMessagesOpened({
|
||||
required PlatformInt64 contactId,
|
||||
|
|
@ -253,13 +250,18 @@ class RustApi {
|
|||
messageIds: messageIds,
|
||||
);
|
||||
|
||||
static Future<void> performPasswordlessRecoveryHeartbeat() => RustLib
|
||||
.instance
|
||||
.api
|
||||
.crateBridgeApiRustApiPerformPasswordlessRecoveryHeartbeat();
|
||||
|
||||
static Future<PreparedOutgoingMessage?> prepareQueuedMessage({
|
||||
required String receiptId,
|
||||
}) => RustLib.instance.api.crateBridgeApiRustApiPrepareQueuedMessage(
|
||||
receiptId: receiptId,
|
||||
);
|
||||
|
||||
static Future<ServerResultI64> register({
|
||||
static Future<PlatformInt64> register({
|
||||
required String username,
|
||||
required PlatformInt64 proofOfWork,
|
||||
required String langCode,
|
||||
|
|
@ -271,7 +273,7 @@ class RustApi {
|
|||
isIos: isIos,
|
||||
);
|
||||
|
||||
static Future<ServerResultEmpty> registerPasswordlessNotification({
|
||||
static Future<void> registerPasswordlessNotification({
|
||||
required String notificationId,
|
||||
required List<int> downloadAuthToken,
|
||||
required String langCode,
|
||||
|
|
@ -284,7 +286,7 @@ class RustApi {
|
|||
googleFcm: googleFcm,
|
||||
);
|
||||
|
||||
static Future<ServerResultEmpty> registerPasswordlessRecovery({
|
||||
static Future<void> registerPasswordlessRecovery({
|
||||
required List<int> encryptedServerKey,
|
||||
Uint8List? pinUnlockToken,
|
||||
}) => RustLib.instance.api.crateBridgeApiRustApiRegisterPasswordlessRecovery(
|
||||
|
|
@ -297,13 +299,12 @@ class RustApi {
|
|||
static Future<void> reloadConfiguration() =>
|
||||
RustLib.instance.api.crateBridgeApiRustApiReloadConfiguration();
|
||||
|
||||
static Future<ServerResultEmpty> removeAdditionalUser({
|
||||
required PlatformInt64 userId,
|
||||
}) => RustLib.instance.api.crateBridgeApiRustApiRemoveAdditionalUser(
|
||||
userId: userId,
|
||||
);
|
||||
static Future<void> removeAdditionalUser({required PlatformInt64 userId}) =>
|
||||
RustLib.instance.api.crateBridgeApiRustApiRemoveAdditionalUser(
|
||||
userId: userId,
|
||||
);
|
||||
|
||||
static Future<ServerResultEmpty> reportUser({
|
||||
static Future<void> reportUser({
|
||||
required PlatformInt64 userId,
|
||||
required String reason,
|
||||
}) => RustLib.instance.api.crateBridgeApiRustApiReportUser(
|
||||
|
|
@ -321,7 +322,12 @@ class RustApi {
|
|||
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 originalDate,
|
||||
required String mediaId,
|
||||
|
|
@ -375,7 +381,7 @@ class RustApi {
|
|||
.api
|
||||
.crateBridgeApiRustApiSendQueuedMessage(receiptId: receiptId);
|
||||
|
||||
static Future<ServerResultEmpty> sendTextMessage({
|
||||
static Future<void> sendTextMessage({
|
||||
required PlatformInt64 userId,
|
||||
required List<int> body,
|
||||
Uint8List? pushData,
|
||||
|
|
@ -398,7 +404,7 @@ class RustApi {
|
|||
.api
|
||||
.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);
|
||||
|
||||
static Future<void> setNetworkAvailable({required bool available}) => RustLib
|
||||
|
|
@ -406,7 +412,7 @@ class RustApi {
|
|||
.api
|
||||
.crateBridgeApiRustApiSetNetworkAvailable(available: available);
|
||||
|
||||
static Future<ServerResultEmpty> submitRecoveryShare({
|
||||
static Future<void> submitRecoveryShare({
|
||||
required String notificationId,
|
||||
required List<int> encryptedMessage,
|
||||
}) => RustLib.instance.api.crateBridgeApiRustApiSubmitRecoveryShare(
|
||||
|
|
@ -414,10 +420,10 @@ class RustApi {
|
|||
encryptedMessage: encryptedMessage,
|
||||
);
|
||||
|
||||
static Future<ServerResultEmpty> updateFcmToken({required String token}) =>
|
||||
static Future<void> updateFcmToken({required String token}) =>
|
||||
RustLib.instance.api.crateBridgeApiRustApiUpdateFcmToken(token: token);
|
||||
|
||||
static Future<Uint8List> updateSignedPreKey({
|
||||
static Future<void> updateSignedPreKey({
|
||||
required PlatformInt64 id,
|
||||
required List<int> key,
|
||||
required List<int> signature,
|
||||
|
|
@ -427,7 +433,7 @@ class RustApi {
|
|||
signature: signature,
|
||||
);
|
||||
|
||||
static Future<Uint8List> uploadPqcPreKeys({
|
||||
static Future<void> uploadPqcPreKeys({
|
||||
required PlatformInt64 eccSignedPrekeyId,
|
||||
required List<int> eccSignedPrekey,
|
||||
required List<int> eccSignedPrekeySignature,
|
||||
|
|
@ -453,37 +459,3 @@ class RustApi {
|
|||
identical(this, other) ||
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
// 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';
|
||||
|
||||
// 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()
|
||||
legacySignalGeneratePrekeys,
|
||||
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)
|
||||
apiMediaAction,
|
||||
required FutureOr<void> Function(PlatformInt64, Uint8List)
|
||||
apiVerificationProof,
|
||||
required FutureOr<Uint8List?> Function(PlatformInt64, String?, Uint8List, int)
|
||||
apiCreatePushData,
|
||||
required FutureOr<void> Function(PlatformInt64) apiCreatePushAvatars,
|
||||
required FutureOr<void> Function() apiRecoveryChanged,
|
||||
required FutureOr<void> Function(String, PlatformInt64) apiMediaReceived,
|
||||
required FutureOr<void> Function(String, bool) apiGroupStateRefresh,
|
||||
required FutureOr<void> Function(UserConfig) apiUserConfigChanged,
|
||||
}) => RustLib.instance.api.crateBridgeCallbacksInitFlutterCallbacks(
|
||||
callbackId: callbackId,
|
||||
loggingGetStreamSink: loggingGetStreamSink,
|
||||
|
|
@ -48,15 +42,11 @@ Future<void> initFlutterCallbacks({
|
|||
legacySignalEncrypt: legacySignalEncrypt,
|
||||
legacySignalGeneratePrekeys: legacySignalGeneratePrekeys,
|
||||
apiResyncSignalSession: apiResyncSignalSession,
|
||||
apiPushKeyRequested: apiPushKeyRequested,
|
||||
apiGroupMembershipError: apiGroupMembershipError,
|
||||
apiMediaAction: apiMediaAction,
|
||||
apiVerificationProof: apiVerificationProof,
|
||||
apiCreatePushData: apiCreatePushData,
|
||||
apiCreatePushAvatars: apiCreatePushAvatars,
|
||||
apiRecoveryChanged: apiRecoveryChanged,
|
||||
apiMediaReceived: apiMediaReceived,
|
||||
apiGroupStateRefresh: apiGroupStateRefresh,
|
||||
apiUserConfigChanged: apiUserConfigChanged,
|
||||
);
|
||||
|
||||
class LegacySignalDecryptResult {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
// 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';
|
||||
|
||||
Future<bool> createNewGroup({
|
||||
|
|
|
|||
58
lib/core/bridge/user_config.dart
Normal file
58
lib/core/bridge/user_config.dart
Normal 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;
|
||||
}
|
||||
|
|
@ -9,62 +9,25 @@ import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
|
|||
class 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}) =>
|
||||
RustLib.instance.api
|
||||
.crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetCurrentVersion(
|
||||
callbackId: callbackId,
|
||||
);
|
||||
|
||||
static Future<List<Uint8List>> getNewMessages({
|
||||
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,
|
||||
);
|
||||
|
||||
/// UI-facing hook used when a user manually changes contact verification.
|
||||
static Future<void> updateVerificationStateForUser({
|
||||
required int callbackId,
|
||||
required PlatformInt64 contactId,
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -8,6 +8,7 @@ import 'bridge.dart';
|
|||
import 'bridge/api.dart';
|
||||
import 'bridge/callbacks.dart';
|
||||
import 'bridge/groups.dart';
|
||||
import 'bridge/user_config.dart';
|
||||
import 'bridge/wrapper.dart';
|
||||
import 'bridge/wrapper/app_database.dart';
|
||||
import 'bridge/wrapper/backup.dart';
|
||||
|
|
@ -23,6 +24,7 @@ import 'keys/backup_password_keys.dart';
|
|||
import 'lib.dart';
|
||||
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_io.dart';
|
||||
import 'signal/engine.dart';
|
||||
import 'user_config.dart';
|
||||
|
||||
abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
RustLibApiImplPlatform({
|
||||
|
|
@ -35,16 +37,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
@protected
|
||||
AnyhowException dco_decode_AnyhowException(dynamic raw);
|
||||
|
||||
@protected
|
||||
DateTime dco_decode_Chrono_Utc(dynamic raw);
|
||||
|
||||
@protected
|
||||
FutureOr<void> Function(String, String, PlatformInt64, String)
|
||||
dco_decode_DartFn_Inputs_String_String_i_64_String_Output_unit_AnyhowException(
|
||||
dynamic raw,
|
||||
);
|
||||
|
||||
@protected
|
||||
FutureOr<void> Function(String, bool)
|
||||
dco_decode_DartFn_Inputs_String_bool_Output_unit_AnyhowException(dynamic raw);
|
||||
|
||||
@protected
|
||||
FutureOr<void> Function(String, PlatformInt64)
|
||||
dco_decode_DartFn_Inputs_String_i_64_Output_unit_AnyhowException(dynamic raw);
|
||||
|
|
@ -61,20 +62,10 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
dynamic raw,
|
||||
);
|
||||
|
||||
@protected
|
||||
FutureOr<void> Function()
|
||||
dco_decode_DartFn_Inputs__Output_unit_AnyhowException(dynamic raw);
|
||||
|
||||
@protected
|
||||
FutureOr<void> Function(PlatformInt64)
|
||||
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
|
||||
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(
|
||||
|
|
@ -94,14 +85,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
);
|
||||
|
||||
@protected
|
||||
FutureOr<Uint8List?> Function(PlatformInt64, String?, Uint8List, int)
|
||||
dco_decode_DartFn_Inputs_i_64_opt_String_list_prim_u_8_strict_i_32_Output_opt_list_prim_u_8_strict_AnyhowException(
|
||||
dynamic raw,
|
||||
);
|
||||
FutureOr<void> Function(UserConfig)
|
||||
dco_decode_DartFn_Inputs_user_config_Output_unit_AnyhowException(dynamic raw);
|
||||
|
||||
@protected
|
||||
Object dco_decode_DartOpaque(dynamic raw);
|
||||
|
||||
@protected
|
||||
Map<String, List<String>> dco_decode_Map_String_list_String_None(dynamic raw);
|
||||
|
||||
@protected
|
||||
Map<PlatformInt64, Uint8List> dco_decode_Map_i_64_list_prim_u_8_strict_None(
|
||||
dynamic raw,
|
||||
|
|
@ -131,6 +123,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
@protected
|
||||
bool dco_decode_bool(dynamic raw);
|
||||
|
||||
@protected
|
||||
DateTime dco_decode_box_autoadd_Chrono_Utc(dynamic raw);
|
||||
|
||||
@protected
|
||||
ApiConnectionState dco_decode_box_autoadd_api_connection_state(dynamic raw);
|
||||
|
||||
|
|
@ -157,14 +152,24 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
dynamic raw,
|
||||
);
|
||||
|
||||
@protected
|
||||
PasswordlessRecoveryConfig
|
||||
dco_decode_box_autoadd_passwordless_recovery_config(dynamic raw);
|
||||
|
||||
@protected
|
||||
PreparedOutgoingMessage dco_decode_box_autoadd_prepared_outgoing_message(
|
||||
dynamic raw,
|
||||
);
|
||||
|
||||
@protected
|
||||
TwonlySafeBackup dco_decode_box_autoadd_twonly_safe_backup(dynamic raw);
|
||||
|
||||
@protected
|
||||
int dco_decode_box_autoadd_u_32(dynamic raw);
|
||||
|
||||
@protected
|
||||
UserConfig dco_decode_box_autoadd_user_config(dynamic raw);
|
||||
|
||||
@protected
|
||||
double dco_decode_f_64(dynamic raw);
|
||||
|
||||
|
|
@ -189,6 +194,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
@protected
|
||||
PlatformInt64 dco_decode_isize(dynamic raw);
|
||||
|
||||
@protected
|
||||
LastBackupUploadState dco_decode_last_backup_upload_state(dynamic raw);
|
||||
|
||||
@protected
|
||||
LegacyMigrationReport dco_decode_legacy_migration_report(dynamic raw);
|
||||
|
||||
|
|
@ -243,15 +251,28 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
List<(PlatformInt64, Uint8List)>
|
||||
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
|
||||
List<SqlRow> dco_decode_list_sql_row(dynamic raw);
|
||||
|
||||
@protected
|
||||
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
|
||||
String? dco_decode_opt_String(dynamic raw);
|
||||
|
||||
@protected
|
||||
DateTime? dco_decode_opt_box_autoadd_Chrono_Utc(dynamic raw);
|
||||
|
||||
@protected
|
||||
ApiConnectionState? dco_decode_opt_box_autoadd_api_connection_state(
|
||||
dynamic raw,
|
||||
|
|
@ -270,17 +291,35 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
LegacySignalEncryptResult?
|
||||
dco_decode_opt_box_autoadd_legacy_signal_encrypt_result(dynamic raw);
|
||||
|
||||
@protected
|
||||
PasswordlessRecoveryConfig?
|
||||
dco_decode_opt_box_autoadd_passwordless_recovery_config(dynamic raw);
|
||||
|
||||
@protected
|
||||
PreparedOutgoingMessage? dco_decode_opt_box_autoadd_prepared_outgoing_message(
|
||||
dynamic raw,
|
||||
);
|
||||
|
||||
@protected
|
||||
TwonlySafeBackup? dco_decode_opt_box_autoadd_twonly_safe_backup(dynamic raw);
|
||||
|
||||
@protected
|
||||
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
|
||||
Uint8List? dco_decode_opt_list_prim_u_8_strict(dynamic raw);
|
||||
|
||||
@protected
|
||||
PasswordlessRecoveryConfig dco_decode_passwordless_recovery_config(
|
||||
dynamic raw,
|
||||
);
|
||||
|
||||
@protected
|
||||
PqcPreKeyInput dco_decode_pqc_pre_key_input(dynamic raw);
|
||||
|
||||
|
|
@ -297,6 +336,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
dynamic raw,
|
||||
);
|
||||
|
||||
@protected
|
||||
(String, List<String>) dco_decode_record_string_list_string(dynamic raw);
|
||||
|
||||
@protected
|
||||
(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);
|
||||
|
||||
@protected
|
||||
ServerResultEmpty dco_decode_server_result_empty(dynamic raw);
|
||||
|
||||
@protected
|
||||
ServerResultI64 dco_decode_server_result_i_64(dynamic raw);
|
||||
|
||||
@protected
|
||||
ServerResultVecU8 dco_decode_server_result_vec_u_8(dynamic raw);
|
||||
SetupProfile dco_decode_setup_profile(dynamic raw);
|
||||
|
||||
@protected
|
||||
SqlExecutionResult dco_decode_sql_execution_result(dynamic raw);
|
||||
|
|
@ -342,6 +378,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
@protected
|
||||
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
|
||||
int dco_decode_u_32(dynamic raw);
|
||||
|
||||
|
|
@ -357,15 +399,29 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
@protected
|
||||
void dco_decode_unit(dynamic raw);
|
||||
|
||||
@protected
|
||||
UserConfig dco_decode_user_config(dynamic raw);
|
||||
|
||||
@protected
|
||||
UserConfigApi dco_decode_user_config_api(dynamic raw);
|
||||
|
||||
@protected
|
||||
BigInt dco_decode_usize(dynamic raw);
|
||||
|
||||
@protected
|
||||
AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
DateTime sse_decode_Chrono_Utc(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
Object sse_decode_DartOpaque(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
Map<String, List<String>> sse_decode_Map_String_list_String_None(
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
Map<PlatformInt64, Uint8List> sse_decode_Map_i_64_list_prim_u_8_strict_None(
|
||||
SseDeserializer deserializer,
|
||||
|
|
@ -403,6 +459,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
@protected
|
||||
bool sse_decode_bool(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
DateTime sse_decode_box_autoadd_Chrono_Utc(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
ApiConnectionState sse_decode_box_autoadd_api_connection_state(
|
||||
SseDeserializer deserializer,
|
||||
|
|
@ -435,14 +494,28 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
PasswordlessRecoveryConfig
|
||||
sse_decode_box_autoadd_passwordless_recovery_config(
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
PreparedOutgoingMessage sse_decode_box_autoadd_prepared_outgoing_message(
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
TwonlySafeBackup sse_decode_box_autoadd_twonly_safe_backup(
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
int sse_decode_box_autoadd_u_32(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
UserConfig sse_decode_box_autoadd_user_config(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
double sse_decode_f_64(SseDeserializer deserializer);
|
||||
|
||||
|
|
@ -469,6 +542,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
@protected
|
||||
PlatformInt64 sse_decode_isize(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
LastBackupUploadState sse_decode_last_backup_upload_state(
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
LegacyMigrationReport sse_decode_legacy_migration_report(
|
||||
SseDeserializer deserializer,
|
||||
|
|
@ -537,15 +615,28 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
List<(String, List<String>)> sse_decode_list_record_string_list_string(
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
List<SqlRow> sse_decode_list_sql_row(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
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
|
||||
String? sse_decode_opt_String(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
DateTime? sse_decode_opt_box_autoadd_Chrono_Utc(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
ApiConnectionState? sse_decode_opt_box_autoadd_api_connection_state(
|
||||
SseDeserializer deserializer,
|
||||
|
|
@ -566,17 +657,41 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
PasswordlessRecoveryConfig?
|
||||
sse_decode_opt_box_autoadd_passwordless_recovery_config(
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
PreparedOutgoingMessage? sse_decode_opt_box_autoadd_prepared_outgoing_message(
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
TwonlySafeBackup? sse_decode_opt_box_autoadd_twonly_safe_backup(
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
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
|
||||
Uint8List? sse_decode_opt_list_prim_u_8_strict(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
PasswordlessRecoveryConfig sse_decode_passwordless_recovery_config(
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
PqcPreKeyInput sse_decode_pqc_pre_key_input(SseDeserializer deserializer);
|
||||
|
||||
|
|
@ -595,6 +710,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
(String, List<String>) sse_decode_record_string_list_string(
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
(String, String) sse_decode_record_string_string(
|
||||
SseDeserializer deserializer,
|
||||
|
|
@ -626,17 +746,7 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
RustUtils sse_decode_rust_utils(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
ServerResultEmpty sse_decode_server_result_empty(
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
ServerResultI64 sse_decode_server_result_i_64(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
ServerResultVecU8 sse_decode_server_result_vec_u_8(
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
SetupProfile sse_decode_setup_profile(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
SqlExecutionResult sse_decode_sql_execution_result(
|
||||
|
|
@ -652,6 +762,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
@protected
|
||||
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
|
||||
int sse_decode_u_32(SseDeserializer deserializer);
|
||||
|
||||
|
|
@ -667,6 +783,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
@protected
|
||||
void sse_decode_unit(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
UserConfig sse_decode_user_config(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
UserConfigApi sse_decode_user_config_api(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
BigInt sse_decode_usize(SseDeserializer deserializer);
|
||||
|
||||
|
|
@ -676,6 +798,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_Chrono_Utc(DateTime self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void
|
||||
sse_encode_DartFn_Inputs_String_String_i_64_String_Output_unit_AnyhowException(
|
||||
|
|
@ -683,12 +808,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_DartFn_Inputs_String_bool_Output_unit_AnyhowException(
|
||||
FutureOr<void> Function(String, bool) self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_DartFn_Inputs_String_i_64_Output_unit_AnyhowException(
|
||||
FutureOr<void> Function(String, PlatformInt64) self,
|
||||
|
|
@ -708,24 +827,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_DartFn_Inputs__Output_unit_AnyhowException(
|
||||
FutureOr<void> Function() self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_DartFn_Inputs_i_64_Output_unit_AnyhowException(
|
||||
FutureOr<void> Function(PlatformInt64) self,
|
||||
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
|
||||
void
|
||||
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
|
||||
void
|
||||
sse_encode_DartFn_Inputs_i_64_opt_String_list_prim_u_8_strict_i_32_Output_opt_list_prim_u_8_strict_AnyhowException(
|
||||
FutureOr<Uint8List?> Function(PlatformInt64, String?, Uint8List, int) self,
|
||||
void sse_encode_DartFn_Inputs_user_config_Output_unit_AnyhowException(
|
||||
FutureOr<void> Function(UserConfig) self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
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
|
||||
void sse_encode_Map_i_64_list_prim_u_8_strict_None(
|
||||
Map<PlatformInt64, Uint8List> self,
|
||||
|
|
@ -801,6 +913,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
@protected
|
||||
void sse_encode_bool(bool self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_box_autoadd_Chrono_Utc(
|
||||
DateTime self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_box_autoadd_api_connection_state(
|
||||
ApiConnectionState self,
|
||||
|
|
@ -843,15 +961,33 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_box_autoadd_passwordless_recovery_config(
|
||||
PasswordlessRecoveryConfig self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_box_autoadd_prepared_outgoing_message(
|
||||
PreparedOutgoingMessage self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_box_autoadd_twonly_safe_backup(
|
||||
TwonlySafeBackup self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_box_autoadd_u_32(int self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_box_autoadd_user_config(
|
||||
UserConfig self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_f_64(double self, SseSerializer serializer);
|
||||
|
||||
|
|
@ -882,6 +1018,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
@protected
|
||||
void sse_encode_isize(PlatformInt64 self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_last_backup_upload_state(
|
||||
LastBackupUploadState self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_legacy_migration_report(
|
||||
LegacyMigrationReport self,
|
||||
|
|
@ -966,15 +1108,33 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_list_record_string_list_string(
|
||||
List<(String, List<String>)> self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_list_sql_row(List<SqlRow> self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
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
|
||||
void sse_encode_opt_String(String? self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_opt_box_autoadd_Chrono_Utc(
|
||||
DateTime? self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_opt_box_autoadd_api_connection_state(
|
||||
ApiConnectionState? self,
|
||||
|
|
@ -999,21 +1159,48 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_opt_box_autoadd_passwordless_recovery_config(
|
||||
PasswordlessRecoveryConfig? self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_opt_box_autoadd_prepared_outgoing_message(
|
||||
PreparedOutgoingMessage? self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_opt_box_autoadd_twonly_safe_backup(
|
||||
TwonlySafeBackup? self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
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
|
||||
void sse_encode_opt_list_prim_u_8_strict(
|
||||
Uint8List? self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_passwordless_recovery_config(
|
||||
PasswordlessRecoveryConfig self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_pqc_pre_key_input(
|
||||
PqcPreKeyInput self,
|
||||
|
|
@ -1038,6 +1225,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_record_string_list_string(
|
||||
(String, List<String>) self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_record_string_string(
|
||||
(String, String) self,
|
||||
|
|
@ -1078,22 +1271,7 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
void sse_encode_rust_utils(RustUtils self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_server_result_empty(
|
||||
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,
|
||||
);
|
||||
void sse_encode_setup_profile(SetupProfile self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_sql_execution_result(
|
||||
|
|
@ -1110,6 +1288,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
@protected
|
||||
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
|
||||
void sse_encode_u_32(int self, SseSerializer serializer);
|
||||
|
||||
|
|
@ -1125,6 +1312,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
@protected
|
||||
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
|
||||
void sse_encode_usize(BigInt self, SseSerializer serializer);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import 'bridge.dart';
|
|||
import 'bridge/api.dart';
|
||||
import 'bridge/callbacks.dart';
|
||||
import 'bridge/groups.dart';
|
||||
import 'bridge/user_config.dart';
|
||||
import 'bridge/wrapper.dart';
|
||||
import 'bridge/wrapper/app_database.dart';
|
||||
import 'bridge/wrapper/backup.dart';
|
||||
|
|
@ -25,6 +26,7 @@ import 'keys/backup_password_keys.dart';
|
|||
import 'lib.dart';
|
||||
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_web.dart';
|
||||
import 'signal/engine.dart';
|
||||
import 'user_config.dart';
|
||||
|
||||
abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
RustLibApiImplPlatform({
|
||||
|
|
@ -37,16 +39,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
@protected
|
||||
AnyhowException dco_decode_AnyhowException(dynamic raw);
|
||||
|
||||
@protected
|
||||
DateTime dco_decode_Chrono_Utc(dynamic raw);
|
||||
|
||||
@protected
|
||||
FutureOr<void> Function(String, String, PlatformInt64, String)
|
||||
dco_decode_DartFn_Inputs_String_String_i_64_String_Output_unit_AnyhowException(
|
||||
dynamic raw,
|
||||
);
|
||||
|
||||
@protected
|
||||
FutureOr<void> Function(String, bool)
|
||||
dco_decode_DartFn_Inputs_String_bool_Output_unit_AnyhowException(dynamic raw);
|
||||
|
||||
@protected
|
||||
FutureOr<void> Function(String, PlatformInt64)
|
||||
dco_decode_DartFn_Inputs_String_i_64_Output_unit_AnyhowException(dynamic raw);
|
||||
|
|
@ -63,20 +64,10 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
dynamic raw,
|
||||
);
|
||||
|
||||
@protected
|
||||
FutureOr<void> Function()
|
||||
dco_decode_DartFn_Inputs__Output_unit_AnyhowException(dynamic raw);
|
||||
|
||||
@protected
|
||||
FutureOr<void> Function(PlatformInt64)
|
||||
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
|
||||
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(
|
||||
|
|
@ -96,14 +87,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
);
|
||||
|
||||
@protected
|
||||
FutureOr<Uint8List?> Function(PlatformInt64, String?, Uint8List, int)
|
||||
dco_decode_DartFn_Inputs_i_64_opt_String_list_prim_u_8_strict_i_32_Output_opt_list_prim_u_8_strict_AnyhowException(
|
||||
dynamic raw,
|
||||
);
|
||||
FutureOr<void> Function(UserConfig)
|
||||
dco_decode_DartFn_Inputs_user_config_Output_unit_AnyhowException(dynamic raw);
|
||||
|
||||
@protected
|
||||
Object dco_decode_DartOpaque(dynamic raw);
|
||||
|
||||
@protected
|
||||
Map<String, List<String>> dco_decode_Map_String_list_String_None(dynamic raw);
|
||||
|
||||
@protected
|
||||
Map<PlatformInt64, Uint8List> dco_decode_Map_i_64_list_prim_u_8_strict_None(
|
||||
dynamic raw,
|
||||
|
|
@ -133,6 +125,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
@protected
|
||||
bool dco_decode_bool(dynamic raw);
|
||||
|
||||
@protected
|
||||
DateTime dco_decode_box_autoadd_Chrono_Utc(dynamic raw);
|
||||
|
||||
@protected
|
||||
ApiConnectionState dco_decode_box_autoadd_api_connection_state(dynamic raw);
|
||||
|
||||
|
|
@ -159,14 +154,24 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
dynamic raw,
|
||||
);
|
||||
|
||||
@protected
|
||||
PasswordlessRecoveryConfig
|
||||
dco_decode_box_autoadd_passwordless_recovery_config(dynamic raw);
|
||||
|
||||
@protected
|
||||
PreparedOutgoingMessage dco_decode_box_autoadd_prepared_outgoing_message(
|
||||
dynamic raw,
|
||||
);
|
||||
|
||||
@protected
|
||||
TwonlySafeBackup dco_decode_box_autoadd_twonly_safe_backup(dynamic raw);
|
||||
|
||||
@protected
|
||||
int dco_decode_box_autoadd_u_32(dynamic raw);
|
||||
|
||||
@protected
|
||||
UserConfig dco_decode_box_autoadd_user_config(dynamic raw);
|
||||
|
||||
@protected
|
||||
double dco_decode_f_64(dynamic raw);
|
||||
|
||||
|
|
@ -191,6 +196,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
@protected
|
||||
PlatformInt64 dco_decode_isize(dynamic raw);
|
||||
|
||||
@protected
|
||||
LastBackupUploadState dco_decode_last_backup_upload_state(dynamic raw);
|
||||
|
||||
@protected
|
||||
LegacyMigrationReport dco_decode_legacy_migration_report(dynamic raw);
|
||||
|
||||
|
|
@ -245,15 +253,28 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
List<(PlatformInt64, Uint8List)>
|
||||
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
|
||||
List<SqlRow> dco_decode_list_sql_row(dynamic raw);
|
||||
|
||||
@protected
|
||||
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
|
||||
String? dco_decode_opt_String(dynamic raw);
|
||||
|
||||
@protected
|
||||
DateTime? dco_decode_opt_box_autoadd_Chrono_Utc(dynamic raw);
|
||||
|
||||
@protected
|
||||
ApiConnectionState? dco_decode_opt_box_autoadd_api_connection_state(
|
||||
dynamic raw,
|
||||
|
|
@ -272,17 +293,35 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
LegacySignalEncryptResult?
|
||||
dco_decode_opt_box_autoadd_legacy_signal_encrypt_result(dynamic raw);
|
||||
|
||||
@protected
|
||||
PasswordlessRecoveryConfig?
|
||||
dco_decode_opt_box_autoadd_passwordless_recovery_config(dynamic raw);
|
||||
|
||||
@protected
|
||||
PreparedOutgoingMessage? dco_decode_opt_box_autoadd_prepared_outgoing_message(
|
||||
dynamic raw,
|
||||
);
|
||||
|
||||
@protected
|
||||
TwonlySafeBackup? dco_decode_opt_box_autoadd_twonly_safe_backup(dynamic raw);
|
||||
|
||||
@protected
|
||||
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
|
||||
Uint8List? dco_decode_opt_list_prim_u_8_strict(dynamic raw);
|
||||
|
||||
@protected
|
||||
PasswordlessRecoveryConfig dco_decode_passwordless_recovery_config(
|
||||
dynamic raw,
|
||||
);
|
||||
|
||||
@protected
|
||||
PqcPreKeyInput dco_decode_pqc_pre_key_input(dynamic raw);
|
||||
|
||||
|
|
@ -299,6 +338,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
dynamic raw,
|
||||
);
|
||||
|
||||
@protected
|
||||
(String, List<String>) dco_decode_record_string_list_string(dynamic raw);
|
||||
|
||||
@protected
|
||||
(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);
|
||||
|
||||
@protected
|
||||
ServerResultEmpty dco_decode_server_result_empty(dynamic raw);
|
||||
|
||||
@protected
|
||||
ServerResultI64 dco_decode_server_result_i_64(dynamic raw);
|
||||
|
||||
@protected
|
||||
ServerResultVecU8 dco_decode_server_result_vec_u_8(dynamic raw);
|
||||
SetupProfile dco_decode_setup_profile(dynamic raw);
|
||||
|
||||
@protected
|
||||
SqlExecutionResult dco_decode_sql_execution_result(dynamic raw);
|
||||
|
|
@ -344,6 +380,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
@protected
|
||||
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
|
||||
int dco_decode_u_32(dynamic raw);
|
||||
|
||||
|
|
@ -359,15 +401,29 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
@protected
|
||||
void dco_decode_unit(dynamic raw);
|
||||
|
||||
@protected
|
||||
UserConfig dco_decode_user_config(dynamic raw);
|
||||
|
||||
@protected
|
||||
UserConfigApi dco_decode_user_config_api(dynamic raw);
|
||||
|
||||
@protected
|
||||
BigInt dco_decode_usize(dynamic raw);
|
||||
|
||||
@protected
|
||||
AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
DateTime sse_decode_Chrono_Utc(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
Object sse_decode_DartOpaque(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
Map<String, List<String>> sse_decode_Map_String_list_String_None(
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
Map<PlatformInt64, Uint8List> sse_decode_Map_i_64_list_prim_u_8_strict_None(
|
||||
SseDeserializer deserializer,
|
||||
|
|
@ -405,6 +461,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
@protected
|
||||
bool sse_decode_bool(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
DateTime sse_decode_box_autoadd_Chrono_Utc(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
ApiConnectionState sse_decode_box_autoadd_api_connection_state(
|
||||
SseDeserializer deserializer,
|
||||
|
|
@ -437,14 +496,28 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
PasswordlessRecoveryConfig
|
||||
sse_decode_box_autoadd_passwordless_recovery_config(
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
PreparedOutgoingMessage sse_decode_box_autoadd_prepared_outgoing_message(
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
TwonlySafeBackup sse_decode_box_autoadd_twonly_safe_backup(
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
int sse_decode_box_autoadd_u_32(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
UserConfig sse_decode_box_autoadd_user_config(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
double sse_decode_f_64(SseDeserializer deserializer);
|
||||
|
||||
|
|
@ -471,6 +544,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
@protected
|
||||
PlatformInt64 sse_decode_isize(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
LastBackupUploadState sse_decode_last_backup_upload_state(
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
LegacyMigrationReport sse_decode_legacy_migration_report(
|
||||
SseDeserializer deserializer,
|
||||
|
|
@ -539,15 +617,28 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
List<(String, List<String>)> sse_decode_list_record_string_list_string(
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
List<SqlRow> sse_decode_list_sql_row(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
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
|
||||
String? sse_decode_opt_String(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
DateTime? sse_decode_opt_box_autoadd_Chrono_Utc(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
ApiConnectionState? sse_decode_opt_box_autoadd_api_connection_state(
|
||||
SseDeserializer deserializer,
|
||||
|
|
@ -568,17 +659,41 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
PasswordlessRecoveryConfig?
|
||||
sse_decode_opt_box_autoadd_passwordless_recovery_config(
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
PreparedOutgoingMessage? sse_decode_opt_box_autoadd_prepared_outgoing_message(
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
TwonlySafeBackup? sse_decode_opt_box_autoadd_twonly_safe_backup(
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
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
|
||||
Uint8List? sse_decode_opt_list_prim_u_8_strict(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
PasswordlessRecoveryConfig sse_decode_passwordless_recovery_config(
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
PqcPreKeyInput sse_decode_pqc_pre_key_input(SseDeserializer deserializer);
|
||||
|
||||
|
|
@ -597,6 +712,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
(String, List<String>) sse_decode_record_string_list_string(
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
(String, String) sse_decode_record_string_string(
|
||||
SseDeserializer deserializer,
|
||||
|
|
@ -628,17 +748,7 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
RustUtils sse_decode_rust_utils(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
ServerResultEmpty sse_decode_server_result_empty(
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
ServerResultI64 sse_decode_server_result_i_64(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
ServerResultVecU8 sse_decode_server_result_vec_u_8(
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
SetupProfile sse_decode_setup_profile(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
SqlExecutionResult sse_decode_sql_execution_result(
|
||||
|
|
@ -654,6 +764,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
@protected
|
||||
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
|
||||
int sse_decode_u_32(SseDeserializer deserializer);
|
||||
|
||||
|
|
@ -669,6 +785,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
@protected
|
||||
void sse_decode_unit(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
UserConfig sse_decode_user_config(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
UserConfigApi sse_decode_user_config_api(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
BigInt sse_decode_usize(SseDeserializer deserializer);
|
||||
|
||||
|
|
@ -678,6 +800,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_Chrono_Utc(DateTime self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void
|
||||
sse_encode_DartFn_Inputs_String_String_i_64_String_Output_unit_AnyhowException(
|
||||
|
|
@ -685,12 +810,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_DartFn_Inputs_String_bool_Output_unit_AnyhowException(
|
||||
FutureOr<void> Function(String, bool) self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_DartFn_Inputs_String_i_64_Output_unit_AnyhowException(
|
||||
FutureOr<void> Function(String, PlatformInt64) self,
|
||||
|
|
@ -710,24 +829,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_DartFn_Inputs__Output_unit_AnyhowException(
|
||||
FutureOr<void> Function() self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_DartFn_Inputs_i_64_Output_unit_AnyhowException(
|
||||
FutureOr<void> Function(PlatformInt64) self,
|
||||
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
|
||||
void
|
||||
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
|
||||
void
|
||||
sse_encode_DartFn_Inputs_i_64_opt_String_list_prim_u_8_strict_i_32_Output_opt_list_prim_u_8_strict_AnyhowException(
|
||||
FutureOr<Uint8List?> Function(PlatformInt64, String?, Uint8List, int) self,
|
||||
void sse_encode_DartFn_Inputs_user_config_Output_unit_AnyhowException(
|
||||
FutureOr<void> Function(UserConfig) self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
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
|
||||
void sse_encode_Map_i_64_list_prim_u_8_strict_None(
|
||||
Map<PlatformInt64, Uint8List> self,
|
||||
|
|
@ -803,6 +915,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
@protected
|
||||
void sse_encode_bool(bool self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_box_autoadd_Chrono_Utc(
|
||||
DateTime self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_box_autoadd_api_connection_state(
|
||||
ApiConnectionState self,
|
||||
|
|
@ -845,15 +963,33 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_box_autoadd_passwordless_recovery_config(
|
||||
PasswordlessRecoveryConfig self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_box_autoadd_prepared_outgoing_message(
|
||||
PreparedOutgoingMessage self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_box_autoadd_twonly_safe_backup(
|
||||
TwonlySafeBackup self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_box_autoadd_u_32(int self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_box_autoadd_user_config(
|
||||
UserConfig self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_f_64(double self, SseSerializer serializer);
|
||||
|
||||
|
|
@ -884,6 +1020,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
@protected
|
||||
void sse_encode_isize(PlatformInt64 self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_last_backup_upload_state(
|
||||
LastBackupUploadState self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_legacy_migration_report(
|
||||
LegacyMigrationReport self,
|
||||
|
|
@ -968,15 +1110,33 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_list_record_string_list_string(
|
||||
List<(String, List<String>)> self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_list_sql_row(List<SqlRow> self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
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
|
||||
void sse_encode_opt_String(String? self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_opt_box_autoadd_Chrono_Utc(
|
||||
DateTime? self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_opt_box_autoadd_api_connection_state(
|
||||
ApiConnectionState? self,
|
||||
|
|
@ -1001,21 +1161,48 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_opt_box_autoadd_passwordless_recovery_config(
|
||||
PasswordlessRecoveryConfig? self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_opt_box_autoadd_prepared_outgoing_message(
|
||||
PreparedOutgoingMessage? self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_opt_box_autoadd_twonly_safe_backup(
|
||||
TwonlySafeBackup? self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
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
|
||||
void sse_encode_opt_list_prim_u_8_strict(
|
||||
Uint8List? self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_passwordless_recovery_config(
|
||||
PasswordlessRecoveryConfig self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_pqc_pre_key_input(
|
||||
PqcPreKeyInput self,
|
||||
|
|
@ -1040,6 +1227,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_record_string_list_string(
|
||||
(String, List<String>) self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_record_string_string(
|
||||
(String, String) self,
|
||||
|
|
@ -1080,22 +1273,7 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
void sse_encode_rust_utils(RustUtils self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_server_result_empty(
|
||||
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,
|
||||
);
|
||||
void sse_encode_setup_profile(SetupProfile self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_sql_execution_result(
|
||||
|
|
@ -1112,6 +1290,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
@protected
|
||||
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
|
||||
void sse_encode_u_32(int self, SseSerializer serializer);
|
||||
|
||||
|
|
@ -1127,6 +1314,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
@protected
|
||||
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
|
||||
void sse_encode_usize(BigInt self, SseSerializer serializer);
|
||||
}
|
||||
|
|
|
|||
357
lib/core/user_config.dart
Normal file
357
lib/core/user_config.dart
Normal 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;
|
||||
}
|
||||
|
|
@ -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/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;
|
||||
|
||||
void setupLocator() {
|
||||
|
|
|
|||
|
|
@ -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/notifications/fcm.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/exclusive_access.utils.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));
|
||||
unawaited(initializeBackgroundTaskManager());
|
||||
// 3. Delayed tasks (Wait for app to settle)
|
||||
|
|
|
|||
|
|
@ -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/globals.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/callbacks/legacy_signal.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/messages.api.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/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/user.service.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(
|
||||
String kind,
|
||||
String mediaId,
|
||||
int contactId,
|
||||
String messageId,
|
||||
) 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);
|
||||
if (media == null) return;
|
||||
switch (kind) {
|
||||
case 'download':
|
||||
await startDownloadMedia(media, false);
|
||||
case 'stored':
|
||||
await MediaFileService(media).storeMediaFile();
|
||||
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 {
|
||||
await initFlutterCallbacks(
|
||||
callbackId: isolateCallbackId,
|
||||
|
|
@ -95,31 +35,15 @@ Future<void> initFlutterCallbacksForRust() async {
|
|||
legacySignalEncrypt: LegacySignalCallbacks.encrypt,
|
||||
legacySignalGeneratePrekeys: LegacySignalCallbacks.generatePrekeys,
|
||||
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,
|
||||
apiVerificationProof: KeyVerificationService.handleVerificationProof,
|
||||
apiCreatePushData: _apiCreatePushData,
|
||||
apiCreatePushAvatars: (contactId) =>
|
||||
createPushAvatars(forceForUserId: contactId),
|
||||
apiRecoveryChanged: PasswordlessRecoveryService.performHeartbeat,
|
||||
apiMediaReceived: (groupId, timestamp) => incFlameCounter(
|
||||
groupId,
|
||||
true,
|
||||
DateTime.fromMillisecondsSinceEpoch(timestamp),
|
||||
),
|
||||
apiGroupStateRefresh: _apiGroupStateRefresh,
|
||||
apiUserConfigChanged: UserService.handleRustUserConfigChanged,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import 'package:drift/drift.dart';
|
|||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/database/tables/contacts.table.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';
|
||||
|
||||
part 'contacts.dao.g.dart';
|
||||
|
|
@ -86,7 +85,6 @@ class ContactsDao extends DatabaseAccessor<TwonlyDB> with _$ContactsDaoMixin {
|
|||
updatedValues.username.present) {
|
||||
final contact = await getContactByUserId(userId).getSingleOrNull();
|
||||
if (contact != null) {
|
||||
await updatePushUser(contact);
|
||||
final group = await twonlyDB.groupsDao.getDirectChat(userId);
|
||||
if (group != null) {
|
||||
await twonlyDB.groupsDao.updateGroup(
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -1618,7 +1618,7 @@ class Response_UserData extends $pb.GeneratedMessage {
|
|||
create()..mergeFromJson(json, registry);
|
||||
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
||||
_omitMessageNames ? '' : 'Response.UserData',
|
||||
_omitMessageNames ? '' : 'Response.UserConfig',
|
||||
package:
|
||||
const $pb.PackageName(_omitMessageNames ? '' : 'server_to_client'),
|
||||
createEmptyInstance: create)
|
||||
|
|
|
|||
|
|
@ -564,7 +564,7 @@ const Response_PqcBundle$json = {
|
|||
|
||||
@$core.Deprecated('Use responseDescriptor instead')
|
||||
const Response_UserData$json = {
|
||||
'1': 'UserData',
|
||||
'1': 'UserConfig',
|
||||
'2': [
|
||||
{'1': 'user_id', '3': 1, '4': 1, '5': 3, '10': 'userId'},
|
||||
{
|
||||
|
|
@ -868,7 +868,7 @@ const Response_Ok$json = {
|
|||
'3': 5,
|
||||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.server_to_client.Response.UserData',
|
||||
'6': '.server_to_client.Response.UserConfig',
|
||||
'9': 0,
|
||||
'10': 'userdata'
|
||||
},
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import 'package:flutter/foundation.dart';
|
|||
import 'package:in_app_purchase/in_app_purchase.dart';
|
||||
import 'package:twonly/locator.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/services/subscription.service.dart';
|
||||
import 'package:twonly/src/services/user.service.dart';
|
||||
|
|
@ -107,7 +106,7 @@ class PurchasesProvider with ChangeNotifier, DiagnosticableTreeMixin {
|
|||
Log.info(
|
||||
'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.verificationData.source);
|
||||
}
|
||||
final res = await apiService.ipaPurchase(
|
||||
purchaseDetails.productID,
|
||||
purchaseDetails.verificationData.source,
|
||||
purchaseDetails.verificationData.serverVerificationData,
|
||||
final res = await rustApiResult(
|
||||
RustApi.ipaPurchase(
|
||||
productId: purchaseDetails.productID,
|
||||
source: purchaseDetails.verificationData.source,
|
||||
verificationData:
|
||||
purchaseDetails.verificationData.serverVerificationData,
|
||||
),
|
||||
);
|
||||
// plan is updated in the apiProvider, as the server updates its states and responses with
|
||||
// an ok authenticated which is processed in the apiProvider...
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/core/user_config.dart' as rust_config;
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/services/user.service.dart';
|
||||
import 'package:twonly/src/visual/themes/light.dart';
|
||||
|
|
@ -13,8 +14,15 @@ class SettingsChangeProvider with ChangeNotifier, DiagnosticableTreeMixin {
|
|||
|
||||
void loadSettings() {
|
||||
if (userService.isUserCreated) {
|
||||
_themeMode = userService.currentUser.themeMode;
|
||||
_primaryColor = userService.currentUser.primaryColor;
|
||||
_themeMode = switch (userService.currentUser.themeMode) {
|
||||
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();
|
||||
} else {
|
||||
_themeMode = ThemeMode.system;
|
||||
|
|
@ -31,7 +39,13 @@ class SettingsChangeProvider with ChangeNotifier, DiagnosticableTreeMixin {
|
|||
|
||||
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 {
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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}',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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(),
|
||||
);
|
||||
}
|
||||
|
|
@ -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: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/src/database/tables/mediafiles.table.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 {
|
||||
// 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
|
||||
.getAllMediaFilesPendingDownload();
|
||||
|
||||
for (final mediaFile in mediaFiles) {
|
||||
if (await canMediaFileBeDownloaded(mediaFile)) {
|
||||
await startDownloadMedia(mediaFile, force);
|
||||
for (final media in mediaFiles) {
|
||||
if (await isAllowedToDownload(media.type)) {
|
||||
await RustApi.downloadMedia(mediaId: media.mediaId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> canMediaFileBeDownloaded(MediaFile mediaFile) async {
|
||||
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,
|
||||
}
|
||||
enum DownloadMediaTypes { video, image, audio }
|
||||
|
||||
Map<String, List<String>> defaultAutoDownloadOptions = {
|
||||
ConnectivityResult.mobile.name: [
|
||||
DownloadMediaTypes.audio.name,
|
||||
],
|
||||
ConnectivityResult.mobile.name: [DownloadMediaTypes.audio.name],
|
||||
ConnectivityResult.wifi.name: [
|
||||
DownloadMediaTypes.video.name,
|
||||
DownloadMediaTypes.image.name,
|
||||
|
|
@ -93,288 +31,40 @@ Map<String, List<String>> defaultAutoDownloadOptions = {
|
|||
};
|
||||
|
||||
Future<bool> isAllowedToDownload(MediaType type) async {
|
||||
if (type == MediaType.audio) {
|
||||
return true; // always download audio files
|
||||
}
|
||||
if (type == MediaType.audio) return true;
|
||||
final connectivityResult = await Connectivity().checkConnectivity();
|
||||
|
||||
final options =
|
||||
userService.currentUser.autoDownloadOptions ?? defaultAutoDownloadOptions;
|
||||
|
||||
if (connectivityResult.contains(ConnectivityResult.mobile)) {
|
||||
if (type == MediaType.video) {
|
||||
if (options[ConnectivityResult.mobile.name]!.contains(
|
||||
DownloadMediaTypes.video.name,
|
||||
)) {
|
||||
return true;
|
||||
}
|
||||
} else if (options[ConnectivityResult.mobile.name]!.contains(
|
||||
DownloadMediaTypes.image.name,
|
||||
)) {
|
||||
return true;
|
||||
}
|
||||
return options[ConnectivityResult.mobile.name]!.contains(
|
||||
type == MediaType.video
|
||||
? DownloadMediaTypes.video.name
|
||||
: DownloadMediaTypes.image.name,
|
||||
);
|
||||
}
|
||||
if (connectivityResult.contains(ConnectivityResult.wifi)) {
|
||||
if (type == MediaType.video) {
|
||||
if (options[ConnectivityResult.wifi.name]!.contains(
|
||||
DownloadMediaTypes.video.name,
|
||||
)) {
|
||||
return true;
|
||||
}
|
||||
} else if (options[ConnectivityResult.wifi.name]!.contains(
|
||||
DownloadMediaTypes.image.name,
|
||||
)) {
|
||||
return true;
|
||||
}
|
||||
return options[ConnectivityResult.wifi.name]!.contains(
|
||||
type == MediaType.video
|
||||
? DownloadMediaTypes.video.name
|
||||
: DownloadMediaTypes.image.name,
|
||||
);
|
||||
}
|
||||
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 {
|
||||
final mediaService = MediaFileService(media);
|
||||
|
||||
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');
|
||||
if (force || await isAllowedToDownload(media.type)) {
|
||||
await RustApi.downloadMedia(mediaId: media.mediaId);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> downloadFileFast(
|
||||
MediaFile media,
|
||||
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> requestMediaReupload(String mediaId) =>
|
||||
RustApi.requestMediaReupload(mediaId: mediaId);
|
||||
|
||||
Future<void> makeMigrationToVersion91() async {
|
||||
final messages = await twonlyDB.mediaFilesDao
|
||||
final mediaFiles = await twonlyDB.mediaFilesDao
|
||||
.getAllMediaFilesReuploadRequested();
|
||||
for (final message in messages) {
|
||||
await requestMediaReupload(message.mediaId);
|
||||
for (final media in mediaFiles) {
|
||||
await RustApi.requestMediaReupload(mediaId: media.mediaId);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import 'package:flutter/foundation.dart';
|
|||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/services/api/mediafiles/download.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/mediafiles/mediafile.service.dart';
|
||||
|
|
@ -19,10 +18,6 @@ Future<void> initFileDownloader() async {
|
|||
if (update.task.taskId.contains('upload_')) {
|
||||
await handleUploadStatusUpdate(update);
|
||||
}
|
||||
if (update.task.taskId.contains('download_')) {
|
||||
await handleDownloadStatusUpdate(update);
|
||||
}
|
||||
|
||||
if (update.task.taskId.contains('backup_')) {
|
||||
await BackupService.handleBackupStatusUpdate(
|
||||
update.task.taskId,
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
as pb;
|
||||
|
||||
// Compatibility adapters. All messaging state and behavior lives in Rust.
|
||||
Future<void> retransmitAllMessages() =>
|
||||
rust_api.RustApi.retransmitAllMessages();
|
||||
|
||||
Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({
|
||||
String? receiptId,
|
||||
Receipt? receipt,
|
||||
|
|
|
|||
50
lib/src/services/api/rust_api_result.dart
Normal file
50
lib/src/services/api/rust_api_result.dart
Normal 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);
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -6,30 +6,26 @@ import 'package:twonly/core/bridge/wrapper/key_manager.dart';
|
|||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/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'
|
||||
as server;
|
||||
import 'package:twonly/src/model/protobuf/client/generated/messages.pbserver.dart'
|
||||
hide Message;
|
||||
import 'package:twonly/src/services/api/messages.api.dart';
|
||||
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
|
||||
import 'package:twonly/src/services/signal/session.signal.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/utils/secure_storage.dart';
|
||||
|
||||
class Result<T, E> {
|
||||
Result.error(this.error) : value = null;
|
||||
Result.success(this.value) : error = null;
|
||||
Result.error(this.error) : value = null, _isSuccess = false;
|
||||
Result.success(this.value) : error = null, _isSuccess = true;
|
||||
|
||||
final T? value;
|
||||
final E? error;
|
||||
final bool _isSuccess;
|
||||
|
||||
bool get isSuccess => value != null;
|
||||
bool get isError => error != null;
|
||||
bool get isSuccess => _isSuccess;
|
||||
bool get isError => !_isSuccess;
|
||||
}
|
||||
|
||||
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 {
|
||||
await twonlyDB.mediaFilesDao.updateMedia(
|
||||
media.mediaId,
|
||||
|
|
@ -100,11 +80,6 @@ Future<bool> importSignalContactAndCreateRequest(
|
|||
return false;
|
||||
}
|
||||
|
||||
// 1. Setup notifications keys with the other user
|
||||
await setupNotificationWithUsers(
|
||||
forceContact: userdata.userId.toInt(),
|
||||
);
|
||||
|
||||
// 2. Then send user request
|
||||
await sendCipherText(
|
||||
userdata.userId.toInt(),
|
||||
|
|
|
|||
|
|
@ -309,8 +309,11 @@ class BackupService {
|
|||
String username,
|
||||
String password,
|
||||
) async {
|
||||
final userId = await apiService.getUserIdFromUsername(username);
|
||||
if (userId == null) {
|
||||
late final int userId;
|
||||
try {
|
||||
userId = await RustApi.getUserIdFromUsername(username: username);
|
||||
} catch (error) {
|
||||
Log.error('Could not resolve backup username', error: error);
|
||||
return RecoveryError.usernameNotValid;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -171,7 +171,10 @@ class MemoriesCloudService {
|
|||
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;
|
||||
|
||||
try {
|
||||
|
|
@ -205,10 +208,13 @@ class MemoriesCloudService {
|
|||
}
|
||||
|
||||
final sizeBytes = ms.storedPath.lengthSync();
|
||||
final urls = await apiService.requestMemoriesUpload(
|
||||
sizeBytes,
|
||||
mediaFile.createdAt,
|
||||
mediaFile.mediaId,
|
||||
final urls = await rustApiProtobuf(
|
||||
RustApi.requestMemoriesUpload(
|
||||
size: sizeBytes,
|
||||
originalDate: mediaFile.createdAt.millisecondsSinceEpoch,
|
||||
mediaId: mediaFile.mediaId,
|
||||
),
|
||||
decodeMemoriesUploadUrls,
|
||||
);
|
||||
|
||||
if (urls == null) {
|
||||
|
|
@ -268,8 +274,8 @@ class MemoriesCloudService {
|
|||
}
|
||||
|
||||
// 3. Confirm upload
|
||||
final confirmRes = await apiService.confirmMemoriesUpload(
|
||||
mediaFile.mediaId,
|
||||
final confirmRes = await rustApiResult(
|
||||
RustApi.confirmMemoriesUpload(mediaId: mediaFile.mediaId),
|
||||
);
|
||||
if (confirmRes.isSuccess) {
|
||||
await twonlyDB.mediaFilesDao.updateMedia(
|
||||
|
|
|
|||
|
|
@ -98,10 +98,8 @@ Future<void> runMigrations() async {
|
|||
..canUseLoginTokenForAuth = false
|
||||
// As usernames changes where not considered in the old version force users
|
||||
// to reenter there passwords.
|
||||
// ignore: deprecated_member_use_from_same_package
|
||||
..twonlySafeBackup?.encryptionKey = []
|
||||
// ignore: deprecated_member_use_from_same_package
|
||||
..twonlySafeBackup?.backupId = [];
|
||||
..twonlySafeBackup?.encryptionKey = Uint8List(0)
|
||||
..twonlySafeBackup?.backupId = Uint8List(0);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,14 +9,11 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
|||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/locator.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_de.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/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/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(
|
||||
List<int> key,
|
||||
EncryptedPushNotification push,
|
||||
|
|
|
|||
|
|
@ -36,22 +36,16 @@ class FcmNotificationService {
|
|||
|
||||
static Future<void> initFCMAfterAuthenticated({bool force = false}) async {
|
||||
final fcmToken = userService.currentUser.fcmToken;
|
||||
if (userService.currentUser.updateFCMToken || force) {
|
||||
if (userService.currentUser.updateFcmToken || force) {
|
||||
if (fcmToken == null) {
|
||||
Log.error('FCM token could not be updated as it is empty');
|
||||
await _checkForTokenUpdates();
|
||||
return;
|
||||
}
|
||||
final res = await apiService.updateFCMToken(
|
||||
fcmToken,
|
||||
);
|
||||
if (res.isSuccess) {
|
||||
Log.info('Uploaded new FCM token!');
|
||||
if (await _uploadFcmToken(fcmToken)) {
|
||||
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.');
|
||||
await UserService.update((u) {
|
||||
u
|
||||
..updateFCMToken = true
|
||||
..updateFcmToken = true
|
||||
..fcmToken = fcmToken;
|
||||
});
|
||||
if (apiService.isAuthenticated) {
|
||||
final res = await apiService.updateFCMToken(fcmToken);
|
||||
if (res.isSuccess) {
|
||||
Log.info('Uploaded new FCM token!');
|
||||
if (await _uploadFcmToken(fcmToken)) {
|
||||
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 {
|
||||
await UserService.update((u) {
|
||||
u
|
||||
..updateFCMToken = true
|
||||
..updateFcmToken = true
|
||||
..fcmToken = fcmToken;
|
||||
});
|
||||
if (apiService.isAuthenticated) {
|
||||
final res = await apiService.updateFCMToken(fcmToken);
|
||||
if (res.isSuccess) {
|
||||
Log.info('Uploaded new FCM token!');
|
||||
if (await _uploadFcmToken(fcmToken)) {
|
||||
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 {
|
||||
Log.info('handleRemoteMessage received message: ${message.messageId}');
|
||||
await _updateLastFcmMessageTimestamp();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,23 +1,23 @@
|
|||
import 'dart:async';
|
||||
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'
|
||||
show Hkdf, Hmac, Mac, SecretBox, SecretKey, Xchacha20;
|
||||
import 'package:drift/drift.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/key_manager.dart';
|
||||
import 'package:twonly/core/user_config.dart' show PasswordlessRecoveryConfig;
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/constants/keyvalue.keys.dart';
|
||||
import 'package:twonly/src/database/daos/contacts.dao.dart'
|
||||
show getContactDisplayName;
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/model/json/onboarding_state.model.dart';
|
||||
import 'package:twonly/src/model/json/userdata.model.dart'
|
||||
show PasswordLessRecovery;
|
||||
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart'
|
||||
as server;
|
||||
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'
|
||||
as pb;
|
||||
import 'package:twonly/src/model/protobuf/client/generated/passwordless_recovery.pb.dart';
|
||||
|
|
@ -128,11 +128,11 @@ class PasswordlessRecoveryService {
|
|||
mac: secretBox.mac.bytes,
|
||||
);
|
||||
|
||||
final res = await apiService.submitRecoveryShare(
|
||||
await RustApi.submitRecoveryShare(
|
||||
notificationId: notificationId,
|
||||
encryptedMessage: envelope.writeToBuffer(),
|
||||
);
|
||||
return res.isSuccess;
|
||||
return true;
|
||||
} catch (e) {
|
||||
Log.error('Failed to submit recovery share', error: e);
|
||||
return false;
|
||||
|
|
@ -172,7 +172,7 @@ class PasswordlessRecoveryService {
|
|||
await twonlyDB.contactsDao.resetRecoveryDataForAllContacts();
|
||||
await UserService.update((u) => u.passwordLessRecovery = null);
|
||||
|
||||
final config = PasswordLessRecovery(threshold);
|
||||
final config = PasswordlessRecoveryConfig(threshold: threshold);
|
||||
final xchacha20 = Xchacha20.poly1305Aead();
|
||||
|
||||
// 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.
|
||||
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(
|
||||
OnboardingState state,
|
||||
) async {
|
||||
|
|
@ -554,17 +367,29 @@ class PasswordlessRecoveryService {
|
|||
return false;
|
||||
}
|
||||
|
||||
final alreadyReceivedIds = state.receivedShares
|
||||
.map((s) => Int64(s.messageId))
|
||||
.toList();
|
||||
|
||||
final response = await apiService.checkForPasswordlessNotification(
|
||||
notificationId: state.notificationId!,
|
||||
downloadAuthToken: state.downloadAuthToken!,
|
||||
alreadyReceivedIds: alreadyReceivedIds,
|
||||
final alreadyReceivedIds = Int64List.fromList(
|
||||
state.receivedShares.map((share) => share.messageId).toList(),
|
||||
);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
enum SetupProfile { standard, customized }
|
||||
|
|
@ -30,10 +30,12 @@ class SignalIdentityService {
|
|||
await UserService.update((user) {
|
||||
user.signalLastSignedPreKeyUpdated = now;
|
||||
});
|
||||
final res = await apiService.updateSignedPreKey(
|
||||
signedPreKey.id,
|
||||
signedPreKey.getKeyPair().publicKey.serialize(),
|
||||
signedPreKey.signature,
|
||||
final res = await rustApiResult(
|
||||
RustApi.updateSignedPreKey(
|
||||
id: signedPreKey.id,
|
||||
key: signedPreKey.getKeyPair().publicKey.serialize(),
|
||||
signature: signedPreKey.signature,
|
||||
),
|
||||
);
|
||||
if (res.isError) {
|
||||
Log.error('could not update the signed pre key: ${res.error}');
|
||||
|
|
@ -52,14 +54,16 @@ class SignalIdentityService {
|
|||
)) {
|
||||
final bundle = await RustSignal.generateBundle();
|
||||
|
||||
final pqcRes = await apiService.uploadPqcPreKeys(
|
||||
bundle.signedPreKeyId,
|
||||
bundle.signedPreKeyPublic,
|
||||
bundle.signedPreKeySignature,
|
||||
bundle.kyberPreKeyId,
|
||||
bundle.kyberPreKeyPublic,
|
||||
bundle.kyberPreKeySignature,
|
||||
[],
|
||||
final pqcRes = await rustApiResult(
|
||||
RustApi.uploadPqcPreKeys(
|
||||
eccSignedPrekeyId: bundle.signedPreKeyId,
|
||||
eccSignedPrekey: bundle.signedPreKeyPublic,
|
||||
eccSignedPrekeySignature: bundle.signedPreKeySignature,
|
||||
kyberSignedPrekeyId: bundle.kyberPreKeyId,
|
||||
kyberSignedPrekey: bundle.kyberPreKeyPublic,
|
||||
kyberSignedPrekeySignature: bundle.kyberPreKeySignature,
|
||||
prekeys: const [],
|
||||
),
|
||||
);
|
||||
|
||||
if (pqcRes.isError) {
|
||||
|
|
|
|||
|
|
@ -206,7 +206,10 @@ Future<Uint8List?> getPublicKeyFromContact(int contactId) 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) {
|
||||
Log.info('Got new session data from the server to re-sync the session');
|
||||
return processSignalUserData(userData);
|
||||
|
|
|
|||
|
|
@ -1,16 +1,14 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
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/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/secure_storage.dart';
|
||||
|
||||
class UserService {
|
||||
late UserData currentUser;
|
||||
late UserConfig currentUser;
|
||||
bool isUserCreated = false;
|
||||
static final Mutex _updateProtection = Mutex();
|
||||
|
||||
|
|
@ -18,43 +16,26 @@ class UserService {
|
|||
Stream<void> get onUserUpdated => _userDataUpdateController.stream;
|
||||
|
||||
Future<bool> tryInit() async {
|
||||
final user = await getUser();
|
||||
if (user == null) return false;
|
||||
userService.currentUser = user;
|
||||
userService.isUserCreated = true;
|
||||
return true;
|
||||
final config = await UserConfigApi.load();
|
||||
if (config == null) return false;
|
||||
_applyRustUserConfig(config, notify: false);
|
||||
return isUserCreated;
|
||||
}
|
||||
|
||||
static Future<UserData?> getUser() async {
|
||||
static Future<UserConfig?> getUser() async {
|
||||
try {
|
||||
// 1. Try to load from KeyValueStore (user.json)
|
||||
final userDataMap = await KeyValueStore.get('user');
|
||||
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;
|
||||
}
|
||||
final config = await UserConfigApi.load();
|
||||
if (config != null) return config;
|
||||
|
||||
// 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(
|
||||
key: 'userData',
|
||||
);
|
||||
|
||||
if (userDataJson != null) {
|
||||
final userData = UserData.fromJson(
|
||||
jsonDecode(userDataJson) as Map<String, dynamic>,
|
||||
);
|
||||
|
||||
// 3. Run migration
|
||||
await _migrateFromSecureStorage(userData);
|
||||
return userData;
|
||||
final migrated = await UserConfigApi.importJson(json: userDataJson);
|
||||
await _removeLegacySecureStorageUser();
|
||||
return migrated;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
|
@ -64,15 +45,7 @@ class UserService {
|
|||
}
|
||||
}
|
||||
|
||||
static Future<void> _migrateFromSecureStorage(UserData userData) 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');
|
||||
}
|
||||
|
||||
static Future<void> _removeLegacySecureStorageUser() async {
|
||||
try {
|
||||
await SecureStorage.instance.delete(key: 'userData');
|
||||
} catch (e) {
|
||||
|
|
@ -83,35 +56,51 @@ class UserService {
|
|||
}
|
||||
|
||||
static Future<void> update(
|
||||
void Function(UserData userData) updateUser,
|
||||
void Function(UserConfig userData) updateUser,
|
||||
) async {
|
||||
await _updateProtection.protect(() async {
|
||||
try {
|
||||
final user = await getUser();
|
||||
if (user == null) return;
|
||||
final config = await UserConfigApi.load();
|
||||
|
||||
if (config == null) {
|
||||
throw Exception('User Config is missing');
|
||||
}
|
||||
|
||||
final user = UserConfigApi.clone(config: config);
|
||||
if (user.defaultShowTime == 999999) {
|
||||
// This was the old version for infinity -> change it to null
|
||||
user.defaultShowTime = null;
|
||||
}
|
||||
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) {
|
||||
Log.error('Could not update the user: $e');
|
||||
}
|
||||
});
|
||||
|
||||
userService.triggerUserUpdate();
|
||||
}
|
||||
|
||||
static Future<void> save(UserData user) async {
|
||||
await KeyValueStore.put('user', user.toJson());
|
||||
try {
|
||||
await RustKeyManager.setUserId(userId: user.userId);
|
||||
} catch (e) {
|
||||
Log.error('Could not set userId in RustKeyManager during save: $e');
|
||||
}
|
||||
await userService.tryInit();
|
||||
static Future<void> save(UserConfig user) async {
|
||||
final normalized = await UserConfigApi.save(config: user);
|
||||
userService._applyRustUserConfig(normalized);
|
||||
}
|
||||
|
||||
static Future<void> handleRustUserConfigChanged(UserConfig config) async {
|
||||
userService._applyRustUserConfig(config);
|
||||
}
|
||||
|
||||
void _applyRustUserConfig(UserConfig config, {bool notify = true}) {
|
||||
currentUser = config;
|
||||
isUserCreated = true;
|
||||
if (notify) triggerUserUpdate();
|
||||
}
|
||||
|
||||
void triggerUserUpdate() {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
|
@ -9,7 +8,6 @@ import 'package:twonly/globals.dart';
|
|||
import 'package:twonly/locator.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/services/signal/identity.signal.dart';
|
||||
import 'package:twonly/src/services/user.service.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
|
||||
|
|
@ -19,8 +17,9 @@ class UserDiscoveryService {
|
|||
.getNewAnnouncementsWithoutData();
|
||||
|
||||
for (final announcedUser in announcedUsers) {
|
||||
final userdata = await apiService.getUserById(
|
||||
announcedUser.announcedUserId,
|
||||
final userdata = await rustApiProtobuf(
|
||||
RustApi.getUserById(userId: announcedUser.announcedUserId),
|
||||
decodeUserData,
|
||||
);
|
||||
if (userdata == null) continue;
|
||||
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) {
|
||||
final u = userService.currentUser;
|
||||
if (!c.accepted || c.blocked) return false;
|
||||
|
|
@ -79,20 +63,6 @@ class UserDiscoveryService {
|
|||
required int threshold,
|
||||
required bool sharePromotion,
|
||||
}) 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(
|
||||
(u) => u
|
||||
..isUserDiscoveryEnabled = true
|
||||
|
|
@ -120,14 +90,6 @@ class UserDiscoveryService {
|
|||
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(
|
||||
Contact contact,
|
||||
) {
|
||||
|
|
@ -135,89 +97,14 @@ class UserDiscoveryService {
|
|||
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(
|
||||
int contactId,
|
||||
bool exclude,
|
||||
) async {
|
||||
// Remove old versions from the user...
|
||||
await (twonlyDB.update(
|
||||
twonlyDB.userDiscoveryOwnPromotions,
|
||||
)..where((t) => t.contactId.equals(contactId))).write(
|
||||
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
|
||||
),
|
||||
),
|
||||
await FlutterUserDiscovery.changeExclusionForContact(
|
||||
callbackId: isolateCallbackId,
|
||||
contactId: contactId,
|
||||
exclude: exclude,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -226,35 +113,4 @@ class UserDiscoveryService {
|
|||
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',
|
||||
);
|
||||
}
|
||||
}());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ class _ChatAskAFriendEntryState extends State<ChatAskAFriendEntry> {
|
|||
_username = contact.displayName ?? contact.username;
|
||||
} else {
|
||||
// Fetch from API
|
||||
final userdata = await apiService.getUserById(userId);
|
||||
final userdata = await rustApiProtobuf(RustApi.getUserById(userId: userId), decodeUserData);
|
||||
if (userdata != null) {
|
||||
_username = utf8.decode(userdata.username);
|
||||
}
|
||||
|
|
@ -122,7 +122,7 @@ class _ChatAskAFriendEntryState extends State<ChatAskAFriendEntry> {
|
|||
});
|
||||
try {
|
||||
final userId = _data!.askAboutUserId.toInt();
|
||||
final userdata = await apiService.getUserById(userId);
|
||||
final userdata = await rustApiProtobuf(RustApi.getUserById(userId: userId), decodeUserData);
|
||||
if (userdata != null) {
|
||||
await twonlyDB.contactsDao.insertOnConflictUpdate(
|
||||
ContactsCompanion(
|
||||
|
|
|
|||
|
|
@ -117,8 +117,9 @@ class _ContactRowState extends State<_ContactRow> {
|
|||
});
|
||||
|
||||
try {
|
||||
final userdata = await apiService.getUserById(
|
||||
widget.contact.userId.toInt(),
|
||||
final userdata = await rustApiProtobuf(
|
||||
RustApi.getUserById(userId: widget.contact.userId.toInt()),
|
||||
decodeUserData,
|
||||
);
|
||||
if (userdata == null) return;
|
||||
|
||||
|
|
|
|||
|
|
@ -369,7 +369,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
'Calling downloadDone for media ID: ${currentMediaLocal.mediaFile.mediaId}',
|
||||
);
|
||||
unawaited(
|
||||
apiService.downloadDone(currentMediaLocal.mediaFile.downloadToken!),
|
||||
RustApi.downloadDone(token: currentMediaLocal.mediaFile.downloadToken!),
|
||||
);
|
||||
|
||||
if (currentMediaLocal.mediaFile.type == MediaType.video) {
|
||||
|
|
|
|||
|
|
@ -117,7 +117,10 @@ class _SearchUsernameView extends State<AddNewUserView> {
|
|||
_isLoading = true;
|
||||
});
|
||||
|
||||
final userdata = await apiService.getUserData(username);
|
||||
final userdata = await rustApiProtobuf(
|
||||
RustApi.getUserData(username: username),
|
||||
decodeUserData,
|
||||
);
|
||||
if (!mounted) return;
|
||||
|
||||
setState(() {
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ class FriendSuggestionsComp extends StatelessWidget {
|
|||
) async {
|
||||
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 (context.mounted) {
|
||||
|
|
|
|||
|
|
@ -123,7 +123,7 @@ class _ContactViewState extends State<ContactView> {
|
|||
Future<void> handleReportUser(Contact contact) async {
|
||||
final reason = await showReportDialog(context, contact);
|
||||
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 (res.isSuccess) {
|
||||
showSnackbar(
|
||||
|
|
@ -281,7 +281,7 @@ class _ContactViewState extends State<ContactView> {
|
|||
icon: FontAwesomeIcons.arrowsRotate,
|
||||
text: 'Update Connection to V2 (PQXDH)',
|
||||
onTap: () async {
|
||||
final userData = await apiService.getUserById(contact.userId);
|
||||
final userData = await rustApiProtobuf(RustApi.getUserById(userId: contact.userId), decodeUserData);
|
||||
if (userData != null) {
|
||||
await processSignalUserData(userData);
|
||||
final updatedContact = await twonlyDB.contactsDao
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ class MemoriesViewState extends State<MemoriesView>
|
|||
}
|
||||
|
||||
Future<void> _checkUsage() async {
|
||||
final usage = await apiService.getMemoriesUsage();
|
||||
final usage = await rustApiProtobuf(RustApi.getMemoriesUsage(), decodeMemoriesUsage);
|
||||
if (usage != null &&
|
||||
usage.maxBytes > 0 &&
|
||||
usage.currentBytes >= usage.maxBytes) {
|
||||
|
|
@ -320,7 +320,7 @@ class MemoriesViewState extends State<MemoriesView>
|
|||
if (item != null) {
|
||||
if (isCompletely) {
|
||||
item.mediaService.fullMediaRemoval();
|
||||
await apiService.deleteMemory(mediaId);
|
||||
await RustApi.deleteMemory(mediaId: mediaId);
|
||||
await twonlyDB.mediaFilesDao.deleteMediaFile(mediaId);
|
||||
} else {
|
||||
if (item.mediaService.storedPath.existsSync()) {
|
||||
|
|
|
|||
|
|
@ -177,7 +177,7 @@ class _SynchronizedImageViewerScreenState
|
|||
|
||||
if (deleteCompletely) {
|
||||
item.mediaService.fullMediaRemoval();
|
||||
await apiService.deleteMemory(mediaId);
|
||||
await RustApi.deleteMemory(mediaId: mediaId);
|
||||
await twonlyDB.mediaFilesDao.deleteMediaFile(mediaId);
|
||||
|
||||
widget.galleryItems.removeAt(_currentIndex);
|
||||
|
|
|
|||
|
|
@ -15,9 +15,6 @@ import 'package:twonly/core/bridge/wrapper.dart' show RustUtils;
|
|||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/constants/keyvalue.keys.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/services/backup.service.dart';
|
||||
import 'package:twonly/src/services/passwordless_recovery.service.dart';
|
||||
|
|
@ -262,10 +259,13 @@ class _RecoverPasswordlessState extends State<RecoverPasswordless> {
|
|||
);
|
||||
|
||||
// Fetch serverKey
|
||||
final res = await apiService.getServerKeyForPasswordlessRecovery(
|
||||
userId: userId,
|
||||
pinUnlockToken: reconstructed.pinUnlockToken,
|
||||
pinProtectionKey: await pinKey.extractBytes(),
|
||||
final res = await rustApiResult(
|
||||
RustApi.getServerKeyForPasswordlessRecovery(
|
||||
userId: userId,
|
||||
serverKeyProtection: const [],
|
||||
pinUnlockToken: Uint8List.fromList(reconstructed.pinUnlockToken),
|
||||
pinProtectionKey: Uint8List.fromList(await pinKey.extractBytes()),
|
||||
),
|
||||
);
|
||||
|
||||
if (res.isError) {
|
||||
|
|
@ -281,8 +281,7 @@ class _RecoverPasswordlessState extends State<RecoverPasswordless> {
|
|||
return;
|
||||
}
|
||||
|
||||
final ok = res.value as server.Response_Ok;
|
||||
serverKey = Uint8List.fromList(ok.passwordlessRecoveryServerKey);
|
||||
serverKey = Uint8List.fromList(res.value!);
|
||||
} else if (reconstructed.hasEmailHint()) {
|
||||
final state = _onboardingState;
|
||||
if (state == null) return;
|
||||
|
|
@ -302,10 +301,12 @@ class _RecoverPasswordlessState extends State<RecoverPasswordless> {
|
|||
}
|
||||
|
||||
// Fetch serverKey (sends recovery email)
|
||||
final res = await apiService.getServerKeyForPasswordlessRecovery(
|
||||
userId: userId,
|
||||
email: email,
|
||||
serverKeyProtection: reconstructed.serverKeyProtection,
|
||||
final res = await rustApiResult(
|
||||
RustApi.getServerKeyForPasswordlessRecovery(
|
||||
userId: userId,
|
||||
email: email,
|
||||
serverKeyProtection: reconstructed.serverKeyProtection,
|
||||
),
|
||||
);
|
||||
|
||||
if (res.isError) {
|
||||
|
|
@ -444,11 +445,13 @@ class _RecoverPasswordlessState extends State<RecoverPasswordless> {
|
|||
Future<void> _registerPasswordlessNotification(OnboardingState state) async {
|
||||
final fcmToken = await FirebaseMessaging.instance.getToken();
|
||||
if (!mounted) return;
|
||||
final res = await apiService.registerPasswordlessNotification(
|
||||
notificationId: state.notificationId!,
|
||||
downloadAuthToken: state.downloadAuthToken!,
|
||||
langCode: Localizations.localeOf(context).languageCode,
|
||||
googleFcm: fcmToken,
|
||||
final res = await rustApiResult(
|
||||
RustApi.registerPasswordlessNotification(
|
||||
notificationId: state.notificationId!,
|
||||
downloadAuthToken: state.downloadAuthToken!,
|
||||
langCode: Localizations.localeOf(context).languageCode,
|
||||
googleFcm: fcmToken,
|
||||
),
|
||||
);
|
||||
if (res.isSuccess) {
|
||||
state.serverRegistered = true;
|
||||
|
|
|
|||
|
|
@ -1,15 +1,16 @@
|
|||
// ignore_for_file: avoid_dynamic_calls
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:twonly/core/bridge/user_config.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/locator.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/signal/identity.signal.dart';
|
||||
import 'package:twonly/src/services/user.service.dart';
|
||||
|
|
@ -77,7 +78,12 @@ class _RegisterViewState extends State<RegisterView> {
|
|||
if (proofOfWork != null) {
|
||||
proof = await proofOfWork!;
|
||||
} 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) {
|
||||
setState(() {
|
||||
_registrationDisabled = registrationDisabled;
|
||||
|
|
@ -97,10 +103,17 @@ class _RegisterViewState extends State<RegisterView> {
|
|||
|
||||
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) {
|
||||
Log.info('Got user_id ${res.value} from server');
|
||||
userId = res.value.userid.toInt() as int;
|
||||
userId = res.value!;
|
||||
} else {
|
||||
proofOfWork = null;
|
||||
if (res.error == ErrorCode.RegistrationDisabled) {
|
||||
|
|
@ -120,7 +133,7 @@ class _RegisterViewState extends State<RegisterView> {
|
|||
setState(() {
|
||||
_usernameErrorText = errorCodeToText(
|
||||
context,
|
||||
res.error as ErrorCode,
|
||||
res.error!,
|
||||
);
|
||||
_isTryingToRegister = false;
|
||||
});
|
||||
|
|
@ -141,7 +154,7 @@ class _RegisterViewState extends State<RegisterView> {
|
|||
await showAlertDialog(
|
||||
context,
|
||||
'Oh no!',
|
||||
errorCodeToText(context, res.error as ErrorCode),
|
||||
errorCodeToText(context, res.error!),
|
||||
);
|
||||
}
|
||||
return;
|
||||
|
|
@ -151,11 +164,10 @@ class _RegisterViewState extends State<RegisterView> {
|
|||
_isTryingToRegister = false;
|
||||
});
|
||||
|
||||
final userData = UserData(
|
||||
final userData = await UserConfigApi.create(
|
||||
userId: userId,
|
||||
username: username,
|
||||
displayName: username,
|
||||
subscriptionPlan: 'Free',
|
||||
currentSetupPage: SetupPages.profile.name,
|
||||
appVersion: AppState.latestAppVersionId,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/core/user_config.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/utils/misc.dart';
|
||||
import 'package:twonly/src/visual/elements/my_button.element.dart';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
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';
|
||||
|
||||
class SafetyProfileCard extends StatelessWidget {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/core/user_config.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/utils/misc.dart';
|
||||
import 'package:twonly/src/visual/views/onboarding/setup/components/next_button.comp.dart';
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ class AccountView extends StatelessWidget {
|
|||
context.lang.settingsAccountDeleteModalBody,
|
||||
);
|
||||
if (ok) {
|
||||
final res = await apiService.deleteAccount();
|
||||
final res = await rustApiResult(RustApi.deleteAccount());
|
||||
if (res.isError) {
|
||||
if (!context.mounted) return;
|
||||
showSnackbar(
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ class _BackupViewState extends State<BackupView> {
|
|||
Future<void> _loadBackupStatus() async {
|
||||
setState(() => _isLoading = true);
|
||||
final status = await BackupService.getData();
|
||||
final memoriesUsage = await apiService.getMemoriesUsage();
|
||||
final memoriesUsage = await rustApiProtobuf(RustApi.getMemoriesUsage(), decodeMemoriesUsage);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_backupStatus = status;
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ Future<bool> promptAndDisableMemoriesBackup(BuildContext context) async {
|
|||
|
||||
if (confirmed == true) {
|
||||
try {
|
||||
await apiService.disableMemoriesBackup();
|
||||
await RustApi.disableMemoriesBackup();
|
||||
final allMedias = await (twonlyDB.select(
|
||||
twonlyDB.mediaFiles,
|
||||
)..where((t) => t.stored.equals(true))).get();
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ class _MemoriesBackupDetailViewState extends State<MemoriesBackupDetailView> {
|
|||
}
|
||||
|
||||
Future<void> _loadStats() async {
|
||||
final memoriesUsage = await apiService.getMemoriesUsage();
|
||||
final memoriesUsage = await rustApiProtobuf(RustApi.getMemoriesUsage(), decodeMemoriesUsage);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_memoriesUsage = memoriesUsage;
|
||||
|
|
|
|||
|
|
@ -335,7 +335,7 @@ class _StorageContentsViewState extends State<StorageContentsView> {
|
|||
try {
|
||||
if (deleteCompletely) {
|
||||
await twonlyDB.mediaFilesDao.deleteMediaFile(file.mediaId);
|
||||
unawaited(apiService.deleteMemory(file.mediaId));
|
||||
unawaited(RustApi.deleteMemory(mediaId: file.mediaId));
|
||||
MediaFileService(file).fullMediaRemoval();
|
||||
} else {
|
||||
MediaFileService(file).storedPath.deleteSync();
|
||||
|
|
@ -384,7 +384,7 @@ class _StorageContentsViewState extends State<StorageContentsView> {
|
|||
for (final file in selectedFiles) {
|
||||
if (deleteCompletely) {
|
||||
await twonlyDB.mediaFilesDao.deleteMediaFile(file.mediaId);
|
||||
unawaited(apiService.deleteMemory(file.mediaId));
|
||||
unawaited(RustApi.deleteMemory(mediaId: file.mediaId));
|
||||
MediaFileService(file).fullMediaRemoval();
|
||||
} else {
|
||||
MediaFileService(file).storedPath.deleteSync();
|
||||
|
|
|
|||
|
|
@ -7,10 +7,7 @@ import 'package:hashlib/random.dart';
|
|||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'
|
||||
as pb;
|
||||
import 'package:twonly/src/services/api/messages.api.dart';
|
||||
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
|
||||
import 'package:twonly/src/visual/components/alert.dialog.dart';
|
||||
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
|
||||
|
||||
|
|
@ -186,24 +183,7 @@ class _RetransmissionDataViewState extends State<RetransmissionDataView> {
|
|||
Text(
|
||||
'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(
|
||||
'Retry: ${retrans.receipt.retryCount} : ${retrans.receipt.lastRetry}',
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,14 +1,9 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.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: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/pushkeys.notifications.dart';
|
||||
import 'package:twonly/src/utils/misc.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 setupNotificationWithUsers(force: true);
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
if (userService.currentUser.fcmToken == null) {
|
||||
|
|
@ -66,18 +59,6 @@ class _NotificationViewState extends State<NotificationView> {
|
|||
);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
|||
import 'package:go_router/go_router.dart';
|
||||
import 'package:twonly/locator.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/utils/misc.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);
|
||||
}
|
||||
|
||||
final result = await apiService.changeUsername(filteredUsername);
|
||||
final result = await rustApiResult(
|
||||
RustApi.changeUsername(username: filteredUsername),
|
||||
);
|
||||
if (result.isError) {
|
||||
if (!mounted) return;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,10 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:fixnum/fixnum.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:twonly/locator.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/providers/purchases.provider.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 {
|
||||
if (force) {
|
||||
ballance = await apiService.loadPlanBalance();
|
||||
ballance = await rustApiProtobuf(
|
||||
RustApi.loadPlanBalance(),
|
||||
decodePlanBalance,
|
||||
);
|
||||
_unusedAdditionalAccounts =
|
||||
_planLimit - (ballance?.additionalAccounts.length ?? _planLimit);
|
||||
}
|
||||
|
|
@ -72,7 +73,9 @@ class _AdditionalUsersViewState extends State<AdditionalUsersView> {
|
|||
as List<int>?;
|
||||
if (selectedUserIds == null) return;
|
||||
for (final selectedUserId in selectedUserIds) {
|
||||
final res = await apiService.addAdditionalUser(Int64(selectedUserId));
|
||||
final res = await rustApiResult(
|
||||
RustApi.addAdditionalUser(userId: selectedUserId),
|
||||
);
|
||||
if (res.isError && mounted) {
|
||||
final contact = await twonlyDB.contactsDao.getContactById(
|
||||
selectedUserId,
|
||||
|
|
@ -220,8 +223,10 @@ class _AdditionalAccountState extends State<AdditionalAccount> {
|
|||
context.lang.additionalUserRemoveDesc,
|
||||
);
|
||||
if (remove) {
|
||||
final res = await apiService.removeAdditionalUser(
|
||||
widget.account.userId,
|
||||
final res = await rustApiResult(
|
||||
RustApi.removeAdditionalUser(
|
||||
userId: widget.account.userId.toInt(),
|
||||
),
|
||||
);
|
||||
if (!context.mounted) return;
|
||||
if (res.isSuccess) {
|
||||
|
|
@ -231,7 +236,7 @@ class _AdditionalAccountState extends State<AdditionalAccount> {
|
|||
context,
|
||||
errorCodeToText(
|
||||
context,
|
||||
res.error as ErrorCode,
|
||||
res.error!,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
|
@ -35,7 +36,10 @@ class _SubscriptionViewState extends State<SubscriptionView> {
|
|||
}
|
||||
|
||||
Future<void> initAsync() async {
|
||||
ballance = await apiService.loadPlanBalance();
|
||||
ballance = await rustApiProtobuf(
|
||||
RustApi.loadPlanBalance(),
|
||||
decodePlanBalance,
|
||||
);
|
||||
if (ballance != null && ballance!.hasAdditionalAccountOwnerId()) {
|
||||
final ownerId = ballance!.additionalAccountOwnerId.toInt();
|
||||
final contact = await twonlyDB.contactsDao
|
||||
|
|
@ -49,7 +53,7 @@ class _SubscriptionViewState extends State<SubscriptionView> {
|
|||
}
|
||||
if (!mounted) return;
|
||||
setState(() {});
|
||||
await apiService.forceIpaCheck();
|
||||
await RustApi.forceIpaCheck();
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
|||
|
|
@ -866,7 +866,7 @@ packages:
|
|||
source: hosted
|
||||
version: "4.0.0"
|
||||
freezed_annotation:
|
||||
dependency: "direct main"
|
||||
dependency: transitive
|
||||
description:
|
||||
name: freezed_annotation
|
||||
sha256: "7294967ff0a6d98638e7acb774aac3af2550777accd8149c90af5b014e6d44d8"
|
||||
|
|
|
|||
|
|
@ -258,4 +258,3 @@ flutter:
|
|||
fonts:
|
||||
- asset: assets/fonts/NotoColorEmoji.ttf
|
||||
|
||||
|
||||
|
|
|
|||
2
rust/Cargo.lock
generated
2
rust/Cargo.lock
generated
|
|
@ -97,6 +97,7 @@ dependencies = [
|
|||
"anyhow",
|
||||
"atomic",
|
||||
"backtrace",
|
||||
"chrono",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1093,6 +1094,7 @@ dependencies = [
|
|||
"build-target",
|
||||
"bytemuck",
|
||||
"byteorder",
|
||||
"chrono",
|
||||
"console_error_panic_hook",
|
||||
"dart-sys",
|
||||
"delegate-attr",
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ edition = "2021"
|
|||
crate-type = ["cdylib", "staticlib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
flutter_rust_bridge = "=2.12.0"
|
||||
flutter_rust_bridge = { version = "=2.12.0", features = ["chrono"] }
|
||||
thiserror = "2.0.18"
|
||||
sqlx = { version = "0.9.0-alpha.1", default-features = false, features = [
|
||||
"runtime-tokio",
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ fn main() -> Result<()> {
|
|||
.include_file("websocket_protocol.rs")
|
||||
.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()
|
||||
.include_file("client_messages.rs")
|
||||
.compile_protos(
|
||||
|
|
|
|||
|
|
@ -91,11 +91,10 @@ async fn verify_shared_contacts(
|
|||
.await?;
|
||||
|
||||
let verified_at = chrono::Utc::now().timestamp_millis();
|
||||
|
||||
ctx.get_user_discovery()
|
||||
.get()
|
||||
.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?;
|
||||
|
||||
tracing::info!("verified a contact from shared additional data");
|
||||
|
|
|
|||
|
|
@ -17,8 +17,68 @@ use crate::error::{Result, TwonlyError};
|
|||
use crate::user_config::UserConfig;
|
||||
use encrypted_content::contact_request::Type;
|
||||
use sqlx::{Sqlite, Transaction};
|
||||
use std::collections::HashMap;
|
||||
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(
|
||||
ctx: &Arc<Context>,
|
||||
|
|
@ -73,6 +133,7 @@ pub(crate) async fn handle_contact_request(
|
|||
)));
|
||||
}
|
||||
};
|
||||
|
||||
let username = user
|
||||
.username
|
||||
.ok_or_else(|| TwonlyError::Generic("user response has no username".into()))?;
|
||||
|
|
@ -149,8 +210,8 @@ pub(crate) async fn handle_contact_update(
|
|||
EncryptedContent {
|
||||
contact_update: Some(encrypted_content::ContactUpdate {
|
||||
r#type: encrypted_content::contact_update::Type::Update as i32,
|
||||
username: user.username,
|
||||
display_name: user.display_name,
|
||||
username: Some(user.username),
|
||||
display_name: Some(user.display_name),
|
||||
avatar_svg_compressed,
|
||||
}),
|
||||
sender_profile_counter: Some(user.avatar_counter),
|
||||
|
|
|
|||
|
|
@ -4,19 +4,25 @@
|
|||
*/
|
||||
|
||||
use crate::api::proto::client::encrypted_content;
|
||||
use crate::context::Context;
|
||||
use crate::database::app::tables::UpdateContact;
|
||||
use crate::error::Result;
|
||||
use crate::services::groups::GroupService;
|
||||
use encrypted_content::error_messages::Type;
|
||||
use sqlx::{Sqlite, Transaction};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub(crate) async fn handle_error_message(
|
||||
transaction: &mut Transaction<'_, Sqlite>,
|
||||
ctx: &Arc<Context>,
|
||||
t: &mut Transaction<'_, Sqlite>,
|
||||
from_user_id: i64,
|
||||
_receipt_id: &str,
|
||||
group_id: Option<&str>,
|
||||
error: encrypted_content::ErrorMessages,
|
||||
) -> 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 => {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
|
|
@ -27,30 +33,27 @@ pub(crate) async fn handle_error_message(
|
|||
error.related_receipt_id,
|
||||
from_user_id,
|
||||
)
|
||||
.execute(&mut **transaction)
|
||||
.execute(&mut **t)
|
||||
.await?;
|
||||
|
||||
UpdateContact::builder()
|
||||
.user_id(from_user_id)
|
||||
.accepted(false)
|
||||
.requested(true)
|
||||
.build()
|
||||
.update(transaction)
|
||||
.update(t)
|
||||
.await?;
|
||||
}
|
||||
Type::GroupNotFoundOrNotAMember => {
|
||||
if let Some(group_id) = group_id {
|
||||
if let Ok(callbacks) = crate::bridge::callbacks::get_callbacks() {
|
||||
let group_id = group_id.to_owned();
|
||||
let related_receipt_id = error.related_receipt_id.clone();
|
||||
tokio::spawn(async move {
|
||||
(callbacks.api.group_membership_error)(
|
||||
from_user_id,
|
||||
group_id,
|
||||
related_receipt_id,
|
||||
)
|
||||
.await;
|
||||
});
|
||||
}
|
||||
GroupService::new(ctx)
|
||||
.handle_membership_error(
|
||||
t,
|
||||
from_user_id,
|
||||
group_id.to_owned(),
|
||||
error.related_receipt_id,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
Type::SessionOutOfSync | Type::UnknownMessageType => {}
|
||||
|
|
|
|||
|
|
@ -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::context::Context;
|
||||
use crate::database::app::tables::{Contact, Group};
|
||||
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 sqlx::{Sqlite, Transaction};
|
||||
|
||||
pub(crate) async fn ensure_group_member(
|
||||
transaction: &mut Transaction<'_, Sqlite>,
|
||||
t: &mut Transaction<'_, Sqlite>,
|
||||
from_user_id: i64,
|
||||
group_id: &str,
|
||||
) -> Result<()> {
|
||||
let allowed = sqlx::query_scalar!(
|
||||
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;
|
||||
let allowed = Group::is_member(t, group_id, from_user_id).await?;
|
||||
|
||||
if !allowed {
|
||||
return Err(TwonlyError::Generic(format!(
|
||||
"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(
|
||||
transaction: &mut Transaction<'_, Sqlite>,
|
||||
ctx: &std::sync::Arc<crate::context::Context>,
|
||||
t: &mut Transaction<'_, Sqlite>,
|
||||
from_user_id: i64,
|
||||
group_id: &str,
|
||||
create: encrypted_content::GroupCreate,
|
||||
) -> Result<()> {
|
||||
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 !contact_exists {
|
||||
return Err(TwonlyError::Generic(
|
||||
"only known contacts may create a group".into(),
|
||||
));
|
||||
}
|
||||
Contact::ensure_exists(t, from_user_id).await?;
|
||||
|
||||
let mut rng = rand::rngs::StdRng::from_os_rng();
|
||||
let identity = libsignal_protocol::IdentityKeyPair::generate(&mut rng);
|
||||
let private_key = identity.serialize().to_vec();
|
||||
let group_name = create.group_name.unwrap_or_default();
|
||||
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO groups(
|
||||
|
|
@ -83,8 +64,9 @@ pub(crate) async fn handle_group_create(
|
|||
private_key,
|
||||
group_name,
|
||||
)
|
||||
.execute(&mut **transaction)
|
||||
.execute(&mut **t)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
r#"
|
||||
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,
|
||||
create.group_public_key,
|
||||
)
|
||||
.execute(&mut **transaction)
|
||||
.execute(&mut **t)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
r#"INSERT INTO group_histories(group_history_id, group_id, contact_id, type)
|
||||
VALUES (?, ?, ?, 'addMember')"#,
|
||||
|
|
@ -105,42 +88,24 @@ pub(crate) async fn handle_group_create(
|
|||
group_id,
|
||||
from_user_id,
|
||||
)
|
||||
.execute(&mut **transaction)
|
||||
.execute(&mut **t)
|
||||
.await?;
|
||||
if let Ok(callbacks) = crate::bridge::callbacks::get_callbacks() {
|
||||
let group_id = group_id.to_owned();
|
||||
tokio::spawn(async move {
|
||||
(callbacks.api.group_state_refresh)(group_id, true).await;
|
||||
});
|
||||
}
|
||||
|
||||
GroupService::new(ctx)
|
||||
.refresh_group_state(t, group_id.to_owned(), true)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn handle_group_join(
|
||||
transaction: &mut Transaction<'_, Sqlite>,
|
||||
t: &mut Transaction<'_, Sqlite>,
|
||||
from_user_id: i64,
|
||||
group_id: &str,
|
||||
join: encrypted_content::GroupJoin,
|
||||
) -> Result<()> {
|
||||
let group_exists = sqlx::query_scalar!(
|
||||
r#"SELECT EXISTS(SELECT 1 FROM groups WHERE group_id = ?)"#,
|
||||
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(),
|
||||
));
|
||||
}
|
||||
Contact::ensure_exists(t, from_user_id).await?;
|
||||
Group::ensure_exists(t, group_id).await?;
|
||||
|
||||
sqlx::query!(
|
||||
r#"
|
||||
|
|
@ -153,13 +118,14 @@ pub(crate) async fn handle_group_join(
|
|||
from_user_id,
|
||||
join.group_public_key,
|
||||
)
|
||||
.execute(&mut **transaction)
|
||||
.execute(&mut **t)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn handle_resend_group_public_key(
|
||||
transaction: &mut Transaction<'_, Sqlite>,
|
||||
t: &mut Transaction<'_, Sqlite>,
|
||||
from_user_id: i64,
|
||||
group_id: &str,
|
||||
) -> Result<()> {
|
||||
|
|
@ -171,18 +137,19 @@ pub(crate) async fn handle_resend_group_public_key(
|
|||
"#,
|
||||
group_id,
|
||||
)
|
||||
.fetch_optional(&mut **transaction)
|
||||
.fetch_optional(&mut **t)
|
||||
.await?
|
||||
.flatten();
|
||||
|
||||
let Some(private_key) = private_key else {
|
||||
return Err(TwonlyError::Generic(format!(
|
||||
"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()))?;
|
||||
super::messages::queue_encrypted_content(
|
||||
transaction,
|
||||
|
||||
let identity = libsignal_protocol::IdentityKeyPair::try_from(private_key.as_slice())?;
|
||||
queue_encrypted_content(
|
||||
t,
|
||||
from_user_id,
|
||||
crate::api::proto::client::EncryptedContent {
|
||||
group_id: Some(group_id.to_owned()),
|
||||
|
|
@ -194,31 +161,25 @@ pub(crate) async fn handle_resend_group_public_key(
|
|||
true,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn handle_group_update(
|
||||
transaction: &mut Transaction<'_, Sqlite>,
|
||||
ctx: &Arc<Context>,
|
||||
t: &mut Transaction<'_, Sqlite>,
|
||||
from_user_id: i64,
|
||||
group_id: &str,
|
||||
update: encrypted_content::GroupUpdate,
|
||||
) -> Result<()> {
|
||||
let is_direct = sqlx::query_scalar!(
|
||||
"SELECT is_direct_chat FROM groups WHERE group_id = ?",
|
||||
group_id
|
||||
)
|
||||
.fetch_optional(&mut **transaction)
|
||||
.await?
|
||||
.unwrap_or(0)
|
||||
!= 0;
|
||||
let is_direct = Group::is_direct_chat(t, group_id).await?;
|
||||
|
||||
if !is_direct {
|
||||
if let Ok(callbacks) = crate::bridge::callbacks::get_callbacks() {
|
||||
let group_id = group_id.to_owned();
|
||||
tokio::spawn(async move {
|
||||
(callbacks.api.group_state_refresh)(group_id, false).await;
|
||||
});
|
||||
}
|
||||
GroupService::new(ctx)
|
||||
.refresh_group_state(t, group_id.to_owned(), false)
|
||||
.await;
|
||||
}
|
||||
|
||||
if update.group_action_type == "updatedGroupName" {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
|
|
@ -229,7 +190,7 @@ pub(crate) async fn handle_group_update(
|
|||
update.new_group_name,
|
||||
group_id,
|
||||
)
|
||||
.execute(&mut **transaction)
|
||||
.execute(&mut **t)
|
||||
.await?;
|
||||
} else if update.group_action_type == "changeDisplayMaxTime" && is_direct {
|
||||
sqlx::query!(
|
||||
|
|
@ -243,7 +204,7 @@ pub(crate) async fn handle_group_update(
|
|||
update.new_delete_messages_after_milliseconds,
|
||||
group_id,
|
||||
)
|
||||
.execute(&mut **transaction)
|
||||
.execute(&mut **t)
|
||||
.await?;
|
||||
}
|
||||
|
||||
|
|
@ -267,43 +228,66 @@ pub(crate) async fn handle_group_update(
|
|||
update.new_delete_messages_after_milliseconds,
|
||||
update.group_action_type,
|
||||
)
|
||||
.execute(&mut **transaction)
|
||||
.execute(&mut **t)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn handle_flame_sync(
|
||||
transaction: &mut Transaction<'_, Sqlite>,
|
||||
t: &mut Transaction<'_, Sqlite>,
|
||||
group_id: &str,
|
||||
flame: encrypted_content::FlameSync,
|
||||
) -> Result<()> {
|
||||
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!(
|
||||
r#"
|
||||
UPDATE groups
|
||||
SET also_best_friend = ?,
|
||||
flame_counter = CASE WHEN (
|
||||
(date(last_flame_counter_change, 'unixepoch', 'localtime') = date('now', 'localtime')
|
||||
AND date(?, 'unixepoch', 'localtime') = date('now', 'localtime'))
|
||||
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_counter = ?,
|
||||
max_flame_counter = ?
|
||||
WHERE group_id = ?
|
||||
"#,
|
||||
flame.best_friend,
|
||||
last_flame_counter_change,
|
||||
flame.force_update,
|
||||
flame.flame_counter,
|
||||
last_flame_counter_change,
|
||||
flame.force_update,
|
||||
flame.flame_counter,
|
||||
flame_counter,
|
||||
max_flame_counter,
|
||||
group_id,
|
||||
)
|
||||
.execute(&mut **transaction)
|
||||
.execute(&mut **t)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,13 +5,31 @@
|
|||
|
||||
use crate::api::proto::client::encrypted_content;
|
||||
use crate::bridge::callbacks::get_callbacks;
|
||||
use crate::context::Context;
|
||||
use crate::database::app::tables::Group;
|
||||
use crate::error::{Result, TwonlyError};
|
||||
use crate::services::mediafiles::MediaFileService;
|
||||
use crate::utils::{milliseconds_to_seconds, new_uuid_v4};
|
||||
use encrypted_content::media::Type as MediaType;
|
||||
use encrypted_content::media_update::Type as MediaUpdateType;
|
||||
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) {
|
||||
if let Ok(callbacks) = get_callbacks() {
|
||||
tokio::spawn(async move {
|
||||
|
|
@ -65,7 +83,7 @@ pub(crate) async fn handle_media(
|
|||
.execute(&mut **t)
|
||||
.await?;
|
||||
|
||||
spawn_media_action("download", media_id, from_user_id, media.sender_message_id);
|
||||
spawn_media_download(media_id);
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
|
@ -122,7 +140,7 @@ pub(crate) async fn handle_media(
|
|||
.execute(&mut **t)
|
||||
.await?;
|
||||
|
||||
spawn_media_action("download", media_id, from_user_id, media.sender_message_id);
|
||||
spawn_media_download(media_id);
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
|
@ -178,7 +196,9 @@ pub(crate) async fn handle_media(
|
|||
)
|
||||
.execute(&mut **t)
|
||||
.await?;
|
||||
|
||||
Group::increase_last_message_exchange(t, group_id, timestamp).await?;
|
||||
|
||||
if let Ok(callbacks) = get_callbacks() {
|
||||
let group_id = group_id.to_owned();
|
||||
let timestamp = media.timestamp;
|
||||
|
|
@ -186,7 +206,9 @@ pub(crate) async fn handle_media(
|
|||
(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(())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,13 +4,12 @@
|
|||
*/
|
||||
|
||||
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::bridge::api::ServerResult;
|
||||
use crate::bridge::callbacks::get_callbacks;
|
||||
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::utils::new_uuid_v4;
|
||||
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>>> =
|
||||
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(
|
||||
transaction: &mut Transaction<'_, Sqlite>,
|
||||
target_user_id: i64,
|
||||
|
|
@ -103,7 +82,7 @@ pub(crate) async fn process_encrypted_or_queue_error(
|
|||
let Some(error_type) = error_type else {
|
||||
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 {
|
||||
group_id,
|
||||
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 push_data = if row.retry_count == 0 {
|
||||
native_push_data(
|
||||
ctx,
|
||||
row.contact_id,
|
||||
row.message_id.clone(),
|
||||
&message.encrypted_content.clone().unwrap_or_default(),
|
||||
message.r#type,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let push_data: Option<Vec<u8>> = None;
|
||||
|
||||
match message_type {
|
||||
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())
|
||||
.map_err(|error| TwonlyError::Generic(format!("invalid queued message: {error}")))?;
|
||||
message.receipt_id = receipt_id.to_owned();
|
||||
let push_data = if row.retry_count == 0 {
|
||||
native_push_data(
|
||||
ctx,
|
||||
row.contact_id,
|
||||
row.message_id,
|
||||
&message.encrypted_content.clone().unwrap_or_default(),
|
||||
message.r#type,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let push_data: Option<Vec<u8>> = None;
|
||||
|
||||
match proto::message::Type::try_from(message.r#type)
|
||||
.map_err(|_| TwonlyError::Generic("queued message has invalid type".into()))?
|
||||
|
|
@ -593,6 +550,8 @@ pub(crate) async fn handle_sender_delivery_receipt(
|
|||
)
|
||||
.execute(&mut **transaction)
|
||||
.await?;
|
||||
|
||||
MediaFile::handle_response_from_receiver(transaction, &message_id).await?;
|
||||
}
|
||||
sqlx::query!(
|
||||
r#"
|
||||
|
|
@ -635,144 +594,3 @@ pub async fn handle_plaintext_content(
|
|||
}
|
||||
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(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,21 +9,17 @@ use crate::context::Context;
|
|||
use crate::database::app::tables::{Contact, Group, Receipt};
|
||||
use crate::error::{Result, TwonlyError};
|
||||
use sqlx::{Sqlite, Transaction};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex, OnceLock};
|
||||
use std::sync::Arc;
|
||||
use typing_indicator::handle_typing_indicator;
|
||||
|
||||
static REQUESTED_PROFILES: OnceLock<Mutex<HashMap<i64, i64>>> = OnceLock::new();
|
||||
|
||||
mod additional_data;
|
||||
pub(crate) mod contact;
|
||||
mod errors;
|
||||
mod groups;
|
||||
mod media;
|
||||
pub mod messages;
|
||||
mod pushkeys;
|
||||
mod reaction;
|
||||
mod recovery;
|
||||
pub(crate) mod recovery;
|
||||
mod text_message;
|
||||
mod typing_indicator;
|
||||
mod user_discovery;
|
||||
|
|
@ -34,64 +30,21 @@ mod verification;
|
|||
/// this dispatcher.
|
||||
pub(crate) async fn handle_encrypted(
|
||||
ctx: &Arc<Context>,
|
||||
tr: &mut Transaction<'_, Sqlite>,
|
||||
t: &mut Transaction<'_, Sqlite>,
|
||||
from_user_id: i64,
|
||||
receipt_id: &str,
|
||||
content: proto::EncryptedContent,
|
||||
) -> 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() {
|
||||
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 {
|
||||
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?;
|
||||
}
|
||||
}
|
||||
}
|
||||
contact::check_for_profile_update(t, from_user_id, &content).await?;
|
||||
|
||||
if content.ask_for_friend_promotions == Some(true) {
|
||||
Contact::update_ask_for_friend_promotions(tr, from_user_id).await?;
|
||||
Contact::update_ask_for_friend_promotions(t, from_user_id).await?;
|
||||
}
|
||||
|
||||
let type_kind = content_type_kind(&content);
|
||||
|
|
@ -100,13 +53,13 @@ pub(crate) async fn handle_encrypted(
|
|||
tracing::info!("Handling incoming message: {type_kind}");
|
||||
|
||||
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 {
|
||||
return contact::handle_contact_update(
|
||||
ctx,
|
||||
tr,
|
||||
t,
|
||||
from_user_id,
|
||||
content.sender_profile_counter,
|
||||
update,
|
||||
|
|
@ -115,46 +68,42 @@ pub(crate) async fn handle_encrypted(
|
|||
}
|
||||
|
||||
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 {
|
||||
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 {
|
||||
return errors::handle_error_message(
|
||||
tr,
|
||||
ctx,
|
||||
t,
|
||||
from_user_id,
|
||||
receipt_id,
|
||||
content.group_id.as_deref(),
|
||||
error,
|
||||
)
|
||||
.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 {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
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
|
||||
|
|
@ -162,11 +111,11 @@ pub(crate) async fn handle_encrypted(
|
|||
.ok_or_else(|| TwonlyError::Generic("group-scoped message has no group ID".into()))?;
|
||||
|
||||
if let Some(create) = content.group_create {
|
||||
return groups::handle_group_create(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 {
|
||||
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!(
|
||||
|
|
@ -174,7 +123,7 @@ pub(crate) async fn handle_encrypted(
|
|||
group_id,
|
||||
from_user_id,
|
||||
)
|
||||
.fetch_one(&mut **tr)
|
||||
.fetch_one(&mut **t)
|
||||
.await?
|
||||
!= 0;
|
||||
|
||||
|
|
@ -182,21 +131,21 @@ pub(crate) async fn handle_encrypted(
|
|||
let local_user_id = ctx.user_id().await?;
|
||||
|
||||
if Group::direct_chat_id(local_user_id, from_user_id) == group_id {
|
||||
let contact = Contact::get_contact_by_id(tr, from_user_id).await?;
|
||||
let contact = Contact::get_contact_by_id(t, from_user_id).await?;
|
||||
|
||||
if let Some(contact) =
|
||||
contact.filter(|value| value.accepted != 0 && value.deleted_by_user == 0)
|
||||
{
|
||||
Group::create_direct_chat(ctx, tr, contact).await?;
|
||||
Group::create_direct_chat(ctx, t, contact).await?;
|
||||
} else {
|
||||
sqlx::query!(
|
||||
"UPDATE contacts SET requested = 1, deleted_by_user = 0 WHERE user_id = ?",
|
||||
from_user_id,
|
||||
)
|
||||
.execute(&mut **tr)
|
||||
.execute(&mut **t)
|
||||
.await?;
|
||||
messages::queue_encrypted_content(
|
||||
tr,
|
||||
t,
|
||||
from_user_id,
|
||||
proto::EncryptedContent {
|
||||
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() {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
return additional_data::handle_additional_data_message(
|
||||
ctx,
|
||||
tr,
|
||||
t,
|
||||
from_user_id,
|
||||
&group_id,
|
||||
message,
|
||||
|
|
@ -243,15 +192,15 @@ pub(crate) async fn handle_encrypted(
|
|||
}
|
||||
|
||||
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 {
|
||||
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 {
|
||||
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!(
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,8 +4,7 @@
|
|||
*/
|
||||
|
||||
use crate::api::proto::client::encrypted_content;
|
||||
use crate::bridge::callbacks::get_callbacks;
|
||||
use crate::database::app::tables::Group;
|
||||
use crate::database::app::tables::{Group, MediaFile, Receipt};
|
||||
use crate::error::Result;
|
||||
use sqlx::{Sqlite, Transaction};
|
||||
|
||||
|
|
@ -16,29 +15,20 @@ pub(crate) async fn handle_reaction(
|
|||
reaction: encrypted_content::Reaction,
|
||||
) -> Result<()> {
|
||||
if reaction.remove {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
DELETE FROM reactions
|
||||
WHERE message_id = ? AND sender_id = ? AND emoji = ?
|
||||
"#,
|
||||
reaction.target_message_id,
|
||||
Receipt::delete_reaction(
|
||||
t,
|
||||
&reaction.target_message_id,
|
||||
from_user_id,
|
||||
reaction.emoji,
|
||||
&reaction.emoji,
|
||||
)
|
||||
.execute(&mut **t)
|
||||
.await?;
|
||||
} else {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO reactions(message_id, emoji, sender_id)
|
||||
VALUES (?, ?, ?)
|
||||
ON CONFLICT(message_id, sender_id, emoji) DO NOTHING
|
||||
"#,
|
||||
reaction.target_message_id,
|
||||
reaction.emoji,
|
||||
Receipt::insert_reaction(
|
||||
t,
|
||||
&reaction.target_message_id,
|
||||
from_user_id,
|
||||
&reaction.emoji,
|
||||
)
|
||||
.execute(&mut **t)
|
||||
.await?;
|
||||
}
|
||||
|
||||
|
|
@ -46,18 +36,7 @@ pub(crate) async fn handle_reaction(
|
|||
Group::increase_last_message_exchange_to_now(t, group_id).await?;
|
||||
}
|
||||
|
||||
if let Ok(callbacks) = get_callbacks() {
|
||||
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;
|
||||
});
|
||||
}
|
||||
MediaFile::handle_response_from_receiver(t, &reaction.target_message_id).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,13 +3,21 @@
|
|||
*
|
||||
*/
|
||||
|
||||
use crate::api::messages::outgoing::send_c2c_message_to_contact;
|
||||
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 sqlx::{Sqlite, Transaction};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub(crate) async fn handle_passwordless_recovery(
|
||||
transaction: &mut Transaction<'_, Sqlite>,
|
||||
t: &mut Transaction<'_, Sqlite>,
|
||||
from_user_id: i64,
|
||||
recovery: encrypted_content::PasswordLessRecovery,
|
||||
) -> Result<()> {
|
||||
|
|
@ -24,7 +32,7 @@ pub(crate) async fn handle_passwordless_recovery(
|
|||
"#,
|
||||
from_user_id,
|
||||
)
|
||||
.execute(&mut **transaction)
|
||||
.execute(&mut **t)
|
||||
.await?;
|
||||
} else if let Some(share) = recovery.recovery_secret_share {
|
||||
sqlx::query!(
|
||||
|
|
@ -39,19 +47,137 @@ pub(crate) async fn handle_passwordless_recovery(
|
|||
recovery.threshold,
|
||||
from_user_id,
|
||||
)
|
||||
.execute(&mut **transaction)
|
||||
.execute(&mut **t)
|
||||
.await?;
|
||||
}
|
||||
if let Ok(callbacks) = crate::bridge::callbacks::get_callbacks() {
|
||||
#[cfg(not(test))]
|
||||
{
|
||||
let ctx = Context::get_static()?.clone();
|
||||
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(())
|
||||
}
|
||||
|
||||
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(
|
||||
transaction: &mut Transaction<'_, Sqlite>,
|
||||
t: &mut Transaction<'_, Sqlite>,
|
||||
from_user_id: i64,
|
||||
heartbeat: encrypted_content::PasswordLessRecoveryHeartbeat,
|
||||
) -> Result<()> {
|
||||
|
|
@ -63,15 +189,17 @@ pub(crate) async fn handle_passwordless_recovery_heartbeat(
|
|||
"#,
|
||||
from_user_id,
|
||||
)
|
||||
.fetch_optional(&mut **transaction)
|
||||
.fetch_optional(&mut **t)
|
||||
.await?
|
||||
.flatten();
|
||||
|
||||
let valid = share
|
||||
.as_deref()
|
||||
.is_some_and(|share| Sha256::digest(share).as_slice() == heartbeat.hash);
|
||||
|
||||
if share.is_none() {
|
||||
super::messages::queue_encrypted_content(
|
||||
transaction,
|
||||
t,
|
||||
from_user_id,
|
||||
crate::api::proto::client::EncryptedContent {
|
||||
passwordless_recovery: Some(encrypted_content::PasswordLessRecovery {
|
||||
|
|
@ -97,7 +225,153 @@ pub(crate) async fn handle_passwordless_recovery_heartbeat(
|
|||
valid,
|
||||
from_user_id,
|
||||
)
|
||||
.execute(&mut **transaction)
|
||||
.execute(&mut **t)
|
||||
.await?;
|
||||
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ use crate::api::proto::client::encrypted_content;
|
|||
use crate::database::app::tables::{Group, Message, MessageType, NewMessage};
|
||||
use crate::error::Result;
|
||||
use crate::utils::milliseconds_to_seconds;
|
||||
use encrypted_content::message_update::Type;
|
||||
use sqlx::{Sqlite, Transaction};
|
||||
|
||||
pub(crate) async fn handle_text_message(
|
||||
|
|
@ -36,3 +37,162 @@ pub(crate) async fn handle_text_message(
|
|||
|
||||
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(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,13 +9,10 @@ use crate::context::Context;
|
|||
use crate::database::app::tables::Contact;
|
||||
use crate::error::{twonly_error, Result, TwonlyError};
|
||||
use crate::user_config::UserConfig;
|
||||
use crate::user_discovery::UserDiscoveryVersion;
|
||||
use prost::Message;
|
||||
use sqlx::{Sqlite, Transaction};
|
||||
use std::collections::HashSet;
|
||||
use std::sync::LazyLock;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
static REQUESTED_UPDATES: LazyLock<Mutex<HashSet<i64>>> =
|
||||
LazyLock::new(|| Mutex::new(HashSet::new()));
|
||||
use std::sync::Arc;
|
||||
|
||||
pub(crate) async fn check_sender_version(
|
||||
ctx: &Context,
|
||||
|
|
@ -33,20 +30,30 @@ pub(crate) async fn check_sender_version(
|
|||
));
|
||||
}
|
||||
|
||||
let Some(current_version) = ctx
|
||||
.get_user_discovery()
|
||||
.get()
|
||||
.await
|
||||
.should_request_new_messages(from_user_id, &version)
|
||||
.await?
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
// The inbound message transaction owns the app database's sole connection.
|
||||
// Going through `UserDiscovery::should_request_new_messages` here would ask
|
||||
// the native store to acquire that same connection and deadlock until the
|
||||
// pool's 30-second acquire timeout expires.
|
||||
let received_version = UserDiscoveryVersion::decode(version.as_slice())?;
|
||||
let stored_version = sqlx::query_scalar!(
|
||||
"SELECT user_discovery_version FROM contacts WHERE user_id = ?",
|
||||
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(());
|
||||
}
|
||||
|
||||
let current_version = stored_version.encode_to_vec();
|
||||
|
||||
queue_encrypted_content(
|
||||
t,
|
||||
from_user_id,
|
||||
|
|
@ -62,8 +69,9 @@ pub(crate) async fn check_sender_version(
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn handle_user_discovery_request(
|
||||
ctx: &Context,
|
||||
ctx: &Arc<Context>,
|
||||
t: &mut Transaction<'_, Sqlite>,
|
||||
from_user_id: i64,
|
||||
request: encrypted_content::UserDiscoveryRequest,
|
||||
|
|
@ -82,7 +90,7 @@ pub(crate) async fn handle_user_discovery_request(
|
|||
.get_user_discovery()
|
||||
.get()
|
||||
.await
|
||||
.get_new_messages(from_user_id, &request.current_version)
|
||||
.get_new_messages(from_user_id, &request.current_version, t)
|
||||
.await?;
|
||||
if !messages.is_empty() {
|
||||
queue_encrypted_content(
|
||||
|
|
@ -98,23 +106,27 @@ pub(crate) async fn handle_user_discovery_request(
|
|||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn handle_user_discovery_update(
|
||||
ctx: &Context,
|
||||
ctx: &Arc<Context>,
|
||||
t: &mut Transaction<'_, Sqlite>,
|
||||
from_user_id: i64,
|
||||
update: encrypted_content::UserDiscoveryUpdate,
|
||||
) -> Result<()> {
|
||||
if !UserConfig::load_required_from(ctx)?.is_user_discovery_enabled {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if update.messages.iter().any(|message| message.is_empty()) {
|
||||
return Err(TwonlyError::Generic(
|
||||
"user-discovery update contains an empty message".into(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(ctx
|
||||
.get_user_discovery()
|
||||
.get()
|
||||
.await
|
||||
.handle_new_messages(from_user_id, None, update.messages)
|
||||
.handle_new_messages(from_user_id, None, update.messages, t)
|
||||
.await?)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -333,6 +333,8 @@ pub(crate) async fn handle_decoded_server_message(
|
|||
"messages",
|
||||
"groups",
|
||||
"contacts",
|
||||
"key_verifications",
|
||||
"user_discovery_own_promotions",
|
||||
]);
|
||||
|
||||
let ctx = ctx.clone();
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ use crate::user_config::UserConfig;
|
|||
use prost::Message as _;
|
||||
use std::sync::Arc;
|
||||
|
||||
async fn decorate_content(
|
||||
pub(crate) async fn decorate_content(
|
||||
ctx: &Context,
|
||||
contact_id: i64,
|
||||
content: &mut proto::EncryptedContent,
|
||||
|
|
@ -35,6 +35,7 @@ async fn decorate_content(
|
|||
}
|
||||
|
||||
if config.is_user_discovery_enabled && is_persisted_message {
|
||||
ctx.initialize_user_discovery_from_config().await?;
|
||||
let database = ctx.get_app_database().await;
|
||||
let allowed = sqlx::query_scalar!(
|
||||
r#"SELECT EXISTS(SELECT 1 FROM contacts WHERE user_id = ? AND accepted = 1
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ impl ApiAuthHandshaker {
|
|||
.ok_or_else(|| TwonlyError::Generic("User configuration not found".into()))?;
|
||||
let device_id = user.device_id;
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ use crate::error::Result;
|
|||
use std::collections::HashMap;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, LazyLock, Weak};
|
||||
use std::time::Duration;
|
||||
use stream_tungstenite::WebSocketClient;
|
||||
use tokio::sync::{broadcast, oneshot, Mutex, RwLock};
|
||||
|
||||
|
|
@ -90,6 +91,7 @@ impl ApiClient {
|
|||
};
|
||||
|
||||
let client = WebSocketClient::builder(host)
|
||||
.receive_timeout(Duration::from_secs(60))
|
||||
.handshaker(handshaker)
|
||||
.build();
|
||||
|
||||
|
|
@ -155,8 +157,11 @@ impl ApiClient {
|
|||
|
||||
pub async fn close(&self) {
|
||||
self.deliberately_closed.store(true, Ordering::Release);
|
||||
if let Some(_) = self.ws_client.lock().await.take() {
|
||||
// Drop client to disconnect
|
||||
let client = self.ws_client.lock().await.take();
|
||||
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.set_state(ApiConnectionState::Stopped).await;
|
||||
|
|
|
|||
|
|
@ -24,31 +24,8 @@ impl Server {
|
|||
.await
|
||||
}
|
||||
|
||||
pub async fn load_plan_balance(ctx: &Arc<Context>, use_cache: bool) -> Result<Vec<u8>> {
|
||||
let database = ctx.get_app_database().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 load_plan_balance(ctx: &Arc<Context>) -> Result<Vec<u8>> {
|
||||
Self::get_plan_balance(ctx).await
|
||||
}
|
||||
|
||||
pub async fn ipa_purchase(
|
||||
|
|
|
|||
|
|
@ -4,12 +4,15 @@
|
|||
*/
|
||||
|
||||
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;
|
||||
pub use crate::api::PqcPreKeyInput;
|
||||
use crate::api::Server;
|
||||
use crate::context::Context;
|
||||
use crate::error::Result;
|
||||
use crate::error::{Result, TwonlyError};
|
||||
use crate::frb_generated::StreamSink;
|
||||
use crate::services::contacts::ContactService;
|
||||
use crate::user_config::UserConfig;
|
||||
use flutter_rust_bridge::frb;
|
||||
use prost::Message;
|
||||
|
|
@ -20,49 +23,25 @@ pub enum ServerResult<T> {
|
|||
ErrorCode(i32),
|
||||
}
|
||||
|
||||
#[frb]
|
||||
pub enum ServerResultEmpty {
|
||||
Ok,
|
||||
ErrorCode(i32),
|
||||
}
|
||||
|
||||
#[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),
|
||||
}
|
||||
fn api_result<T>(result: ServerResult<T>) -> Result<T> {
|
||||
match result {
|
||||
ServerResult::Ok(value) => Ok(value),
|
||||
ServerResult::ErrorCode(code) => Err(TwonlyError::Api(code)),
|
||||
}
|
||||
}
|
||||
|
||||
impl ServerResult<Vec<u8>> {
|
||||
pub fn into_bridge(self) -> ServerResultVecU8 {
|
||||
match self {
|
||||
ServerResult::Ok(v) => ServerResultVecU8::Ok(v),
|
||||
ServerResult::ErrorCode(c) => ServerResultVecU8::ErrorCode(c),
|
||||
}
|
||||
}
|
||||
fn empty_api_response(bytes: Vec<u8>) -> Result<()> {
|
||||
api_result(decode_ok_value(bytes, |value| match value {
|
||||
ResponseOk::None(_) => Some(()),
|
||||
_ => None,
|
||||
})?)
|
||||
}
|
||||
|
||||
impl ServerResult<i64> {
|
||||
pub fn into_bridge(self) -> ServerResultI64 {
|
||||
match self {
|
||||
ServerResult::Ok(v) => ServerResultI64::Ok(v),
|
||||
ServerResult::ErrorCode(c) => ServerResultI64::ErrorCode(c),
|
||||
}
|
||||
}
|
||||
fn encoded_api_response<T: Message>(
|
||||
bytes: Vec<u8>,
|
||||
extract: impl FnOnce(ResponseOk) -> Option<T>,
|
||||
) -> Result<Vec<u8>> {
|
||||
api_result(decode_ok_value(bytes, extract)?).map(|value| value.encode_to_vec())
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
|
|
@ -133,7 +112,7 @@ pub struct PreparedOutgoingMessage {
|
|||
impl RustApi {
|
||||
pub async fn request_contact_by_username(username: String) -> Result<()> {
|
||||
let ctx = Context::get_static()?;
|
||||
crate::services::contacts::ContactService::new(ctx)
|
||||
ContactService::new(ctx)
|
||||
.request_by_username(username, false)
|
||||
.await
|
||||
}
|
||||
|
|
@ -206,122 +185,156 @@ impl RustApi {
|
|||
proof_of_work: i64,
|
||||
lang_code: String,
|
||||
is_ios: bool,
|
||||
) -> Result<ServerResultI64> {
|
||||
) -> Result<i64> {
|
||||
let ctx = Context::get_static()?;
|
||||
Server::register(ctx, username, proof_of_work, lang_code, is_ios)
|
||||
.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()?;
|
||||
Server::get_user_by_id(ctx, user_id)
|
||||
.await
|
||||
.map(|r| r.into_bridge())
|
||||
.and_then(api_result)
|
||||
.map(|value| value.encode_to_vec())
|
||||
}
|
||||
pub async fn check_for_deleted_usernames() -> Result<()> {
|
||||
let ctx = Context::get_static()?;
|
||||
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()?;
|
||||
Server::get_user_id_from_username(ctx, username)
|
||||
.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()?;
|
||||
Server::get_user_by_username(ctx, username)
|
||||
.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()?;
|
||||
Server::get_proof_of_work(ctx).await.map(|r| match r {
|
||||
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)
|
||||
Server::get_proof_of_work(ctx)
|
||||
.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()?;
|
||||
Server::set_login_token(ctx, token)
|
||||
.await
|
||||
.map(|r| r.into_bridge())
|
||||
.and_then(api_result)
|
||||
}
|
||||
pub async fn request_memories_upload(
|
||||
size: i64,
|
||||
original_date: i64,
|
||||
media_id: String,
|
||||
) -> Result<ServerResultVecU8> {
|
||||
) -> Result<Vec<u8>> {
|
||||
let ctx = Context::get_static()?;
|
||||
Server::request_memories_upload(ctx, size, original_date, media_id)
|
||||
.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()?;
|
||||
Server::get_memories_usage(ctx)
|
||||
.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()?;
|
||||
Server::get_memories_url(ctx, media_id, thumbnail)
|
||||
.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()?;
|
||||
Server::confirm_memories_upload(ctx, media_id)
|
||||
.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()?;
|
||||
Server::delete_memory(ctx, media_id)
|
||||
.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()?;
|
||||
Server::disable_memories_backup(ctx)
|
||||
.await
|
||||
.map(|r| r.into_bridge())
|
||||
.and_then(api_result)
|
||||
}
|
||||
pub async fn get_plan_balance() -> Result<Vec<u8>> {
|
||||
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()?;
|
||||
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()?;
|
||||
Server::remove_additional_user(ctx, user_id)
|
||||
.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()?;
|
||||
Server::add_additional_user(ctx, user_id)
|
||||
.await
|
||||
.map(|r| r.into_bridge())
|
||||
.and_then(api_result)
|
||||
}
|
||||
pub async fn register_passwordless_recovery(
|
||||
encrypted_server_key: Vec<u8>,
|
||||
pin_unlock_token: Option<Vec<u8>>,
|
||||
) -> Result<ServerResultEmpty> {
|
||||
) -> Result<()> {
|
||||
let ctx = Context::get_static()?;
|
||||
Server::register_passwordless_recovery(ctx, encrypted_server_key, pin_unlock_token)
|
||||
.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(
|
||||
user_id: i64,
|
||||
|
|
@ -329,7 +342,7 @@ impl RustApi {
|
|||
pin_unlock_token: Option<Vec<u8>>,
|
||||
pin_protection_key: Option<Vec<u8>>,
|
||||
email: Option<String>,
|
||||
) -> Result<ServerResultVecU8> {
|
||||
) -> Result<Vec<u8>> {
|
||||
let ctx = Context::get_static()?;
|
||||
Server::get_server_key_for_passwordless_recovery(
|
||||
ctx,
|
||||
|
|
@ -340,23 +353,23 @@ impl RustApi {
|
|||
email,
|
||||
)
|
||||
.await
|
||||
.map(|r| r.into_bridge())
|
||||
.and_then(api_result)
|
||||
}
|
||||
pub async fn submit_recovery_share(
|
||||
notification_id: String,
|
||||
encrypted_message: Vec<u8>,
|
||||
) -> Result<ServerResultEmpty> {
|
||||
) -> Result<()> {
|
||||
let ctx = Context::get_static()?;
|
||||
Server::submit_recovery_share(ctx, notification_id, encrypted_message)
|
||||
.await
|
||||
.map(|r| r.into_bridge())
|
||||
.and_then(api_result)
|
||||
}
|
||||
pub async fn register_passwordless_notification(
|
||||
notification_id: String,
|
||||
download_auth_token: Vec<u8>,
|
||||
lang_code: String,
|
||||
google_fcm: Option<String>,
|
||||
) -> Result<ServerResultEmpty> {
|
||||
) -> Result<()> {
|
||||
let ctx = Context::get_static()?;
|
||||
Server::register_passwordless_notification(
|
||||
ctx,
|
||||
|
|
@ -366,13 +379,13 @@ impl RustApi {
|
|||
google_fcm,
|
||||
)
|
||||
.await
|
||||
.map(|r| r.into_bridge())
|
||||
.and_then(api_result)
|
||||
}
|
||||
pub async fn check_for_passwordless_notification(
|
||||
notification_id: String,
|
||||
download_auth_token: Vec<u8>,
|
||||
already_received_message_ids: Vec<i64>,
|
||||
) -> Result<ServerResultVecU8> {
|
||||
) -> Result<Vec<u8>> {
|
||||
let ctx = Context::get_static()?;
|
||||
Server::check_for_passwordless_notification(
|
||||
ctx,
|
||||
|
|
@ -381,49 +394,46 @@ impl RustApi {
|
|||
already_received_message_ids,
|
||||
)
|
||||
.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()?;
|
||||
Server::report_user(ctx, user_id, reason)
|
||||
.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()?;
|
||||
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()?;
|
||||
Server::update_fcm_token(ctx, token)
|
||||
.await
|
||||
.map(|r| r.into_bridge())
|
||||
.and_then(api_result)
|
||||
}
|
||||
pub async fn ipa_purchase(
|
||||
product_id: String,
|
||||
source: String,
|
||||
verification_data: String,
|
||||
) -> Result<Vec<u8>> {
|
||||
) -> Result<()> {
|
||||
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()?;
|
||||
Server::change_username(ctx, username)
|
||||
.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()?;
|
||||
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(
|
||||
id: i64,
|
||||
key: Vec<u8>,
|
||||
signature: Vec<u8>,
|
||||
) -> Result<Vec<u8>> {
|
||||
pub async fn update_signed_pre_key(id: i64, key: Vec<u8>, signature: Vec<u8>) -> Result<()> {
|
||||
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)]
|
||||
pub async fn upload_pqc_pre_keys(
|
||||
|
|
@ -434,7 +444,7 @@ impl RustApi {
|
|||
kyber_signed_prekey: Vec<u8>,
|
||||
kyber_signed_prekey_signature: Vec<u8>,
|
||||
prekeys: Vec<PqcPreKeyInput>,
|
||||
) -> Result<Vec<u8>> {
|
||||
) -> Result<()> {
|
||||
let ctx = Context::get_static()?;
|
||||
Server::upload_pqc_pre_keys(
|
||||
ctx,
|
||||
|
|
@ -447,16 +457,17 @@ impl RustApi {
|
|||
prekeys,
|
||||
)
|
||||
.await
|
||||
.and_then(empty_api_response)
|
||||
}
|
||||
pub async fn send_text_message(
|
||||
user_id: i64,
|
||||
body: Vec<u8>,
|
||||
push_data: Option<Vec<u8>>,
|
||||
) -> Result<ServerResultEmpty> {
|
||||
) -> Result<()> {
|
||||
let ctx = Context::get_static()?;
|
||||
Server::send_text_message(ctx, user_id, body, push_data)
|
||||
.await
|
||||
.map(|r| r.into_bridge())
|
||||
.and_then(api_result)
|
||||
}
|
||||
|
||||
pub async fn send_encrypted_content(
|
||||
|
|
@ -587,48 +598,3 @@ impl RustApi {
|
|||
.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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,15 +54,11 @@ callback_generator! {
|
|||
},
|
||||
Api api {
|
||||
resync_signal_session: (i64) => (),
|
||||
push_key_requested: (i64) => (),
|
||||
group_membership_error: (i64, String, String) => (),
|
||||
media_action: (String, String, i64, String) => (),
|
||||
verification_proof: (i64, Vec<u8>) => (),
|
||||
create_push_data: (i64, Option<String>, Vec<u8>, i32) => Option<Vec<u8>>,
|
||||
create_push_avatars: (i64) => (),
|
||||
recovery_changed: () => (),
|
||||
media_received: (String, i64) => (),
|
||||
group_state_refresh: (String, bool) => ()
|
||||
user_config_changed: (crate::user_config::UserConfig) => ()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
pub mod api;
|
||||
pub mod callbacks;
|
||||
pub mod groups;
|
||||
pub mod user_config;
|
||||
pub mod wrapper;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
|
@ -20,13 +21,12 @@ use crate::error::TwonlyError;
|
|||
use crate::keys::KeyManager;
|
||||
use crate::secure_storage::SecureStorage;
|
||||
use crate::signal::engine::RustSignalEngine;
|
||||
use crate::user_discovery::stores::{NativeUserDiscoveryStore, NativeUserDiscoveryUtils};
|
||||
use crate::user_discovery::UserDiscovery;
|
||||
use crate::utils::Shared;
|
||||
use flutter_rust_bridge::frb;
|
||||
|
||||
pub use crate::user_discovery::traits::AnnouncedUser;
|
||||
pub use crate::user_discovery::traits::OtherPromotion;
|
||||
pub use crate::user_discovery::AnnouncedUser;
|
||||
pub use crate::user_discovery::OtherPromotion;
|
||||
use tokio::sync::{Mutex, OnceCell, RwLock};
|
||||
|
||||
pub struct InitConfig {
|
||||
|
|
@ -54,8 +54,7 @@ pub struct _AnnouncedUser {
|
|||
pub(crate) struct TwonlyFlutter {
|
||||
#[allow(dead_code)]
|
||||
pub(crate) config: InitConfig,
|
||||
pub(crate) user_discovery:
|
||||
Shared<UserDiscovery<NativeUserDiscoveryStore, NativeUserDiscoveryUtils>>,
|
||||
pub(crate) user_discovery: Shared<UserDiscovery>,
|
||||
#[allow(dead_code)]
|
||||
pub(crate) rust_db: Arc<RwLock<Arc<Database>>>,
|
||||
pub(crate) app_db: Arc<RwLock<Arc<AppDatabase>>>,
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue