mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-09-01 08:54:08 +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
|
|
@ -220,7 +220,11 @@ class _AppMainWidgetState extends State<AppMainWidget> {
|
||||||
} else {
|
} else {
|
||||||
// This means the user is in the onboarding screen, so start with the Proof of Work.
|
// This means the user is in the onboarding screen, so start with the Proof of Work.
|
||||||
|
|
||||||
final (proof, disabled) = await apiService.getProofOfWork();
|
final proofResult = await rustApiResult(RustApi.getProofOfWork());
|
||||||
|
final proof = proofResult.value == null
|
||||||
|
? null
|
||||||
|
: decodeProofOfWork(proofResult.value!);
|
||||||
|
final disabled = proofResult.error == ErrorCode.RegistrationDisabled;
|
||||||
if (proof != null) {
|
if (proof != null) {
|
||||||
Log.info('Starting with proof of work calculation.');
|
Log.info('Starting with proof of work calculation.');
|
||||||
_proofOfWork = (
|
_proofOfWork = (
|
||||||
|
|
|
||||||
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 '../api/server/prekeys.dart';
|
||||||
import '../frb_generated.dart';
|
import '../frb_generated.dart';
|
||||||
|
|
||||||
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
|
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart' hide protected;
|
|
||||||
part 'api.freezed.dart';
|
|
||||||
|
|
||||||
// These functions are ignored because they are not marked as `pub`: `from_rust_state`
|
// These functions are ignored because they are not marked as `pub`: `api_result`, `empty_api_response`, `encoded_api_response`, `from_rust_state`
|
||||||
// These types are ignored because they are neither used by any `pub` functions nor (for structs and enums) marked `#[frb(unignore)]`: `ApiConfig`, `ServerResult`
|
// These types are ignored because they are neither used by any `pub` functions nor (for structs and enums) marked `#[frb(unignore)]`: `ApiConfig`, `ServerResult`
|
||||||
// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `assert_fields_are_eq`, `assert_fields_are_eq`, `clone`, `clone`, `clone`, `clone`, `eq`, `eq`, `fmt`, `fmt`, `fmt`, `fmt`
|
// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `assert_fields_are_eq`, `assert_fields_are_eq`, `clone`, `clone`, `clone`, `clone`, `eq`, `eq`, `fmt`, `fmt`, `fmt`, `fmt`
|
||||||
// These functions are ignored (category: IgnoreBecauseOwnerTyShouldIgnore): `into_bridge`, `into_bridge`, `into_bridge`, `into_bridge`, `into_bridge`, `into_bridge`, `into_bridge`, `into_bridge`
|
|
||||||
|
|
||||||
enum ApiConnectionState {
|
enum ApiConnectionState {
|
||||||
stopped,
|
stopped,
|
||||||
|
|
@ -85,9 +81,8 @@ class PreparedOutgoingMessage {
|
||||||
class RustApi {
|
class RustApi {
|
||||||
const RustApi();
|
const RustApi();
|
||||||
|
|
||||||
static Future<ServerResultEmpty> addAdditionalUser({
|
static Future<void> addAdditionalUser({required PlatformInt64 userId}) =>
|
||||||
required PlatformInt64 userId,
|
RustLib.instance.api.crateBridgeApiRustApiAddAdditionalUser(
|
||||||
}) => RustLib.instance.api.crateBridgeApiRustApiAddAdditionalUser(
|
|
||||||
userId: userId,
|
userId: userId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -97,15 +92,15 @@ class RustApi {
|
||||||
static String apiBaseUrl({required String protocol}) =>
|
static String apiBaseUrl({required String protocol}) =>
|
||||||
RustLib.instance.api.crateBridgeApiRustApiApiBaseUrl(protocol: protocol);
|
RustLib.instance.api.crateBridgeApiRustApiApiBaseUrl(protocol: protocol);
|
||||||
|
|
||||||
static Future<ServerResultEmpty> changeUsername({required String username}) =>
|
static Future<void> changeUsername({required String username}) => RustLib
|
||||||
RustLib.instance.api.crateBridgeApiRustApiChangeUsername(
|
.instance
|
||||||
username: username,
|
.api
|
||||||
);
|
.crateBridgeApiRustApiChangeUsername(username: username);
|
||||||
|
|
||||||
static Future<void> checkForDeletedUsernames() =>
|
static Future<void> checkForDeletedUsernames() =>
|
||||||
RustLib.instance.api.crateBridgeApiRustApiCheckForDeletedUsernames();
|
RustLib.instance.api.crateBridgeApiRustApiCheckForDeletedUsernames();
|
||||||
|
|
||||||
static Future<ServerResultVecU8> checkForPasswordlessNotification({
|
static Future<Uint8List> checkForPasswordlessNotification({
|
||||||
required String notificationId,
|
required String notificationId,
|
||||||
required List<int> downloadAuthToken,
|
required List<int> downloadAuthToken,
|
||||||
required Int64List alreadyReceivedMessageIds,
|
required Int64List alreadyReceivedMessageIds,
|
||||||
|
|
@ -119,9 +114,8 @@ class RustApi {
|
||||||
static Future<void> close() =>
|
static Future<void> close() =>
|
||||||
RustLib.instance.api.crateBridgeApiRustApiClose();
|
RustLib.instance.api.crateBridgeApiRustApiClose();
|
||||||
|
|
||||||
static Future<ServerResultEmpty> confirmMemoriesUpload({
|
static Future<void> confirmMemoriesUpload({required String mediaId}) =>
|
||||||
required String mediaId,
|
RustLib.instance.api.crateBridgeApiRustApiConfirmMemoriesUpload(
|
||||||
}) => RustLib.instance.api.crateBridgeApiRustApiConfirmMemoriesUpload(
|
|
||||||
mediaId: mediaId,
|
mediaId: mediaId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -131,25 +125,31 @@ class RustApi {
|
||||||
static Future<ApiConnectionState> connectionState() =>
|
static Future<ApiConnectionState> connectionState() =>
|
||||||
RustLib.instance.api.crateBridgeApiRustApiConnectionState();
|
RustLib.instance.api.crateBridgeApiRustApiConnectionState();
|
||||||
|
|
||||||
static Future<ServerResultEmpty> deleteAccount() =>
|
static Future<void> deleteAccount() =>
|
||||||
RustLib.instance.api.crateBridgeApiRustApiDeleteAccount();
|
RustLib.instance.api.crateBridgeApiRustApiDeleteAccount();
|
||||||
|
|
||||||
static Future<ServerResultEmpty> deleteMemory({required String mediaId}) =>
|
static Future<void> deleteMemory({required String mediaId}) =>
|
||||||
RustLib.instance.api.crateBridgeApiRustApiDeleteMemory(mediaId: mediaId);
|
RustLib.instance.api.crateBridgeApiRustApiDeleteMemory(mediaId: mediaId);
|
||||||
|
|
||||||
static Future<ServerResultEmpty> disableMemoriesBackup() =>
|
static Future<void> disableMemoriesBackup() =>
|
||||||
RustLib.instance.api.crateBridgeApiRustApiDisableMemoriesBackup();
|
RustLib.instance.api.crateBridgeApiRustApiDisableMemoriesBackup();
|
||||||
|
|
||||||
static Future<ServerResultEmpty> downloadDone({required List<int> token}) =>
|
static Future<void> downloadDone({required List<int> token}) =>
|
||||||
RustLib.instance.api.crateBridgeApiRustApiDownloadDone(token: token);
|
RustLib.instance.api.crateBridgeApiRustApiDownloadDone(token: token);
|
||||||
|
|
||||||
|
static Future<void> downloadMedia({required String mediaId}) =>
|
||||||
|
RustLib.instance.api.crateBridgeApiRustApiDownloadMedia(mediaId: mediaId);
|
||||||
|
|
||||||
|
static Future<void> downloadPendingMedia() =>
|
||||||
|
RustLib.instance.api.crateBridgeApiRustApiDownloadPendingMedia();
|
||||||
|
|
||||||
static Stream<ApiEvent> events() =>
|
static Stream<ApiEvent> events() =>
|
||||||
RustLib.instance.api.crateBridgeApiRustApiEvents();
|
RustLib.instance.api.crateBridgeApiRustApiEvents();
|
||||||
|
|
||||||
static Future<ServerResultEmpty> forceIpaCheck() =>
|
static Future<void> forceIpaCheck() =>
|
||||||
RustLib.instance.api.crateBridgeApiRustApiForceIpaCheck();
|
RustLib.instance.api.crateBridgeApiRustApiForceIpaCheck();
|
||||||
|
|
||||||
static Future<ServerResultVecU8> getMemoriesUrl({
|
static Future<Uint8List> getMemoriesUrl({
|
||||||
required String mediaId,
|
required String mediaId,
|
||||||
required bool thumbnail,
|
required bool thumbnail,
|
||||||
}) => RustLib.instance.api.crateBridgeApiRustApiGetMemoriesUrl(
|
}) => RustLib.instance.api.crateBridgeApiRustApiGetMemoriesUrl(
|
||||||
|
|
@ -157,16 +157,16 @@ class RustApi {
|
||||||
thumbnail: thumbnail,
|
thumbnail: thumbnail,
|
||||||
);
|
);
|
||||||
|
|
||||||
static Future<ServerResultVecU8> getMemoriesUsage() =>
|
static Future<Uint8List> getMemoriesUsage() =>
|
||||||
RustLib.instance.api.crateBridgeApiRustApiGetMemoriesUsage();
|
RustLib.instance.api.crateBridgeApiRustApiGetMemoriesUsage();
|
||||||
|
|
||||||
static Future<Uint8List> getPlanBalance() =>
|
static Future<Uint8List> getPlanBalance() =>
|
||||||
RustLib.instance.api.crateBridgeApiRustApiGetPlanBalance();
|
RustLib.instance.api.crateBridgeApiRustApiGetPlanBalance();
|
||||||
|
|
||||||
static Future<ServerResultVecU8> getProofOfWork() =>
|
static Future<Uint8List> getProofOfWork() =>
|
||||||
RustLib.instance.api.crateBridgeApiRustApiGetProofOfWork();
|
RustLib.instance.api.crateBridgeApiRustApiGetProofOfWork();
|
||||||
|
|
||||||
static Future<ServerResultVecU8> getServerKeyForPasswordlessRecovery({
|
static Future<Uint8List> getServerKeyForPasswordlessRecovery({
|
||||||
required PlatformInt64 userId,
|
required PlatformInt64 userId,
|
||||||
required List<int> serverKeyProtection,
|
required List<int> serverKeyProtection,
|
||||||
Uint8List? pinUnlockToken,
|
Uint8List? pinUnlockToken,
|
||||||
|
|
@ -181,14 +181,13 @@ class RustApi {
|
||||||
email: email,
|
email: email,
|
||||||
);
|
);
|
||||||
|
|
||||||
static Future<ServerResultVecU8> getUserById({
|
static Future<Uint8List> getUserById({required PlatformInt64 userId}) =>
|
||||||
required PlatformInt64 userId,
|
RustLib.instance.api.crateBridgeApiRustApiGetUserById(userId: userId);
|
||||||
}) => RustLib.instance.api.crateBridgeApiRustApiGetUserById(userId: userId);
|
|
||||||
|
|
||||||
static Future<ServerResultVecU8> getUserData({required String username}) =>
|
static Future<Uint8List> getUserData({required String username}) =>
|
||||||
RustLib.instance.api.crateBridgeApiRustApiGetUserData(username: username);
|
RustLib.instance.api.crateBridgeApiRustApiGetUserData(username: username);
|
||||||
|
|
||||||
static Future<ServerResultI64> getUserIdFromUsername({
|
static Future<PlatformInt64> getUserIdFromUsername({
|
||||||
required String username,
|
required String username,
|
||||||
}) => RustLib.instance.api.crateBridgeApiRustApiGetUserIdFromUsername(
|
}) => RustLib.instance.api.crateBridgeApiRustApiGetUserIdFromUsername(
|
||||||
username: username,
|
username: username,
|
||||||
|
|
@ -230,7 +229,7 @@ class RustApi {
|
||||||
quoteMessageId: quoteMessageId,
|
quoteMessageId: quoteMessageId,
|
||||||
);
|
);
|
||||||
|
|
||||||
static Future<Uint8List> ipaPurchase({
|
static Future<void> ipaPurchase({
|
||||||
required String productId,
|
required String productId,
|
||||||
required String source,
|
required String source,
|
||||||
required String verificationData,
|
required String verificationData,
|
||||||
|
|
@ -240,10 +239,8 @@ class RustApi {
|
||||||
verificationData: verificationData,
|
verificationData: verificationData,
|
||||||
);
|
);
|
||||||
|
|
||||||
static Future<Uint8List> loadPlanBalance({required bool useCache}) => RustLib
|
static Future<Uint8List> loadPlanBalance() =>
|
||||||
.instance
|
RustLib.instance.api.crateBridgeApiRustApiLoadPlanBalance();
|
||||||
.api
|
|
||||||
.crateBridgeApiRustApiLoadPlanBalance(useCache: useCache);
|
|
||||||
|
|
||||||
static Future<void> notifyMessagesOpened({
|
static Future<void> notifyMessagesOpened({
|
||||||
required PlatformInt64 contactId,
|
required PlatformInt64 contactId,
|
||||||
|
|
@ -253,13 +250,18 @@ class RustApi {
|
||||||
messageIds: messageIds,
|
messageIds: messageIds,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
static Future<void> performPasswordlessRecoveryHeartbeat() => RustLib
|
||||||
|
.instance
|
||||||
|
.api
|
||||||
|
.crateBridgeApiRustApiPerformPasswordlessRecoveryHeartbeat();
|
||||||
|
|
||||||
static Future<PreparedOutgoingMessage?> prepareQueuedMessage({
|
static Future<PreparedOutgoingMessage?> prepareQueuedMessage({
|
||||||
required String receiptId,
|
required String receiptId,
|
||||||
}) => RustLib.instance.api.crateBridgeApiRustApiPrepareQueuedMessage(
|
}) => RustLib.instance.api.crateBridgeApiRustApiPrepareQueuedMessage(
|
||||||
receiptId: receiptId,
|
receiptId: receiptId,
|
||||||
);
|
);
|
||||||
|
|
||||||
static Future<ServerResultI64> register({
|
static Future<PlatformInt64> register({
|
||||||
required String username,
|
required String username,
|
||||||
required PlatformInt64 proofOfWork,
|
required PlatformInt64 proofOfWork,
|
||||||
required String langCode,
|
required String langCode,
|
||||||
|
|
@ -271,7 +273,7 @@ class RustApi {
|
||||||
isIos: isIos,
|
isIos: isIos,
|
||||||
);
|
);
|
||||||
|
|
||||||
static Future<ServerResultEmpty> registerPasswordlessNotification({
|
static Future<void> registerPasswordlessNotification({
|
||||||
required String notificationId,
|
required String notificationId,
|
||||||
required List<int> downloadAuthToken,
|
required List<int> downloadAuthToken,
|
||||||
required String langCode,
|
required String langCode,
|
||||||
|
|
@ -284,7 +286,7 @@ class RustApi {
|
||||||
googleFcm: googleFcm,
|
googleFcm: googleFcm,
|
||||||
);
|
);
|
||||||
|
|
||||||
static Future<ServerResultEmpty> registerPasswordlessRecovery({
|
static Future<void> registerPasswordlessRecovery({
|
||||||
required List<int> encryptedServerKey,
|
required List<int> encryptedServerKey,
|
||||||
Uint8List? pinUnlockToken,
|
Uint8List? pinUnlockToken,
|
||||||
}) => RustLib.instance.api.crateBridgeApiRustApiRegisterPasswordlessRecovery(
|
}) => RustLib.instance.api.crateBridgeApiRustApiRegisterPasswordlessRecovery(
|
||||||
|
|
@ -297,13 +299,12 @@ class RustApi {
|
||||||
static Future<void> reloadConfiguration() =>
|
static Future<void> reloadConfiguration() =>
|
||||||
RustLib.instance.api.crateBridgeApiRustApiReloadConfiguration();
|
RustLib.instance.api.crateBridgeApiRustApiReloadConfiguration();
|
||||||
|
|
||||||
static Future<ServerResultEmpty> removeAdditionalUser({
|
static Future<void> removeAdditionalUser({required PlatformInt64 userId}) =>
|
||||||
required PlatformInt64 userId,
|
RustLib.instance.api.crateBridgeApiRustApiRemoveAdditionalUser(
|
||||||
}) => RustLib.instance.api.crateBridgeApiRustApiRemoveAdditionalUser(
|
|
||||||
userId: userId,
|
userId: userId,
|
||||||
);
|
);
|
||||||
|
|
||||||
static Future<ServerResultEmpty> reportUser({
|
static Future<void> reportUser({
|
||||||
required PlatformInt64 userId,
|
required PlatformInt64 userId,
|
||||||
required String reason,
|
required String reason,
|
||||||
}) => RustLib.instance.api.crateBridgeApiRustApiReportUser(
|
}) => RustLib.instance.api.crateBridgeApiRustApiReportUser(
|
||||||
|
|
@ -321,7 +322,12 @@ class RustApi {
|
||||||
username: username,
|
username: username,
|
||||||
);
|
);
|
||||||
|
|
||||||
static Future<ServerResultVecU8> requestMemoriesUpload({
|
static Future<void> requestMediaReupload({required String mediaId}) => RustLib
|
||||||
|
.instance
|
||||||
|
.api
|
||||||
|
.crateBridgeApiRustApiRequestMediaReupload(mediaId: mediaId);
|
||||||
|
|
||||||
|
static Future<Uint8List> requestMemoriesUpload({
|
||||||
required PlatformInt64 size,
|
required PlatformInt64 size,
|
||||||
required PlatformInt64 originalDate,
|
required PlatformInt64 originalDate,
|
||||||
required String mediaId,
|
required String mediaId,
|
||||||
|
|
@ -375,7 +381,7 @@ class RustApi {
|
||||||
.api
|
.api
|
||||||
.crateBridgeApiRustApiSendQueuedMessage(receiptId: receiptId);
|
.crateBridgeApiRustApiSendQueuedMessage(receiptId: receiptId);
|
||||||
|
|
||||||
static Future<ServerResultEmpty> sendTextMessage({
|
static Future<void> sendTextMessage({
|
||||||
required PlatformInt64 userId,
|
required PlatformInt64 userId,
|
||||||
required List<int> body,
|
required List<int> body,
|
||||||
Uint8List? pushData,
|
Uint8List? pushData,
|
||||||
|
|
@ -398,7 +404,7 @@ class RustApi {
|
||||||
.api
|
.api
|
||||||
.crateBridgeApiRustApiSetBackground(inBackground: inBackground);
|
.crateBridgeApiRustApiSetBackground(inBackground: inBackground);
|
||||||
|
|
||||||
static Future<ServerResultEmpty> setLoginToken({required List<int> token}) =>
|
static Future<void> setLoginToken({required List<int> token}) =>
|
||||||
RustLib.instance.api.crateBridgeApiRustApiSetLoginToken(token: token);
|
RustLib.instance.api.crateBridgeApiRustApiSetLoginToken(token: token);
|
||||||
|
|
||||||
static Future<void> setNetworkAvailable({required bool available}) => RustLib
|
static Future<void> setNetworkAvailable({required bool available}) => RustLib
|
||||||
|
|
@ -406,7 +412,7 @@ class RustApi {
|
||||||
.api
|
.api
|
||||||
.crateBridgeApiRustApiSetNetworkAvailable(available: available);
|
.crateBridgeApiRustApiSetNetworkAvailable(available: available);
|
||||||
|
|
||||||
static Future<ServerResultEmpty> submitRecoveryShare({
|
static Future<void> submitRecoveryShare({
|
||||||
required String notificationId,
|
required String notificationId,
|
||||||
required List<int> encryptedMessage,
|
required List<int> encryptedMessage,
|
||||||
}) => RustLib.instance.api.crateBridgeApiRustApiSubmitRecoveryShare(
|
}) => RustLib.instance.api.crateBridgeApiRustApiSubmitRecoveryShare(
|
||||||
|
|
@ -414,10 +420,10 @@ class RustApi {
|
||||||
encryptedMessage: encryptedMessage,
|
encryptedMessage: encryptedMessage,
|
||||||
);
|
);
|
||||||
|
|
||||||
static Future<ServerResultEmpty> updateFcmToken({required String token}) =>
|
static Future<void> updateFcmToken({required String token}) =>
|
||||||
RustLib.instance.api.crateBridgeApiRustApiUpdateFcmToken(token: token);
|
RustLib.instance.api.crateBridgeApiRustApiUpdateFcmToken(token: token);
|
||||||
|
|
||||||
static Future<Uint8List> updateSignedPreKey({
|
static Future<void> updateSignedPreKey({
|
||||||
required PlatformInt64 id,
|
required PlatformInt64 id,
|
||||||
required List<int> key,
|
required List<int> key,
|
||||||
required List<int> signature,
|
required List<int> signature,
|
||||||
|
|
@ -427,7 +433,7 @@ class RustApi {
|
||||||
signature: signature,
|
signature: signature,
|
||||||
);
|
);
|
||||||
|
|
||||||
static Future<Uint8List> uploadPqcPreKeys({
|
static Future<void> uploadPqcPreKeys({
|
||||||
required PlatformInt64 eccSignedPrekeyId,
|
required PlatformInt64 eccSignedPrekeyId,
|
||||||
required List<int> eccSignedPrekey,
|
required List<int> eccSignedPrekey,
|
||||||
required List<int> eccSignedPrekeySignature,
|
required List<int> eccSignedPrekeySignature,
|
||||||
|
|
@ -453,37 +459,3 @@ class RustApi {
|
||||||
identical(this, other) ||
|
identical(this, other) ||
|
||||||
other is RustApi && runtimeType == other.runtimeType;
|
other is RustApi && runtimeType == other.runtimeType;
|
||||||
}
|
}
|
||||||
|
|
||||||
@freezed
|
|
||||||
sealed class ServerResultEmpty with _$ServerResultEmpty {
|
|
||||||
const ServerResultEmpty._();
|
|
||||||
|
|
||||||
const factory ServerResultEmpty.ok() = ServerResultEmpty_Ok;
|
|
||||||
const factory ServerResultEmpty.errorCode(
|
|
||||||
int field0,
|
|
||||||
) = ServerResultEmpty_ErrorCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
sealed class ServerResultI64 with _$ServerResultI64 {
|
|
||||||
const ServerResultI64._();
|
|
||||||
|
|
||||||
const factory ServerResultI64.ok(
|
|
||||||
PlatformInt64 field0,
|
|
||||||
) = ServerResultI64_Ok;
|
|
||||||
const factory ServerResultI64.errorCode(
|
|
||||||
int field0,
|
|
||||||
) = ServerResultI64_ErrorCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
sealed class ServerResultVecU8 with _$ServerResultVecU8 {
|
|
||||||
const ServerResultVecU8._();
|
|
||||||
|
|
||||||
const factory ServerResultVecU8.ok(
|
|
||||||
Uint8List field0,
|
|
||||||
) = ServerResultVecU8_Ok;
|
|
||||||
const factory ServerResultVecU8.errorCode(
|
|
||||||
int field0,
|
|
||||||
) = ServerResultVecU8_ErrorCode;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
|
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
|
||||||
|
|
||||||
import '../frb_generated.dart';
|
import '../frb_generated.dart';
|
||||||
|
import '../user_config.dart';
|
||||||
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
|
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
|
||||||
|
|
||||||
// These functions are ignored because they are not marked as `pub`: `get_callbacks`
|
// These functions are ignored because they are not marked as `pub`: `get_callbacks`
|
||||||
|
|
@ -28,19 +28,13 @@ Future<void> initFlutterCallbacks({
|
||||||
required FutureOr<List<LegacySignalPreKey>> Function()
|
required FutureOr<List<LegacySignalPreKey>> Function()
|
||||||
legacySignalGeneratePrekeys,
|
legacySignalGeneratePrekeys,
|
||||||
required FutureOr<void> Function(PlatformInt64) apiResyncSignalSession,
|
required FutureOr<void> Function(PlatformInt64) apiResyncSignalSession,
|
||||||
required FutureOr<void> Function(PlatformInt64) apiPushKeyRequested,
|
|
||||||
required FutureOr<void> Function(PlatformInt64, String, String)
|
|
||||||
apiGroupMembershipError,
|
|
||||||
required FutureOr<void> Function(String, String, PlatformInt64, String)
|
required FutureOr<void> Function(String, String, PlatformInt64, String)
|
||||||
apiMediaAction,
|
apiMediaAction,
|
||||||
required FutureOr<void> Function(PlatformInt64, Uint8List)
|
required FutureOr<void> Function(PlatformInt64, Uint8List)
|
||||||
apiVerificationProof,
|
apiVerificationProof,
|
||||||
required FutureOr<Uint8List?> Function(PlatformInt64, String?, Uint8List, int)
|
|
||||||
apiCreatePushData,
|
|
||||||
required FutureOr<void> Function(PlatformInt64) apiCreatePushAvatars,
|
required FutureOr<void> Function(PlatformInt64) apiCreatePushAvatars,
|
||||||
required FutureOr<void> Function() apiRecoveryChanged,
|
|
||||||
required FutureOr<void> Function(String, PlatformInt64) apiMediaReceived,
|
required FutureOr<void> Function(String, PlatformInt64) apiMediaReceived,
|
||||||
required FutureOr<void> Function(String, bool) apiGroupStateRefresh,
|
required FutureOr<void> Function(UserConfig) apiUserConfigChanged,
|
||||||
}) => RustLib.instance.api.crateBridgeCallbacksInitFlutterCallbacks(
|
}) => RustLib.instance.api.crateBridgeCallbacksInitFlutterCallbacks(
|
||||||
callbackId: callbackId,
|
callbackId: callbackId,
|
||||||
loggingGetStreamSink: loggingGetStreamSink,
|
loggingGetStreamSink: loggingGetStreamSink,
|
||||||
|
|
@ -48,15 +42,11 @@ Future<void> initFlutterCallbacks({
|
||||||
legacySignalEncrypt: legacySignalEncrypt,
|
legacySignalEncrypt: legacySignalEncrypt,
|
||||||
legacySignalGeneratePrekeys: legacySignalGeneratePrekeys,
|
legacySignalGeneratePrekeys: legacySignalGeneratePrekeys,
|
||||||
apiResyncSignalSession: apiResyncSignalSession,
|
apiResyncSignalSession: apiResyncSignalSession,
|
||||||
apiPushKeyRequested: apiPushKeyRequested,
|
|
||||||
apiGroupMembershipError: apiGroupMembershipError,
|
|
||||||
apiMediaAction: apiMediaAction,
|
apiMediaAction: apiMediaAction,
|
||||||
apiVerificationProof: apiVerificationProof,
|
apiVerificationProof: apiVerificationProof,
|
||||||
apiCreatePushData: apiCreatePushData,
|
|
||||||
apiCreatePushAvatars: apiCreatePushAvatars,
|
apiCreatePushAvatars: apiCreatePushAvatars,
|
||||||
apiRecoveryChanged: apiRecoveryChanged,
|
|
||||||
apiMediaReceived: apiMediaReceived,
|
apiMediaReceived: apiMediaReceived,
|
||||||
apiGroupStateRefresh: apiGroupStateRefresh,
|
apiUserConfigChanged: apiUserConfigChanged,
|
||||||
);
|
);
|
||||||
|
|
||||||
class LegacySignalDecryptResult {
|
class LegacySignalDecryptResult {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@
|
||||||
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
|
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
|
||||||
|
|
||||||
import '../frb_generated.dart';
|
import '../frb_generated.dart';
|
||||||
|
|
||||||
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
|
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
|
||||||
|
|
||||||
Future<bool> createNewGroup({
|
Future<bool> createNewGroup({
|
||||||
|
|
|
||||||
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 {
|
class FlutterUserDiscovery {
|
||||||
const FlutterUserDiscovery();
|
const FlutterUserDiscovery();
|
||||||
|
|
||||||
|
static Future<void> changeExclusionForContact({
|
||||||
|
required int callbackId,
|
||||||
|
required PlatformInt64 contactId,
|
||||||
|
required bool exclude,
|
||||||
|
}) => RustLib.instance.api
|
||||||
|
.crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryChangeExclusionForContact(
|
||||||
|
callbackId: callbackId,
|
||||||
|
contactId: contactId,
|
||||||
|
exclude: exclude,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// UI-facing read used to display the current discovery version.
|
||||||
static Future<Uint8List> getCurrentVersion({required int callbackId}) =>
|
static Future<Uint8List> getCurrentVersion({required int callbackId}) =>
|
||||||
RustLib.instance.api
|
RustLib.instance.api
|
||||||
.crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetCurrentVersion(
|
.crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetCurrentVersion(
|
||||||
callbackId: callbackId,
|
callbackId: callbackId,
|
||||||
);
|
);
|
||||||
|
|
||||||
static Future<List<Uint8List>> getNewMessages({
|
/// UI-facing hook used when a user manually changes contact verification.
|
||||||
required int callbackId,
|
|
||||||
required PlatformInt64 contactId,
|
|
||||||
required List<int> receivedVersion,
|
|
||||||
}) => RustLib.instance.api
|
|
||||||
.crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetNewMessages(
|
|
||||||
callbackId: callbackId,
|
|
||||||
contactId: contactId,
|
|
||||||
receivedVersion: receivedVersion,
|
|
||||||
);
|
|
||||||
|
|
||||||
static Future<void> handleNewMessages({
|
|
||||||
required int callbackId,
|
|
||||||
required PlatformInt64 contactId,
|
|
||||||
PlatformInt64? publicKeyVerifiedTimestamp,
|
|
||||||
required List<Uint8List> messages,
|
|
||||||
}) => RustLib.instance.api
|
|
||||||
.crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryHandleNewMessages(
|
|
||||||
callbackId: callbackId,
|
|
||||||
contactId: contactId,
|
|
||||||
publicKeyVerifiedTimestamp: publicKeyVerifiedTimestamp,
|
|
||||||
messages: messages,
|
|
||||||
);
|
|
||||||
|
|
||||||
static Future<void> initializeOrUpdate({
|
|
||||||
required int callbackId,
|
|
||||||
required int threshold,
|
|
||||||
required PlatformInt64 userId,
|
|
||||||
required List<int> publicKey,
|
|
||||||
required bool sharePromotion,
|
|
||||||
}) => RustLib.instance.api
|
|
||||||
.crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryInitializeOrUpdate(
|
|
||||||
callbackId: callbackId,
|
|
||||||
threshold: threshold,
|
|
||||||
userId: userId,
|
|
||||||
publicKey: publicKey,
|
|
||||||
sharePromotion: sharePromotion,
|
|
||||||
);
|
|
||||||
|
|
||||||
static Future<Uint8List?> shouldRequestNewMessages({
|
|
||||||
required int callbackId,
|
|
||||||
required PlatformInt64 contactId,
|
|
||||||
required List<int> version,
|
|
||||||
}) => RustLib.instance.api
|
|
||||||
.crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryShouldRequestNewMessages(
|
|
||||||
callbackId: callbackId,
|
|
||||||
contactId: contactId,
|
|
||||||
version: version,
|
|
||||||
);
|
|
||||||
|
|
||||||
static Future<void> updateVerificationStateForUser({
|
static Future<void> updateVerificationStateForUser({
|
||||||
required int callbackId,
|
required int callbackId,
|
||||||
required PlatformInt64 contactId,
|
required PlatformInt64 contactId,
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -8,6 +8,7 @@ import 'bridge.dart';
|
||||||
import 'bridge/api.dart';
|
import 'bridge/api.dart';
|
||||||
import 'bridge/callbacks.dart';
|
import 'bridge/callbacks.dart';
|
||||||
import 'bridge/groups.dart';
|
import 'bridge/groups.dart';
|
||||||
|
import 'bridge/user_config.dart';
|
||||||
import 'bridge/wrapper.dart';
|
import 'bridge/wrapper.dart';
|
||||||
import 'bridge/wrapper/app_database.dart';
|
import 'bridge/wrapper/app_database.dart';
|
||||||
import 'bridge/wrapper/backup.dart';
|
import 'bridge/wrapper/backup.dart';
|
||||||
|
|
@ -23,6 +24,7 @@ import 'keys/backup_password_keys.dart';
|
||||||
import 'lib.dart';
|
import 'lib.dart';
|
||||||
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_io.dart';
|
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_io.dart';
|
||||||
import 'signal/engine.dart';
|
import 'signal/engine.dart';
|
||||||
|
import 'user_config.dart';
|
||||||
|
|
||||||
abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
RustLibApiImplPlatform({
|
RustLibApiImplPlatform({
|
||||||
|
|
@ -35,16 +37,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
AnyhowException dco_decode_AnyhowException(dynamic raw);
|
AnyhowException dco_decode_AnyhowException(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
DateTime dco_decode_Chrono_Utc(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
FutureOr<void> Function(String, String, PlatformInt64, String)
|
FutureOr<void> Function(String, String, PlatformInt64, String)
|
||||||
dco_decode_DartFn_Inputs_String_String_i_64_String_Output_unit_AnyhowException(
|
dco_decode_DartFn_Inputs_String_String_i_64_String_Output_unit_AnyhowException(
|
||||||
dynamic raw,
|
dynamic raw,
|
||||||
);
|
);
|
||||||
|
|
||||||
@protected
|
|
||||||
FutureOr<void> Function(String, bool)
|
|
||||||
dco_decode_DartFn_Inputs_String_bool_Output_unit_AnyhowException(dynamic raw);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
FutureOr<void> Function(String, PlatformInt64)
|
FutureOr<void> Function(String, PlatformInt64)
|
||||||
dco_decode_DartFn_Inputs_String_i_64_Output_unit_AnyhowException(dynamic raw);
|
dco_decode_DartFn_Inputs_String_i_64_Output_unit_AnyhowException(dynamic raw);
|
||||||
|
|
@ -61,20 +62,10 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
dynamic raw,
|
dynamic raw,
|
||||||
);
|
);
|
||||||
|
|
||||||
@protected
|
|
||||||
FutureOr<void> Function()
|
|
||||||
dco_decode_DartFn_Inputs__Output_unit_AnyhowException(dynamic raw);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
FutureOr<void> Function(PlatformInt64)
|
FutureOr<void> Function(PlatformInt64)
|
||||||
dco_decode_DartFn_Inputs_i_64_Output_unit_AnyhowException(dynamic raw);
|
dco_decode_DartFn_Inputs_i_64_Output_unit_AnyhowException(dynamic raw);
|
||||||
|
|
||||||
@protected
|
|
||||||
FutureOr<void> Function(PlatformInt64, String, String)
|
|
||||||
dco_decode_DartFn_Inputs_i_64_String_String_Output_unit_AnyhowException(
|
|
||||||
dynamic raw,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
FutureOr<LegacySignalEncryptResult?> Function(PlatformInt64, Uint8List)
|
FutureOr<LegacySignalEncryptResult?> Function(PlatformInt64, Uint8List)
|
||||||
dco_decode_DartFn_Inputs_i_64_list_prim_u_8_strict_Output_opt_box_autoadd_legacy_signal_encrypt_result_AnyhowException(
|
dco_decode_DartFn_Inputs_i_64_list_prim_u_8_strict_Output_opt_box_autoadd_legacy_signal_encrypt_result_AnyhowException(
|
||||||
|
|
@ -94,14 +85,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
);
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
FutureOr<Uint8List?> Function(PlatformInt64, String?, Uint8List, int)
|
FutureOr<void> Function(UserConfig)
|
||||||
dco_decode_DartFn_Inputs_i_64_opt_String_list_prim_u_8_strict_i_32_Output_opt_list_prim_u_8_strict_AnyhowException(
|
dco_decode_DartFn_Inputs_user_config_Output_unit_AnyhowException(dynamic raw);
|
||||||
dynamic raw,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
Object dco_decode_DartOpaque(dynamic raw);
|
Object dco_decode_DartOpaque(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
Map<String, List<String>> dco_decode_Map_String_list_String_None(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
Map<PlatformInt64, Uint8List> dco_decode_Map_i_64_list_prim_u_8_strict_None(
|
Map<PlatformInt64, Uint8List> dco_decode_Map_i_64_list_prim_u_8_strict_None(
|
||||||
dynamic raw,
|
dynamic raw,
|
||||||
|
|
@ -131,6 +123,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
bool dco_decode_bool(dynamic raw);
|
bool dco_decode_bool(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
DateTime dco_decode_box_autoadd_Chrono_Utc(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
ApiConnectionState dco_decode_box_autoadd_api_connection_state(dynamic raw);
|
ApiConnectionState dco_decode_box_autoadd_api_connection_state(dynamic raw);
|
||||||
|
|
||||||
|
|
@ -157,14 +152,24 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
dynamic raw,
|
dynamic raw,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
PasswordlessRecoveryConfig
|
||||||
|
dco_decode_box_autoadd_passwordless_recovery_config(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
PreparedOutgoingMessage dco_decode_box_autoadd_prepared_outgoing_message(
|
PreparedOutgoingMessage dco_decode_box_autoadd_prepared_outgoing_message(
|
||||||
dynamic raw,
|
dynamic raw,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
TwonlySafeBackup dco_decode_box_autoadd_twonly_safe_backup(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
int dco_decode_box_autoadd_u_32(dynamic raw);
|
int dco_decode_box_autoadd_u_32(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
UserConfig dco_decode_box_autoadd_user_config(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
double dco_decode_f_64(dynamic raw);
|
double dco_decode_f_64(dynamic raw);
|
||||||
|
|
||||||
|
|
@ -189,6 +194,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
PlatformInt64 dco_decode_isize(dynamic raw);
|
PlatformInt64 dco_decode_isize(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
LastBackupUploadState dco_decode_last_backup_upload_state(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
LegacyMigrationReport dco_decode_legacy_migration_report(dynamic raw);
|
LegacyMigrationReport dco_decode_legacy_migration_report(dynamic raw);
|
||||||
|
|
||||||
|
|
@ -243,15 +251,28 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
List<(PlatformInt64, Uint8List)>
|
List<(PlatformInt64, Uint8List)>
|
||||||
dco_decode_list_record_i_64_list_prim_u_8_strict(dynamic raw);
|
dco_decode_list_record_i_64_list_prim_u_8_strict(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
List<(String, List<String>)> dco_decode_list_record_string_list_string(
|
||||||
|
dynamic raw,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
List<SqlRow> dco_decode_list_sql_row(dynamic raw);
|
List<SqlRow> dco_decode_list_sql_row(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
List<SqlValue> dco_decode_list_sql_value(dynamic raw);
|
List<SqlValue> dco_decode_list_sql_value(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
Map<String, List<String>>? dco_decode_opt_Map_String_list_String_None(
|
||||||
|
dynamic raw,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
String? dco_decode_opt_String(dynamic raw);
|
String? dco_decode_opt_String(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
DateTime? dco_decode_opt_box_autoadd_Chrono_Utc(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
ApiConnectionState? dco_decode_opt_box_autoadd_api_connection_state(
|
ApiConnectionState? dco_decode_opt_box_autoadd_api_connection_state(
|
||||||
dynamic raw,
|
dynamic raw,
|
||||||
|
|
@ -270,17 +291,35 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
LegacySignalEncryptResult?
|
LegacySignalEncryptResult?
|
||||||
dco_decode_opt_box_autoadd_legacy_signal_encrypt_result(dynamic raw);
|
dco_decode_opt_box_autoadd_legacy_signal_encrypt_result(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
PasswordlessRecoveryConfig?
|
||||||
|
dco_decode_opt_box_autoadd_passwordless_recovery_config(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
PreparedOutgoingMessage? dco_decode_opt_box_autoadd_prepared_outgoing_message(
|
PreparedOutgoingMessage? dco_decode_opt_box_autoadd_prepared_outgoing_message(
|
||||||
dynamic raw,
|
dynamic raw,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
TwonlySafeBackup? dco_decode_opt_box_autoadd_twonly_safe_backup(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
int? dco_decode_opt_box_autoadd_u_32(dynamic raw);
|
int? dco_decode_opt_box_autoadd_u_32(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
UserConfig? dco_decode_opt_box_autoadd_user_config(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
List<String>? dco_decode_opt_list_String(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
Uint8List? dco_decode_opt_list_prim_u_8_strict(dynamic raw);
|
Uint8List? dco_decode_opt_list_prim_u_8_strict(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
PasswordlessRecoveryConfig dco_decode_passwordless_recovery_config(
|
||||||
|
dynamic raw,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
PqcPreKeyInput dco_decode_pqc_pre_key_input(dynamic raw);
|
PqcPreKeyInput dco_decode_pqc_pre_key_input(dynamic raw);
|
||||||
|
|
||||||
|
|
@ -297,6 +336,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
dynamic raw,
|
dynamic raw,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
(String, List<String>) dco_decode_record_string_list_string(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
(String, String) dco_decode_record_string_string(dynamic raw);
|
(String, String) dco_decode_record_string_string(dynamic raw);
|
||||||
|
|
||||||
|
|
@ -322,13 +364,7 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
RustUtils dco_decode_rust_utils(dynamic raw);
|
RustUtils dco_decode_rust_utils(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
ServerResultEmpty dco_decode_server_result_empty(dynamic raw);
|
SetupProfile dco_decode_setup_profile(dynamic raw);
|
||||||
|
|
||||||
@protected
|
|
||||||
ServerResultI64 dco_decode_server_result_i_64(dynamic raw);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
ServerResultVecU8 dco_decode_server_result_vec_u_8(dynamic raw);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
SqlExecutionResult dco_decode_sql_execution_result(dynamic raw);
|
SqlExecutionResult dco_decode_sql_execution_result(dynamic raw);
|
||||||
|
|
@ -342,6 +378,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
SqlValue dco_decode_sql_value(dynamic raw);
|
SqlValue dco_decode_sql_value(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
ThemeMode dco_decode_theme_mode(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
TwonlySafeBackup dco_decode_twonly_safe_backup(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
int dco_decode_u_32(dynamic raw);
|
int dco_decode_u_32(dynamic raw);
|
||||||
|
|
||||||
|
|
@ -357,15 +399,29 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
void dco_decode_unit(dynamic raw);
|
void dco_decode_unit(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
UserConfig dco_decode_user_config(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
UserConfigApi dco_decode_user_config_api(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
BigInt dco_decode_usize(dynamic raw);
|
BigInt dco_decode_usize(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer);
|
AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
DateTime sse_decode_Chrono_Utc(SseDeserializer deserializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
Object sse_decode_DartOpaque(SseDeserializer deserializer);
|
Object sse_decode_DartOpaque(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
Map<String, List<String>> sse_decode_Map_String_list_String_None(
|
||||||
|
SseDeserializer deserializer,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
Map<PlatformInt64, Uint8List> sse_decode_Map_i_64_list_prim_u_8_strict_None(
|
Map<PlatformInt64, Uint8List> sse_decode_Map_i_64_list_prim_u_8_strict_None(
|
||||||
SseDeserializer deserializer,
|
SseDeserializer deserializer,
|
||||||
|
|
@ -403,6 +459,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
bool sse_decode_bool(SseDeserializer deserializer);
|
bool sse_decode_bool(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
DateTime sse_decode_box_autoadd_Chrono_Utc(SseDeserializer deserializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
ApiConnectionState sse_decode_box_autoadd_api_connection_state(
|
ApiConnectionState sse_decode_box_autoadd_api_connection_state(
|
||||||
SseDeserializer deserializer,
|
SseDeserializer deserializer,
|
||||||
|
|
@ -435,14 +494,28 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
SseDeserializer deserializer,
|
SseDeserializer deserializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
PasswordlessRecoveryConfig
|
||||||
|
sse_decode_box_autoadd_passwordless_recovery_config(
|
||||||
|
SseDeserializer deserializer,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
PreparedOutgoingMessage sse_decode_box_autoadd_prepared_outgoing_message(
|
PreparedOutgoingMessage sse_decode_box_autoadd_prepared_outgoing_message(
|
||||||
SseDeserializer deserializer,
|
SseDeserializer deserializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
TwonlySafeBackup sse_decode_box_autoadd_twonly_safe_backup(
|
||||||
|
SseDeserializer deserializer,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
int sse_decode_box_autoadd_u_32(SseDeserializer deserializer);
|
int sse_decode_box_autoadd_u_32(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
UserConfig sse_decode_box_autoadd_user_config(SseDeserializer deserializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
double sse_decode_f_64(SseDeserializer deserializer);
|
double sse_decode_f_64(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
|
@ -469,6 +542,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
PlatformInt64 sse_decode_isize(SseDeserializer deserializer);
|
PlatformInt64 sse_decode_isize(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
LastBackupUploadState sse_decode_last_backup_upload_state(
|
||||||
|
SseDeserializer deserializer,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
LegacyMigrationReport sse_decode_legacy_migration_report(
|
LegacyMigrationReport sse_decode_legacy_migration_report(
|
||||||
SseDeserializer deserializer,
|
SseDeserializer deserializer,
|
||||||
|
|
@ -537,15 +615,28 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
SseDeserializer deserializer,
|
SseDeserializer deserializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
List<(String, List<String>)> sse_decode_list_record_string_list_string(
|
||||||
|
SseDeserializer deserializer,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
List<SqlRow> sse_decode_list_sql_row(SseDeserializer deserializer);
|
List<SqlRow> sse_decode_list_sql_row(SseDeserializer deserializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
List<SqlValue> sse_decode_list_sql_value(SseDeserializer deserializer);
|
List<SqlValue> sse_decode_list_sql_value(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
Map<String, List<String>>? sse_decode_opt_Map_String_list_String_None(
|
||||||
|
SseDeserializer deserializer,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
String? sse_decode_opt_String(SseDeserializer deserializer);
|
String? sse_decode_opt_String(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
DateTime? sse_decode_opt_box_autoadd_Chrono_Utc(SseDeserializer deserializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
ApiConnectionState? sse_decode_opt_box_autoadd_api_connection_state(
|
ApiConnectionState? sse_decode_opt_box_autoadd_api_connection_state(
|
||||||
SseDeserializer deserializer,
|
SseDeserializer deserializer,
|
||||||
|
|
@ -566,17 +657,41 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
SseDeserializer deserializer,
|
SseDeserializer deserializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
PasswordlessRecoveryConfig?
|
||||||
|
sse_decode_opt_box_autoadd_passwordless_recovery_config(
|
||||||
|
SseDeserializer deserializer,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
PreparedOutgoingMessage? sse_decode_opt_box_autoadd_prepared_outgoing_message(
|
PreparedOutgoingMessage? sse_decode_opt_box_autoadd_prepared_outgoing_message(
|
||||||
SseDeserializer deserializer,
|
SseDeserializer deserializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
TwonlySafeBackup? sse_decode_opt_box_autoadd_twonly_safe_backup(
|
||||||
|
SseDeserializer deserializer,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
int? sse_decode_opt_box_autoadd_u_32(SseDeserializer deserializer);
|
int? sse_decode_opt_box_autoadd_u_32(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
UserConfig? sse_decode_opt_box_autoadd_user_config(
|
||||||
|
SseDeserializer deserializer,
|
||||||
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
List<String>? sse_decode_opt_list_String(SseDeserializer deserializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
Uint8List? sse_decode_opt_list_prim_u_8_strict(SseDeserializer deserializer);
|
Uint8List? sse_decode_opt_list_prim_u_8_strict(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
PasswordlessRecoveryConfig sse_decode_passwordless_recovery_config(
|
||||||
|
SseDeserializer deserializer,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
PqcPreKeyInput sse_decode_pqc_pre_key_input(SseDeserializer deserializer);
|
PqcPreKeyInput sse_decode_pqc_pre_key_input(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
|
@ -595,6 +710,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
SseDeserializer deserializer,
|
SseDeserializer deserializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
(String, List<String>) sse_decode_record_string_list_string(
|
||||||
|
SseDeserializer deserializer,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
(String, String) sse_decode_record_string_string(
|
(String, String) sse_decode_record_string_string(
|
||||||
SseDeserializer deserializer,
|
SseDeserializer deserializer,
|
||||||
|
|
@ -626,17 +746,7 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
RustUtils sse_decode_rust_utils(SseDeserializer deserializer);
|
RustUtils sse_decode_rust_utils(SseDeserializer deserializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
ServerResultEmpty sse_decode_server_result_empty(
|
SetupProfile sse_decode_setup_profile(SseDeserializer deserializer);
|
||||||
SseDeserializer deserializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
ServerResultI64 sse_decode_server_result_i_64(SseDeserializer deserializer);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
ServerResultVecU8 sse_decode_server_result_vec_u_8(
|
|
||||||
SseDeserializer deserializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
SqlExecutionResult sse_decode_sql_execution_result(
|
SqlExecutionResult sse_decode_sql_execution_result(
|
||||||
|
|
@ -652,6 +762,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
SqlValue sse_decode_sql_value(SseDeserializer deserializer);
|
SqlValue sse_decode_sql_value(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
ThemeMode sse_decode_theme_mode(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
TwonlySafeBackup sse_decode_twonly_safe_backup(SseDeserializer deserializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
int sse_decode_u_32(SseDeserializer deserializer);
|
int sse_decode_u_32(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
|
@ -667,6 +783,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
void sse_decode_unit(SseDeserializer deserializer);
|
void sse_decode_unit(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
UserConfig sse_decode_user_config(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
UserConfigApi sse_decode_user_config_api(SseDeserializer deserializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
BigInt sse_decode_usize(SseDeserializer deserializer);
|
BigInt sse_decode_usize(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
|
@ -676,6 +798,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
SseSerializer serializer,
|
SseSerializer serializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_Chrono_Utc(DateTime self, SseSerializer serializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void
|
void
|
||||||
sse_encode_DartFn_Inputs_String_String_i_64_String_Output_unit_AnyhowException(
|
sse_encode_DartFn_Inputs_String_String_i_64_String_Output_unit_AnyhowException(
|
||||||
|
|
@ -683,12 +808,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
SseSerializer serializer,
|
SseSerializer serializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
@protected
|
|
||||||
void sse_encode_DartFn_Inputs_String_bool_Output_unit_AnyhowException(
|
|
||||||
FutureOr<void> Function(String, bool) self,
|
|
||||||
SseSerializer serializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_DartFn_Inputs_String_i_64_Output_unit_AnyhowException(
|
void sse_encode_DartFn_Inputs_String_i_64_Output_unit_AnyhowException(
|
||||||
FutureOr<void> Function(String, PlatformInt64) self,
|
FutureOr<void> Function(String, PlatformInt64) self,
|
||||||
|
|
@ -708,24 +827,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
SseSerializer serializer,
|
SseSerializer serializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
@protected
|
|
||||||
void sse_encode_DartFn_Inputs__Output_unit_AnyhowException(
|
|
||||||
FutureOr<void> Function() self,
|
|
||||||
SseSerializer serializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_DartFn_Inputs_i_64_Output_unit_AnyhowException(
|
void sse_encode_DartFn_Inputs_i_64_Output_unit_AnyhowException(
|
||||||
FutureOr<void> Function(PlatformInt64) self,
|
FutureOr<void> Function(PlatformInt64) self,
|
||||||
SseSerializer serializer,
|
SseSerializer serializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
@protected
|
|
||||||
void sse_encode_DartFn_Inputs_i_64_String_String_Output_unit_AnyhowException(
|
|
||||||
FutureOr<void> Function(PlatformInt64, String, String) self,
|
|
||||||
SseSerializer serializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void
|
void
|
||||||
sse_encode_DartFn_Inputs_i_64_list_prim_u_8_strict_Output_opt_box_autoadd_legacy_signal_encrypt_result_AnyhowException(
|
sse_encode_DartFn_Inputs_i_64_list_prim_u_8_strict_Output_opt_box_autoadd_legacy_signal_encrypt_result_AnyhowException(
|
||||||
|
|
@ -750,15 +857,20 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
);
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void
|
void sse_encode_DartFn_Inputs_user_config_Output_unit_AnyhowException(
|
||||||
sse_encode_DartFn_Inputs_i_64_opt_String_list_prim_u_8_strict_i_32_Output_opt_list_prim_u_8_strict_AnyhowException(
|
FutureOr<void> Function(UserConfig) self,
|
||||||
FutureOr<Uint8List?> Function(PlatformInt64, String?, Uint8List, int) self,
|
|
||||||
SseSerializer serializer,
|
SseSerializer serializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_DartOpaque(Object self, SseSerializer serializer);
|
void sse_encode_DartOpaque(Object self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_Map_String_list_String_None(
|
||||||
|
Map<String, List<String>> self,
|
||||||
|
SseSerializer serializer,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_Map_i_64_list_prim_u_8_strict_None(
|
void sse_encode_Map_i_64_list_prim_u_8_strict_None(
|
||||||
Map<PlatformInt64, Uint8List> self,
|
Map<PlatformInt64, Uint8List> self,
|
||||||
|
|
@ -801,6 +913,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_bool(bool self, SseSerializer serializer);
|
void sse_encode_bool(bool self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_box_autoadd_Chrono_Utc(
|
||||||
|
DateTime self,
|
||||||
|
SseSerializer serializer,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_box_autoadd_api_connection_state(
|
void sse_encode_box_autoadd_api_connection_state(
|
||||||
ApiConnectionState self,
|
ApiConnectionState self,
|
||||||
|
|
@ -843,15 +961,33 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
SseSerializer serializer,
|
SseSerializer serializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_box_autoadd_passwordless_recovery_config(
|
||||||
|
PasswordlessRecoveryConfig self,
|
||||||
|
SseSerializer serializer,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_box_autoadd_prepared_outgoing_message(
|
void sse_encode_box_autoadd_prepared_outgoing_message(
|
||||||
PreparedOutgoingMessage self,
|
PreparedOutgoingMessage self,
|
||||||
SseSerializer serializer,
|
SseSerializer serializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_box_autoadd_twonly_safe_backup(
|
||||||
|
TwonlySafeBackup self,
|
||||||
|
SseSerializer serializer,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_box_autoadd_u_32(int self, SseSerializer serializer);
|
void sse_encode_box_autoadd_u_32(int self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_box_autoadd_user_config(
|
||||||
|
UserConfig self,
|
||||||
|
SseSerializer serializer,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_f_64(double self, SseSerializer serializer);
|
void sse_encode_f_64(double self, SseSerializer serializer);
|
||||||
|
|
||||||
|
|
@ -882,6 +1018,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_isize(PlatformInt64 self, SseSerializer serializer);
|
void sse_encode_isize(PlatformInt64 self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_last_backup_upload_state(
|
||||||
|
LastBackupUploadState self,
|
||||||
|
SseSerializer serializer,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_legacy_migration_report(
|
void sse_encode_legacy_migration_report(
|
||||||
LegacyMigrationReport self,
|
LegacyMigrationReport self,
|
||||||
|
|
@ -966,15 +1108,33 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
SseSerializer serializer,
|
SseSerializer serializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_list_record_string_list_string(
|
||||||
|
List<(String, List<String>)> self,
|
||||||
|
SseSerializer serializer,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_list_sql_row(List<SqlRow> self, SseSerializer serializer);
|
void sse_encode_list_sql_row(List<SqlRow> self, SseSerializer serializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_list_sql_value(List<SqlValue> self, SseSerializer serializer);
|
void sse_encode_list_sql_value(List<SqlValue> self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_opt_Map_String_list_String_None(
|
||||||
|
Map<String, List<String>>? self,
|
||||||
|
SseSerializer serializer,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_opt_String(String? self, SseSerializer serializer);
|
void sse_encode_opt_String(String? self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_opt_box_autoadd_Chrono_Utc(
|
||||||
|
DateTime? self,
|
||||||
|
SseSerializer serializer,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_opt_box_autoadd_api_connection_state(
|
void sse_encode_opt_box_autoadd_api_connection_state(
|
||||||
ApiConnectionState? self,
|
ApiConnectionState? self,
|
||||||
|
|
@ -999,21 +1159,48 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
SseSerializer serializer,
|
SseSerializer serializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_opt_box_autoadd_passwordless_recovery_config(
|
||||||
|
PasswordlessRecoveryConfig? self,
|
||||||
|
SseSerializer serializer,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_opt_box_autoadd_prepared_outgoing_message(
|
void sse_encode_opt_box_autoadd_prepared_outgoing_message(
|
||||||
PreparedOutgoingMessage? self,
|
PreparedOutgoingMessage? self,
|
||||||
SseSerializer serializer,
|
SseSerializer serializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_opt_box_autoadd_twonly_safe_backup(
|
||||||
|
TwonlySafeBackup? self,
|
||||||
|
SseSerializer serializer,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_opt_box_autoadd_u_32(int? self, SseSerializer serializer);
|
void sse_encode_opt_box_autoadd_u_32(int? self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_opt_box_autoadd_user_config(
|
||||||
|
UserConfig? self,
|
||||||
|
SseSerializer serializer,
|
||||||
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_opt_list_String(List<String>? self, SseSerializer serializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_opt_list_prim_u_8_strict(
|
void sse_encode_opt_list_prim_u_8_strict(
|
||||||
Uint8List? self,
|
Uint8List? self,
|
||||||
SseSerializer serializer,
|
SseSerializer serializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_passwordless_recovery_config(
|
||||||
|
PasswordlessRecoveryConfig self,
|
||||||
|
SseSerializer serializer,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_pqc_pre_key_input(
|
void sse_encode_pqc_pre_key_input(
|
||||||
PqcPreKeyInput self,
|
PqcPreKeyInput self,
|
||||||
|
|
@ -1038,6 +1225,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
SseSerializer serializer,
|
SseSerializer serializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_record_string_list_string(
|
||||||
|
(String, List<String>) self,
|
||||||
|
SseSerializer serializer,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_record_string_string(
|
void sse_encode_record_string_string(
|
||||||
(String, String) self,
|
(String, String) self,
|
||||||
|
|
@ -1078,22 +1271,7 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
void sse_encode_rust_utils(RustUtils self, SseSerializer serializer);
|
void sse_encode_rust_utils(RustUtils self, SseSerializer serializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_server_result_empty(
|
void sse_encode_setup_profile(SetupProfile self, SseSerializer serializer);
|
||||||
ServerResultEmpty self,
|
|
||||||
SseSerializer serializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
void sse_encode_server_result_i_64(
|
|
||||||
ServerResultI64 self,
|
|
||||||
SseSerializer serializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
void sse_encode_server_result_vec_u_8(
|
|
||||||
ServerResultVecU8 self,
|
|
||||||
SseSerializer serializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_sql_execution_result(
|
void sse_encode_sql_execution_result(
|
||||||
|
|
@ -1110,6 +1288,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_sql_value(SqlValue self, SseSerializer serializer);
|
void sse_encode_sql_value(SqlValue self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_theme_mode(ThemeMode self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_twonly_safe_backup(
|
||||||
|
TwonlySafeBackup self,
|
||||||
|
SseSerializer serializer,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_u_32(int self, SseSerializer serializer);
|
void sse_encode_u_32(int self, SseSerializer serializer);
|
||||||
|
|
||||||
|
|
@ -1125,6 +1312,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_unit(void self, SseSerializer serializer);
|
void sse_encode_unit(void self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_user_config(UserConfig self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_user_config_api(UserConfigApi self, SseSerializer serializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_usize(BigInt self, SseSerializer serializer);
|
void sse_encode_usize(BigInt self, SseSerializer serializer);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import 'bridge.dart';
|
||||||
import 'bridge/api.dart';
|
import 'bridge/api.dart';
|
||||||
import 'bridge/callbacks.dart';
|
import 'bridge/callbacks.dart';
|
||||||
import 'bridge/groups.dart';
|
import 'bridge/groups.dart';
|
||||||
|
import 'bridge/user_config.dart';
|
||||||
import 'bridge/wrapper.dart';
|
import 'bridge/wrapper.dart';
|
||||||
import 'bridge/wrapper/app_database.dart';
|
import 'bridge/wrapper/app_database.dart';
|
||||||
import 'bridge/wrapper/backup.dart';
|
import 'bridge/wrapper/backup.dart';
|
||||||
|
|
@ -25,6 +26,7 @@ import 'keys/backup_password_keys.dart';
|
||||||
import 'lib.dart';
|
import 'lib.dart';
|
||||||
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_web.dart';
|
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_web.dart';
|
||||||
import 'signal/engine.dart';
|
import 'signal/engine.dart';
|
||||||
|
import 'user_config.dart';
|
||||||
|
|
||||||
abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
RustLibApiImplPlatform({
|
RustLibApiImplPlatform({
|
||||||
|
|
@ -37,16 +39,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
AnyhowException dco_decode_AnyhowException(dynamic raw);
|
AnyhowException dco_decode_AnyhowException(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
DateTime dco_decode_Chrono_Utc(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
FutureOr<void> Function(String, String, PlatformInt64, String)
|
FutureOr<void> Function(String, String, PlatformInt64, String)
|
||||||
dco_decode_DartFn_Inputs_String_String_i_64_String_Output_unit_AnyhowException(
|
dco_decode_DartFn_Inputs_String_String_i_64_String_Output_unit_AnyhowException(
|
||||||
dynamic raw,
|
dynamic raw,
|
||||||
);
|
);
|
||||||
|
|
||||||
@protected
|
|
||||||
FutureOr<void> Function(String, bool)
|
|
||||||
dco_decode_DartFn_Inputs_String_bool_Output_unit_AnyhowException(dynamic raw);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
FutureOr<void> Function(String, PlatformInt64)
|
FutureOr<void> Function(String, PlatformInt64)
|
||||||
dco_decode_DartFn_Inputs_String_i_64_Output_unit_AnyhowException(dynamic raw);
|
dco_decode_DartFn_Inputs_String_i_64_Output_unit_AnyhowException(dynamic raw);
|
||||||
|
|
@ -63,20 +64,10 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
dynamic raw,
|
dynamic raw,
|
||||||
);
|
);
|
||||||
|
|
||||||
@protected
|
|
||||||
FutureOr<void> Function()
|
|
||||||
dco_decode_DartFn_Inputs__Output_unit_AnyhowException(dynamic raw);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
FutureOr<void> Function(PlatformInt64)
|
FutureOr<void> Function(PlatformInt64)
|
||||||
dco_decode_DartFn_Inputs_i_64_Output_unit_AnyhowException(dynamic raw);
|
dco_decode_DartFn_Inputs_i_64_Output_unit_AnyhowException(dynamic raw);
|
||||||
|
|
||||||
@protected
|
|
||||||
FutureOr<void> Function(PlatformInt64, String, String)
|
|
||||||
dco_decode_DartFn_Inputs_i_64_String_String_Output_unit_AnyhowException(
|
|
||||||
dynamic raw,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
FutureOr<LegacySignalEncryptResult?> Function(PlatformInt64, Uint8List)
|
FutureOr<LegacySignalEncryptResult?> Function(PlatformInt64, Uint8List)
|
||||||
dco_decode_DartFn_Inputs_i_64_list_prim_u_8_strict_Output_opt_box_autoadd_legacy_signal_encrypt_result_AnyhowException(
|
dco_decode_DartFn_Inputs_i_64_list_prim_u_8_strict_Output_opt_box_autoadd_legacy_signal_encrypt_result_AnyhowException(
|
||||||
|
|
@ -96,14 +87,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
);
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
FutureOr<Uint8List?> Function(PlatformInt64, String?, Uint8List, int)
|
FutureOr<void> Function(UserConfig)
|
||||||
dco_decode_DartFn_Inputs_i_64_opt_String_list_prim_u_8_strict_i_32_Output_opt_list_prim_u_8_strict_AnyhowException(
|
dco_decode_DartFn_Inputs_user_config_Output_unit_AnyhowException(dynamic raw);
|
||||||
dynamic raw,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
Object dco_decode_DartOpaque(dynamic raw);
|
Object dco_decode_DartOpaque(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
Map<String, List<String>> dco_decode_Map_String_list_String_None(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
Map<PlatformInt64, Uint8List> dco_decode_Map_i_64_list_prim_u_8_strict_None(
|
Map<PlatformInt64, Uint8List> dco_decode_Map_i_64_list_prim_u_8_strict_None(
|
||||||
dynamic raw,
|
dynamic raw,
|
||||||
|
|
@ -133,6 +125,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
bool dco_decode_bool(dynamic raw);
|
bool dco_decode_bool(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
DateTime dco_decode_box_autoadd_Chrono_Utc(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
ApiConnectionState dco_decode_box_autoadd_api_connection_state(dynamic raw);
|
ApiConnectionState dco_decode_box_autoadd_api_connection_state(dynamic raw);
|
||||||
|
|
||||||
|
|
@ -159,14 +154,24 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
dynamic raw,
|
dynamic raw,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
PasswordlessRecoveryConfig
|
||||||
|
dco_decode_box_autoadd_passwordless_recovery_config(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
PreparedOutgoingMessage dco_decode_box_autoadd_prepared_outgoing_message(
|
PreparedOutgoingMessage dco_decode_box_autoadd_prepared_outgoing_message(
|
||||||
dynamic raw,
|
dynamic raw,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
TwonlySafeBackup dco_decode_box_autoadd_twonly_safe_backup(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
int dco_decode_box_autoadd_u_32(dynamic raw);
|
int dco_decode_box_autoadd_u_32(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
UserConfig dco_decode_box_autoadd_user_config(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
double dco_decode_f_64(dynamic raw);
|
double dco_decode_f_64(dynamic raw);
|
||||||
|
|
||||||
|
|
@ -191,6 +196,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
PlatformInt64 dco_decode_isize(dynamic raw);
|
PlatformInt64 dco_decode_isize(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
LastBackupUploadState dco_decode_last_backup_upload_state(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
LegacyMigrationReport dco_decode_legacy_migration_report(dynamic raw);
|
LegacyMigrationReport dco_decode_legacy_migration_report(dynamic raw);
|
||||||
|
|
||||||
|
|
@ -245,15 +253,28 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
List<(PlatformInt64, Uint8List)>
|
List<(PlatformInt64, Uint8List)>
|
||||||
dco_decode_list_record_i_64_list_prim_u_8_strict(dynamic raw);
|
dco_decode_list_record_i_64_list_prim_u_8_strict(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
List<(String, List<String>)> dco_decode_list_record_string_list_string(
|
||||||
|
dynamic raw,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
List<SqlRow> dco_decode_list_sql_row(dynamic raw);
|
List<SqlRow> dco_decode_list_sql_row(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
List<SqlValue> dco_decode_list_sql_value(dynamic raw);
|
List<SqlValue> dco_decode_list_sql_value(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
Map<String, List<String>>? dco_decode_opt_Map_String_list_String_None(
|
||||||
|
dynamic raw,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
String? dco_decode_opt_String(dynamic raw);
|
String? dco_decode_opt_String(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
DateTime? dco_decode_opt_box_autoadd_Chrono_Utc(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
ApiConnectionState? dco_decode_opt_box_autoadd_api_connection_state(
|
ApiConnectionState? dco_decode_opt_box_autoadd_api_connection_state(
|
||||||
dynamic raw,
|
dynamic raw,
|
||||||
|
|
@ -272,17 +293,35 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
LegacySignalEncryptResult?
|
LegacySignalEncryptResult?
|
||||||
dco_decode_opt_box_autoadd_legacy_signal_encrypt_result(dynamic raw);
|
dco_decode_opt_box_autoadd_legacy_signal_encrypt_result(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
PasswordlessRecoveryConfig?
|
||||||
|
dco_decode_opt_box_autoadd_passwordless_recovery_config(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
PreparedOutgoingMessage? dco_decode_opt_box_autoadd_prepared_outgoing_message(
|
PreparedOutgoingMessage? dco_decode_opt_box_autoadd_prepared_outgoing_message(
|
||||||
dynamic raw,
|
dynamic raw,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
TwonlySafeBackup? dco_decode_opt_box_autoadd_twonly_safe_backup(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
int? dco_decode_opt_box_autoadd_u_32(dynamic raw);
|
int? dco_decode_opt_box_autoadd_u_32(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
UserConfig? dco_decode_opt_box_autoadd_user_config(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
List<String>? dco_decode_opt_list_String(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
Uint8List? dco_decode_opt_list_prim_u_8_strict(dynamic raw);
|
Uint8List? dco_decode_opt_list_prim_u_8_strict(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
PasswordlessRecoveryConfig dco_decode_passwordless_recovery_config(
|
||||||
|
dynamic raw,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
PqcPreKeyInput dco_decode_pqc_pre_key_input(dynamic raw);
|
PqcPreKeyInput dco_decode_pqc_pre_key_input(dynamic raw);
|
||||||
|
|
||||||
|
|
@ -299,6 +338,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
dynamic raw,
|
dynamic raw,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
(String, List<String>) dco_decode_record_string_list_string(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
(String, String) dco_decode_record_string_string(dynamic raw);
|
(String, String) dco_decode_record_string_string(dynamic raw);
|
||||||
|
|
||||||
|
|
@ -324,13 +366,7 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
RustUtils dco_decode_rust_utils(dynamic raw);
|
RustUtils dco_decode_rust_utils(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
ServerResultEmpty dco_decode_server_result_empty(dynamic raw);
|
SetupProfile dco_decode_setup_profile(dynamic raw);
|
||||||
|
|
||||||
@protected
|
|
||||||
ServerResultI64 dco_decode_server_result_i_64(dynamic raw);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
ServerResultVecU8 dco_decode_server_result_vec_u_8(dynamic raw);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
SqlExecutionResult dco_decode_sql_execution_result(dynamic raw);
|
SqlExecutionResult dco_decode_sql_execution_result(dynamic raw);
|
||||||
|
|
@ -344,6 +380,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
SqlValue dco_decode_sql_value(dynamic raw);
|
SqlValue dco_decode_sql_value(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
ThemeMode dco_decode_theme_mode(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
TwonlySafeBackup dco_decode_twonly_safe_backup(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
int dco_decode_u_32(dynamic raw);
|
int dco_decode_u_32(dynamic raw);
|
||||||
|
|
||||||
|
|
@ -359,15 +401,29 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
void dco_decode_unit(dynamic raw);
|
void dco_decode_unit(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
UserConfig dco_decode_user_config(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
UserConfigApi dco_decode_user_config_api(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
BigInt dco_decode_usize(dynamic raw);
|
BigInt dco_decode_usize(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer);
|
AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
DateTime sse_decode_Chrono_Utc(SseDeserializer deserializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
Object sse_decode_DartOpaque(SseDeserializer deserializer);
|
Object sse_decode_DartOpaque(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
Map<String, List<String>> sse_decode_Map_String_list_String_None(
|
||||||
|
SseDeserializer deserializer,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
Map<PlatformInt64, Uint8List> sse_decode_Map_i_64_list_prim_u_8_strict_None(
|
Map<PlatformInt64, Uint8List> sse_decode_Map_i_64_list_prim_u_8_strict_None(
|
||||||
SseDeserializer deserializer,
|
SseDeserializer deserializer,
|
||||||
|
|
@ -405,6 +461,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
bool sse_decode_bool(SseDeserializer deserializer);
|
bool sse_decode_bool(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
DateTime sse_decode_box_autoadd_Chrono_Utc(SseDeserializer deserializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
ApiConnectionState sse_decode_box_autoadd_api_connection_state(
|
ApiConnectionState sse_decode_box_autoadd_api_connection_state(
|
||||||
SseDeserializer deserializer,
|
SseDeserializer deserializer,
|
||||||
|
|
@ -437,14 +496,28 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
SseDeserializer deserializer,
|
SseDeserializer deserializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
PasswordlessRecoveryConfig
|
||||||
|
sse_decode_box_autoadd_passwordless_recovery_config(
|
||||||
|
SseDeserializer deserializer,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
PreparedOutgoingMessage sse_decode_box_autoadd_prepared_outgoing_message(
|
PreparedOutgoingMessage sse_decode_box_autoadd_prepared_outgoing_message(
|
||||||
SseDeserializer deserializer,
|
SseDeserializer deserializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
TwonlySafeBackup sse_decode_box_autoadd_twonly_safe_backup(
|
||||||
|
SseDeserializer deserializer,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
int sse_decode_box_autoadd_u_32(SseDeserializer deserializer);
|
int sse_decode_box_autoadd_u_32(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
UserConfig sse_decode_box_autoadd_user_config(SseDeserializer deserializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
double sse_decode_f_64(SseDeserializer deserializer);
|
double sse_decode_f_64(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
|
@ -471,6 +544,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
PlatformInt64 sse_decode_isize(SseDeserializer deserializer);
|
PlatformInt64 sse_decode_isize(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
LastBackupUploadState sse_decode_last_backup_upload_state(
|
||||||
|
SseDeserializer deserializer,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
LegacyMigrationReport sse_decode_legacy_migration_report(
|
LegacyMigrationReport sse_decode_legacy_migration_report(
|
||||||
SseDeserializer deserializer,
|
SseDeserializer deserializer,
|
||||||
|
|
@ -539,15 +617,28 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
SseDeserializer deserializer,
|
SseDeserializer deserializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
List<(String, List<String>)> sse_decode_list_record_string_list_string(
|
||||||
|
SseDeserializer deserializer,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
List<SqlRow> sse_decode_list_sql_row(SseDeserializer deserializer);
|
List<SqlRow> sse_decode_list_sql_row(SseDeserializer deserializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
List<SqlValue> sse_decode_list_sql_value(SseDeserializer deserializer);
|
List<SqlValue> sse_decode_list_sql_value(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
Map<String, List<String>>? sse_decode_opt_Map_String_list_String_None(
|
||||||
|
SseDeserializer deserializer,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
String? sse_decode_opt_String(SseDeserializer deserializer);
|
String? sse_decode_opt_String(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
DateTime? sse_decode_opt_box_autoadd_Chrono_Utc(SseDeserializer deserializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
ApiConnectionState? sse_decode_opt_box_autoadd_api_connection_state(
|
ApiConnectionState? sse_decode_opt_box_autoadd_api_connection_state(
|
||||||
SseDeserializer deserializer,
|
SseDeserializer deserializer,
|
||||||
|
|
@ -568,17 +659,41 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
SseDeserializer deserializer,
|
SseDeserializer deserializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
PasswordlessRecoveryConfig?
|
||||||
|
sse_decode_opt_box_autoadd_passwordless_recovery_config(
|
||||||
|
SseDeserializer deserializer,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
PreparedOutgoingMessage? sse_decode_opt_box_autoadd_prepared_outgoing_message(
|
PreparedOutgoingMessage? sse_decode_opt_box_autoadd_prepared_outgoing_message(
|
||||||
SseDeserializer deserializer,
|
SseDeserializer deserializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
TwonlySafeBackup? sse_decode_opt_box_autoadd_twonly_safe_backup(
|
||||||
|
SseDeserializer deserializer,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
int? sse_decode_opt_box_autoadd_u_32(SseDeserializer deserializer);
|
int? sse_decode_opt_box_autoadd_u_32(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
UserConfig? sse_decode_opt_box_autoadd_user_config(
|
||||||
|
SseDeserializer deserializer,
|
||||||
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
List<String>? sse_decode_opt_list_String(SseDeserializer deserializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
Uint8List? sse_decode_opt_list_prim_u_8_strict(SseDeserializer deserializer);
|
Uint8List? sse_decode_opt_list_prim_u_8_strict(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
PasswordlessRecoveryConfig sse_decode_passwordless_recovery_config(
|
||||||
|
SseDeserializer deserializer,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
PqcPreKeyInput sse_decode_pqc_pre_key_input(SseDeserializer deserializer);
|
PqcPreKeyInput sse_decode_pqc_pre_key_input(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
|
@ -597,6 +712,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
SseDeserializer deserializer,
|
SseDeserializer deserializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
(String, List<String>) sse_decode_record_string_list_string(
|
||||||
|
SseDeserializer deserializer,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
(String, String) sse_decode_record_string_string(
|
(String, String) sse_decode_record_string_string(
|
||||||
SseDeserializer deserializer,
|
SseDeserializer deserializer,
|
||||||
|
|
@ -628,17 +748,7 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
RustUtils sse_decode_rust_utils(SseDeserializer deserializer);
|
RustUtils sse_decode_rust_utils(SseDeserializer deserializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
ServerResultEmpty sse_decode_server_result_empty(
|
SetupProfile sse_decode_setup_profile(SseDeserializer deserializer);
|
||||||
SseDeserializer deserializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
ServerResultI64 sse_decode_server_result_i_64(SseDeserializer deserializer);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
ServerResultVecU8 sse_decode_server_result_vec_u_8(
|
|
||||||
SseDeserializer deserializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
SqlExecutionResult sse_decode_sql_execution_result(
|
SqlExecutionResult sse_decode_sql_execution_result(
|
||||||
|
|
@ -654,6 +764,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
SqlValue sse_decode_sql_value(SseDeserializer deserializer);
|
SqlValue sse_decode_sql_value(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
ThemeMode sse_decode_theme_mode(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
TwonlySafeBackup sse_decode_twonly_safe_backup(SseDeserializer deserializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
int sse_decode_u_32(SseDeserializer deserializer);
|
int sse_decode_u_32(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
|
@ -669,6 +785,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
void sse_decode_unit(SseDeserializer deserializer);
|
void sse_decode_unit(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
UserConfig sse_decode_user_config(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
UserConfigApi sse_decode_user_config_api(SseDeserializer deserializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
BigInt sse_decode_usize(SseDeserializer deserializer);
|
BigInt sse_decode_usize(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
|
@ -678,6 +800,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
SseSerializer serializer,
|
SseSerializer serializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_Chrono_Utc(DateTime self, SseSerializer serializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void
|
void
|
||||||
sse_encode_DartFn_Inputs_String_String_i_64_String_Output_unit_AnyhowException(
|
sse_encode_DartFn_Inputs_String_String_i_64_String_Output_unit_AnyhowException(
|
||||||
|
|
@ -685,12 +810,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
SseSerializer serializer,
|
SseSerializer serializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
@protected
|
|
||||||
void sse_encode_DartFn_Inputs_String_bool_Output_unit_AnyhowException(
|
|
||||||
FutureOr<void> Function(String, bool) self,
|
|
||||||
SseSerializer serializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_DartFn_Inputs_String_i_64_Output_unit_AnyhowException(
|
void sse_encode_DartFn_Inputs_String_i_64_Output_unit_AnyhowException(
|
||||||
FutureOr<void> Function(String, PlatformInt64) self,
|
FutureOr<void> Function(String, PlatformInt64) self,
|
||||||
|
|
@ -710,24 +829,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
SseSerializer serializer,
|
SseSerializer serializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
@protected
|
|
||||||
void sse_encode_DartFn_Inputs__Output_unit_AnyhowException(
|
|
||||||
FutureOr<void> Function() self,
|
|
||||||
SseSerializer serializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_DartFn_Inputs_i_64_Output_unit_AnyhowException(
|
void sse_encode_DartFn_Inputs_i_64_Output_unit_AnyhowException(
|
||||||
FutureOr<void> Function(PlatformInt64) self,
|
FutureOr<void> Function(PlatformInt64) self,
|
||||||
SseSerializer serializer,
|
SseSerializer serializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
@protected
|
|
||||||
void sse_encode_DartFn_Inputs_i_64_String_String_Output_unit_AnyhowException(
|
|
||||||
FutureOr<void> Function(PlatformInt64, String, String) self,
|
|
||||||
SseSerializer serializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void
|
void
|
||||||
sse_encode_DartFn_Inputs_i_64_list_prim_u_8_strict_Output_opt_box_autoadd_legacy_signal_encrypt_result_AnyhowException(
|
sse_encode_DartFn_Inputs_i_64_list_prim_u_8_strict_Output_opt_box_autoadd_legacy_signal_encrypt_result_AnyhowException(
|
||||||
|
|
@ -752,15 +859,20 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
);
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void
|
void sse_encode_DartFn_Inputs_user_config_Output_unit_AnyhowException(
|
||||||
sse_encode_DartFn_Inputs_i_64_opt_String_list_prim_u_8_strict_i_32_Output_opt_list_prim_u_8_strict_AnyhowException(
|
FutureOr<void> Function(UserConfig) self,
|
||||||
FutureOr<Uint8List?> Function(PlatformInt64, String?, Uint8List, int) self,
|
|
||||||
SseSerializer serializer,
|
SseSerializer serializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_DartOpaque(Object self, SseSerializer serializer);
|
void sse_encode_DartOpaque(Object self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_Map_String_list_String_None(
|
||||||
|
Map<String, List<String>> self,
|
||||||
|
SseSerializer serializer,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_Map_i_64_list_prim_u_8_strict_None(
|
void sse_encode_Map_i_64_list_prim_u_8_strict_None(
|
||||||
Map<PlatformInt64, Uint8List> self,
|
Map<PlatformInt64, Uint8List> self,
|
||||||
|
|
@ -803,6 +915,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_bool(bool self, SseSerializer serializer);
|
void sse_encode_bool(bool self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_box_autoadd_Chrono_Utc(
|
||||||
|
DateTime self,
|
||||||
|
SseSerializer serializer,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_box_autoadd_api_connection_state(
|
void sse_encode_box_autoadd_api_connection_state(
|
||||||
ApiConnectionState self,
|
ApiConnectionState self,
|
||||||
|
|
@ -845,15 +963,33 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
SseSerializer serializer,
|
SseSerializer serializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_box_autoadd_passwordless_recovery_config(
|
||||||
|
PasswordlessRecoveryConfig self,
|
||||||
|
SseSerializer serializer,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_box_autoadd_prepared_outgoing_message(
|
void sse_encode_box_autoadd_prepared_outgoing_message(
|
||||||
PreparedOutgoingMessage self,
|
PreparedOutgoingMessage self,
|
||||||
SseSerializer serializer,
|
SseSerializer serializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_box_autoadd_twonly_safe_backup(
|
||||||
|
TwonlySafeBackup self,
|
||||||
|
SseSerializer serializer,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_box_autoadd_u_32(int self, SseSerializer serializer);
|
void sse_encode_box_autoadd_u_32(int self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_box_autoadd_user_config(
|
||||||
|
UserConfig self,
|
||||||
|
SseSerializer serializer,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_f_64(double self, SseSerializer serializer);
|
void sse_encode_f_64(double self, SseSerializer serializer);
|
||||||
|
|
||||||
|
|
@ -884,6 +1020,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_isize(PlatformInt64 self, SseSerializer serializer);
|
void sse_encode_isize(PlatformInt64 self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_last_backup_upload_state(
|
||||||
|
LastBackupUploadState self,
|
||||||
|
SseSerializer serializer,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_legacy_migration_report(
|
void sse_encode_legacy_migration_report(
|
||||||
LegacyMigrationReport self,
|
LegacyMigrationReport self,
|
||||||
|
|
@ -968,15 +1110,33 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
SseSerializer serializer,
|
SseSerializer serializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_list_record_string_list_string(
|
||||||
|
List<(String, List<String>)> self,
|
||||||
|
SseSerializer serializer,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_list_sql_row(List<SqlRow> self, SseSerializer serializer);
|
void sse_encode_list_sql_row(List<SqlRow> self, SseSerializer serializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_list_sql_value(List<SqlValue> self, SseSerializer serializer);
|
void sse_encode_list_sql_value(List<SqlValue> self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_opt_Map_String_list_String_None(
|
||||||
|
Map<String, List<String>>? self,
|
||||||
|
SseSerializer serializer,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_opt_String(String? self, SseSerializer serializer);
|
void sse_encode_opt_String(String? self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_opt_box_autoadd_Chrono_Utc(
|
||||||
|
DateTime? self,
|
||||||
|
SseSerializer serializer,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_opt_box_autoadd_api_connection_state(
|
void sse_encode_opt_box_autoadd_api_connection_state(
|
||||||
ApiConnectionState? self,
|
ApiConnectionState? self,
|
||||||
|
|
@ -1001,21 +1161,48 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
SseSerializer serializer,
|
SseSerializer serializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_opt_box_autoadd_passwordless_recovery_config(
|
||||||
|
PasswordlessRecoveryConfig? self,
|
||||||
|
SseSerializer serializer,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_opt_box_autoadd_prepared_outgoing_message(
|
void sse_encode_opt_box_autoadd_prepared_outgoing_message(
|
||||||
PreparedOutgoingMessage? self,
|
PreparedOutgoingMessage? self,
|
||||||
SseSerializer serializer,
|
SseSerializer serializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_opt_box_autoadd_twonly_safe_backup(
|
||||||
|
TwonlySafeBackup? self,
|
||||||
|
SseSerializer serializer,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_opt_box_autoadd_u_32(int? self, SseSerializer serializer);
|
void sse_encode_opt_box_autoadd_u_32(int? self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_opt_box_autoadd_user_config(
|
||||||
|
UserConfig? self,
|
||||||
|
SseSerializer serializer,
|
||||||
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_opt_list_String(List<String>? self, SseSerializer serializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_opt_list_prim_u_8_strict(
|
void sse_encode_opt_list_prim_u_8_strict(
|
||||||
Uint8List? self,
|
Uint8List? self,
|
||||||
SseSerializer serializer,
|
SseSerializer serializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_passwordless_recovery_config(
|
||||||
|
PasswordlessRecoveryConfig self,
|
||||||
|
SseSerializer serializer,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_pqc_pre_key_input(
|
void sse_encode_pqc_pre_key_input(
|
||||||
PqcPreKeyInput self,
|
PqcPreKeyInput self,
|
||||||
|
|
@ -1040,6 +1227,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
SseSerializer serializer,
|
SseSerializer serializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_record_string_list_string(
|
||||||
|
(String, List<String>) self,
|
||||||
|
SseSerializer serializer,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_record_string_string(
|
void sse_encode_record_string_string(
|
||||||
(String, String) self,
|
(String, String) self,
|
||||||
|
|
@ -1080,22 +1273,7 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
void sse_encode_rust_utils(RustUtils self, SseSerializer serializer);
|
void sse_encode_rust_utils(RustUtils self, SseSerializer serializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_server_result_empty(
|
void sse_encode_setup_profile(SetupProfile self, SseSerializer serializer);
|
||||||
ServerResultEmpty self,
|
|
||||||
SseSerializer serializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
void sse_encode_server_result_i_64(
|
|
||||||
ServerResultI64 self,
|
|
||||||
SseSerializer serializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
void sse_encode_server_result_vec_u_8(
|
|
||||||
ServerResultVecU8 self,
|
|
||||||
SseSerializer serializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_sql_execution_result(
|
void sse_encode_sql_execution_result(
|
||||||
|
|
@ -1112,6 +1290,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_sql_value(SqlValue self, SseSerializer serializer);
|
void sse_encode_sql_value(SqlValue self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_theme_mode(ThemeMode self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_twonly_safe_backup(
|
||||||
|
TwonlySafeBackup self,
|
||||||
|
SseSerializer serializer,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_u_32(int self, SseSerializer serializer);
|
void sse_encode_u_32(int self, SseSerializer serializer);
|
||||||
|
|
||||||
|
|
@ -1127,6 +1314,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_unit(void self, SseSerializer serializer);
|
void sse_encode_unit(void self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_user_config(UserConfig self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_user_config_api(UserConfigApi self, SseSerializer serializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_usize(BigInt self, SseSerializer serializer);
|
void sse_encode_usize(BigInt self, SseSerializer serializer);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
357
lib/core/user_config.dart
Normal file
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/news.service.dart';
|
||||||
import 'package:twonly/src/services/user.service.dart';
|
import 'package:twonly/src/services/user.service.dart';
|
||||||
|
|
||||||
|
export 'package:twonly/core/bridge/api.dart';
|
||||||
|
export 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
|
||||||
|
export 'package:twonly/src/services/api/rust_api_result.dart';
|
||||||
|
|
||||||
final GetIt locator = GetIt.instance;
|
final GetIt locator = GetIt.instance;
|
||||||
|
|
||||||
void setupLocator() {
|
void setupLocator() {
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,6 @@ import 'package:twonly/src/services/memories/memories.service.dart';
|
||||||
import 'package:twonly/src/services/migrations.service.dart';
|
import 'package:twonly/src/services/migrations.service.dart';
|
||||||
import 'package:twonly/src/services/notifications/fcm.notifications.dart';
|
import 'package:twonly/src/services/notifications/fcm.notifications.dart';
|
||||||
import 'package:twonly/src/services/notifications/setup.notifications.dart';
|
import 'package:twonly/src/services/notifications/setup.notifications.dart';
|
||||||
import 'package:twonly/src/services/user_discovery.service.dart';
|
|
||||||
import 'package:twonly/src/utils/avatars.dart';
|
import 'package:twonly/src/utils/avatars.dart';
|
||||||
import 'package:twonly/src/utils/exclusive_access.utils.dart';
|
import 'package:twonly/src/utils/exclusive_access.utils.dart';
|
||||||
import 'package:twonly/src/utils/log.dart';
|
import 'package:twonly/src/utils/log.dart';
|
||||||
|
|
@ -204,8 +203,6 @@ Future<void> postStartupTasks() async {
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
unawaited(UserDiscoveryService.verifyInitializationOnStartup());
|
|
||||||
|
|
||||||
await Future.delayed(const Duration(seconds: 10));
|
await Future.delayed(const Duration(seconds: 10));
|
||||||
unawaited(initializeBackgroundTaskManager());
|
unawaited(initializeBackgroundTaskManager());
|
||||||
// 3. Delayed tasks (Wait for app to settle)
|
// 3. Delayed tasks (Wait for app to settle)
|
||||||
|
|
|
||||||
|
|
@ -1,65 +1,25 @@
|
||||||
import 'dart:typed_data';
|
|
||||||
|
|
||||||
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
|
|
||||||
import 'package:twonly/core/bridge/callbacks.dart';
|
import 'package:twonly/core/bridge/callbacks.dart';
|
||||||
import 'package:twonly/globals.dart';
|
import 'package:twonly/globals.dart';
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/callbacks/legacy_signal.callbacks.dart';
|
import 'package:twonly/src/callbacks/legacy_signal.callbacks.dart';
|
||||||
import 'package:twonly/src/callbacks/logging.callbacks.dart';
|
import 'package:twonly/src/callbacks/logging.callbacks.dart';
|
||||||
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'
|
|
||||||
as pb;
|
|
||||||
import 'package:twonly/src/model/protobuf/client/generated/push_notification.pb.dart'
|
|
||||||
as push_pb;
|
|
||||||
import 'package:twonly/src/services/api/client2client/errors.c2c.dart';
|
|
||||||
import 'package:twonly/src/services/api/mediafiles/download.api.dart';
|
|
||||||
import 'package:twonly/src/services/api/mediafiles/upload.api.dart';
|
import 'package:twonly/src/services/api/mediafiles/upload.api.dart';
|
||||||
import 'package:twonly/src/services/api/messages.api.dart';
|
|
||||||
import 'package:twonly/src/services/flame.service.dart';
|
import 'package:twonly/src/services/flame.service.dart';
|
||||||
import 'package:twonly/src/services/group.service.dart';
|
|
||||||
import 'package:twonly/src/services/key_verification.service.dart';
|
import 'package:twonly/src/services/key_verification.service.dart';
|
||||||
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
||||||
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
|
|
||||||
import 'package:twonly/src/services/passwordless_recovery.service.dart';
|
|
||||||
import 'package:twonly/src/services/signal/session.signal.dart';
|
import 'package:twonly/src/services/signal/session.signal.dart';
|
||||||
|
import 'package:twonly/src/services/user.service.dart';
|
||||||
import 'package:twonly/src/utils/avatars.dart';
|
import 'package:twonly/src/utils/avatars.dart';
|
||||||
|
|
||||||
Future<Uint8List?> _apiCreatePushData(
|
|
||||||
int contactId,
|
|
||||||
String? messageId,
|
|
||||||
Uint8List plaintext,
|
|
||||||
int messageType,
|
|
||||||
) async {
|
|
||||||
final notification = messageType == pb.Message_Type.TEST_NOTIFICATION.value
|
|
||||||
? push_pb.PushNotification(kind: push_pb.PushKind.TEST_NOTIFICATION)
|
|
||||||
: await getPushNotificationFromEncryptedContent(
|
|
||||||
contactId,
|
|
||||||
messageId,
|
|
||||||
pb.EncryptedContent.fromBuffer(plaintext),
|
|
||||||
);
|
|
||||||
if (notification == null) return null;
|
|
||||||
return encryptPushNotification(contactId, notification);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _apiMediaAction(
|
Future<void> _apiMediaAction(
|
||||||
String kind,
|
String kind,
|
||||||
String mediaId,
|
String mediaId,
|
||||||
int contactId,
|
int contactId,
|
||||||
String messageId,
|
String messageId,
|
||||||
) async {
|
) async {
|
||||||
if (kind == 'response') {
|
|
||||||
await handleMediaRelatedResponseFromReceiver(messageId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (kind == 'delete') {
|
|
||||||
final media = await MediaFileService.fromMediaId(mediaId);
|
|
||||||
media?.fullMediaRemoval();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final media = await twonlyDB.mediaFilesDao.getMediaFileById(mediaId);
|
final media = await twonlyDB.mediaFilesDao.getMediaFileById(mediaId);
|
||||||
if (media == null) return;
|
if (media == null) return;
|
||||||
switch (kind) {
|
switch (kind) {
|
||||||
case 'download':
|
|
||||||
await startDownloadMedia(media, false);
|
|
||||||
case 'stored':
|
case 'stored':
|
||||||
await MediaFileService(media).storeMediaFile();
|
await MediaFileService(media).storeMediaFile();
|
||||||
case 'reupload':
|
case 'reupload':
|
||||||
|
|
@ -67,26 +27,6 @@ Future<void> _apiMediaAction(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _apiGroupStateRefresh(String groupId, bool created) async {
|
|
||||||
if (created) {
|
|
||||||
await fetchGroupStatesForUnjoinedGroups();
|
|
||||||
final group = await twonlyDB.groupsDao.getGroup(groupId);
|
|
||||||
if (group?.myGroupPrivateKey == null) return;
|
|
||||||
final key = IdentityKeyPair.fromSerialized(group!.myGroupPrivateKey!);
|
|
||||||
await sendCipherTextToGroup(
|
|
||||||
groupId,
|
|
||||||
pb.EncryptedContent(
|
|
||||||
groupJoin: pb.EncryptedContent_GroupJoin(
|
|
||||||
groupPublicKey: key.getPublicKey().serialize(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final group = await twonlyDB.groupsDao.getGroup(groupId);
|
|
||||||
if (group != null) await fetchGroupState(group);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> initFlutterCallbacksForRust() async {
|
Future<void> initFlutterCallbacksForRust() async {
|
||||||
await initFlutterCallbacks(
|
await initFlutterCallbacks(
|
||||||
callbackId: isolateCallbackId,
|
callbackId: isolateCallbackId,
|
||||||
|
|
@ -95,31 +35,15 @@ Future<void> initFlutterCallbacksForRust() async {
|
||||||
legacySignalEncrypt: LegacySignalCallbacks.encrypt,
|
legacySignalEncrypt: LegacySignalCallbacks.encrypt,
|
||||||
legacySignalGeneratePrekeys: LegacySignalCallbacks.generatePrekeys,
|
legacySignalGeneratePrekeys: LegacySignalCallbacks.generatePrekeys,
|
||||||
apiResyncSignalSession: handleSessionResync,
|
apiResyncSignalSession: handleSessionResync,
|
||||||
apiPushKeyRequested: (contactId) =>
|
|
||||||
setupNotificationWithUsers(forceContact: contactId),
|
|
||||||
apiGroupMembershipError: (contactId, groupId, relatedReceiptId) =>
|
|
||||||
handleErrorMessage(
|
|
||||||
contactId,
|
|
||||||
pb.EncryptedContent_ErrorMessages(
|
|
||||||
type: pb
|
|
||||||
.EncryptedContent_ErrorMessages_Type
|
|
||||||
.GROUP_NOT_FOUND_OR_NOT_A_MEMBER,
|
|
||||||
relatedReceiptId: relatedReceiptId,
|
|
||||||
),
|
|
||||||
relatedReceiptId,
|
|
||||||
groupId: groupId,
|
|
||||||
),
|
|
||||||
apiMediaAction: _apiMediaAction,
|
apiMediaAction: _apiMediaAction,
|
||||||
apiVerificationProof: KeyVerificationService.handleVerificationProof,
|
apiVerificationProof: KeyVerificationService.handleVerificationProof,
|
||||||
apiCreatePushData: _apiCreatePushData,
|
|
||||||
apiCreatePushAvatars: (contactId) =>
|
apiCreatePushAvatars: (contactId) =>
|
||||||
createPushAvatars(forceForUserId: contactId),
|
createPushAvatars(forceForUserId: contactId),
|
||||||
apiRecoveryChanged: PasswordlessRecoveryService.performHeartbeat,
|
|
||||||
apiMediaReceived: (groupId, timestamp) => incFlameCounter(
|
apiMediaReceived: (groupId, timestamp) => incFlameCounter(
|
||||||
groupId,
|
groupId,
|
||||||
true,
|
true,
|
||||||
DateTime.fromMillisecondsSinceEpoch(timestamp),
|
DateTime.fromMillisecondsSinceEpoch(timestamp),
|
||||||
),
|
),
|
||||||
apiGroupStateRefresh: _apiGroupStateRefresh,
|
apiUserConfigChanged: UserService.handleRustUserConfigChanged,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import 'package:drift/drift.dart';
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/database/tables/contacts.table.dart';
|
import 'package:twonly/src/database/tables/contacts.table.dart';
|
||||||
import 'package:twonly/src/database/twonly.db.dart';
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
|
|
||||||
import 'package:twonly/src/utils/log.dart';
|
import 'package:twonly/src/utils/log.dart';
|
||||||
|
|
||||||
part 'contacts.dao.g.dart';
|
part 'contacts.dao.g.dart';
|
||||||
|
|
@ -86,7 +85,6 @@ class ContactsDao extends DatabaseAccessor<TwonlyDB> with _$ContactsDaoMixin {
|
||||||
updatedValues.username.present) {
|
updatedValues.username.present) {
|
||||||
final contact = await getContactByUserId(userId).getSingleOrNull();
|
final contact = await getContactByUserId(userId).getSingleOrNull();
|
||||||
if (contact != null) {
|
if (contact != null) {
|
||||||
await updatePushUser(contact);
|
|
||||||
final group = await twonlyDB.groupsDao.getDirectChat(userId);
|
final group = await twonlyDB.groupsDao.getDirectChat(userId);
|
||||||
if (group != null) {
|
if (group != null) {
|
||||||
await twonlyDB.groupsDao.updateGroup(
|
await twonlyDB.groupsDao.updateGroup(
|
||||||
|
|
|
||||||
|
|
@ -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);
|
create()..mergeFromJson(json, registry);
|
||||||
|
|
||||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
||||||
_omitMessageNames ? '' : 'Response.UserData',
|
_omitMessageNames ? '' : 'Response.UserConfig',
|
||||||
package:
|
package:
|
||||||
const $pb.PackageName(_omitMessageNames ? '' : 'server_to_client'),
|
const $pb.PackageName(_omitMessageNames ? '' : 'server_to_client'),
|
||||||
createEmptyInstance: create)
|
createEmptyInstance: create)
|
||||||
|
|
|
||||||
|
|
@ -564,7 +564,7 @@ const Response_PqcBundle$json = {
|
||||||
|
|
||||||
@$core.Deprecated('Use responseDescriptor instead')
|
@$core.Deprecated('Use responseDescriptor instead')
|
||||||
const Response_UserData$json = {
|
const Response_UserData$json = {
|
||||||
'1': 'UserData',
|
'1': 'UserConfig',
|
||||||
'2': [
|
'2': [
|
||||||
{'1': 'user_id', '3': 1, '4': 1, '5': 3, '10': 'userId'},
|
{'1': 'user_id', '3': 1, '4': 1, '5': 3, '10': 'userId'},
|
||||||
{
|
{
|
||||||
|
|
@ -868,7 +868,7 @@ const Response_Ok$json = {
|
||||||
'3': 5,
|
'3': 5,
|
||||||
'4': 1,
|
'4': 1,
|
||||||
'5': 11,
|
'5': 11,
|
||||||
'6': '.server_to_client.Response.UserData',
|
'6': '.server_to_client.Response.UserConfig',
|
||||||
'9': 0,
|
'9': 0,
|
||||||
'10': 'userdata'
|
'10': 'userdata'
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import 'package:flutter/foundation.dart';
|
||||||
import 'package:in_app_purchase/in_app_purchase.dart';
|
import 'package:in_app_purchase/in_app_purchase.dart';
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/constants/subscription.keys.dart';
|
import 'package:twonly/src/constants/subscription.keys.dart';
|
||||||
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
|
|
||||||
import 'package:twonly/src/model/purchasable_product.model.dart';
|
import 'package:twonly/src/model/purchasable_product.model.dart';
|
||||||
import 'package:twonly/src/services/subscription.service.dart';
|
import 'package:twonly/src/services/subscription.service.dart';
|
||||||
import 'package:twonly/src/services/user.service.dart';
|
import 'package:twonly/src/services/user.service.dart';
|
||||||
|
|
@ -107,7 +106,7 @@ class PurchasesProvider with ChangeNotifier, DiagnosticableTreeMixin {
|
||||||
Log.info(
|
Log.info(
|
||||||
'Force Ipa check was not stopped. Requesting forced check...',
|
'Force Ipa check was not stopped. Requesting forced check...',
|
||||||
);
|
);
|
||||||
await apiService.forceIpaCheck();
|
await RustApi.forceIpaCheck();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -181,10 +180,13 @@ class PurchasesProvider with ChangeNotifier, DiagnosticableTreeMixin {
|
||||||
Log.info(purchaseDetails.productID);
|
Log.info(purchaseDetails.productID);
|
||||||
Log.info(purchaseDetails.verificationData.source);
|
Log.info(purchaseDetails.verificationData.source);
|
||||||
}
|
}
|
||||||
final res = await apiService.ipaPurchase(
|
final res = await rustApiResult(
|
||||||
purchaseDetails.productID,
|
RustApi.ipaPurchase(
|
||||||
purchaseDetails.verificationData.source,
|
productId: purchaseDetails.productID,
|
||||||
|
source: purchaseDetails.verificationData.source,
|
||||||
|
verificationData:
|
||||||
purchaseDetails.verificationData.serverVerificationData,
|
purchaseDetails.verificationData.serverVerificationData,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
// plan is updated in the apiProvider, as the server updates its states and responses with
|
// plan is updated in the apiProvider, as the server updates its states and responses with
|
||||||
// an ok authenticated which is processed in the apiProvider...
|
// an ok authenticated which is processed in the apiProvider...
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:twonly/core/user_config.dart' as rust_config;
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/services/user.service.dart';
|
import 'package:twonly/src/services/user.service.dart';
|
||||||
import 'package:twonly/src/visual/themes/light.dart';
|
import 'package:twonly/src/visual/themes/light.dart';
|
||||||
|
|
@ -13,8 +14,15 @@ class SettingsChangeProvider with ChangeNotifier, DiagnosticableTreeMixin {
|
||||||
|
|
||||||
void loadSettings() {
|
void loadSettings() {
|
||||||
if (userService.isUserCreated) {
|
if (userService.isUserCreated) {
|
||||||
_themeMode = userService.currentUser.themeMode;
|
_themeMode = switch (userService.currentUser.themeMode) {
|
||||||
_primaryColor = userService.currentUser.primaryColor;
|
rust_config.ThemeMode.system => ThemeMode.system,
|
||||||
|
rust_config.ThemeMode.light => ThemeMode.light,
|
||||||
|
rust_config.ThemeMode.dark => ThemeMode.dark,
|
||||||
|
};
|
||||||
|
final primaryColorValue = userService.currentUser.primaryColorValue;
|
||||||
|
_primaryColor = primaryColorValue == null
|
||||||
|
? defaultPrimaryColor
|
||||||
|
: Color(primaryColorValue);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
} else {
|
} else {
|
||||||
_themeMode = ThemeMode.system;
|
_themeMode = ThemeMode.system;
|
||||||
|
|
@ -31,7 +39,13 @@ class SettingsChangeProvider with ChangeNotifier, DiagnosticableTreeMixin {
|
||||||
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
||||||
await UserService.update((u) => u.themeMode = newThemeMode);
|
await UserService.update(
|
||||||
|
(u) => u.themeMode = switch (newThemeMode) {
|
||||||
|
ThemeMode.system => rust_config.ThemeMode.system,
|
||||||
|
ThemeMode.light => rust_config.ThemeMode.light,
|
||||||
|
ThemeMode.dark => rust_config.ThemeMode.dark,
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updatePrimaryColor(Color newColor) async {
|
Future<void> updatePrimaryColor(Color newColor) async {
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -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:connectivity_plus/connectivity_plus.dart';
|
||||||
import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart';
|
|
||||||
import 'package:cryptography_plus/cryptography_plus.dart';
|
|
||||||
import 'package:drift/drift.dart';
|
|
||||||
import 'package:http/http.dart' as http;
|
|
||||||
import 'package:mutex/mutex.dart';
|
|
||||||
import 'package:path/path.dart';
|
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||||
import 'package:twonly/src/database/twonly.db.dart';
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
import 'package:twonly/src/model/protobuf/client/generated/messages.pbserver.dart';
|
|
||||||
import 'package:twonly/src/services/api/messages.api.dart';
|
|
||||||
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
|
||||||
import 'package:twonly/src/utils/exclusive_access.utils.dart';
|
|
||||||
import 'package:twonly/src/utils/log.dart';
|
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
|
||||||
|
|
||||||
|
/// Flutter only decides whether the user's network policy permits a download.
|
||||||
|
/// Transfer, validation, decryption, hashing, and state changes are Rust-owned.
|
||||||
Future<void> tryDownloadAllMediaFiles({bool force = false}) async {
|
Future<void> tryDownloadAllMediaFiles({bool force = false}) async {
|
||||||
// This is called when WebSocket is newly connected, so allow all downloads to be restarted.
|
if (force) {
|
||||||
|
await RustApi.downloadPendingMedia();
|
||||||
|
return;
|
||||||
|
}
|
||||||
final mediaFiles = await twonlyDB.mediaFilesDao
|
final mediaFiles = await twonlyDB.mediaFilesDao
|
||||||
.getAllMediaFilesPendingDownload();
|
.getAllMediaFilesPendingDownload();
|
||||||
|
for (final media in mediaFiles) {
|
||||||
for (final mediaFile in mediaFiles) {
|
if (await isAllowedToDownload(media.type)) {
|
||||||
if (await canMediaFileBeDownloaded(mediaFile)) {
|
await RustApi.downloadMedia(mediaId: media.mediaId);
|
||||||
await startDownloadMedia(mediaFile, force);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> canMediaFileBeDownloaded(MediaFile mediaFile) async {
|
enum DownloadMediaTypes { video, image, audio }
|
||||||
final messages = await twonlyDB.messagesDao.getMessagesByMediaId(
|
|
||||||
mediaFile.mediaId,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Verify that the sender of the original image / message does still exists.
|
|
||||||
// If not delete the message as it can not be downloaded from the server anymore.
|
|
||||||
|
|
||||||
if (messages.length != 1) {
|
|
||||||
if (messages.isEmpty) {
|
|
||||||
MediaFileService(mediaFile).fullMediaRemoval();
|
|
||||||
await twonlyDB.mediaFilesDao.deleteMediaFile(mediaFile.mediaId);
|
|
||||||
Log.warn(
|
|
||||||
'Media file which is in downloading status has not text message. Deleting media file. ${mediaFile.mediaId}.',
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
Log.warn(
|
|
||||||
'A media for download must have one original message, but it has ${messages.length}.',
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (messages.first.senderId == null) {
|
|
||||||
Log.error('A media for download must have a sender id.');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
final contact = await twonlyDB.contactsDao.getContactById(
|
|
||||||
messages.first.senderId!,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (contact == null || contact.accountDeleted) {
|
|
||||||
Log.info(
|
|
||||||
'Sender does not exists anymore. Delete media file and message.',
|
|
||||||
);
|
|
||||||
await twonlyDB.mediaFilesDao.deleteMediaFile(mediaFile.mediaId);
|
|
||||||
await twonlyDB.messagesDao.deleteMessagesById(messages.first.messageId);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
enum DownloadMediaTypes {
|
|
||||||
video,
|
|
||||||
image,
|
|
||||||
audio,
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, List<String>> defaultAutoDownloadOptions = {
|
Map<String, List<String>> defaultAutoDownloadOptions = {
|
||||||
ConnectivityResult.mobile.name: [
|
ConnectivityResult.mobile.name: [DownloadMediaTypes.audio.name],
|
||||||
DownloadMediaTypes.audio.name,
|
|
||||||
],
|
|
||||||
ConnectivityResult.wifi.name: [
|
ConnectivityResult.wifi.name: [
|
||||||
DownloadMediaTypes.video.name,
|
DownloadMediaTypes.video.name,
|
||||||
DownloadMediaTypes.image.name,
|
DownloadMediaTypes.image.name,
|
||||||
|
|
@ -93,288 +31,40 @@ Map<String, List<String>> defaultAutoDownloadOptions = {
|
||||||
};
|
};
|
||||||
|
|
||||||
Future<bool> isAllowedToDownload(MediaType type) async {
|
Future<bool> isAllowedToDownload(MediaType type) async {
|
||||||
if (type == MediaType.audio) {
|
if (type == MediaType.audio) return true;
|
||||||
return true; // always download audio files
|
|
||||||
}
|
|
||||||
final connectivityResult = await Connectivity().checkConnectivity();
|
final connectivityResult = await Connectivity().checkConnectivity();
|
||||||
|
|
||||||
final options =
|
final options =
|
||||||
userService.currentUser.autoDownloadOptions ?? defaultAutoDownloadOptions;
|
userService.currentUser.autoDownloadOptions ?? defaultAutoDownloadOptions;
|
||||||
|
|
||||||
if (connectivityResult.contains(ConnectivityResult.mobile)) {
|
if (connectivityResult.contains(ConnectivityResult.mobile)) {
|
||||||
if (type == MediaType.video) {
|
return options[ConnectivityResult.mobile.name]!.contains(
|
||||||
if (options[ConnectivityResult.mobile.name]!.contains(
|
type == MediaType.video
|
||||||
DownloadMediaTypes.video.name,
|
? DownloadMediaTypes.video.name
|
||||||
)) {
|
: DownloadMediaTypes.image.name,
|
||||||
return true;
|
);
|
||||||
}
|
|
||||||
} else if (options[ConnectivityResult.mobile.name]!.contains(
|
|
||||||
DownloadMediaTypes.image.name,
|
|
||||||
)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (connectivityResult.contains(ConnectivityResult.wifi)) {
|
if (connectivityResult.contains(ConnectivityResult.wifi)) {
|
||||||
if (type == MediaType.video) {
|
return options[ConnectivityResult.wifi.name]!.contains(
|
||||||
if (options[ConnectivityResult.wifi.name]!.contains(
|
type == MediaType.video
|
||||||
DownloadMediaTypes.video.name,
|
? DownloadMediaTypes.video.name
|
||||||
)) {
|
: DownloadMediaTypes.image.name,
|
||||||
return true;
|
);
|
||||||
}
|
|
||||||
} else if (options[ConnectivityResult.wifi.name]!.contains(
|
|
||||||
DownloadMediaTypes.image.name,
|
|
||||||
)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> handleDownloadStatusUpdate(TaskStatusUpdate update) async {
|
|
||||||
final mediaId = update.task.taskId.replaceAll('download_', '');
|
|
||||||
var failed = false;
|
|
||||||
|
|
||||||
if (update.status == TaskStatus.failed ||
|
|
||||||
update.status == TaskStatus.canceled ||
|
|
||||||
update.status == TaskStatus.notFound) {
|
|
||||||
failed = true;
|
|
||||||
} else if (update.status == TaskStatus.complete) {
|
|
||||||
if (update.responseStatusCode == 200) {
|
|
||||||
failed = false;
|
|
||||||
} else {
|
|
||||||
failed = true;
|
|
||||||
Log.warn(
|
|
||||||
'[$mediaId] Got invalid response status code: ${update.responseStatusCode}',
|
|
||||||
);
|
|
||||||
Log.error(
|
|
||||||
'Got invalid response status code: ${update.responseStatusCode}',
|
|
||||||
onlyIfSentryEnabled: true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Log.info('Got ${update.status} for $mediaId');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (failed) {
|
|
||||||
await requestMediaReupload(mediaId);
|
|
||||||
} else {
|
|
||||||
await handleEncryptedFile(mediaId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Mutex _protectDownload = Mutex();
|
|
||||||
Mutex _protectDecryption = Mutex();
|
|
||||||
|
|
||||||
Future<void> startDownloadMedia(MediaFile media, bool force) async {
|
Future<void> startDownloadMedia(MediaFile media, bool force) async {
|
||||||
final mediaService = MediaFileService(media);
|
if (force || await isAllowedToDownload(media.type)) {
|
||||||
|
await RustApi.downloadMedia(mediaId: media.mediaId);
|
||||||
if (mediaService.encryptedPath.existsSync()) {
|
|
||||||
await handleEncryptedFile(media.mediaId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!force && !await isAllowedToDownload(media.type)) {
|
|
||||||
Log.warn(
|
|
||||||
'Download blocked for ${media.mediaId} because of network state.',
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final isBlocked = await _protectDownload.protect<bool>(() async {
|
|
||||||
final msg = await twonlyDB.mediaFilesDao.getMediaFileById(media.mediaId);
|
|
||||||
|
|
||||||
if (msg == null || msg.downloadState != DownloadState.pending) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
await twonlyDB.mediaFilesDao.updateMedia(
|
|
||||||
msg.mediaId,
|
|
||||||
const MediaFilesCompanion(
|
|
||||||
downloadState: Value(DownloadState.downloading),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isBlocked) {
|
|
||||||
Log.info('Download for ${media.mediaId} already started.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (media.downloadToken == null) {
|
|
||||||
Log.info('Download token for ${media.mediaId} not found.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final downloadToken = uint8ListToHex(media.downloadToken!);
|
|
||||||
|
|
||||||
final apiUrl =
|
|
||||||
'http${apiService.apiSecure}://${apiService.apiHost}/api/download/$downloadToken';
|
|
||||||
|
|
||||||
try {
|
|
||||||
final task = DownloadTask(
|
|
||||||
url: apiUrl,
|
|
||||||
taskId: 'download_${media.mediaId}',
|
|
||||||
directory: mediaService.encryptedPath.parent.path,
|
|
||||||
baseDirectory: BaseDirectory.root,
|
|
||||||
filename: basename(mediaService.encryptedPath.path),
|
|
||||||
priority: 0,
|
|
||||||
retries: 10,
|
|
||||||
);
|
|
||||||
|
|
||||||
Log.info(
|
|
||||||
'Downloading ${media.mediaId} to ${mediaService.encryptedPath}',
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await downloadFileFast(media, apiUrl, mediaService.encryptedPath);
|
|
||||||
} catch (e) {
|
|
||||||
Log.warn('Fast download failed: $e');
|
|
||||||
await FileDownloader().enqueue(task);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
Log.error('Exception during download: $e');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> downloadFileFast(
|
Future<void> requestMediaReupload(String mediaId) =>
|
||||||
MediaFile media,
|
RustApi.requestMediaReupload(mediaId: mediaId);
|
||||||
String apiUrl,
|
|
||||||
File filePath,
|
|
||||||
) async {
|
|
||||||
final response = await http
|
|
||||||
.get(Uri.parse(apiUrl))
|
|
||||||
.timeout(const Duration(seconds: 30));
|
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
|
||||||
await filePath.writeAsBytes(response.bodyBytes);
|
|
||||||
Log.info('Fast Download successful: $filePath');
|
|
||||||
await handleEncryptedFile(media.mediaId);
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
if (response.statusCode == 404 || response.statusCode == 403) {
|
|
||||||
Log.warn(
|
|
||||||
'Got ${response.statusCode} from server for media ID ${media.mediaId}. Requesting upload again',
|
|
||||||
);
|
|
||||||
Log.error(
|
|
||||||
'Got ${response.statusCode} from server for media ID.',
|
|
||||||
onlyIfSentryEnabled: true,
|
|
||||||
);
|
|
||||||
// Message was deleted from the server. Requesting it again from the sender to upload it again...
|
|
||||||
await requestMediaReupload(media.mediaId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Will be tried again using the slow method...
|
|
||||||
throw Exception('Fast download failed with status: ${response.statusCode}');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> requestMediaReupload(String mediaId) async {
|
|
||||||
await twonlyDB.mediaFilesDao.updateMedia(
|
|
||||||
mediaId,
|
|
||||||
const MediaFilesCompanion(
|
|
||||||
downloadState: Value(DownloadState.reuploadRequested),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
final messages = await twonlyDB.messagesDao.getMessagesByMediaId(mediaId);
|
|
||||||
|
|
||||||
for (final message in messages) {
|
|
||||||
if (message.openedAt != null || message.senderId == null) continue;
|
|
||||||
await sendCipherText(
|
|
||||||
message.senderId!,
|
|
||||||
EncryptedContent(
|
|
||||||
mediaUpdate: EncryptedContent_MediaUpdate(
|
|
||||||
type: EncryptedContent_MediaUpdate_Type.DECRYPTION_ERROR,
|
|
||||||
targetMessageId: message.messageId,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> handleEncryptedFile(String mediaId) async {
|
|
||||||
await exclusiveAccess(
|
|
||||||
lockName: 'decryption-$mediaId',
|
|
||||||
mutex: _protectDecryption,
|
|
||||||
action: () async {
|
|
||||||
final mediaService = await MediaFileService.fromMediaId(mediaId);
|
|
||||||
if (mediaService == null) {
|
|
||||||
Log.warn('[$mediaId] Media file not found in database.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mediaService.mediaFile.downloadState == DownloadState.ready) {
|
|
||||||
Log.info('Decryption of $mediaId already finished.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!mediaService.encryptedPath.existsSync()) {
|
|
||||||
Log.warn(
|
|
||||||
'Encrypted media file $mediaId does not exist anymore. Decryption probably already finished.',
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
late Uint8List encryptedBytes;
|
|
||||||
try {
|
|
||||||
encryptedBytes = await mediaService.encryptedPath.readAsBytes();
|
|
||||||
} catch (e) {
|
|
||||||
Log.error('Could not read encrypted media file: $e');
|
|
||||||
await requestMediaReupload(mediaId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
final chacha20 = FlutterChacha20.poly1305Aead();
|
|
||||||
final secretKeyData = SecretKeyData(
|
|
||||||
mediaService.mediaFile.encryptionKey!,
|
|
||||||
);
|
|
||||||
|
|
||||||
final secretBox = SecretBox(
|
|
||||||
encryptedBytes,
|
|
||||||
nonce: mediaService.mediaFile.encryptionNonce!,
|
|
||||||
mac: Mac(mediaService.mediaFile.encryptionMac!),
|
|
||||||
);
|
|
||||||
|
|
||||||
final plaintextBytes = await chacha20.decrypt(
|
|
||||||
secretBox,
|
|
||||||
secretKey: secretKeyData,
|
|
||||||
);
|
|
||||||
|
|
||||||
final rawMediaBytes = Uint8List.fromList(plaintextBytes);
|
|
||||||
|
|
||||||
await mediaService.tempPath.writeAsBytes(rawMediaBytes);
|
|
||||||
} catch (e) {
|
|
||||||
Log.error(
|
|
||||||
'Could not decrypt the media file. Requesting a new upload.',
|
|
||||||
);
|
|
||||||
await requestMediaReupload(mediaId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await twonlyDB.mediaFilesDao.updateMedia(
|
|
||||||
mediaId,
|
|
||||||
const MediaFilesCompanion(
|
|
||||||
downloadState: Value(DownloadState.ready),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
await mediaService.hashMediaFile();
|
|
||||||
|
|
||||||
Log.info('Decryption of $mediaId was successful');
|
|
||||||
|
|
||||||
mediaService.encryptedPath.deleteSync();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> makeMigrationToVersion91() async {
|
Future<void> makeMigrationToVersion91() async {
|
||||||
final messages = await twonlyDB.mediaFilesDao
|
final mediaFiles = await twonlyDB.mediaFilesDao
|
||||||
.getAllMediaFilesReuploadRequested();
|
.getAllMediaFilesReuploadRequested();
|
||||||
for (final message in messages) {
|
for (final media in mediaFiles) {
|
||||||
await requestMediaReupload(message.mediaId);
|
await RustApi.requestMediaReupload(mediaId: media.mediaId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import 'package:flutter/foundation.dart';
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||||
import 'package:twonly/src/database/twonly.db.dart';
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
import 'package:twonly/src/services/api/mediafiles/download.api.dart';
|
|
||||||
import 'package:twonly/src/services/api/mediafiles/upload.api.dart';
|
import 'package:twonly/src/services/api/mediafiles/upload.api.dart';
|
||||||
import 'package:twonly/src/services/backup.service.dart';
|
import 'package:twonly/src/services/backup.service.dart';
|
||||||
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
||||||
|
|
@ -19,10 +18,6 @@ Future<void> initFileDownloader() async {
|
||||||
if (update.task.taskId.contains('upload_')) {
|
if (update.task.taskId.contains('upload_')) {
|
||||||
await handleUploadStatusUpdate(update);
|
await handleUploadStatusUpdate(update);
|
||||||
}
|
}
|
||||||
if (update.task.taskId.contains('download_')) {
|
|
||||||
await handleDownloadStatusUpdate(update);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (update.task.taskId.contains('backup_')) {
|
if (update.task.taskId.contains('backup_')) {
|
||||||
await BackupService.handleBackupStatusUpdate(
|
await BackupService.handleBackupStatusUpdate(
|
||||||
update.task.taskId,
|
update.task.taskId,
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,6 @@ import 'package:twonly/src/database/twonly.db.dart' show Receipt;
|
||||||
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'
|
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'
|
||||||
as pb;
|
as pb;
|
||||||
|
|
||||||
// Compatibility adapters. All messaging state and behavior lives in Rust.
|
|
||||||
Future<void> retransmitAllMessages() =>
|
|
||||||
rust_api.RustApi.retransmitAllMessages();
|
|
||||||
|
|
||||||
Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({
|
Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({
|
||||||
String? receiptId,
|
String? receiptId,
|
||||||
Receipt? receipt,
|
Receipt? receipt,
|
||||||
|
|
|
||||||
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/locator.dart';
|
||||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||||
import 'package:twonly/src/database/twonly.db.dart';
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pb.dart'
|
|
||||||
as client;
|
|
||||||
import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pbserver.dart';
|
|
||||||
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
|
|
||||||
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart'
|
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart'
|
||||||
as server;
|
as server;
|
||||||
import 'package:twonly/src/model/protobuf/client/generated/messages.pbserver.dart'
|
import 'package:twonly/src/model/protobuf/client/generated/messages.pbserver.dart'
|
||||||
hide Message;
|
hide Message;
|
||||||
import 'package:twonly/src/services/api/messages.api.dart';
|
import 'package:twonly/src/services/api/messages.api.dart';
|
||||||
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
|
|
||||||
import 'package:twonly/src/services/signal/session.signal.dart';
|
import 'package:twonly/src/services/signal/session.signal.dart';
|
||||||
import 'package:twonly/src/utils/log.dart';
|
import 'package:twonly/src/utils/log.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
import 'package:twonly/src/utils/secure_storage.dart';
|
import 'package:twonly/src/utils/secure_storage.dart';
|
||||||
|
|
||||||
class Result<T, E> {
|
class Result<T, E> {
|
||||||
Result.error(this.error) : value = null;
|
Result.error(this.error) : value = null, _isSuccess = false;
|
||||||
Result.success(this.value) : error = null;
|
Result.success(this.value) : error = null, _isSuccess = true;
|
||||||
|
|
||||||
final T? value;
|
final T? value;
|
||||||
final E? error;
|
final E? error;
|
||||||
|
final bool _isSuccess;
|
||||||
|
|
||||||
bool get isSuccess => value != null;
|
bool get isSuccess => _isSuccess;
|
||||||
bool get isError => error != null;
|
bool get isError => !_isSuccess;
|
||||||
}
|
}
|
||||||
|
|
||||||
DateTime fromTimestamp(Int64 timeStamp) {
|
DateTime fromTimestamp(Int64 timeStamp) {
|
||||||
|
|
@ -53,22 +49,6 @@ Result asResult(server.ServerToClient? msg) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ClientToServer createClientToServerFromHandshake(Handshake handshake) {
|
|
||||||
final v0 = client.V0()
|
|
||||||
..seq = Int64()
|
|
||||||
..handshake = handshake;
|
|
||||||
return ClientToServer()..v0 = v0;
|
|
||||||
}
|
|
||||||
|
|
||||||
ClientToServer createClientToServerFromApplicationData(
|
|
||||||
ApplicationData applicationData,
|
|
||||||
) {
|
|
||||||
final v0 = client.V0()
|
|
||||||
..seq = Int64()
|
|
||||||
..applicationdata = applicationData;
|
|
||||||
return ClientToServer()..v0 = v0;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> handleMediaError(MediaFile media) async {
|
Future<void> handleMediaError(MediaFile media) async {
|
||||||
await twonlyDB.mediaFilesDao.updateMedia(
|
await twonlyDB.mediaFilesDao.updateMedia(
|
||||||
media.mediaId,
|
media.mediaId,
|
||||||
|
|
@ -100,11 +80,6 @@ Future<bool> importSignalContactAndCreateRequest(
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. Setup notifications keys with the other user
|
|
||||||
await setupNotificationWithUsers(
|
|
||||||
forceContact: userdata.userId.toInt(),
|
|
||||||
);
|
|
||||||
|
|
||||||
// 2. Then send user request
|
// 2. Then send user request
|
||||||
await sendCipherText(
|
await sendCipherText(
|
||||||
userdata.userId.toInt(),
|
userdata.userId.toInt(),
|
||||||
|
|
|
||||||
|
|
@ -309,8 +309,11 @@ class BackupService {
|
||||||
String username,
|
String username,
|
||||||
String password,
|
String password,
|
||||||
) async {
|
) async {
|
||||||
final userId = await apiService.getUserIdFromUsername(username);
|
late final int userId;
|
||||||
if (userId == null) {
|
try {
|
||||||
|
userId = await RustApi.getUserIdFromUsername(username: username);
|
||||||
|
} catch (error) {
|
||||||
|
Log.error('Could not resolve backup username', error: error);
|
||||||
return RecoveryError.usernameNotValid;
|
return RecoveryError.usernameNotValid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -171,7 +171,10 @@ class MemoriesCloudService {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
final urls = await apiService.getMemoriesUrl(mediaId, isThumbnail);
|
final urls = await rustApiProtobuf(
|
||||||
|
RustApi.getMemoriesUrl(mediaId: mediaId, thumbnail: isThumbnail),
|
||||||
|
decodeMemoriesUrl,
|
||||||
|
);
|
||||||
if (urls == null || !urls.hasFullDownloadUrl()) return false;
|
if (urls == null || !urls.hasFullDownloadUrl()) return false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -205,10 +208,13 @@ class MemoriesCloudService {
|
||||||
}
|
}
|
||||||
|
|
||||||
final sizeBytes = ms.storedPath.lengthSync();
|
final sizeBytes = ms.storedPath.lengthSync();
|
||||||
final urls = await apiService.requestMemoriesUpload(
|
final urls = await rustApiProtobuf(
|
||||||
sizeBytes,
|
RustApi.requestMemoriesUpload(
|
||||||
mediaFile.createdAt,
|
size: sizeBytes,
|
||||||
mediaFile.mediaId,
|
originalDate: mediaFile.createdAt.millisecondsSinceEpoch,
|
||||||
|
mediaId: mediaFile.mediaId,
|
||||||
|
),
|
||||||
|
decodeMemoriesUploadUrls,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (urls == null) {
|
if (urls == null) {
|
||||||
|
|
@ -268,8 +274,8 @@ class MemoriesCloudService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Confirm upload
|
// 3. Confirm upload
|
||||||
final confirmRes = await apiService.confirmMemoriesUpload(
|
final confirmRes = await rustApiResult(
|
||||||
mediaFile.mediaId,
|
RustApi.confirmMemoriesUpload(mediaId: mediaFile.mediaId),
|
||||||
);
|
);
|
||||||
if (confirmRes.isSuccess) {
|
if (confirmRes.isSuccess) {
|
||||||
await twonlyDB.mediaFilesDao.updateMedia(
|
await twonlyDB.mediaFilesDao.updateMedia(
|
||||||
|
|
|
||||||
|
|
@ -98,10 +98,8 @@ Future<void> runMigrations() async {
|
||||||
..canUseLoginTokenForAuth = false
|
..canUseLoginTokenForAuth = false
|
||||||
// As usernames changes where not considered in the old version force users
|
// As usernames changes where not considered in the old version force users
|
||||||
// to reenter there passwords.
|
// to reenter there passwords.
|
||||||
// ignore: deprecated_member_use_from_same_package
|
..twonlySafeBackup?.encryptionKey = Uint8List(0)
|
||||||
..twonlySafeBackup?.encryptionKey = []
|
..twonlySafeBackup?.backupId = Uint8List(0);
|
||||||
// ignore: deprecated_member_use_from_same_package
|
|
||||||
..twonlySafeBackup?.backupId = [];
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,14 +9,11 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||||
import 'package:twonly/globals.dart';
|
import 'package:twonly/globals.dart';
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/constants/routes.keys.dart';
|
import 'package:twonly/src/constants/routes.keys.dart';
|
||||||
import 'package:twonly/src/constants/secure_storage.keys.dart';
|
|
||||||
import 'package:twonly/src/localization/generated/app_localizations.dart';
|
import 'package:twonly/src/localization/generated/app_localizations.dart';
|
||||||
import 'package:twonly/src/localization/generated/app_localizations_de.dart';
|
import 'package:twonly/src/localization/generated/app_localizations_de.dart';
|
||||||
import 'package:twonly/src/localization/generated/app_localizations_en.dart';
|
import 'package:twonly/src/localization/generated/app_localizations_en.dart';
|
||||||
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
|
|
||||||
import 'package:twonly/src/model/protobuf/client/generated/push_notification.pb.dart';
|
import 'package:twonly/src/model/protobuf/client/generated/push_notification.pb.dart';
|
||||||
import 'package:twonly/src/providers/routing.provider.dart';
|
import 'package:twonly/src/providers/routing.provider.dart';
|
||||||
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
|
|
||||||
import 'package:twonly/src/utils/log.dart';
|
import 'package:twonly/src/utils/log.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
|
||||||
|
|
@ -50,29 +47,6 @@ Future<void> customLocalPushNotification(String title, String msg) async {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> showPushNotificationFromServerMessages(
|
|
||||||
int fromUserId,
|
|
||||||
EncryptedContent encryptedContent,
|
|
||||||
) async {
|
|
||||||
final pushData = await getPushNotificationFromEncryptedContent(
|
|
||||||
null, // this is the toUserID which must be null as this means that the targetMessageId was send from this user.
|
|
||||||
null,
|
|
||||||
encryptedContent,
|
|
||||||
);
|
|
||||||
if (pushData != null) {
|
|
||||||
final pushUsers = await getPushKeys(SecureStorageKeys.receivingPushKeys);
|
|
||||||
for (final pushUser in pushUsers) {
|
|
||||||
if (pushUser.userId.toInt() == fromUserId) {
|
|
||||||
String? groupId;
|
|
||||||
if (encryptedContent.hasGroupId()) {
|
|
||||||
groupId = encryptedContent.groupId;
|
|
||||||
}
|
|
||||||
return showLocalPushNotification(pushUser, pushData, groupId: groupId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<PushNotification?> tryDecryptMessage(
|
Future<PushNotification?> tryDecryptMessage(
|
||||||
List<int> key,
|
List<int> key,
|
||||||
EncryptedPushNotification push,
|
EncryptedPushNotification push,
|
||||||
|
|
|
||||||
|
|
@ -36,22 +36,16 @@ class FcmNotificationService {
|
||||||
|
|
||||||
static Future<void> initFCMAfterAuthenticated({bool force = false}) async {
|
static Future<void> initFCMAfterAuthenticated({bool force = false}) async {
|
||||||
final fcmToken = userService.currentUser.fcmToken;
|
final fcmToken = userService.currentUser.fcmToken;
|
||||||
if (userService.currentUser.updateFCMToken || force) {
|
if (userService.currentUser.updateFcmToken || force) {
|
||||||
if (fcmToken == null) {
|
if (fcmToken == null) {
|
||||||
Log.error('FCM token could not be updated as it is empty');
|
Log.error('FCM token could not be updated as it is empty');
|
||||||
await _checkForTokenUpdates();
|
await _checkForTokenUpdates();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final res = await apiService.updateFCMToken(
|
if (await _uploadFcmToken(fcmToken)) {
|
||||||
fcmToken,
|
|
||||||
);
|
|
||||||
if (res.isSuccess) {
|
|
||||||
Log.info('Uploaded new FCM token!');
|
|
||||||
await UserService.update((u) {
|
await UserService.update((u) {
|
||||||
u.updateFCMToken = false;
|
u.updateFcmToken = false;
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
Log.error('Could not update FCM token!');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -100,18 +94,14 @@ class FcmNotificationService {
|
||||||
Log.info('Got new FCM token.');
|
Log.info('Got new FCM token.');
|
||||||
await UserService.update((u) {
|
await UserService.update((u) {
|
||||||
u
|
u
|
||||||
..updateFCMToken = true
|
..updateFcmToken = true
|
||||||
..fcmToken = fcmToken;
|
..fcmToken = fcmToken;
|
||||||
});
|
});
|
||||||
if (apiService.isAuthenticated) {
|
if (apiService.isAuthenticated) {
|
||||||
final res = await apiService.updateFCMToken(fcmToken);
|
if (await _uploadFcmToken(fcmToken)) {
|
||||||
if (res.isSuccess) {
|
|
||||||
Log.info('Uploaded new FCM token!');
|
|
||||||
await UserService.update((u) {
|
await UserService.update((u) {
|
||||||
u.updateFCMToken = false;
|
u.updateFcmToken = false;
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
Log.error('Could not update FCM token!');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -121,18 +111,14 @@ class FcmNotificationService {
|
||||||
.listen((String fcmToken) async {
|
.listen((String fcmToken) async {
|
||||||
await UserService.update((u) {
|
await UserService.update((u) {
|
||||||
u
|
u
|
||||||
..updateFCMToken = true
|
..updateFcmToken = true
|
||||||
..fcmToken = fcmToken;
|
..fcmToken = fcmToken;
|
||||||
});
|
});
|
||||||
if (apiService.isAuthenticated) {
|
if (apiService.isAuthenticated) {
|
||||||
final res = await apiService.updateFCMToken(fcmToken);
|
if (await _uploadFcmToken(fcmToken)) {
|
||||||
if (res.isSuccess) {
|
|
||||||
Log.info('Uploaded new FCM token!');
|
|
||||||
await UserService.update((u) {
|
await UserService.update((u) {
|
||||||
u.updateFCMToken = false;
|
u.updateFcmToken = false;
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
Log.error('Could not update FCM token!');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -144,6 +130,17 @@ class FcmNotificationService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Future<bool> _uploadFcmToken(String token) async {
|
||||||
|
try {
|
||||||
|
await RustApi.updateFcmToken(token: token);
|
||||||
|
Log.info('Uploaded new FCM token!');
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
Log.error('Could not update FCM token!', error: error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static Future<void> handleRemoteMessage(RemoteMessage message) async {
|
static Future<void> handleRemoteMessage(RemoteMessage message) async {
|
||||||
Log.info('handleRemoteMessage received message: ${message.messageId}');
|
Log.info('handleRemoteMessage received message: ${message.messageId}');
|
||||||
await _updateLastFcmMessageTimestamp();
|
await _updateLastFcmMessageTimestamp();
|
||||||
|
|
|
||||||
|
|
@ -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:async';
|
||||||
import 'dart:convert' show base64Url, utf8;
|
import 'dart:convert' show base64Url, utf8;
|
||||||
|
|
||||||
import 'package:clock/clock.dart';
|
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:crypto/crypto.dart' hide Hmac;
|
|
||||||
import 'package:cryptography_plus/cryptography_plus.dart'
|
import 'package:cryptography_plus/cryptography_plus.dart'
|
||||||
show Hkdf, Hmac, Mac, SecretBox, SecretKey, Xchacha20;
|
show Hkdf, Hmac, Mac, SecretBox, SecretKey, Xchacha20;
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:fixnum/fixnum.dart';
|
import 'package:fixnum/fixnum.dart';
|
||||||
|
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'
|
||||||
|
show Int64List;
|
||||||
import 'package:twonly/core/bridge/wrapper.dart';
|
import 'package:twonly/core/bridge/wrapper.dart';
|
||||||
import 'package:twonly/core/bridge/wrapper/key_manager.dart';
|
import 'package:twonly/core/bridge/wrapper/key_manager.dart';
|
||||||
|
import 'package:twonly/core/user_config.dart' show PasswordlessRecoveryConfig;
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/constants/keyvalue.keys.dart';
|
import 'package:twonly/src/constants/keyvalue.keys.dart';
|
||||||
import 'package:twonly/src/database/daos/contacts.dao.dart'
|
import 'package:twonly/src/database/daos/contacts.dao.dart'
|
||||||
show getContactDisplayName;
|
show getContactDisplayName;
|
||||||
import 'package:twonly/src/database/twonly.db.dart';
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
import 'package:twonly/src/model/json/onboarding_state.model.dart';
|
import 'package:twonly/src/model/json/onboarding_state.model.dart';
|
||||||
import 'package:twonly/src/model/json/userdata.model.dart'
|
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart'
|
||||||
show PasswordLessRecovery;
|
as server;
|
||||||
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'
|
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'
|
||||||
as pb;
|
as pb;
|
||||||
import 'package:twonly/src/model/protobuf/client/generated/passwordless_recovery.pb.dart';
|
import 'package:twonly/src/model/protobuf/client/generated/passwordless_recovery.pb.dart';
|
||||||
|
|
@ -128,11 +128,11 @@ class PasswordlessRecoveryService {
|
||||||
mac: secretBox.mac.bytes,
|
mac: secretBox.mac.bytes,
|
||||||
);
|
);
|
||||||
|
|
||||||
final res = await apiService.submitRecoveryShare(
|
await RustApi.submitRecoveryShare(
|
||||||
notificationId: notificationId,
|
notificationId: notificationId,
|
||||||
encryptedMessage: envelope.writeToBuffer(),
|
encryptedMessage: envelope.writeToBuffer(),
|
||||||
);
|
);
|
||||||
return res.isSuccess;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Log.error('Failed to submit recovery share', error: e);
|
Log.error('Failed to submit recovery share', error: e);
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -172,7 +172,7 @@ class PasswordlessRecoveryService {
|
||||||
await twonlyDB.contactsDao.resetRecoveryDataForAllContacts();
|
await twonlyDB.contactsDao.resetRecoveryDataForAllContacts();
|
||||||
await UserService.update((u) => u.passwordLessRecovery = null);
|
await UserService.update((u) => u.passwordLessRecovery = null);
|
||||||
|
|
||||||
final config = PasswordLessRecovery(threshold);
|
final config = PasswordlessRecoveryConfig(threshold: threshold);
|
||||||
final xchacha20 = Xchacha20.poly1305Aead();
|
final xchacha20 = Xchacha20.poly1305Aead();
|
||||||
|
|
||||||
// 2. If enabled, handle the second factor and create serverKey
|
// 2. If enabled, handle the second factor and create serverKey
|
||||||
|
|
@ -309,7 +309,7 @@ class PasswordlessRecoveryService {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
unawaited(performHeartbeat());
|
unawaited(RustApi.performPasswordlessRecoveryHeartbeat());
|
||||||
|
|
||||||
// The passwordless is configured successfully.
|
// The passwordless is configured successfully.
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -357,193 +357,6 @@ class PasswordlessRecoveryService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> performHeartbeat() async {
|
|
||||||
final config = userService.currentUser.passwordLessRecovery;
|
|
||||||
|
|
||||||
if (config != null) {
|
|
||||||
final lastHeartbeat = config.lastServerHeartbeat;
|
|
||||||
final isOlderThanAMonth =
|
|
||||||
lastHeartbeat != null &&
|
|
||||||
clock.now().difference(lastHeartbeat).inDays > 20;
|
|
||||||
|
|
||||||
if ((lastHeartbeat == null || isOlderThanAMonth) &&
|
|
||||||
config.encryptedServerKey != null) {
|
|
||||||
final res = await apiService.registerPasswordLessRecovery(
|
|
||||||
config.encryptedServerKey!,
|
|
||||||
config.pinUnlockToken,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (res.isSuccess) {
|
|
||||||
await UserService.update((u) {
|
|
||||||
u.passwordLessRecovery?.lastServerHeartbeat = clock.now();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final lastContactHeartbeat = config.lastContactHeartbeat;
|
|
||||||
final isContactHeartbeatOlderThan24h =
|
|
||||||
lastContactHeartbeat == null ||
|
|
||||||
clock.now().difference(lastContactHeartbeat).inHours >= 24;
|
|
||||||
|
|
||||||
if (isContactHeartbeatOlderThan24h) {
|
|
||||||
// Get all contacts where recoveryLastHeartbeat is NULL. Then for each contact send.
|
|
||||||
// recoveryLastHeartbeat is ONLY updated in case the contact has responded.
|
|
||||||
final pendingShares =
|
|
||||||
await (twonlyDB.select(twonlyDB.contacts)..where(
|
|
||||||
(t) =>
|
|
||||||
t.recoveryIsTrustedFriend.equals(true) &
|
|
||||||
t.recoveryLastHeartbeat.isNull() &
|
|
||||||
t.recoverySecretShare.isNotNull(),
|
|
||||||
))
|
|
||||||
.get();
|
|
||||||
|
|
||||||
for (final contact in pendingShares) {
|
|
||||||
try {
|
|
||||||
await sendCipherText(
|
|
||||||
contact.userId,
|
|
||||||
pb.EncryptedContent(
|
|
||||||
passwordlessRecovery: pb.EncryptedContent_PasswordLessRecovery(
|
|
||||||
recoverySecretShare: contact.recoverySecretShare,
|
|
||||||
delete: false,
|
|
||||||
threshold: Int64(config.threshold),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
Log.error(
|
|
||||||
'Failed to send PasswordLessRecovery share to contact ${contact.userId}: $e',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await UserService.update((u) {
|
|
||||||
u.passwordLessRecovery?.lastContactHeartbeat = clock.now();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send heartbeat to the friends I am a trusted friend.
|
|
||||||
final oneWeekAgo = clock.now().subtract(const Duration(days: 7));
|
|
||||||
final trustedFriendsToNotify =
|
|
||||||
await (twonlyDB.select(twonlyDB.contacts)..where(
|
|
||||||
(t) =>
|
|
||||||
t.recoveryContactsSecretShare.isNotNull() &
|
|
||||||
(t.recoveryContactsLastHeartbeat.isNull() |
|
|
||||||
t.recoveryContactsLastHeartbeat.isSmallerThanValue(
|
|
||||||
oneWeekAgo,
|
|
||||||
)),
|
|
||||||
))
|
|
||||||
.get();
|
|
||||||
|
|
||||||
for (final contact in trustedFriendsToNotify) {
|
|
||||||
try {
|
|
||||||
final share = contact.recoveryContactsSecretShare!;
|
|
||||||
final hash = sha256.convert(share).bytes;
|
|
||||||
|
|
||||||
await sendCipherText(
|
|
||||||
contact.userId,
|
|
||||||
pb.EncryptedContent(
|
|
||||||
passwordlessRecoveryHeartbeat:
|
|
||||||
pb.EncryptedContent_PasswordLessRecoveryHeartbeat(
|
|
||||||
hash: hash,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
await twonlyDB.contactsDao.updateContact(
|
|
||||||
contact.userId,
|
|
||||||
ContactsCompanion(
|
|
||||||
recoveryContactsLastHeartbeat: Value(clock.now()),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
Log.error(
|
|
||||||
'Failed to send PasswordLessRecoveryHeartbeat to contact ${contact.userId}: $e',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<void> handlePasswordlessRecovery(
|
|
||||||
int fromUserId,
|
|
||||||
pb.EncryptedContent_PasswordLessRecovery msg,
|
|
||||||
String receiptId,
|
|
||||||
) async {
|
|
||||||
if (msg.delete) {
|
|
||||||
Log.info(
|
|
||||||
'[$receiptId] Received request to delete passwordless recovery share from contact $fromUserId',
|
|
||||||
);
|
|
||||||
await twonlyDB.contactsDao.updateContact(
|
|
||||||
fromUserId,
|
|
||||||
const ContactsCompanion(
|
|
||||||
recoveryContactsSecretShare: Value(null),
|
|
||||||
recoveryContactsLastHeartbeat: Value(null),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else if (msg.hasRecoverySecretShare() && msg.hasThreshold()) {
|
|
||||||
Log.info(
|
|
||||||
'[$receiptId] Received new passwordless recovery share from contact $fromUserId',
|
|
||||||
);
|
|
||||||
await twonlyDB.contactsDao.updateContact(
|
|
||||||
fromUserId,
|
|
||||||
ContactsCompanion(
|
|
||||||
recoveryContactsSecretShare: Value(
|
|
||||||
Uint8List.fromList(msg.recoverySecretShare),
|
|
||||||
),
|
|
||||||
recoveryContactsThreshold: Value(msg.threshold.toInt()),
|
|
||||||
recoveryContactsLastHeartbeat: const Value(
|
|
||||||
null, // this will trigger that a heartbeat will be send...
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
unawaited(performHeartbeat());
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<void> handlePasswordlessRecoveryHeartbeat(
|
|
||||||
int fromUserId,
|
|
||||||
pb.EncryptedContent_PasswordLessRecoveryHeartbeat msg,
|
|
||||||
String receiptId,
|
|
||||||
) async {
|
|
||||||
Log.info(
|
|
||||||
'[$receiptId] Received passwordless recovery heartbeat from contact $fromUserId',
|
|
||||||
);
|
|
||||||
final contact = await twonlyDB.contactsDao.getContactById(fromUserId);
|
|
||||||
final storedShare = contact?.recoverySecretShare;
|
|
||||||
|
|
||||||
if (storedShare == null) {
|
|
||||||
unawaited(
|
|
||||||
sendCipherText(
|
|
||||||
fromUserId,
|
|
||||||
pb.EncryptedContent(
|
|
||||||
passwordlessRecovery: pb.EncryptedContent_PasswordLessRecovery(
|
|
||||||
delete: true,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
Log.warn(
|
|
||||||
'[$receiptId] Received passwordless recovery heartbeat from $fromUserId but we did not send him a secret share.',
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final computedHash = sha256.convert(storedShare).bytes;
|
|
||||||
final recoveryLastHeartbeat =
|
|
||||||
const ListEquality().equals(computedHash, msg.hash)
|
|
||||||
? clock.now()
|
|
||||||
: null; // The stored share not valid (maybe an old backup was restored). This will cause the performHeartbeat to resend him his share
|
|
||||||
Log.info(
|
|
||||||
'[$receiptId] Got heartbeat: ($recoveryLastHeartbeat)',
|
|
||||||
);
|
|
||||||
await twonlyDB.contactsDao.updateContact(
|
|
||||||
fromUserId,
|
|
||||||
ContactsCompanion(
|
|
||||||
recoveryLastHeartbeat: Value(recoveryLastHeartbeat),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<bool> checkAndStorePasswordlessMessages(
|
static Future<bool> checkAndStorePasswordlessMessages(
|
||||||
OnboardingState state,
|
OnboardingState state,
|
||||||
) async {
|
) async {
|
||||||
|
|
@ -554,17 +367,29 @@ class PasswordlessRecoveryService {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
final alreadyReceivedIds = state.receivedShares
|
final alreadyReceivedIds = Int64List.fromList(
|
||||||
.map((s) => Int64(s.messageId))
|
state.receivedShares.map((share) => share.messageId).toList(),
|
||||||
.toList();
|
|
||||||
|
|
||||||
final response = await apiService.checkForPasswordlessNotification(
|
|
||||||
notificationId: state.notificationId!,
|
|
||||||
downloadAuthToken: state.downloadAuthToken!,
|
|
||||||
alreadyReceivedIds: alreadyReceivedIds,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response == null || response.messages.isEmpty) {
|
late final server.Response_PasswordlessNotificationMessages response;
|
||||||
|
try {
|
||||||
|
final responseBytes = await RustApi.checkForPasswordlessNotification(
|
||||||
|
notificationId: state.notificationId!,
|
||||||
|
downloadAuthToken: state.downloadAuthToken!,
|
||||||
|
alreadyReceivedMessageIds: alreadyReceivedIds,
|
||||||
|
);
|
||||||
|
response = server.Response_PasswordlessNotificationMessages.fromBuffer(
|
||||||
|
responseBytes,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
Log.error(
|
||||||
|
'Failed to load passwordless recovery messages',
|
||||||
|
error: error,
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.messages.isEmpty) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
enum SetupProfile { standard, customized }
|
|
||||||
|
|
@ -30,10 +30,12 @@ class SignalIdentityService {
|
||||||
await UserService.update((user) {
|
await UserService.update((user) {
|
||||||
user.signalLastSignedPreKeyUpdated = now;
|
user.signalLastSignedPreKeyUpdated = now;
|
||||||
});
|
});
|
||||||
final res = await apiService.updateSignedPreKey(
|
final res = await rustApiResult(
|
||||||
signedPreKey.id,
|
RustApi.updateSignedPreKey(
|
||||||
signedPreKey.getKeyPair().publicKey.serialize(),
|
id: signedPreKey.id,
|
||||||
signedPreKey.signature,
|
key: signedPreKey.getKeyPair().publicKey.serialize(),
|
||||||
|
signature: signedPreKey.signature,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
if (res.isError) {
|
if (res.isError) {
|
||||||
Log.error('could not update the signed pre key: ${res.error}');
|
Log.error('could not update the signed pre key: ${res.error}');
|
||||||
|
|
@ -52,14 +54,16 @@ class SignalIdentityService {
|
||||||
)) {
|
)) {
|
||||||
final bundle = await RustSignal.generateBundle();
|
final bundle = await RustSignal.generateBundle();
|
||||||
|
|
||||||
final pqcRes = await apiService.uploadPqcPreKeys(
|
final pqcRes = await rustApiResult(
|
||||||
bundle.signedPreKeyId,
|
RustApi.uploadPqcPreKeys(
|
||||||
bundle.signedPreKeyPublic,
|
eccSignedPrekeyId: bundle.signedPreKeyId,
|
||||||
bundle.signedPreKeySignature,
|
eccSignedPrekey: bundle.signedPreKeyPublic,
|
||||||
bundle.kyberPreKeyId,
|
eccSignedPrekeySignature: bundle.signedPreKeySignature,
|
||||||
bundle.kyberPreKeyPublic,
|
kyberSignedPrekeyId: bundle.kyberPreKeyId,
|
||||||
bundle.kyberPreKeySignature,
|
kyberSignedPrekey: bundle.kyberPreKeyPublic,
|
||||||
[],
|
kyberSignedPrekeySignature: bundle.kyberPreKeySignature,
|
||||||
|
prekeys: const [],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (pqcRes.isError) {
|
if (pqcRes.isError) {
|
||||||
|
|
|
||||||
|
|
@ -206,7 +206,10 @@ Future<Uint8List?> getPublicKeyFromContact(int contactId) async {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> handleSessionResync(int fromUserId) async {
|
Future<bool> handleSessionResync(int fromUserId) async {
|
||||||
final userData = await apiService.getUserById(fromUserId);
|
final userData = await rustApiProtobuf(
|
||||||
|
RustApi.getUserById(userId: fromUserId),
|
||||||
|
decodeUserData,
|
||||||
|
);
|
||||||
if (userData != null) {
|
if (userData != null) {
|
||||||
Log.info('Got new session data from the server to re-sync the session');
|
Log.info('Got new session data from the server to re-sync the session');
|
||||||
return processSignalUserData(userData);
|
return processSignalUserData(userData);
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,14 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:mutex/mutex.dart';
|
import 'package:mutex/mutex.dart';
|
||||||
import 'package:twonly/core/bridge/wrapper/key_manager.dart';
|
import 'package:twonly/core/bridge/user_config.dart';
|
||||||
|
import 'package:twonly/core/user_config.dart';
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/model/json/userdata.model.dart';
|
|
||||||
import 'package:twonly/src/utils/keyvalue.dart';
|
|
||||||
import 'package:twonly/src/utils/log.dart';
|
import 'package:twonly/src/utils/log.dart';
|
||||||
import 'package:twonly/src/utils/secure_storage.dart';
|
import 'package:twonly/src/utils/secure_storage.dart';
|
||||||
|
|
||||||
class UserService {
|
class UserService {
|
||||||
late UserData currentUser;
|
late UserConfig currentUser;
|
||||||
bool isUserCreated = false;
|
bool isUserCreated = false;
|
||||||
static final Mutex _updateProtection = Mutex();
|
static final Mutex _updateProtection = Mutex();
|
||||||
|
|
||||||
|
|
@ -18,43 +16,26 @@ class UserService {
|
||||||
Stream<void> get onUserUpdated => _userDataUpdateController.stream;
|
Stream<void> get onUserUpdated => _userDataUpdateController.stream;
|
||||||
|
|
||||||
Future<bool> tryInit() async {
|
Future<bool> tryInit() async {
|
||||||
final user = await getUser();
|
final config = await UserConfigApi.load();
|
||||||
if (user == null) return false;
|
if (config == null) return false;
|
||||||
userService.currentUser = user;
|
_applyRustUserConfig(config, notify: false);
|
||||||
userService.isUserCreated = true;
|
return isUserCreated;
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<UserData?> getUser() async {
|
static Future<UserConfig?> getUser() async {
|
||||||
try {
|
try {
|
||||||
// 1. Try to load from KeyValueStore (user.json)
|
final config = await UserConfigApi.load();
|
||||||
final userDataMap = await KeyValueStore.get('user');
|
if (config != null) return config;
|
||||||
if (userDataMap != null) {
|
|
||||||
final userData = UserData.fromJson(userDataMap);
|
|
||||||
await RustKeyManager.setUserId(userId: userData.userId);
|
|
||||||
try {
|
|
||||||
// Ensure that the old userData is removed as it breaks the backup mechanism.
|
|
||||||
// This code can be removed when all users have updated to the latest version...
|
|
||||||
await SecureStorage.instance.delete(key: 'userData');
|
|
||||||
} catch (e) {
|
|
||||||
Log.error('Could not delete user data from SecureStorage: $e');
|
|
||||||
}
|
|
||||||
return userData;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. If not found, try to load from SecureStorage (Migration path)
|
// One-time migration from the pre-user.json secure-storage format.
|
||||||
final userDataJson = await SecureStorage.instance.read(
|
final userDataJson = await SecureStorage.instance.read(
|
||||||
key: 'userData',
|
key: 'userData',
|
||||||
);
|
);
|
||||||
|
|
||||||
if (userDataJson != null) {
|
if (userDataJson != null) {
|
||||||
final userData = UserData.fromJson(
|
final migrated = await UserConfigApi.importJson(json: userDataJson);
|
||||||
jsonDecode(userDataJson) as Map<String, dynamic>,
|
await _removeLegacySecureStorageUser();
|
||||||
);
|
return migrated;
|
||||||
|
|
||||||
// 3. Run migration
|
|
||||||
await _migrateFromSecureStorage(userData);
|
|
||||||
return userData;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -64,15 +45,7 @@ class UserService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> _migrateFromSecureStorage(UserData userData) async {
|
static Future<void> _removeLegacySecureStorageUser() async {
|
||||||
await KeyValueStore.put('user', userData.toJson());
|
|
||||||
|
|
||||||
try {
|
|
||||||
await RustKeyManager.setUserId(userId: userData.userId);
|
|
||||||
} catch (e) {
|
|
||||||
Log.error('Could not set userId in RustKeyManager during migration: $e');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await SecureStorage.instance.delete(key: 'userData');
|
await SecureStorage.instance.delete(key: 'userData');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -83,35 +56,51 @@ class UserService {
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> update(
|
static Future<void> update(
|
||||||
void Function(UserData userData) updateUser,
|
void Function(UserConfig userData) updateUser,
|
||||||
) async {
|
) async {
|
||||||
await _updateProtection.protect(() async {
|
await _updateProtection.protect(() async {
|
||||||
try {
|
try {
|
||||||
final user = await getUser();
|
final config = await UserConfigApi.load();
|
||||||
if (user == null) return;
|
|
||||||
|
if (config == null) {
|
||||||
|
throw Exception('User Config is missing');
|
||||||
|
}
|
||||||
|
|
||||||
|
final user = UserConfigApi.clone(config: config);
|
||||||
if (user.defaultShowTime == 999999) {
|
if (user.defaultShowTime == 999999) {
|
||||||
// This was the old version for infinity -> change it to null
|
// This was the old version for infinity -> change it to null
|
||||||
user.defaultShowTime = null;
|
user.defaultShowTime = null;
|
||||||
}
|
}
|
||||||
updateUser(user);
|
updateUser(user);
|
||||||
await KeyValueStore.put('user', user.toJson());
|
|
||||||
userService.currentUser = user;
|
if (config == user) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final normalized = await UserConfigApi.update(
|
||||||
|
base: config,
|
||||||
|
config: user,
|
||||||
|
);
|
||||||
|
userService._applyRustUserConfig(normalized);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Log.error('Could not update the user: $e');
|
Log.error('Could not update the user: $e');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
userService.triggerUserUpdate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> save(UserData user) async {
|
static Future<void> save(UserConfig user) async {
|
||||||
await KeyValueStore.put('user', user.toJson());
|
final normalized = await UserConfigApi.save(config: user);
|
||||||
try {
|
userService._applyRustUserConfig(normalized);
|
||||||
await RustKeyManager.setUserId(userId: user.userId);
|
|
||||||
} catch (e) {
|
|
||||||
Log.error('Could not set userId in RustKeyManager during save: $e');
|
|
||||||
}
|
}
|
||||||
await userService.tryInit();
|
|
||||||
|
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() {
|
void triggerUserUpdate() {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
@ -9,7 +8,6 @@ import 'package:twonly/globals.dart';
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/database/twonly.db.dart';
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
import 'package:twonly/src/model/protobuf/client/generated/user_discovery/types.pb.dart';
|
import 'package:twonly/src/model/protobuf/client/generated/user_discovery/types.pb.dart';
|
||||||
import 'package:twonly/src/services/signal/identity.signal.dart';
|
|
||||||
import 'package:twonly/src/services/user.service.dart';
|
import 'package:twonly/src/services/user.service.dart';
|
||||||
import 'package:twonly/src/utils/log.dart';
|
import 'package:twonly/src/utils/log.dart';
|
||||||
|
|
||||||
|
|
@ -19,8 +17,9 @@ class UserDiscoveryService {
|
||||||
.getNewAnnouncementsWithoutData();
|
.getNewAnnouncementsWithoutData();
|
||||||
|
|
||||||
for (final announcedUser in announcedUsers) {
|
for (final announcedUser in announcedUsers) {
|
||||||
final userdata = await apiService.getUserById(
|
final userdata = await rustApiProtobuf(
|
||||||
announcedUser.announcedUserId,
|
RustApi.getUserById(userId: announcedUser.announcedUserId),
|
||||||
|
decodeUserData,
|
||||||
);
|
);
|
||||||
if (userdata == null) continue;
|
if (userdata == null) continue;
|
||||||
if (!userdata.publicIdentityKey.equals(
|
if (!userdata.publicIdentityKey.equals(
|
||||||
|
|
@ -49,21 +48,6 @@ class UserDiscoveryService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool isContactAllowed(Contact? c) {
|
|
||||||
if (c == null) return false;
|
|
||||||
final u = userService.currentUser;
|
|
||||||
// Only accepted users are allowed.
|
|
||||||
if (!c.accepted || c.blocked) return false;
|
|
||||||
if (c.mediaSendCounter < u.requiredSendImages) return false;
|
|
||||||
if (c.userDiscoveryExcluded) return false;
|
|
||||||
if (u.userDiscoveryRequiresManualApproval &&
|
|
||||||
(c.userDiscoveryManualApproved == null ||
|
|
||||||
!c.userDiscoveryManualApproved!)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool shouldRequestManualApproval(Contact c) {
|
static bool shouldRequestManualApproval(Contact c) {
|
||||||
final u = userService.currentUser;
|
final u = userService.currentUser;
|
||||||
if (!c.accepted || c.blocked) return false;
|
if (!c.accepted || c.blocked) return false;
|
||||||
|
|
@ -79,20 +63,6 @@ class UserDiscoveryService {
|
||||||
required int threshold,
|
required int threshold,
|
||||||
required bool sharePromotion,
|
required bool sharePromotion,
|
||||||
}) async {
|
}) async {
|
||||||
Log.info('UserDiscoveryService: initializeOrUpdate started');
|
|
||||||
final userId = userService.currentUser.userId;
|
|
||||||
final publicKey = await getUserPublicKey();
|
|
||||||
Log.info('UserDiscoveryService: initializing Rust bridge');
|
|
||||||
await FlutterUserDiscovery.initializeOrUpdate(
|
|
||||||
callbackId: isolateCallbackId,
|
|
||||||
threshold: threshold,
|
|
||||||
userId: userId,
|
|
||||||
publicKey: publicKey,
|
|
||||||
sharePromotion: sharePromotion,
|
|
||||||
).timeout(const Duration(seconds: 8));
|
|
||||||
Log.info(
|
|
||||||
'UserDiscoveryService: Rust bridge initialized, updating UserService',
|
|
||||||
);
|
|
||||||
await UserService.update(
|
await UserService.update(
|
||||||
(u) => u
|
(u) => u
|
||||||
..isUserDiscoveryEnabled = true
|
..isUserDiscoveryEnabled = true
|
||||||
|
|
@ -120,14 +90,6 @@ class UserDiscoveryService {
|
||||||
return UserDiscoveryVersion.fromBuffer(version);
|
return UserDiscoveryVersion.fromBuffer(version);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<UserDiscoveryVersion?> getContactVersionTyped(
|
|
||||||
int contactId,
|
|
||||||
) async {
|
|
||||||
final contact = await twonlyDB.contactsDao.getContactById(contactId);
|
|
||||||
if (contact == null || contact.userDiscoveryVersion == null) return null;
|
|
||||||
return UserDiscoveryVersion.fromBuffer(contact.userDiscoveryVersion!);
|
|
||||||
}
|
|
||||||
|
|
||||||
static UserDiscoveryVersion? getContactVersionTypedFromContact(
|
static UserDiscoveryVersion? getContactVersionTypedFromContact(
|
||||||
Contact contact,
|
Contact contact,
|
||||||
) {
|
) {
|
||||||
|
|
@ -135,89 +97,14 @@ class UserDiscoveryService {
|
||||||
return UserDiscoveryVersion.fromBuffer(contact.userDiscoveryVersion!);
|
return UserDiscoveryVersion.fromBuffer(contact.userDiscoveryVersion!);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<Uint8List?> shouldRequestNewMessages(
|
|
||||||
int fromUserId,
|
|
||||||
List<int> receivedVersion,
|
|
||||||
) async {
|
|
||||||
try {
|
|
||||||
return await FlutterUserDiscovery.shouldRequestNewMessages(
|
|
||||||
callbackId: isolateCallbackId,
|
|
||||||
contactId: fromUserId,
|
|
||||||
version: receivedVersion,
|
|
||||||
).timeout(const Duration(seconds: 5));
|
|
||||||
} catch (e) {
|
|
||||||
Log.error(e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<List<Uint8List>?> getNewMessages(
|
|
||||||
int fromUserId,
|
|
||||||
List<int> receivedVersion,
|
|
||||||
) async {
|
|
||||||
try {
|
|
||||||
return await FlutterUserDiscovery.getNewMessages(
|
|
||||||
callbackId: isolateCallbackId,
|
|
||||||
contactId: fromUserId,
|
|
||||||
receivedVersion: receivedVersion,
|
|
||||||
).timeout(const Duration(seconds: 5));
|
|
||||||
} catch (e) {
|
|
||||||
Log.error(e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<void> handleNewMessages(
|
|
||||||
int fromUserId,
|
|
||||||
List<Uint8List> messages,
|
|
||||||
) async {
|
|
||||||
try {
|
|
||||||
final verifications = await twonlyDB.keyVerificationDao
|
|
||||||
.getContactVerification(fromUserId);
|
|
||||||
|
|
||||||
return await FlutterUserDiscovery.handleNewMessages(
|
|
||||||
callbackId: isolateCallbackId,
|
|
||||||
contactId: fromUserId,
|
|
||||||
messages: messages,
|
|
||||||
publicKeyVerifiedTimestamp:
|
|
||||||
verifications.lastOrNull?.createdAt.millisecondsSinceEpoch,
|
|
||||||
).timeout(const Duration(seconds: 5));
|
|
||||||
} catch (e) {
|
|
||||||
Log.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<void> _removeDeletedContacts() async {
|
|
||||||
final subquery = twonlyDB.selectOnly(twonlyDB.contacts)
|
|
||||||
..addColumns([twonlyDB.contacts.userId])
|
|
||||||
..where(twonlyDB.contacts.accountDeleted.equals(true));
|
|
||||||
|
|
||||||
await (twonlyDB.update(
|
|
||||||
twonlyDB.userDiscoveryOwnPromotions,
|
|
||||||
)..where((t) => t.contactId.isInQuery(subquery))).write(
|
|
||||||
UserDiscoveryOwnPromotionsCompanion(promotion: Value(Uint8List(0))),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<void> changeExclusionForContact(
|
static Future<void> changeExclusionForContact(
|
||||||
int contactId,
|
int contactId,
|
||||||
bool exclude,
|
bool exclude,
|
||||||
) async {
|
) async {
|
||||||
// Remove old versions from the user...
|
await FlutterUserDiscovery.changeExclusionForContact(
|
||||||
await (twonlyDB.update(
|
callbackId: isolateCallbackId,
|
||||||
twonlyDB.userDiscoveryOwnPromotions,
|
contactId: contactId,
|
||||||
)..where((t) => t.contactId.equals(contactId))).write(
|
exclude: exclude,
|
||||||
UserDiscoveryOwnPromotionsCompanion(promotion: Value(Uint8List(0))),
|
|
||||||
);
|
|
||||||
|
|
||||||
await twonlyDB.contactsDao.updateContact(
|
|
||||||
contactId,
|
|
||||||
ContactsCompanion(
|
|
||||||
userDiscoveryExcluded: Value(exclude),
|
|
||||||
userDiscoveryVersion: const Value(
|
|
||||||
null, // If the user is included again, this will trigger a new request of his original announcement
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -226,35 +113,4 @@ class UserDiscoveryService {
|
||||||
u.isUserDiscoveryEnabled = false;
|
u.isUserDiscoveryEnabled = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> verifyInitializationOnStartup() async {
|
|
||||||
await _removeDeletedContacts();
|
|
||||||
final configExists = File(
|
|
||||||
'${AppEnvironment.supportDir}/user_discovery_config.json',
|
|
||||||
).existsSync();
|
|
||||||
final hasShares = await (twonlyDB.select(
|
|
||||||
twonlyDB.userDiscoveryShares,
|
|
||||||
)..limit(1)).get().then((list) => list.isNotEmpty);
|
|
||||||
|
|
||||||
if (userService.currentUser.isUserDiscoveryEnabled &&
|
|
||||||
(userService.currentUser.userDiscoveryInitializationError ||
|
|
||||||
!configExists ||
|
|
||||||
!hasShares)) {
|
|
||||||
unawaited(() async {
|
|
||||||
try {
|
|
||||||
Log.info(
|
|
||||||
'Retrying UserDiscovery initialization on startup (configExists: $configExists, hasShares: $hasShares)',
|
|
||||||
);
|
|
||||||
await initializeOrUpdate(
|
|
||||||
threshold: userService.currentUser.userDiscoveryThreshold,
|
|
||||||
sharePromotion: userService.currentUser.userDiscoverySharePromotion,
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
Log.error(
|
|
||||||
'Failed to retry UserDiscovery initialization on startup: $e',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,7 @@ class _ChatAskAFriendEntryState extends State<ChatAskAFriendEntry> {
|
||||||
_username = contact.displayName ?? contact.username;
|
_username = contact.displayName ?? contact.username;
|
||||||
} else {
|
} else {
|
||||||
// Fetch from API
|
// Fetch from API
|
||||||
final userdata = await apiService.getUserById(userId);
|
final userdata = await rustApiProtobuf(RustApi.getUserById(userId: userId), decodeUserData);
|
||||||
if (userdata != null) {
|
if (userdata != null) {
|
||||||
_username = utf8.decode(userdata.username);
|
_username = utf8.decode(userdata.username);
|
||||||
}
|
}
|
||||||
|
|
@ -122,7 +122,7 @@ class _ChatAskAFriendEntryState extends State<ChatAskAFriendEntry> {
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
final userId = _data!.askAboutUserId.toInt();
|
final userId = _data!.askAboutUserId.toInt();
|
||||||
final userdata = await apiService.getUserById(userId);
|
final userdata = await rustApiProtobuf(RustApi.getUserById(userId: userId), decodeUserData);
|
||||||
if (userdata != null) {
|
if (userdata != null) {
|
||||||
await twonlyDB.contactsDao.insertOnConflictUpdate(
|
await twonlyDB.contactsDao.insertOnConflictUpdate(
|
||||||
ContactsCompanion(
|
ContactsCompanion(
|
||||||
|
|
|
||||||
|
|
@ -117,8 +117,9 @@ class _ContactRowState extends State<_ContactRow> {
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final userdata = await apiService.getUserById(
|
final userdata = await rustApiProtobuf(
|
||||||
widget.contact.userId.toInt(),
|
RustApi.getUserById(userId: widget.contact.userId.toInt()),
|
||||||
|
decodeUserData,
|
||||||
);
|
);
|
||||||
if (userdata == null) return;
|
if (userdata == null) return;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -369,7 +369,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
||||||
'Calling downloadDone for media ID: ${currentMediaLocal.mediaFile.mediaId}',
|
'Calling downloadDone for media ID: ${currentMediaLocal.mediaFile.mediaId}',
|
||||||
);
|
);
|
||||||
unawaited(
|
unawaited(
|
||||||
apiService.downloadDone(currentMediaLocal.mediaFile.downloadToken!),
|
RustApi.downloadDone(token: currentMediaLocal.mediaFile.downloadToken!),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (currentMediaLocal.mediaFile.type == MediaType.video) {
|
if (currentMediaLocal.mediaFile.type == MediaType.video) {
|
||||||
|
|
|
||||||
|
|
@ -117,7 +117,10 @@ class _SearchUsernameView extends State<AddNewUserView> {
|
||||||
_isLoading = true;
|
_isLoading = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
final userdata = await apiService.getUserData(username);
|
final userdata = await rustApiProtobuf(
|
||||||
|
RustApi.getUserData(username: username),
|
||||||
|
decodeUserData,
|
||||||
|
);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ class FriendSuggestionsComp extends StatelessWidget {
|
||||||
) async {
|
) async {
|
||||||
Log.info('Requesting user via friend suggestions');
|
Log.info('Requesting user via friend suggestions');
|
||||||
|
|
||||||
final userdata = await apiService.getUserById(user.announcedUserId);
|
final userdata = await rustApiProtobuf(RustApi.getUserById(userId: user.announcedUserId), decodeUserData);
|
||||||
|
|
||||||
if (userdata == null) {
|
if (userdata == null) {
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
|
|
|
||||||
|
|
@ -123,7 +123,7 @@ class _ContactViewState extends State<ContactView> {
|
||||||
Future<void> handleReportUser(Contact contact) async {
|
Future<void> handleReportUser(Contact contact) async {
|
||||||
final reason = await showReportDialog(context, contact);
|
final reason = await showReportDialog(context, contact);
|
||||||
if (reason == null) return;
|
if (reason == null) return;
|
||||||
final res = await apiService.reportUser(contact.userId, reason);
|
final res = await rustApiResult(RustApi.reportUser(userId: contact.userId, reason: reason));
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
if (res.isSuccess) {
|
if (res.isSuccess) {
|
||||||
showSnackbar(
|
showSnackbar(
|
||||||
|
|
@ -281,7 +281,7 @@ class _ContactViewState extends State<ContactView> {
|
||||||
icon: FontAwesomeIcons.arrowsRotate,
|
icon: FontAwesomeIcons.arrowsRotate,
|
||||||
text: 'Update Connection to V2 (PQXDH)',
|
text: 'Update Connection to V2 (PQXDH)',
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
final userData = await apiService.getUserById(contact.userId);
|
final userData = await rustApiProtobuf(RustApi.getUserById(userId: contact.userId), decodeUserData);
|
||||||
if (userData != null) {
|
if (userData != null) {
|
||||||
await processSignalUserData(userData);
|
await processSignalUserData(userData);
|
||||||
final updatedContact = await twonlyDB.contactsDao
|
final updatedContact = await twonlyDB.contactsDao
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ class MemoriesViewState extends State<MemoriesView>
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _checkUsage() async {
|
Future<void> _checkUsage() async {
|
||||||
final usage = await apiService.getMemoriesUsage();
|
final usage = await rustApiProtobuf(RustApi.getMemoriesUsage(), decodeMemoriesUsage);
|
||||||
if (usage != null &&
|
if (usage != null &&
|
||||||
usage.maxBytes > 0 &&
|
usage.maxBytes > 0 &&
|
||||||
usage.currentBytes >= usage.maxBytes) {
|
usage.currentBytes >= usage.maxBytes) {
|
||||||
|
|
@ -320,7 +320,7 @@ class MemoriesViewState extends State<MemoriesView>
|
||||||
if (item != null) {
|
if (item != null) {
|
||||||
if (isCompletely) {
|
if (isCompletely) {
|
||||||
item.mediaService.fullMediaRemoval();
|
item.mediaService.fullMediaRemoval();
|
||||||
await apiService.deleteMemory(mediaId);
|
await RustApi.deleteMemory(mediaId: mediaId);
|
||||||
await twonlyDB.mediaFilesDao.deleteMediaFile(mediaId);
|
await twonlyDB.mediaFilesDao.deleteMediaFile(mediaId);
|
||||||
} else {
|
} else {
|
||||||
if (item.mediaService.storedPath.existsSync()) {
|
if (item.mediaService.storedPath.existsSync()) {
|
||||||
|
|
|
||||||
|
|
@ -177,7 +177,7 @@ class _SynchronizedImageViewerScreenState
|
||||||
|
|
||||||
if (deleteCompletely) {
|
if (deleteCompletely) {
|
||||||
item.mediaService.fullMediaRemoval();
|
item.mediaService.fullMediaRemoval();
|
||||||
await apiService.deleteMemory(mediaId);
|
await RustApi.deleteMemory(mediaId: mediaId);
|
||||||
await twonlyDB.mediaFilesDao.deleteMediaFile(mediaId);
|
await twonlyDB.mediaFilesDao.deleteMediaFile(mediaId);
|
||||||
|
|
||||||
widget.galleryItems.removeAt(_currentIndex);
|
widget.galleryItems.removeAt(_currentIndex);
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,6 @@ import 'package:twonly/core/bridge/wrapper.dart' show RustUtils;
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/constants/keyvalue.keys.dart';
|
import 'package:twonly/src/constants/keyvalue.keys.dart';
|
||||||
import 'package:twonly/src/model/json/onboarding_state.model.dart';
|
import 'package:twonly/src/model/json/onboarding_state.model.dart';
|
||||||
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
|
|
||||||
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart'
|
|
||||||
as server;
|
|
||||||
import 'package:twonly/src/model/protobuf/client/generated/passwordless_recovery.pb.dart';
|
import 'package:twonly/src/model/protobuf/client/generated/passwordless_recovery.pb.dart';
|
||||||
import 'package:twonly/src/services/backup.service.dart';
|
import 'package:twonly/src/services/backup.service.dart';
|
||||||
import 'package:twonly/src/services/passwordless_recovery.service.dart';
|
import 'package:twonly/src/services/passwordless_recovery.service.dart';
|
||||||
|
|
@ -262,10 +259,13 @@ class _RecoverPasswordlessState extends State<RecoverPasswordless> {
|
||||||
);
|
);
|
||||||
|
|
||||||
// Fetch serverKey
|
// Fetch serverKey
|
||||||
final res = await apiService.getServerKeyForPasswordlessRecovery(
|
final res = await rustApiResult(
|
||||||
|
RustApi.getServerKeyForPasswordlessRecovery(
|
||||||
userId: userId,
|
userId: userId,
|
||||||
pinUnlockToken: reconstructed.pinUnlockToken,
|
serverKeyProtection: const [],
|
||||||
pinProtectionKey: await pinKey.extractBytes(),
|
pinUnlockToken: Uint8List.fromList(reconstructed.pinUnlockToken),
|
||||||
|
pinProtectionKey: Uint8List.fromList(await pinKey.extractBytes()),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (res.isError) {
|
if (res.isError) {
|
||||||
|
|
@ -281,8 +281,7 @@ class _RecoverPasswordlessState extends State<RecoverPasswordless> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final ok = res.value as server.Response_Ok;
|
serverKey = Uint8List.fromList(res.value!);
|
||||||
serverKey = Uint8List.fromList(ok.passwordlessRecoveryServerKey);
|
|
||||||
} else if (reconstructed.hasEmailHint()) {
|
} else if (reconstructed.hasEmailHint()) {
|
||||||
final state = _onboardingState;
|
final state = _onboardingState;
|
||||||
if (state == null) return;
|
if (state == null) return;
|
||||||
|
|
@ -302,10 +301,12 @@ class _RecoverPasswordlessState extends State<RecoverPasswordless> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch serverKey (sends recovery email)
|
// Fetch serverKey (sends recovery email)
|
||||||
final res = await apiService.getServerKeyForPasswordlessRecovery(
|
final res = await rustApiResult(
|
||||||
|
RustApi.getServerKeyForPasswordlessRecovery(
|
||||||
userId: userId,
|
userId: userId,
|
||||||
email: email,
|
email: email,
|
||||||
serverKeyProtection: reconstructed.serverKeyProtection,
|
serverKeyProtection: reconstructed.serverKeyProtection,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (res.isError) {
|
if (res.isError) {
|
||||||
|
|
@ -444,11 +445,13 @@ class _RecoverPasswordlessState extends State<RecoverPasswordless> {
|
||||||
Future<void> _registerPasswordlessNotification(OnboardingState state) async {
|
Future<void> _registerPasswordlessNotification(OnboardingState state) async {
|
||||||
final fcmToken = await FirebaseMessaging.instance.getToken();
|
final fcmToken = await FirebaseMessaging.instance.getToken();
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
final res = await apiService.registerPasswordlessNotification(
|
final res = await rustApiResult(
|
||||||
|
RustApi.registerPasswordlessNotification(
|
||||||
notificationId: state.notificationId!,
|
notificationId: state.notificationId!,
|
||||||
downloadAuthToken: state.downloadAuthToken!,
|
downloadAuthToken: state.downloadAuthToken!,
|
||||||
langCode: Localizations.localeOf(context).languageCode,
|
langCode: Localizations.localeOf(context).languageCode,
|
||||||
googleFcm: fcmToken,
|
googleFcm: fcmToken,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
if (res.isSuccess) {
|
if (res.isSuccess) {
|
||||||
state.serverRegistered = true;
|
state.serverRegistered = true;
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,16 @@
|
||||||
// ignore_for_file: avoid_dynamic_calls
|
// ignore_for_file: avoid_dynamic_calls
|
||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
|
import 'dart:ui' as ui;
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:twonly/core/bridge/user_config.dart';
|
||||||
import 'package:twonly/globals.dart';
|
import 'package:twonly/globals.dart';
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/constants/routes.keys.dart';
|
import 'package:twonly/src/constants/routes.keys.dart';
|
||||||
import 'package:twonly/src/model/json/userdata.model.dart';
|
|
||||||
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
|
|
||||||
import 'package:twonly/src/services/notifications/fcm.notifications.dart';
|
import 'package:twonly/src/services/notifications/fcm.notifications.dart';
|
||||||
import 'package:twonly/src/services/signal/identity.signal.dart';
|
import 'package:twonly/src/services/signal/identity.signal.dart';
|
||||||
import 'package:twonly/src/services/user.service.dart';
|
import 'package:twonly/src/services/user.service.dart';
|
||||||
|
|
@ -77,7 +78,12 @@ class _RegisterViewState extends State<RegisterView> {
|
||||||
if (proofOfWork != null) {
|
if (proofOfWork != null) {
|
||||||
proof = await proofOfWork!;
|
proof = await proofOfWork!;
|
||||||
} else {
|
} else {
|
||||||
final (pow, registrationDisabled) = await apiService.getProofOfWork();
|
final proofResult = await rustApiResult(RustApi.getProofOfWork());
|
||||||
|
final pow = proofResult.value == null
|
||||||
|
? null
|
||||||
|
: decodeProofOfWork(proofResult.value!);
|
||||||
|
final registrationDisabled =
|
||||||
|
proofResult.error == ErrorCode.RegistrationDisabled;
|
||||||
if (pow == null) {
|
if (pow == null) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_registrationDisabled = registrationDisabled;
|
_registrationDisabled = registrationDisabled;
|
||||||
|
|
@ -97,10 +103,17 @@ class _RegisterViewState extends State<RegisterView> {
|
||||||
|
|
||||||
var userId = 0;
|
var userId = 0;
|
||||||
|
|
||||||
final res = await apiService.register(username, null, proof);
|
final res = await rustApiResult(
|
||||||
|
RustApi.register(
|
||||||
|
username: username,
|
||||||
|
proofOfWork: proof,
|
||||||
|
langCode: ui.PlatformDispatcher.instance.locale.languageCode,
|
||||||
|
isIos: Platform.isIOS,
|
||||||
|
),
|
||||||
|
);
|
||||||
if (res.isSuccess) {
|
if (res.isSuccess) {
|
||||||
Log.info('Got user_id ${res.value} from server');
|
Log.info('Got user_id ${res.value} from server');
|
||||||
userId = res.value.userid.toInt() as int;
|
userId = res.value!;
|
||||||
} else {
|
} else {
|
||||||
proofOfWork = null;
|
proofOfWork = null;
|
||||||
if (res.error == ErrorCode.RegistrationDisabled) {
|
if (res.error == ErrorCode.RegistrationDisabled) {
|
||||||
|
|
@ -120,7 +133,7 @@ class _RegisterViewState extends State<RegisterView> {
|
||||||
setState(() {
|
setState(() {
|
||||||
_usernameErrorText = errorCodeToText(
|
_usernameErrorText = errorCodeToText(
|
||||||
context,
|
context,
|
||||||
res.error as ErrorCode,
|
res.error!,
|
||||||
);
|
);
|
||||||
_isTryingToRegister = false;
|
_isTryingToRegister = false;
|
||||||
});
|
});
|
||||||
|
|
@ -141,7 +154,7 @@ class _RegisterViewState extends State<RegisterView> {
|
||||||
await showAlertDialog(
|
await showAlertDialog(
|
||||||
context,
|
context,
|
||||||
'Oh no!',
|
'Oh no!',
|
||||||
errorCodeToText(context, res.error as ErrorCode),
|
errorCodeToText(context, res.error!),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
|
@ -151,11 +164,10 @@ class _RegisterViewState extends State<RegisterView> {
|
||||||
_isTryingToRegister = false;
|
_isTryingToRegister = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
final userData = UserData(
|
final userData = await UserConfigApi.create(
|
||||||
userId: userId,
|
userId: userId,
|
||||||
username: username,
|
username: username,
|
||||||
displayName: username,
|
displayName: username,
|
||||||
subscriptionPlan: 'Free',
|
|
||||||
currentSetupPage: SetupPages.profile.name,
|
currentSetupPage: SetupPages.profile.name,
|
||||||
appVersion: AppState.latestAppVersionId,
|
appVersion: AppState.latestAppVersionId,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:twonly/core/user_config.dart';
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/services/profile.service.dart';
|
|
||||||
import 'package:twonly/src/services/user.service.dart';
|
import 'package:twonly/src/services/user.service.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
import 'package:twonly/src/visual/elements/my_button.element.dart';
|
import 'package:twonly/src/visual/elements/my_button.element.dart';
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:twonly/src/services/profile.service.dart';
|
import 'package:twonly/core/user_config.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
|
||||||
class SafetyProfileCard extends StatelessWidget {
|
class SafetyProfileCard extends StatelessWidget {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:twonly/core/user_config.dart';
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/services/profile.service.dart';
|
|
||||||
import 'package:twonly/src/services/user.service.dart';
|
import 'package:twonly/src/services/user.service.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
import 'package:twonly/src/visual/views/onboarding/setup/components/next_button.comp.dart';
|
import 'package:twonly/src/visual/views/onboarding/setup/components/next_button.comp.dart';
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ class AccountView extends StatelessWidget {
|
||||||
context.lang.settingsAccountDeleteModalBody,
|
context.lang.settingsAccountDeleteModalBody,
|
||||||
);
|
);
|
||||||
if (ok) {
|
if (ok) {
|
||||||
final res = await apiService.deleteAccount();
|
final res = await rustApiResult(RustApi.deleteAccount());
|
||||||
if (res.isError) {
|
if (res.isError) {
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
showSnackbar(
|
showSnackbar(
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ class _BackupViewState extends State<BackupView> {
|
||||||
Future<void> _loadBackupStatus() async {
|
Future<void> _loadBackupStatus() async {
|
||||||
setState(() => _isLoading = true);
|
setState(() => _isLoading = true);
|
||||||
final status = await BackupService.getData();
|
final status = await BackupService.getData();
|
||||||
final memoriesUsage = await apiService.getMemoriesUsage();
|
final memoriesUsage = await rustApiProtobuf(RustApi.getMemoriesUsage(), decodeMemoriesUsage);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
_backupStatus = status;
|
_backupStatus = status;
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@ Future<bool> promptAndDisableMemoriesBackup(BuildContext context) async {
|
||||||
|
|
||||||
if (confirmed == true) {
|
if (confirmed == true) {
|
||||||
try {
|
try {
|
||||||
await apiService.disableMemoriesBackup();
|
await RustApi.disableMemoriesBackup();
|
||||||
final allMedias = await (twonlyDB.select(
|
final allMedias = await (twonlyDB.select(
|
||||||
twonlyDB.mediaFiles,
|
twonlyDB.mediaFiles,
|
||||||
)..where((t) => t.stored.equals(true))).get();
|
)..where((t) => t.stored.equals(true))).get();
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ class _MemoriesBackupDetailViewState extends State<MemoriesBackupDetailView> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadStats() async {
|
Future<void> _loadStats() async {
|
||||||
final memoriesUsage = await apiService.getMemoriesUsage();
|
final memoriesUsage = await rustApiProtobuf(RustApi.getMemoriesUsage(), decodeMemoriesUsage);
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_memoriesUsage = memoriesUsage;
|
_memoriesUsage = memoriesUsage;
|
||||||
|
|
|
||||||
|
|
@ -335,7 +335,7 @@ class _StorageContentsViewState extends State<StorageContentsView> {
|
||||||
try {
|
try {
|
||||||
if (deleteCompletely) {
|
if (deleteCompletely) {
|
||||||
await twonlyDB.mediaFilesDao.deleteMediaFile(file.mediaId);
|
await twonlyDB.mediaFilesDao.deleteMediaFile(file.mediaId);
|
||||||
unawaited(apiService.deleteMemory(file.mediaId));
|
unawaited(RustApi.deleteMemory(mediaId: file.mediaId));
|
||||||
MediaFileService(file).fullMediaRemoval();
|
MediaFileService(file).fullMediaRemoval();
|
||||||
} else {
|
} else {
|
||||||
MediaFileService(file).storedPath.deleteSync();
|
MediaFileService(file).storedPath.deleteSync();
|
||||||
|
|
@ -384,7 +384,7 @@ class _StorageContentsViewState extends State<StorageContentsView> {
|
||||||
for (final file in selectedFiles) {
|
for (final file in selectedFiles) {
|
||||||
if (deleteCompletely) {
|
if (deleteCompletely) {
|
||||||
await twonlyDB.mediaFilesDao.deleteMediaFile(file.mediaId);
|
await twonlyDB.mediaFilesDao.deleteMediaFile(file.mediaId);
|
||||||
unawaited(apiService.deleteMemory(file.mediaId));
|
unawaited(RustApi.deleteMemory(mediaId: file.mediaId));
|
||||||
MediaFileService(file).fullMediaRemoval();
|
MediaFileService(file).fullMediaRemoval();
|
||||||
} else {
|
} else {
|
||||||
MediaFileService(file).storedPath.deleteSync();
|
MediaFileService(file).storedPath.deleteSync();
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,7 @@ import 'package:hashlib/random.dart';
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||||
import 'package:twonly/src/database/twonly.db.dart';
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'
|
|
||||||
as pb;
|
|
||||||
import 'package:twonly/src/services/api/messages.api.dart';
|
import 'package:twonly/src/services/api/messages.api.dart';
|
||||||
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
|
|
||||||
import 'package:twonly/src/visual/components/alert.dialog.dart';
|
import 'package:twonly/src/visual/components/alert.dialog.dart';
|
||||||
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
|
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
|
||||||
|
|
||||||
|
|
@ -186,24 +183,7 @@ class _RetransmissionDataViewState extends State<RetransmissionDataView> {
|
||||||
Text(
|
Text(
|
||||||
'MessageId: ${retrans.receipt.messageId}',
|
'MessageId: ${retrans.receipt.messageId}',
|
||||||
),
|
),
|
||||||
if (retrans.receipt.messageId != null)
|
|
||||||
FutureBuilder(
|
|
||||||
future: getPushNotificationFromEncryptedContent(
|
|
||||||
retrans.receipt.contactId,
|
|
||||||
retrans.receipt.messageId,
|
|
||||||
pb.EncryptedContent.fromBuffer(
|
|
||||||
pb.Message.fromBuffer(
|
|
||||||
retrans.receipt.message,
|
|
||||||
).encryptedContent,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
builder: (d, a) {
|
|
||||||
if (!a.hasData) return Container();
|
|
||||||
return Text(
|
|
||||||
'PushKind: ${a.data?.kind}',
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Text(
|
Text(
|
||||||
'Retry: ${retrans.receipt.retryCount} : ${retrans.receipt.lastRetry}',
|
'Retry: ${retrans.receipt.retryCount} : ${retrans.receipt.lastRetry}',
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,9 @@
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:hashlib/random.dart';
|
|
||||||
import 'package:permission_handler/permission_handler.dart';
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/model/protobuf/client/generated/push_notification.pb.dart';
|
|
||||||
import 'package:twonly/src/services/notifications/fcm.notifications.dart';
|
import 'package:twonly/src/services/notifications/fcm.notifications.dart';
|
||||||
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
|
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
import 'package:twonly/src/visual/components/alert.dialog.dart';
|
import 'package:twonly/src/visual/components/alert.dialog.dart';
|
||||||
|
|
||||||
|
|
@ -47,8 +42,6 @@ class _NotificationViewState extends State<NotificationView> {
|
||||||
|
|
||||||
await FcmNotificationService.initFCMAfterAuthenticated(force: true);
|
await FcmNotificationService.initFCMAfterAuthenticated(force: true);
|
||||||
|
|
||||||
await setupNotificationWithUsers(force: true);
|
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
||||||
if (userService.currentUser.fcmToken == null) {
|
if (userService.currentUser.fcmToken == null) {
|
||||||
|
|
@ -66,18 +59,6 @@ class _NotificationViewState extends State<NotificationView> {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (run) {
|
if (run) {
|
||||||
final pushData = await encryptPushNotification(
|
|
||||||
userService.currentUser.userId,
|
|
||||||
PushNotification(
|
|
||||||
messageId: uuid.v4(),
|
|
||||||
kind: PushKind.TEST_NOTIFICATION,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
await apiService.sendTextMessage(
|
|
||||||
userService.currentUser.userId,
|
|
||||||
Uint8List(0),
|
|
||||||
pushData,
|
|
||||||
);
|
|
||||||
_troubleshootingDidRun = true;
|
_troubleshootingDidRun = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/constants/routes.keys.dart';
|
import 'package:twonly/src/constants/routes.keys.dart';
|
||||||
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
|
|
||||||
import 'package:twonly/src/services/user.service.dart';
|
import 'package:twonly/src/services/user.service.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
|
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
|
||||||
|
|
@ -62,7 +61,9 @@ class _ProfileViewState extends State<ProfileView> {
|
||||||
filteredUsername = filteredUsername.substring(0, 12);
|
filteredUsername = filteredUsername.substring(0, 12);
|
||||||
}
|
}
|
||||||
|
|
||||||
final result = await apiService.changeUsername(filteredUsername);
|
final result = await rustApiResult(
|
||||||
|
RustApi.changeUsername(username: filteredUsername),
|
||||||
|
);
|
||||||
if (result.isError) {
|
if (result.isError) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,10 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:fixnum/fixnum.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||||
import 'package:twonly/src/model/protobuf/api/websocket/error.pbserver.dart';
|
|
||||||
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart';
|
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart';
|
||||||
import 'package:twonly/src/providers/purchases.provider.dart';
|
import 'package:twonly/src/providers/purchases.provider.dart';
|
||||||
import 'package:twonly/src/services/subscription.service.dart';
|
import 'package:twonly/src/services/subscription.service.dart';
|
||||||
|
|
@ -50,7 +48,10 @@ class _AdditionalUsersViewState extends State<AdditionalUsersView> {
|
||||||
|
|
||||||
Future<void> initAsync({required bool force}) async {
|
Future<void> initAsync({required bool force}) async {
|
||||||
if (force) {
|
if (force) {
|
||||||
ballance = await apiService.loadPlanBalance();
|
ballance = await rustApiProtobuf(
|
||||||
|
RustApi.loadPlanBalance(),
|
||||||
|
decodePlanBalance,
|
||||||
|
);
|
||||||
_unusedAdditionalAccounts =
|
_unusedAdditionalAccounts =
|
||||||
_planLimit - (ballance?.additionalAccounts.length ?? _planLimit);
|
_planLimit - (ballance?.additionalAccounts.length ?? _planLimit);
|
||||||
}
|
}
|
||||||
|
|
@ -72,7 +73,9 @@ class _AdditionalUsersViewState extends State<AdditionalUsersView> {
|
||||||
as List<int>?;
|
as List<int>?;
|
||||||
if (selectedUserIds == null) return;
|
if (selectedUserIds == null) return;
|
||||||
for (final selectedUserId in selectedUserIds) {
|
for (final selectedUserId in selectedUserIds) {
|
||||||
final res = await apiService.addAdditionalUser(Int64(selectedUserId));
|
final res = await rustApiResult(
|
||||||
|
RustApi.addAdditionalUser(userId: selectedUserId),
|
||||||
|
);
|
||||||
if (res.isError && mounted) {
|
if (res.isError && mounted) {
|
||||||
final contact = await twonlyDB.contactsDao.getContactById(
|
final contact = await twonlyDB.contactsDao.getContactById(
|
||||||
selectedUserId,
|
selectedUserId,
|
||||||
|
|
@ -220,8 +223,10 @@ class _AdditionalAccountState extends State<AdditionalAccount> {
|
||||||
context.lang.additionalUserRemoveDesc,
|
context.lang.additionalUserRemoveDesc,
|
||||||
);
|
);
|
||||||
if (remove) {
|
if (remove) {
|
||||||
final res = await apiService.removeAdditionalUser(
|
final res = await rustApiResult(
|
||||||
widget.account.userId,
|
RustApi.removeAdditionalUser(
|
||||||
|
userId: widget.account.userId.toInt(),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
if (res.isSuccess) {
|
if (res.isSuccess) {
|
||||||
|
|
@ -231,7 +236,7 @@ class _AdditionalAccountState extends State<AdditionalAccount> {
|
||||||
context,
|
context,
|
||||||
errorCodeToText(
|
errorCodeToText(
|
||||||
context,
|
context,
|
||||||
res.error as ErrorCode,
|
res.error!,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
@ -35,7 +36,10 @@ class _SubscriptionViewState extends State<SubscriptionView> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> initAsync() async {
|
Future<void> initAsync() async {
|
||||||
ballance = await apiService.loadPlanBalance();
|
ballance = await rustApiProtobuf(
|
||||||
|
RustApi.loadPlanBalance(),
|
||||||
|
decodePlanBalance,
|
||||||
|
);
|
||||||
if (ballance != null && ballance!.hasAdditionalAccountOwnerId()) {
|
if (ballance != null && ballance!.hasAdditionalAccountOwnerId()) {
|
||||||
final ownerId = ballance!.additionalAccountOwnerId.toInt();
|
final ownerId = ballance!.additionalAccountOwnerId.toInt();
|
||||||
final contact = await twonlyDB.contactsDao
|
final contact = await twonlyDB.contactsDao
|
||||||
|
|
@ -49,7 +53,7 @@ class _SubscriptionViewState extends State<SubscriptionView> {
|
||||||
}
|
}
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {});
|
setState(() {});
|
||||||
await apiService.forceIpaCheck();
|
await RustApi.forceIpaCheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
||||||
|
|
@ -866,7 +866,7 @@ packages:
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.0.0"
|
version: "4.0.0"
|
||||||
freezed_annotation:
|
freezed_annotation:
|
||||||
dependency: "direct main"
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: freezed_annotation
|
name: freezed_annotation
|
||||||
sha256: "7294967ff0a6d98638e7acb774aac3af2550777accd8149c90af5b014e6d44d8"
|
sha256: "7294967ff0a6d98638e7acb774aac3af2550777accd8149c90af5b014e6d44d8"
|
||||||
|
|
|
||||||
|
|
@ -258,4 +258,3 @@ flutter:
|
||||||
fonts:
|
fonts:
|
||||||
- asset: assets/fonts/NotoColorEmoji.ttf
|
- asset: assets/fonts/NotoColorEmoji.ttf
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
2
rust/Cargo.lock
generated
2
rust/Cargo.lock
generated
|
|
@ -97,6 +97,7 @@ dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"atomic",
|
"atomic",
|
||||||
"backtrace",
|
"backtrace",
|
||||||
|
"chrono",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1093,6 +1094,7 @@ dependencies = [
|
||||||
"build-target",
|
"build-target",
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"byteorder",
|
"byteorder",
|
||||||
|
"chrono",
|
||||||
"console_error_panic_hook",
|
"console_error_panic_hook",
|
||||||
"dart-sys",
|
"dart-sys",
|
||||||
"delegate-attr",
|
"delegate-attr",
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ edition = "2021"
|
||||||
crate-type = ["cdylib", "staticlib", "rlib"]
|
crate-type = ["cdylib", "staticlib", "rlib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
flutter_rust_bridge = "=2.12.0"
|
flutter_rust_bridge = { version = "=2.12.0", features = ["chrono"] }
|
||||||
thiserror = "2.0.18"
|
thiserror = "2.0.18"
|
||||||
sqlx = { version = "0.9.0-alpha.1", default-features = false, features = [
|
sqlx = { version = "0.9.0-alpha.1", default-features = false, features = [
|
||||||
"runtime-tokio",
|
"runtime-tokio",
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ fn main() -> Result<()> {
|
||||||
.include_file("websocket_protocol.rs")
|
.include_file("websocket_protocol.rs")
|
||||||
.compile_protos(&websocket_protos, &[websocket_proto_root])?;
|
.compile_protos(&websocket_protos, &[websocket_proto_root])?;
|
||||||
|
|
||||||
prost_build::compile_protos(&["src/user_discovery/types.proto"], &["src/"])?;
|
prost_build::compile_protos(&["models/user_discovery.proto"], &["src/"])?;
|
||||||
prost_build::Config::new()
|
prost_build::Config::new()
|
||||||
.include_file("client_messages.rs")
|
.include_file("client_messages.rs")
|
||||||
.compile_protos(
|
.compile_protos(
|
||||||
|
|
|
||||||
|
|
@ -91,11 +91,10 @@ async fn verify_shared_contacts(
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let verified_at = chrono::Utc::now().timestamp_millis();
|
let verified_at = chrono::Utc::now().timestamp_millis();
|
||||||
|
|
||||||
ctx.get_user_discovery()
|
ctx.get_user_discovery()
|
||||||
.get()
|
.get()
|
||||||
.await
|
.await
|
||||||
.update_verification_state_for_user(contact.user_id, Some(verified_at))
|
.update_verification_state_for_user(contact.user_id, Some(verified_at), tr)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
tracing::info!("verified a contact from shared additional data");
|
tracing::info!("verified a contact from shared additional data");
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,68 @@ use crate::error::{Result, TwonlyError};
|
||||||
use crate::user_config::UserConfig;
|
use crate::user_config::UserConfig;
|
||||||
use encrypted_content::contact_request::Type;
|
use encrypted_content::contact_request::Type;
|
||||||
use sqlx::{Sqlite, Transaction};
|
use sqlx::{Sqlite, Transaction};
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::io::Write as _;
|
use std::io::Write as _;
|
||||||
use std::sync::Arc;
|
use std::sync::{Arc, Mutex, OnceLock};
|
||||||
|
|
||||||
|
static REQUESTED_PROFILES: OnceLock<Mutex<HashMap<i64, i64>>> = OnceLock::new();
|
||||||
|
|
||||||
|
pub(crate) async fn check_for_profile_update(
|
||||||
|
t: &mut Transaction<'_, Sqlite>,
|
||||||
|
from_user_id: i64,
|
||||||
|
content: &EncryptedContent,
|
||||||
|
) -> Result<()> {
|
||||||
|
let Some(sender_profile_counter) = content.sender_profile_counter else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
let current_counter = sqlx::query_scalar!(
|
||||||
|
"SELECT sender_profile_counter FROM contacts WHERE user_id = ?",
|
||||||
|
from_user_id,
|
||||||
|
)
|
||||||
|
.fetch_optional(&mut **t)
|
||||||
|
.await?
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
if content.contact_update.is_some() || sender_profile_counter <= current_counter {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let should_request = {
|
||||||
|
let mut requested = REQUESTED_PROFILES
|
||||||
|
.get_or_init(|| Mutex::new(HashMap::new()))
|
||||||
|
.lock()
|
||||||
|
.unwrap();
|
||||||
|
let last_requested = requested.get(&from_user_id).copied().unwrap_or(0);
|
||||||
|
|
||||||
|
if sender_profile_counter > last_requested {
|
||||||
|
requested.insert(from_user_id, sender_profile_counter);
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if should_request {
|
||||||
|
queue_encrypted_content(
|
||||||
|
t,
|
||||||
|
from_user_id,
|
||||||
|
EncryptedContent {
|
||||||
|
contact_update: Some(encrypted_content::ContactUpdate {
|
||||||
|
r#type: encrypted_content::contact_update::Type::Request as i32,
|
||||||
|
username: None,
|
||||||
|
display_name: None,
|
||||||
|
avatar_svg_compressed: None,
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) async fn handle_contact_request(
|
pub(crate) async fn handle_contact_request(
|
||||||
ctx: &Arc<Context>,
|
ctx: &Arc<Context>,
|
||||||
|
|
@ -73,6 +133,7 @@ pub(crate) async fn handle_contact_request(
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let username = user
|
let username = user
|
||||||
.username
|
.username
|
||||||
.ok_or_else(|| TwonlyError::Generic("user response has no username".into()))?;
|
.ok_or_else(|| TwonlyError::Generic("user response has no username".into()))?;
|
||||||
|
|
@ -149,8 +210,8 @@ pub(crate) async fn handle_contact_update(
|
||||||
EncryptedContent {
|
EncryptedContent {
|
||||||
contact_update: Some(encrypted_content::ContactUpdate {
|
contact_update: Some(encrypted_content::ContactUpdate {
|
||||||
r#type: encrypted_content::contact_update::Type::Update as i32,
|
r#type: encrypted_content::contact_update::Type::Update as i32,
|
||||||
username: user.username,
|
username: Some(user.username),
|
||||||
display_name: user.display_name,
|
display_name: Some(user.display_name),
|
||||||
avatar_svg_compressed,
|
avatar_svg_compressed,
|
||||||
}),
|
}),
|
||||||
sender_profile_counter: Some(user.avatar_counter),
|
sender_profile_counter: Some(user.avatar_counter),
|
||||||
|
|
|
||||||
|
|
@ -4,19 +4,25 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use crate::api::proto::client::encrypted_content;
|
use crate::api::proto::client::encrypted_content;
|
||||||
|
use crate::context::Context;
|
||||||
use crate::database::app::tables::UpdateContact;
|
use crate::database::app::tables::UpdateContact;
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
|
use crate::services::groups::GroupService;
|
||||||
use encrypted_content::error_messages::Type;
|
use encrypted_content::error_messages::Type;
|
||||||
use sqlx::{Sqlite, Transaction};
|
use sqlx::{Sqlite, Transaction};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
pub(crate) async fn handle_error_message(
|
pub(crate) async fn handle_error_message(
|
||||||
transaction: &mut Transaction<'_, Sqlite>,
|
ctx: &Arc<Context>,
|
||||||
|
t: &mut Transaction<'_, Sqlite>,
|
||||||
from_user_id: i64,
|
from_user_id: i64,
|
||||||
_receipt_id: &str,
|
|
||||||
group_id: Option<&str>,
|
group_id: Option<&str>,
|
||||||
error: encrypted_content::ErrorMessages,
|
error: encrypted_content::ErrorMessages,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
match Type::try_from(error.r#type)? {
|
let error_type = Type::try_from(error.r#type)?;
|
||||||
|
tracing::warn!(?error_type, from_user_id, "received client error message");
|
||||||
|
|
||||||
|
match error_type {
|
||||||
Type::ErrorProcessingMessageCreatedAccountRequestInstead => {
|
Type::ErrorProcessingMessageCreatedAccountRequestInstead => {
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
r#"
|
r#"
|
||||||
|
|
@ -27,30 +33,27 @@ pub(crate) async fn handle_error_message(
|
||||||
error.related_receipt_id,
|
error.related_receipt_id,
|
||||||
from_user_id,
|
from_user_id,
|
||||||
)
|
)
|
||||||
.execute(&mut **transaction)
|
.execute(&mut **t)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
UpdateContact::builder()
|
UpdateContact::builder()
|
||||||
.user_id(from_user_id)
|
.user_id(from_user_id)
|
||||||
.accepted(false)
|
.accepted(false)
|
||||||
.requested(true)
|
.requested(true)
|
||||||
.build()
|
.build()
|
||||||
.update(transaction)
|
.update(t)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
Type::GroupNotFoundOrNotAMember => {
|
Type::GroupNotFoundOrNotAMember => {
|
||||||
if let Some(group_id) = group_id {
|
if let Some(group_id) = group_id {
|
||||||
if let Ok(callbacks) = crate::bridge::callbacks::get_callbacks() {
|
GroupService::new(ctx)
|
||||||
let group_id = group_id.to_owned();
|
.handle_membership_error(
|
||||||
let related_receipt_id = error.related_receipt_id.clone();
|
t,
|
||||||
tokio::spawn(async move {
|
|
||||||
(callbacks.api.group_membership_error)(
|
|
||||||
from_user_id,
|
from_user_id,
|
||||||
group_id,
|
group_id.to_owned(),
|
||||||
related_receipt_id,
|
error.related_receipt_id,
|
||||||
)
|
)
|
||||||
.await;
|
.await?;
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Type::SessionOutOfSync | Type::UnknownMessageType => {}
|
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::api::proto::client::encrypted_content;
|
||||||
|
use crate::context::Context;
|
||||||
|
use crate::database::app::tables::{Contact, Group};
|
||||||
use crate::error::{Result, TwonlyError};
|
use crate::error::{Result, TwonlyError};
|
||||||
use crate::utils::{milliseconds_to_seconds, new_uuid_v4};
|
use crate::services::groups::GroupService;
|
||||||
|
use crate::utils::{is_today, milliseconds_to_seconds, new_uuid_v4};
|
||||||
use rand::SeedableRng;
|
use rand::SeedableRng;
|
||||||
use sqlx::{Sqlite, Transaction};
|
use sqlx::{Sqlite, Transaction};
|
||||||
|
|
||||||
pub(crate) async fn ensure_group_member(
|
pub(crate) async fn ensure_group_member(
|
||||||
transaction: &mut Transaction<'_, Sqlite>,
|
t: &mut Transaction<'_, Sqlite>,
|
||||||
from_user_id: i64,
|
from_user_id: i64,
|
||||||
group_id: &str,
|
group_id: &str,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let allowed = sqlx::query_scalar!(
|
let allowed = Group::is_member(t, group_id, from_user_id).await?;
|
||||||
r#"
|
|
||||||
SELECT EXISTS(
|
|
||||||
SELECT 1
|
|
||||||
FROM group_members
|
|
||||||
WHERE group_id = ? AND contact_id = ?
|
|
||||||
)
|
|
||||||
"#,
|
|
||||||
group_id,
|
|
||||||
from_user_id,
|
|
||||||
)
|
|
||||||
.fetch_one(&mut **transaction)
|
|
||||||
.await?
|
|
||||||
!= 0;
|
|
||||||
if !allowed {
|
if !allowed {
|
||||||
return Err(TwonlyError::Generic(format!(
|
return Err(TwonlyError::Generic(format!(
|
||||||
"user {from_user_id} is not a member of group {group_id}"
|
"user {from_user_id} is not a member of group {group_id}"
|
||||||
|
|
@ -37,32 +31,19 @@ pub(crate) async fn ensure_group_member(
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn handle_group_create(
|
pub(crate) async fn handle_group_create(
|
||||||
transaction: &mut Transaction<'_, Sqlite>,
|
ctx: &std::sync::Arc<crate::context::Context>,
|
||||||
|
t: &mut Transaction<'_, Sqlite>,
|
||||||
from_user_id: i64,
|
from_user_id: i64,
|
||||||
group_id: &str,
|
group_id: &str,
|
||||||
create: encrypted_content::GroupCreate,
|
create: encrypted_content::GroupCreate,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let contact_exists = sqlx::query_scalar!(
|
Contact::ensure_exists(t, from_user_id).await?;
|
||||||
r#"
|
|
||||||
SELECT EXISTS(
|
|
||||||
SELECT 1 FROM contacts WHERE user_id = ?
|
|
||||||
)
|
|
||||||
"#,
|
|
||||||
from_user_id,
|
|
||||||
)
|
|
||||||
.fetch_one(&mut **transaction)
|
|
||||||
.await?
|
|
||||||
!= 0;
|
|
||||||
if !contact_exists {
|
|
||||||
return Err(TwonlyError::Generic(
|
|
||||||
"only known contacts may create a group".into(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut rng = rand::rngs::StdRng::from_os_rng();
|
let mut rng = rand::rngs::StdRng::from_os_rng();
|
||||||
let identity = libsignal_protocol::IdentityKeyPair::generate(&mut rng);
|
let identity = libsignal_protocol::IdentityKeyPair::generate(&mut rng);
|
||||||
let private_key = identity.serialize().to_vec();
|
let private_key = identity.serialize().to_vec();
|
||||||
let group_name = create.group_name.unwrap_or_default();
|
let group_name = create.group_name.unwrap_or_default();
|
||||||
|
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
r#"
|
r#"
|
||||||
INSERT INTO groups(
|
INSERT INTO groups(
|
||||||
|
|
@ -83,8 +64,9 @@ pub(crate) async fn handle_group_create(
|
||||||
private_key,
|
private_key,
|
||||||
group_name,
|
group_name,
|
||||||
)
|
)
|
||||||
.execute(&mut **transaction)
|
.execute(&mut **t)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
r#"
|
r#"
|
||||||
INSERT INTO group_members(group_id, contact_id, group_public_key)
|
INSERT INTO group_members(group_id, contact_id, group_public_key)
|
||||||
|
|
@ -96,8 +78,9 @@ pub(crate) async fn handle_group_create(
|
||||||
from_user_id,
|
from_user_id,
|
||||||
create.group_public_key,
|
create.group_public_key,
|
||||||
)
|
)
|
||||||
.execute(&mut **transaction)
|
.execute(&mut **t)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
r#"INSERT INTO group_histories(group_history_id, group_id, contact_id, type)
|
r#"INSERT INTO group_histories(group_history_id, group_id, contact_id, type)
|
||||||
VALUES (?, ?, ?, 'addMember')"#,
|
VALUES (?, ?, ?, 'addMember')"#,
|
||||||
|
|
@ -105,42 +88,24 @@ pub(crate) async fn handle_group_create(
|
||||||
group_id,
|
group_id,
|
||||||
from_user_id,
|
from_user_id,
|
||||||
)
|
)
|
||||||
.execute(&mut **transaction)
|
.execute(&mut **t)
|
||||||
.await?;
|
.await?;
|
||||||
if let Ok(callbacks) = crate::bridge::callbacks::get_callbacks() {
|
|
||||||
let group_id = group_id.to_owned();
|
GroupService::new(ctx)
|
||||||
tokio::spawn(async move {
|
.refresh_group_state(t, group_id.to_owned(), true)
|
||||||
(callbacks.api.group_state_refresh)(group_id, true).await;
|
.await;
|
||||||
});
|
|
||||||
}
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn handle_group_join(
|
pub(crate) async fn handle_group_join(
|
||||||
transaction: &mut Transaction<'_, Sqlite>,
|
t: &mut Transaction<'_, Sqlite>,
|
||||||
from_user_id: i64,
|
from_user_id: i64,
|
||||||
group_id: &str,
|
group_id: &str,
|
||||||
join: encrypted_content::GroupJoin,
|
join: encrypted_content::GroupJoin,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let group_exists = sqlx::query_scalar!(
|
Contact::ensure_exists(t, from_user_id).await?;
|
||||||
r#"SELECT EXISTS(SELECT 1 FROM groups WHERE group_id = ?)"#,
|
Group::ensure_exists(t, group_id).await?;
|
||||||
group_id,
|
|
||||||
)
|
|
||||||
.fetch_one(&mut **transaction)
|
|
||||||
.await?
|
|
||||||
!= 0;
|
|
||||||
let contact_exists = sqlx::query_scalar!(
|
|
||||||
r#"SELECT EXISTS(SELECT 1 FROM contacts WHERE user_id = ?)"#,
|
|
||||||
from_user_id,
|
|
||||||
)
|
|
||||||
.fetch_one(&mut **transaction)
|
|
||||||
.await?
|
|
||||||
!= 0;
|
|
||||||
if !group_exists || !contact_exists {
|
|
||||||
return Err(TwonlyError::Generic(
|
|
||||||
"group join arrived before group/contact state".into(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
r#"
|
r#"
|
||||||
|
|
@ -153,13 +118,14 @@ pub(crate) async fn handle_group_join(
|
||||||
from_user_id,
|
from_user_id,
|
||||||
join.group_public_key,
|
join.group_public_key,
|
||||||
)
|
)
|
||||||
.execute(&mut **transaction)
|
.execute(&mut **t)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn handle_resend_group_public_key(
|
pub(crate) async fn handle_resend_group_public_key(
|
||||||
transaction: &mut Transaction<'_, Sqlite>,
|
t: &mut Transaction<'_, Sqlite>,
|
||||||
from_user_id: i64,
|
from_user_id: i64,
|
||||||
group_id: &str,
|
group_id: &str,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
|
@ -171,18 +137,19 @@ pub(crate) async fn handle_resend_group_public_key(
|
||||||
"#,
|
"#,
|
||||||
group_id,
|
group_id,
|
||||||
)
|
)
|
||||||
.fetch_optional(&mut **transaction)
|
.fetch_optional(&mut **t)
|
||||||
.await?
|
.await?
|
||||||
.flatten();
|
.flatten();
|
||||||
|
|
||||||
let Some(private_key) = private_key else {
|
let Some(private_key) = private_key else {
|
||||||
return Err(TwonlyError::Generic(format!(
|
return Err(TwonlyError::Generic(format!(
|
||||||
"cannot resend the group public key for {group_id} to {from_user_id}"
|
"cannot resend the group public key for {group_id} to {from_user_id}"
|
||||||
)));
|
)));
|
||||||
};
|
};
|
||||||
let identity = libsignal_protocol::IdentityKeyPair::try_from(private_key.as_slice())
|
|
||||||
.map_err(|error| TwonlyError::Signal(error.to_string()))?;
|
let identity = libsignal_protocol::IdentityKeyPair::try_from(private_key.as_slice())?;
|
||||||
super::messages::queue_encrypted_content(
|
queue_encrypted_content(
|
||||||
transaction,
|
t,
|
||||||
from_user_id,
|
from_user_id,
|
||||||
crate::api::proto::client::EncryptedContent {
|
crate::api::proto::client::EncryptedContent {
|
||||||
group_id: Some(group_id.to_owned()),
|
group_id: Some(group_id.to_owned()),
|
||||||
|
|
@ -194,31 +161,25 @@ pub(crate) async fn handle_resend_group_public_key(
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn handle_group_update(
|
pub(crate) async fn handle_group_update(
|
||||||
transaction: &mut Transaction<'_, Sqlite>,
|
ctx: &Arc<Context>,
|
||||||
|
t: &mut Transaction<'_, Sqlite>,
|
||||||
from_user_id: i64,
|
from_user_id: i64,
|
||||||
group_id: &str,
|
group_id: &str,
|
||||||
update: encrypted_content::GroupUpdate,
|
update: encrypted_content::GroupUpdate,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let is_direct = sqlx::query_scalar!(
|
let is_direct = Group::is_direct_chat(t, group_id).await?;
|
||||||
"SELECT is_direct_chat FROM groups WHERE group_id = ?",
|
|
||||||
group_id
|
|
||||||
)
|
|
||||||
.fetch_optional(&mut **transaction)
|
|
||||||
.await?
|
|
||||||
.unwrap_or(0)
|
|
||||||
!= 0;
|
|
||||||
if !is_direct {
|
if !is_direct {
|
||||||
if let Ok(callbacks) = crate::bridge::callbacks::get_callbacks() {
|
GroupService::new(ctx)
|
||||||
let group_id = group_id.to_owned();
|
.refresh_group_state(t, group_id.to_owned(), false)
|
||||||
tokio::spawn(async move {
|
.await;
|
||||||
(callbacks.api.group_state_refresh)(group_id, false).await;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if update.group_action_type == "updatedGroupName" {
|
if update.group_action_type == "updatedGroupName" {
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
r#"
|
r#"
|
||||||
|
|
@ -229,7 +190,7 @@ pub(crate) async fn handle_group_update(
|
||||||
update.new_group_name,
|
update.new_group_name,
|
||||||
group_id,
|
group_id,
|
||||||
)
|
)
|
||||||
.execute(&mut **transaction)
|
.execute(&mut **t)
|
||||||
.await?;
|
.await?;
|
||||||
} else if update.group_action_type == "changeDisplayMaxTime" && is_direct {
|
} else if update.group_action_type == "changeDisplayMaxTime" && is_direct {
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
|
|
@ -243,7 +204,7 @@ pub(crate) async fn handle_group_update(
|
||||||
update.new_delete_messages_after_milliseconds,
|
update.new_delete_messages_after_milliseconds,
|
||||||
group_id,
|
group_id,
|
||||||
)
|
)
|
||||||
.execute(&mut **transaction)
|
.execute(&mut **t)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -267,43 +228,66 @@ pub(crate) async fn handle_group_update(
|
||||||
update.new_delete_messages_after_milliseconds,
|
update.new_delete_messages_after_milliseconds,
|
||||||
update.group_action_type,
|
update.group_action_type,
|
||||||
)
|
)
|
||||||
.execute(&mut **transaction)
|
.execute(&mut **t)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn handle_flame_sync(
|
pub(crate) async fn handle_flame_sync(
|
||||||
transaction: &mut Transaction<'_, Sqlite>,
|
t: &mut Transaction<'_, Sqlite>,
|
||||||
group_id: &str,
|
group_id: &str,
|
||||||
flame: encrypted_content::FlameSync,
|
flame: encrypted_content::FlameSync,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let last_flame_counter_change = milliseconds_to_seconds(flame.last_flame_counter_change);
|
let last_flame_counter_change = milliseconds_to_seconds(flame.last_flame_counter_change);
|
||||||
|
|
||||||
|
let Some(group) = sqlx::query!(
|
||||||
|
r#"
|
||||||
|
SELECT last_flame_counter_change, flame_counter, max_flame_counter
|
||||||
|
FROM groups
|
||||||
|
WHERE group_id = ?
|
||||||
|
"#,
|
||||||
|
group_id,
|
||||||
|
)
|
||||||
|
.fetch_optional(&mut **t)
|
||||||
|
.await?
|
||||||
|
else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(group_last_flame_counter_change) = group.last_flame_counter_change else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
let update_counters = flame.force_update
|
||||||
|
|| (is_today(group_last_flame_counter_change) && is_today(last_flame_counter_change));
|
||||||
|
|
||||||
|
let flame_counter = if update_counters {
|
||||||
|
group.flame_counter.max(flame.flame_counter)
|
||||||
|
} else {
|
||||||
|
group.flame_counter
|
||||||
|
};
|
||||||
|
let max_flame_counter = if update_counters {
|
||||||
|
group.max_flame_counter.max(flame.flame_counter)
|
||||||
|
} else {
|
||||||
|
group.max_flame_counter
|
||||||
|
};
|
||||||
|
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
r#"
|
r#"
|
||||||
UPDATE groups
|
UPDATE groups
|
||||||
SET also_best_friend = ?,
|
SET also_best_friend = ?,
|
||||||
flame_counter = CASE WHEN (
|
flame_counter = ?,
|
||||||
(date(last_flame_counter_change, 'unixepoch', 'localtime') = date('now', 'localtime')
|
max_flame_counter = ?
|
||||||
AND date(?, 'unixepoch', 'localtime') = date('now', 'localtime'))
|
WHERE group_id = ?
|
||||||
OR ?
|
|
||||||
) THEN MAX(flame_counter, ?) ELSE flame_counter END,
|
|
||||||
max_flame_counter = CASE WHEN (
|
|
||||||
(date(last_flame_counter_change, 'unixepoch', 'localtime') = date('now', 'localtime')
|
|
||||||
AND date(?, 'unixepoch', 'localtime') = date('now', 'localtime'))
|
|
||||||
OR ?
|
|
||||||
) THEN MAX(max_flame_counter, ?) ELSE max_flame_counter END
|
|
||||||
WHERE group_id = ? AND last_flame_counter_change IS NOT NULL
|
|
||||||
"#,
|
"#,
|
||||||
flame.best_friend,
|
flame.best_friend,
|
||||||
last_flame_counter_change,
|
flame_counter,
|
||||||
flame.force_update,
|
max_flame_counter,
|
||||||
flame.flame_counter,
|
|
||||||
last_flame_counter_change,
|
|
||||||
flame.force_update,
|
|
||||||
flame.flame_counter,
|
|
||||||
group_id,
|
group_id,
|
||||||
)
|
)
|
||||||
.execute(&mut **transaction)
|
.execute(&mut **t)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,31 @@
|
||||||
|
|
||||||
use crate::api::proto::client::encrypted_content;
|
use crate::api::proto::client::encrypted_content;
|
||||||
use crate::bridge::callbacks::get_callbacks;
|
use crate::bridge::callbacks::get_callbacks;
|
||||||
|
use crate::context::Context;
|
||||||
use crate::database::app::tables::Group;
|
use crate::database::app::tables::Group;
|
||||||
use crate::error::{Result, TwonlyError};
|
use crate::error::{Result, TwonlyError};
|
||||||
|
use crate::services::mediafiles::MediaFileService;
|
||||||
use crate::utils::{milliseconds_to_seconds, new_uuid_v4};
|
use crate::utils::{milliseconds_to_seconds, new_uuid_v4};
|
||||||
use encrypted_content::media::Type as MediaType;
|
use encrypted_content::media::Type as MediaType;
|
||||||
use encrypted_content::media_update::Type as MediaUpdateType;
|
use encrypted_content::media_update::Type as MediaUpdateType;
|
||||||
use sqlx::{Sqlite, Transaction};
|
use sqlx::{Sqlite, Transaction};
|
||||||
|
|
||||||
|
fn spawn_media_download(media_id: String) {
|
||||||
|
let Ok(ctx) = Context::get_static() else {
|
||||||
|
tracing::warn!(media_id, "could not start media download without context");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let ctx = ctx.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
if let Err(error) = MediaFileService::new(&ctx)
|
||||||
|
.download_when_available(&media_id)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
tracing::warn!(media_id, %error, "media download failed");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
fn spawn_media_action(kind: &'static str, media_id: String, contact_id: i64, message_id: String) {
|
fn spawn_media_action(kind: &'static str, media_id: String, contact_id: i64, message_id: String) {
|
||||||
if let Ok(callbacks) = get_callbacks() {
|
if let Ok(callbacks) = get_callbacks() {
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
|
|
@ -65,7 +83,7 @@ pub(crate) async fn handle_media(
|
||||||
.execute(&mut **t)
|
.execute(&mut **t)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
spawn_media_action("download", media_id, from_user_id, media.sender_message_id);
|
spawn_media_download(media_id);
|
||||||
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
@ -122,7 +140,7 @@ pub(crate) async fn handle_media(
|
||||||
.execute(&mut **t)
|
.execute(&mut **t)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
spawn_media_action("download", media_id, from_user_id, media.sender_message_id);
|
spawn_media_download(media_id);
|
||||||
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
@ -178,7 +196,9 @@ pub(crate) async fn handle_media(
|
||||||
)
|
)
|
||||||
.execute(&mut **t)
|
.execute(&mut **t)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Group::increase_last_message_exchange(t, group_id, timestamp).await?;
|
Group::increase_last_message_exchange(t, group_id, timestamp).await?;
|
||||||
|
|
||||||
if let Ok(callbacks) = get_callbacks() {
|
if let Ok(callbacks) = get_callbacks() {
|
||||||
let group_id = group_id.to_owned();
|
let group_id = group_id.to_owned();
|
||||||
let timestamp = media.timestamp;
|
let timestamp = media.timestamp;
|
||||||
|
|
@ -186,7 +206,9 @@ pub(crate) async fn handle_media(
|
||||||
(callbacks.api.media_received)(group_id, timestamp).await;
|
(callbacks.api.media_received)(group_id, timestamp).await;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
spawn_media_action("download", media_id, from_user_id, media.sender_message_id);
|
|
||||||
|
spawn_media_download(media_id);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,12 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use super::handle_encrypted;
|
use super::handle_encrypted;
|
||||||
use crate::api::proto::client::{self as proto, encrypted_content};
|
use crate::api::proto::client::{self as proto};
|
||||||
|
|
||||||
use crate::api::Server;
|
use crate::api::Server;
|
||||||
use crate::bridge::api::ServerResult;
|
use crate::bridge::api::ServerResult;
|
||||||
use crate::bridge::callbacks::get_callbacks;
|
use crate::bridge::callbacks::get_callbacks;
|
||||||
use crate::context::Context;
|
use crate::context::Context;
|
||||||
use crate::database::app::tables::{Contact, NewReceipt, Receipt};
|
use crate::database::app::tables::{Contact, MediaFile, NewReceipt, Receipt};
|
||||||
use crate::error::{twonly_error, Result, TwonlyError};
|
use crate::error::{twonly_error, Result, TwonlyError};
|
||||||
use crate::utils::new_uuid_v4;
|
use crate::utils::new_uuid_v4;
|
||||||
use prost::Message as ProstMessage;
|
use prost::Message as ProstMessage;
|
||||||
|
|
@ -25,26 +24,6 @@ use std::{collections::HashMap, sync::LazyLock};
|
||||||
static ALREADY_QUEUED_RECEIPTS: LazyLock<std::sync::Mutex<HashMap<String, std::time::Instant>>> =
|
static ALREADY_QUEUED_RECEIPTS: LazyLock<std::sync::Mutex<HashMap<String, std::time::Instant>>> =
|
||||||
LazyLock::new(|| std::sync::Mutex::new(HashMap::new()));
|
LazyLock::new(|| std::sync::Mutex::new(HashMap::new()));
|
||||||
|
|
||||||
async fn native_push_data(
|
|
||||||
_ctx: &Context,
|
|
||||||
contact_id: i64,
|
|
||||||
message_id: Option<String>,
|
|
||||||
plaintext: &[u8],
|
|
||||||
message_type: i32,
|
|
||||||
) -> Result<Option<Vec<u8>>> {
|
|
||||||
let callbacks = match get_callbacks() {
|
|
||||||
Ok(callbacks) => callbacks,
|
|
||||||
Err(TwonlyError::MissingCallbackInitialization) => {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
Err(error) => return Err(error),
|
|
||||||
};
|
|
||||||
Ok(
|
|
||||||
(callbacks.api.create_push_data)(contact_id, message_id, plaintext.to_vec(), message_type)
|
|
||||||
.await,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn queue_encrypted_content(
|
pub(crate) async fn queue_encrypted_content(
|
||||||
transaction: &mut Transaction<'_, Sqlite>,
|
transaction: &mut Transaction<'_, Sqlite>,
|
||||||
target_user_id: i64,
|
target_user_id: i64,
|
||||||
|
|
@ -103,7 +82,7 @@ pub(crate) async fn process_encrypted_or_queue_error(
|
||||||
let Some(error_type) = error_type else {
|
let Some(error_type) = error_type else {
|
||||||
return Err(error);
|
return Err(error);
|
||||||
};
|
};
|
||||||
let outgoing_receipt_id = uuid::Uuid::new_v4().to_string();
|
let outgoing_receipt_id = new_uuid_v4();
|
||||||
let response_content = proto::EncryptedContent {
|
let response_content = proto::EncryptedContent {
|
||||||
group_id,
|
group_id,
|
||||||
error_messages: Some(proto::encrypted_content::ErrorMessages {
|
error_messages: Some(proto::encrypted_content::ErrorMessages {
|
||||||
|
|
@ -318,18 +297,7 @@ pub(crate) async fn send_queued_receipt(ctx: &Arc<Context>, receipt_id: &str) ->
|
||||||
|
|
||||||
let message_type = proto::message::Type::try_from(message.r#type)?;
|
let message_type = proto::message::Type::try_from(message.r#type)?;
|
||||||
|
|
||||||
let push_data = if row.retry_count == 0 {
|
let push_data: Option<Vec<u8>> = None;
|
||||||
native_push_data(
|
|
||||||
ctx,
|
|
||||||
row.contact_id,
|
|
||||||
row.message_id.clone(),
|
|
||||||
&message.encrypted_content.clone().unwrap_or_default(),
|
|
||||||
message.r#type,
|
|
||||||
)
|
|
||||||
.await?
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
match message_type {
|
match message_type {
|
||||||
proto::message::Type::Ciphertext | proto::message::Type::PrekeyBundle => {
|
proto::message::Type::Ciphertext | proto::message::Type::PrekeyBundle => {
|
||||||
|
|
@ -422,18 +390,7 @@ pub(crate) async fn prepare_queued_receipt(
|
||||||
let mut message = proto::Message::decode(row.message.as_slice())
|
let mut message = proto::Message::decode(row.message.as_slice())
|
||||||
.map_err(|error| TwonlyError::Generic(format!("invalid queued message: {error}")))?;
|
.map_err(|error| TwonlyError::Generic(format!("invalid queued message: {error}")))?;
|
||||||
message.receipt_id = receipt_id.to_owned();
|
message.receipt_id = receipt_id.to_owned();
|
||||||
let push_data = if row.retry_count == 0 {
|
let push_data: Option<Vec<u8>> = None;
|
||||||
native_push_data(
|
|
||||||
ctx,
|
|
||||||
row.contact_id,
|
|
||||||
row.message_id,
|
|
||||||
&message.encrypted_content.clone().unwrap_or_default(),
|
|
||||||
message.r#type,
|
|
||||||
)
|
|
||||||
.await?
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
match proto::message::Type::try_from(message.r#type)
|
match proto::message::Type::try_from(message.r#type)
|
||||||
.map_err(|_| TwonlyError::Generic("queued message has invalid type".into()))?
|
.map_err(|_| TwonlyError::Generic("queued message has invalid type".into()))?
|
||||||
|
|
@ -593,6 +550,8 @@ pub(crate) async fn handle_sender_delivery_receipt(
|
||||||
)
|
)
|
||||||
.execute(&mut **transaction)
|
.execute(&mut **transaction)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
MediaFile::handle_response_from_receiver(transaction, &message_id).await?;
|
||||||
}
|
}
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
r#"
|
r#"
|
||||||
|
|
@ -635,144 +594,3 @@ pub async fn handle_plaintext_content(
|
||||||
}
|
}
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn handle_message_update(
|
|
||||||
transaction: &mut Transaction<'_, Sqlite>,
|
|
||||||
from_user_id: i64,
|
|
||||||
update: encrypted_content::MessageUpdate,
|
|
||||||
) -> Result<()> {
|
|
||||||
use encrypted_content::message_update::Type;
|
|
||||||
let timestamp = crate::utils::milliseconds_to_seconds(update.timestamp);
|
|
||||||
|
|
||||||
match Type::try_from(update.r#type)
|
|
||||||
.map_err(|_| TwonlyError::Generic("invalid message update".into()))?
|
|
||||||
{
|
|
||||||
Type::Opened => {
|
|
||||||
for message_id in update.multiple_target_message_ids {
|
|
||||||
let action_at = sqlx::query_scalar::<_, i64>(
|
|
||||||
"SELECT MAX(created_at, ?) FROM messages WHERE message_id = ?",
|
|
||||||
)
|
|
||||||
.bind(timestamp)
|
|
||||||
.bind(&message_id)
|
|
||||||
.fetch_optional(&mut **transaction)
|
|
||||||
.await?;
|
|
||||||
let Some(action_at) = action_at else { continue };
|
|
||||||
sqlx::query!(
|
|
||||||
r#"
|
|
||||||
INSERT INTO message_actions(message_id, contact_id, type, action_at)
|
|
||||||
VALUES (?, ?, 'openedAt', ?)
|
|
||||||
ON CONFLICT(message_id, contact_id, type)
|
|
||||||
DO UPDATE SET action_at = excluded.action_at
|
|
||||||
"#,
|
|
||||||
message_id,
|
|
||||||
from_user_id,
|
|
||||||
action_at,
|
|
||||||
)
|
|
||||||
.execute(&mut **transaction)
|
|
||||||
.await?;
|
|
||||||
sqlx::query!(
|
|
||||||
r#"UPDATE messages SET opened_at = ?, opened_by_all = CASE WHEN NOT EXISTS(
|
|
||||||
SELECT 1 FROM group_members gm
|
|
||||||
WHERE gm.group_id = messages.group_id AND NOT EXISTS(
|
|
||||||
SELECT 1 FROM message_actions ma
|
|
||||||
WHERE ma.message_id = messages.message_id
|
|
||||||
AND ma.contact_id = gm.contact_id AND ma.type = 'openedAt'
|
|
||||||
)
|
|
||||||
) THEN ? ELSE NULL END
|
|
||||||
WHERE message_id = ?"#,
|
|
||||||
action_at,
|
|
||||||
action_at,
|
|
||||||
message_id,
|
|
||||||
)
|
|
||||||
.execute(&mut **transaction)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Type::Delete => {
|
|
||||||
let media_id = sqlx::query_scalar!(
|
|
||||||
"SELECT media_id FROM messages WHERE message_id = ? AND sender_id = ?",
|
|
||||||
update.sender_message_id,
|
|
||||||
from_user_id,
|
|
||||||
)
|
|
||||||
.fetch_optional(&mut **transaction)
|
|
||||||
.await?
|
|
||||||
.flatten();
|
|
||||||
sqlx::query!(
|
|
||||||
"DELETE FROM message_histories WHERE message_id = ?",
|
|
||||||
update.sender_message_id
|
|
||||||
)
|
|
||||||
.execute(&mut **transaction)
|
|
||||||
.await?;
|
|
||||||
sqlx::query!(
|
|
||||||
"DELETE FROM receipts WHERE message_id = ?",
|
|
||||||
update.sender_message_id
|
|
||||||
)
|
|
||||||
.execute(&mut **transaction)
|
|
||||||
.await?;
|
|
||||||
sqlx::query!(
|
|
||||||
r#"
|
|
||||||
UPDATE messages
|
|
||||||
SET is_deleted_from_sender = 1, content = NULL, media_id = NULL, modified_at = ?
|
|
||||||
WHERE message_id = ? AND sender_id = ?
|
|
||||||
"#,
|
|
||||||
timestamp,
|
|
||||||
update.sender_message_id,
|
|
||||||
from_user_id,
|
|
||||||
)
|
|
||||||
.execute(&mut **transaction)
|
|
||||||
.await?;
|
|
||||||
if let Some(media_id) = media_id {
|
|
||||||
let references = sqlx::query_scalar!(
|
|
||||||
"SELECT COUNT(*) FROM messages WHERE media_id = ?",
|
|
||||||
media_id,
|
|
||||||
)
|
|
||||||
.fetch_one(&mut **transaction)
|
|
||||||
.await?;
|
|
||||||
if references == 0 {
|
|
||||||
sqlx::query!("DELETE FROM media_files WHERE media_id = ?", media_id)
|
|
||||||
.execute(&mut **transaction)
|
|
||||||
.await?;
|
|
||||||
if let Ok(callbacks) = get_callbacks() {
|
|
||||||
if let Some(message_id) = update.sender_message_id.clone() {
|
|
||||||
tokio::spawn(async move {
|
|
||||||
(callbacks.api.media_action)(
|
|
||||||
"delete".into(),
|
|
||||||
media_id,
|
|
||||||
from_user_id,
|
|
||||||
message_id,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Type::EditText => {
|
|
||||||
sqlx::query!(
|
|
||||||
r#"INSERT INTO message_histories(message_id, content, created_at)
|
|
||||||
SELECT message_id, content, ? FROM messages
|
|
||||||
WHERE message_id = ? AND sender_id = ? AND content IS NOT NULL"#,
|
|
||||||
timestamp,
|
|
||||||
update.sender_message_id,
|
|
||||||
from_user_id,
|
|
||||||
)
|
|
||||||
.execute(&mut **transaction)
|
|
||||||
.await?;
|
|
||||||
sqlx::query!(
|
|
||||||
r#"
|
|
||||||
UPDATE messages
|
|
||||||
SET content = ?, modified_at = ?
|
|
||||||
WHERE message_id = ? AND sender_id = ? AND content IS NOT NULL
|
|
||||||
"#,
|
|
||||||
update.text,
|
|
||||||
timestamp,
|
|
||||||
update.sender_message_id,
|
|
||||||
from_user_id,
|
|
||||||
)
|
|
||||||
.execute(&mut **transaction)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -9,21 +9,17 @@ use crate::context::Context;
|
||||||
use crate::database::app::tables::{Contact, Group, Receipt};
|
use crate::database::app::tables::{Contact, Group, Receipt};
|
||||||
use crate::error::{Result, TwonlyError};
|
use crate::error::{Result, TwonlyError};
|
||||||
use sqlx::{Sqlite, Transaction};
|
use sqlx::{Sqlite, Transaction};
|
||||||
use std::collections::HashMap;
|
use std::sync::Arc;
|
||||||
use std::sync::{Arc, Mutex, OnceLock};
|
|
||||||
use typing_indicator::handle_typing_indicator;
|
use typing_indicator::handle_typing_indicator;
|
||||||
|
|
||||||
static REQUESTED_PROFILES: OnceLock<Mutex<HashMap<i64, i64>>> = OnceLock::new();
|
|
||||||
|
|
||||||
mod additional_data;
|
mod additional_data;
|
||||||
pub(crate) mod contact;
|
pub(crate) mod contact;
|
||||||
mod errors;
|
mod errors;
|
||||||
mod groups;
|
mod groups;
|
||||||
mod media;
|
mod media;
|
||||||
pub mod messages;
|
pub mod messages;
|
||||||
mod pushkeys;
|
|
||||||
mod reaction;
|
mod reaction;
|
||||||
mod recovery;
|
pub(crate) mod recovery;
|
||||||
mod text_message;
|
mod text_message;
|
||||||
mod typing_indicator;
|
mod typing_indicator;
|
||||||
mod user_discovery;
|
mod user_discovery;
|
||||||
|
|
@ -34,64 +30,21 @@ mod verification;
|
||||||
/// this dispatcher.
|
/// this dispatcher.
|
||||||
pub(crate) async fn handle_encrypted(
|
pub(crate) async fn handle_encrypted(
|
||||||
ctx: &Arc<Context>,
|
ctx: &Arc<Context>,
|
||||||
tr: &mut Transaction<'_, Sqlite>,
|
t: &mut Transaction<'_, Sqlite>,
|
||||||
from_user_id: i64,
|
from_user_id: i64,
|
||||||
receipt_id: &str,
|
receipt_id: &str,
|
||||||
content: proto::EncryptedContent,
|
content: proto::EncryptedContent,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
Receipt::mark_all_for_retry(tr, from_user_id).await?;
|
Receipt::mark_all_for_retry(t, from_user_id).await?;
|
||||||
|
|
||||||
if let Some(version) = content.sender_user_discovery_version.clone() {
|
if let Some(version) = content.sender_user_discovery_version.clone() {
|
||||||
user_discovery::check_sender_version(ctx, tr, from_user_id, version).await?;
|
user_discovery::check_sender_version(ctx, t, from_user_id, version).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(sender_profile_counter) = content.sender_profile_counter {
|
contact::check_for_profile_update(t, from_user_id, &content).await?;
|
||||||
let current_counter = sqlx::query_scalar!(
|
|
||||||
"SELECT sender_profile_counter FROM contacts WHERE user_id = ?",
|
|
||||||
from_user_id,
|
|
||||||
)
|
|
||||||
.fetch_optional(&mut **tr)
|
|
||||||
.await?
|
|
||||||
.unwrap_or(0);
|
|
||||||
|
|
||||||
if content.contact_update.is_none() && sender_profile_counter > current_counter {
|
|
||||||
let should_request = {
|
|
||||||
let mut requested = REQUESTED_PROFILES
|
|
||||||
.get_or_init(|| Mutex::new(HashMap::new()))
|
|
||||||
.lock()
|
|
||||||
.unwrap();
|
|
||||||
let last_requested = requested.get(&from_user_id).copied().unwrap_or(0);
|
|
||||||
|
|
||||||
if sender_profile_counter > last_requested {
|
|
||||||
requested.insert(from_user_id, sender_profile_counter);
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if should_request {
|
|
||||||
messages::queue_encrypted_content(
|
|
||||||
tr,
|
|
||||||
from_user_id,
|
|
||||||
proto::EncryptedContent {
|
|
||||||
contact_update: Some(proto::encrypted_content::ContactUpdate {
|
|
||||||
r#type: proto::encrypted_content::contact_update::Type::Request as i32,
|
|
||||||
username: None,
|
|
||||||
display_name: None,
|
|
||||||
avatar_svg_compressed: None,
|
|
||||||
}),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
true,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if content.ask_for_friend_promotions == Some(true) {
|
if content.ask_for_friend_promotions == Some(true) {
|
||||||
Contact::update_ask_for_friend_promotions(tr, from_user_id).await?;
|
Contact::update_ask_for_friend_promotions(t, from_user_id).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let type_kind = content_type_kind(&content);
|
let type_kind = content_type_kind(&content);
|
||||||
|
|
@ -100,13 +53,13 @@ pub(crate) async fn handle_encrypted(
|
||||||
tracing::info!("Handling incoming message: {type_kind}");
|
tracing::info!("Handling incoming message: {type_kind}");
|
||||||
|
|
||||||
if let Some(request) = content.contact_request {
|
if let Some(request) = content.contact_request {
|
||||||
return contact::handle_contact_request(ctx, tr, from_user_id, request).await;
|
return contact::handle_contact_request(ctx, t, from_user_id, request).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(update) = content.contact_update {
|
if let Some(update) = content.contact_update {
|
||||||
return contact::handle_contact_update(
|
return contact::handle_contact_update(
|
||||||
ctx,
|
ctx,
|
||||||
tr,
|
t,
|
||||||
from_user_id,
|
from_user_id,
|
||||||
content.sender_profile_counter,
|
content.sender_profile_counter,
|
||||||
update,
|
update,
|
||||||
|
|
@ -115,46 +68,42 @@ pub(crate) async fn handle_encrypted(
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(update) = content.message_update {
|
if let Some(update) = content.message_update {
|
||||||
return messages::handle_message_update(tr, from_user_id, update).await;
|
return text_message::handle_message_update(t, from_user_id, update).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(update) = content.media_update {
|
if let Some(update) = content.media_update {
|
||||||
return media::handle_media_update(tr, from_user_id, update).await;
|
return media::handle_media_update(t, from_user_id, update).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(error) = content.error_messages {
|
if let Some(error) = content.error_messages {
|
||||||
return errors::handle_error_message(
|
return errors::handle_error_message(
|
||||||
tr,
|
ctx,
|
||||||
|
t,
|
||||||
from_user_id,
|
from_user_id,
|
||||||
receipt_id,
|
|
||||||
content.group_id.as_deref(),
|
content.group_id.as_deref(),
|
||||||
error,
|
error,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(push_keys) = content.push_keys {
|
|
||||||
return pushkeys::handle_push_key(tr, from_user_id, push_keys).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(update) = content.user_discovery_update {
|
if let Some(update) = content.user_discovery_update {
|
||||||
return user_discovery::handle_user_discovery_update(ctx, from_user_id, update).await;
|
return user_discovery::handle_user_discovery_update(ctx, t, from_user_id, update).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(request) = content.user_discovery_request {
|
if let Some(request) = content.user_discovery_request {
|
||||||
return user_discovery::handle_user_discovery_request(ctx, tr, from_user_id, request).await;
|
return user_discovery::handle_user_discovery_request(ctx, t, from_user_id, request).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(proof) = content.key_verification_proof {
|
if let Some(proof) = content.key_verification_proof {
|
||||||
return verification::handle_key_verification_proof(tr, from_user_id, proof).await;
|
return verification::handle_key_verification_proof(t, from_user_id, proof).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(recovery) = content.passwordless_recovery {
|
if let Some(recovery) = content.passwordless_recovery {
|
||||||
return recovery::handle_passwordless_recovery(tr, from_user_id, recovery).await;
|
return recovery::handle_passwordless_recovery(t, from_user_id, recovery).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(heartbeat) = content.passwordless_recovery_heartbeat {
|
if let Some(heartbeat) = content.passwordless_recovery_heartbeat {
|
||||||
return recovery::handle_passwordless_recovery_heartbeat(tr, from_user_id, heartbeat).await;
|
return recovery::handle_passwordless_recovery_heartbeat(t, from_user_id, heartbeat).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
let group_id = content
|
let group_id = content
|
||||||
|
|
@ -162,11 +111,11 @@ pub(crate) async fn handle_encrypted(
|
||||||
.ok_or_else(|| TwonlyError::Generic("group-scoped message has no group ID".into()))?;
|
.ok_or_else(|| TwonlyError::Generic("group-scoped message has no group ID".into()))?;
|
||||||
|
|
||||||
if let Some(create) = content.group_create {
|
if let Some(create) = content.group_create {
|
||||||
return groups::handle_group_create(tr, from_user_id, &group_id, create).await;
|
return groups::handle_group_create(ctx, t, from_user_id, &group_id, create).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(join) = content.group_join {
|
if let Some(join) = content.group_join {
|
||||||
return groups::handle_group_join(tr, from_user_id, &group_id, join).await;
|
return groups::handle_group_join(t, from_user_id, &group_id, join).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
let is_member = sqlx::query_scalar!(
|
let is_member = sqlx::query_scalar!(
|
||||||
|
|
@ -174,7 +123,7 @@ pub(crate) async fn handle_encrypted(
|
||||||
group_id,
|
group_id,
|
||||||
from_user_id,
|
from_user_id,
|
||||||
)
|
)
|
||||||
.fetch_one(&mut **tr)
|
.fetch_one(&mut **t)
|
||||||
.await?
|
.await?
|
||||||
!= 0;
|
!= 0;
|
||||||
|
|
||||||
|
|
@ -182,21 +131,21 @@ pub(crate) async fn handle_encrypted(
|
||||||
let local_user_id = ctx.user_id().await?;
|
let local_user_id = ctx.user_id().await?;
|
||||||
|
|
||||||
if Group::direct_chat_id(local_user_id, from_user_id) == group_id {
|
if Group::direct_chat_id(local_user_id, from_user_id) == group_id {
|
||||||
let contact = Contact::get_contact_by_id(tr, from_user_id).await?;
|
let contact = Contact::get_contact_by_id(t, from_user_id).await?;
|
||||||
|
|
||||||
if let Some(contact) =
|
if let Some(contact) =
|
||||||
contact.filter(|value| value.accepted != 0 && value.deleted_by_user == 0)
|
contact.filter(|value| value.accepted != 0 && value.deleted_by_user == 0)
|
||||||
{
|
{
|
||||||
Group::create_direct_chat(ctx, tr, contact).await?;
|
Group::create_direct_chat(ctx, t, contact).await?;
|
||||||
} else {
|
} else {
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"UPDATE contacts SET requested = 1, deleted_by_user = 0 WHERE user_id = ?",
|
"UPDATE contacts SET requested = 1, deleted_by_user = 0 WHERE user_id = ?",
|
||||||
from_user_id,
|
from_user_id,
|
||||||
)
|
)
|
||||||
.execute(&mut **tr)
|
.execute(&mut **t)
|
||||||
.await?;
|
.await?;
|
||||||
messages::queue_encrypted_content(
|
messages::queue_encrypted_content(
|
||||||
tr,
|
t,
|
||||||
from_user_id,
|
from_user_id,
|
||||||
proto::EncryptedContent {
|
proto::EncryptedContent {
|
||||||
error_messages: Some(proto::encrypted_content::ErrorMessages {
|
error_messages: Some(proto::encrypted_content::ErrorMessages {
|
||||||
|
|
@ -213,28 +162,28 @@ pub(crate) async fn handle_encrypted(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
groups::ensure_group_member(tr, from_user_id, &group_id).await?;
|
groups::ensure_group_member(t, from_user_id, &group_id).await?;
|
||||||
|
|
||||||
if content.resend_group_public_key.is_some() {
|
if content.resend_group_public_key.is_some() {
|
||||||
return groups::handle_resend_group_public_key(tr, from_user_id, &group_id).await;
|
return groups::handle_resend_group_public_key(t, from_user_id, &group_id).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(update) = content.group_update {
|
if let Some(update) = content.group_update {
|
||||||
return groups::handle_group_update(tr, from_user_id, &group_id, update).await;
|
return groups::handle_group_update(ctx, t, from_user_id, &group_id, update).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(flame) = content.flame_sync {
|
if let Some(flame) = content.flame_sync {
|
||||||
return groups::handle_flame_sync(tr, &group_id, flame).await;
|
return groups::handle_flame_sync(t, &group_id, flame).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(message) = content.text_message {
|
if let Some(message) = content.text_message {
|
||||||
return text_message::handle_text_message(tr, from_user_id, &group_id, message).await;
|
return text_message::handle_text_message(t, from_user_id, &group_id, message).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(message) = content.additional_data_message {
|
if let Some(message) = content.additional_data_message {
|
||||||
return additional_data::handle_additional_data_message(
|
return additional_data::handle_additional_data_message(
|
||||||
ctx,
|
ctx,
|
||||||
tr,
|
t,
|
||||||
from_user_id,
|
from_user_id,
|
||||||
&group_id,
|
&group_id,
|
||||||
message,
|
message,
|
||||||
|
|
@ -243,15 +192,15 @@ pub(crate) async fn handle_encrypted(
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(media) = content.media {
|
if let Some(media) = content.media {
|
||||||
return media::handle_media(tr, from_user_id, &group_id, media).await;
|
return media::handle_media(t, from_user_id, &group_id, media).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(reaction) = content.reaction {
|
if let Some(reaction) = content.reaction {
|
||||||
return reaction::handle_reaction(tr, from_user_id, &group_id, reaction).await;
|
return reaction::handle_reaction(t, from_user_id, &group_id, reaction).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(indicator) = content.typing_indicator {
|
if let Some(indicator) = content.typing_indicator {
|
||||||
return handle_typing_indicator(tr, from_user_id, &group_id, indicator).await;
|
return handle_typing_indicator(t, from_user_id, &group_id, indicator).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(TwonlyError::Generic(format!(
|
Err(TwonlyError::Generic(format!(
|
||||||
|
|
|
||||||
|
|
@ -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::api::proto::client::encrypted_content;
|
||||||
use crate::bridge::callbacks::get_callbacks;
|
use crate::database::app::tables::{Group, MediaFile, Receipt};
|
||||||
use crate::database::app::tables::Group;
|
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use sqlx::{Sqlite, Transaction};
|
use sqlx::{Sqlite, Transaction};
|
||||||
|
|
||||||
|
|
@ -16,29 +15,20 @@ pub(crate) async fn handle_reaction(
|
||||||
reaction: encrypted_content::Reaction,
|
reaction: encrypted_content::Reaction,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
if reaction.remove {
|
if reaction.remove {
|
||||||
sqlx::query!(
|
Receipt::delete_reaction(
|
||||||
r#"
|
t,
|
||||||
DELETE FROM reactions
|
&reaction.target_message_id,
|
||||||
WHERE message_id = ? AND sender_id = ? AND emoji = ?
|
|
||||||
"#,
|
|
||||||
reaction.target_message_id,
|
|
||||||
from_user_id,
|
from_user_id,
|
||||||
reaction.emoji,
|
&reaction.emoji,
|
||||||
)
|
)
|
||||||
.execute(&mut **t)
|
|
||||||
.await?;
|
.await?;
|
||||||
} else {
|
} else {
|
||||||
sqlx::query!(
|
Receipt::insert_reaction(
|
||||||
r#"
|
t,
|
||||||
INSERT INTO reactions(message_id, emoji, sender_id)
|
&reaction.target_message_id,
|
||||||
VALUES (?, ?, ?)
|
|
||||||
ON CONFLICT(message_id, sender_id, emoji) DO NOTHING
|
|
||||||
"#,
|
|
||||||
reaction.target_message_id,
|
|
||||||
reaction.emoji,
|
|
||||||
from_user_id,
|
from_user_id,
|
||||||
|
&reaction.emoji,
|
||||||
)
|
)
|
||||||
.execute(&mut **t)
|
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -46,18 +36,7 @@ pub(crate) async fn handle_reaction(
|
||||||
Group::increase_last_message_exchange_to_now(t, group_id).await?;
|
Group::increase_last_message_exchange_to_now(t, group_id).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Ok(callbacks) = get_callbacks() {
|
MediaFile::handle_response_from_receiver(t, &reaction.target_message_id).await?;
|
||||||
let message_id = reaction.target_message_id;
|
|
||||||
tokio::spawn(async move {
|
|
||||||
(callbacks.api.media_action)(
|
|
||||||
"response".into(),
|
|
||||||
String::new(),
|
|
||||||
from_user_id,
|
|
||||||
message_id,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,21 @@
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
use crate::api::messages::outgoing::send_c2c_message_to_contact;
|
||||||
use crate::api::proto::client::encrypted_content;
|
use crate::api::proto::client::encrypted_content;
|
||||||
use crate::error::Result;
|
use crate::api::proto::client::EncryptedContent;
|
||||||
|
use crate::api::Server;
|
||||||
|
use crate::bridge::api::ServerResult;
|
||||||
|
use crate::context::Context;
|
||||||
|
use crate::error::{twonly_error, Result};
|
||||||
|
use crate::user_config::UserConfig;
|
||||||
|
use prost::Message as _;
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
use sqlx::{Sqlite, Transaction};
|
use sqlx::{Sqlite, Transaction};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
pub(crate) async fn handle_passwordless_recovery(
|
pub(crate) async fn handle_passwordless_recovery(
|
||||||
transaction: &mut Transaction<'_, Sqlite>,
|
t: &mut Transaction<'_, Sqlite>,
|
||||||
from_user_id: i64,
|
from_user_id: i64,
|
||||||
recovery: encrypted_content::PasswordLessRecovery,
|
recovery: encrypted_content::PasswordLessRecovery,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
|
@ -24,7 +32,7 @@ pub(crate) async fn handle_passwordless_recovery(
|
||||||
"#,
|
"#,
|
||||||
from_user_id,
|
from_user_id,
|
||||||
)
|
)
|
||||||
.execute(&mut **transaction)
|
.execute(&mut **t)
|
||||||
.await?;
|
.await?;
|
||||||
} else if let Some(share) = recovery.recovery_secret_share {
|
} else if let Some(share) = recovery.recovery_secret_share {
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
|
|
@ -39,19 +47,137 @@ pub(crate) async fn handle_passwordless_recovery(
|
||||||
recovery.threshold,
|
recovery.threshold,
|
||||||
from_user_id,
|
from_user_id,
|
||||||
)
|
)
|
||||||
.execute(&mut **transaction)
|
.execute(&mut **t)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
if let Ok(callbacks) = crate::bridge::callbacks::get_callbacks() {
|
#[cfg(not(test))]
|
||||||
|
{
|
||||||
|
let ctx = Context::get_static()?.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
(callbacks.api.recovery_changed)().await;
|
if let Err(error) = perform_heartbeat(&ctx).await {
|
||||||
|
tracing::warn!(%error, "passwordless recovery heartbeat failed");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn perform_heartbeat(ctx: &Arc<Context>) -> Result<()> {
|
||||||
|
let now = chrono::Utc::now();
|
||||||
|
let base_config = UserConfig::load_required_from(ctx)?;
|
||||||
|
let mut config = base_config.clone();
|
||||||
|
|
||||||
|
if let Some(recovery) = config.password_less_recovery.as_mut() {
|
||||||
|
let server_due = recovery
|
||||||
|
.last_server_heartbeat
|
||||||
|
.is_none_or(|last| now.signed_duration_since(last).num_days() > 20);
|
||||||
|
if server_due {
|
||||||
|
if let Some(encrypted_key) = recovery.encrypted_server_key.clone() {
|
||||||
|
if let ServerResult::ErrorCode(_) = Server::register_passwordless_recovery(
|
||||||
|
ctx,
|
||||||
|
encrypted_key,
|
||||||
|
recovery.pin_unlock_token.clone(),
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
return Err(twonly_error!("passwordless registration failed: {code}"));
|
||||||
|
}
|
||||||
|
recovery.last_server_heartbeat = Some(now);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let contacts_due = recovery
|
||||||
|
.last_contact_heartbeat
|
||||||
|
.is_none_or(|last| now.signed_duration_since(last).num_hours() >= 24);
|
||||||
|
if contacts_due {
|
||||||
|
let database = ctx.get_app_database().await;
|
||||||
|
let contacts = sqlx::query!(
|
||||||
|
r#"SELECT user_id, recovery_secret_share FROM contacts
|
||||||
|
WHERE recovery_is_trusted_friend = 1
|
||||||
|
AND recovery_last_heartbeat IS NULL
|
||||||
|
AND recovery_secret_share IS NOT NULL"#
|
||||||
|
)
|
||||||
|
.fetch_all(&database.pool)
|
||||||
|
.await?;
|
||||||
|
drop(database);
|
||||||
|
|
||||||
|
for contact in contacts {
|
||||||
|
let content = EncryptedContent {
|
||||||
|
passwordless_recovery: Some(encrypted_content::PasswordLessRecovery {
|
||||||
|
recovery_secret_share: contact.recovery_secret_share,
|
||||||
|
threshold: recovery.threshold,
|
||||||
|
delete: false,
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
send_c2c_message_to_contact()
|
||||||
|
.ctx(ctx)
|
||||||
|
.contact_id(contact.user_id)
|
||||||
|
.encrypted_content(content.encode_to_vec())
|
||||||
|
.call()
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
recovery.last_contact_heartbeat = Some(now);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let database = ctx.get_app_database().await;
|
||||||
|
let contacts = sqlx::query!(
|
||||||
|
r#"SELECT user_id, recovery_contacts_secret_share FROM contacts
|
||||||
|
WHERE recovery_contacts_secret_share IS NOT NULL
|
||||||
|
AND (recovery_contacts_last_heartbeat IS NULL
|
||||||
|
OR recovery_contacts_last_heartbeat <= ?)"#,
|
||||||
|
(now - chrono::Duration::days(7)).timestamp(),
|
||||||
|
)
|
||||||
|
.fetch_all(&database.pool)
|
||||||
|
.await?;
|
||||||
|
drop(database);
|
||||||
|
|
||||||
|
for contact in contacts {
|
||||||
|
let Some(share) = contact.recovery_contacts_secret_share else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let content = EncryptedContent {
|
||||||
|
passwordless_recovery_heartbeat: Some(
|
||||||
|
encrypted_content::PasswordLessRecoveryHeartbeat {
|
||||||
|
hash: Sha256::digest(share).to_vec(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
send_c2c_message_to_contact()
|
||||||
|
.ctx(ctx)
|
||||||
|
.contact_id(contact.user_id)
|
||||||
|
.encrypted_content(content.encode_to_vec())
|
||||||
|
.call()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let database = ctx.get_app_database().await;
|
||||||
|
sqlx::query!(
|
||||||
|
"UPDATE contacts SET recovery_contacts_last_heartbeat = ? WHERE user_id = ?",
|
||||||
|
now.timestamp(),
|
||||||
|
contact.user_id,
|
||||||
|
)
|
||||||
|
.execute(&database.pool)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if config != base_config {
|
||||||
|
UserConfig::update_json(
|
||||||
|
ctx,
|
||||||
|
&serde_json::to_string(&base_config)?,
|
||||||
|
&serde_json::to_string(&config)?,
|
||||||
|
)?;
|
||||||
|
if let Ok(callbacks) = crate::bridge::callbacks::get_callbacks() {
|
||||||
|
(callbacks.api.user_config_changed)(config).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) async fn handle_passwordless_recovery_heartbeat(
|
pub(crate) async fn handle_passwordless_recovery_heartbeat(
|
||||||
transaction: &mut Transaction<'_, Sqlite>,
|
t: &mut Transaction<'_, Sqlite>,
|
||||||
from_user_id: i64,
|
from_user_id: i64,
|
||||||
heartbeat: encrypted_content::PasswordLessRecoveryHeartbeat,
|
heartbeat: encrypted_content::PasswordLessRecoveryHeartbeat,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
|
@ -63,15 +189,17 @@ pub(crate) async fn handle_passwordless_recovery_heartbeat(
|
||||||
"#,
|
"#,
|
||||||
from_user_id,
|
from_user_id,
|
||||||
)
|
)
|
||||||
.fetch_optional(&mut **transaction)
|
.fetch_optional(&mut **t)
|
||||||
.await?
|
.await?
|
||||||
.flatten();
|
.flatten();
|
||||||
|
|
||||||
let valid = share
|
let valid = share
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.is_some_and(|share| Sha256::digest(share).as_slice() == heartbeat.hash);
|
.is_some_and(|share| Sha256::digest(share).as_slice() == heartbeat.hash);
|
||||||
|
|
||||||
if share.is_none() {
|
if share.is_none() {
|
||||||
super::messages::queue_encrypted_content(
|
super::messages::queue_encrypted_content(
|
||||||
transaction,
|
t,
|
||||||
from_user_id,
|
from_user_id,
|
||||||
crate::api::proto::client::EncryptedContent {
|
crate::api::proto::client::EncryptedContent {
|
||||||
passwordless_recovery: Some(encrypted_content::PasswordLessRecovery {
|
passwordless_recovery: Some(encrypted_content::PasswordLessRecovery {
|
||||||
|
|
@ -97,7 +225,153 @@ pub(crate) async fn handle_passwordless_recovery_heartbeat(
|
||||||
valid,
|
valid,
|
||||||
from_user_id,
|
from_user_id,
|
||||||
)
|
)
|
||||||
.execute(&mut **transaction)
|
.execute(&mut **t)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
async fn context() -> anyhow::Result<(tempfile::TempDir, Arc<Context>)> {
|
||||||
|
let temp = tempfile::tempdir()?;
|
||||||
|
let database_dir = temp.path().join("database");
|
||||||
|
let data_dir = temp.path().join("data");
|
||||||
|
std::fs::create_dir_all(data_dir.join("keyvalue"))?;
|
||||||
|
std::fs::write(
|
||||||
|
data_dir.join("keyvalue/user.json"),
|
||||||
|
serde_json::to_vec(&UserConfig::default())?,
|
||||||
|
)?;
|
||||||
|
let context = Context::init_for_testing(database_dir, data_dir).await?;
|
||||||
|
Ok((temp, context))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn insert_contact(ctx: &Arc<Context>, user_id: i64) -> Result<()> {
|
||||||
|
let database = ctx.get_app_database().await;
|
||||||
|
sqlx::query!(
|
||||||
|
"INSERT INTO contacts(user_id, username, accepted) VALUES (?, ?, 1)",
|
||||||
|
user_id,
|
||||||
|
format!("user_{user_id}"),
|
||||||
|
)
|
||||||
|
.execute(&database.pool)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn recovery_share_is_stored_and_deleted() -> anyhow::Result<()> {
|
||||||
|
let (_temp, ctx) = context().await?;
|
||||||
|
insert_contact(&ctx, 7).await?;
|
||||||
|
let database = ctx.get_app_database().await;
|
||||||
|
|
||||||
|
let mut transaction = database.pool.begin().await?;
|
||||||
|
handle_passwordless_recovery(
|
||||||
|
&mut transaction,
|
||||||
|
7,
|
||||||
|
encrypted_content::PasswordLessRecovery {
|
||||||
|
recovery_secret_share: Some(vec![1, 2, 3]),
|
||||||
|
threshold: 2,
|
||||||
|
delete: false,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
transaction.commit().await?;
|
||||||
|
|
||||||
|
let stored = sqlx::query!(
|
||||||
|
"SELECT recovery_contacts_secret_share, recovery_contacts_threshold FROM contacts WHERE user_id = 7"
|
||||||
|
)
|
||||||
|
.fetch_one(&database.pool)
|
||||||
|
.await?;
|
||||||
|
assert_eq!(stored.recovery_contacts_secret_share, Some(vec![1, 2, 3]));
|
||||||
|
assert_eq!(stored.recovery_contacts_threshold, Some(2));
|
||||||
|
|
||||||
|
let mut transaction = database.pool.begin().await?;
|
||||||
|
handle_passwordless_recovery(
|
||||||
|
&mut transaction,
|
||||||
|
7,
|
||||||
|
encrypted_content::PasswordLessRecovery {
|
||||||
|
delete: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
transaction.commit().await?;
|
||||||
|
|
||||||
|
let deleted = sqlx::query!(
|
||||||
|
"SELECT recovery_contacts_secret_share, recovery_contacts_threshold FROM contacts WHERE user_id = 7"
|
||||||
|
)
|
||||||
|
.fetch_one(&database.pool)
|
||||||
|
.await?;
|
||||||
|
assert_eq!(deleted.recovery_contacts_secret_share, None);
|
||||||
|
assert_eq!(deleted.recovery_contacts_threshold, None);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn valid_and_invalid_heartbeat_update_the_expected_state() -> anyhow::Result<()> {
|
||||||
|
let (_temp, ctx) = context().await?;
|
||||||
|
insert_contact(&ctx, 8).await?;
|
||||||
|
let database = ctx.get_app_database().await;
|
||||||
|
let share = vec![4, 5, 6];
|
||||||
|
sqlx::query!(
|
||||||
|
"UPDATE contacts SET recovery_secret_share = ? WHERE user_id = 8",
|
||||||
|
share,
|
||||||
|
)
|
||||||
|
.execute(&database.pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let mut transaction = database.pool.begin().await?;
|
||||||
|
handle_passwordless_recovery_heartbeat(
|
||||||
|
&mut transaction,
|
||||||
|
8,
|
||||||
|
encrypted_content::PasswordLessRecoveryHeartbeat {
|
||||||
|
hash: Sha256::digest([4, 5, 6]).to_vec(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
transaction.commit().await?;
|
||||||
|
let valid =
|
||||||
|
sqlx::query_scalar!("SELECT recovery_last_heartbeat FROM contacts WHERE user_id = 8")
|
||||||
|
.fetch_one(&database.pool)
|
||||||
|
.await?;
|
||||||
|
assert!(valid.is_some());
|
||||||
|
|
||||||
|
let mut transaction = database.pool.begin().await?;
|
||||||
|
handle_passwordless_recovery_heartbeat(
|
||||||
|
&mut transaction,
|
||||||
|
8,
|
||||||
|
encrypted_content::PasswordLessRecoveryHeartbeat { hash: vec![0; 32] },
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
transaction.commit().await?;
|
||||||
|
let invalid =
|
||||||
|
sqlx::query_scalar!("SELECT recovery_last_heartbeat FROM contacts WHERE user_id = 8")
|
||||||
|
.fetch_one(&database.pool)
|
||||||
|
.await?;
|
||||||
|
assert_eq!(invalid, None);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn heartbeat_without_share_queues_deletion_response() -> anyhow::Result<()> {
|
||||||
|
let (_temp, ctx) = context().await?;
|
||||||
|
insert_contact(&ctx, 9).await?;
|
||||||
|
let database = ctx.get_app_database().await;
|
||||||
|
let mut transaction = database.pool.begin().await?;
|
||||||
|
handle_passwordless_recovery_heartbeat(
|
||||||
|
&mut transaction,
|
||||||
|
9,
|
||||||
|
encrypted_content::PasswordLessRecoveryHeartbeat { hash: vec![1; 32] },
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
transaction.commit().await?;
|
||||||
|
|
||||||
|
let queued = sqlx::query_scalar!("SELECT COUNT(*) FROM receipts WHERE contact_id = 9")
|
||||||
|
.fetch_one(&database.pool)
|
||||||
|
.await?;
|
||||||
|
assert_eq!(queued, 1);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ use crate::api::proto::client::encrypted_content;
|
||||||
use crate::database::app::tables::{Group, Message, MessageType, NewMessage};
|
use crate::database::app::tables::{Group, Message, MessageType, NewMessage};
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use crate::utils::milliseconds_to_seconds;
|
use crate::utils::milliseconds_to_seconds;
|
||||||
|
use encrypted_content::message_update::Type;
|
||||||
use sqlx::{Sqlite, Transaction};
|
use sqlx::{Sqlite, Transaction};
|
||||||
|
|
||||||
pub(crate) async fn handle_text_message(
|
pub(crate) async fn handle_text_message(
|
||||||
|
|
@ -36,3 +37,162 @@ pub(crate) async fn handle_text_message(
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn handle_message_update(
|
||||||
|
t: &mut Transaction<'_, Sqlite>,
|
||||||
|
from_user_id: i64,
|
||||||
|
update: encrypted_content::MessageUpdate,
|
||||||
|
) -> Result<()> {
|
||||||
|
let timestamp = milliseconds_to_seconds(update.timestamp);
|
||||||
|
|
||||||
|
let update_type = Type::try_from(update.r#type)?;
|
||||||
|
tracing::info!(?update_type, from_user_id, "update text message");
|
||||||
|
|
||||||
|
match update_type {
|
||||||
|
Type::Opened => {
|
||||||
|
for message_id in update.multiple_target_message_ids {
|
||||||
|
let action_at = sqlx::query_scalar::<_, i64>(
|
||||||
|
"SELECT MAX(created_at, ?) FROM messages WHERE message_id = ?",
|
||||||
|
)
|
||||||
|
.bind(timestamp)
|
||||||
|
.bind(&message_id)
|
||||||
|
.fetch_optional(&mut **t)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let Some(action_at) = action_at else { continue };
|
||||||
|
|
||||||
|
sqlx::query!(
|
||||||
|
r#"
|
||||||
|
INSERT INTO message_actions(message_id, contact_id, type, action_at)
|
||||||
|
VALUES (?, ?, 'openedAt', ?)
|
||||||
|
ON CONFLICT(message_id, contact_id, type)
|
||||||
|
DO UPDATE SET action_at = excluded.action_at
|
||||||
|
"#,
|
||||||
|
message_id,
|
||||||
|
from_user_id,
|
||||||
|
action_at,
|
||||||
|
)
|
||||||
|
.execute(&mut **t)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
sqlx::query!(
|
||||||
|
r#"UPDATE messages SET opened_at = ?, opened_by_all = CASE WHEN NOT EXISTS(
|
||||||
|
SELECT 1 FROM group_members gm
|
||||||
|
WHERE gm.group_id = messages.group_id AND NOT EXISTS(
|
||||||
|
SELECT 1 FROM message_actions ma
|
||||||
|
WHERE ma.message_id = messages.message_id
|
||||||
|
AND ma.contact_id = gm.contact_id AND ma.type = 'openedAt'
|
||||||
|
)
|
||||||
|
) THEN ? ELSE NULL END
|
||||||
|
WHERE message_id = ?"#,
|
||||||
|
action_at,
|
||||||
|
action_at,
|
||||||
|
message_id,
|
||||||
|
)
|
||||||
|
.execute(&mut **t)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Type::Delete => {
|
||||||
|
let media_id = sqlx::query_scalar!(
|
||||||
|
"SELECT media_id FROM messages WHERE message_id = ? AND sender_id = ?",
|
||||||
|
update.sender_message_id,
|
||||||
|
from_user_id,
|
||||||
|
)
|
||||||
|
.fetch_optional(&mut **t)
|
||||||
|
.await?
|
||||||
|
.flatten();
|
||||||
|
|
||||||
|
sqlx::query!(
|
||||||
|
"DELETE FROM message_histories WHERE message_id = ?",
|
||||||
|
update.sender_message_id
|
||||||
|
)
|
||||||
|
.execute(&mut **t)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
sqlx::query!(
|
||||||
|
"DELETE FROM receipts WHERE message_id = ?",
|
||||||
|
update.sender_message_id
|
||||||
|
)
|
||||||
|
.execute(&mut **t)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
sqlx::query!(
|
||||||
|
r#"
|
||||||
|
UPDATE messages
|
||||||
|
SET is_deleted_from_sender = 1, content = NULL, media_id = NULL, modified_at = ?
|
||||||
|
WHERE message_id = ? AND sender_id = ?
|
||||||
|
"#,
|
||||||
|
timestamp,
|
||||||
|
update.sender_message_id,
|
||||||
|
from_user_id,
|
||||||
|
)
|
||||||
|
.execute(&mut **t)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if let Some(media_id) = media_id {
|
||||||
|
let references = sqlx::query_scalar!(
|
||||||
|
"SELECT COUNT(*) FROM messages WHERE media_id = ?",
|
||||||
|
media_id,
|
||||||
|
)
|
||||||
|
.fetch_one(&mut **t)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if references == 0 {
|
||||||
|
let media_type = sqlx::query_scalar!(
|
||||||
|
"SELECT type FROM media_files WHERE media_id = ?",
|
||||||
|
media_id,
|
||||||
|
)
|
||||||
|
.fetch_optional(&mut **t)
|
||||||
|
.await?;
|
||||||
|
sqlx::query!("DELETE FROM media_files WHERE media_id = ?", media_id)
|
||||||
|
.execute(&mut **t)
|
||||||
|
.await?;
|
||||||
|
if let Some(media_type) = media_type {
|
||||||
|
let ctx = crate::context::Context::get_static()?;
|
||||||
|
let ctx = ctx.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
// Let the surrounding message transaction commit before
|
||||||
|
// applying its corresponding filesystem side effect.
|
||||||
|
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
|
||||||
|
if let Err(error) =
|
||||||
|
crate::services::mediafiles::MediaFileService::new(&ctx)
|
||||||
|
.remove_files_if_deleted(&media_id, &media_type)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
tracing::warn!(media_id, %error, "could not remove media files");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Type::EditText => {
|
||||||
|
sqlx::query!(
|
||||||
|
r#"INSERT INTO message_histories(message_id, content, created_at)
|
||||||
|
SELECT message_id, content, ? FROM messages
|
||||||
|
WHERE message_id = ? AND sender_id = ? AND content IS NOT NULL"#,
|
||||||
|
timestamp,
|
||||||
|
update.sender_message_id,
|
||||||
|
from_user_id,
|
||||||
|
)
|
||||||
|
.execute(&mut **t)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
sqlx::query!(
|
||||||
|
r#"
|
||||||
|
UPDATE messages
|
||||||
|
SET content = ?, modified_at = ?
|
||||||
|
WHERE message_id = ? AND sender_id = ? AND content IS NOT NULL
|
||||||
|
"#,
|
||||||
|
update.text,
|
||||||
|
timestamp,
|
||||||
|
update.sender_message_id,
|
||||||
|
from_user_id,
|
||||||
|
)
|
||||||
|
.execute(&mut **t)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,13 +9,10 @@ use crate::context::Context;
|
||||||
use crate::database::app::tables::Contact;
|
use crate::database::app::tables::Contact;
|
||||||
use crate::error::{twonly_error, Result, TwonlyError};
|
use crate::error::{twonly_error, Result, TwonlyError};
|
||||||
use crate::user_config::UserConfig;
|
use crate::user_config::UserConfig;
|
||||||
|
use crate::user_discovery::UserDiscoveryVersion;
|
||||||
|
use prost::Message;
|
||||||
use sqlx::{Sqlite, Transaction};
|
use sqlx::{Sqlite, Transaction};
|
||||||
use std::collections::HashSet;
|
use std::sync::Arc;
|
||||||
use std::sync::LazyLock;
|
|
||||||
use tokio::sync::Mutex;
|
|
||||||
|
|
||||||
static REQUESTED_UPDATES: LazyLock<Mutex<HashSet<i64>>> =
|
|
||||||
LazyLock::new(|| Mutex::new(HashSet::new()));
|
|
||||||
|
|
||||||
pub(crate) async fn check_sender_version(
|
pub(crate) async fn check_sender_version(
|
||||||
ctx: &Context,
|
ctx: &Context,
|
||||||
|
|
@ -33,20 +30,30 @@ pub(crate) async fn check_sender_version(
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(current_version) = ctx
|
// The inbound message transaction owns the app database's sole connection.
|
||||||
.get_user_discovery()
|
// Going through `UserDiscovery::should_request_new_messages` here would ask
|
||||||
.get()
|
// the native store to acquire that same connection and deadlock until the
|
||||||
.await
|
// pool's 30-second acquire timeout expires.
|
||||||
.should_request_new_messages(from_user_id, &version)
|
let received_version = UserDiscoveryVersion::decode(version.as_slice())?;
|
||||||
|
let stored_version = sqlx::query_scalar!(
|
||||||
|
"SELECT user_discovery_version FROM contacts WHERE user_id = ?",
|
||||||
|
from_user_id,
|
||||||
|
)
|
||||||
|
.fetch_optional(&mut **t)
|
||||||
.await?
|
.await?
|
||||||
else {
|
.flatten()
|
||||||
return Ok(());
|
.map(|version| UserDiscoveryVersion::decode(version.as_slice()))
|
||||||
};
|
.transpose()?
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
if !REQUESTED_UPDATES.lock().await.insert(from_user_id) {
|
if received_version.announcement <= stored_version.announcement
|
||||||
|
&& received_version.promotion <= stored_version.promotion
|
||||||
|
{
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let current_version = stored_version.encode_to_vec();
|
||||||
|
|
||||||
queue_encrypted_content(
|
queue_encrypted_content(
|
||||||
t,
|
t,
|
||||||
from_user_id,
|
from_user_id,
|
||||||
|
|
@ -62,8 +69,9 @@ pub(crate) async fn check_sender_version(
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn handle_user_discovery_request(
|
pub(crate) async fn handle_user_discovery_request(
|
||||||
ctx: &Context,
|
ctx: &Arc<Context>,
|
||||||
t: &mut Transaction<'_, Sqlite>,
|
t: &mut Transaction<'_, Sqlite>,
|
||||||
from_user_id: i64,
|
from_user_id: i64,
|
||||||
request: encrypted_content::UserDiscoveryRequest,
|
request: encrypted_content::UserDiscoveryRequest,
|
||||||
|
|
@ -82,7 +90,7 @@ pub(crate) async fn handle_user_discovery_request(
|
||||||
.get_user_discovery()
|
.get_user_discovery()
|
||||||
.get()
|
.get()
|
||||||
.await
|
.await
|
||||||
.get_new_messages(from_user_id, &request.current_version)
|
.get_new_messages(from_user_id, &request.current_version, t)
|
||||||
.await?;
|
.await?;
|
||||||
if !messages.is_empty() {
|
if !messages.is_empty() {
|
||||||
queue_encrypted_content(
|
queue_encrypted_content(
|
||||||
|
|
@ -98,23 +106,27 @@ pub(crate) async fn handle_user_discovery_request(
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn handle_user_discovery_update(
|
pub(crate) async fn handle_user_discovery_update(
|
||||||
ctx: &Context,
|
ctx: &Arc<Context>,
|
||||||
|
t: &mut Transaction<'_, Sqlite>,
|
||||||
from_user_id: i64,
|
from_user_id: i64,
|
||||||
update: encrypted_content::UserDiscoveryUpdate,
|
update: encrypted_content::UserDiscoveryUpdate,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
if !UserConfig::load_required_from(ctx)?.is_user_discovery_enabled {
|
if !UserConfig::load_required_from(ctx)?.is_user_discovery_enabled {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
if update.messages.iter().any(|message| message.is_empty()) {
|
if update.messages.iter().any(|message| message.is_empty()) {
|
||||||
return Err(TwonlyError::Generic(
|
return Err(TwonlyError::Generic(
|
||||||
"user-discovery update contains an empty message".into(),
|
"user-discovery update contains an empty message".into(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(ctx
|
Ok(ctx
|
||||||
.get_user_discovery()
|
.get_user_discovery()
|
||||||
.get()
|
.get()
|
||||||
.await
|
.await
|
||||||
.handle_new_messages(from_user_id, None, update.messages)
|
.handle_new_messages(from_user_id, None, update.messages, t)
|
||||||
.await?)
|
.await?)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -333,6 +333,8 @@ pub(crate) async fn handle_decoded_server_message(
|
||||||
"messages",
|
"messages",
|
||||||
"groups",
|
"groups",
|
||||||
"contacts",
|
"contacts",
|
||||||
|
"key_verifications",
|
||||||
|
"user_discovery_own_promotions",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
let ctx = ctx.clone();
|
let ctx = ctx.clone();
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ use crate::user_config::UserConfig;
|
||||||
use prost::Message as _;
|
use prost::Message as _;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
async fn decorate_content(
|
pub(crate) async fn decorate_content(
|
||||||
ctx: &Context,
|
ctx: &Context,
|
||||||
contact_id: i64,
|
contact_id: i64,
|
||||||
content: &mut proto::EncryptedContent,
|
content: &mut proto::EncryptedContent,
|
||||||
|
|
@ -35,6 +35,7 @@ async fn decorate_content(
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.is_user_discovery_enabled && is_persisted_message {
|
if config.is_user_discovery_enabled && is_persisted_message {
|
||||||
|
ctx.initialize_user_discovery_from_config().await?;
|
||||||
let database = ctx.get_app_database().await;
|
let database = ctx.get_app_database().await;
|
||||||
let allowed = sqlx::query_scalar!(
|
let allowed = sqlx::query_scalar!(
|
||||||
r#"SELECT EXISTS(SELECT 1 FROM contacts WHERE user_id = ? AND accepted = 1
|
r#"SELECT EXISTS(SELECT 1 FROM contacts WHERE user_id = ? AND accepted = 1
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ impl ApiAuthHandshaker {
|
||||||
.ok_or_else(|| TwonlyError::Generic("User configuration not found".into()))?;
|
.ok_or_else(|| TwonlyError::Generic("User configuration not found".into()))?;
|
||||||
let device_id = user.device_id;
|
let device_id = user.device_id;
|
||||||
let app_version = user.app_version.to_string();
|
let app_version = user.app_version.to_string();
|
||||||
Ok((user.user_id, device_id, app_version))
|
Ok((Some(user.user_id), device_id, app_version))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn request_handshake(
|
async fn request_handshake(
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ use crate::error::Result;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::sync::{Arc, LazyLock, Weak};
|
use std::sync::{Arc, LazyLock, Weak};
|
||||||
|
use std::time::Duration;
|
||||||
use stream_tungstenite::WebSocketClient;
|
use stream_tungstenite::WebSocketClient;
|
||||||
use tokio::sync::{broadcast, oneshot, Mutex, RwLock};
|
use tokio::sync::{broadcast, oneshot, Mutex, RwLock};
|
||||||
|
|
||||||
|
|
@ -90,6 +91,7 @@ impl ApiClient {
|
||||||
};
|
};
|
||||||
|
|
||||||
let client = WebSocketClient::builder(host)
|
let client = WebSocketClient::builder(host)
|
||||||
|
.receive_timeout(Duration::from_secs(60))
|
||||||
.handshaker(handshaker)
|
.handshaker(handshaker)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
|
@ -155,8 +157,11 @@ impl ApiClient {
|
||||||
|
|
||||||
pub async fn close(&self) {
|
pub async fn close(&self) {
|
||||||
self.deliberately_closed.store(true, Ordering::Release);
|
self.deliberately_closed.store(true, Ordering::Release);
|
||||||
if let Some(_) = self.ws_client.lock().await.take() {
|
let client = self.ws_client.lock().await.take();
|
||||||
// Drop client to disconnect
|
if let Some(client) = client {
|
||||||
|
if let Err(error) = client.shutdown_graceful(Duration::from_secs(5)).await {
|
||||||
|
tracing::warn!("WebSocket shutdown did not finish cleanly: {error}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
self.fail_pending().await;
|
self.fail_pending().await;
|
||||||
self.set_state(ApiConnectionState::Stopped).await;
|
self.set_state(ApiConnectionState::Stopped).await;
|
||||||
|
|
|
||||||
|
|
@ -24,31 +24,8 @@ impl Server {
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn load_plan_balance(ctx: &Arc<Context>, use_cache: bool) -> Result<Vec<u8>> {
|
pub async fn load_plan_balance(ctx: &Arc<Context>) -> Result<Vec<u8>> {
|
||||||
let database = ctx.get_app_database().await;
|
Self::get_plan_balance(ctx).await
|
||||||
match Self::get_plan_balance(ctx).await {
|
|
||||||
Ok(response) => {
|
|
||||||
sqlx::query!(
|
|
||||||
r#"
|
|
||||||
INSERT INTO api_state(key, value) VALUES('plan_balance', ?)
|
|
||||||
ON CONFLICT(key) DO UPDATE SET value = excluded.value,
|
|
||||||
updated_at = CAST(strftime('%s', 'now') AS INTEGER)
|
|
||||||
"#,
|
|
||||||
response,
|
|
||||||
)
|
|
||||||
.execute(&database.pool)
|
|
||||||
.await?;
|
|
||||||
database.notify_committed(["api_state"]);
|
|
||||||
Ok(response)
|
|
||||||
}
|
|
||||||
Err(network_error) if use_cache => {
|
|
||||||
sqlx::query_scalar!("SELECT value FROM api_state WHERE key = 'plan_balance'")
|
|
||||||
.fetch_optional(&database.pool)
|
|
||||||
.await?
|
|
||||||
.ok_or(network_error)
|
|
||||||
}
|
|
||||||
Err(error) => Err(error),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn ipa_purchase(
|
pub async fn ipa_purchase(
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,15 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use crate::api::messages::incoming::client2client::messages;
|
use crate::api::messages::incoming::client2client::messages;
|
||||||
|
use crate::api::proto::server_to_client::response::ok::Ok as ResponseOk;
|
||||||
|
use crate::api::runtime::helpers::decode_ok_value;
|
||||||
use crate::api::ApiRuntime;
|
use crate::api::ApiRuntime;
|
||||||
pub use crate::api::PqcPreKeyInput;
|
pub use crate::api::PqcPreKeyInput;
|
||||||
use crate::api::Server;
|
use crate::api::Server;
|
||||||
use crate::context::Context;
|
use crate::context::Context;
|
||||||
use crate::error::Result;
|
use crate::error::{Result, TwonlyError};
|
||||||
use crate::frb_generated::StreamSink;
|
use crate::frb_generated::StreamSink;
|
||||||
|
use crate::services::contacts::ContactService;
|
||||||
use crate::user_config::UserConfig;
|
use crate::user_config::UserConfig;
|
||||||
use flutter_rust_bridge::frb;
|
use flutter_rust_bridge::frb;
|
||||||
use prost::Message;
|
use prost::Message;
|
||||||
|
|
@ -20,49 +23,25 @@ pub enum ServerResult<T> {
|
||||||
ErrorCode(i32),
|
ErrorCode(i32),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[frb]
|
fn api_result<T>(result: ServerResult<T>) -> Result<T> {
|
||||||
pub enum ServerResultEmpty {
|
match result {
|
||||||
Ok,
|
ServerResult::Ok(value) => Ok(value),
|
||||||
ErrorCode(i32),
|
ServerResult::ErrorCode(code) => Err(TwonlyError::Api(code)),
|
||||||
}
|
|
||||||
|
|
||||||
#[frb]
|
|
||||||
pub enum ServerResultVecU8 {
|
|
||||||
Ok(Vec<u8>),
|
|
||||||
ErrorCode(i32),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[frb]
|
|
||||||
pub enum ServerResultI64 {
|
|
||||||
Ok(i64),
|
|
||||||
ErrorCode(i32),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ServerResult<()> {
|
|
||||||
pub fn into_bridge(self) -> ServerResultEmpty {
|
|
||||||
match self {
|
|
||||||
ServerResult::Ok(()) => ServerResultEmpty::Ok,
|
|
||||||
ServerResult::ErrorCode(c) => ServerResultEmpty::ErrorCode(c),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ServerResult<Vec<u8>> {
|
fn empty_api_response(bytes: Vec<u8>) -> Result<()> {
|
||||||
pub fn into_bridge(self) -> ServerResultVecU8 {
|
api_result(decode_ok_value(bytes, |value| match value {
|
||||||
match self {
|
ResponseOk::None(_) => Some(()),
|
||||||
ServerResult::Ok(v) => ServerResultVecU8::Ok(v),
|
_ => None,
|
||||||
ServerResult::ErrorCode(c) => ServerResultVecU8::ErrorCode(c),
|
})?)
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ServerResult<i64> {
|
fn encoded_api_response<T: Message>(
|
||||||
pub fn into_bridge(self) -> ServerResultI64 {
|
bytes: Vec<u8>,
|
||||||
match self {
|
extract: impl FnOnce(ResponseOk) -> Option<T>,
|
||||||
ServerResult::Ok(v) => ServerResultI64::Ok(v),
|
) -> Result<Vec<u8>> {
|
||||||
ServerResult::ErrorCode(c) => ServerResultI64::ErrorCode(c),
|
api_result(decode_ok_value(bytes, extract)?).map(|value| value.encode_to_vec())
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
|
@ -133,7 +112,7 @@ pub struct PreparedOutgoingMessage {
|
||||||
impl RustApi {
|
impl RustApi {
|
||||||
pub async fn request_contact_by_username(username: String) -> Result<()> {
|
pub async fn request_contact_by_username(username: String) -> Result<()> {
|
||||||
let ctx = Context::get_static()?;
|
let ctx = Context::get_static()?;
|
||||||
crate::services::contacts::ContactService::new(ctx)
|
ContactService::new(ctx)
|
||||||
.request_by_username(username, false)
|
.request_by_username(username, false)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
@ -206,122 +185,156 @@ impl RustApi {
|
||||||
proof_of_work: i64,
|
proof_of_work: i64,
|
||||||
lang_code: String,
|
lang_code: String,
|
||||||
is_ios: bool,
|
is_ios: bool,
|
||||||
) -> Result<ServerResultI64> {
|
) -> Result<i64> {
|
||||||
let ctx = Context::get_static()?;
|
let ctx = Context::get_static()?;
|
||||||
Server::register(ctx, username, proof_of_work, lang_code, is_ios)
|
Server::register(ctx, username, proof_of_work, lang_code, is_ios)
|
||||||
.await
|
.await
|
||||||
.map(|r| r.into_bridge())
|
.and_then(api_result)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_user_by_id(user_id: i64) -> Result<ServerResultVecU8> {
|
pub async fn get_user_by_id(user_id: i64) -> Result<Vec<u8>> {
|
||||||
let ctx = Context::get_static()?;
|
let ctx = Context::get_static()?;
|
||||||
Server::get_user_by_id(ctx, user_id)
|
Server::get_user_by_id(ctx, user_id)
|
||||||
.await
|
.await
|
||||||
.map(|r| r.into_bridge())
|
.and_then(api_result)
|
||||||
|
.map(|value| value.encode_to_vec())
|
||||||
}
|
}
|
||||||
pub async fn check_for_deleted_usernames() -> Result<()> {
|
pub async fn check_for_deleted_usernames() -> Result<()> {
|
||||||
let ctx = Context::get_static()?;
|
let ctx = Context::get_static()?;
|
||||||
Server::check_for_deleted_usernames(ctx).await
|
Server::check_for_deleted_usernames(ctx).await
|
||||||
}
|
}
|
||||||
pub async fn get_user_id_from_username(username: String) -> Result<ServerResultI64> {
|
pub async fn get_user_id_from_username(username: String) -> Result<i64> {
|
||||||
let ctx = Context::get_static()?;
|
let ctx = Context::get_static()?;
|
||||||
Server::get_user_id_from_username(ctx, username)
|
Server::get_user_id_from_username(ctx, username)
|
||||||
.await
|
.await
|
||||||
.map(|r| r.into_bridge())
|
.and_then(api_result)
|
||||||
}
|
}
|
||||||
pub async fn get_user_data(username: String) -> Result<ServerResultVecU8> {
|
pub async fn get_user_data(username: String) -> Result<Vec<u8>> {
|
||||||
let ctx = Context::get_static()?;
|
let ctx = Context::get_static()?;
|
||||||
Server::get_user_by_username(ctx, username)
|
Server::get_user_by_username(ctx, username)
|
||||||
.await
|
.await
|
||||||
.map(|r| r.into_bridge())
|
.and_then(api_result)
|
||||||
|
.map(|value| value.encode_to_vec())
|
||||||
}
|
}
|
||||||
pub async fn get_proof_of_work() -> Result<ServerResultVecU8> {
|
pub async fn get_proof_of_work() -> Result<Vec<u8>> {
|
||||||
let ctx = Context::get_static()?;
|
let ctx = Context::get_static()?;
|
||||||
Server::get_proof_of_work(ctx).await.map(|r| match r {
|
Server::get_proof_of_work(ctx)
|
||||||
ServerResult::Ok(data) => ServerResultVecU8::Ok(data.encode_to_vec()),
|
|
||||||
ServerResult::ErrorCode(c) => ServerResultVecU8::ErrorCode(c),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
pub async fn download_done(token: Vec<u8>) -> Result<ServerResultEmpty> {
|
|
||||||
let ctx = Context::get_static()?;
|
|
||||||
Server::download_done(ctx, token)
|
|
||||||
.await
|
.await
|
||||||
.map(|r| r.into_bridge())
|
.and_then(api_result)
|
||||||
|
.map(|value| value.encode_to_vec())
|
||||||
}
|
}
|
||||||
pub async fn set_login_token(token: Vec<u8>) -> Result<ServerResultEmpty> {
|
pub async fn download_done(token: Vec<u8>) -> Result<()> {
|
||||||
|
let ctx = Context::get_static()?;
|
||||||
|
Server::download_done(ctx, token).await.and_then(api_result)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn download_media(media_id: String) -> Result<()> {
|
||||||
|
let ctx = Context::get_static()?;
|
||||||
|
crate::services::mediafiles::MediaFileService::new(ctx)
|
||||||
|
.download_when_available(&media_id)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn download_pending_media() -> Result<()> {
|
||||||
|
let ctx = Context::get_static()?;
|
||||||
|
crate::services::mediafiles::MediaFileService::new(ctx)
|
||||||
|
.download_pending()
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn request_media_reupload(media_id: String) -> Result<()> {
|
||||||
|
let ctx = Context::get_static()?;
|
||||||
|
crate::services::mediafiles::MediaFileService::new(ctx)
|
||||||
|
.request_reupload(&media_id)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
pub async fn set_login_token(token: Vec<u8>) -> Result<()> {
|
||||||
let ctx = Context::get_static()?;
|
let ctx = Context::get_static()?;
|
||||||
Server::set_login_token(ctx, token)
|
Server::set_login_token(ctx, token)
|
||||||
.await
|
.await
|
||||||
.map(|r| r.into_bridge())
|
.and_then(api_result)
|
||||||
}
|
}
|
||||||
pub async fn request_memories_upload(
|
pub async fn request_memories_upload(
|
||||||
size: i64,
|
size: i64,
|
||||||
original_date: i64,
|
original_date: i64,
|
||||||
media_id: String,
|
media_id: String,
|
||||||
) -> Result<ServerResultVecU8> {
|
) -> Result<Vec<u8>> {
|
||||||
let ctx = Context::get_static()?;
|
let ctx = Context::get_static()?;
|
||||||
Server::request_memories_upload(ctx, size, original_date, media_id)
|
Server::request_memories_upload(ctx, size, original_date, media_id)
|
||||||
.await
|
.await
|
||||||
.map(|r| r.into_bridge())
|
.and_then(api_result)
|
||||||
|
.map(|value| value.encode_to_vec())
|
||||||
}
|
}
|
||||||
pub async fn get_memories_usage() -> Result<ServerResultVecU8> {
|
pub async fn get_memories_usage() -> Result<Vec<u8>> {
|
||||||
let ctx = Context::get_static()?;
|
let ctx = Context::get_static()?;
|
||||||
Server::get_memories_usage(ctx)
|
Server::get_memories_usage(ctx)
|
||||||
.await
|
.await
|
||||||
.map(|r| r.into_bridge())
|
.and_then(api_result)
|
||||||
|
.map(|value| value.encode_to_vec())
|
||||||
}
|
}
|
||||||
pub async fn get_memories_url(media_id: String, thumbnail: bool) -> Result<ServerResultVecU8> {
|
pub async fn get_memories_url(media_id: String, thumbnail: bool) -> Result<Vec<u8>> {
|
||||||
let ctx = Context::get_static()?;
|
let ctx = Context::get_static()?;
|
||||||
Server::get_memories_url(ctx, media_id, thumbnail)
|
Server::get_memories_url(ctx, media_id, thumbnail)
|
||||||
.await
|
.await
|
||||||
.map(|r| r.into_bridge())
|
.and_then(api_result)
|
||||||
|
.map(|value| value.encode_to_vec())
|
||||||
}
|
}
|
||||||
pub async fn confirm_memories_upload(media_id: String) -> Result<ServerResultEmpty> {
|
pub async fn confirm_memories_upload(media_id: String) -> Result<()> {
|
||||||
let ctx = Context::get_static()?;
|
let ctx = Context::get_static()?;
|
||||||
Server::confirm_memories_upload(ctx, media_id)
|
Server::confirm_memories_upload(ctx, media_id)
|
||||||
.await
|
.await
|
||||||
.map(|r| r.into_bridge())
|
.and_then(api_result)
|
||||||
}
|
}
|
||||||
pub async fn delete_memory(media_id: String) -> Result<ServerResultEmpty> {
|
pub async fn delete_memory(media_id: String) -> Result<()> {
|
||||||
let ctx = Context::get_static()?;
|
let ctx = Context::get_static()?;
|
||||||
Server::delete_memory(ctx, media_id)
|
Server::delete_memory(ctx, media_id)
|
||||||
.await
|
.await
|
||||||
.map(|r| r.into_bridge())
|
.and_then(api_result)
|
||||||
}
|
}
|
||||||
pub async fn disable_memories_backup() -> Result<ServerResultEmpty> {
|
pub async fn disable_memories_backup() -> Result<()> {
|
||||||
let ctx = Context::get_static()?;
|
let ctx = Context::get_static()?;
|
||||||
Server::disable_memories_backup(ctx)
|
Server::disable_memories_backup(ctx)
|
||||||
.await
|
.await
|
||||||
.map(|r| r.into_bridge())
|
.and_then(api_result)
|
||||||
}
|
}
|
||||||
pub async fn get_plan_balance() -> Result<Vec<u8>> {
|
pub async fn get_plan_balance() -> Result<Vec<u8>> {
|
||||||
let ctx = Context::get_static()?;
|
let ctx = Context::get_static()?;
|
||||||
Server::get_plan_balance(ctx).await
|
encoded_api_response(Server::get_plan_balance(ctx).await?, |value| match value {
|
||||||
|
ResponseOk::Planballance(balance) => Some(balance),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
pub async fn load_plan_balance(use_cache: bool) -> Result<Vec<u8>> {
|
pub async fn load_plan_balance() -> Result<Vec<u8>> {
|
||||||
let ctx = Context::get_static()?;
|
let ctx = Context::get_static()?;
|
||||||
Server::load_plan_balance(ctx, use_cache).await
|
encoded_api_response(Server::load_plan_balance(ctx).await?, |value| match value {
|
||||||
|
ResponseOk::Planballance(balance) => Some(balance),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
pub async fn remove_additional_user(user_id: i64) -> Result<ServerResultEmpty> {
|
pub async fn remove_additional_user(user_id: i64) -> Result<()> {
|
||||||
let ctx = Context::get_static()?;
|
let ctx = Context::get_static()?;
|
||||||
Server::remove_additional_user(ctx, user_id)
|
Server::remove_additional_user(ctx, user_id)
|
||||||
.await
|
.await
|
||||||
.map(|r| r.into_bridge())
|
.and_then(api_result)
|
||||||
}
|
}
|
||||||
pub async fn add_additional_user(user_id: i64) -> Result<ServerResultEmpty> {
|
pub async fn add_additional_user(user_id: i64) -> Result<()> {
|
||||||
let ctx = Context::get_static()?;
|
let ctx = Context::get_static()?;
|
||||||
Server::add_additional_user(ctx, user_id)
|
Server::add_additional_user(ctx, user_id)
|
||||||
.await
|
.await
|
||||||
.map(|r| r.into_bridge())
|
.and_then(api_result)
|
||||||
}
|
}
|
||||||
pub async fn register_passwordless_recovery(
|
pub async fn register_passwordless_recovery(
|
||||||
encrypted_server_key: Vec<u8>,
|
encrypted_server_key: Vec<u8>,
|
||||||
pin_unlock_token: Option<Vec<u8>>,
|
pin_unlock_token: Option<Vec<u8>>,
|
||||||
) -> Result<ServerResultEmpty> {
|
) -> Result<()> {
|
||||||
let ctx = Context::get_static()?;
|
let ctx = Context::get_static()?;
|
||||||
Server::register_passwordless_recovery(ctx, encrypted_server_key, pin_unlock_token)
|
Server::register_passwordless_recovery(ctx, encrypted_server_key, pin_unlock_token)
|
||||||
.await
|
.await
|
||||||
.map(|r| r.into_bridge())
|
.and_then(api_result)
|
||||||
|
}
|
||||||
|
pub async fn perform_passwordless_recovery_heartbeat() -> Result<()> {
|
||||||
|
let ctx = Context::get_static()?;
|
||||||
|
crate::api::messages::incoming::client2client::recovery::perform_heartbeat(ctx).await
|
||||||
}
|
}
|
||||||
pub async fn get_server_key_for_passwordless_recovery(
|
pub async fn get_server_key_for_passwordless_recovery(
|
||||||
user_id: i64,
|
user_id: i64,
|
||||||
|
|
@ -329,7 +342,7 @@ impl RustApi {
|
||||||
pin_unlock_token: Option<Vec<u8>>,
|
pin_unlock_token: Option<Vec<u8>>,
|
||||||
pin_protection_key: Option<Vec<u8>>,
|
pin_protection_key: Option<Vec<u8>>,
|
||||||
email: Option<String>,
|
email: Option<String>,
|
||||||
) -> Result<ServerResultVecU8> {
|
) -> Result<Vec<u8>> {
|
||||||
let ctx = Context::get_static()?;
|
let ctx = Context::get_static()?;
|
||||||
Server::get_server_key_for_passwordless_recovery(
|
Server::get_server_key_for_passwordless_recovery(
|
||||||
ctx,
|
ctx,
|
||||||
|
|
@ -340,23 +353,23 @@ impl RustApi {
|
||||||
email,
|
email,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map(|r| r.into_bridge())
|
.and_then(api_result)
|
||||||
}
|
}
|
||||||
pub async fn submit_recovery_share(
|
pub async fn submit_recovery_share(
|
||||||
notification_id: String,
|
notification_id: String,
|
||||||
encrypted_message: Vec<u8>,
|
encrypted_message: Vec<u8>,
|
||||||
) -> Result<ServerResultEmpty> {
|
) -> Result<()> {
|
||||||
let ctx = Context::get_static()?;
|
let ctx = Context::get_static()?;
|
||||||
Server::submit_recovery_share(ctx, notification_id, encrypted_message)
|
Server::submit_recovery_share(ctx, notification_id, encrypted_message)
|
||||||
.await
|
.await
|
||||||
.map(|r| r.into_bridge())
|
.and_then(api_result)
|
||||||
}
|
}
|
||||||
pub async fn register_passwordless_notification(
|
pub async fn register_passwordless_notification(
|
||||||
notification_id: String,
|
notification_id: String,
|
||||||
download_auth_token: Vec<u8>,
|
download_auth_token: Vec<u8>,
|
||||||
lang_code: String,
|
lang_code: String,
|
||||||
google_fcm: Option<String>,
|
google_fcm: Option<String>,
|
||||||
) -> Result<ServerResultEmpty> {
|
) -> Result<()> {
|
||||||
let ctx = Context::get_static()?;
|
let ctx = Context::get_static()?;
|
||||||
Server::register_passwordless_notification(
|
Server::register_passwordless_notification(
|
||||||
ctx,
|
ctx,
|
||||||
|
|
@ -366,13 +379,13 @@ impl RustApi {
|
||||||
google_fcm,
|
google_fcm,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map(|r| r.into_bridge())
|
.and_then(api_result)
|
||||||
}
|
}
|
||||||
pub async fn check_for_passwordless_notification(
|
pub async fn check_for_passwordless_notification(
|
||||||
notification_id: String,
|
notification_id: String,
|
||||||
download_auth_token: Vec<u8>,
|
download_auth_token: Vec<u8>,
|
||||||
already_received_message_ids: Vec<i64>,
|
already_received_message_ids: Vec<i64>,
|
||||||
) -> Result<ServerResultVecU8> {
|
) -> Result<Vec<u8>> {
|
||||||
let ctx = Context::get_static()?;
|
let ctx = Context::get_static()?;
|
||||||
Server::check_for_passwordless_notification(
|
Server::check_for_passwordless_notification(
|
||||||
ctx,
|
ctx,
|
||||||
|
|
@ -381,49 +394,46 @@ impl RustApi {
|
||||||
already_received_message_ids,
|
already_received_message_ids,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map(|r| r.into_bridge())
|
.and_then(api_result)
|
||||||
|
.map(|value| value.encode_to_vec())
|
||||||
}
|
}
|
||||||
pub async fn report_user(user_id: i64, reason: String) -> Result<ServerResultEmpty> {
|
pub async fn report_user(user_id: i64, reason: String) -> Result<()> {
|
||||||
let ctx = Context::get_static()?;
|
let ctx = Context::get_static()?;
|
||||||
Server::report_user(ctx, user_id, reason)
|
Server::report_user(ctx, user_id, reason)
|
||||||
.await
|
.await
|
||||||
.map(|r| r.into_bridge())
|
.and_then(api_result)
|
||||||
}
|
}
|
||||||
pub async fn delete_account() -> Result<ServerResultEmpty> {
|
pub async fn delete_account() -> Result<()> {
|
||||||
let ctx = Context::get_static()?;
|
let ctx = Context::get_static()?;
|
||||||
Server::delete_account(ctx).await.map(|r| r.into_bridge())
|
Server::delete_account(ctx).await.and_then(api_result)
|
||||||
}
|
}
|
||||||
pub async fn update_fcm_token(token: String) -> Result<ServerResultEmpty> {
|
pub async fn update_fcm_token(token: String) -> Result<()> {
|
||||||
let ctx = Context::get_static()?;
|
let ctx = Context::get_static()?;
|
||||||
Server::update_fcm_token(ctx, token)
|
Server::update_fcm_token(ctx, token)
|
||||||
.await
|
.await
|
||||||
.map(|r| r.into_bridge())
|
.and_then(api_result)
|
||||||
}
|
}
|
||||||
pub async fn ipa_purchase(
|
pub async fn ipa_purchase(
|
||||||
product_id: String,
|
product_id: String,
|
||||||
source: String,
|
source: String,
|
||||||
verification_data: String,
|
verification_data: String,
|
||||||
) -> Result<Vec<u8>> {
|
) -> Result<()> {
|
||||||
let ctx = Context::get_static()?;
|
let ctx = Context::get_static()?;
|
||||||
Server::ipa_purchase(ctx, product_id, source, verification_data).await
|
empty_api_response(Server::ipa_purchase(ctx, product_id, source, verification_data).await?)
|
||||||
}
|
}
|
||||||
pub async fn change_username(username: String) -> Result<ServerResultEmpty> {
|
pub async fn change_username(username: String) -> Result<()> {
|
||||||
let ctx = Context::get_static()?;
|
let ctx = Context::get_static()?;
|
||||||
Server::change_username(ctx, username)
|
Server::change_username(ctx, username)
|
||||||
.await
|
.await
|
||||||
.map(|r| r.into_bridge())
|
.and_then(api_result)
|
||||||
}
|
}
|
||||||
pub async fn force_ipa_check() -> Result<ServerResultEmpty> {
|
pub async fn force_ipa_check() -> Result<()> {
|
||||||
let ctx = Context::get_static()?;
|
let ctx = Context::get_static()?;
|
||||||
Server::force_ipa_check(ctx).await.map(|r| r.into_bridge())
|
Server::force_ipa_check(ctx).await.and_then(api_result)
|
||||||
}
|
}
|
||||||
pub async fn update_signed_pre_key(
|
pub async fn update_signed_pre_key(id: i64, key: Vec<u8>, signature: Vec<u8>) -> Result<()> {
|
||||||
id: i64,
|
|
||||||
key: Vec<u8>,
|
|
||||||
signature: Vec<u8>,
|
|
||||||
) -> Result<Vec<u8>> {
|
|
||||||
let ctx = Context::get_static()?;
|
let ctx = Context::get_static()?;
|
||||||
Server::update_signed_pre_key(ctx, id, key, signature).await
|
empty_api_response(Server::update_signed_pre_key(ctx, id, key, signature).await?)
|
||||||
}
|
}
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub async fn upload_pqc_pre_keys(
|
pub async fn upload_pqc_pre_keys(
|
||||||
|
|
@ -434,7 +444,7 @@ impl RustApi {
|
||||||
kyber_signed_prekey: Vec<u8>,
|
kyber_signed_prekey: Vec<u8>,
|
||||||
kyber_signed_prekey_signature: Vec<u8>,
|
kyber_signed_prekey_signature: Vec<u8>,
|
||||||
prekeys: Vec<PqcPreKeyInput>,
|
prekeys: Vec<PqcPreKeyInput>,
|
||||||
) -> Result<Vec<u8>> {
|
) -> Result<()> {
|
||||||
let ctx = Context::get_static()?;
|
let ctx = Context::get_static()?;
|
||||||
Server::upload_pqc_pre_keys(
|
Server::upload_pqc_pre_keys(
|
||||||
ctx,
|
ctx,
|
||||||
|
|
@ -447,16 +457,17 @@ impl RustApi {
|
||||||
prekeys,
|
prekeys,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
.and_then(empty_api_response)
|
||||||
}
|
}
|
||||||
pub async fn send_text_message(
|
pub async fn send_text_message(
|
||||||
user_id: i64,
|
user_id: i64,
|
||||||
body: Vec<u8>,
|
body: Vec<u8>,
|
||||||
push_data: Option<Vec<u8>>,
|
push_data: Option<Vec<u8>>,
|
||||||
) -> Result<ServerResultEmpty> {
|
) -> Result<()> {
|
||||||
let ctx = Context::get_static()?;
|
let ctx = Context::get_static()?;
|
||||||
Server::send_text_message(ctx, user_id, body, push_data)
|
Server::send_text_message(ctx, user_id, body, push_data)
|
||||||
.await
|
.await
|
||||||
.map(|r| r.into_bridge())
|
.and_then(api_result)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn send_encrypted_content(
|
pub async fn send_encrypted_content(
|
||||||
|
|
@ -587,48 +598,3 @@ impl RustApi {
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ServerResult<crate::api::proto::server_to_client::response::UserData> {
|
|
||||||
pub fn into_bridge(self) -> ServerResultVecU8 {
|
|
||||||
match self {
|
|
||||||
ServerResult::Ok(v) => ServerResultVecU8::Ok(prost::Message::encode_to_vec(&v)),
|
|
||||||
ServerResult::ErrorCode(c) => ServerResultVecU8::ErrorCode(c),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ServerResult<crate::api::proto::server_to_client::response::PasswordlessNotificationMessages> {
|
|
||||||
pub fn into_bridge(self) -> ServerResultVecU8 {
|
|
||||||
match self {
|
|
||||||
ServerResult::Ok(v) => ServerResultVecU8::Ok(prost::Message::encode_to_vec(&v)),
|
|
||||||
ServerResult::ErrorCode(c) => ServerResultVecU8::ErrorCode(c),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ServerResult<crate::api::proto::server_to_client::response::MemoriesUploadUrls> {
|
|
||||||
pub fn into_bridge(self) -> ServerResultVecU8 {
|
|
||||||
match self {
|
|
||||||
ServerResult::Ok(v) => ServerResultVecU8::Ok(prost::Message::encode_to_vec(&v)),
|
|
||||||
ServerResult::ErrorCode(c) => ServerResultVecU8::ErrorCode(c),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ServerResult<crate::api::proto::server_to_client::response::MemoriesUsage> {
|
|
||||||
pub fn into_bridge(self) -> ServerResultVecU8 {
|
|
||||||
match self {
|
|
||||||
ServerResult::Ok(v) => ServerResultVecU8::Ok(prost::Message::encode_to_vec(&v)),
|
|
||||||
ServerResult::ErrorCode(c) => ServerResultVecU8::ErrorCode(c),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ServerResult<crate::api::proto::server_to_client::response::MemoriesUrl> {
|
|
||||||
pub fn into_bridge(self) -> ServerResultVecU8 {
|
|
||||||
match self {
|
|
||||||
ServerResult::Ok(v) => ServerResultVecU8::Ok(prost::Message::encode_to_vec(&v)),
|
|
||||||
ServerResult::ErrorCode(c) => ServerResultVecU8::ErrorCode(c),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -54,15 +54,11 @@ callback_generator! {
|
||||||
},
|
},
|
||||||
Api api {
|
Api api {
|
||||||
resync_signal_session: (i64) => (),
|
resync_signal_session: (i64) => (),
|
||||||
push_key_requested: (i64) => (),
|
|
||||||
group_membership_error: (i64, String, String) => (),
|
|
||||||
media_action: (String, String, i64, String) => (),
|
media_action: (String, String, i64, String) => (),
|
||||||
verification_proof: (i64, Vec<u8>) => (),
|
verification_proof: (i64, Vec<u8>) => (),
|
||||||
create_push_data: (i64, Option<String>, Vec<u8>, i32) => Option<Vec<u8>>,
|
|
||||||
create_push_avatars: (i64) => (),
|
create_push_avatars: (i64) => (),
|
||||||
recovery_changed: () => (),
|
|
||||||
media_received: (String, i64) => (),
|
media_received: (String, i64) => (),
|
||||||
group_state_refresh: (String, bool) => ()
|
user_config_changed: (crate::user_config::UserConfig) => ()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
pub mod api;
|
pub mod api;
|
||||||
pub mod callbacks;
|
pub mod callbacks;
|
||||||
pub mod groups;
|
pub mod groups;
|
||||||
|
pub mod user_config;
|
||||||
pub mod wrapper;
|
pub mod wrapper;
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
@ -20,13 +21,12 @@ use crate::error::TwonlyError;
|
||||||
use crate::keys::KeyManager;
|
use crate::keys::KeyManager;
|
||||||
use crate::secure_storage::SecureStorage;
|
use crate::secure_storage::SecureStorage;
|
||||||
use crate::signal::engine::RustSignalEngine;
|
use crate::signal::engine::RustSignalEngine;
|
||||||
use crate::user_discovery::stores::{NativeUserDiscoveryStore, NativeUserDiscoveryUtils};
|
|
||||||
use crate::user_discovery::UserDiscovery;
|
use crate::user_discovery::UserDiscovery;
|
||||||
use crate::utils::Shared;
|
use crate::utils::Shared;
|
||||||
use flutter_rust_bridge::frb;
|
use flutter_rust_bridge::frb;
|
||||||
|
|
||||||
pub use crate::user_discovery::traits::AnnouncedUser;
|
pub use crate::user_discovery::AnnouncedUser;
|
||||||
pub use crate::user_discovery::traits::OtherPromotion;
|
pub use crate::user_discovery::OtherPromotion;
|
||||||
use tokio::sync::{Mutex, OnceCell, RwLock};
|
use tokio::sync::{Mutex, OnceCell, RwLock};
|
||||||
|
|
||||||
pub struct InitConfig {
|
pub struct InitConfig {
|
||||||
|
|
@ -54,8 +54,7 @@ pub struct _AnnouncedUser {
|
||||||
pub(crate) struct TwonlyFlutter {
|
pub(crate) struct TwonlyFlutter {
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub(crate) config: InitConfig,
|
pub(crate) config: InitConfig,
|
||||||
pub(crate) user_discovery:
|
pub(crate) user_discovery: Shared<UserDiscovery>,
|
||||||
Shared<UserDiscovery<NativeUserDiscoveryStore, NativeUserDiscoveryUtils>>,
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub(crate) rust_db: Arc<RwLock<Arc<Database>>>,
|
pub(crate) rust_db: Arc<RwLock<Arc<Database>>>,
|
||||||
pub(crate) app_db: Arc<RwLock<Arc<AppDatabase>>>,
|
pub(crate) app_db: Arc<RwLock<Arc<AppDatabase>>>,
|
||||||
|
|
|
||||||
70
rust/src/bridge/user_config.rs
Normal file
70
rust/src/bridge/user_config.rs
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
use crate::context::Context;
|
||||||
|
use crate::error::Result;
|
||||||
|
use crate::user_config::UserConfig;
|
||||||
|
|
||||||
|
pub struct UserConfigApi {}
|
||||||
|
|
||||||
|
impl UserConfigApi {
|
||||||
|
pub async fn load() -> Result<Option<UserConfig>> {
|
||||||
|
let ctx = Context::get_static()?;
|
||||||
|
UserConfig::load_from(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create(
|
||||||
|
user_id: i64,
|
||||||
|
username: String,
|
||||||
|
display_name: String,
|
||||||
|
current_setup_page: Option<String>,
|
||||||
|
app_version: i64,
|
||||||
|
) -> Result<UserConfig> {
|
||||||
|
serde_json::from_value(serde_json::json!({
|
||||||
|
"userId": user_id,
|
||||||
|
"username": username,
|
||||||
|
"displayName": display_name,
|
||||||
|
"subscriptionPlan": "Free",
|
||||||
|
"currentSetupPage": current_setup_page,
|
||||||
|
"appVersion": app_version,
|
||||||
|
}))
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[flutter_rust_bridge::frb(sync)]
|
||||||
|
pub fn clone(config: UserConfig) -> UserConfig {
|
||||||
|
config
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn save(config: UserConfig) -> Result<UserConfig> {
|
||||||
|
let ctx = Context::get_static()?;
|
||||||
|
let normalized = UserConfig::save_json(ctx, &serde_json::to_string(&config)?)?;
|
||||||
|
let config: UserConfig = serde_json::from_str(&normalized)?;
|
||||||
|
let mut key_manager = ctx.get_key_manager().await?;
|
||||||
|
if key_manager.user_id != Some(config.user_id) {
|
||||||
|
key_manager.user_id = Some(config.user_id);
|
||||||
|
key_manager.store_to_keychain(ctx.get_secure_storage())?;
|
||||||
|
}
|
||||||
|
drop(key_manager);
|
||||||
|
if let Ok(callbacks) = crate::bridge::callbacks::get_callbacks() {
|
||||||
|
(callbacks.api.user_config_changed)(config.clone()).await;
|
||||||
|
}
|
||||||
|
Ok(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update(base: UserConfig, config: UserConfig) -> Result<UserConfig> {
|
||||||
|
let ctx = Context::get_static()?;
|
||||||
|
let normalized = UserConfig::update_json(
|
||||||
|
ctx,
|
||||||
|
&serde_json::to_string(&base)?,
|
||||||
|
&serde_json::to_string(&config)?,
|
||||||
|
)?;
|
||||||
|
let config: UserConfig = serde_json::from_str(&normalized)?;
|
||||||
|
if let Ok(callbacks) = crate::bridge::callbacks::get_callbacks() {
|
||||||
|
(callbacks.api.user_config_changed)(config.clone()).await;
|
||||||
|
}
|
||||||
|
Ok(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn import_json(json: String) -> Result<UserConfig> {
|
||||||
|
let config: UserConfig = serde_json::from_str(&json)?;
|
||||||
|
Self::save(config).await
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue