mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-09-01 08:04:07 +00:00
move ownership of the database to rust
This commit is contained in:
parent
1fb6b3c94a
commit
74c20389c9
94 changed files with 7866 additions and 7329 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -56,4 +56,6 @@ devtools_options.yaml
|
||||||
rust/target
|
rust/target
|
||||||
rust_dependencies/target
|
rust_dependencies/target
|
||||||
fastlane/repo/status/running.json
|
fastlane/repo/status/running.json
|
||||||
.cache/
|
.cache/
|
||||||
|
# Widget Preview related
|
||||||
|
.widget_preview/
|
||||||
|
|
|
||||||
|
|
@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.5-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-all.zip
|
||||||
|
|
|
||||||
|
|
@ -18,11 +18,11 @@ pluginManagement {
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||||
id "com.android.application" version '8.11.1' apply false
|
id "com.android.application" version '9.0.1' apply false
|
||||||
// START: FlutterFire Configuration
|
// START: FlutterFire Configuration
|
||||||
id "com.google.gms.google-services" version "4.3.15" apply false
|
id "com.google.gms.google-services" version "4.3.15" apply false
|
||||||
// END: FlutterFire Configuration
|
// END: FlutterFire Configuration
|
||||||
id "org.jetbrains.kotlin.android" version "2.2.20" apply false
|
id "org.jetbrains.kotlin.android" version "2.3.20" apply false
|
||||||
}
|
}
|
||||||
|
|
||||||
include ":app"
|
include ":app"
|
||||||
|
|
|
||||||
103
lib/core/app_database.dart
Normal file
103
lib/core/app_database.dart
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
// 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 SqlExecutionResult {
|
||||||
|
final PlatformInt64 affectedRows;
|
||||||
|
final PlatformInt64 lastInsertRowId;
|
||||||
|
|
||||||
|
const SqlExecutionResult({
|
||||||
|
required this.affectedRows,
|
||||||
|
required this.lastInsertRowId,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => affectedRows.hashCode ^ lastInsertRowId.hashCode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
other is SqlExecutionResult &&
|
||||||
|
runtimeType == other.runtimeType &&
|
||||||
|
affectedRows == other.affectedRows &&
|
||||||
|
lastInsertRowId == other.lastInsertRowId;
|
||||||
|
}
|
||||||
|
|
||||||
|
class SqlRow {
|
||||||
|
final List<SqlValue> values;
|
||||||
|
|
||||||
|
const SqlRow({
|
||||||
|
required this.values,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => values.hashCode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
other is SqlRow &&
|
||||||
|
runtimeType == other.runtimeType &&
|
||||||
|
values == other.values;
|
||||||
|
}
|
||||||
|
|
||||||
|
class SqlRows {
|
||||||
|
final List<String> columns;
|
||||||
|
final List<SqlRow> rows;
|
||||||
|
|
||||||
|
const SqlRows({
|
||||||
|
required this.columns,
|
||||||
|
required this.rows,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => columns.hashCode ^ rows.hashCode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
other is SqlRows &&
|
||||||
|
runtimeType == other.runtimeType &&
|
||||||
|
columns == other.columns &&
|
||||||
|
rows == other.rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
class SqlValue {
|
||||||
|
/// 0 = null, 1 = integer, 2 = real, 3 = text, 4 = blob.
|
||||||
|
final int kind;
|
||||||
|
final PlatformInt64? integerValue;
|
||||||
|
final double? realValue;
|
||||||
|
final String? textValue;
|
||||||
|
final Uint8List? blobValue;
|
||||||
|
|
||||||
|
const SqlValue({
|
||||||
|
required this.kind,
|
||||||
|
this.integerValue,
|
||||||
|
this.realValue,
|
||||||
|
this.textValue,
|
||||||
|
this.blobValue,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
kind.hashCode ^
|
||||||
|
integerValue.hashCode ^
|
||||||
|
realValue.hashCode ^
|
||||||
|
textValue.hashCode ^
|
||||||
|
blobValue.hashCode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
other is SqlValue &&
|
||||||
|
runtimeType == other.runtimeType &&
|
||||||
|
kind == other.kind &&
|
||||||
|
integerValue == other.integerValue &&
|
||||||
|
realValue == other.realValue &&
|
||||||
|
textValue == other.textValue &&
|
||||||
|
blobValue == other.blobValue;
|
||||||
|
}
|
||||||
|
|
@ -7,35 +7,11 @@ 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';
|
||||||
|
|
||||||
// These functions are ignored because they are not marked as `pub`: `get_twonly_flutter`
|
// These functions are ignored because they are not marked as `pub`: `get_twonly_flutter`
|
||||||
// These types are ignored because they are neither used by any `pub` functions nor (for structs and enums) marked `#[frb(unignore)]`: `TwonlyFlutter`
|
// These types are ignored because they are neither used by any `pub` functions nor (for structs and enums) marked `#[frb(unignore)]`: `AnnouncedUser`, `OtherPromotion`, `TwonlyFlutter`
|
||||||
|
|
||||||
Future<void> initializeTwonlyFlutter({required InitConfig config}) =>
|
Future<void> initializeTwonlyFlutter({required InitConfig config}) =>
|
||||||
RustLib.instance.api.crateBridgeInitializeTwonlyFlutter(config: config);
|
RustLib.instance.api.crateBridgeInitializeTwonlyFlutter(config: config);
|
||||||
|
|
||||||
class AnnouncedUser {
|
|
||||||
final PlatformInt64 userId;
|
|
||||||
final Uint8List publicKey;
|
|
||||||
final PlatformInt64 publicId;
|
|
||||||
|
|
||||||
const AnnouncedUser({
|
|
||||||
required this.userId,
|
|
||||||
required this.publicKey,
|
|
||||||
required this.publicId,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => userId.hashCode ^ publicKey.hashCode ^ publicId.hashCode;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) =>
|
|
||||||
identical(this, other) ||
|
|
||||||
other is AnnouncedUser &&
|
|
||||||
runtimeType == other.runtimeType &&
|
|
||||||
userId == other.userId &&
|
|
||||||
publicKey == other.publicKey &&
|
|
||||||
publicId == other.publicId;
|
|
||||||
}
|
|
||||||
|
|
||||||
class InitConfig {
|
class InitConfig {
|
||||||
final String databaseDir;
|
final String databaseDir;
|
||||||
final String dataDir;
|
final String dataDir;
|
||||||
|
|
@ -56,42 +32,3 @@ class InitConfig {
|
||||||
databaseDir == other.databaseDir &&
|
databaseDir == other.databaseDir &&
|
||||||
dataDir == other.dataDir;
|
dataDir == other.dataDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
class OtherPromotion {
|
|
||||||
final int promotionId;
|
|
||||||
final PlatformInt64 publicId;
|
|
||||||
final PlatformInt64 fromContactId;
|
|
||||||
final int threshold;
|
|
||||||
final Uint8List announcementShare;
|
|
||||||
final PlatformInt64? publicKeyVerifiedTimestamp;
|
|
||||||
|
|
||||||
const OtherPromotion({
|
|
||||||
required this.promotionId,
|
|
||||||
required this.publicId,
|
|
||||||
required this.fromContactId,
|
|
||||||
required this.threshold,
|
|
||||||
required this.announcementShare,
|
|
||||||
this.publicKeyVerifiedTimestamp,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode =>
|
|
||||||
promotionId.hashCode ^
|
|
||||||
publicId.hashCode ^
|
|
||||||
fromContactId.hashCode ^
|
|
||||||
threshold.hashCode ^
|
|
||||||
announcementShare.hashCode ^
|
|
||||||
publicKeyVerifiedTimestamp.hashCode;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) =>
|
|
||||||
identical(this, other) ||
|
|
||||||
other is OtherPromotion &&
|
|
||||||
runtimeType == other.runtimeType &&
|
|
||||||
promotionId == other.promotionId &&
|
|
||||||
publicId == other.publicId &&
|
|
||||||
fromContactId == other.fromContactId &&
|
|
||||||
threshold == other.threshold &&
|
|
||||||
announcementShare == other.announcementShare &&
|
|
||||||
publicKeyVerifiedTimestamp == other.publicKeyVerifiedTimestamp;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -3,62 +3,17 @@
|
||||||
|
|
||||||
// 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 '../bridge.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';
|
||||||
|
|
||||||
// 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`
|
||||||
// These types are ignored because they are neither used by any `pub` functions nor (for structs and enums) marked `#[frb(unignore)]`: `FlutterCallbacks`, `Logging`, `UserDiscoveryCallbacks`
|
// These types are ignored because they are neither used by any `pub` functions nor (for structs and enums) marked `#[frb(unignore)]`: `FlutterCallbacks`, `Logging`
|
||||||
// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `clone`, `clone`, `clone`
|
// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `clone`, `clone`
|
||||||
|
|
||||||
Future<void> initFlutterCallbacks({
|
Future<void> initFlutterCallbacks({
|
||||||
required int callbackId,
|
required int callbackId,
|
||||||
required FutureOr<RustStreamSink<String>> Function() loggingGetStreamSink,
|
required FutureOr<RustStreamSink<String>> Function() loggingGetStreamSink,
|
||||||
required FutureOr<Uint8List?> Function(Uint8List) userDiscoverySignData,
|
|
||||||
required FutureOr<bool> Function(Uint8List, Uint8List, Uint8List)
|
|
||||||
userDiscoveryVerifySignature,
|
|
||||||
required FutureOr<bool> Function(PlatformInt64, Uint8List)
|
|
||||||
userDiscoveryVerifyStoredPubkey,
|
|
||||||
required FutureOr<bool> Function(List<Uint8List>) userDiscoverySetShares,
|
|
||||||
required FutureOr<Uint8List?> Function(PlatformInt64)
|
|
||||||
userDiscoveryGetShareForContact,
|
|
||||||
required FutureOr<bool> Function(PlatformInt64, PlatformInt64, Uint8List)
|
|
||||||
userDiscoveryPushOwnPromotionAndClearOldVersion,
|
|
||||||
required FutureOr<List<Uint8List>?> Function(PlatformInt64)
|
|
||||||
userDiscoveryGetOwnPromotionsAfterVersion,
|
|
||||||
required FutureOr<bool> Function(OtherPromotion)
|
|
||||||
userDiscoveryStoreOtherPromotion,
|
|
||||||
required FutureOr<List<OtherPromotion>?> Function(PlatformInt64)
|
|
||||||
userDiscoveryGetOtherPromotionsByPublicId,
|
|
||||||
required FutureOr<AnnouncedUser?> Function(PlatformInt64)
|
|
||||||
userDiscoveryGetAnnouncedUserByPublicId,
|
|
||||||
required FutureOr<Uint8List?> Function(PlatformInt64)
|
|
||||||
userDiscoveryGetContactVersion,
|
|
||||||
required FutureOr<bool> Function(PlatformInt64, Uint8List)
|
|
||||||
userDiscoverySetContactVersion,
|
|
||||||
required FutureOr<bool> Function(PlatformInt64, AnnouncedUser, PlatformInt64?)
|
|
||||||
userDiscoveryPushNewUserRelation,
|
|
||||||
required FutureOr<Uint8List?> Function(PlatformInt64)
|
|
||||||
userDiscoveryGetContactPromotion,
|
|
||||||
}) => RustLib.instance.api.crateBridgeCallbacksInitFlutterCallbacks(
|
}) => RustLib.instance.api.crateBridgeCallbacksInitFlutterCallbacks(
|
||||||
callbackId: callbackId,
|
callbackId: callbackId,
|
||||||
loggingGetStreamSink: loggingGetStreamSink,
|
loggingGetStreamSink: loggingGetStreamSink,
|
||||||
userDiscoverySignData: userDiscoverySignData,
|
|
||||||
userDiscoveryVerifySignature: userDiscoveryVerifySignature,
|
|
||||||
userDiscoveryVerifyStoredPubkey: userDiscoveryVerifyStoredPubkey,
|
|
||||||
userDiscoverySetShares: userDiscoverySetShares,
|
|
||||||
userDiscoveryGetShareForContact: userDiscoveryGetShareForContact,
|
|
||||||
userDiscoveryPushOwnPromotionAndClearOldVersion:
|
|
||||||
userDiscoveryPushOwnPromotionAndClearOldVersion,
|
|
||||||
userDiscoveryGetOwnPromotionsAfterVersion:
|
|
||||||
userDiscoveryGetOwnPromotionsAfterVersion,
|
|
||||||
userDiscoveryStoreOtherPromotion: userDiscoveryStoreOtherPromotion,
|
|
||||||
userDiscoveryGetOtherPromotionsByPublicId:
|
|
||||||
userDiscoveryGetOtherPromotionsByPublicId,
|
|
||||||
userDiscoveryGetAnnouncedUserByPublicId:
|
|
||||||
userDiscoveryGetAnnouncedUserByPublicId,
|
|
||||||
userDiscoveryGetContactVersion: userDiscoveryGetContactVersion,
|
|
||||||
userDiscoverySetContactVersion: userDiscoverySetContactVersion,
|
|
||||||
userDiscoveryPushNewUserRelation: userDiscoveryPushNewUserRelation,
|
|
||||||
userDiscoveryGetContactPromotion: userDiscoveryGetContactPromotion,
|
|
||||||
);
|
);
|
||||||
|
|
|
||||||
89
lib/core/bridge/wrapper/app_database.dart
Normal file
89
lib/core/bridge/wrapper/app_database.dart
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
// 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 '../../database/app.dart';
|
||||||
|
import '../../frb_generated.dart';
|
||||||
|
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
|
||||||
|
|
||||||
|
class LegacyMigrationReport {
|
||||||
|
final PlatformInt64 legacyVersion;
|
||||||
|
final List<LegacyTableMigrationCount> tables;
|
||||||
|
|
||||||
|
const LegacyMigrationReport({
|
||||||
|
required this.legacyVersion,
|
||||||
|
required this.tables,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => legacyVersion.hashCode ^ tables.hashCode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
other is LegacyMigrationReport &&
|
||||||
|
runtimeType == other.runtimeType &&
|
||||||
|
legacyVersion == other.legacyVersion &&
|
||||||
|
tables == other.tables;
|
||||||
|
}
|
||||||
|
|
||||||
|
class LegacyTableMigrationCount {
|
||||||
|
final String table;
|
||||||
|
final PlatformInt64 rows;
|
||||||
|
|
||||||
|
const LegacyTableMigrationCount({
|
||||||
|
required this.table,
|
||||||
|
required this.rows,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => table.hashCode ^ rows.hashCode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
other is LegacyTableMigrationCount &&
|
||||||
|
runtimeType == other.runtimeType &&
|
||||||
|
table == other.table &&
|
||||||
|
rows == other.rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
class RustAppDatabase {
|
||||||
|
const RustAppDatabase();
|
||||||
|
|
||||||
|
static Future<SqlExecutionResult> execute({
|
||||||
|
required String statement,
|
||||||
|
required List<SqlValue> arguments,
|
||||||
|
}) =>
|
||||||
|
RustLib.instance.api.crateBridgeWrapperAppDatabaseRustAppDatabaseExecute(
|
||||||
|
statement: statement,
|
||||||
|
arguments: arguments,
|
||||||
|
);
|
||||||
|
|
||||||
|
static Future<bool> legacyImportComplete() => RustLib.instance.api
|
||||||
|
.crateBridgeWrapperAppDatabaseRustAppDatabaseLegacyImportComplete();
|
||||||
|
|
||||||
|
/// Imports the non-Signal tables from a Drift v25 database. The import is
|
||||||
|
/// transactional and idempotent. Drift must be closed while this runs.
|
||||||
|
static Future<LegacyMigrationReport> migrateLegacyDatabase() => RustLib
|
||||||
|
.instance
|
||||||
|
.api
|
||||||
|
.crateBridgeWrapperAppDatabaseRustAppDatabaseMigrateLegacyDatabase();
|
||||||
|
|
||||||
|
static Future<SqlRows> select({
|
||||||
|
required String statement,
|
||||||
|
required List<SqlValue> arguments,
|
||||||
|
}) => RustLib.instance.api.crateBridgeWrapperAppDatabaseRustAppDatabaseSelect(
|
||||||
|
statement: statement,
|
||||||
|
arguments: arguments,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
other is RustAppDatabase && runtimeType == other.runtimeType;
|
||||||
|
}
|
||||||
103
lib/core/database/app.dart
Normal file
103
lib/core/database/app.dart
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
// 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 SqlExecutionResult {
|
||||||
|
final PlatformInt64 affectedRows;
|
||||||
|
final PlatformInt64 lastInsertRowId;
|
||||||
|
|
||||||
|
const SqlExecutionResult({
|
||||||
|
required this.affectedRows,
|
||||||
|
required this.lastInsertRowId,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => affectedRows.hashCode ^ lastInsertRowId.hashCode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
other is SqlExecutionResult &&
|
||||||
|
runtimeType == other.runtimeType &&
|
||||||
|
affectedRows == other.affectedRows &&
|
||||||
|
lastInsertRowId == other.lastInsertRowId;
|
||||||
|
}
|
||||||
|
|
||||||
|
class SqlRow {
|
||||||
|
final List<SqlValue> values;
|
||||||
|
|
||||||
|
const SqlRow({
|
||||||
|
required this.values,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => values.hashCode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
other is SqlRow &&
|
||||||
|
runtimeType == other.runtimeType &&
|
||||||
|
values == other.values;
|
||||||
|
}
|
||||||
|
|
||||||
|
class SqlRows {
|
||||||
|
final List<String> columns;
|
||||||
|
final List<SqlRow> rows;
|
||||||
|
|
||||||
|
const SqlRows({
|
||||||
|
required this.columns,
|
||||||
|
required this.rows,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => columns.hashCode ^ rows.hashCode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
other is SqlRows &&
|
||||||
|
runtimeType == other.runtimeType &&
|
||||||
|
columns == other.columns &&
|
||||||
|
rows == other.rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
class SqlValue {
|
||||||
|
/// 0 = null, 1 = integer, 2 = real, 3 = text, 4 = blob.
|
||||||
|
final int kind;
|
||||||
|
final PlatformInt64? integerValue;
|
||||||
|
final double? realValue;
|
||||||
|
final String? textValue;
|
||||||
|
final Uint8List? blobValue;
|
||||||
|
|
||||||
|
const SqlValue({
|
||||||
|
required this.kind,
|
||||||
|
this.integerValue,
|
||||||
|
this.realValue,
|
||||||
|
this.textValue,
|
||||||
|
this.blobValue,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
kind.hashCode ^
|
||||||
|
integerValue.hashCode ^
|
||||||
|
realValue.hashCode ^
|
||||||
|
textValue.hashCode ^
|
||||||
|
blobValue.hashCode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
other is SqlValue &&
|
||||||
|
runtimeType == other.runtimeType &&
|
||||||
|
kind == other.kind &&
|
||||||
|
integerValue == other.integerValue &&
|
||||||
|
realValue == other.realValue &&
|
||||||
|
textValue == other.textValue &&
|
||||||
|
blobValue == other.blobValue;
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -5,8 +5,8 @@
|
||||||
|
|
||||||
import 'bridge.dart';
|
import 'bridge.dart';
|
||||||
import 'bridge/callbacks.dart';
|
import 'bridge/callbacks.dart';
|
||||||
import 'bridge/callbacks/user_discovery.dart';
|
|
||||||
import 'bridge/wrapper.dart';
|
import 'bridge/wrapper.dart';
|
||||||
|
import 'bridge/wrapper/app_database.dart';
|
||||||
import 'bridge/wrapper/backup.dart';
|
import 'bridge/wrapper/backup.dart';
|
||||||
import 'bridge/wrapper/key_manager.dart';
|
import 'bridge/wrapper/key_manager.dart';
|
||||||
import 'bridge/wrapper/signal.dart';
|
import 'bridge/wrapper/signal.dart';
|
||||||
|
|
@ -14,6 +14,7 @@ import 'bridge/wrapper/user_discovery.dart';
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:ffi' as ffi;
|
import 'dart:ffi' as ffi;
|
||||||
|
import 'database/app.dart';
|
||||||
import 'frb_generated.dart';
|
import 'frb_generated.dart';
|
||||||
import 'keys/backup_password_keys.dart';
|
import 'keys/backup_password_keys.dart';
|
||||||
import 'lib.dart';
|
import 'lib.dart';
|
||||||
|
|
@ -37,72 +38,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
dynamic raw,
|
dynamic raw,
|
||||||
);
|
);
|
||||||
|
|
||||||
@protected
|
|
||||||
FutureOr<AnnouncedUser?> Function(PlatformInt64)
|
|
||||||
dco_decode_DartFn_Inputs_i_64_Output_opt_box_autoadd_announced_user_AnyhowException(
|
|
||||||
dynamic raw,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
FutureOr<List<Uint8List>?> Function(PlatformInt64)
|
|
||||||
dco_decode_DartFn_Inputs_i_64_Output_opt_list_list_prim_u_8_strict_AnyhowException(
|
|
||||||
dynamic raw,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
FutureOr<List<OtherPromotion>?> Function(PlatformInt64)
|
|
||||||
dco_decode_DartFn_Inputs_i_64_Output_opt_list_other_promotion_AnyhowException(
|
|
||||||
dynamic raw,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
FutureOr<Uint8List?> Function(PlatformInt64)
|
|
||||||
dco_decode_DartFn_Inputs_i_64_Output_opt_list_prim_u_8_strict_AnyhowException(
|
|
||||||
dynamic raw,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
FutureOr<bool> Function(PlatformInt64, AnnouncedUser, PlatformInt64?)
|
|
||||||
dco_decode_DartFn_Inputs_i_64_announced_user_opt_box_autoadd_i_64_Output_bool_AnyhowException(
|
|
||||||
dynamic raw,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
FutureOr<bool> Function(PlatformInt64, PlatformInt64, Uint8List)
|
|
||||||
dco_decode_DartFn_Inputs_i_64_i_64_list_prim_u_8_strict_Output_bool_AnyhowException(
|
|
||||||
dynamic raw,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
FutureOr<bool> Function(PlatformInt64, Uint8List)
|
|
||||||
dco_decode_DartFn_Inputs_i_64_list_prim_u_8_strict_Output_bool_AnyhowException(
|
|
||||||
dynamic raw,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
FutureOr<bool> Function(List<Uint8List>)
|
|
||||||
dco_decode_DartFn_Inputs_list_list_prim_u_8_strict_Output_bool_AnyhowException(
|
|
||||||
dynamic raw,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
FutureOr<Uint8List?> Function(Uint8List)
|
|
||||||
dco_decode_DartFn_Inputs_list_prim_u_8_strict_Output_opt_list_prim_u_8_strict_AnyhowException(
|
|
||||||
dynamic raw,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
FutureOr<bool> Function(Uint8List, Uint8List, Uint8List)
|
|
||||||
dco_decode_DartFn_Inputs_list_prim_u_8_strict_list_prim_u_8_strict_list_prim_u_8_strict_Output_bool_AnyhowException(
|
|
||||||
dynamic raw,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
FutureOr<bool> Function(OtherPromotion)
|
|
||||||
dco_decode_DartFn_Inputs_other_promotion_Output_bool_AnyhowException(
|
|
||||||
dynamic raw,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
Object dco_decode_DartOpaque(dynamic raw);
|
Object dco_decode_DartOpaque(dynamic raw);
|
||||||
|
|
||||||
|
|
@ -117,9 +52,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
String dco_decode_String(dynamic raw);
|
String dco_decode_String(dynamic raw);
|
||||||
|
|
||||||
@protected
|
|
||||||
AnnouncedUser dco_decode_announced_user(dynamic raw);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
BackupPasswordKeys dco_decode_backup_password_keys(dynamic raw);
|
BackupPasswordKeys dco_decode_backup_password_keys(dynamic raw);
|
||||||
|
|
||||||
|
|
@ -127,10 +59,10 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
bool dco_decode_bool(dynamic raw);
|
bool dco_decode_bool(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
AnnouncedUser dco_decode_box_autoadd_announced_user(dynamic raw);
|
BackupPasswordKeys dco_decode_box_autoadd_backup_password_keys(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
BackupPasswordKeys dco_decode_box_autoadd_backup_password_keys(dynamic raw);
|
double dco_decode_box_autoadd_f_64(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
FrbPreKeyBundle dco_decode_box_autoadd_frb_pre_key_bundle(dynamic raw);
|
FrbPreKeyBundle dco_decode_box_autoadd_frb_pre_key_bundle(dynamic raw);
|
||||||
|
|
@ -141,21 +73,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
InitConfig dco_decode_box_autoadd_init_config(dynamic raw);
|
InitConfig dco_decode_box_autoadd_init_config(dynamic raw);
|
||||||
|
|
||||||
@protected
|
|
||||||
OtherPromotion dco_decode_box_autoadd_other_promotion(dynamic raw);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
int dco_decode_box_autoadd_u_32(dynamic raw);
|
int dco_decode_box_autoadd_u_32(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
UserDiscoveryStoreFlutter dco_decode_box_autoadd_user_discovery_store_flutter(
|
double dco_decode_f_64(dynamic raw);
|
||||||
dynamic raw,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
UserDiscoveryUtilsFlutter dco_decode_box_autoadd_user_discovery_utils_flutter(
|
|
||||||
dynamic raw,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
FlutterUserDiscovery dco_decode_flutter_user_discovery(dynamic raw);
|
FlutterUserDiscovery dco_decode_flutter_user_discovery(dynamic raw);
|
||||||
|
|
@ -175,14 +97,27 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
PlatformInt64 dco_decode_isize(dynamic raw);
|
PlatformInt64 dco_decode_isize(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
LegacyMigrationReport dco_decode_legacy_migration_report(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
LegacyTableMigrationCount dco_decode_legacy_table_migration_count(
|
||||||
|
dynamic raw,
|
||||||
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
List<String> dco_decode_list_String(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
List<FrbPqcPreKey> dco_decode_list_frb_pqc_pre_key(dynamic raw);
|
List<FrbPqcPreKey> dco_decode_list_frb_pqc_pre_key(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
List<Uint8List> dco_decode_list_list_prim_u_8_strict(dynamic raw);
|
List<LegacyTableMigrationCount> dco_decode_list_legacy_table_migration_count(
|
||||||
|
dynamic raw,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
List<OtherPromotion> dco_decode_list_other_promotion(dynamic raw);
|
List<Uint8List> dco_decode_list_list_prim_u_8_strict(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
List<int> dco_decode_list_prim_u_8_loose(dynamic raw);
|
List<int> dco_decode_list_prim_u_8_loose(dynamic raw);
|
||||||
|
|
@ -194,11 +129,17 @@ 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<SqlRow> dco_decode_list_sql_row(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
List<SqlValue> dco_decode_list_sql_value(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
String? dco_decode_opt_String(dynamic raw);
|
String? dco_decode_opt_String(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
AnnouncedUser? dco_decode_opt_box_autoadd_announced_user(dynamic raw);
|
double? dco_decode_opt_box_autoadd_f_64(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
PlatformInt64? dco_decode_opt_box_autoadd_i_64(dynamic raw);
|
PlatformInt64? dco_decode_opt_box_autoadd_i_64(dynamic raw);
|
||||||
|
|
@ -206,18 +147,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
int? dco_decode_opt_box_autoadd_u_32(dynamic raw);
|
int? dco_decode_opt_box_autoadd_u_32(dynamic raw);
|
||||||
|
|
||||||
@protected
|
|
||||||
List<Uint8List>? dco_decode_opt_list_list_prim_u_8_strict(dynamic raw);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
List<OtherPromotion>? dco_decode_opt_list_other_promotion(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
|
|
||||||
OtherPromotion dco_decode_other_promotion(dynamic raw);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
(PlatformInt64, Uint8List) dco_decode_record_i_64_list_prim_u_8_strict(
|
(PlatformInt64, Uint8List) dco_decode_record_i_64_list_prim_u_8_strict(
|
||||||
dynamic raw,
|
dynamic raw,
|
||||||
|
|
@ -231,6 +163,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
(String, String) dco_decode_record_string_string(dynamic raw);
|
(String, String) dco_decode_record_string_string(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
RustAppDatabase dco_decode_rust_app_database(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
RustBackupArchive dco_decode_rust_backup_archive(dynamic raw);
|
RustBackupArchive dco_decode_rust_backup_archive(dynamic raw);
|
||||||
|
|
||||||
|
|
@ -246,6 +181,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
RustUtils dco_decode_rust_utils(dynamic raw);
|
RustUtils dco_decode_rust_utils(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
SqlExecutionResult dco_decode_sql_execution_result(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
SqlRow dco_decode_sql_row(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
SqlRows dco_decode_sql_rows(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
SqlValue dco_decode_sql_value(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
int dco_decode_u_32(dynamic raw);
|
int dco_decode_u_32(dynamic raw);
|
||||||
|
|
||||||
|
|
@ -258,16 +205,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
void dco_decode_unit(dynamic raw);
|
void dco_decode_unit(dynamic raw);
|
||||||
|
|
||||||
@protected
|
|
||||||
UserDiscoveryStoreFlutter dco_decode_user_discovery_store_flutter(
|
|
||||||
dynamic raw,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
UserDiscoveryUtilsFlutter dco_decode_user_discovery_utils_flutter(
|
|
||||||
dynamic raw,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
BigInt dco_decode_usize(dynamic raw);
|
BigInt dco_decode_usize(dynamic raw);
|
||||||
|
|
||||||
|
|
@ -290,9 +227,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
String sse_decode_String(SseDeserializer deserializer);
|
String sse_decode_String(SseDeserializer deserializer);
|
||||||
|
|
||||||
@protected
|
|
||||||
AnnouncedUser sse_decode_announced_user(SseDeserializer deserializer);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
BackupPasswordKeys sse_decode_backup_password_keys(
|
BackupPasswordKeys sse_decode_backup_password_keys(
|
||||||
SseDeserializer deserializer,
|
SseDeserializer deserializer,
|
||||||
|
|
@ -302,14 +236,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
bool sse_decode_bool(SseDeserializer deserializer);
|
bool sse_decode_bool(SseDeserializer deserializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
AnnouncedUser sse_decode_box_autoadd_announced_user(
|
BackupPasswordKeys sse_decode_box_autoadd_backup_password_keys(
|
||||||
SseDeserializer deserializer,
|
SseDeserializer deserializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
BackupPasswordKeys sse_decode_box_autoadd_backup_password_keys(
|
double sse_decode_box_autoadd_f_64(SseDeserializer deserializer);
|
||||||
SseDeserializer deserializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
FrbPreKeyBundle sse_decode_box_autoadd_frb_pre_key_bundle(
|
FrbPreKeyBundle sse_decode_box_autoadd_frb_pre_key_bundle(
|
||||||
|
|
@ -322,23 +254,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
InitConfig sse_decode_box_autoadd_init_config(SseDeserializer deserializer);
|
InitConfig sse_decode_box_autoadd_init_config(SseDeserializer deserializer);
|
||||||
|
|
||||||
@protected
|
|
||||||
OtherPromotion sse_decode_box_autoadd_other_promotion(
|
|
||||||
SseDeserializer deserializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
int sse_decode_box_autoadd_u_32(SseDeserializer deserializer);
|
int sse_decode_box_autoadd_u_32(SseDeserializer deserializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
UserDiscoveryStoreFlutter sse_decode_box_autoadd_user_discovery_store_flutter(
|
double sse_decode_f_64(SseDeserializer deserializer);
|
||||||
SseDeserializer deserializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
UserDiscoveryUtilsFlutter sse_decode_box_autoadd_user_discovery_utils_flutter(
|
|
||||||
SseDeserializer deserializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
FlutterUserDiscovery sse_decode_flutter_user_discovery(
|
FlutterUserDiscovery sse_decode_flutter_user_discovery(
|
||||||
|
|
@ -360,18 +280,31 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
PlatformInt64 sse_decode_isize(SseDeserializer deserializer);
|
PlatformInt64 sse_decode_isize(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
LegacyMigrationReport sse_decode_legacy_migration_report(
|
||||||
|
SseDeserializer deserializer,
|
||||||
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
LegacyTableMigrationCount sse_decode_legacy_table_migration_count(
|
||||||
|
SseDeserializer deserializer,
|
||||||
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
List<String> sse_decode_list_String(SseDeserializer deserializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
List<FrbPqcPreKey> sse_decode_list_frb_pqc_pre_key(
|
List<FrbPqcPreKey> sse_decode_list_frb_pqc_pre_key(
|
||||||
SseDeserializer deserializer,
|
SseDeserializer deserializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
List<Uint8List> sse_decode_list_list_prim_u_8_strict(
|
List<LegacyTableMigrationCount> sse_decode_list_legacy_table_migration_count(
|
||||||
SseDeserializer deserializer,
|
SseDeserializer deserializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
List<OtherPromotion> sse_decode_list_other_promotion(
|
List<Uint8List> sse_decode_list_list_prim_u_8_strict(
|
||||||
SseDeserializer deserializer,
|
SseDeserializer deserializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -387,13 +320,17 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
SseDeserializer deserializer,
|
SseDeserializer deserializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
List<SqlRow> sse_decode_list_sql_row(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
List<SqlValue> sse_decode_list_sql_value(SseDeserializer deserializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
String? sse_decode_opt_String(SseDeserializer deserializer);
|
String? sse_decode_opt_String(SseDeserializer deserializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
AnnouncedUser? sse_decode_opt_box_autoadd_announced_user(
|
double? sse_decode_opt_box_autoadd_f_64(SseDeserializer deserializer);
|
||||||
SseDeserializer deserializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
PlatformInt64? sse_decode_opt_box_autoadd_i_64(SseDeserializer deserializer);
|
PlatformInt64? sse_decode_opt_box_autoadd_i_64(SseDeserializer deserializer);
|
||||||
|
|
@ -401,22 +338,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
int? sse_decode_opt_box_autoadd_u_32(SseDeserializer deserializer);
|
int? sse_decode_opt_box_autoadd_u_32(SseDeserializer deserializer);
|
||||||
|
|
||||||
@protected
|
|
||||||
List<Uint8List>? sse_decode_opt_list_list_prim_u_8_strict(
|
|
||||||
SseDeserializer deserializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
List<OtherPromotion>? sse_decode_opt_list_other_promotion(
|
|
||||||
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
|
|
||||||
OtherPromotion sse_decode_other_promotion(SseDeserializer deserializer);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
(PlatformInt64, Uint8List) sse_decode_record_i_64_list_prim_u_8_strict(
|
(PlatformInt64, Uint8List) sse_decode_record_i_64_list_prim_u_8_strict(
|
||||||
SseDeserializer deserializer,
|
SseDeserializer deserializer,
|
||||||
|
|
@ -432,6 +356,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
SseDeserializer deserializer,
|
SseDeserializer deserializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
RustAppDatabase sse_decode_rust_app_database(SseDeserializer deserializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
RustBackupArchive sse_decode_rust_backup_archive(
|
RustBackupArchive sse_decode_rust_backup_archive(
|
||||||
SseDeserializer deserializer,
|
SseDeserializer deserializer,
|
||||||
|
|
@ -451,6 +378,20 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
RustUtils sse_decode_rust_utils(SseDeserializer deserializer);
|
RustUtils sse_decode_rust_utils(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
SqlExecutionResult sse_decode_sql_execution_result(
|
||||||
|
SseDeserializer deserializer,
|
||||||
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
SqlRow sse_decode_sql_row(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
SqlRows sse_decode_sql_rows(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
SqlValue sse_decode_sql_value(SseDeserializer deserializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
int sse_decode_u_32(SseDeserializer deserializer);
|
int sse_decode_u_32(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
|
@ -463,16 +404,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
void sse_decode_unit(SseDeserializer deserializer);
|
void sse_decode_unit(SseDeserializer deserializer);
|
||||||
|
|
||||||
@protected
|
|
||||||
UserDiscoveryStoreFlutter sse_decode_user_discovery_store_flutter(
|
|
||||||
SseDeserializer deserializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
UserDiscoveryUtilsFlutter sse_decode_user_discovery_utils_flutter(
|
|
||||||
SseDeserializer deserializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
BigInt sse_decode_usize(SseDeserializer deserializer);
|
BigInt sse_decode_usize(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
|
@ -491,82 +422,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
SseSerializer serializer,
|
SseSerializer serializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
@protected
|
|
||||||
void
|
|
||||||
sse_encode_DartFn_Inputs_i_64_Output_opt_box_autoadd_announced_user_AnyhowException(
|
|
||||||
FutureOr<AnnouncedUser?> Function(PlatformInt64) self,
|
|
||||||
SseSerializer serializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
void
|
|
||||||
sse_encode_DartFn_Inputs_i_64_Output_opt_list_list_prim_u_8_strict_AnyhowException(
|
|
||||||
FutureOr<List<Uint8List>?> Function(PlatformInt64) self,
|
|
||||||
SseSerializer serializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
void
|
|
||||||
sse_encode_DartFn_Inputs_i_64_Output_opt_list_other_promotion_AnyhowException(
|
|
||||||
FutureOr<List<OtherPromotion>?> Function(PlatformInt64) self,
|
|
||||||
SseSerializer serializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
void
|
|
||||||
sse_encode_DartFn_Inputs_i_64_Output_opt_list_prim_u_8_strict_AnyhowException(
|
|
||||||
FutureOr<Uint8List?> Function(PlatformInt64) self,
|
|
||||||
SseSerializer serializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
void
|
|
||||||
sse_encode_DartFn_Inputs_i_64_announced_user_opt_box_autoadd_i_64_Output_bool_AnyhowException(
|
|
||||||
FutureOr<bool> Function(PlatformInt64, AnnouncedUser, PlatformInt64?) self,
|
|
||||||
SseSerializer serializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
void
|
|
||||||
sse_encode_DartFn_Inputs_i_64_i_64_list_prim_u_8_strict_Output_bool_AnyhowException(
|
|
||||||
FutureOr<bool> Function(PlatformInt64, PlatformInt64, Uint8List) self,
|
|
||||||
SseSerializer serializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
void
|
|
||||||
sse_encode_DartFn_Inputs_i_64_list_prim_u_8_strict_Output_bool_AnyhowException(
|
|
||||||
FutureOr<bool> Function(PlatformInt64, Uint8List) self,
|
|
||||||
SseSerializer serializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
void
|
|
||||||
sse_encode_DartFn_Inputs_list_list_prim_u_8_strict_Output_bool_AnyhowException(
|
|
||||||
FutureOr<bool> Function(List<Uint8List>) self,
|
|
||||||
SseSerializer serializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
void
|
|
||||||
sse_encode_DartFn_Inputs_list_prim_u_8_strict_Output_opt_list_prim_u_8_strict_AnyhowException(
|
|
||||||
FutureOr<Uint8List?> Function(Uint8List) self,
|
|
||||||
SseSerializer serializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
void
|
|
||||||
sse_encode_DartFn_Inputs_list_prim_u_8_strict_list_prim_u_8_strict_list_prim_u_8_strict_Output_bool_AnyhowException(
|
|
||||||
FutureOr<bool> Function(Uint8List, Uint8List, Uint8List) self,
|
|
||||||
SseSerializer serializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
void sse_encode_DartFn_Inputs_other_promotion_Output_bool_AnyhowException(
|
|
||||||
FutureOr<bool> Function(OtherPromotion) self,
|
|
||||||
SseSerializer serializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_DartOpaque(Object self, SseSerializer serializer);
|
void sse_encode_DartOpaque(Object self, SseSerializer serializer);
|
||||||
|
|
||||||
|
|
@ -585,9 +440,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_String(String self, SseSerializer serializer);
|
void sse_encode_String(String self, SseSerializer serializer);
|
||||||
|
|
||||||
@protected
|
|
||||||
void sse_encode_announced_user(AnnouncedUser self, SseSerializer serializer);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_backup_password_keys(
|
void sse_encode_backup_password_keys(
|
||||||
BackupPasswordKeys self,
|
BackupPasswordKeys self,
|
||||||
|
|
@ -597,18 +449,15 @@ 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_announced_user(
|
|
||||||
AnnouncedUser self,
|
|
||||||
SseSerializer serializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_box_autoadd_backup_password_keys(
|
void sse_encode_box_autoadd_backup_password_keys(
|
||||||
BackupPasswordKeys self,
|
BackupPasswordKeys self,
|
||||||
SseSerializer serializer,
|
SseSerializer serializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_box_autoadd_f_64(double self, SseSerializer serializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_box_autoadd_frb_pre_key_bundle(
|
void sse_encode_box_autoadd_frb_pre_key_bundle(
|
||||||
FrbPreKeyBundle self,
|
FrbPreKeyBundle self,
|
||||||
|
|
@ -627,26 +476,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
SseSerializer serializer,
|
SseSerializer serializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
@protected
|
|
||||||
void sse_encode_box_autoadd_other_promotion(
|
|
||||||
OtherPromotion 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
|
@protected
|
||||||
void sse_encode_box_autoadd_user_discovery_store_flutter(
|
void sse_encode_f_64(double self, SseSerializer serializer);
|
||||||
UserDiscoveryStoreFlutter self,
|
|
||||||
SseSerializer serializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
void sse_encode_box_autoadd_user_discovery_utils_flutter(
|
|
||||||
UserDiscoveryUtilsFlutter self,
|
|
||||||
SseSerializer serializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_flutter_user_discovery(
|
void sse_encode_flutter_user_discovery(
|
||||||
|
|
@ -672,6 +506,21 @@ 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_legacy_migration_report(
|
||||||
|
LegacyMigrationReport self,
|
||||||
|
SseSerializer serializer,
|
||||||
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_legacy_table_migration_count(
|
||||||
|
LegacyTableMigrationCount self,
|
||||||
|
SseSerializer serializer,
|
||||||
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_list_String(List<String> self, SseSerializer serializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_list_frb_pqc_pre_key(
|
void sse_encode_list_frb_pqc_pre_key(
|
||||||
List<FrbPqcPreKey> self,
|
List<FrbPqcPreKey> self,
|
||||||
|
|
@ -679,14 +528,14 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
);
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_list_list_prim_u_8_strict(
|
void sse_encode_list_legacy_table_migration_count(
|
||||||
List<Uint8List> self,
|
List<LegacyTableMigrationCount> self,
|
||||||
SseSerializer serializer,
|
SseSerializer serializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_list_other_promotion(
|
void sse_encode_list_list_prim_u_8_strict(
|
||||||
List<OtherPromotion> self,
|
List<Uint8List> self,
|
||||||
SseSerializer serializer,
|
SseSerializer serializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -705,14 +554,17 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
SseSerializer serializer,
|
SseSerializer serializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_list_sql_row(List<SqlRow> self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_list_sql_value(List<SqlValue> self, SseSerializer serializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_opt_String(String? self, SseSerializer serializer);
|
void sse_encode_opt_String(String? self, SseSerializer serializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_opt_box_autoadd_announced_user(
|
void sse_encode_opt_box_autoadd_f_64(double? self, SseSerializer serializer);
|
||||||
AnnouncedUser? self,
|
|
||||||
SseSerializer serializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_opt_box_autoadd_i_64(
|
void sse_encode_opt_box_autoadd_i_64(
|
||||||
|
|
@ -723,30 +575,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@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_list_list_prim_u_8_strict(
|
|
||||||
List<Uint8List>? self,
|
|
||||||
SseSerializer serializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
void sse_encode_opt_list_other_promotion(
|
|
||||||
List<OtherPromotion>? 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_other_promotion(
|
|
||||||
OtherPromotion self,
|
|
||||||
SseSerializer serializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_record_i_64_list_prim_u_8_strict(
|
void sse_encode_record_i_64_list_prim_u_8_strict(
|
||||||
(PlatformInt64, Uint8List) self,
|
(PlatformInt64, Uint8List) self,
|
||||||
|
|
@ -765,6 +599,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
SseSerializer serializer,
|
SseSerializer serializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_rust_app_database(
|
||||||
|
RustAppDatabase self,
|
||||||
|
SseSerializer serializer,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_rust_backup_archive(
|
void sse_encode_rust_backup_archive(
|
||||||
RustBackupArchive self,
|
RustBackupArchive self,
|
||||||
|
|
@ -789,6 +629,21 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_rust_utils(RustUtils self, SseSerializer serializer);
|
void sse_encode_rust_utils(RustUtils self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_sql_execution_result(
|
||||||
|
SqlExecutionResult self,
|
||||||
|
SseSerializer serializer,
|
||||||
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_sql_row(SqlRow self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_sql_rows(SqlRows self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_sql_value(SqlValue self, SseSerializer serializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_u_32(int self, SseSerializer serializer);
|
void sse_encode_u_32(int self, SseSerializer serializer);
|
||||||
|
|
||||||
|
|
@ -801,18 +656,6 @@ 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_discovery_store_flutter(
|
|
||||||
UserDiscoveryStoreFlutter self,
|
|
||||||
SseSerializer serializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
void sse_encode_user_discovery_utils_flutter(
|
|
||||||
UserDiscoveryUtilsFlutter self,
|
|
||||||
SseSerializer serializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_usize(BigInt self, SseSerializer serializer);
|
void sse_encode_usize(BigInt self, SseSerializer serializer);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,14 +8,15 @@
|
||||||
|
|
||||||
import 'bridge.dart';
|
import 'bridge.dart';
|
||||||
import 'bridge/callbacks.dart';
|
import 'bridge/callbacks.dart';
|
||||||
import 'bridge/callbacks/user_discovery.dart';
|
|
||||||
import 'bridge/wrapper.dart';
|
import 'bridge/wrapper.dart';
|
||||||
|
import 'bridge/wrapper/app_database.dart';
|
||||||
import 'bridge/wrapper/backup.dart';
|
import 'bridge/wrapper/backup.dart';
|
||||||
import 'bridge/wrapper/key_manager.dart';
|
import 'bridge/wrapper/key_manager.dart';
|
||||||
import 'bridge/wrapper/signal.dart';
|
import 'bridge/wrapper/signal.dart';
|
||||||
import 'bridge/wrapper/user_discovery.dart';
|
import 'bridge/wrapper/user_discovery.dart';
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'database/app.dart';
|
||||||
import 'frb_generated.dart';
|
import 'frb_generated.dart';
|
||||||
import 'keys/backup_password_keys.dart';
|
import 'keys/backup_password_keys.dart';
|
||||||
import 'lib.dart';
|
import 'lib.dart';
|
||||||
|
|
@ -39,72 +40,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
dynamic raw,
|
dynamic raw,
|
||||||
);
|
);
|
||||||
|
|
||||||
@protected
|
|
||||||
FutureOr<AnnouncedUser?> Function(PlatformInt64)
|
|
||||||
dco_decode_DartFn_Inputs_i_64_Output_opt_box_autoadd_announced_user_AnyhowException(
|
|
||||||
dynamic raw,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
FutureOr<List<Uint8List>?> Function(PlatformInt64)
|
|
||||||
dco_decode_DartFn_Inputs_i_64_Output_opt_list_list_prim_u_8_strict_AnyhowException(
|
|
||||||
dynamic raw,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
FutureOr<List<OtherPromotion>?> Function(PlatformInt64)
|
|
||||||
dco_decode_DartFn_Inputs_i_64_Output_opt_list_other_promotion_AnyhowException(
|
|
||||||
dynamic raw,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
FutureOr<Uint8List?> Function(PlatformInt64)
|
|
||||||
dco_decode_DartFn_Inputs_i_64_Output_opt_list_prim_u_8_strict_AnyhowException(
|
|
||||||
dynamic raw,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
FutureOr<bool> Function(PlatformInt64, AnnouncedUser, PlatformInt64?)
|
|
||||||
dco_decode_DartFn_Inputs_i_64_announced_user_opt_box_autoadd_i_64_Output_bool_AnyhowException(
|
|
||||||
dynamic raw,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
FutureOr<bool> Function(PlatformInt64, PlatformInt64, Uint8List)
|
|
||||||
dco_decode_DartFn_Inputs_i_64_i_64_list_prim_u_8_strict_Output_bool_AnyhowException(
|
|
||||||
dynamic raw,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
FutureOr<bool> Function(PlatformInt64, Uint8List)
|
|
||||||
dco_decode_DartFn_Inputs_i_64_list_prim_u_8_strict_Output_bool_AnyhowException(
|
|
||||||
dynamic raw,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
FutureOr<bool> Function(List<Uint8List>)
|
|
||||||
dco_decode_DartFn_Inputs_list_list_prim_u_8_strict_Output_bool_AnyhowException(
|
|
||||||
dynamic raw,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
FutureOr<Uint8List?> Function(Uint8List)
|
|
||||||
dco_decode_DartFn_Inputs_list_prim_u_8_strict_Output_opt_list_prim_u_8_strict_AnyhowException(
|
|
||||||
dynamic raw,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
FutureOr<bool> Function(Uint8List, Uint8List, Uint8List)
|
|
||||||
dco_decode_DartFn_Inputs_list_prim_u_8_strict_list_prim_u_8_strict_list_prim_u_8_strict_Output_bool_AnyhowException(
|
|
||||||
dynamic raw,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
FutureOr<bool> Function(OtherPromotion)
|
|
||||||
dco_decode_DartFn_Inputs_other_promotion_Output_bool_AnyhowException(
|
|
||||||
dynamic raw,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
Object dco_decode_DartOpaque(dynamic raw);
|
Object dco_decode_DartOpaque(dynamic raw);
|
||||||
|
|
||||||
|
|
@ -119,9 +54,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
String dco_decode_String(dynamic raw);
|
String dco_decode_String(dynamic raw);
|
||||||
|
|
||||||
@protected
|
|
||||||
AnnouncedUser dco_decode_announced_user(dynamic raw);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
BackupPasswordKeys dco_decode_backup_password_keys(dynamic raw);
|
BackupPasswordKeys dco_decode_backup_password_keys(dynamic raw);
|
||||||
|
|
||||||
|
|
@ -129,10 +61,10 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
bool dco_decode_bool(dynamic raw);
|
bool dco_decode_bool(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
AnnouncedUser dco_decode_box_autoadd_announced_user(dynamic raw);
|
BackupPasswordKeys dco_decode_box_autoadd_backup_password_keys(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
BackupPasswordKeys dco_decode_box_autoadd_backup_password_keys(dynamic raw);
|
double dco_decode_box_autoadd_f_64(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
FrbPreKeyBundle dco_decode_box_autoadd_frb_pre_key_bundle(dynamic raw);
|
FrbPreKeyBundle dco_decode_box_autoadd_frb_pre_key_bundle(dynamic raw);
|
||||||
|
|
@ -143,21 +75,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
InitConfig dco_decode_box_autoadd_init_config(dynamic raw);
|
InitConfig dco_decode_box_autoadd_init_config(dynamic raw);
|
||||||
|
|
||||||
@protected
|
|
||||||
OtherPromotion dco_decode_box_autoadd_other_promotion(dynamic raw);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
int dco_decode_box_autoadd_u_32(dynamic raw);
|
int dco_decode_box_autoadd_u_32(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
UserDiscoveryStoreFlutter dco_decode_box_autoadd_user_discovery_store_flutter(
|
double dco_decode_f_64(dynamic raw);
|
||||||
dynamic raw,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
UserDiscoveryUtilsFlutter dco_decode_box_autoadd_user_discovery_utils_flutter(
|
|
||||||
dynamic raw,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
FlutterUserDiscovery dco_decode_flutter_user_discovery(dynamic raw);
|
FlutterUserDiscovery dco_decode_flutter_user_discovery(dynamic raw);
|
||||||
|
|
@ -177,14 +99,27 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
PlatformInt64 dco_decode_isize(dynamic raw);
|
PlatformInt64 dco_decode_isize(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
LegacyMigrationReport dco_decode_legacy_migration_report(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
LegacyTableMigrationCount dco_decode_legacy_table_migration_count(
|
||||||
|
dynamic raw,
|
||||||
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
List<String> dco_decode_list_String(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
List<FrbPqcPreKey> dco_decode_list_frb_pqc_pre_key(dynamic raw);
|
List<FrbPqcPreKey> dco_decode_list_frb_pqc_pre_key(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
List<Uint8List> dco_decode_list_list_prim_u_8_strict(dynamic raw);
|
List<LegacyTableMigrationCount> dco_decode_list_legacy_table_migration_count(
|
||||||
|
dynamic raw,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
List<OtherPromotion> dco_decode_list_other_promotion(dynamic raw);
|
List<Uint8List> dco_decode_list_list_prim_u_8_strict(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
List<int> dco_decode_list_prim_u_8_loose(dynamic raw);
|
List<int> dco_decode_list_prim_u_8_loose(dynamic raw);
|
||||||
|
|
@ -196,11 +131,17 @@ 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<SqlRow> dco_decode_list_sql_row(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
List<SqlValue> dco_decode_list_sql_value(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
String? dco_decode_opt_String(dynamic raw);
|
String? dco_decode_opt_String(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
AnnouncedUser? dco_decode_opt_box_autoadd_announced_user(dynamic raw);
|
double? dco_decode_opt_box_autoadd_f_64(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
PlatformInt64? dco_decode_opt_box_autoadd_i_64(dynamic raw);
|
PlatformInt64? dco_decode_opt_box_autoadd_i_64(dynamic raw);
|
||||||
|
|
@ -208,18 +149,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
int? dco_decode_opt_box_autoadd_u_32(dynamic raw);
|
int? dco_decode_opt_box_autoadd_u_32(dynamic raw);
|
||||||
|
|
||||||
@protected
|
|
||||||
List<Uint8List>? dco_decode_opt_list_list_prim_u_8_strict(dynamic raw);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
List<OtherPromotion>? dco_decode_opt_list_other_promotion(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
|
|
||||||
OtherPromotion dco_decode_other_promotion(dynamic raw);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
(PlatformInt64, Uint8List) dco_decode_record_i_64_list_prim_u_8_strict(
|
(PlatformInt64, Uint8List) dco_decode_record_i_64_list_prim_u_8_strict(
|
||||||
dynamic raw,
|
dynamic raw,
|
||||||
|
|
@ -233,6 +165,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
(String, String) dco_decode_record_string_string(dynamic raw);
|
(String, String) dco_decode_record_string_string(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
RustAppDatabase dco_decode_rust_app_database(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
RustBackupArchive dco_decode_rust_backup_archive(dynamic raw);
|
RustBackupArchive dco_decode_rust_backup_archive(dynamic raw);
|
||||||
|
|
||||||
|
|
@ -248,6 +183,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
RustUtils dco_decode_rust_utils(dynamic raw);
|
RustUtils dco_decode_rust_utils(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
SqlExecutionResult dco_decode_sql_execution_result(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
SqlRow dco_decode_sql_row(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
SqlRows dco_decode_sql_rows(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
SqlValue dco_decode_sql_value(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
int dco_decode_u_32(dynamic raw);
|
int dco_decode_u_32(dynamic raw);
|
||||||
|
|
||||||
|
|
@ -260,16 +207,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
void dco_decode_unit(dynamic raw);
|
void dco_decode_unit(dynamic raw);
|
||||||
|
|
||||||
@protected
|
|
||||||
UserDiscoveryStoreFlutter dco_decode_user_discovery_store_flutter(
|
|
||||||
dynamic raw,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
UserDiscoveryUtilsFlutter dco_decode_user_discovery_utils_flutter(
|
|
||||||
dynamic raw,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
BigInt dco_decode_usize(dynamic raw);
|
BigInt dco_decode_usize(dynamic raw);
|
||||||
|
|
||||||
|
|
@ -292,9 +229,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
String sse_decode_String(SseDeserializer deserializer);
|
String sse_decode_String(SseDeserializer deserializer);
|
||||||
|
|
||||||
@protected
|
|
||||||
AnnouncedUser sse_decode_announced_user(SseDeserializer deserializer);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
BackupPasswordKeys sse_decode_backup_password_keys(
|
BackupPasswordKeys sse_decode_backup_password_keys(
|
||||||
SseDeserializer deserializer,
|
SseDeserializer deserializer,
|
||||||
|
|
@ -304,14 +238,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
bool sse_decode_bool(SseDeserializer deserializer);
|
bool sse_decode_bool(SseDeserializer deserializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
AnnouncedUser sse_decode_box_autoadd_announced_user(
|
BackupPasswordKeys sse_decode_box_autoadd_backup_password_keys(
|
||||||
SseDeserializer deserializer,
|
SseDeserializer deserializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
BackupPasswordKeys sse_decode_box_autoadd_backup_password_keys(
|
double sse_decode_box_autoadd_f_64(SseDeserializer deserializer);
|
||||||
SseDeserializer deserializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
FrbPreKeyBundle sse_decode_box_autoadd_frb_pre_key_bundle(
|
FrbPreKeyBundle sse_decode_box_autoadd_frb_pre_key_bundle(
|
||||||
|
|
@ -324,23 +256,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
InitConfig sse_decode_box_autoadd_init_config(SseDeserializer deserializer);
|
InitConfig sse_decode_box_autoadd_init_config(SseDeserializer deserializer);
|
||||||
|
|
||||||
@protected
|
|
||||||
OtherPromotion sse_decode_box_autoadd_other_promotion(
|
|
||||||
SseDeserializer deserializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
int sse_decode_box_autoadd_u_32(SseDeserializer deserializer);
|
int sse_decode_box_autoadd_u_32(SseDeserializer deserializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
UserDiscoveryStoreFlutter sse_decode_box_autoadd_user_discovery_store_flutter(
|
double sse_decode_f_64(SseDeserializer deserializer);
|
||||||
SseDeserializer deserializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
UserDiscoveryUtilsFlutter sse_decode_box_autoadd_user_discovery_utils_flutter(
|
|
||||||
SseDeserializer deserializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
FlutterUserDiscovery sse_decode_flutter_user_discovery(
|
FlutterUserDiscovery sse_decode_flutter_user_discovery(
|
||||||
|
|
@ -362,18 +282,31 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
PlatformInt64 sse_decode_isize(SseDeserializer deserializer);
|
PlatformInt64 sse_decode_isize(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
LegacyMigrationReport sse_decode_legacy_migration_report(
|
||||||
|
SseDeserializer deserializer,
|
||||||
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
LegacyTableMigrationCount sse_decode_legacy_table_migration_count(
|
||||||
|
SseDeserializer deserializer,
|
||||||
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
List<String> sse_decode_list_String(SseDeserializer deserializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
List<FrbPqcPreKey> sse_decode_list_frb_pqc_pre_key(
|
List<FrbPqcPreKey> sse_decode_list_frb_pqc_pre_key(
|
||||||
SseDeserializer deserializer,
|
SseDeserializer deserializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
List<Uint8List> sse_decode_list_list_prim_u_8_strict(
|
List<LegacyTableMigrationCount> sse_decode_list_legacy_table_migration_count(
|
||||||
SseDeserializer deserializer,
|
SseDeserializer deserializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
List<OtherPromotion> sse_decode_list_other_promotion(
|
List<Uint8List> sse_decode_list_list_prim_u_8_strict(
|
||||||
SseDeserializer deserializer,
|
SseDeserializer deserializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -389,13 +322,17 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
SseDeserializer deserializer,
|
SseDeserializer deserializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
List<SqlRow> sse_decode_list_sql_row(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
List<SqlValue> sse_decode_list_sql_value(SseDeserializer deserializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
String? sse_decode_opt_String(SseDeserializer deserializer);
|
String? sse_decode_opt_String(SseDeserializer deserializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
AnnouncedUser? sse_decode_opt_box_autoadd_announced_user(
|
double? sse_decode_opt_box_autoadd_f_64(SseDeserializer deserializer);
|
||||||
SseDeserializer deserializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
PlatformInt64? sse_decode_opt_box_autoadd_i_64(SseDeserializer deserializer);
|
PlatformInt64? sse_decode_opt_box_autoadd_i_64(SseDeserializer deserializer);
|
||||||
|
|
@ -403,22 +340,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
int? sse_decode_opt_box_autoadd_u_32(SseDeserializer deserializer);
|
int? sse_decode_opt_box_autoadd_u_32(SseDeserializer deserializer);
|
||||||
|
|
||||||
@protected
|
|
||||||
List<Uint8List>? sse_decode_opt_list_list_prim_u_8_strict(
|
|
||||||
SseDeserializer deserializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
List<OtherPromotion>? sse_decode_opt_list_other_promotion(
|
|
||||||
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
|
|
||||||
OtherPromotion sse_decode_other_promotion(SseDeserializer deserializer);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
(PlatformInt64, Uint8List) sse_decode_record_i_64_list_prim_u_8_strict(
|
(PlatformInt64, Uint8List) sse_decode_record_i_64_list_prim_u_8_strict(
|
||||||
SseDeserializer deserializer,
|
SseDeserializer deserializer,
|
||||||
|
|
@ -434,6 +358,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
SseDeserializer deserializer,
|
SseDeserializer deserializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
RustAppDatabase sse_decode_rust_app_database(SseDeserializer deserializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
RustBackupArchive sse_decode_rust_backup_archive(
|
RustBackupArchive sse_decode_rust_backup_archive(
|
||||||
SseDeserializer deserializer,
|
SseDeserializer deserializer,
|
||||||
|
|
@ -453,6 +380,20 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
RustUtils sse_decode_rust_utils(SseDeserializer deserializer);
|
RustUtils sse_decode_rust_utils(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
SqlExecutionResult sse_decode_sql_execution_result(
|
||||||
|
SseDeserializer deserializer,
|
||||||
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
SqlRow sse_decode_sql_row(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
SqlRows sse_decode_sql_rows(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
SqlValue sse_decode_sql_value(SseDeserializer deserializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
int sse_decode_u_32(SseDeserializer deserializer);
|
int sse_decode_u_32(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
|
@ -465,16 +406,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
void sse_decode_unit(SseDeserializer deserializer);
|
void sse_decode_unit(SseDeserializer deserializer);
|
||||||
|
|
||||||
@protected
|
|
||||||
UserDiscoveryStoreFlutter sse_decode_user_discovery_store_flutter(
|
|
||||||
SseDeserializer deserializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
UserDiscoveryUtilsFlutter sse_decode_user_discovery_utils_flutter(
|
|
||||||
SseDeserializer deserializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
BigInt sse_decode_usize(SseDeserializer deserializer);
|
BigInt sse_decode_usize(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
|
@ -493,82 +424,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
SseSerializer serializer,
|
SseSerializer serializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
@protected
|
|
||||||
void
|
|
||||||
sse_encode_DartFn_Inputs_i_64_Output_opt_box_autoadd_announced_user_AnyhowException(
|
|
||||||
FutureOr<AnnouncedUser?> Function(PlatformInt64) self,
|
|
||||||
SseSerializer serializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
void
|
|
||||||
sse_encode_DartFn_Inputs_i_64_Output_opt_list_list_prim_u_8_strict_AnyhowException(
|
|
||||||
FutureOr<List<Uint8List>?> Function(PlatformInt64) self,
|
|
||||||
SseSerializer serializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
void
|
|
||||||
sse_encode_DartFn_Inputs_i_64_Output_opt_list_other_promotion_AnyhowException(
|
|
||||||
FutureOr<List<OtherPromotion>?> Function(PlatformInt64) self,
|
|
||||||
SseSerializer serializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
void
|
|
||||||
sse_encode_DartFn_Inputs_i_64_Output_opt_list_prim_u_8_strict_AnyhowException(
|
|
||||||
FutureOr<Uint8List?> Function(PlatformInt64) self,
|
|
||||||
SseSerializer serializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
void
|
|
||||||
sse_encode_DartFn_Inputs_i_64_announced_user_opt_box_autoadd_i_64_Output_bool_AnyhowException(
|
|
||||||
FutureOr<bool> Function(PlatformInt64, AnnouncedUser, PlatformInt64?) self,
|
|
||||||
SseSerializer serializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
void
|
|
||||||
sse_encode_DartFn_Inputs_i_64_i_64_list_prim_u_8_strict_Output_bool_AnyhowException(
|
|
||||||
FutureOr<bool> Function(PlatformInt64, PlatformInt64, Uint8List) self,
|
|
||||||
SseSerializer serializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
void
|
|
||||||
sse_encode_DartFn_Inputs_i_64_list_prim_u_8_strict_Output_bool_AnyhowException(
|
|
||||||
FutureOr<bool> Function(PlatformInt64, Uint8List) self,
|
|
||||||
SseSerializer serializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
void
|
|
||||||
sse_encode_DartFn_Inputs_list_list_prim_u_8_strict_Output_bool_AnyhowException(
|
|
||||||
FutureOr<bool> Function(List<Uint8List>) self,
|
|
||||||
SseSerializer serializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
void
|
|
||||||
sse_encode_DartFn_Inputs_list_prim_u_8_strict_Output_opt_list_prim_u_8_strict_AnyhowException(
|
|
||||||
FutureOr<Uint8List?> Function(Uint8List) self,
|
|
||||||
SseSerializer serializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
void
|
|
||||||
sse_encode_DartFn_Inputs_list_prim_u_8_strict_list_prim_u_8_strict_list_prim_u_8_strict_Output_bool_AnyhowException(
|
|
||||||
FutureOr<bool> Function(Uint8List, Uint8List, Uint8List) self,
|
|
||||||
SseSerializer serializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
void sse_encode_DartFn_Inputs_other_promotion_Output_bool_AnyhowException(
|
|
||||||
FutureOr<bool> Function(OtherPromotion) self,
|
|
||||||
SseSerializer serializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_DartOpaque(Object self, SseSerializer serializer);
|
void sse_encode_DartOpaque(Object self, SseSerializer serializer);
|
||||||
|
|
||||||
|
|
@ -587,9 +442,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_String(String self, SseSerializer serializer);
|
void sse_encode_String(String self, SseSerializer serializer);
|
||||||
|
|
||||||
@protected
|
|
||||||
void sse_encode_announced_user(AnnouncedUser self, SseSerializer serializer);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_backup_password_keys(
|
void sse_encode_backup_password_keys(
|
||||||
BackupPasswordKeys self,
|
BackupPasswordKeys self,
|
||||||
|
|
@ -599,18 +451,15 @@ 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_announced_user(
|
|
||||||
AnnouncedUser self,
|
|
||||||
SseSerializer serializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_box_autoadd_backup_password_keys(
|
void sse_encode_box_autoadd_backup_password_keys(
|
||||||
BackupPasswordKeys self,
|
BackupPasswordKeys self,
|
||||||
SseSerializer serializer,
|
SseSerializer serializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_box_autoadd_f_64(double self, SseSerializer serializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_box_autoadd_frb_pre_key_bundle(
|
void sse_encode_box_autoadd_frb_pre_key_bundle(
|
||||||
FrbPreKeyBundle self,
|
FrbPreKeyBundle self,
|
||||||
|
|
@ -629,26 +478,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
SseSerializer serializer,
|
SseSerializer serializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
@protected
|
|
||||||
void sse_encode_box_autoadd_other_promotion(
|
|
||||||
OtherPromotion 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
|
@protected
|
||||||
void sse_encode_box_autoadd_user_discovery_store_flutter(
|
void sse_encode_f_64(double self, SseSerializer serializer);
|
||||||
UserDiscoveryStoreFlutter self,
|
|
||||||
SseSerializer serializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
void sse_encode_box_autoadd_user_discovery_utils_flutter(
|
|
||||||
UserDiscoveryUtilsFlutter self,
|
|
||||||
SseSerializer serializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_flutter_user_discovery(
|
void sse_encode_flutter_user_discovery(
|
||||||
|
|
@ -674,6 +508,21 @@ 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_legacy_migration_report(
|
||||||
|
LegacyMigrationReport self,
|
||||||
|
SseSerializer serializer,
|
||||||
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_legacy_table_migration_count(
|
||||||
|
LegacyTableMigrationCount self,
|
||||||
|
SseSerializer serializer,
|
||||||
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_list_String(List<String> self, SseSerializer serializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_list_frb_pqc_pre_key(
|
void sse_encode_list_frb_pqc_pre_key(
|
||||||
List<FrbPqcPreKey> self,
|
List<FrbPqcPreKey> self,
|
||||||
|
|
@ -681,14 +530,14 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
);
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_list_list_prim_u_8_strict(
|
void sse_encode_list_legacy_table_migration_count(
|
||||||
List<Uint8List> self,
|
List<LegacyTableMigrationCount> self,
|
||||||
SseSerializer serializer,
|
SseSerializer serializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_list_other_promotion(
|
void sse_encode_list_list_prim_u_8_strict(
|
||||||
List<OtherPromotion> self,
|
List<Uint8List> self,
|
||||||
SseSerializer serializer,
|
SseSerializer serializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -707,14 +556,17 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
SseSerializer serializer,
|
SseSerializer serializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_list_sql_row(List<SqlRow> self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_list_sql_value(List<SqlValue> self, SseSerializer serializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_opt_String(String? self, SseSerializer serializer);
|
void sse_encode_opt_String(String? self, SseSerializer serializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_opt_box_autoadd_announced_user(
|
void sse_encode_opt_box_autoadd_f_64(double? self, SseSerializer serializer);
|
||||||
AnnouncedUser? self,
|
|
||||||
SseSerializer serializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_opt_box_autoadd_i_64(
|
void sse_encode_opt_box_autoadd_i_64(
|
||||||
|
|
@ -725,30 +577,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@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_list_list_prim_u_8_strict(
|
|
||||||
List<Uint8List>? self,
|
|
||||||
SseSerializer serializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
void sse_encode_opt_list_other_promotion(
|
|
||||||
List<OtherPromotion>? 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_other_promotion(
|
|
||||||
OtherPromotion self,
|
|
||||||
SseSerializer serializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_record_i_64_list_prim_u_8_strict(
|
void sse_encode_record_i_64_list_prim_u_8_strict(
|
||||||
(PlatformInt64, Uint8List) self,
|
(PlatformInt64, Uint8List) self,
|
||||||
|
|
@ -767,6 +601,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
SseSerializer serializer,
|
SseSerializer serializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_rust_app_database(
|
||||||
|
RustAppDatabase self,
|
||||||
|
SseSerializer serializer,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_rust_backup_archive(
|
void sse_encode_rust_backup_archive(
|
||||||
RustBackupArchive self,
|
RustBackupArchive self,
|
||||||
|
|
@ -791,6 +631,21 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_rust_utils(RustUtils self, SseSerializer serializer);
|
void sse_encode_rust_utils(RustUtils self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_sql_execution_result(
|
||||||
|
SqlExecutionResult self,
|
||||||
|
SseSerializer serializer,
|
||||||
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_sql_row(SqlRow self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_sql_rows(SqlRows self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_sql_value(SqlValue self, SseSerializer serializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_u_32(int self, SseSerializer serializer);
|
void sse_encode_u_32(int self, SseSerializer serializer);
|
||||||
|
|
||||||
|
|
@ -803,18 +658,6 @@ 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_discovery_store_flutter(
|
|
||||||
UserDiscoveryStoreFlutter self,
|
|
||||||
SseSerializer serializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
void sse_encode_user_discovery_utils_flutter(
|
|
||||||
UserDiscoveryUtilsFlutter self,
|
|
||||||
SseSerializer serializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_usize(BigInt self, SseSerializer serializer);
|
void sse_encode_usize(BigInt self, SseSerializer serializer);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
|
import 'package:twonly/src/database/signal.db.dart';
|
||||||
import 'package:twonly/src/database/twonly.db.dart';
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
import 'package:twonly/src/services/api/api.service.dart';
|
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';
|
||||||
|
|
@ -11,10 +12,12 @@ void setupLocator() {
|
||||||
..registerLazySingleton<UserService>(UserService.new)
|
..registerLazySingleton<UserService>(UserService.new)
|
||||||
..registerLazySingleton<ApiService>(ApiService.new)
|
..registerLazySingleton<ApiService>(ApiService.new)
|
||||||
..registerLazySingleton<TwonlyDB>(TwonlyDB.new)
|
..registerLazySingleton<TwonlyDB>(TwonlyDB.new)
|
||||||
|
..registerLazySingleton<SignalDB>(SignalDB.new)
|
||||||
..registerLazySingleton<NewsService>(NewsService.new);
|
..registerLazySingleton<NewsService>(NewsService.new);
|
||||||
}
|
}
|
||||||
|
|
||||||
UserService get userService => locator<UserService>();
|
UserService get userService => locator<UserService>();
|
||||||
ApiService get apiService => locator<ApiService>();
|
ApiService get apiService => locator<ApiService>();
|
||||||
TwonlyDB get twonlyDB => locator<TwonlyDB>();
|
TwonlyDB get twonlyDB => locator<TwonlyDB>();
|
||||||
|
SignalDB get signalDB => locator<SignalDB>();
|
||||||
NewsService get newsService => locator<NewsService>();
|
NewsService get newsService => locator<NewsService>();
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
import 'package:camera/camera.dart';
|
import 'package:camera/camera.dart';
|
||||||
|
import 'package:drift/native.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:mutex/mutex.dart';
|
import 'package:mutex/mutex.dart';
|
||||||
|
|
@ -7,11 +9,13 @@ import 'package:provider/provider.dart';
|
||||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||||
import 'package:twonly/app.dart';
|
import 'package:twonly/app.dart';
|
||||||
import 'package:twonly/core/bridge.dart' as bridge;
|
import 'package:twonly/core/bridge.dart' as bridge;
|
||||||
|
import 'package:twonly/core/bridge/wrapper/app_database.dart';
|
||||||
import 'package:twonly/core/bridge/wrapper/key_manager.dart';
|
import 'package:twonly/core/bridge/wrapper/key_manager.dart';
|
||||||
import 'package:twonly/core/frb_generated.dart';
|
import 'package:twonly/core/frb_generated.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/callbacks.dart';
|
import 'package:twonly/src/callbacks/callbacks.dart';
|
||||||
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
import 'package:twonly/src/providers/connection.provider.dart';
|
import 'package:twonly/src/providers/connection.provider.dart';
|
||||||
import 'package:twonly/src/providers/image_editor.provider.dart';
|
import 'package:twonly/src/providers/image_editor.provider.dart';
|
||||||
import 'package:twonly/src/providers/purchases.provider.dart';
|
import 'package:twonly/src/providers/purchases.provider.dart';
|
||||||
|
|
@ -55,6 +59,19 @@ Future<bool> twonlyMinimumInitialization() async {
|
||||||
dataDir: AppEnvironment.supportDir,
|
dataDir: AppEnvironment.supportDir,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
if (!await RustAppDatabase.legacyImportComplete()) {
|
||||||
|
final legacyFile = File(
|
||||||
|
'${AppEnvironment.supportDir}/twonly.sqlite',
|
||||||
|
);
|
||||||
|
if (legacyFile.existsSync()) {
|
||||||
|
final legacyDatabase = TwonlyDB(NativeDatabase(legacyFile));
|
||||||
|
// Opening the database applies every existing Drift migration up
|
||||||
|
// to v25 before Rust copies the application tables.
|
||||||
|
await legacyDatabase.customSelect('PRAGMA user_version').getSingle();
|
||||||
|
await legacyDatabase.close();
|
||||||
|
}
|
||||||
|
await RustAppDatabase.migrateLegacyDatabase();
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Log.error(e);
|
Log.error(e);
|
||||||
return true;
|
return true;
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,10 @@
|
||||||
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/src/callbacks/logging.callbacks.dart';
|
import 'package:twonly/src/callbacks/logging.callbacks.dart';
|
||||||
import 'package:twonly/src/callbacks/user_discovery.callbacks.dart';
|
|
||||||
|
|
||||||
Future<void> initFlutterCallbacksForRust() async {
|
Future<void> initFlutterCallbacksForRust() async {
|
||||||
await initFlutterCallbacks(
|
await initFlutterCallbacks(
|
||||||
callbackId: isolateCallbackId,
|
callbackId: isolateCallbackId,
|
||||||
loggingGetStreamSink: LoggingCallbacks.getStreamSink,
|
loggingGetStreamSink: LoggingCallbacks.getStreamSink,
|
||||||
userDiscoverySetShares: UserDiscoveryCallbacks.setShares,
|
|
||||||
userDiscoveryGetShareForContact:
|
|
||||||
UserDiscoveryCallbacks.userDiscoveryGetShareForContact,
|
|
||||||
userDiscoveryPushOwnPromotionAndClearOldVersion:
|
|
||||||
UserDiscoveryCallbacks.userDiscoveryPushOwnPromotionAndClearOldVersion,
|
|
||||||
userDiscoveryPushNewUserRelation:
|
|
||||||
UserDiscoveryCallbacks.pushNewUserRelation,
|
|
||||||
userDiscoveryGetOwnPromotionsAfterVersion:
|
|
||||||
UserDiscoveryCallbacks.getOwnPromotionsAfterVersion,
|
|
||||||
userDiscoveryStoreOtherPromotion:
|
|
||||||
UserDiscoveryCallbacks.storeOtherPromotion,
|
|
||||||
userDiscoveryGetOtherPromotionsByPublicId:
|
|
||||||
UserDiscoveryCallbacks.getOtherPromotionsByPublicId,
|
|
||||||
userDiscoveryGetAnnouncedUserByPublicId:
|
|
||||||
UserDiscoveryCallbacks.getAnnouncedUserByPublicId,
|
|
||||||
userDiscoveryGetContactVersion: UserDiscoveryCallbacks.getContactVersion,
|
|
||||||
userDiscoverySetContactVersion: UserDiscoveryCallbacks.setContactVersion,
|
|
||||||
userDiscoverySignData: UserDiscoveryCallbacks.signData,
|
|
||||||
userDiscoveryVerifySignature: UserDiscoveryCallbacks.verifySignature,
|
|
||||||
userDiscoveryVerifyStoredPubkey: UserDiscoveryCallbacks.verifyStoredPubKey,
|
|
||||||
userDiscoveryGetContactPromotion:
|
|
||||||
UserDiscoveryCallbacks.getContactPromotion,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,328 +0,0 @@
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:drift/drift.dart';
|
|
||||||
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'
|
|
||||||
show Curve, IdentityKey;
|
|
||||||
// ignore: implementation_imports
|
|
||||||
import 'package:libsignal_protocol_dart/src/ecc/ed25519.dart';
|
|
||||||
import 'package:twonly/core/bridge.dart';
|
|
||||||
import 'package:twonly/locator.dart';
|
|
||||||
import 'package:twonly/src/database/twonly.db.dart';
|
|
||||||
import 'package:twonly/src/services/signal/identity.signal.dart';
|
|
||||||
import 'package:twonly/src/services/signal/session.signal.dart';
|
|
||||||
import 'package:twonly/src/utils/log.dart';
|
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
|
||||||
|
|
||||||
class UserDiscoveryCallbacks {
|
|
||||||
static Future<Uint8List?> signData(
|
|
||||||
Uint8List inputData,
|
|
||||||
) async {
|
|
||||||
Log.info('UserDiscoveryCallbacks: signData started');
|
|
||||||
var privKey = (await getSignalIdentityKeyPair())?.getPrivateKey();
|
|
||||||
if (privKey == null) {
|
|
||||||
Log.error('UserDiscoveryCallbacks: signData failed, privKey is null');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
final random = getRandomUint8List(32);
|
|
||||||
final signature = sign(
|
|
||||||
privKey.serialize(),
|
|
||||||
inputData,
|
|
||||||
random,
|
|
||||||
);
|
|
||||||
privKey = null;
|
|
||||||
Log.info('UserDiscoveryCallbacks: signData finished');
|
|
||||||
return signature;
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<bool> verifySignature(
|
|
||||||
Uint8List inputData,
|
|
||||||
Uint8List pubKey,
|
|
||||||
Uint8List signature,
|
|
||||||
) async {
|
|
||||||
try {
|
|
||||||
return Curve.verifySignature(
|
|
||||||
IdentityKey.fromBytes(pubKey, 0).publicKey,
|
|
||||||
inputData,
|
|
||||||
signature,
|
|
||||||
);
|
|
||||||
} catch (_) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<bool> verifyStoredPubKey(
|
|
||||||
int contactId,
|
|
||||||
Uint8List pubKey,
|
|
||||||
) async {
|
|
||||||
try {
|
|
||||||
final storedPublicKey = await getPublicKeyFromContact(contactId);
|
|
||||||
if (storedPublicKey != null) {
|
|
||||||
return storedPublicKey.equals(pubKey);
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} catch (_) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<bool> setShares(List<Uint8List> shares) async {
|
|
||||||
try {
|
|
||||||
// First remove all old shares then insert all the new shares
|
|
||||||
await twonlyDB.delete(twonlyDB.userDiscoveryShares).go();
|
|
||||||
await twonlyDB.batch((b) {
|
|
||||||
b.insertAll(
|
|
||||||
twonlyDB.userDiscoveryShares,
|
|
||||||
shares
|
|
||||||
.map((s) => UserDiscoverySharesCompanion(share: Value(s)))
|
|
||||||
.toList(),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
Log.error(e);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<Uint8List?> userDiscoveryGetShareForContact(
|
|
||||||
int contactId,
|
|
||||||
) async {
|
|
||||||
return twonlyDB.transaction(() async {
|
|
||||||
// 1. Check if this contact already has a share assigned
|
|
||||||
final existing =
|
|
||||||
await (twonlyDB.select(twonlyDB.userDiscoveryShares)
|
|
||||||
..where((tbl) => tbl.contactId.equals(contactId))
|
|
||||||
..limit(1))
|
|
||||||
.getSingleOrNull();
|
|
||||||
|
|
||||||
if (existing != null) {
|
|
||||||
return existing.share;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. No share found. Find an available one (where contactId is null)
|
|
||||||
final available =
|
|
||||||
await (twonlyDB.select(twonlyDB.userDiscoveryShares)
|
|
||||||
..where((tbl) => tbl.contactId.isNull())
|
|
||||||
..limit(1))
|
|
||||||
.getSingleOrNull();
|
|
||||||
|
|
||||||
if (available != null) {
|
|
||||||
// 3. Assign the contactId to this available share
|
|
||||||
await (twonlyDB.update(
|
|
||||||
twonlyDB.userDiscoveryShares,
|
|
||||||
)..where((tbl) => tbl.shareId.equals(available.shareId))).write(
|
|
||||||
UserDiscoverySharesCompanion(
|
|
||||||
contactId: Value(contactId),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
return available.share;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null; // 4. No existing or available shares found
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<bool> userDiscoveryPushOwnPromotionAndClearOldVersion(
|
|
||||||
int contactId,
|
|
||||||
int version,
|
|
||||||
Uint8List promotion,
|
|
||||||
) async {
|
|
||||||
try {
|
|
||||||
// Old promotions from this users should be removed...
|
|
||||||
await (twonlyDB.update(
|
|
||||||
twonlyDB.userDiscoveryOwnPromotions,
|
|
||||||
)..where((t) => t.contactId.equals(contactId))).write(
|
|
||||||
UserDiscoveryOwnPromotionsCompanion(promotion: Value(Uint8List(0))),
|
|
||||||
);
|
|
||||||
await twonlyDB
|
|
||||||
.into(twonlyDB.userDiscoveryOwnPromotions)
|
|
||||||
.insert(
|
|
||||||
UserDiscoveryOwnPromotionsCompanion.insert(
|
|
||||||
contactId: contactId,
|
|
||||||
promotion: promotion,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
Log.error(e);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<List<Uint8List>> getOwnPromotionsAfterVersion(
|
|
||||||
int version,
|
|
||||||
) async {
|
|
||||||
final query = twonlyDB.select(twonlyDB.userDiscoveryOwnPromotions)
|
|
||||||
..where((tbl) => tbl.versionId.isBiggerThanValue(version));
|
|
||||||
|
|
||||||
final rows = await query.get();
|
|
||||||
return rows.map((r) => r.promotion).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<bool> storeOtherPromotion(
|
|
||||||
OtherPromotion promotion,
|
|
||||||
) async {
|
|
||||||
try {
|
|
||||||
await twonlyDB
|
|
||||||
.into(twonlyDB.userDiscoveryOtherPromotions)
|
|
||||||
.insertOnConflictUpdate(
|
|
||||||
UserDiscoveryOtherPromotionsCompanion(
|
|
||||||
promotionId: Value(promotion.promotionId),
|
|
||||||
publicId: Value(promotion.publicId),
|
|
||||||
fromContactId: Value(promotion.fromContactId),
|
|
||||||
threshold: Value(promotion.threshold),
|
|
||||||
announcementShare: Value(promotion.announcementShare),
|
|
||||||
publicKeyVerifiedTimestamp: Value(
|
|
||||||
promotion.publicKeyVerifiedTimestamp == null
|
|
||||||
? null
|
|
||||||
: DateTime.fromMillisecondsSinceEpoch(
|
|
||||||
promotion.publicKeyVerifiedTimestamp!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
Log.error(e);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<List<OtherPromotion>> getOtherPromotionsByPublicId(
|
|
||||||
int publicId,
|
|
||||||
) async {
|
|
||||||
final rows = await (twonlyDB.select(
|
|
||||||
twonlyDB.userDiscoveryOtherPromotions,
|
|
||||||
)..where((tbl) => tbl.publicId.equals(publicId))).get();
|
|
||||||
|
|
||||||
return rows
|
|
||||||
.map(
|
|
||||||
(row) => OtherPromotion(
|
|
||||||
promotionId: row.promotionId,
|
|
||||||
publicId: row.publicId,
|
|
||||||
fromContactId: row.fromContactId,
|
|
||||||
threshold: row.threshold,
|
|
||||||
announcementShare: row.announcementShare,
|
|
||||||
publicKeyVerifiedTimestamp:
|
|
||||||
row.publicKeyVerifiedTimestamp?.millisecondsSinceEpoch,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<AnnouncedUser?> getAnnouncedUserByPublicId(
|
|
||||||
int publicId,
|
|
||||||
) async {
|
|
||||||
final row = await (twonlyDB.select(
|
|
||||||
twonlyDB.userDiscoveryAnnouncedUsers,
|
|
||||||
)..where((tbl) => tbl.publicId.equals(publicId))).getSingleOrNull();
|
|
||||||
if (row == null) return null;
|
|
||||||
return AnnouncedUser(
|
|
||||||
userId: row.announcedUserId,
|
|
||||||
publicKey: row.announcedPublicKey,
|
|
||||||
publicId: row.publicId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<bool> pushNewUserRelation(
|
|
||||||
int fromContactId,
|
|
||||||
AnnouncedUser announcedUser,
|
|
||||||
int? publicKeyVerifiedTimestamp,
|
|
||||||
) async {
|
|
||||||
try {
|
|
||||||
await twonlyDB.transaction(() async {
|
|
||||||
// 1. Ensure the user exists in the AnnouncedUsers table
|
|
||||||
await twonlyDB
|
|
||||||
.into(twonlyDB.userDiscoveryAnnouncedUsers)
|
|
||||||
.insertOnConflictUpdate(
|
|
||||||
UserDiscoveryAnnouncedUsersCompanion(
|
|
||||||
announcedUserId: Value(announcedUser.userId),
|
|
||||||
announcedPublicKey: Value(announcedUser.publicKey),
|
|
||||||
publicId: Value(announcedUser.publicId),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
// 2. Insert or update the relation
|
|
||||||
await twonlyDB
|
|
||||||
.into(twonlyDB.userDiscoveryUserRelations)
|
|
||||||
.insertOnConflictUpdate(
|
|
||||||
UserDiscoveryUserRelationsCompanion.insert(
|
|
||||||
announcedUserId: announcedUser.userId,
|
|
||||||
fromContactId: fromContactId,
|
|
||||||
publicKeyVerifiedTimestamp: Value(
|
|
||||||
publicKeyVerifiedTimestamp != null
|
|
||||||
? DateTime.fromMillisecondsSinceEpoch(
|
|
||||||
publicKeyVerifiedTimestamp,
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
Log.error(e);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// static Future<Map<AnnouncedUser, List<(int, DateTime?)>>>
|
|
||||||
// getAllAnnouncedUsers() async {
|
|
||||||
// final query = twonlyDB.select(twonlyDB.userDiscoveryAnnouncedUsers).join([
|
|
||||||
// innerJoin(
|
|
||||||
// twonlyDB.userDiscoveryUserRelations,
|
|
||||||
// twonlyDB.userDiscoveryUserRelations.announcedUserId.equalsExp(
|
|
||||||
// twonlyDB.userDiscoveryAnnouncedUsers.announcedUserId,
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// ]);
|
|
||||||
|
|
||||||
// final results = await query.get();
|
|
||||||
// final map = <UserDiscoveryAnnouncedUser, List<(int, DateTime?)>>{};
|
|
||||||
|
|
||||||
// for (final row in results) {
|
|
||||||
// final user = row.readTable(twonlyDB.userDiscoveryAnnouncedUsers);
|
|
||||||
// final relation = row.readTable(twonlyDB.userDiscoveryUserRelations);
|
|
||||||
|
|
||||||
// map.putIfAbsent(user, () => []).add(
|
|
||||||
// (relation.fromContactId, relation.publicKeyVerifiedTimestamp),
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return map;
|
|
||||||
// }
|
|
||||||
|
|
||||||
static Future<Uint8List?> getContactVersion(int contactId) async {
|
|
||||||
final row = await (twonlyDB.select(
|
|
||||||
twonlyDB.contacts,
|
|
||||||
)..where((tbl) => tbl.userId.equals(contactId))).getSingleOrNull();
|
|
||||||
return row?.userDiscoveryVersion;
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<bool> setContactVersion(int contactId, Uint8List update) async {
|
|
||||||
try {
|
|
||||||
await (twonlyDB.update(twonlyDB.contacts)
|
|
||||||
..where((tbl) => tbl.userId.equals(contactId)))
|
|
||||||
.write(ContactsCompanion(userDiscoveryVersion: Value(update)));
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
Log.error(e);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<Uint8List?> getContactPromotion(int contactId) async {
|
|
||||||
try {
|
|
||||||
final query = twonlyDB.select(twonlyDB.userDiscoveryOwnPromotions)
|
|
||||||
..where((tbl) => tbl.contactId.equals(contactId))
|
|
||||||
..orderBy([(tbl) => OrderingTerm.desc(tbl.versionId)])
|
|
||||||
..limit(1);
|
|
||||||
final row = await query.getSingleOrNull();
|
|
||||||
return row?.promotion;
|
|
||||||
} catch (e) {
|
|
||||||
Log.error(e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -188,6 +188,15 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
|
||||||
.watch();
|
.watch();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Stream<List<(Contact, GroupMember)>> watchAllGroupMembers() {
|
||||||
|
final query = select(groupMembers).join([
|
||||||
|
innerJoin(contacts, contacts.userId.equalsExp(groupMembers.contactId)),
|
||||||
|
]);
|
||||||
|
return query
|
||||||
|
.map((row) => (row.readTable(contacts), row.readTable(groupMembers)))
|
||||||
|
.watch();
|
||||||
|
}
|
||||||
|
|
||||||
Stream<List<Group>> watchGroupsForShareImage() {
|
Stream<List<Group>> watchGroupsForShareImage() {
|
||||||
return (select(groups)..where(
|
return (select(groups)..where(
|
||||||
(g) => g.leftGroup.equals(false) & g.deletedContent.equals(false),
|
(g) => g.leftGroup.equals(false) & g.deletedContent.equals(false),
|
||||||
|
|
@ -202,6 +211,12 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
|
||||||
.watch();
|
.watch();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Stream<List<GroupMember>> watchTypingGroupMembers() {
|
||||||
|
return (select(
|
||||||
|
groupMembers,
|
||||||
|
)..where((member) => member.lastTypeIndicator.isNotNull())).watch();
|
||||||
|
}
|
||||||
|
|
||||||
Stream<Group?> watchGroup(String groupId) {
|
Stream<Group?> watchGroup(String groupId) {
|
||||||
return (select(
|
return (select(
|
||||||
groups,
|
groups,
|
||||||
|
|
|
||||||
|
|
@ -259,6 +259,51 @@ class KeyVerificationDao extends DatabaseAccessor<TwonlyDB>
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Stream<Map<String, VerificationStatus>> watchAllGroupsVerificationStatus() {
|
||||||
|
final gm = groupMembers;
|
||||||
|
final directKv = alias(keyVerifications, 'allGroupsDirectKv');
|
||||||
|
final ur = userDiscoveryUserRelations;
|
||||||
|
final verifierKv = alias(keyVerifications, 'allGroupsVerifierKv');
|
||||||
|
|
||||||
|
final query = select(gm).join([
|
||||||
|
leftOuterJoin(directKv, directKv.contactId.equalsExp(gm.contactId)),
|
||||||
|
leftOuterJoin(
|
||||||
|
ur,
|
||||||
|
ur.announcedUserId.equalsExp(gm.contactId) &
|
||||||
|
ur.publicKeyVerifiedTimestamp.isNotNull() &
|
||||||
|
ur.fromContactId.equalsExp(gm.contactId).not(),
|
||||||
|
),
|
||||||
|
leftOuterJoin(
|
||||||
|
verifierKv,
|
||||||
|
verifierKv.contactId.equalsExp(ur.fromContactId),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return query.watch().map((rows) {
|
||||||
|
final groups = <String, Map<int, ({bool direct, bool partial})>>{};
|
||||||
|
for (final row in rows) {
|
||||||
|
final member = row.readTable(gm);
|
||||||
|
final members = groups.putIfAbsent(member.groupId, () => {});
|
||||||
|
final current =
|
||||||
|
members[member.contactId] ?? (direct: false, partial: false);
|
||||||
|
members[member.contactId] = (
|
||||||
|
direct: current.direct || row.readTableOrNull(directKv) != null,
|
||||||
|
partial: current.partial || row.readTableOrNull(verifierKv) != null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
for (final entry in groups.entries)
|
||||||
|
entry.key: entry.value.values.every((member) => member.direct)
|
||||||
|
? VerificationStatus.trusted
|
||||||
|
: entry.value.values.every(
|
||||||
|
(member) => member.direct || member.partial,
|
||||||
|
)
|
||||||
|
? VerificationStatus.partialTrusted
|
||||||
|
: VerificationStatus.notTrusted,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Stream<int> watchUnverifiedGroupMembersCount(String groupId) {
|
Stream<int> watchUnverifiedGroupMembersCount(String groupId) {
|
||||||
final gm = groupMembers;
|
final gm = groupMembers;
|
||||||
final directKv = alias(keyVerifications, 'directKv');
|
final directKv = alias(keyVerifications, 'directKv');
|
||||||
|
|
|
||||||
|
|
@ -14,11 +14,15 @@ class LabelsDao extends DatabaseAccessor<TwonlyDB> with _$LabelsDaoMixin {
|
||||||
LabelsDao(super.db);
|
LabelsDao(super.db);
|
||||||
|
|
||||||
Stream<List<Label>> watchAllLabels() {
|
Stream<List<Label>> watchAllLabels() {
|
||||||
return (select(labels)..orderBy([(t) => OrderingTerm(expression: t.name)])).watch();
|
return (select(
|
||||||
|
labels,
|
||||||
|
)..orderBy([(t) => OrderingTerm(expression: t.name)])).watch();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<Label>> getAllLabels() {
|
Future<List<Label>> getAllLabels() {
|
||||||
return (select(labels)..orderBy([(t) => OrderingTerm(expression: t.name)])).get();
|
return (select(
|
||||||
|
labels,
|
||||||
|
)..orderBy([(t) => OrderingTerm(expression: t.name)])).get();
|
||||||
}
|
}
|
||||||
|
|
||||||
Stream<List<Label>> watchContactLabels(int contactId) {
|
Stream<List<Label>> watchContactLabels(int contactId) {
|
||||||
|
|
@ -27,8 +31,24 @@ class LabelsDao extends DatabaseAccessor<TwonlyDB> with _$LabelsDaoMixin {
|
||||||
])..where(contactLabels.contactId.equals(contactId));
|
])..where(contactLabels.contactId.equals(contactId));
|
||||||
|
|
||||||
return query.watch().map(
|
return query.watch().map(
|
||||||
(rows) => rows.map((row) => row.readTable(labels)).toList(),
|
(rows) => rows.map((row) => row.readTable(labels)).toList(),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream<List<(int, Label)>> watchAllContactLabels() {
|
||||||
|
final query = select(contactLabels).join([
|
||||||
|
innerJoin(labels, labels.id.equalsExp(contactLabels.labelId)),
|
||||||
|
]);
|
||||||
|
return query.watch().map(
|
||||||
|
(rows) => rows
|
||||||
|
.map(
|
||||||
|
(row) => (
|
||||||
|
row.readTable(contactLabels).contactId,
|
||||||
|
row.readTable(labels),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<Label>> getContactLabels(int contactId) {
|
Future<List<Label>> getContactLabels(int contactId) {
|
||||||
|
|
@ -37,14 +57,16 @@ class LabelsDao extends DatabaseAccessor<TwonlyDB> with _$LabelsDaoMixin {
|
||||||
])..where(contactLabels.contactId.equals(contactId));
|
])..where(contactLabels.contactId.equals(contactId));
|
||||||
|
|
||||||
return query.get().then(
|
return query.get().then(
|
||||||
(rows) => rows.map((row) => row.readTable(labels)).toList(),
|
(rows) => rows.map((row) => row.readTable(labels)).toList(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> setContactLabels(int contactId, List<int> labelIds) async {
|
Future<void> setContactLabels(int contactId, List<int> labelIds) async {
|
||||||
final sanitizedLabelIds = labelIds.take(3).toList();
|
final sanitizedLabelIds = labelIds.take(3).toList();
|
||||||
await transaction(() async {
|
await transaction(() async {
|
||||||
await (delete(contactLabels)..where((t) => t.contactId.equals(contactId))).go();
|
await (delete(
|
||||||
|
contactLabels,
|
||||||
|
)..where((t) => t.contactId.equals(contactId))).go();
|
||||||
if (sanitizedLabelIds.isNotEmpty) {
|
if (sanitizedLabelIds.isNotEmpty) {
|
||||||
await batch((b) {
|
await batch((b) {
|
||||||
b.insertAll(
|
b.insertAll(
|
||||||
|
|
@ -72,7 +94,12 @@ class LabelsDao extends DatabaseAccessor<TwonlyDB> with _$LabelsDaoMixin {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> updateLabel(int id, String name, int textColor, int backgroundColor) {
|
Future<bool> updateLabel(
|
||||||
|
int id,
|
||||||
|
String name,
|
||||||
|
int textColor,
|
||||||
|
int backgroundColor,
|
||||||
|
) {
|
||||||
final sanitizedName = name.length > 8 ? name.substring(0, 8) : name;
|
final sanitizedName = name.length > 8 ? name.substring(0, 8) : name;
|
||||||
return (update(labels)..where((t) => t.id.equals(id)))
|
return (update(labels)..where((t) => t.id.equals(id)))
|
||||||
.write(
|
.write(
|
||||||
|
|
|
||||||
|
|
@ -177,6 +177,32 @@ class MediaFilesDao extends DatabaseAccessor<TwonlyDB>
|
||||||
return query.map((row) => row.readTable(mediaFiles)).watch();
|
return query.map((row) => row.readTable(mediaFiles)).watch();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Stream<List<MediaFile>> watchChatListMediaFiles() {
|
||||||
|
return customSelect(
|
||||||
|
'''
|
||||||
|
WITH ranked_messages AS (
|
||||||
|
SELECT messages.media_id,
|
||||||
|
messages.opened_at,
|
||||||
|
ROW_NUMBER() OVER (
|
||||||
|
PARTITION BY messages.group_id
|
||||||
|
ORDER BY messages.created_at DESC
|
||||||
|
) AS message_rank
|
||||||
|
FROM messages
|
||||||
|
WHERE messages.media_id IS NOT NULL
|
||||||
|
)
|
||||||
|
SELECT DISTINCT media_files.*
|
||||||
|
FROM media_files
|
||||||
|
INNER JOIN ranked_messages
|
||||||
|
ON ranked_messages.media_id = media_files.media_id
|
||||||
|
WHERE ranked_messages.message_rank = 1
|
||||||
|
OR ranked_messages.opened_at IS NULL
|
||||||
|
''',
|
||||||
|
readsFrom: {mediaFiles, db.messages},
|
||||||
|
).watch().map(
|
||||||
|
(rows) => rows.map((row) => mediaFiles.map(row.data)).toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> updateAllRetransmissionUploadingState() async {
|
Future<void> updateAllRetransmissionUploadingState() async {
|
||||||
await (update(mediaFiles)..where(
|
await (update(mediaFiles)..where(
|
||||||
(t) =>
|
(t) =>
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,27 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
||||||
return query.map((row) => row.readTable(messages)).watch();
|
return query.map((row) => row.readTable(messages)).watch();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Stream<List<Message>> watchAllMessagesNotOpened() {
|
||||||
|
final query =
|
||||||
|
select(messages).join([
|
||||||
|
leftOuterJoin(
|
||||||
|
mediaFiles,
|
||||||
|
mediaFiles.mediaId.equalsExp(messages.mediaId),
|
||||||
|
),
|
||||||
|
])
|
||||||
|
..where(
|
||||||
|
messages.openedAt.isNull() &
|
||||||
|
messages.isDeletedFromSender.equals(false) &
|
||||||
|
(messages.mediaId.isNull() |
|
||||||
|
mediaFiles.downloadState.isNull() |
|
||||||
|
mediaFiles.downloadState
|
||||||
|
.equals(DownloadState.reuploadRequested.name)
|
||||||
|
.not()),
|
||||||
|
)
|
||||||
|
..orderBy([OrderingTerm.desc(messages.createdAt)]);
|
||||||
|
return query.map((row) => row.readTable(messages)).watch();
|
||||||
|
}
|
||||||
|
|
||||||
Stream<List<Message>> watchMediaNotOpened(String groupId) {
|
Stream<List<Message>> watchMediaNotOpened(String groupId) {
|
||||||
final query =
|
final query =
|
||||||
select(messages).join([
|
select(messages).join([
|
||||||
|
|
@ -127,6 +148,38 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
||||||
return query.map((row) => row.readTable(messages)).watchSingleOrNull();
|
return query.map((row) => row.readTable(messages)).watchSingleOrNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Stream<List<Message>> watchLatestMessagesByGroup() {
|
||||||
|
return customSelect(
|
||||||
|
'''
|
||||||
|
SELECT message_rows.*
|
||||||
|
FROM (
|
||||||
|
SELECT messages.*,
|
||||||
|
ROW_NUMBER() OVER (
|
||||||
|
PARTITION BY messages.group_id
|
||||||
|
ORDER BY messages.created_at DESC
|
||||||
|
) AS message_rank
|
||||||
|
FROM messages
|
||||||
|
INNER JOIN groups ON groups.group_id = messages.group_id
|
||||||
|
LEFT JOIN media_files ON media_files.media_id = messages.media_id
|
||||||
|
WHERE (
|
||||||
|
messages.opened_at IS NULL OR
|
||||||
|
messages.media_stored = 1 OR
|
||||||
|
messages.opened_at > CAST(strftime('%s', 'now') AS INTEGER) -
|
||||||
|
(groups.delete_messages_after_milliseconds / 1000)
|
||||||
|
)
|
||||||
|
AND (
|
||||||
|
media_files.download_state IS NULL OR
|
||||||
|
media_files.download_state != 'reuploadRequested'
|
||||||
|
)
|
||||||
|
) AS message_rows
|
||||||
|
WHERE message_rank = 1
|
||||||
|
''',
|
||||||
|
readsFrom: {messages, groups, mediaFiles},
|
||||||
|
).watch().map(
|
||||||
|
(rows) => rows.map((row) => messages.map(row.data)).toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Future<Stream<List<Message>>> watchByGroupId(String groupId) async {
|
Future<Stream<List<Message>>> watchByGroupId(String groupId) async {
|
||||||
final group = await twonlyDB.groupsDao.getGroup(groupId);
|
final group = await twonlyDB.groupsDao.getGroup(groupId);
|
||||||
final deletionTime = clock.now().subtract(
|
final deletionTime = clock.now().subtract(
|
||||||
|
|
@ -575,6 +628,19 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
||||||
.watch();
|
.watch();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Stream<List<MessageAction>> watchMessageActionsForGroup(
|
||||||
|
String groupId,
|
||||||
|
) {
|
||||||
|
final query = select(messageActions).join([
|
||||||
|
innerJoin(
|
||||||
|
messages,
|
||||||
|
messages.messageId.equalsExp(messageActions.messageId),
|
||||||
|
useColumns: false,
|
||||||
|
),
|
||||||
|
])..where(messages.groupId.equals(groupId));
|
||||||
|
return query.map((row) => row.readTable(messageActions)).watch();
|
||||||
|
}
|
||||||
|
|
||||||
Stream<List<MessageHistory>> watchMessageHistory(String messageId) {
|
Stream<List<MessageHistory>> watchMessageHistory(String messageId) {
|
||||||
return (select(messageHistories)
|
return (select(messageHistories)
|
||||||
..where((t) => t.messageId.equals(messageId))
|
..where((t) => t.messageId.equals(messageId))
|
||||||
|
|
|
||||||
|
|
@ -108,6 +108,20 @@ class ReactionsDao extends DatabaseAccessor<TwonlyDB> with _$ReactionsDaoMixin {
|
||||||
.watch();
|
.watch();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Stream<List<Reaction>> watchReactionsForGroup(String groupId) {
|
||||||
|
final query =
|
||||||
|
select(reactions).join([
|
||||||
|
innerJoin(
|
||||||
|
messages,
|
||||||
|
messages.messageId.equalsExp(reactions.messageId),
|
||||||
|
useColumns: false,
|
||||||
|
),
|
||||||
|
])
|
||||||
|
..where(messages.groupId.equals(groupId))
|
||||||
|
..orderBy([OrderingTerm.desc(reactions.createdAt)]);
|
||||||
|
return query.map((row) => row.readTable(reactions)).watch();
|
||||||
|
}
|
||||||
|
|
||||||
Stream<Reaction?> watchLastReactions(String groupId) {
|
Stream<Reaction?> watchLastReactions(String groupId) {
|
||||||
final query =
|
final query =
|
||||||
(select(reactions)).join(
|
(select(reactions)).join(
|
||||||
|
|
@ -125,6 +139,35 @@ class ReactionsDao extends DatabaseAccessor<TwonlyDB> with _$ReactionsDaoMixin {
|
||||||
return query.map((row) => row.readTable(reactions)).watchSingleOrNull();
|
return query.map((row) => row.readTable(reactions)).watchSingleOrNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Stream<List<(String, Reaction)>> watchLatestReactionsByGroup() {
|
||||||
|
return customSelect(
|
||||||
|
'''
|
||||||
|
SELECT reaction_rows.*
|
||||||
|
FROM (
|
||||||
|
SELECT reactions.*,
|
||||||
|
messages.group_id AS reaction_group_id,
|
||||||
|
ROW_NUMBER() OVER (
|
||||||
|
PARTITION BY messages.group_id
|
||||||
|
ORDER BY messages.created_at DESC, reactions.created_at DESC
|
||||||
|
) AS reaction_rank
|
||||||
|
FROM reactions
|
||||||
|
INNER JOIN messages ON messages.message_id = reactions.message_id
|
||||||
|
) AS reaction_rows
|
||||||
|
WHERE reaction_rank = 1
|
||||||
|
''',
|
||||||
|
readsFrom: {reactions, messages},
|
||||||
|
).watch().map(
|
||||||
|
(rows) => rows
|
||||||
|
.map(
|
||||||
|
(row) => (
|
||||||
|
row.read<String>('reaction_group_id'),
|
||||||
|
reactions.map(row.data),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Stream<List<(Reaction, Contact?)>> watchReactionWithContacts(
|
Stream<List<(Reaction, Contact?)>> watchReactionWithContacts(
|
||||||
String messageId,
|
String messageId,
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
96
lib/src/database/rust_query_executor.dart
Normal file
96
lib/src/database/rust_query_executor.dart
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
import 'package:drift/backends.dart';
|
||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:twonly/core/bridge/wrapper/app_database.dart';
|
||||||
|
import 'package:twonly/core/database/app.dart' as rust;
|
||||||
|
|
||||||
|
/// Drift compatibility executor backed by the Rust-owned SQLCipher database.
|
||||||
|
///
|
||||||
|
/// This keeps the existing generated models, DAO methods and query watches
|
||||||
|
/// while moving connection, encryption and storage ownership into Rust.
|
||||||
|
QueryExecutor openRustAppDatabase() => DelegatedDatabase(
|
||||||
|
_RustDatabaseDelegate(),
|
||||||
|
isSequential: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
class _RustDatabaseDelegate extends DatabaseDelegate {
|
||||||
|
@override
|
||||||
|
Future<bool> get isOpen async => true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
DbVersionDelegate get versionDelegate => const NoVersionDelegate();
|
||||||
|
|
||||||
|
@override
|
||||||
|
TransactionDelegate get transactionDelegate => const NoTransactionDelegate();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> open(QueryExecutorUser db) async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> close() async {
|
||||||
|
// The Rust application context owns the database lifecycle.
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<QueryResult> runSelect(String statement, List<Object?> args) async {
|
||||||
|
final result = await RustAppDatabase.select(
|
||||||
|
statement: statement,
|
||||||
|
arguments: args.map(_encode).toList(growable: false),
|
||||||
|
);
|
||||||
|
return QueryResult(
|
||||||
|
result.columns,
|
||||||
|
result.rows
|
||||||
|
.map((row) => row.values.map(_decode).toList(growable: false))
|
||||||
|
.toList(growable: false),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<int> runInsert(String statement, List<Object?> args) async {
|
||||||
|
final result = await _execute(statement, args);
|
||||||
|
return result.lastInsertRowId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<int> runUpdate(String statement, List<Object?> args) async {
|
||||||
|
final result = await _execute(statement, args);
|
||||||
|
return result.affectedRows;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> runCustom(String statement, List<Object?> args) async {
|
||||||
|
await _execute(statement, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<rust.SqlExecutionResult> _execute(
|
||||||
|
String statement,
|
||||||
|
List<Object?> args,
|
||||||
|
) {
|
||||||
|
return RustAppDatabase.execute(
|
||||||
|
statement: statement,
|
||||||
|
arguments: args.map(_encode).toList(growable: false),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rust.SqlValue _encode(Object? value) {
|
||||||
|
return switch (value) {
|
||||||
|
null => const rust.SqlValue(kind: 0),
|
||||||
|
final bool value => rust.SqlValue(kind: 1, integerValue: value ? 1 : 0),
|
||||||
|
final int value => rust.SqlValue(kind: 1, integerValue: value),
|
||||||
|
final double value => rust.SqlValue(kind: 2, realValue: value),
|
||||||
|
final String value => rust.SqlValue(kind: 3, textValue: value),
|
||||||
|
final Uint8List value => rust.SqlValue(kind: 4, blobValue: value),
|
||||||
|
_ => throw ArgumentError.value(value, 'value', 'Unsupported SQLite value'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Object? _decode(rust.SqlValue value) {
|
||||||
|
return switch (value.kind) {
|
||||||
|
0 => null,
|
||||||
|
1 => value.integerValue,
|
||||||
|
2 => value.realValue,
|
||||||
|
3 => value.textValue,
|
||||||
|
4 => value.blobValue,
|
||||||
|
_ => throw StateError('Unknown SQLite value kind ${value.kind}'),
|
||||||
|
};
|
||||||
|
}
|
||||||
47
lib/src/database/signal.db.dart
Normal file
47
lib/src/database/signal.db.dart
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:drift_flutter/drift_flutter.dart'
|
||||||
|
show DriftNativeOptions, driftDatabase;
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:twonly/src/database/tables/signal_identity_key_store.table.dart';
|
||||||
|
import 'package:twonly/src/database/tables/signal_pre_key_store.table.dart';
|
||||||
|
import 'package:twonly/src/database/tables/signal_sender_key_store.table.dart';
|
||||||
|
import 'package:twonly/src/database/tables/signal_session_store.table.dart';
|
||||||
|
import 'package:twonly/src/database/tables/signal_signed_pre_key_store.table.dart';
|
||||||
|
|
||||||
|
part 'signal.db.g.dart';
|
||||||
|
|
||||||
|
@DriftDatabase(
|
||||||
|
tables: [
|
||||||
|
SignalIdentityKeyStores,
|
||||||
|
SignalPreKeyStores,
|
||||||
|
SignalSenderKeyStores,
|
||||||
|
SignalSessionStores,
|
||||||
|
SignalSignedPreKeyStores,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
class SignalDB extends _$SignalDB {
|
||||||
|
SignalDB([QueryExecutor? executor]) : super(executor ?? _openConnection());
|
||||||
|
|
||||||
|
@override
|
||||||
|
// This database shares the legacy twonly.sqlite file, whose user_version is
|
||||||
|
// already 25. Keeping that version avoids a false downgrade while only the
|
||||||
|
// Signal tables remain owned by Drift.
|
||||||
|
int get schemaVersion => 25;
|
||||||
|
|
||||||
|
static QueryExecutor _openConnection() {
|
||||||
|
return driftDatabase(
|
||||||
|
name: 'twonly',
|
||||||
|
native: DriftNativeOptions(
|
||||||
|
databaseDirectory: getApplicationSupportDirectory,
|
||||||
|
shareAcrossIsolates: true,
|
||||||
|
setup: (database) {
|
||||||
|
database
|
||||||
|
..execute('PRAGMA journal_mode=DELETE;')
|
||||||
|
..execute('PRAGMA synchronous=FULL;')
|
||||||
|
..execute('PRAGMA busy_timeout=5000;')
|
||||||
|
..execute('PRAGMA foreign_keys=ON;');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
2383
lib/src/database/signal.db.g.dart
Normal file
2383
lib/src/database/signal.db.g.dart
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -2,7 +2,7 @@ import 'package:collection/collection.dart';
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
|
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/database/twonly.db.dart';
|
import 'package:twonly/src/database/signal.db.dart';
|
||||||
|
|
||||||
class SignalIdentityKeyStore extends IdentityKeyStore {
|
class SignalIdentityKeyStore extends IdentityKeyStore {
|
||||||
SignalIdentityKeyStore(this.identityKeyPair, this.localRegistrationId);
|
SignalIdentityKeyStore(this.identityKeyPair, this.localRegistrationId);
|
||||||
|
|
@ -13,7 +13,7 @@ class SignalIdentityKeyStore extends IdentityKeyStore {
|
||||||
@override
|
@override
|
||||||
Future<IdentityKey?> getIdentity(SignalProtocolAddress address) async {
|
Future<IdentityKey?> getIdentity(SignalProtocolAddress address) async {
|
||||||
final identity =
|
final identity =
|
||||||
await (twonlyDB.select(twonlyDB.signalIdentityKeyStores)..where(
|
await (signalDB.select(signalDB.signalIdentityKeyStores)..where(
|
||||||
(t) =>
|
(t) =>
|
||||||
t.deviceId.equals(address.getDeviceId()) &
|
t.deviceId.equals(address.getDeviceId()) &
|
||||||
t.name.equals(address.getName()),
|
t.name.equals(address.getName()),
|
||||||
|
|
@ -55,8 +55,8 @@ class SignalIdentityKeyStore extends IdentityKeyStore {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (await getIdentity(address) == null) {
|
if (await getIdentity(address) == null) {
|
||||||
await twonlyDB
|
await signalDB
|
||||||
.into(twonlyDB.signalIdentityKeyStores)
|
.into(signalDB.signalIdentityKeyStores)
|
||||||
.insert(
|
.insert(
|
||||||
SignalIdentityKeyStoresCompanion(
|
SignalIdentityKeyStoresCompanion(
|
||||||
deviceId: Value(address.getDeviceId()),
|
deviceId: Value(address.getDeviceId()),
|
||||||
|
|
@ -65,7 +65,7 @@ class SignalIdentityKeyStore extends IdentityKeyStore {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
await (twonlyDB.update(twonlyDB.signalIdentityKeyStores)..where(
|
await (signalDB.update(signalDB.signalIdentityKeyStores)..where(
|
||||||
(t) =>
|
(t) =>
|
||||||
t.deviceId.equals(address.getDeviceId()) &
|
t.deviceId.equals(address.getDeviceId()) &
|
||||||
t.name.equals(address.getName()),
|
t.name.equals(address.getName()),
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,22 @@
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
|
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/database/twonly.db.dart';
|
import 'package:twonly/src/database/signal.db.dart';
|
||||||
import 'package:twonly/src/utils/log.dart';
|
import 'package:twonly/src/utils/log.dart';
|
||||||
|
|
||||||
class SignalPreKeyStore extends PreKeyStore {
|
class SignalPreKeyStore extends PreKeyStore {
|
||||||
@override
|
@override
|
||||||
Future<bool> containsPreKey(int preKeyId) async {
|
Future<bool> containsPreKey(int preKeyId) async {
|
||||||
final preKeyRecord = await (twonlyDB.select(
|
final preKeyRecord = await (signalDB.select(
|
||||||
twonlyDB.signalPreKeyStores,
|
signalDB.signalPreKeyStores,
|
||||||
)..where((tbl) => tbl.preKeyId.equals(preKeyId))).get();
|
)..where((tbl) => tbl.preKeyId.equals(preKeyId))).get();
|
||||||
return preKeyRecord.isNotEmpty;
|
return preKeyRecord.isNotEmpty;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<PreKeyRecord> loadPreKey(int preKeyId) async {
|
Future<PreKeyRecord> loadPreKey(int preKeyId) async {
|
||||||
final preKeyRecord = await (twonlyDB.select(
|
final preKeyRecord = await (signalDB.select(
|
||||||
twonlyDB.signalPreKeyStores,
|
signalDB.signalPreKeyStores,
|
||||||
)..where((tbl) => tbl.preKeyId.equals(preKeyId))).get();
|
)..where((tbl) => tbl.preKeyId.equals(preKeyId))).get();
|
||||||
if (preKeyRecord.isEmpty) {
|
if (preKeyRecord.isEmpty) {
|
||||||
throw InvalidKeyIdException(
|
throw InvalidKeyIdException(
|
||||||
|
|
@ -29,8 +29,8 @@ class SignalPreKeyStore extends PreKeyStore {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> removePreKey(int preKeyId) async {
|
Future<void> removePreKey(int preKeyId) async {
|
||||||
await (twonlyDB.delete(
|
await (signalDB.delete(
|
||||||
twonlyDB.signalPreKeyStores,
|
signalDB.signalPreKeyStores,
|
||||||
)..where((tbl) => tbl.preKeyId.equals(preKeyId))).go();
|
)..where((tbl) => tbl.preKeyId.equals(preKeyId))).go();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -42,8 +42,8 @@ class SignalPreKeyStore extends PreKeyStore {
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await twonlyDB
|
await signalDB
|
||||||
.into(twonlyDB.signalPreKeyStores)
|
.into(signalDB.signalPreKeyStores)
|
||||||
.insert(preKeyCompanion, mode: InsertMode.insertOrReplace);
|
.insert(preKeyCompanion, mode: InsertMode.insertOrReplace);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Log.error('$e');
|
Log.error('$e');
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
|
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/database/twonly.db.dart';
|
import 'package:twonly/src/database/signal.db.dart';
|
||||||
|
|
||||||
class SignalSenderKeyStore extends SenderKeyStore {
|
class SignalSenderKeyStore extends SenderKeyStore {
|
||||||
@override
|
@override
|
||||||
Future<SenderKeyRecord> loadSenderKey(SenderKeyName senderKeyName) async {
|
Future<SenderKeyRecord> loadSenderKey(SenderKeyName senderKeyName) async {
|
||||||
final identity =
|
final identity =
|
||||||
await (twonlyDB.select(twonlyDB.signalSenderKeyStores)
|
await (signalDB.select(signalDB.signalSenderKeyStores)
|
||||||
..where((t) => t.senderKeyName.equals(senderKeyName.serialize())))
|
..where((t) => t.senderKeyName.equals(senderKeyName.serialize())))
|
||||||
.getSingleOrNull();
|
.getSingleOrNull();
|
||||||
if (identity == null) {
|
if (identity == null) {
|
||||||
|
|
@ -23,8 +23,8 @@ class SignalSenderKeyStore extends SenderKeyStore {
|
||||||
SenderKeyName senderKeyName,
|
SenderKeyName senderKeyName,
|
||||||
SenderKeyRecord record,
|
SenderKeyRecord record,
|
||||||
) async {
|
) async {
|
||||||
await twonlyDB
|
await signalDB
|
||||||
.into(twonlyDB.signalSenderKeyStores)
|
.into(signalDB.signalSenderKeyStores)
|
||||||
.insert(
|
.insert(
|
||||||
SignalSenderKeyStoresCompanion(
|
SignalSenderKeyStoresCompanion(
|
||||||
senderKey: Value(record.serialize()),
|
senderKey: Value(record.serialize()),
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
|
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/database/twonly.db.dart';
|
import 'package:twonly/src/database/signal.db.dart';
|
||||||
|
|
||||||
class SignalSessionStore extends SessionStore {
|
class SignalSessionStore extends SessionStore {
|
||||||
@override
|
@override
|
||||||
Future<bool> containsSession(SignalProtocolAddress address) async {
|
Future<bool> containsSession(SignalProtocolAddress address) async {
|
||||||
final sessions =
|
final sessions =
|
||||||
await (twonlyDB.select(twonlyDB.signalSessionStores)..where(
|
await (signalDB.select(signalDB.signalSessionStores)..where(
|
||||||
(tbl) =>
|
(tbl) =>
|
||||||
tbl.deviceId.equals(address.getDeviceId()) &
|
tbl.deviceId.equals(address.getDeviceId()) &
|
||||||
tbl.name.equals(address.getName()),
|
tbl.name.equals(address.getName()),
|
||||||
|
|
@ -18,14 +18,14 @@ class SignalSessionStore extends SessionStore {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> deleteAllSessions(String name) async {
|
Future<void> deleteAllSessions(String name) async {
|
||||||
await (twonlyDB.delete(
|
await (signalDB.delete(
|
||||||
twonlyDB.signalSessionStores,
|
signalDB.signalSessionStores,
|
||||||
)..where((tbl) => tbl.name.equals(name))).go();
|
)..where((tbl) => tbl.name.equals(name))).go();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> deleteSession(SignalProtocolAddress address) async {
|
Future<void> deleteSession(SignalProtocolAddress address) async {
|
||||||
await (twonlyDB.delete(twonlyDB.signalSessionStores)..where(
|
await (signalDB.delete(signalDB.signalSessionStores)..where(
|
||||||
(tbl) =>
|
(tbl) =>
|
||||||
tbl.deviceId.equals(address.getDeviceId()) &
|
tbl.deviceId.equals(address.getDeviceId()) &
|
||||||
tbl.name.equals(address.getName()),
|
tbl.name.equals(address.getName()),
|
||||||
|
|
@ -36,7 +36,7 @@ class SignalSessionStore extends SessionStore {
|
||||||
@override
|
@override
|
||||||
Future<List<int>> getSubDeviceSessions(String name) async {
|
Future<List<int>> getSubDeviceSessions(String name) async {
|
||||||
final deviceIds =
|
final deviceIds =
|
||||||
await (twonlyDB.select(twonlyDB.signalSessionStores)..where(
|
await (signalDB.select(signalDB.signalSessionStores)..where(
|
||||||
(tbl) => tbl.deviceId.equals(1).not() & tbl.name.equals(name),
|
(tbl) => tbl.deviceId.equals(1).not() & tbl.name.equals(name),
|
||||||
))
|
))
|
||||||
.get();
|
.get();
|
||||||
|
|
@ -46,7 +46,7 @@ class SignalSessionStore extends SessionStore {
|
||||||
@override
|
@override
|
||||||
Future<SessionRecord> loadSession(SignalProtocolAddress address) async {
|
Future<SessionRecord> loadSession(SignalProtocolAddress address) async {
|
||||||
final dbSession =
|
final dbSession =
|
||||||
await (twonlyDB.select(twonlyDB.signalSessionStores)..where(
|
await (signalDB.select(signalDB.signalSessionStores)..where(
|
||||||
(tbl) =>
|
(tbl) =>
|
||||||
tbl.deviceId.equals(address.getDeviceId()) &
|
tbl.deviceId.equals(address.getDeviceId()) &
|
||||||
tbl.name.equals(address.getName()),
|
tbl.name.equals(address.getName()),
|
||||||
|
|
@ -72,11 +72,11 @@ class SignalSessionStore extends SessionStore {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!await containsSession(address)) {
|
if (!await containsSession(address)) {
|
||||||
await twonlyDB
|
await signalDB
|
||||||
.into(twonlyDB.signalSessionStores)
|
.into(signalDB.signalSessionStores)
|
||||||
.insert(sessionCompanion);
|
.insert(sessionCompanion);
|
||||||
} else {
|
} else {
|
||||||
await (twonlyDB.update(twonlyDB.signalSessionStores)..where(
|
await (signalDB.update(signalDB.signalSessionStores)..where(
|
||||||
(tbl) =>
|
(tbl) =>
|
||||||
tbl.deviceId.equals(address.getDeviceId()) &
|
tbl.deviceId.equals(address.getDeviceId()) &
|
||||||
tbl.name.equals(address.getName()),
|
tbl.name.equals(address.getName()),
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import 'dart:convert';
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
|
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/database/twonly.db.dart';
|
import 'package:twonly/src/database/signal.db.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';
|
||||||
|
|
||||||
|
|
@ -26,8 +26,8 @@ Future<HashMap<int, Uint8List>> getSignalSignedPreKeyStoreOld() async {
|
||||||
class SignalSignedPreKeyStore extends SignedPreKeyStore {
|
class SignalSignedPreKeyStore extends SignedPreKeyStore {
|
||||||
@override
|
@override
|
||||||
Future<SignedPreKeyRecord> loadSignedPreKey(int signedPreKeyId) async {
|
Future<SignedPreKeyRecord> loadSignedPreKey(int signedPreKeyId) async {
|
||||||
final record = await (twonlyDB.select(
|
final record = await (signalDB.select(
|
||||||
twonlyDB.signalSignedPreKeyStores,
|
signalDB.signalSignedPreKeyStores,
|
||||||
)..where((tbl) => tbl.signedPreKeyId.equals(signedPreKeyId))).get();
|
)..where((tbl) => tbl.signedPreKeyId.equals(signedPreKeyId))).get();
|
||||||
if (record.isEmpty) {
|
if (record.isEmpty) {
|
||||||
throw InvalidKeyIdException(
|
throw InvalidKeyIdException(
|
||||||
|
|
@ -39,8 +39,8 @@ class SignalSignedPreKeyStore extends SignedPreKeyStore {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<SignedPreKeyRecord>> loadSignedPreKeys() async {
|
Future<List<SignedPreKeyRecord>> loadSignedPreKeys() async {
|
||||||
final records = await twonlyDB
|
final records = await signalDB
|
||||||
.select(twonlyDB.signalSignedPreKeyStores)
|
.select(signalDB.signalSignedPreKeyStores)
|
||||||
.get();
|
.get();
|
||||||
return records
|
return records
|
||||||
.map((r) => SignedPreKeyRecord.fromSerialized(r.signedPreKey))
|
.map((r) => SignedPreKeyRecord.fromSerialized(r.signedPreKey))
|
||||||
|
|
@ -58,8 +58,8 @@ class SignalSignedPreKeyStore extends SignedPreKeyStore {
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await twonlyDB
|
await signalDB
|
||||||
.into(twonlyDB.signalSignedPreKeyStores)
|
.into(signalDB.signalSignedPreKeyStores)
|
||||||
.insert(companion, mode: InsertMode.insertOrReplace);
|
.insert(companion, mode: InsertMode.insertOrReplace);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Log.error('$e');
|
Log.error('$e');
|
||||||
|
|
@ -69,16 +69,16 @@ class SignalSignedPreKeyStore extends SignedPreKeyStore {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<bool> containsSignedPreKey(int signedPreKeyId) async {
|
Future<bool> containsSignedPreKey(int signedPreKeyId) async {
|
||||||
final record = await (twonlyDB.select(
|
final record = await (signalDB.select(
|
||||||
twonlyDB.signalSignedPreKeyStores,
|
signalDB.signalSignedPreKeyStores,
|
||||||
)..where((tbl) => tbl.signedPreKeyId.equals(signedPreKeyId))).get();
|
)..where((tbl) => tbl.signedPreKeyId.equals(signedPreKeyId))).get();
|
||||||
return record.isNotEmpty;
|
return record.isNotEmpty;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> removeSignedPreKey(int signedPreKeyId) async {
|
Future<void> removeSignedPreKey(int signedPreKeyId) async {
|
||||||
await (twonlyDB.delete(
|
await (signalDB.delete(
|
||||||
twonlyDB.signalSignedPreKeyStores,
|
signalDB.signalSignedPreKeyStores,
|
||||||
)..where((tbl) => tbl.signedPreKeyId.equals(signedPreKeyId))).go();
|
)..where((tbl) => tbl.signedPreKeyId.equals(signedPreKeyId))).go();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,4 @@
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:drift_flutter/drift_flutter.dart'
|
|
||||||
show DriftNativeOptions, driftDatabase;
|
|
||||||
import 'package:path_provider/path_provider.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/daos/groups.dao.dart';
|
import 'package:twonly/src/database/daos/groups.dao.dart';
|
||||||
import 'package:twonly/src/database/daos/key_verification.dao.dart';
|
import 'package:twonly/src/database/daos/key_verification.dao.dart';
|
||||||
|
|
@ -13,7 +9,7 @@ import 'package:twonly/src/database/daos/reactions.dao.dart';
|
||||||
import 'package:twonly/src/database/daos/receipts.dao.dart';
|
import 'package:twonly/src/database/daos/receipts.dao.dart';
|
||||||
import 'package:twonly/src/database/daos/shortcuts.dao.dart';
|
import 'package:twonly/src/database/daos/shortcuts.dao.dart';
|
||||||
import 'package:twonly/src/database/daos/user_discovery.dao.dart';
|
import 'package:twonly/src/database/daos/user_discovery.dao.dart';
|
||||||
import 'package:twonly/src/database/drift_logging_interceptor.dart';
|
import 'package:twonly/src/database/rust_query_executor.dart';
|
||||||
import 'package:twonly/src/database/tables/contacts.table.dart';
|
import 'package:twonly/src/database/tables/contacts.table.dart';
|
||||||
import 'package:twonly/src/database/tables/groups.table.dart';
|
import 'package:twonly/src/database/tables/groups.table.dart';
|
||||||
import 'package:twonly/src/database/tables/labels.table.dart';
|
import 'package:twonly/src/database/tables/labels.table.dart';
|
||||||
|
|
@ -22,11 +18,6 @@ import 'package:twonly/src/database/tables/messages.table.dart';
|
||||||
import 'package:twonly/src/database/tables/reactions.table.dart';
|
import 'package:twonly/src/database/tables/reactions.table.dart';
|
||||||
import 'package:twonly/src/database/tables/receipts.table.dart';
|
import 'package:twonly/src/database/tables/receipts.table.dart';
|
||||||
import 'package:twonly/src/database/tables/shortcuts.table.dart';
|
import 'package:twonly/src/database/tables/shortcuts.table.dart';
|
||||||
import 'package:twonly/src/database/tables/signal_identity_key_store.table.dart';
|
|
||||||
import 'package:twonly/src/database/tables/signal_pre_key_store.table.dart';
|
|
||||||
import 'package:twonly/src/database/tables/signal_sender_key_store.table.dart';
|
|
||||||
import 'package:twonly/src/database/tables/signal_session_store.table.dart';
|
|
||||||
import 'package:twonly/src/database/tables/signal_signed_pre_key_store.table.dart';
|
|
||||||
import 'package:twonly/src/database/tables/user_discovery.table.dart';
|
import 'package:twonly/src/database/tables/user_discovery.table.dart';
|
||||||
import 'package:twonly/src/database/twonly.db.steps.dart';
|
import 'package:twonly/src/database/twonly.db.steps.dart';
|
||||||
import 'package:twonly/src/utils/log.dart';
|
import 'package:twonly/src/utils/log.dart';
|
||||||
|
|
@ -45,11 +36,6 @@ part 'twonly.db.g.dart';
|
||||||
GroupMembers,
|
GroupMembers,
|
||||||
Receipts,
|
Receipts,
|
||||||
ReceivedReceipts,
|
ReceivedReceipts,
|
||||||
SignalIdentityKeyStores,
|
|
||||||
SignalPreKeyStores,
|
|
||||||
SignalSenderKeyStores,
|
|
||||||
SignalSessionStores,
|
|
||||||
SignalSignedPreKeyStores,
|
|
||||||
MessageActions,
|
MessageActions,
|
||||||
GroupHistories,
|
GroupHistories,
|
||||||
KeyVerifications,
|
KeyVerifications,
|
||||||
|
|
@ -80,7 +66,7 @@ part 'twonly.db.g.dart';
|
||||||
class TwonlyDB extends _$TwonlyDB {
|
class TwonlyDB extends _$TwonlyDB {
|
||||||
TwonlyDB([QueryExecutor? e])
|
TwonlyDB([QueryExecutor? e])
|
||||||
: super(
|
: super(
|
||||||
e ?? _openConnection(),
|
e ?? openRustAppDatabase(),
|
||||||
);
|
);
|
||||||
|
|
||||||
// ignore: matching_super_parameters
|
// ignore: matching_super_parameters
|
||||||
|
|
@ -89,29 +75,6 @@ class TwonlyDB extends _$TwonlyDB {
|
||||||
@override
|
@override
|
||||||
int get schemaVersion => 25;
|
int get schemaVersion => 25;
|
||||||
|
|
||||||
static QueryExecutor _openConnection() {
|
|
||||||
final connection = driftDatabase(
|
|
||||||
name: 'twonly',
|
|
||||||
native: DriftNativeOptions(
|
|
||||||
databaseDirectory: getApplicationSupportDirectory,
|
|
||||||
shareAcrossIsolates: true,
|
|
||||||
setup: (rawDb) {
|
|
||||||
rawDb
|
|
||||||
..execute('PRAGMA journal_mode=DELETE;')
|
|
||||||
..execute('PRAGMA synchronous=FULL;')
|
|
||||||
..execute('PRAGMA busy_timeout=5000;');
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
try {
|
|
||||||
if (userService.isUserCreated &&
|
|
||||||
userService.currentUser.enableDatabaseLogging) {
|
|
||||||
return connection.interceptWith(DriftLoggingInterceptor());
|
|
||||||
}
|
|
||||||
} catch (_) {}
|
|
||||||
return connection;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
MigrationStrategy get migration {
|
MigrationStrategy get migration {
|
||||||
return MigrationStrategy(
|
return MigrationStrategy(
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -111,15 +111,6 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) =>
|
||||||
json['passwordLessRecovery'] as Map<String, dynamic>,
|
json['passwordLessRecovery'] as Map<String, dynamic>,
|
||||||
)
|
)
|
||||||
..fcmToken = json['fcmToken'] as String?
|
..fcmToken = json['fcmToken'] as String?
|
||||||
..askedForUserStudyPermission =
|
|
||||||
json['askedForUserStudyPermission'] as bool? ?? false
|
|
||||||
..userStudyParticipantsToken =
|
|
||||||
json['userStudyParticipantsToken'] as String?
|
|
||||||
..userStudyCountNewFriendsViaSuggestion =
|
|
||||||
(json['userStudyCountNewFriendsViaSuggestion'] as num?)?.toInt() ?? 0
|
|
||||||
..lastUserStudyDataUpload = json['lastUserStudyDataUpload'] == null
|
|
||||||
? null
|
|
||||||
: DateTime.parse(json['lastUserStudyDataUpload'] as String)
|
|
||||||
..skipSetupPages = json['skipSetupPages'] as bool? ?? false
|
..skipSetupPages = json['skipSetupPages'] as bool? ?? false
|
||||||
..hasZoomed = json['hasZoomed'] as bool? ?? false;
|
..hasZoomed = json['hasZoomed'] as bool? ?? false;
|
||||||
|
|
||||||
|
|
@ -184,12 +175,6 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
|
||||||
'isBackupEnabled': instance.isBackupEnabled,
|
'isBackupEnabled': instance.isBackupEnabled,
|
||||||
'passwordLessRecovery': instance.passwordLessRecovery,
|
'passwordLessRecovery': instance.passwordLessRecovery,
|
||||||
'fcmToken': instance.fcmToken,
|
'fcmToken': instance.fcmToken,
|
||||||
'askedForUserStudyPermission': instance.askedForUserStudyPermission,
|
|
||||||
'userStudyParticipantsToken': instance.userStudyParticipantsToken,
|
|
||||||
'userStudyCountNewFriendsViaSuggestion':
|
|
||||||
instance.userStudyCountNewFriendsViaSuggestion,
|
|
||||||
'lastUserStudyDataUpload': instance.lastUserStudyDataUpload
|
|
||||||
?.toIso8601String(),
|
|
||||||
'currentSetupPage': instance.currentSetupPage,
|
'currentSetupPage': instance.currentSetupPage,
|
||||||
'skipSetupPages': instance.skipSetupPages,
|
'skipSetupPages': instance.skipSetupPages,
|
||||||
'hasZoomed': instance.hasZoomed,
|
'hasZoomed': instance.hasZoomed,
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import 'package:twonly/core/bridge/wrapper/key_manager.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/secure_storage.keys.dart';
|
import 'package:twonly/src/constants/secure_storage.keys.dart';
|
||||||
|
import 'package:twonly/src/database/signal.db.dart';
|
||||||
import 'package:twonly/src/database/signal/signal_signed_pre_key_store.dart'
|
import 'package:twonly/src/database/signal/signal_signed_pre_key_store.dart'
|
||||||
show getSignalSignedPreKeyStoreOld;
|
show getSignalSignedPreKeyStoreOld;
|
||||||
import 'package:twonly/src/database/tables/contacts.table.dart';
|
import 'package:twonly/src/database/tables/contacts.table.dart';
|
||||||
|
|
@ -129,8 +130,8 @@ Future<void> runMigrations() async {
|
||||||
signedPreKeyId: Value(entry.key),
|
signedPreKeyId: Value(entry.key),
|
||||||
signedPreKey: Value(entry.value),
|
signedPreKey: Value(entry.value),
|
||||||
);
|
);
|
||||||
await twonlyDB
|
await signalDB
|
||||||
.into(twonlyDB.signalSignedPreKeyStores)
|
.into(signalDB.signalSignedPreKeyStores)
|
||||||
.insert(
|
.insert(
|
||||||
companion,
|
companion,
|
||||||
mode: InsertMode.insertOrReplace,
|
mode: InsertMode.insertOrReplace,
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ class AvatarIcon extends StatefulWidget {
|
||||||
const AvatarIcon({
|
const AvatarIcon({
|
||||||
super.key,
|
super.key,
|
||||||
this.group,
|
this.group,
|
||||||
|
this.contacts,
|
||||||
this.contactId,
|
this.contactId,
|
||||||
this.myAvatar = false,
|
this.myAvatar = false,
|
||||||
this.fontSize = 20,
|
this.fontSize = 20,
|
||||||
|
|
@ -20,6 +21,7 @@ class AvatarIcon extends StatefulWidget {
|
||||||
this.color,
|
this.color,
|
||||||
});
|
});
|
||||||
final Group? group;
|
final Group? group;
|
||||||
|
final List<Contact>? contacts;
|
||||||
final int? contactId;
|
final int? contactId;
|
||||||
final bool myAvatar;
|
final bool myAvatar;
|
||||||
final double? fontSize;
|
final double? fontSize;
|
||||||
|
|
@ -45,6 +47,21 @@ class _AvatarIconState extends State<AvatarIcon> {
|
||||||
initAsync();
|
initAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(AvatarIcon oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (widget.contacts != null && widget.contacts != oldWidget.contacts) {
|
||||||
|
_setAvatarContacts(widget.contacts!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setAvatarContacts(List<Contact> contacts) {
|
||||||
|
_avatarContacts = contacts
|
||||||
|
.where((contact) => contact.avatarSvgCompressed != null)
|
||||||
|
.toList();
|
||||||
|
if (mounted) setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
groupStream?.cancel();
|
groupStream?.cancel();
|
||||||
|
|
@ -84,7 +101,9 @@ class _AvatarIconState extends State<AvatarIcon> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> initAsync() async {
|
Future<void> initAsync() async {
|
||||||
if (widget.group != null) {
|
if (widget.contacts != null) {
|
||||||
|
_setAvatarContacts(widget.contacts!);
|
||||||
|
} else if (widget.group != null) {
|
||||||
groupStream = twonlyDB.groupsDao
|
groupStream = twonlyDB.groupsDao
|
||||||
.watchGroupContact(widget.group!.groupId)
|
.watchGroupContact(widget.group!.groupId)
|
||||||
.listen((contacts) {
|
.listen((contacts) {
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ class ContactLabels extends StatefulWidget {
|
||||||
this.padding = const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
this.padding = const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||||
this.emptyText,
|
this.emptyText,
|
||||||
this.showEmptyText = false,
|
this.showEmptyText = false,
|
||||||
|
this.labels,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -19,6 +20,7 @@ class ContactLabels extends StatefulWidget {
|
||||||
final EdgeInsetsGeometry padding;
|
final EdgeInsetsGeometry padding;
|
||||||
final String? emptyText;
|
final String? emptyText;
|
||||||
final bool showEmptyText;
|
final bool showEmptyText;
|
||||||
|
final List<Label>? labels;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ContactLabels> createState() => _ContactLabelsState();
|
State<ContactLabels> createState() => _ContactLabelsState();
|
||||||
|
|
@ -31,20 +33,32 @@ class _ContactLabelsState extends State<ContactLabels> {
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_sub = twonlyDB.labelsDao.watchContactLabels(widget.contactId).listen((
|
if (widget.labels != null) {
|
||||||
labels,
|
_labels = widget.labels!;
|
||||||
) {
|
} else {
|
||||||
if (mounted) {
|
_sub = twonlyDB.labelsDao.watchContactLabels(widget.contactId).listen((
|
||||||
setState(() {
|
labels,
|
||||||
_labels = labels;
|
) {
|
||||||
});
|
if (mounted) {
|
||||||
}
|
setState(() {
|
||||||
});
|
_labels = labels;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(ContactLabels oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (widget.labels != null && widget.labels != oldWidget.labels) {
|
||||||
|
_labels = widget.labels!;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_sub.cancel();
|
if (widget.labels == null) _sub.cancel();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -101,8 +115,7 @@ Widget? buildContactLabelsSubtitle({
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
additionalSubtitle,
|
additionalSubtitle,
|
||||||
if (labels.isNotEmpty)
|
if (labels.isNotEmpty) ContactLabels(contactId: contactId),
|
||||||
ContactLabels(contactId: contactId),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,17 +3,20 @@ import 'dart:async';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.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/services/flame.service.dart';
|
||||||
import 'package:twonly/src/visual/components/animate_icon.comp.dart';
|
import 'package:twonly/src/visual/components/animate_icon.comp.dart';
|
||||||
|
|
||||||
class FlameCounterWidget extends StatefulWidget {
|
class FlameCounterWidget extends StatefulWidget {
|
||||||
const FlameCounterWidget({
|
const FlameCounterWidget({
|
||||||
this.groupId,
|
this.groupId,
|
||||||
this.contactId,
|
this.contactId,
|
||||||
|
this.group,
|
||||||
this.prefix = false,
|
this.prefix = false,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
final String? groupId;
|
final String? groupId;
|
||||||
final int? contactId;
|
final int? contactId;
|
||||||
|
final Group? group;
|
||||||
final bool prefix;
|
final bool prefix;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -33,6 +36,14 @@ class _FlameCounterWidgetState extends State<FlameCounterWidget> {
|
||||||
initAsync();
|
initAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(FlameCounterWidget oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (widget.group != null && widget.group != oldWidget.group) {
|
||||||
|
initAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
flameCounterSub?.cancel();
|
flameCounterSub?.cancel();
|
||||||
|
|
@ -41,8 +52,10 @@ class _FlameCounterWidgetState extends State<FlameCounterWidget> {
|
||||||
|
|
||||||
Future<void> initAsync() async {
|
Future<void> initAsync() async {
|
||||||
var groupId = widget.groupId;
|
var groupId = widget.groupId;
|
||||||
late Group? group;
|
var group = widget.group;
|
||||||
if (widget.groupId == null && widget.contactId != null) {
|
if (group != null) {
|
||||||
|
groupId = group.groupId;
|
||||||
|
} else if (widget.groupId == null && widget.contactId != null) {
|
||||||
group = await twonlyDB.groupsDao.getDirectChat(widget.contactId!);
|
group = await twonlyDB.groupsDao.getDirectChat(widget.contactId!);
|
||||||
groupId = group?.groupId;
|
groupId = group?.groupId;
|
||||||
} else if (groupId != null) {
|
} else if (groupId != null) {
|
||||||
|
|
@ -52,6 +65,16 @@ class _FlameCounterWidgetState extends State<FlameCounterWidget> {
|
||||||
isBestFriend =
|
isBestFriend =
|
||||||
userService.currentUser.myBestFriendGroupId == groupId &&
|
userService.currentUser.myBestFriendGroupId == groupId &&
|
||||||
group.alsoBestFriend;
|
group.alsoBestFriend;
|
||||||
|
if (widget.group != null) {
|
||||||
|
final result = getFlameCounterFromGroup(group);
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
flameCounter = result.counter;
|
||||||
|
isExpiring = result.isExpiring;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
final stream = twonlyDB.groupsDao.watchFlameCounter(groupId);
|
final stream = twonlyDB.groupsDao.watchFlameCounter(groupId);
|
||||||
flameCounterSub = stream.listen((result) {
|
flameCounterSub = stream.listen((result) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,8 @@ class VerificationBadgeComp extends StatefulWidget {
|
||||||
this.showOnlyIfVerified = false,
|
this.showOnlyIfVerified = false,
|
||||||
this.isVerifiedByTransferredTrust,
|
this.isVerifiedByTransferredTrust,
|
||||||
this.clickable = true,
|
this.clickable = true,
|
||||||
|
this.verificationStatus,
|
||||||
|
this.useProvidedStatus = false,
|
||||||
});
|
});
|
||||||
final Group? group;
|
final Group? group;
|
||||||
final Contact? contact;
|
final Contact? contact;
|
||||||
|
|
@ -26,6 +28,8 @@ class VerificationBadgeComp extends StatefulWidget {
|
||||||
|
|
||||||
final bool showOnlyIfVerified;
|
final bool showOnlyIfVerified;
|
||||||
final bool clickable;
|
final bool clickable;
|
||||||
|
final VerificationStatus? verificationStatus;
|
||||||
|
final bool useProvidedStatus;
|
||||||
final bool? isVerifiedByTransferredTrust;
|
final bool? isVerifiedByTransferredTrust;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -52,7 +56,31 @@ class _VerificationBadgeCompState extends State<VerificationBadgeComp> {
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
initAsync();
|
if (widget.useProvidedStatus) {
|
||||||
|
_applyVerificationStatus(
|
||||||
|
widget.verificationStatus ?? VerificationStatus.notTrusted,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
initAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(VerificationBadgeComp oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (widget.useProvidedStatus &&
|
||||||
|
widget.verificationStatus != oldWidget.verificationStatus) {
|
||||||
|
_applyVerificationStatus(
|
||||||
|
widget.verificationStatus ?? VerificationStatus.notTrusted,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _applyVerificationStatus(VerificationStatus status) {
|
||||||
|
_isVerified = status == VerificationStatus.trusted;
|
||||||
|
_isSharedVerified = false;
|
||||||
|
_verifiedByTransferredTrustCount =
|
||||||
|
status == VerificationStatus.partialTrusted ? 10 : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _updateVerificationCounts() {
|
void _updateVerificationCounts() {
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart' show setEquals;
|
||||||
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:go_router/go_router.dart';
|
import 'package:go_router/go_router.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/constants/routes.keys.dart';
|
import 'package:twonly/src/constants/routes.keys.dart';
|
||||||
|
import 'package:twonly/src/database/daos/key_verification.dao.dart';
|
||||||
import 'package:twonly/src/database/twonly.db.dart';
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
import 'package:twonly/src/providers/purchases.provider.dart';
|
import 'package:twonly/src/providers/purchases.provider.dart';
|
||||||
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
||||||
|
|
@ -18,6 +20,7 @@ import 'package:twonly/src/visual/components/notification_badge.comp.dart';
|
||||||
import 'package:twonly/src/visual/views/chats/chat_list_components/empty_chat_list.comp.dart';
|
import 'package:twonly/src/visual/views/chats/chat_list_components/empty_chat_list.comp.dart';
|
||||||
import 'package:twonly/src/visual/views/chats/chat_list_components/group_list_item.comp.dart';
|
import 'package:twonly/src/visual/views/chats/chat_list_components/group_list_item.comp.dart';
|
||||||
import 'package:twonly/src/visual/views/chats/chat_list_components/news_btn.comp.dart';
|
import 'package:twonly/src/visual/views/chats/chat_list_components/news_btn.comp.dart';
|
||||||
|
import 'package:twonly/src/visual/views/chats/chat_messages_components/typing_indicator.dart';
|
||||||
import 'package:twonly/src/visual/views/onboarding/setup/components/finish_setup.comp.dart';
|
import 'package:twonly/src/visual/views/onboarding/setup/components/finish_setup.comp.dart';
|
||||||
import 'package:twonly/src/visual/views/settings/backup/components/missing_backup_setup.comp.dart';
|
import 'package:twonly/src/visual/views/settings/backup/components/missing_backup_setup.comp.dart';
|
||||||
import 'package:twonly/src/visual/views/settings/backup/passwordless_recovery/components/missing_recovery_contacts.comp.dart';
|
import 'package:twonly/src/visual/views/settings/backup/passwordless_recovery/components/missing_recovery_contacts.comp.dart';
|
||||||
|
|
@ -28,15 +31,34 @@ class ChatListView extends StatefulWidget {
|
||||||
State<ChatListView> createState() => _ChatListViewState();
|
State<ChatListView> createState() => _ChatListViewState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ChatListViewState extends State<ChatListView> with AutomaticKeepAliveClientMixin<ChatListView> {
|
class _ChatListViewState extends State<ChatListView>
|
||||||
|
with AutomaticKeepAliveClientMixin<ChatListView> {
|
||||||
StreamSubscription<void>? _userSub;
|
StreamSubscription<void>? _userSub;
|
||||||
StreamSubscription<List<Group>>? _contactsSub;
|
StreamSubscription<List<Group>>? _contactsSub;
|
||||||
StreamSubscription<List<Contact>>? _contactsCountSub;
|
StreamSubscription<List<Contact>>? _contactsCountSub;
|
||||||
StreamSubscription<List<MediaFile>>? _precacheSub;
|
StreamSubscription<List<MediaFile>>? _precacheSub;
|
||||||
|
StreamSubscription<List<GroupMember>>? _typingMembersSub;
|
||||||
|
StreamSubscription<List<(Contact, GroupMember)>>? _groupMembersSub;
|
||||||
|
StreamSubscription<List<Message>>? _unopenedMessagesSub;
|
||||||
|
StreamSubscription<List<(String, Reaction)>>? _reactionsSub;
|
||||||
|
StreamSubscription<List<Message>>? _latestMessagesSub;
|
||||||
|
StreamSubscription<List<MediaFile>>? _chatListMediaSub;
|
||||||
|
StreamSubscription<Map<String, VerificationStatus>>? _verificationSub;
|
||||||
|
StreamSubscription<List<(int, Label)>>? _contactLabelsSub;
|
||||||
|
Timer? _typingUpdateTimer;
|
||||||
final Set<String> _precachedMediaIds = {};
|
final Set<String> _precachedMediaIds = {};
|
||||||
List<Group> _groupsNotPinned = [];
|
List<Group> _groupsNotPinned = [];
|
||||||
List<Group> _groupsPinned = [];
|
List<Group> _groupsPinned = [];
|
||||||
List<Group> _groupsArchived = [];
|
List<Group> _groupsArchived = [];
|
||||||
|
Set<String> _typingGroupIds = {};
|
||||||
|
List<GroupMember> _typingMembers = [];
|
||||||
|
Map<String, List<Contact>> _contactsByGroup = {};
|
||||||
|
Map<String, List<Message>> _unopenedMessagesByGroup = {};
|
||||||
|
Map<String, Reaction> _lastReactionByGroup = {};
|
||||||
|
Map<String, Message> _lastMessageByGroup = {};
|
||||||
|
Map<String, MediaFile> _chatListMediaById = {};
|
||||||
|
Map<String, VerificationStatus> _verificationByGroup = {};
|
||||||
|
Map<int, List<Label>> _labelsByContact = {};
|
||||||
|
|
||||||
final ValueNotifier<bool> _hasContacts = ValueNotifier(false);
|
final ValueNotifier<bool> _hasContacts = ValueNotifier(false);
|
||||||
bool _loading = true;
|
bool _loading = true;
|
||||||
|
|
@ -102,7 +124,9 @@ class _ChatListViewState extends State<ChatListView> with AutomaticKeepAliveClie
|
||||||
_badgeCount.value = _countAnnouncedUsers + _countContactRequest;
|
_badgeCount.value = _countAnnouncedUsers + _countContactRequest;
|
||||||
});
|
});
|
||||||
|
|
||||||
_precacheSub = twonlyDB.messagesDao.watchUnopenedMediaFiles().listen((mediaFiles) {
|
_precacheSub = twonlyDB.messagesDao.watchUnopenedMediaFiles().listen((
|
||||||
|
mediaFiles,
|
||||||
|
) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
for (final media in mediaFiles) {
|
for (final media in mediaFiles) {
|
||||||
if (!_precachedMediaIds.contains(media.mediaId)) {
|
if (!_precachedMediaIds.contains(media.mediaId)) {
|
||||||
|
|
@ -119,6 +143,80 @@ class _ChatListViewState extends State<ChatListView> with AutomaticKeepAliveClie
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
_typingMembersSub = twonlyDB.groupsDao.watchTypingGroupMembers().listen((
|
||||||
|
members,
|
||||||
|
) {
|
||||||
|
if (!mounted) return;
|
||||||
|
_typingMembers = members;
|
||||||
|
_updateTypingGroupIds();
|
||||||
|
});
|
||||||
|
_typingUpdateTimer = Timer.periodic(
|
||||||
|
const Duration(seconds: 1),
|
||||||
|
(_) => _updateTypingGroupIds(),
|
||||||
|
);
|
||||||
|
_groupMembersSub = twonlyDB.groupsDao.watchAllGroupMembers().listen((rows) {
|
||||||
|
if (!mounted) return;
|
||||||
|
final contactsByGroup = <String, List<Contact>>{};
|
||||||
|
for (final row in rows) {
|
||||||
|
contactsByGroup.putIfAbsent(row.$2.groupId, () => []).add(row.$1);
|
||||||
|
}
|
||||||
|
setState(() => _contactsByGroup = contactsByGroup);
|
||||||
|
});
|
||||||
|
_unopenedMessagesSub = twonlyDB.messagesDao
|
||||||
|
.watchAllMessagesNotOpened()
|
||||||
|
.listen((messages) {
|
||||||
|
if (!mounted) return;
|
||||||
|
final byGroup = <String, List<Message>>{};
|
||||||
|
for (final message in messages) {
|
||||||
|
byGroup.putIfAbsent(message.groupId, () => []).add(message);
|
||||||
|
}
|
||||||
|
setState(() => _unopenedMessagesByGroup = byGroup);
|
||||||
|
});
|
||||||
|
_reactionsSub = twonlyDB.reactionsDao.watchLatestReactionsByGroup().listen((
|
||||||
|
rows,
|
||||||
|
) {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(
|
||||||
|
() => _lastReactionByGroup = {for (final row in rows) row.$1: row.$2},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
_latestMessagesSub = twonlyDB.messagesDao
|
||||||
|
.watchLatestMessagesByGroup()
|
||||||
|
.listen((messages) {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(
|
||||||
|
() => _lastMessageByGroup = {
|
||||||
|
for (final message in messages) message.groupId: message,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
_chatListMediaSub = twonlyDB.mediaFilesDao.watchChatListMediaFiles().listen(
|
||||||
|
(mediaFiles) {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(
|
||||||
|
() => _chatListMediaById = {
|
||||||
|
for (final mediaFile in mediaFiles) mediaFile.mediaId: mediaFile,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
_verificationSub = twonlyDB.keyVerificationDao
|
||||||
|
.watchAllGroupsVerificationStatus()
|
||||||
|
.listen((statuses) {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() => _verificationByGroup = statuses);
|
||||||
|
});
|
||||||
|
_contactLabelsSub = twonlyDB.labelsDao.watchAllContactLabels().listen((
|
||||||
|
rows,
|
||||||
|
) {
|
||||||
|
if (!mounted) return;
|
||||||
|
final labels = <int, List<Label>>{};
|
||||||
|
for (final row in rows) {
|
||||||
|
labels.putIfAbsent(row.$1, () => []).add(row.$2);
|
||||||
|
}
|
||||||
|
setState(() => _labelsByContact = labels);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -132,9 +230,44 @@ class _ChatListViewState extends State<ChatListView> with AutomaticKeepAliveClie
|
||||||
_countAnnouncedStream.cancel();
|
_countAnnouncedStream.cancel();
|
||||||
_userSub?.cancel();
|
_userSub?.cancel();
|
||||||
_precacheSub?.cancel();
|
_precacheSub?.cancel();
|
||||||
|
_typingMembersSub?.cancel();
|
||||||
|
_typingUpdateTimer?.cancel();
|
||||||
|
_groupMembersSub?.cancel();
|
||||||
|
_unopenedMessagesSub?.cancel();
|
||||||
|
_reactionsSub?.cancel();
|
||||||
|
_latestMessagesSub?.cancel();
|
||||||
|
_chatListMediaSub?.cancel();
|
||||||
|
_verificationSub?.cancel();
|
||||||
|
_contactLabelsSub?.cancel();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _updateTypingGroupIds() {
|
||||||
|
if (!mounted) return;
|
||||||
|
final typingGroupIds = _typingMembers
|
||||||
|
.where(isTyping)
|
||||||
|
.map((member) => member.groupId)
|
||||||
|
.toSet();
|
||||||
|
if (setEquals(_typingGroupIds, typingGroupIds)) return;
|
||||||
|
setState(() => _typingGroupIds = typingGroupIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, MediaFile> _mediaForGroup(String groupId) {
|
||||||
|
final mediaIds = <String>{
|
||||||
|
for (final message
|
||||||
|
in _unopenedMessagesByGroup[groupId] ?? const <Message>[])
|
||||||
|
if (message.mediaId != null) message.mediaId!,
|
||||||
|
if (_lastMessageByGroup[groupId]?.mediaId != null)
|
||||||
|
_lastMessageByGroup[groupId]!.mediaId!,
|
||||||
|
};
|
||||||
|
final mediaFiles = <String, MediaFile>{};
|
||||||
|
for (final mediaId in mediaIds) {
|
||||||
|
final mediaFile = _chatListMediaById[mediaId];
|
||||||
|
if (mediaFile != null) mediaFiles[mediaId] = mediaFile;
|
||||||
|
}
|
||||||
|
return mediaFiles;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
super.build(context);
|
super.build(context);
|
||||||
|
|
@ -277,6 +410,22 @@ class _ChatListViewState extends State<ChatListView> with AutomaticKeepAliveClie
|
||||||
return GroupListItemComp(
|
return GroupListItemComp(
|
||||||
key: ValueKey(group.groupId),
|
key: ValueKey(group.groupId),
|
||||||
group: group,
|
group: group,
|
||||||
|
isTyping: _typingGroupIds.contains(group.groupId),
|
||||||
|
contacts: _contactsByGroup[group.groupId] ?? const [],
|
||||||
|
unopenedMessages:
|
||||||
|
_unopenedMessagesByGroup[group.groupId] ?? const [],
|
||||||
|
lastReaction: _lastReactionByGroup[group.groupId],
|
||||||
|
lastMessage: _lastMessageByGroup[group.groupId],
|
||||||
|
mediaFiles: _mediaForGroup(group.groupId),
|
||||||
|
useSharedSummary: true,
|
||||||
|
verificationStatus: _verificationByGroup[group.groupId],
|
||||||
|
contactLabels:
|
||||||
|
_contactsByGroup[group.groupId]?.isNotEmpty == true
|
||||||
|
? _labelsByContact[_contactsByGroup[group.groupId]!
|
||||||
|
.first
|
||||||
|
.userId] ??
|
||||||
|
const []
|
||||||
|
: const [],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -296,6 +445,22 @@ class _ChatListViewState extends State<ChatListView> with AutomaticKeepAliveClie
|
||||||
return GroupListItemComp(
|
return GroupListItemComp(
|
||||||
key: ValueKey(group.groupId),
|
key: ValueKey(group.groupId),
|
||||||
group: group,
|
group: group,
|
||||||
|
isTyping: _typingGroupIds.contains(group.groupId),
|
||||||
|
contacts: _contactsByGroup[group.groupId] ?? const [],
|
||||||
|
unopenedMessages:
|
||||||
|
_unopenedMessagesByGroup[group.groupId] ?? const [],
|
||||||
|
lastReaction: _lastReactionByGroup[group.groupId],
|
||||||
|
lastMessage: _lastMessageByGroup[group.groupId],
|
||||||
|
mediaFiles: _mediaForGroup(group.groupId),
|
||||||
|
useSharedSummary: true,
|
||||||
|
verificationStatus: _verificationByGroup[group.groupId],
|
||||||
|
contactLabels:
|
||||||
|
_contactsByGroup[group.groupId]?.isNotEmpty == true
|
||||||
|
? _labelsByContact[_contactsByGroup[group.groupId]!
|
||||||
|
.first
|
||||||
|
.userId] ??
|
||||||
|
const []
|
||||||
|
: const [],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ 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/database/daos/key_verification.dao.dart';
|
||||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||||
import 'package:twonly/src/database/tables/messages.table.dart';
|
import 'package:twonly/src/database/tables/messages.table.dart';
|
||||||
import 'package:twonly/src/database/twonly.db.dart';
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
|
|
@ -23,9 +24,27 @@ import 'package:twonly/src/visual/views/chats/chat_messages_components/message_s
|
||||||
class GroupListItemComp extends StatefulWidget {
|
class GroupListItemComp extends StatefulWidget {
|
||||||
const GroupListItemComp({
|
const GroupListItemComp({
|
||||||
required this.group,
|
required this.group,
|
||||||
|
this.isTyping,
|
||||||
|
this.contacts,
|
||||||
|
this.unopenedMessages,
|
||||||
|
this.lastReaction,
|
||||||
|
this.lastMessage,
|
||||||
|
this.mediaFiles,
|
||||||
|
this.useSharedSummary = false,
|
||||||
|
this.verificationStatus,
|
||||||
|
this.contactLabels = const [],
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
final Group group;
|
final Group group;
|
||||||
|
final bool? isTyping;
|
||||||
|
final List<Contact>? contacts;
|
||||||
|
final List<Message>? unopenedMessages;
|
||||||
|
final Reaction? lastReaction;
|
||||||
|
final Message? lastMessage;
|
||||||
|
final Map<String, MediaFile>? mediaFiles;
|
||||||
|
final bool useSharedSummary;
|
||||||
|
final VerificationStatus? verificationStatus;
|
||||||
|
final List<Label> contactLabels;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<GroupListItemComp> createState() => _UserListItem();
|
State<GroupListItemComp> createState() => _UserListItem();
|
||||||
|
|
@ -53,9 +72,47 @@ class _UserListItem extends State<GroupListItemComp> {
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_applyContacts();
|
||||||
|
_lastReaction = widget.lastReaction;
|
||||||
|
_lastMessage = widget.lastMessage;
|
||||||
|
_applyMediaFiles();
|
||||||
|
if (widget.useSharedSummary) {
|
||||||
|
_updateState(widget.lastMessage, widget.unopenedMessages ?? const []);
|
||||||
|
}
|
||||||
initStreams();
|
initStreams();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(GroupListItemComp oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (widget.contacts != oldWidget.contacts) _applyContacts();
|
||||||
|
if (widget.lastReaction != oldWidget.lastReaction) {
|
||||||
|
_lastReaction = widget.lastReaction;
|
||||||
|
}
|
||||||
|
if (widget.unopenedMessages != oldWidget.unopenedMessages) {
|
||||||
|
_updateState(widget.lastMessage, widget.unopenedMessages ?? const []);
|
||||||
|
} else if (widget.lastMessage != oldWidget.lastMessage) {
|
||||||
|
_updateState(widget.lastMessage, widget.unopenedMessages ?? const []);
|
||||||
|
}
|
||||||
|
if (widget.mediaFiles != oldWidget.mediaFiles) _applyMediaFiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _applyContacts() {
|
||||||
|
if (widget.contacts == null) return;
|
||||||
|
_directContact = widget.group.isDirectChat && widget.contacts!.isNotEmpty
|
||||||
|
? widget.contacts!.first
|
||||||
|
: null;
|
||||||
|
_receiverDeletedAccount =
|
||||||
|
widget.contacts!.length == 1 && widget.contacts!.first.accountDeleted;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _applyMediaFiles() {
|
||||||
|
if (widget.mediaFiles == null) return;
|
||||||
|
_previewMediaFiles
|
||||||
|
..clear()
|
||||||
|
..addAll(widget.mediaFiles!.values);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_messagesNotOpenedStream?.cancel();
|
_messagesNotOpenedStream?.cancel();
|
||||||
|
|
@ -67,45 +124,57 @@ class _UserListItem extends State<GroupListItemComp> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> initStreams() async {
|
Future<void> initStreams() async {
|
||||||
final lastMsgStream = await twonlyDB.messagesDao.watchLastMessage(
|
if (!widget.useSharedSummary) {
|
||||||
widget.group.groupId,
|
final lastMsgStream = await twonlyDB.messagesDao.watchLastMessage(
|
||||||
);
|
widget.group.groupId,
|
||||||
if (!mounted) return;
|
);
|
||||||
_lastMessageStream = lastMsgStream.listen((update) {
|
if (!mounted) return;
|
||||||
_updateState(update, _messagesNotOpened);
|
_lastMessageStream = lastMsgStream.listen((update) {
|
||||||
});
|
_updateState(update, _messagesNotOpened);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
_lastReactionStream = twonlyDB.reactionsDao
|
if (!widget.useSharedSummary) {
|
||||||
.watchLastReactions(widget.group.groupId)
|
_lastReactionStream = twonlyDB.reactionsDao
|
||||||
.listen((update) {
|
.watchLastReactions(widget.group.groupId)
|
||||||
if (!mounted) return;
|
.listen((update) {
|
||||||
setState(() {
|
if (!mounted) return;
|
||||||
_lastReaction = update;
|
setState(() {
|
||||||
|
_lastReaction = update;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
_messagesNotOpenedStream = twonlyDB.messagesDao
|
if (widget.useSharedSummary) {
|
||||||
.watchMessageNotOpened(widget.group.groupId)
|
_messagesNotOpened = widget.unopenedMessages!;
|
||||||
.listen((update) {
|
} else {
|
||||||
_updateState(_lastMessage, update);
|
_messagesNotOpenedStream = twonlyDB.messagesDao
|
||||||
});
|
.watchMessageNotOpened(widget.group.groupId)
|
||||||
|
.listen((update) {
|
||||||
|
_updateState(_lastMessage, update);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
_lastMediaFilesStream = twonlyDB.mediaFilesDao
|
if (!widget.useSharedSummary) {
|
||||||
.watchMediaFilesForGroup(widget.group.groupId)
|
_lastMediaFilesStream = twonlyDB.mediaFilesDao
|
||||||
.listen((mediaFiles) {
|
.watchMediaFilesForGroup(widget.group.groupId)
|
||||||
if (!mounted) return;
|
.listen((mediaFiles) {
|
||||||
for (final mediaFile in mediaFiles) {
|
if (!mounted) return;
|
||||||
final index = _previewMediaFiles.indexWhere(
|
for (final mediaFile in mediaFiles) {
|
||||||
(t) => t.mediaId == mediaFile.mediaId,
|
final index = _previewMediaFiles.indexWhere(
|
||||||
);
|
(t) => t.mediaId == mediaFile.mediaId,
|
||||||
if (index >= 0) {
|
);
|
||||||
_previewMediaFiles[index] = mediaFile;
|
if (index >= 0) {
|
||||||
|
_previewMediaFiles[index] = mediaFile;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
setState(() {});
|
||||||
setState(() {});
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
if (widget.group.isDirectChat) {
|
if (widget.contacts != null) {
|
||||||
|
// Contact data is shared by the parent chat list.
|
||||||
|
} else if (widget.group.isDirectChat) {
|
||||||
_directContactStream = twonlyDB.groupsDao
|
_directContactStream = twonlyDB.groupsDao
|
||||||
.watchGroupContact(widget.group.groupId)
|
.watchGroupContact(widget.group.groupId)
|
||||||
.listen((contacts) {
|
.listen((contacts) {
|
||||||
|
|
@ -191,6 +260,7 @@ class _UserListItem extends State<GroupListItemComp> {
|
||||||
/// Fetches any media files referenced by preview messages but not yet in the
|
/// Fetches any media files referenced by preview messages but not yet in the
|
||||||
/// local cache. Fire-and-forget; updates state when results arrive.
|
/// local cache. Fire-and-forget; updates state when results arrive.
|
||||||
Future<void> _fetchMissingMediaFiles() async {
|
Future<void> _fetchMissingMediaFiles() async {
|
||||||
|
if (widget.useSharedSummary) return;
|
||||||
final missing = <MediaFile>[];
|
final missing = <MediaFile>[];
|
||||||
for (final message in _previewMessages) {
|
for (final message in _previewMessages) {
|
||||||
if (message.mediaId != null &&
|
if (message.mediaId != null &&
|
||||||
|
|
@ -266,6 +336,8 @@ class _UserListItem extends State<GroupListItemComp> {
|
||||||
const SizedBox(width: 3),
|
const SizedBox(width: 3),
|
||||||
VerificationBadgeComp(
|
VerificationBadgeComp(
|
||||||
group: widget.group,
|
group: widget.group,
|
||||||
|
verificationStatus: widget.verificationStatus,
|
||||||
|
useProvidedStatus: widget.useSharedSummary,
|
||||||
showOnlyIfVerified: true,
|
showOnlyIfVerified: true,
|
||||||
clickable: false,
|
clickable: false,
|
||||||
size: 12,
|
size: 12,
|
||||||
|
|
@ -278,6 +350,7 @@ class _UserListItem extends State<GroupListItemComp> {
|
||||||
physics: const BouncingScrollPhysics(),
|
physics: const BouncingScrollPhysics(),
|
||||||
child: ContactLabels(
|
child: ContactLabels(
|
||||||
contactId: _directContact!.userId,
|
contactId: _directContact!.userId,
|
||||||
|
labels: widget.contactLabels,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -296,7 +369,7 @@ class _UserListItem extends State<GroupListItemComp> {
|
||||||
dateTime: widget.group.lastMessageExchange,
|
dateTime: widget.group.lastMessageExchange,
|
||||||
),
|
),
|
||||||
FlameCounterWidget(
|
FlameCounterWidget(
|
||||||
groupId: widget.group.groupId,
|
group: widget.group,
|
||||||
prefix: true,
|
prefix: true,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -305,6 +378,7 @@ class _UserListItem extends State<GroupListItemComp> {
|
||||||
children: [
|
children: [
|
||||||
TypingIndicatorSubtitleComp(
|
TypingIndicatorSubtitleComp(
|
||||||
groupId: widget.group.groupId,
|
groupId: widget.group.groupId,
|
||||||
|
isTyping: widget.isTyping,
|
||||||
),
|
),
|
||||||
MessageSendStateIcon(
|
MessageSendStateIcon(
|
||||||
_previewMessages,
|
_previewMessages,
|
||||||
|
|
@ -320,7 +394,7 @@ class _UserListItem extends State<GroupListItemComp> {
|
||||||
message: _currentMessage,
|
message: _currentMessage,
|
||||||
),
|
),
|
||||||
FlameCounterWidget(
|
FlameCounterWidget(
|
||||||
groupId: widget.group.groupId,
|
group: widget.group,
|
||||||
prefix: true,
|
prefix: true,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -340,7 +414,7 @@ class _UserListItem extends State<GroupListItemComp> {
|
||||||
await context.push(Routes.profileGroup(widget.group.groupId));
|
await context.push(Routes.profileGroup(widget.group.groupId));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: AvatarIcon(group: widget.group),
|
child: AvatarIcon(group: widget.group, contacts: widget.contacts),
|
||||||
),
|
),
|
||||||
trailing: (widget.group.leftGroup || _receiverDeletedAccount)
|
trailing: (widget.group.leftGroup || _receiverDeletedAccount)
|
||||||
? null
|
? null
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,14 @@ import 'package:twonly/src/visual/views/chats/chat_messages.view.dart';
|
||||||
import 'package:twonly/src/visual/views/chats/chat_messages_components/typing_indicator.dart';
|
import 'package:twonly/src/visual/views/chats/chat_messages_components/typing_indicator.dart';
|
||||||
|
|
||||||
class TypingIndicatorSubtitleComp extends StatefulWidget {
|
class TypingIndicatorSubtitleComp extends StatefulWidget {
|
||||||
const TypingIndicatorSubtitleComp({required this.groupId, super.key});
|
const TypingIndicatorSubtitleComp({
|
||||||
|
required this.groupId,
|
||||||
|
this.isTyping,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
final String groupId;
|
final String groupId;
|
||||||
|
final bool? isTyping;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<TypingIndicatorSubtitleComp> createState() =>
|
State<TypingIndicatorSubtitleComp> createState() =>
|
||||||
|
|
@ -28,6 +33,8 @@ class _TypingIndicatorSubtitleCompState
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
|
if (widget.isTyping != null) return;
|
||||||
|
|
||||||
final membersStream = twonlyDB.groupsDao.watchGroupMembers(
|
final membersStream = twonlyDB.groupsDao.watchGroupMembers(
|
||||||
widget.groupId,
|
widget.groupId,
|
||||||
);
|
);
|
||||||
|
|
@ -38,9 +45,9 @@ class _TypingIndicatorSubtitleCompState
|
||||||
|
|
||||||
void filterOpenUsers(List<GroupMember> input) {
|
void filterOpenUsers(List<GroupMember> input) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
||||||
final typingMembers = input.where(isTyping).toList();
|
final typingMembers = input.where(isTyping).toList();
|
||||||
|
|
||||||
if (typingMembers.isEmpty) {
|
if (typingMembers.isEmpty) {
|
||||||
_periodicUpdate?.cancel();
|
_periodicUpdate?.cancel();
|
||||||
_periodicUpdate = null;
|
_periodicUpdate = null;
|
||||||
|
|
@ -64,22 +71,24 @@ class _TypingIndicatorSubtitleCompState
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (_groupMembers.isEmpty) return Container();
|
if (widget.isTyping ?? _groupMembers.isNotEmpty) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(right: 5),
|
padding: const EdgeInsets.only(right: 5),
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 3),
|
padding: const EdgeInsets.symmetric(vertical: 3),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: getMessageColor(true),
|
color: getMessageColor(true),
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
child: Transform.scale(
|
child: Transform.scale(
|
||||||
scale: 0.6,
|
scale: 0.6,
|
||||||
child: const AnimatedTypingDots(
|
child: const AnimatedTypingDots(
|
||||||
isTyping: true,
|
isTyping: true,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
}
|
||||||
|
return Container();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:collection';
|
import 'dart:collection';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart' show setEquals;
|
||||||
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:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
@ -25,11 +26,52 @@ import 'package:twonly/src/visual/views/chats/chat_messages_components/blink.com
|
||||||
import 'package:twonly/src/visual/views/chats/chat_messages_components/chat_group_action.dart';
|
import 'package:twonly/src/visual/views/chats/chat_messages_components/chat_group_action.dart';
|
||||||
import 'package:twonly/src/visual/views/chats/chat_messages_components/chat_list_entry.dart';
|
import 'package:twonly/src/visual/views/chats/chat_messages_components/chat_list_entry.dart';
|
||||||
import 'package:twonly/src/visual/views/chats/chat_messages_components/entries/chat_date_chip.dart';
|
import 'package:twonly/src/visual/views/chats/chat_messages_components/entries/chat_date_chip.dart';
|
||||||
|
import 'package:twonly/src/visual/views/chats/chat_messages_components/entries/friendly_message_time.comp.dart';
|
||||||
import 'package:twonly/src/visual/views/chats/chat_messages_components/in_chat_group_overview.dart';
|
import 'package:twonly/src/visual/views/chats/chat_messages_components/in_chat_group_overview.dart';
|
||||||
import 'package:twonly/src/visual/views/chats/chat_messages_components/message_input.dart';
|
import 'package:twonly/src/visual/views/chats/chat_messages_components/message_input.dart';
|
||||||
import 'package:twonly/src/visual/views/chats/chat_messages_components/response_container.dart';
|
import 'package:twonly/src/visual/views/chats/chat_messages_components/response_container.dart';
|
||||||
import 'package:twonly/src/visual/views/chats/chat_messages_components/typing_indicator.dart';
|
import 'package:twonly/src/visual/views/chats/chat_messages_components/typing_indicator.dart';
|
||||||
|
|
||||||
|
class _MessageAnimationState {
|
||||||
|
bool hasReceivedFirstBatch = false;
|
||||||
|
final HashSet<String> knownMessageIds = HashSet<String>();
|
||||||
|
final HashSet<String> animateMessageIds = HashSet<String>();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ChatViewData {
|
||||||
|
Map<int, Contact> contactsById = {};
|
||||||
|
Map<String, MediaFile> mediaFilesById = {};
|
||||||
|
Map<String, List<Reaction>> reactionsByMessageId = {};
|
||||||
|
Set<String> ackedMessageIds = {};
|
||||||
|
|
||||||
|
List<ChatItem> chatItems = [];
|
||||||
|
List<Message> allMessages = [];
|
||||||
|
Map<String, Message> messagesById = {};
|
||||||
|
List<GroupHistory> groupActions = [];
|
||||||
|
List<MemoryItem> galleryItems = [];
|
||||||
|
Set<String> galleryMessageIds = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ChatSubscriptions {
|
||||||
|
StreamSubscription<Group?>? group;
|
||||||
|
StreamSubscription<List<Message>>? messages;
|
||||||
|
StreamSubscription<List<GroupHistory>>? groupActions;
|
||||||
|
StreamSubscription<List<Contact>>? contacts;
|
||||||
|
StreamSubscription<List<MediaFile>>? media;
|
||||||
|
StreamSubscription<List<Reaction>>? reactions;
|
||||||
|
StreamSubscription<List<MessageAction>>? messageActions;
|
||||||
|
|
||||||
|
void cancelAll() {
|
||||||
|
group?.cancel();
|
||||||
|
messages?.cancel();
|
||||||
|
groupActions?.cancel();
|
||||||
|
contacts?.cancel();
|
||||||
|
media?.cancel();
|
||||||
|
reactions?.cancel();
|
||||||
|
messageActions?.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class ChatMessagesView extends StatefulWidget {
|
class ChatMessagesView extends StatefulWidget {
|
||||||
const ChatMessagesView(this.groupId, {super.key});
|
const ChatMessagesView(this.groupId, {super.key});
|
||||||
|
|
||||||
|
|
@ -43,23 +85,12 @@ class _ChatMessagesViewState extends State<ChatMessagesView>
|
||||||
with WidgetsBindingObserver {
|
with WidgetsBindingObserver {
|
||||||
HashSet<int> alreadyReportedOpened = HashSet<int>();
|
HashSet<int> alreadyReportedOpened = HashSet<int>();
|
||||||
|
|
||||||
bool _hasReceivedFirstMessageBatch = false;
|
final _animationState = _MessageAnimationState();
|
||||||
final HashSet<String> _knownMessageIds = HashSet<String>();
|
final _subscriptions = _ChatSubscriptions();
|
||||||
final HashSet<String> _animateMessageIds = HashSet<String>();
|
final _data = _ChatViewData();
|
||||||
|
|
||||||
StreamSubscription<Group?>? userSub;
|
|
||||||
StreamSubscription<List<Message>>? messageSub;
|
|
||||||
StreamSubscription<List<GroupHistory>>? groupActionsSub;
|
|
||||||
StreamSubscription<List<Contact>>? contactSub;
|
|
||||||
|
|
||||||
Group? _group;
|
Group? _group;
|
||||||
|
List<Contact> _groupContacts = [];
|
||||||
Map<int, Contact> userIdToContact = {};
|
|
||||||
|
|
||||||
List<ChatItem> messages = [];
|
|
||||||
List<Message> allMessages = [];
|
|
||||||
List<GroupHistory> groupActions = [];
|
|
||||||
List<MemoryItem> galleryItems = [];
|
|
||||||
Message? quotesMessage;
|
Message? quotesMessage;
|
||||||
GlobalKey verifyShieldKey = GlobalKey();
|
GlobalKey verifyShieldKey = GlobalKey();
|
||||||
FocusNode? textFieldFocus;
|
FocusNode? textFieldFocus;
|
||||||
|
|
@ -79,10 +110,7 @@ class _ChatMessagesViewState extends State<ChatMessagesView>
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
userSub?.cancel();
|
_subscriptions.cancelAll();
|
||||||
messageSub?.cancel();
|
|
||||||
contactSub?.cancel();
|
|
||||||
groupActionsSub?.cancel();
|
|
||||||
_nextTypingIndicator?.cancel();
|
_nextTypingIndicator?.cancel();
|
||||||
try {
|
try {
|
||||||
textFieldFocus?.dispose();
|
textFieldFocus?.dispose();
|
||||||
|
|
@ -100,43 +128,86 @@ class _ChatMessagesViewState extends State<ChatMessagesView>
|
||||||
|
|
||||||
Future<void> initStreams() async {
|
Future<void> initStreams() async {
|
||||||
final groupStream = twonlyDB.groupsDao.watchGroup(widget.groupId);
|
final groupStream = twonlyDB.groupsDao.watchGroup(widget.groupId);
|
||||||
userSub = groupStream.listen((newGroup) {
|
_subscriptions.group = groupStream.listen((newGroup) {
|
||||||
if (newGroup == null) return;
|
if (newGroup == null) return;
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
_group = newGroup;
|
_group = newGroup;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (groupActionsSub == null) {
|
if (_subscriptions.groupActions == null) {
|
||||||
final actionsStream = twonlyDB.groupsDao.watchGroupActions(
|
final actionsStream = twonlyDB.groupsDao.watchGroupActions(
|
||||||
newGroup.groupId,
|
newGroup.groupId,
|
||||||
);
|
);
|
||||||
groupActionsSub = actionsStream.listen((update) async {
|
_subscriptions.groupActions = actionsStream.listen((update) async {
|
||||||
groupActions = update;
|
_data.groupActions = update;
|
||||||
await setMessages(allMessages, update);
|
await setMessages(_data.allMessages, update);
|
||||||
});
|
});
|
||||||
|
|
||||||
final contactsStream = twonlyDB.contactsDao.watchAllContacts();
|
final contactsStream = twonlyDB.contactsDao.watchAllContacts();
|
||||||
contactSub = contactsStream.listen((contacts) {
|
_subscriptions.contacts = contactsStream.listen((contacts) {
|
||||||
|
final contactMap = <int, Contact>{};
|
||||||
for (final contact in contacts) {
|
for (final contact in contacts) {
|
||||||
userIdToContact[contact.userId] = contact;
|
contactMap[contact.userId] = contact;
|
||||||
}
|
}
|
||||||
|
if (mounted) setState(() => _data.contactsById = contactMap);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
final msgStream = await twonlyDB.messagesDao.watchByGroupId(widget.groupId);
|
final msgStream = await twonlyDB.messagesDao.watchByGroupId(widget.groupId);
|
||||||
messageSub = msgStream.listen((update) async {
|
_subscriptions.messages = msgStream.listen((update) async {
|
||||||
allMessages = update;
|
_data.allMessages = update;
|
||||||
await setMessages(update, groupActions);
|
_data.messagesById = {
|
||||||
_hasReceivedFirstMessageBatch = true;
|
for (final message in update) message.messageId: message,
|
||||||
|
};
|
||||||
|
await setMessages(update, _data.groupActions);
|
||||||
|
_animationState.hasReceivedFirstBatch = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
_subscriptions.media = twonlyDB.mediaFilesDao
|
||||||
|
.watchMediaFilesForGroup(widget.groupId)
|
||||||
|
.listen((mediaFiles) {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(
|
||||||
|
() => _data.mediaFilesById = {
|
||||||
|
for (final mediaFile in mediaFiles) mediaFile.mediaId: mediaFile,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
_subscriptions.reactions = twonlyDB.reactionsDao
|
||||||
|
.watchReactionsForGroup(widget.groupId)
|
||||||
|
.listen((reactions) {
|
||||||
|
if (!mounted) return;
|
||||||
|
final byMessage = <String, List<Reaction>>{};
|
||||||
|
for (final reaction in reactions) {
|
||||||
|
byMessage.putIfAbsent(reaction.messageId, () => []).add(reaction);
|
||||||
|
}
|
||||||
|
setState(() => _data.reactionsByMessageId = byMessage);
|
||||||
|
});
|
||||||
|
_subscriptions.messageActions = twonlyDB.messagesDao
|
||||||
|
.watchMessageActionsForGroup(widget.groupId)
|
||||||
|
.listen((actions) {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(
|
||||||
|
() => _data.ackedMessageIds = actions
|
||||||
|
.where(
|
||||||
|
(action) => action.type == MessageActionType.ackByUserAt,
|
||||||
|
)
|
||||||
|
.map((action) => action.messageId)
|
||||||
|
.toSet(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
final groupContacts = await twonlyDB.groupsDao.getGroupContact(
|
final groupContacts = await twonlyDB.groupsDao.getGroupContact(
|
||||||
widget.groupId,
|
widget.groupId,
|
||||||
);
|
);
|
||||||
if (groupContacts.length == 1) {
|
if (mounted) {
|
||||||
_receiverDeletedAccount = groupContacts.first.accountDeleted;
|
setState(() {
|
||||||
|
_groupContacts = groupContacts;
|
||||||
|
_receiverDeletedAccount =
|
||||||
|
groupContacts.length == 1 && groupContacts.first.accountDeleted;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userService.currentUser.typingIndicators) {
|
if (userService.currentUser.typingIndicators) {
|
||||||
|
|
@ -160,12 +231,12 @@ class _ChatMessagesViewState extends State<ChatMessagesView>
|
||||||
}
|
}
|
||||||
|
|
||||||
for (final msg in newMessages) {
|
for (final msg in newMessages) {
|
||||||
if (_hasReceivedFirstMessageBatch &&
|
if (_animationState.hasReceivedFirstBatch &&
|
||||||
!_knownMessageIds.contains(msg.messageId) &&
|
!_animationState.knownMessageIds.contains(msg.messageId) &&
|
||||||
msg.senderId == null) {
|
msg.senderId == null) {
|
||||||
_animateMessageIds.add(msg.messageId);
|
_animationState.animateMessageIds.add(msg.messageId);
|
||||||
}
|
}
|
||||||
_knownMessageIds.add(msg.messageId);
|
_animationState.knownMessageIds.add(msg.messageId);
|
||||||
}
|
}
|
||||||
|
|
||||||
final chatItems = <ChatItem>[];
|
final chatItems = <ChatItem>[];
|
||||||
|
|
@ -230,13 +301,13 @@ class _ChatMessagesViewState extends State<ChatMessagesView>
|
||||||
}
|
}
|
||||||
|
|
||||||
final wasSentByMe =
|
final wasSentByMe =
|
||||||
_hasReceivedFirstMessageBatch &&
|
_animationState.hasReceivedFirstBatch &&
|
||||||
newMessages.isNotEmpty &&
|
newMessages.isNotEmpty &&
|
||||||
newMessages.last.senderId == null;
|
newMessages.last.senderId == null;
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
messages = chatItems.reversed.toList();
|
_data.chatItems = chatItems.reversed.toList();
|
||||||
});
|
});
|
||||||
|
|
||||||
if (wasSentByMe) {
|
if (wasSentByMe) {
|
||||||
|
|
@ -255,14 +326,20 @@ class _ChatMessagesViewState extends State<ChatMessagesView>
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final galleryMessageIds = storedMediaFiles
|
||||||
|
.map((message) => message.messageId)
|
||||||
|
.toSet();
|
||||||
|
if (setEquals(_data.galleryMessageIds, galleryMessageIds)) return;
|
||||||
final items = await MemoryItem.convertFromMessages(storedMediaFiles);
|
final items = await MemoryItem.convertFromMessages(storedMediaFiles);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
galleryItems = items.values.toList();
|
setState(() {
|
||||||
setState(() {});
|
_data.galleryMessageIds = galleryMessageIds;
|
||||||
|
_data.galleryItems = items.values.toList();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> scrollToMessage(String messageId) async {
|
Future<void> scrollToMessage(String messageId) async {
|
||||||
final index = messages.indexWhere(
|
final index = _data.chatItems.indexWhere(
|
||||||
(x) => x.isMessage && x.message!.messageId == messageId,
|
(x) => x.isMessage && x.message!.messageId == messageId,
|
||||||
);
|
);
|
||||||
if (index == -1) return;
|
if (index == -1) return;
|
||||||
|
|
@ -309,6 +386,7 @@ class _ChatMessagesViewState extends State<ChatMessagesView>
|
||||||
children: [
|
children: [
|
||||||
AvatarIcon(
|
AvatarIcon(
|
||||||
group: group,
|
group: group,
|
||||||
|
contacts: _groupContacts,
|
||||||
fontSize: 19,
|
fontSize: 19,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
|
|
@ -330,7 +408,7 @@ class _ChatMessagesViewState extends State<ChatMessagesView>
|
||||||
group: group,
|
group: group,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
FlameCounterWidget(groupId: group.groupId),
|
FlameCounterWidget(group: group),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (group.isDirectChat)
|
if (group.isDirectChat)
|
||||||
|
|
@ -362,69 +440,84 @@ class _ChatMessagesViewState extends State<ChatMessagesView>
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Align(
|
child: Align(
|
||||||
alignment: Alignment.topCenter,
|
alignment: Alignment.topCenter,
|
||||||
child: ScrollablePositionedList.builder(
|
child: ChatMessageActionScope(
|
||||||
shrinkWrap: true,
|
ackedMessageIds: _data.ackedMessageIds,
|
||||||
reverse: true,
|
child: ScrollablePositionedList.builder(
|
||||||
itemCount: messages.length + 1 + 1,
|
reverse: true,
|
||||||
itemScrollController: itemScrollController,
|
itemCount: _data.chatItems.length + 1 + 1,
|
||||||
itemBuilder: (context, i) {
|
itemScrollController: itemScrollController,
|
||||||
if (i == 0) {
|
itemBuilder: (context, i) {
|
||||||
return userService.currentUser.typingIndicators
|
if (i == 0) {
|
||||||
? TypingIndicator(group: group)
|
return userService.currentUser.typingIndicators
|
||||||
: Container();
|
? TypingIndicator(group: group)
|
||||||
}
|
: Container();
|
||||||
i -= 1;
|
}
|
||||||
if (i == messages.length) {
|
i -= 1;
|
||||||
return Padding(
|
if (i == _data.chatItems.length) {
|
||||||
key: Key('overview_${group.groupId}'),
|
return Padding(
|
||||||
padding: const EdgeInsets.only(top: 10),
|
key: Key('overview_${group.groupId}'),
|
||||||
child: InChatGroupOverview(
|
padding: const EdgeInsets.only(top: 10),
|
||||||
group: group,
|
child: InChatGroupOverview(
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (messages[i].isDate) {
|
|
||||||
return ChatDateChip(
|
|
||||||
item: messages[i],
|
|
||||||
);
|
|
||||||
} else if (messages[i].isGroupAction) {
|
|
||||||
return ChatGroupAction(
|
|
||||||
key: Key(messages[i].groupAction!.groupHistoryId),
|
|
||||||
action: messages[i].groupAction!,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
final chatMessage = messages[i].message!;
|
|
||||||
return BlinkWidget(
|
|
||||||
key: Key('blink_${chatMessage.messageId}'),
|
|
||||||
enabled: focusedScrollItem == i,
|
|
||||||
child: AnimatedNewMessage(
|
|
||||||
key: Key('anim_${chatMessage.messageId}'),
|
|
||||||
messageId: chatMessage.messageId,
|
|
||||||
animateIds: _animateMessageIds,
|
|
||||||
child: ChatListEntry(
|
|
||||||
key: Key(chatMessage.messageId),
|
|
||||||
message: messages[i].message!,
|
|
||||||
nextMessage: (i > 0)
|
|
||||||
? messages[i - 1].message
|
|
||||||
: null,
|
|
||||||
prevMessage: ((i + 1) < messages.length)
|
|
||||||
? messages[i + 1].message
|
|
||||||
: null,
|
|
||||||
group: group,
|
group: group,
|
||||||
galleryItems: galleryItems,
|
|
||||||
userIdToContact: userIdToContact,
|
|
||||||
scrollToMessage: scrollToMessage,
|
|
||||||
onResponseTriggered: () {
|
|
||||||
setState(() {
|
|
||||||
quotesMessage = chatMessage;
|
|
||||||
});
|
|
||||||
textFieldFocus?.requestFocus();
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
}
|
||||||
}
|
if (_data.chatItems[i].isDate) {
|
||||||
},
|
return ChatDateChip(
|
||||||
|
item: _data.chatItems[i],
|
||||||
|
);
|
||||||
|
} else if (_data.chatItems[i].isGroupAction) {
|
||||||
|
return ChatGroupAction(
|
||||||
|
key: Key(
|
||||||
|
_data.chatItems[i].groupAction!.groupHistoryId,
|
||||||
|
),
|
||||||
|
action: _data.chatItems[i].groupAction!,
|
||||||
|
contactsById: _data.contactsById,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
final chatMessage = _data.chatItems[i].message!;
|
||||||
|
return BlinkWidget(
|
||||||
|
key: Key('blink_${chatMessage.messageId}'),
|
||||||
|
enabled: focusedScrollItem == i,
|
||||||
|
child: AnimatedNewMessage(
|
||||||
|
key: Key('anim_${chatMessage.messageId}'),
|
||||||
|
messageId: chatMessage.messageId,
|
||||||
|
animateIds: _animationState.animateMessageIds,
|
||||||
|
child: ChatListEntry(
|
||||||
|
key: Key(chatMessage.messageId),
|
||||||
|
message: _data.chatItems[i].message!,
|
||||||
|
nextMessage: (i > 0)
|
||||||
|
? _data.chatItems[i - 1].message
|
||||||
|
: null,
|
||||||
|
prevMessage: ((i + 1) < _data.chatItems.length)
|
||||||
|
? _data.chatItems[i + 1].message
|
||||||
|
: null,
|
||||||
|
group: group,
|
||||||
|
galleryItems: _data.galleryItems,
|
||||||
|
userIdToContact: _data.contactsById,
|
||||||
|
mediaFile: chatMessage.mediaId == null
|
||||||
|
? null
|
||||||
|
: _data.mediaFilesById[chatMessage.mediaId],
|
||||||
|
reactions:
|
||||||
|
_data.reactionsByMessageId[chatMessage
|
||||||
|
.messageId] ??
|
||||||
|
const [],
|
||||||
|
messagesById: _data.messagesById,
|
||||||
|
mediaFilesById: _data.mediaFilesById,
|
||||||
|
useSharedData: true,
|
||||||
|
scrollToMessage: scrollToMessage,
|
||||||
|
onResponseTriggered: () {
|
||||||
|
setState(() {
|
||||||
|
quotesMessage = chatMessage;
|
||||||
|
});
|
||||||
|
textFieldFocus?.requestFocus();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,12 @@ import 'package:twonly/src/utils/misc.dart';
|
||||||
class ChatGroupAction extends StatefulWidget {
|
class ChatGroupAction extends StatefulWidget {
|
||||||
const ChatGroupAction({
|
const ChatGroupAction({
|
||||||
required this.action,
|
required this.action,
|
||||||
|
this.contactsById,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
final GroupHistory action;
|
final GroupHistory action;
|
||||||
|
final Map<int, Contact>? contactsById;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ChatGroupAction> createState() => _ChatGroupActionState();
|
State<ChatGroupAction> createState() => _ChatGroupActionState();
|
||||||
|
|
@ -25,7 +27,29 @@ class _ChatGroupActionState extends State<ChatGroupAction> {
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
initAsync();
|
if (widget.contactsById == null) {
|
||||||
|
initAsync();
|
||||||
|
} else {
|
||||||
|
_applyContacts();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(ChatGroupAction oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (widget.contactsById != null &&
|
||||||
|
widget.contactsById != oldWidget.contactsById) {
|
||||||
|
_applyContacts();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _applyContacts() {
|
||||||
|
contact = widget.action.contactId == null
|
||||||
|
? null
|
||||||
|
: widget.contactsById?[widget.action.contactId];
|
||||||
|
affectedContact = widget.action.affectedContactId == null
|
||||||
|
? null
|
||||||
|
: widget.contactsById?[widget.action.affectedContactId];
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> initAsync() async {
|
Future<void> initAsync() async {
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,11 @@ class ChatListEntry extends StatefulWidget {
|
||||||
this.nextMessage,
|
this.nextMessage,
|
||||||
this.userIdToContact,
|
this.userIdToContact,
|
||||||
this.hideReactions = false,
|
this.hideReactions = false,
|
||||||
|
this.mediaFile,
|
||||||
|
this.reactions,
|
||||||
|
this.messagesById,
|
||||||
|
this.mediaFilesById,
|
||||||
|
this.useSharedData = false,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
final Message? prevMessage;
|
final Message? prevMessage;
|
||||||
|
|
@ -45,6 +50,11 @@ class ChatListEntry extends StatefulWidget {
|
||||||
final Group group;
|
final Group group;
|
||||||
final Map<int, Contact>? userIdToContact;
|
final Map<int, Contact>? userIdToContact;
|
||||||
final bool hideReactions;
|
final bool hideReactions;
|
||||||
|
final MediaFile? mediaFile;
|
||||||
|
final List<Reaction>? reactions;
|
||||||
|
final Map<String, Message>? messagesById;
|
||||||
|
final Map<String, MediaFile>? mediaFilesById;
|
||||||
|
final bool useSharedData;
|
||||||
final List<MemoryItem> galleryItems;
|
final List<MemoryItem> galleryItems;
|
||||||
final void Function(String)? scrollToMessage;
|
final void Function(String)? scrollToMessage;
|
||||||
final void Function()? onResponseTriggered;
|
final void Function()? onResponseTriggered;
|
||||||
|
|
@ -64,9 +74,24 @@ class _ChatListEntryState extends State<ChatListEntry> {
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_applySharedData();
|
||||||
initAsync();
|
initAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(ChatListEntry oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (widget.useSharedData) _applySharedData();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _applySharedData() {
|
||||||
|
if (!widget.useSharedData) return;
|
||||||
|
reactions = widget.reactions ?? const [];
|
||||||
|
mediaService = widget.mediaFile == null
|
||||||
|
? null
|
||||||
|
: MediaFileService(widget.mediaFile!);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
mediaFileSub?.cancel();
|
mediaFileSub?.cancel();
|
||||||
|
|
@ -75,6 +100,7 @@ class _ChatListEntryState extends State<ChatListEntry> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> initAsync() async {
|
Future<void> initAsync() async {
|
||||||
|
if (widget.useSharedData) return;
|
||||||
if (widget.message.mediaId != null) {
|
if (widget.message.mediaId != null) {
|
||||||
final mediaFileStream = twonlyDB.mediaFilesDao.watchMedia(
|
final mediaFileStream = twonlyDB.mediaFilesDao.watchMedia(
|
||||||
widget.message.mediaId!,
|
widget.message.mediaId!,
|
||||||
|
|
@ -131,6 +157,7 @@ class _ChatListEntryState extends State<ChatListEntry> {
|
||||||
group: widget.group,
|
group: widget.group,
|
||||||
mediaService: mediaService!,
|
mediaService: mediaService!,
|
||||||
galleryItems: widget.galleryItems,
|
galleryItems: widget.galleryItems,
|
||||||
|
useSharedData: widget.useSharedData,
|
||||||
borderRadius: borderRadius,
|
borderRadius: borderRadius,
|
||||||
info: info,
|
info: info,
|
||||||
);
|
);
|
||||||
|
|
@ -225,6 +252,15 @@ class _ChatListEntryState extends State<ChatListEntry> {
|
||||||
mediaService: mediaService,
|
mediaService: mediaService,
|
||||||
borderRadius: borderRadius,
|
borderRadius: borderRadius,
|
||||||
scrollToMessage: widget.scrollToMessage,
|
scrollToMessage: widget.scrollToMessage,
|
||||||
|
quotedMessage: widget.msgQuote(
|
||||||
|
messagesById: widget.messagesById,
|
||||||
|
),
|
||||||
|
quotedMediaFile: widget.msgQuoteMedia(
|
||||||
|
messagesById: widget.messagesById,
|
||||||
|
mediaFilesById: widget.mediaFilesById,
|
||||||
|
),
|
||||||
|
contactsById: widget.userIdToContact,
|
||||||
|
useSharedData: widget.useSharedData,
|
||||||
child: _getChatEntry(borderRadius, reactionsForWidth, info),
|
child: _getChatEntry(borderRadius, reactionsForWidth, info),
|
||||||
),
|
),
|
||||||
if (reactionsForWidth > 0) const SizedBox(height: 20, width: 10),
|
if (reactionsForWidth > 0) const SizedBox(height: 20, width: 10),
|
||||||
|
|
@ -281,6 +317,10 @@ class _ChatListEntryState extends State<ChatListEntry> {
|
||||||
),
|
),
|
||||||
child: AvatarIcon(
|
child: AvatarIcon(
|
||||||
contactId: widget.message.senderId,
|
contactId: widget.message.senderId,
|
||||||
|
contacts:
|
||||||
|
widget.userIdToContact?[widget.message.senderId] == null
|
||||||
|
? null
|
||||||
|
: [widget.userIdToContact![widget.message.senderId]!],
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -292,6 +332,21 @@ class _ChatListEntryState extends State<ChatListEntry> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension on ChatListEntry {
|
||||||
|
Message? msgQuote({Map<String, Message>? messagesById}) {
|
||||||
|
final quotedId = message.quotesMessageId;
|
||||||
|
return quotedId == null ? null : messagesById?[quotedId];
|
||||||
|
}
|
||||||
|
|
||||||
|
MediaFile? msgQuoteMedia({
|
||||||
|
Map<String, Message>? messagesById,
|
||||||
|
Map<String, MediaFile>? mediaFilesById,
|
||||||
|
}) {
|
||||||
|
final quoted = msgQuote(messagesById: messagesById);
|
||||||
|
return quoted?.mediaId == null ? null : mediaFilesById?[quoted!.mediaId];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
(EdgeInsetsGeometry, BorderRadius, bool) getMessageLayout(
|
(EdgeInsetsGeometry, BorderRadius, bool) getMessageLayout(
|
||||||
Message message,
|
Message message,
|
||||||
Message? prevMessage,
|
Message? prevMessage,
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ class ChatMediaEntry extends StatefulWidget {
|
||||||
required this.mediaService,
|
required this.mediaService,
|
||||||
required this.borderRadius,
|
required this.borderRadius,
|
||||||
required this.info,
|
required this.info,
|
||||||
|
this.useSharedData = false,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -38,6 +39,7 @@ class ChatMediaEntry extends StatefulWidget {
|
||||||
final MediaFileService mediaService;
|
final MediaFileService mediaService;
|
||||||
final BorderRadius borderRadius;
|
final BorderRadius borderRadius;
|
||||||
final BubbleInfo info;
|
final BubbleInfo info;
|
||||||
|
final bool useSharedData;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ChatMediaEntry> createState() => _ChatMediaEntryState();
|
State<ChatMediaEntry> createState() => _ChatMediaEntryState();
|
||||||
|
|
@ -179,6 +181,7 @@ class _ChatMediaEntryState extends State<ChatMediaEntry> {
|
||||||
canBeReopened: _canBeReopened,
|
canBeReopened: _canBeReopened,
|
||||||
borderRadius: imageBorderRadius,
|
borderRadius: imageBorderRadius,
|
||||||
info: widget.info,
|
info: widget.info,
|
||||||
|
useSharedData: widget.useSharedData,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ class FriendlyMessageTime extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final statusIcon = _buildStatusIcon(Colors.grey.shade400);
|
final statusIcon = _buildStatusIcon(context, Colors.grey.shade400);
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(left: 6),
|
padding: const EdgeInsets.only(left: 6),
|
||||||
|
|
@ -59,7 +59,7 @@ class FriendlyMessageTime extends StatelessWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget? _buildStatusIcon(Color iconColor) {
|
Widget? _buildStatusIcon(BuildContext context, Color iconColor) {
|
||||||
if (message.type != MessageType.text.name || message.senderId != null) {
|
if (message.type != MessageType.text.name || message.senderId != null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -82,7 +82,14 @@ class FriendlyMessageTime extends StatelessWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now check message actions for ackByUserAt
|
final sharedAckState = ChatMessageActionScope.maybeOf(context);
|
||||||
|
if (sharedAckState != null) {
|
||||||
|
return _ackIcon(
|
||||||
|
iconColor,
|
||||||
|
sharedAckState.ackedMessageIds.contains(message.messageId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return StreamBuilder<List<(MessageAction, Contact)>>(
|
return StreamBuilder<List<(MessageAction, Contact)>>(
|
||||||
stream: twonlyDB.messagesDao.watchMessageActions(message.messageId),
|
stream: twonlyDB.messagesDao.watchMessageActions(message.messageId),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
|
|
@ -91,28 +98,36 @@ class FriendlyMessageTime extends StatelessWidget {
|
||||||
(t) => t.$1.type == MessageActionType.ackByUserAt,
|
(t) => t.$1.type == MessageActionType.ackByUserAt,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (hasAckByUser) {
|
return _ackIcon(iconColor, hasAckByUser);
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.only(left: 4),
|
|
||||||
child: FaIcon(
|
|
||||||
FontAwesomeIcons.checkDouble,
|
|
||||||
size: 8,
|
|
||||||
color: iconColor,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.only(left: 4),
|
|
||||||
child: FaIcon(
|
|
||||||
FontAwesomeIcons.check,
|
|
||||||
size: 8,
|
|
||||||
color: iconColor,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _ackIcon(Color iconColor, bool acknowledged) => Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 4),
|
||||||
|
child: FaIcon(
|
||||||
|
acknowledged ? FontAwesomeIcons.checkDouble : FontAwesomeIcons.check,
|
||||||
|
size: 8,
|
||||||
|
color: iconColor,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChatMessageActionScope extends InheritedWidget {
|
||||||
|
const ChatMessageActionScope({
|
||||||
|
required this.ackedMessageIds,
|
||||||
|
required super.child,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Set<String> ackedMessageIds;
|
||||||
|
|
||||||
|
static ChatMessageActionScope? maybeOf(BuildContext context) =>
|
||||||
|
context.dependOnInheritedWidgetOfExactType<ChatMessageActionScope>();
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool updateShouldNotify(ChatMessageActionScope oldWidget) =>
|
||||||
|
ackedMessageIds != oldWidget.ackedMessageIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
String friendlyTime(BuildContext context, DateTime dt) {
|
String friendlyTime(BuildContext context, DateTime dt) {
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ class InChatMediaViewer extends StatefulWidget {
|
||||||
required this.canBeReopened,
|
required this.canBeReopened,
|
||||||
required this.borderRadius,
|
required this.borderRadius,
|
||||||
required this.info,
|
required this.info,
|
||||||
|
this.useSharedData = false,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -33,6 +34,7 @@ class InChatMediaViewer extends StatefulWidget {
|
||||||
final bool canBeReopened;
|
final bool canBeReopened;
|
||||||
final BorderRadius borderRadius;
|
final BorderRadius borderRadius;
|
||||||
final BubbleInfo info;
|
final BubbleInfo info;
|
||||||
|
final bool useSharedData;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<InChatMediaViewer> createState() => _InChatMediaViewerState();
|
State<InChatMediaViewer> createState() => _InChatMediaViewerState();
|
||||||
|
|
@ -42,7 +44,6 @@ class _InChatMediaViewerState extends State<InChatMediaViewer> {
|
||||||
bool mirrorVideo = false;
|
bool mirrorVideo = false;
|
||||||
int? galleryItemIndex;
|
int? galleryItemIndex;
|
||||||
StreamSubscription<Message?>? messageStream;
|
StreamSubscription<Message?>? messageStream;
|
||||||
Timer? _timer;
|
|
||||||
late final ValueNotifier<String?> _activeMediaIdNotifier = ValueNotifier(
|
late final ValueNotifier<String?> _activeMediaIdNotifier = ValueNotifier(
|
||||||
widget.message.mediaId,
|
widget.message.mediaId,
|
||||||
);
|
);
|
||||||
|
|
@ -50,33 +51,23 @@ class _InChatMediaViewerState extends State<InChatMediaViewer> {
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
unawaited(loadIndexAsync());
|
loadIndex();
|
||||||
unawaited(initStream());
|
if (!widget.useSharedData) unawaited(initStream());
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didUpdateWidget(InChatMediaViewer oldWidget) {
|
void didUpdateWidget(InChatMediaViewer oldWidget) {
|
||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (widget.message.mediaId != oldWidget.message.mediaId) {
|
||||||
|
_activeMediaIdNotifier.value = widget.message.mediaId;
|
||||||
|
}
|
||||||
if (widget.message.mediaStored != oldWidget.message.mediaStored ||
|
if (widget.message.mediaStored != oldWidget.message.mediaStored ||
|
||||||
widget.galleryItems != oldWidget.galleryItems) {
|
widget.galleryItems != oldWidget.galleryItems) {
|
||||||
if (widget.message.mediaStored) {
|
galleryItemIndex = null;
|
||||||
unawaited(loadIndexAsync());
|
loadIndex();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> loadIndexAsync() async {
|
|
||||||
_timer?.cancel();
|
|
||||||
_timer = Timer.periodic(const Duration(milliseconds: 10), (timer) {
|
|
||||||
/// when the galleryItems are updated this widget is not reloaded
|
|
||||||
/// so using this timer as a workaround
|
|
||||||
if (loadIndex()) {
|
|
||||||
timer.cancel();
|
|
||||||
if (mounted) setState(() {});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
bool loadIndex() {
|
bool loadIndex() {
|
||||||
if (widget.message.mediaStored) {
|
if (widget.message.mediaStored) {
|
||||||
final index = widget.galleryItems.indexWhere(
|
final index = widget.galleryItems.indexWhere(
|
||||||
|
|
@ -93,7 +84,6 @@ class _InChatMediaViewerState extends State<InChatMediaViewer> {
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
messageStream?.cancel();
|
messageStream?.cancel();
|
||||||
_timer?.cancel();
|
|
||||||
_activeMediaIdNotifier.dispose();
|
_activeMediaIdNotifier.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
@ -112,7 +102,7 @@ class _InChatMediaViewerState extends State<InChatMediaViewer> {
|
||||||
if (updated != null) {
|
if (updated != null) {
|
||||||
if (updated.mediaStored) {
|
if (updated.mediaStored) {
|
||||||
await messageStream?.cancel();
|
await messageStream?.cancel();
|
||||||
await loadIndexAsync();
|
if (loadIndex() && mounted) setState(() {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,10 @@ class ResponseContainer extends StatelessWidget {
|
||||||
required this.mediaService,
|
required this.mediaService,
|
||||||
required this.borderRadius,
|
required this.borderRadius,
|
||||||
this.scrollToMessage,
|
this.scrollToMessage,
|
||||||
|
this.quotedMessage,
|
||||||
|
this.quotedMediaFile,
|
||||||
|
this.contactsById,
|
||||||
|
this.useSharedData = false,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -26,6 +30,10 @@ class ResponseContainer extends StatelessWidget {
|
||||||
final MediaFileService? mediaService;
|
final MediaFileService? mediaService;
|
||||||
final BorderRadius borderRadius;
|
final BorderRadius borderRadius;
|
||||||
final void Function(String)? scrollToMessage;
|
final void Function(String)? scrollToMessage;
|
||||||
|
final Message? quotedMessage;
|
||||||
|
final MediaFile? quotedMediaFile;
|
||||||
|
final Map<int, Contact>? contactsById;
|
||||||
|
final bool useSharedData;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
@ -67,6 +75,12 @@ class ResponseContainer extends StatelessWidget {
|
||||||
child: ResponsePreview(
|
child: ResponsePreview(
|
||||||
group: group,
|
group: group,
|
||||||
messageId: msg.quotesMessageId,
|
messageId: msg.quotesMessageId,
|
||||||
|
message: quotedMessage,
|
||||||
|
mediaFile: quotedMediaFile,
|
||||||
|
contact: quotedMessage?.senderId == null
|
||||||
|
? null
|
||||||
|
: contactsById?[quotedMessage!.senderId],
|
||||||
|
useSharedData: useSharedData,
|
||||||
showBorder: false,
|
showBorder: false,
|
||||||
showLeftBorder: false,
|
showLeftBorder: false,
|
||||||
),
|
),
|
||||||
|
|
@ -89,6 +103,9 @@ class ResponsePreview extends StatefulWidget {
|
||||||
this.messageId,
|
this.messageId,
|
||||||
this.showLeftBorder = true,
|
this.showLeftBorder = true,
|
||||||
this.colorUsername = false,
|
this.colorUsername = false,
|
||||||
|
this.mediaFile,
|
||||||
|
this.contact,
|
||||||
|
this.useSharedData = false,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -98,6 +115,9 @@ class ResponsePreview extends StatefulWidget {
|
||||||
final bool showBorder;
|
final bool showBorder;
|
||||||
final bool showLeftBorder;
|
final bool showLeftBorder;
|
||||||
final bool colorUsername;
|
final bool colorUsername;
|
||||||
|
final MediaFile? mediaFile;
|
||||||
|
final Contact? contact;
|
||||||
|
final bool useSharedData;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ResponsePreview> createState() => _ResponsePreviewState();
|
State<ResponsePreview> createState() => _ResponsePreviewState();
|
||||||
|
|
@ -112,17 +132,30 @@ class _ResponsePreviewState extends State<ResponsePreview> {
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_message = widget.message;
|
_message = widget.message;
|
||||||
initAsync();
|
if (widget.mediaFile != null) {
|
||||||
|
_mediaService = MediaFileService(widget.mediaFile!);
|
||||||
|
}
|
||||||
|
if (widget.contact != null) {
|
||||||
|
_username = getContactDisplayName(widget.contact!);
|
||||||
|
}
|
||||||
|
if (!widget.useSharedData &&
|
||||||
|
(widget.message == null ||
|
||||||
|
(widget.message?.mediaId != null && widget.mediaFile == null) ||
|
||||||
|
(widget.message?.senderId != null && widget.contact == null))) {
|
||||||
|
initAsync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> initAsync() async {
|
Future<void> initAsync() async {
|
||||||
_message ??= await twonlyDB.messagesDao
|
if (_message == null && widget.messageId != null) {
|
||||||
.getMessageById(widget.messageId!)
|
_message = await twonlyDB.messagesDao
|
||||||
.getSingleOrNull();
|
.getMessageById(widget.messageId!)
|
||||||
if (_message?.mediaId != null) {
|
.getSingleOrNull();
|
||||||
|
}
|
||||||
|
if (_message?.mediaId != null && _mediaService == null) {
|
||||||
_mediaService = await MediaFileService.fromMediaId(_message!.mediaId!);
|
_mediaService = await MediaFileService.fromMediaId(_message!.mediaId!);
|
||||||
}
|
}
|
||||||
if (_message?.senderId != null) {
|
if (_message?.senderId != null && _username.isEmpty) {
|
||||||
final contact = await twonlyDB.contactsDao
|
final contact = await twonlyDB.contactsDao
|
||||||
.getContactByUserId(_message!.senderId!)
|
.getContactByUserId(_message!.senderId!)
|
||||||
.getSingleOrNull();
|
.getSingleOrNull();
|
||||||
|
|
|
||||||
3
rust/.env
Normal file
3
rust/.env
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Local schema database used by SQLx query macros and rust-analyzer.
|
||||||
|
# Rebuild it after changing a migration with: ./scripts/prepare_sqlx.sh
|
||||||
|
DATABASE_URL=sqlite://sqlx-dev.sqlite
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "\n SELECT identity_key\n FROM signal_identities\n WHERE name = ?\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "identity_key",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Blob",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "signal_identities",
|
||||||
|
"name": "identity_key"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "08ec1e1198b16f3bb313b4c2c5a92bd8ca3251f0aac8d8e2748a7617c53afc65"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "\n INSERT INTO user_discovery_user_relations (\n announced_user_id,\n from_contact_id,\n public_key_verified_timestamp\n ) VALUES (?, ?, ?)\n ON CONFLICT(announced_user_id, from_contact_id) DO UPDATE SET\n public_key_verified_timestamp = excluded.public_key_verified_timestamp\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 3
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "1be4155879419ae7cdcd7f8774ea5a6cce7ecb05e825095f4fffd6aaf4288d18"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "\n SELECT promotion\n FROM user_discovery_own_promotions\n WHERE version_id > ?\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "promotion",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Blob",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "user_discovery_own_promotions",
|
||||||
|
"name": "promotion"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "1bf6e82ae32eff099bd28dc8e577247ebef69141bd7c6b887b5b11ed724e11b3"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,86 @@
|
||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "\n SELECT\n promotion_id,\n public_id,\n from_contact_id,\n threshold,\n announcement_share,\n public_key_verified_timestamp\n FROM user_discovery_other_promotions\n WHERE public_id = ?\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "promotion_id",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Integer",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "user_discovery_other_promotions",
|
||||||
|
"name": "promotion_id"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "public_id",
|
||||||
|
"ordinal": 1,
|
||||||
|
"type_info": "Integer",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "user_discovery_other_promotions",
|
||||||
|
"name": "public_id"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "from_contact_id",
|
||||||
|
"ordinal": 2,
|
||||||
|
"type_info": "Integer",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "user_discovery_other_promotions",
|
||||||
|
"name": "from_contact_id"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "threshold",
|
||||||
|
"ordinal": 3,
|
||||||
|
"type_info": "Integer",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "user_discovery_other_promotions",
|
||||||
|
"name": "threshold"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "announcement_share",
|
||||||
|
"ordinal": 4,
|
||||||
|
"type_info": "Blob",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "user_discovery_other_promotions",
|
||||||
|
"name": "announcement_share"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "public_key_verified_timestamp",
|
||||||
|
"ordinal": 5,
|
||||||
|
"type_info": "Integer",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "user_discovery_other_promotions",
|
||||||
|
"name": "public_key_verified_timestamp"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "229b83c3c0777a900e71c71d83920a2b93f5df34f29921263f0c960fe0a34ceb"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "\n UPDATE contacts\n SET user_discovery_version = ?\n WHERE user_id = ?\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 2
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "3462ce3b677445a641de96634d6f69a661f2b58cbfff84eac55eaec901a54284"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "INSERT INTO user_discovery_shares (share) VALUES (?)",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "385a7075d70cf7ce1194c6ee5757d2ab4145f9bb2d1f65512f17dca5a0e2ed15"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "\n UPDATE user_discovery_own_promotions\n SET promotion = X''\n WHERE contact_id = ?\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "5a7ba59111d6353f7a18d3f8eb8fa1ae3a491f080a4735361e9aaa41ccaa7d51"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "\n UPDATE user_discovery_shares\n SET contact_id = ?\n WHERE share_id = ?\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 2
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "629c9b14b6e65f3209752e66b4f7709d252feae82dd009ce628274ed9b9c680a"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "\n SELECT user_discovery_version\n FROM contacts\n WHERE user_id = ?\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "user_discovery_version",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Blob",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "contacts",
|
||||||
|
"name": "user_discovery_version"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
true
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "70f9d68d251f1c354c61584a84f509682e341595e0d98cb880c50ec80f11f02a"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "\n SELECT promotion\n FROM user_discovery_own_promotions\n WHERE contact_id = ?\n ORDER BY version_id DESC\n LIMIT 1\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "promotion",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Blob",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "user_discovery_own_promotions",
|
||||||
|
"name": "promotion"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "9d236335cde01fe13d6836309e7a4bcb8485282d2230d2504350642ce1236169"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "\n SELECT\n announced_user_id,\n announced_public_key,\n public_id\n FROM user_discovery_announced_users\n WHERE public_id = ?\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "announced_user_id",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Integer",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "user_discovery_announced_users",
|
||||||
|
"name": "announced_user_id"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "announced_public_key",
|
||||||
|
"ordinal": 1,
|
||||||
|
"type_info": "Blob",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "user_discovery_announced_users",
|
||||||
|
"name": "announced_public_key"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "public_id",
|
||||||
|
"ordinal": 2,
|
||||||
|
"type_info": "Integer",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "user_discovery_announced_users",
|
||||||
|
"name": "public_id"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "9f78cc93c1680c63c1a258ca2ce1de0ea43bf3a57382b0505828d34c562f96a6"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "\n SELECT share_id, share\n FROM user_discovery_shares\n WHERE contact_id IS NULL\n LIMIT 1\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "share_id",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Integer",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "user_discovery_shares",
|
||||||
|
"name": "share_id"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "share",
|
||||||
|
"ordinal": 1,
|
||||||
|
"type_info": "Blob",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "user_discovery_shares",
|
||||||
|
"name": "share"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 0
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "bd4750be71cfd64ab00d5a84d28f9df5cca1e61750a186328c65d384cfcefa15"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "\n INSERT INTO user_discovery_other_promotions (\n from_contact_id,\n promotion_id,\n public_id,\n threshold,\n announcement_share,\n public_key_verified_timestamp\n ) VALUES (?, ?, ?, ?, ?, ?)\n ON CONFLICT(from_contact_id, public_id) DO UPDATE SET\n promotion_id = excluded.promotion_id,\n threshold = excluded.threshold,\n announcement_share = excluded.announcement_share,\n public_key_verified_timestamp = excluded.public_key_verified_timestamp\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 6
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "ced6f5964217225fb749bde7641da1f7bf365a53a4d06aa2696a1d24c2b398f7"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "DELETE FROM user_discovery_shares",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 0
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "d36767ac50047037ce2122bd59b84c94407918776b0ad2b8c47a1bfb041b620b"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "\n INSERT INTO user_discovery_announced_users (\n announced_user_id,\n announced_public_key,\n public_id\n ) VALUES (?, ?, ?)\n ON CONFLICT DO UPDATE SET\n announced_user_id = excluded.announced_user_id,\n announced_public_key = excluded.announced_public_key,\n public_id = excluded.public_id\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 3
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "d8a516dd12b7f5d49807b1b5c6970f0e004f9c39a23577473d348ebb6e2b044a"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "\n INSERT INTO user_discovery_own_promotions (\n contact_id,\n promotion\n ) VALUES (?, ?)\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 2
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "e026e8c5eb30ca2f7c98c0d4e8f9ccdf388bf7e98f4336ada2ac09224461082f"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "\n SELECT share\n FROM user_discovery_shares\n WHERE contact_id = ?\n LIMIT 1\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "share",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Blob",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "user_discovery_shares",
|
||||||
|
"name": "share"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "f939d4c2915e167dbcbac32fe36c5caa4719f1a75693e6e2b6bb804ea9f790ed"
|
||||||
|
}
|
||||||
21
rust/scripts/prepare_sqlx.sh
Executable file
21
rust/scripts/prepare_sqlx.sh
Executable file
|
|
@ -0,0 +1,21 @@
|
||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||||
|
rust_dir=$(dirname -- "$script_dir")
|
||||||
|
database_path="$rust_dir/sqlx-dev.sqlite"
|
||||||
|
|
||||||
|
rm -f -- "$database_path"
|
||||||
|
|
||||||
|
for migration in "$rust_dir"/src/database/app/migrations/*.sql; do
|
||||||
|
sqlite3 "$database_path" ".read $migration"
|
||||||
|
done
|
||||||
|
|
||||||
|
for migration in "$rust_dir"/src/database/signal/migrations/*.sql; do
|
||||||
|
sqlite3 "$database_path" ".read $migration"
|
||||||
|
done
|
||||||
|
|
||||||
|
cd "$rust_dir"
|
||||||
|
cargo sqlx prepare -- --lib
|
||||||
|
|
||||||
|
echo "Prepared SQLx schema database and offline query metadata."
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
use crate::context::Context;
|
use crate::context::Context;
|
||||||
use crate::database::Database;
|
use crate::database::app::APP_DATABASE_FILE;
|
||||||
|
use crate::database::signal::Database;
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use crate::keys::{DatabaseKey, KeyManager};
|
use crate::keys::{DatabaseKey, KeyManager};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
use std::collections::BTreeMap;
|
||||||
use std::fs::{remove_file, File};
|
use std::fs::{remove_file, File};
|
||||||
use std::io::{copy, Cursor};
|
use std::io::{copy, Cursor};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
@ -12,6 +16,23 @@ use zip::{CompressionMethod, ZipArchive, ZipWriter};
|
||||||
|
|
||||||
pub(crate) struct BackupArchive {}
|
pub(crate) struct BackupArchive {}
|
||||||
|
|
||||||
|
const BACKUP_MANIFEST_FILE: &str = "backup-manifest.json";
|
||||||
|
const BACKUP_FORMAT_VERSION: u32 = 2;
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
struct BackupManifest {
|
||||||
|
format_version: u32,
|
||||||
|
drift_schema_version: u32,
|
||||||
|
app_schema_version: i64,
|
||||||
|
files: BTreeMap<String, BackupManifestFile>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
struct BackupManifestFile {
|
||||||
|
size: u64,
|
||||||
|
sha256: String,
|
||||||
|
}
|
||||||
|
|
||||||
impl BackupArchive {
|
impl BackupArchive {
|
||||||
#[allow(clippy::type_complexity)]
|
#[allow(clippy::type_complexity)]
|
||||||
fn get_backup_files(
|
fn get_backup_files(
|
||||||
|
|
@ -22,10 +43,17 @@ impl BackupArchive {
|
||||||
let database_dir = PathBuf::from(&config.database_dir);
|
let database_dir = PathBuf::from(&config.database_dir);
|
||||||
let data_dir = PathBuf::from(&config.data_dir);
|
let data_dir = PathBuf::from(&config.data_dir);
|
||||||
let rust_db_key = keys.main_key.get_database_key(DatabaseKey::RustDb);
|
let rust_db_key = keys.main_key.get_database_key(DatabaseKey::RustDb);
|
||||||
|
let app_db_key = keys.main_key.get_database_key(DatabaseKey::AppDb);
|
||||||
|
|
||||||
Ok(vec![
|
Ok(vec![
|
||||||
("twonly.sqlite", database_dir.clone(), true, None),
|
("twonly.sqlite", database_dir.clone(), true, None),
|
||||||
("rust_db.sqlite", database_dir, true, Some(rust_db_key)),
|
(
|
||||||
|
"rust_db.sqlite",
|
||||||
|
database_dir.clone(),
|
||||||
|
true,
|
||||||
|
Some(rust_db_key),
|
||||||
|
),
|
||||||
|
(APP_DATABASE_FILE, database_dir, true, Some(app_db_key)),
|
||||||
("user_discovery_config.json", data_dir.clone(), false, None),
|
("user_discovery_config.json", data_dir.clone(), false, None),
|
||||||
("user.json", data_dir.join("keyvalue"), false, None),
|
("user.json", data_dir.join("keyvalue"), false, None),
|
||||||
])
|
])
|
||||||
|
|
@ -56,6 +84,30 @@ impl BackupArchive {
|
||||||
}
|
}
|
||||||
|
|
||||||
if is_db {
|
if is_db {
|
||||||
|
if file_name == APP_DATABASE_FILE {
|
||||||
|
let backup_database_file = backup_data_dir.join(file_name);
|
||||||
|
let app_database = ctx.get_app_database().await;
|
||||||
|
app_database
|
||||||
|
.create_backup(
|
||||||
|
&backup_database_file.display().to_string(),
|
||||||
|
encryption_key.as_deref().ok_or_else(|| {
|
||||||
|
crate::error::TwonlyError::Generic(
|
||||||
|
"Missing app database backup key".to_owned(),
|
||||||
|
)
|
||||||
|
})?,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let backup_db = Database::new(
|
||||||
|
&backup_database_file.display().to_string(),
|
||||||
|
encryption_key.as_deref(),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
backup_db.check_integrity().await?;
|
||||||
|
backup_db.pool.close().await;
|
||||||
|
encryption_key.zeroize();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
// To avoid write-lock conflicts with Dart (which has the live database open in write mode),
|
// To avoid write-lock conflicts with Dart (which has the live database open in write mode),
|
||||||
// we copy the database file first, then open the copy to perform the backup.
|
// we copy the database file first, then open the copy to perform the backup.
|
||||||
let temp_copy_path = backup_data_dir.join(format!("{}.temp_copy", file_name));
|
let temp_copy_path = backup_data_dir.join(format!("{}.temp_copy", file_name));
|
||||||
|
|
@ -87,6 +139,8 @@ impl BackupArchive {
|
||||||
encryption_key.zeroize();
|
encryption_key.zeroize();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Self::write_manifest(&backup_data_dir)?;
|
||||||
|
|
||||||
let mut zip_data = Vec::new();
|
let mut zip_data = Vec::new();
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|
@ -147,6 +201,60 @@ impl BackupArchive {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Self::validate_manifest(&restore_temp_dir)?;
|
||||||
|
|
||||||
|
let restored_app_database = restore_temp_dir.join(APP_DATABASE_FILE);
|
||||||
|
let has_app_database = restored_app_database.exists();
|
||||||
|
let app_database_key = key_manager.main_key.get_database_key(DatabaseKey::AppDb);
|
||||||
|
if !has_app_database {
|
||||||
|
// A format-v1 archive has no app_db.sqlite. Build and verify it in
|
||||||
|
// staging before touching any active database file.
|
||||||
|
let staged_app_database = crate::database::app::AppDatabase::new(
|
||||||
|
&restored_app_database.display().to_string(),
|
||||||
|
Some(&app_database_key),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
staged_app_database.run_migrations().await?;
|
||||||
|
staged_app_database
|
||||||
|
.import_legacy(&restore_temp_dir.join("twonly.sqlite"))
|
||||||
|
.await?;
|
||||||
|
staged_app_database.pool.close().await;
|
||||||
|
}
|
||||||
|
let staged_app_database = crate::database::app::AppDatabase::new(
|
||||||
|
&restored_app_database.display().to_string(),
|
||||||
|
Some(&app_database_key),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let integrity: String = sqlx::query_scalar("PRAGMA integrity_check")
|
||||||
|
.fetch_one(&staged_app_database.pool)
|
||||||
|
.await?;
|
||||||
|
staged_app_database.pool.close().await;
|
||||||
|
if integrity.to_lowercase() != "ok" {
|
||||||
|
return Err(crate::error::TwonlyError::Generic(format!(
|
||||||
|
"Staged app database integrity check failed: {integrity}"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
let rust_database_key = key_manager.main_key.get_database_key(DatabaseKey::RustDb);
|
||||||
|
let staged_rust_database_path = restore_temp_dir.join("rust_db.sqlite");
|
||||||
|
let staged_rust_database = Database::new(
|
||||||
|
&staged_rust_database_path.display().to_string(),
|
||||||
|
Some(&rust_database_key),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
staged_rust_database.check_integrity().await?;
|
||||||
|
staged_rust_database.pool.close().await;
|
||||||
|
|
||||||
|
// app_db.sqlite is owned by a replaceable Rust handle. Close it before
|
||||||
|
// replacing the file so subsequent DAO calls cannot continue using an
|
||||||
|
// unlinked pre-restore database.
|
||||||
|
let current_app_database = ctx.get_app_database().await;
|
||||||
|
current_app_database.pool.close().await;
|
||||||
|
let current_rust_database = ctx.get_rust_database().await;
|
||||||
|
current_rust_database.pool.close().await;
|
||||||
|
|
||||||
for (file_name, target_dir, is_db, _) in Self::get_backup_files(ctx, &key_manager)? {
|
for (file_name, target_dir, is_db, _) in Self::get_backup_files(ctx, &key_manager)? {
|
||||||
let src = restore_temp_dir.join(file_name);
|
let src = restore_temp_dir.join(file_name);
|
||||||
if src.exists() {
|
if src.exists() {
|
||||||
|
|
@ -163,17 +271,102 @@ impl BackupArchive {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let database_dir = PathBuf::from(&ctx.get_config()?.database_dir);
|
||||||
|
let app_database_path = database_dir.join(APP_DATABASE_FILE);
|
||||||
|
let app_database = crate::database::app::AppDatabase::new(
|
||||||
|
&app_database_path.display().to_string(),
|
||||||
|
Some(&app_database_key),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
app_database.run_migrations().await?;
|
||||||
|
ctx.replace_app_database(app_database).await;
|
||||||
|
|
||||||
|
let rust_database_path = database_dir.join("rust_db.sqlite");
|
||||||
|
let rust_database = Database::new(
|
||||||
|
&rust_database_path.display().to_string(),
|
||||||
|
Some(&rust_database_key),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
rust_database.run_migrations().await?;
|
||||||
|
ctx.replace_rust_database(rust_database, &key_manager)
|
||||||
|
.await?;
|
||||||
|
|
||||||
std::fs::remove_dir_all(&restore_temp_dir)?;
|
std::fs::remove_dir_all(&restore_temp_dir)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn write_manifest(directory: &Path) -> Result<()> {
|
||||||
|
let mut files = BTreeMap::new();
|
||||||
|
for entry in WalkDir::new(directory).min_depth(1).max_depth(1) {
|
||||||
|
let entry = entry?;
|
||||||
|
if !entry.path().is_file() || entry.file_name() == BACKUP_MANIFEST_FILE {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let bytes = std::fs::read(entry.path())?;
|
||||||
|
files.insert(
|
||||||
|
entry.file_name().to_string_lossy().to_string(),
|
||||||
|
BackupManifestFile {
|
||||||
|
size: bytes.len() as u64,
|
||||||
|
sha256: hex::encode(Sha256::digest(&bytes)),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let manifest = BackupManifest {
|
||||||
|
format_version: BACKUP_FORMAT_VERSION,
|
||||||
|
drift_schema_version: 25,
|
||||||
|
app_schema_version: crate::database::app::APP_SCHEMA_VERSION,
|
||||||
|
files,
|
||||||
|
};
|
||||||
|
std::fs::write(
|
||||||
|
directory.join(BACKUP_MANIFEST_FILE),
|
||||||
|
serde_json::to_vec_pretty(&manifest)
|
||||||
|
.map_err(|error| crate::error::TwonlyError::Generic(error.to_string()))?,
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_manifest(directory: &Path) -> Result<()> {
|
||||||
|
let path = directory.join(BACKUP_MANIFEST_FILE);
|
||||||
|
if !path.exists() {
|
||||||
|
// Format v1 archives predate the manifest and are migrated from
|
||||||
|
// twonly.sqlite below.
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let manifest: BackupManifest =
|
||||||
|
serde_json::from_slice(&std::fs::read(path)?).map_err(|error| {
|
||||||
|
crate::error::TwonlyError::Generic(format!("Invalid backup manifest: {error}"))
|
||||||
|
})?;
|
||||||
|
if manifest.format_version != BACKUP_FORMAT_VERSION {
|
||||||
|
return Err(crate::error::TwonlyError::Generic(format!(
|
||||||
|
"Unsupported backup format {}",
|
||||||
|
manifest.format_version
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
for (name, expected) in manifest.files {
|
||||||
|
let file = directory.join(&name);
|
||||||
|
if !file.exists() {
|
||||||
|
return Err(crate::error::TwonlyError::Generic(format!(
|
||||||
|
"Backup entry {name} is missing"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
let bytes = std::fs::read(file)?;
|
||||||
|
let actual_hash = hex::encode(Sha256::digest(&bytes));
|
||||||
|
if bytes.len() as u64 != expected.size || actual_hash != expected.sha256 {
|
||||||
|
return Err(crate::error::TwonlyError::Generic(format!(
|
||||||
|
"Backup entry {name} failed checksum validation"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::{
|
use crate::secure_storage::SecureStorage;
|
||||||
database::tables::received_messages::ReceivedMessage, secure_storage::SecureStorage,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use tempfile::tempdir;
|
use tempfile::tempdir;
|
||||||
|
|
@ -195,27 +388,21 @@ mod tests {
|
||||||
let original_login_token = {
|
let original_login_token = {
|
||||||
let secure_storage = SecureStorage::new("testing");
|
let secure_storage = SecureStorage::new("testing");
|
||||||
let config = ctx.get_config().unwrap();
|
let config = ctx.get_config().unwrap();
|
||||||
let rust_db_path = PathBuf::from(&config.database_dir).join("rust_db.sqlite");
|
|
||||||
let key_manager = ctx.get_key_manager().await.unwrap();
|
let key_manager = ctx.get_key_manager().await.unwrap();
|
||||||
key_manager.store_to_keychain(&secure_storage).unwrap();
|
key_manager.store_to_keychain(&secure_storage).unwrap();
|
||||||
|
|
||||||
let db = Database::new(
|
|
||||||
&rust_db_path.display().to_string(),
|
|
||||||
Some(&key_manager.main_key.get_database_key(DatabaseKey::RustDb)),
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
ReceivedMessage::insert(&db.pool, 1, b"original message")
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// Add a file
|
// Add a file
|
||||||
let config_file = PathBuf::from(&config.data_dir).join("user_discovery_config.json");
|
let config_file = PathBuf::from(&config.data_dir).join("user_discovery_config.json");
|
||||||
std::fs::write(config_file, "original config").unwrap();
|
std::fs::write(config_file, "original config").unwrap();
|
||||||
key_manager.main_key.get_login_token()
|
key_manager.main_key.get_login_token()
|
||||||
};
|
};
|
||||||
|
{
|
||||||
|
let app_db = ctx.get_app_database().await;
|
||||||
|
sqlx::query("INSERT INTO contacts(user_id, username) VALUES(1, 'original contact')")
|
||||||
|
.execute(&app_db.pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
// 2. Create backup
|
// 2. Create backup
|
||||||
let backup_path = BackupArchive::create_backup(&ctx).await.unwrap();
|
let backup_path = BackupArchive::create_backup(&ctx).await.unwrap();
|
||||||
|
|
@ -224,22 +411,15 @@ mod tests {
|
||||||
// 3. Modify data (to simulate state before restore)
|
// 3. Modify data (to simulate state before restore)
|
||||||
{
|
{
|
||||||
let config = ctx.get_config().unwrap();
|
let config = ctx.get_config().unwrap();
|
||||||
let rust_db_path = PathBuf::from(&config.database_dir).join("rust_db.sqlite");
|
|
||||||
let key_manager = ctx.get_key_manager().await.unwrap();
|
|
||||||
let db = Database::new(
|
|
||||||
&rust_db_path.display().to_string(),
|
|
||||||
Some(&key_manager.main_key.get_database_key(DatabaseKey::RustDb)),
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
ReceivedMessage::insert(&db.pool, 2, b"new message")
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let config_file = PathBuf::from(&config.data_dir).join("user_discovery_config.json");
|
let config_file = PathBuf::from(&config.data_dir).join("user_discovery_config.json");
|
||||||
std::fs::write(config_file, "new config").unwrap();
|
std::fs::write(config_file, "new config").unwrap();
|
||||||
|
|
||||||
|
let app_db = ctx.get_app_database().await;
|
||||||
|
sqlx::query("UPDATE contacts SET username = 'changed contact' WHERE user_id = 1")
|
||||||
|
.execute(&app_db.pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Restore backup
|
// 4. Restore backup
|
||||||
|
|
@ -250,27 +430,100 @@ mod tests {
|
||||||
// 5. Verify restored data
|
// 5. Verify restored data
|
||||||
{
|
{
|
||||||
let config = ctx.get_config().unwrap();
|
let config = ctx.get_config().unwrap();
|
||||||
let rust_db_path = PathBuf::from(&config.database_dir).join("rust_db.sqlite");
|
|
||||||
let key_manager = ctx.get_key_manager().await.unwrap();
|
let key_manager = ctx.get_key_manager().await.unwrap();
|
||||||
let db = Database::new(
|
|
||||||
&rust_db_path.display().to_string(),
|
|
||||||
Some(&key_manager.main_key.get_database_key(DatabaseKey::RustDb)),
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let messages = ReceivedMessage::get_all(&db.pool).await.unwrap();
|
|
||||||
// Should only have the original message because restore overwrites
|
|
||||||
assert_eq!(messages.len(), 1);
|
|
||||||
assert_eq!(messages[0].sender_id, 1);
|
|
||||||
assert_eq!(messages[0].content, b"original message");
|
|
||||||
|
|
||||||
let config_file = PathBuf::from(&config.data_dir).join("user_discovery_config.json");
|
let config_file = PathBuf::from(&config.data_dir).join("user_discovery_config.json");
|
||||||
let config_content = std::fs::read_to_string(config_file).unwrap();
|
let config_content = std::fs::read_to_string(config_file).unwrap();
|
||||||
assert_eq!(config_content, "original config");
|
assert_eq!(config_content, "original config");
|
||||||
|
|
||||||
assert_eq!(key_manager.main_key.get_login_token(), original_login_token);
|
assert_eq!(key_manager.main_key.get_login_token(), original_login_token);
|
||||||
|
|
||||||
|
let app_db = ctx.get_app_database().await;
|
||||||
|
let username: String =
|
||||||
|
sqlx::query_scalar("SELECT username FROM contacts WHERE user_id = 1")
|
||||||
|
.fetch_one(&app_db.pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(username, "original contact");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn restores_pre_app_database_backup_by_importing_twonly_sqlite() {
|
||||||
|
let _ = pretty_env_logger::try_init();
|
||||||
|
let temp_dir = tempdir().unwrap();
|
||||||
|
let ctx = Context::init_for_testing(
|
||||||
|
temp_dir.path().join("database"),
|
||||||
|
temp_dir.path().join("data"),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let database_dir = PathBuf::from(&ctx.get_config().unwrap().database_dir);
|
||||||
|
let legacy_path = database_dir.join("twonly.sqlite");
|
||||||
|
let legacy =
|
||||||
|
crate::database::app::AppDatabase::new(&legacy_path.display().to_string(), None, false)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
legacy.run_migrations().await.unwrap();
|
||||||
|
sqlx::query("PRAGMA user_version = 25")
|
||||||
|
.execute(&legacy.pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
sqlx::query("INSERT INTO contacts(user_id, username) VALUES(99, 'from old backup')")
|
||||||
|
.execute(&legacy.pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
legacy.pool.close().await;
|
||||||
|
|
||||||
|
let archive_path = BackupArchive::create_backup(&ctx).await.unwrap();
|
||||||
|
remove_file_from_encrypted_archive(&ctx, &archive_path, APP_DATABASE_FILE).await;
|
||||||
|
|
||||||
|
let app_db = ctx.get_app_database().await;
|
||||||
|
sqlx::query("INSERT INTO contacts(user_id, username) VALUES(1, 'current data')")
|
||||||
|
.execute(&app_db.pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
BackupArchive::restore_from_backup(&ctx, &archive_path)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let restored = ctx.get_app_database().await;
|
||||||
|
let contacts: Vec<(i64, String)> =
|
||||||
|
sqlx::query_as("SELECT user_id, username FROM contacts ORDER BY user_id")
|
||||||
|
.fetch_all(&restored.pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(contacts, vec![(99, "from old backup".to_owned())]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn remove_file_from_encrypted_archive(
|
||||||
|
ctx: &Context,
|
||||||
|
archive_path: &Path,
|
||||||
|
excluded_name: &str,
|
||||||
|
) {
|
||||||
|
let keys = ctx.get_key_manager().await.unwrap();
|
||||||
|
let encrypted = std::fs::read(archive_path).unwrap();
|
||||||
|
let decrypted = keys.main_key.decrypt_backup(&encrypted).unwrap();
|
||||||
|
let mut source = ZipArchive::new(Cursor::new(decrypted)).unwrap();
|
||||||
|
let mut rebuilt = Vec::new();
|
||||||
|
{
|
||||||
|
let mut writer = ZipWriter::new(Cursor::new(&mut rebuilt));
|
||||||
|
let options =
|
||||||
|
SimpleFileOptions::default().compression_method(CompressionMethod::Deflated);
|
||||||
|
for index in 0..source.len() {
|
||||||
|
let mut entry = source.by_index(index).unwrap();
|
||||||
|
if entry.name() == excluded_name
|
||||||
|
|| entry.name() == BACKUP_MANIFEST_FILE
|
||||||
|
|| !entry.is_file()
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
writer.start_file(entry.name(), options).unwrap();
|
||||||
|
copy(&mut entry, &mut writer).unwrap();
|
||||||
|
}
|
||||||
|
writer.finish().unwrap();
|
||||||
|
}
|
||||||
|
std::fs::write(archive_path, keys.main_key.encrypt_backup(&rebuilt)).unwrap();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,5 @@
|
||||||
pub(crate) mod log;
|
pub(crate) mod log;
|
||||||
mod macros;
|
mod macros;
|
||||||
pub(crate) mod user_discovery;
|
|
||||||
|
|
||||||
use crate::user_discovery::traits::{AnnouncedUser, OtherPromotion};
|
|
||||||
use flutter_rust_bridge::DartFnFuture;
|
use flutter_rust_bridge::DartFnFuture;
|
||||||
|
|
||||||
use crate::error::{Result, TwonlyError};
|
use crate::error::{Result, TwonlyError};
|
||||||
|
|
@ -23,25 +20,6 @@ callback_generator! {
|
||||||
FlutterCallbacks {
|
FlutterCallbacks {
|
||||||
Logging logging {
|
Logging logging {
|
||||||
get_stream_sink: () => StreamSink<String>
|
get_stream_sink: () => StreamSink<String>
|
||||||
},
|
|
||||||
UserDiscoveryCallbacks user_discovery {
|
|
||||||
// UserDiscoveryUtils
|
|
||||||
sign_data: (Vec<u8>) => Option<Vec<u8>>,
|
|
||||||
verify_signature: (Vec<u8>, Vec<u8>, Vec<u8>) => bool,
|
|
||||||
verify_stored_pubkey: (i64, Vec<u8>) => bool,
|
|
||||||
|
|
||||||
// UserDiscoveryStore
|
|
||||||
set_shares: (Vec<Vec<u8>>) => bool,
|
|
||||||
get_share_for_contact: (i64) => Option<Vec<u8>>,
|
|
||||||
push_own_promotion_and_clear_old_version: (i64, i64, Vec<u8>) => bool,
|
|
||||||
get_own_promotions_after_version: (i64) => Option<Vec<Vec<u8>>>,
|
|
||||||
store_other_promotion: (OtherPromotion) => bool,
|
|
||||||
get_other_promotions_by_public_id: (i64) => Option<Vec<OtherPromotion>>,
|
|
||||||
get_announced_user_by_public_id: (i64) => Option<AnnouncedUser>,
|
|
||||||
get_contact_version: (i64) => Option<Vec<u8>>,
|
|
||||||
set_contact_version: (i64, Vec<u8>) => bool,
|
|
||||||
push_new_user_relation: (i64, AnnouncedUser, Option<i64>) => bool,
|
|
||||||
get_contact_promotion: (i64) => Option<Vec<u8>>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,174 +0,0 @@
|
||||||
use crate::bridge::callbacks::get_callbacks;
|
|
||||||
use crate::bridge::get_twonly_flutter;
|
|
||||||
use crate::error::TwonlyError;
|
|
||||||
use crate::user_discovery::error::{Result, UserDiscoveryError};
|
|
||||||
use crate::user_discovery::traits::UserDiscoveryUtils;
|
|
||||||
use crate::user_discovery::traits::{AnnouncedUser, OtherPromotion, UserDiscoveryStore};
|
|
||||||
#[cfg(test)]
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub(crate) struct UserDiscoveryStoreFlutter {}
|
|
||||||
pub(crate) struct UserDiscoveryUtilsFlutter {}
|
|
||||||
|
|
||||||
impl UserDiscoveryUtils for UserDiscoveryUtilsFlutter {
|
|
||||||
async fn sign_data(&self, input_data: &[u8]) -> Result<Vec<u8>> {
|
|
||||||
match (get_callbacks()?.user_discovery.sign_data)(input_data.to_vec()).await {
|
|
||||||
Some(signature) => Ok(signature),
|
|
||||||
None => Err(TwonlyError::DartError)?,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn verify_signature(
|
|
||||||
&self,
|
|
||||||
input_data: &[u8],
|
|
||||||
pubkey: &[u8],
|
|
||||||
signature: &[u8],
|
|
||||||
) -> Result<bool> {
|
|
||||||
Ok((get_callbacks()?.user_discovery.verify_signature)(
|
|
||||||
input_data.to_vec(),
|
|
||||||
pubkey.to_vec(),
|
|
||||||
signature.to_vec(),
|
|
||||||
)
|
|
||||||
.await)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn verify_stored_pubkey(&self, from_contact_id: i64, pubkey: &[u8]) -> Result<bool> {
|
|
||||||
Ok(
|
|
||||||
(get_callbacks()?.user_discovery.verify_stored_pubkey)(
|
|
||||||
from_contact_id,
|
|
||||||
pubkey.to_vec(),
|
|
||||||
)
|
|
||||||
.await,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl UserDiscoveryStore for UserDiscoveryStoreFlutter {
|
|
||||||
async fn get_config(&self) -> Result<String> {
|
|
||||||
let ws = get_twonly_flutter()?;
|
|
||||||
let config_path = PathBuf::from(&ws.config.data_dir).join("user_discovery_config.json");
|
|
||||||
|
|
||||||
if !config_path.is_file() {
|
|
||||||
return Err(UserDiscoveryError::NotInitialized);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(std::fs::read_to_string(&config_path)?)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn update_config(&self, update: String) -> Result<()> {
|
|
||||||
tracing::debug!("Updating configuration file.");
|
|
||||||
let ws = get_twonly_flutter()?;
|
|
||||||
let config_path = PathBuf::from(&ws.config.data_dir).join("user_discovery_config.json");
|
|
||||||
std::fs::write(config_path, &update)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn set_shares(&self, shares: Vec<Vec<u8>>) -> Result<()> {
|
|
||||||
(get_callbacks()?.user_discovery.set_shares)(shares).await;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_share_for_contact(&self, contact_id: i64) -> Result<Vec<u8>> {
|
|
||||||
match (get_callbacks()?.user_discovery.get_share_for_contact)(contact_id).await {
|
|
||||||
Some(share) => Ok(share),
|
|
||||||
None => Err(UserDiscoveryError::NoSharesLeft),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn push_own_promotion_and_clear_old_version(
|
|
||||||
&self,
|
|
||||||
contact_id: i64,
|
|
||||||
version: u32,
|
|
||||||
promotion: Vec<u8>,
|
|
||||||
) -> Result<()> {
|
|
||||||
(get_callbacks()?
|
|
||||||
.user_discovery
|
|
||||||
.push_own_promotion_and_clear_old_version)(contact_id, version as i64, promotion)
|
|
||||||
.await
|
|
||||||
.then_some(())
|
|
||||||
.ok_or(TwonlyError::DartError.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_own_promotions_after_version(&self, version: u32) -> Result<Vec<Vec<u8>>> {
|
|
||||||
match (get_callbacks()?
|
|
||||||
.user_discovery
|
|
||||||
.get_own_promotions_after_version)(version as i64)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Some(share) => Ok(share),
|
|
||||||
None => Err(TwonlyError::DartError)?,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn store_other_promotion(&self, promotion: OtherPromotion) -> Result<()> {
|
|
||||||
(get_callbacks()?.user_discovery.store_other_promotion)(promotion)
|
|
||||||
.await
|
|
||||||
.then_some(())
|
|
||||||
.ok_or(TwonlyError::DartError.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_other_promotions_by_public_id(
|
|
||||||
&self,
|
|
||||||
public_id: i64,
|
|
||||||
) -> Result<Vec<OtherPromotion>> {
|
|
||||||
match (get_callbacks()?
|
|
||||||
.user_discovery
|
|
||||||
.get_other_promotions_by_public_id)(public_id)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Some(promotions) => Ok(promotions),
|
|
||||||
None => Err(TwonlyError::DartError)?,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_announced_user_by_public_id(
|
|
||||||
&self,
|
|
||||||
public_id: i64,
|
|
||||||
) -> Result<Option<AnnouncedUser>> {
|
|
||||||
Ok((get_callbacks()?
|
|
||||||
.user_discovery
|
|
||||||
.get_announced_user_by_public_id)(public_id)
|
|
||||||
.await)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn push_new_user_relation(
|
|
||||||
&self,
|
|
||||||
from_contact_id: i64,
|
|
||||||
announced_user: AnnouncedUser,
|
|
||||||
public_key_verified_timestamp: Option<i64>,
|
|
||||||
) -> Result<()> {
|
|
||||||
(get_callbacks()?.user_discovery.push_new_user_relation)(
|
|
||||||
from_contact_id,
|
|
||||||
announced_user,
|
|
||||||
public_key_verified_timestamp,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.then_some(())
|
|
||||||
.ok_or(TwonlyError::DartError.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
async fn get_all_announced_users(
|
|
||||||
&self,
|
|
||||||
) -> Result<HashMap<AnnouncedUser, Vec<(i64, Option<i64>)>>> {
|
|
||||||
// This is never called from the RUST code.
|
|
||||||
Err(TwonlyError::DartError)?
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_contact_version(&self, contact_id: i64) -> Result<Option<Vec<u8>>> {
|
|
||||||
Ok((get_callbacks()?.user_discovery.get_contact_version)(contact_id).await)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn set_contact_version(&self, contact_id: i64, update: Vec<u8>) -> Result<()> {
|
|
||||||
(get_callbacks()?.user_discovery.set_contact_version)(contact_id, update)
|
|
||||||
.await
|
|
||||||
.then_some(())
|
|
||||||
.ok_or(TwonlyError::DartError.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_contact_promotion(&self, contact_id: i64) -> Result<Option<Vec<u8>>> {
|
|
||||||
Ok((get_callbacks()?.user_discovery.get_contact_promotion)(contact_id).await)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -4,23 +4,22 @@ pub mod wrapper;
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::bridge::callbacks::user_discovery::{
|
|
||||||
UserDiscoveryStoreFlutter, UserDiscoveryUtilsFlutter,
|
|
||||||
};
|
|
||||||
use crate::context::Context;
|
use crate::context::Context;
|
||||||
use crate::database::Database;
|
use crate::database::app::AppDatabase;
|
||||||
|
use crate::database::signal::Database;
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use crate::error::TwonlyError;
|
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::traits::AnnouncedUser;
|
||||||
pub use crate::user_discovery::traits::OtherPromotion;
|
pub use crate::user_discovery::traits::OtherPromotion;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::{Mutex, RwLock};
|
||||||
|
|
||||||
pub struct InitConfig {
|
pub struct InitConfig {
|
||||||
pub database_dir: String,
|
pub database_dir: String,
|
||||||
|
|
@ -48,9 +47,10 @@ 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<UserDiscoveryStoreFlutter, UserDiscoveryUtilsFlutter>>,
|
Shared<UserDiscovery<NativeUserDiscoveryStore, NativeUserDiscoveryUtils>>,
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub(crate) rust_db: Arc<Database>,
|
pub(crate) rust_db: Arc<RwLock<Arc<Database>>>,
|
||||||
|
pub(crate) app_db: Arc<RwLock<Arc<AppDatabase>>>,
|
||||||
pub(crate) secure_storage: SecureStorage,
|
pub(crate) secure_storage: SecureStorage,
|
||||||
pub(crate) key_manager: Arc<Mutex<KeyManager>>,
|
pub(crate) key_manager: Arc<Mutex<KeyManager>>,
|
||||||
pub(crate) signal_engine: Arc<Mutex<Option<RustSignalEngine>>>,
|
pub(crate) signal_engine: Arc<Mutex<Option<RustSignalEngine>>>,
|
||||||
|
|
|
||||||
73
rust/src/bridge/wrapper/app_database.rs
Normal file
73
rust/src/bridge/wrapper/app_database.rs
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use crate::bridge::get_twonly_flutter;
|
||||||
|
pub use crate::database::app::{SqlExecutionResult, SqlRow, SqlRows, SqlValue};
|
||||||
|
use crate::error::Result;
|
||||||
|
|
||||||
|
pub struct RustAppDatabase {}
|
||||||
|
|
||||||
|
pub struct LegacyMigrationReport {
|
||||||
|
pub legacy_version: i64,
|
||||||
|
pub tables: Vec<LegacyTableMigrationCount>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct LegacyTableMigrationCount {
|
||||||
|
pub table: String,
|
||||||
|
pub rows: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RustAppDatabase {
|
||||||
|
/// Imports the non-Signal tables from a Drift v25 database. The import is
|
||||||
|
/// transactional and idempotent. Drift must be closed while this runs.
|
||||||
|
pub async fn migrate_legacy_database() -> Result<LegacyMigrationReport> {
|
||||||
|
let context = get_twonly_flutter()?;
|
||||||
|
let legacy_path = PathBuf::from(&context.config.database_dir).join("twonly.sqlite");
|
||||||
|
let app_db = context.app_db.read().await.clone();
|
||||||
|
let report = if legacy_path.exists() {
|
||||||
|
app_db.import_legacy(&legacy_path).await?
|
||||||
|
} else {
|
||||||
|
app_db.complete_empty_legacy_import().await?
|
||||||
|
};
|
||||||
|
Ok(LegacyMigrationReport {
|
||||||
|
legacy_version: report.legacy_version,
|
||||||
|
tables: report
|
||||||
|
.tables
|
||||||
|
.into_iter()
|
||||||
|
.map(|entry| LegacyTableMigrationCount {
|
||||||
|
table: entry.table,
|
||||||
|
rows: entry.rows,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn legacy_import_complete() -> Result<bool> {
|
||||||
|
get_twonly_flutter()?
|
||||||
|
.app_db
|
||||||
|
.read()
|
||||||
|
.await
|
||||||
|
.is_legacy_import_complete()
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn select(statement: String, arguments: Vec<SqlValue>) -> Result<SqlRows> {
|
||||||
|
get_twonly_flutter()?
|
||||||
|
.app_db
|
||||||
|
.read()
|
||||||
|
.await
|
||||||
|
.raw_select(statement, arguments)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn execute(
|
||||||
|
statement: String,
|
||||||
|
arguments: Vec<SqlValue>,
|
||||||
|
) -> Result<SqlExecutionResult> {
|
||||||
|
get_twonly_flutter()?
|
||||||
|
.app_db
|
||||||
|
.read()
|
||||||
|
.await
|
||||||
|
.raw_execute(statement, arguments)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
pub mod app_database;
|
||||||
pub mod backup;
|
pub mod backup;
|
||||||
pub mod key_manager;
|
pub mod key_manager;
|
||||||
pub mod signal;
|
pub mod signal;
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,7 @@ impl RustSignal {
|
||||||
pub async fn generate_bundle() -> Result<FrbPreKeyBundle> {
|
pub async fn generate_bundle() -> Result<FrbPreKeyBundle> {
|
||||||
let guard = get_twonly_flutter()?.signal_engine.lock().await;
|
let guard = get_twonly_flutter()?.signal_engine.lock().await;
|
||||||
let engine = guard.as_ref().ok_or(TwonlyError::Initialization)?;
|
let engine = guard.as_ref().ok_or(TwonlyError::Initialization)?;
|
||||||
engine
|
engine.generate_bundle().await
|
||||||
.generate_bundle()
|
|
||||||
.await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn generate_pqc_prekeys() -> Result<Vec<FrbPqcPreKey>> {
|
pub async fn generate_pqc_prekeys() -> Result<Vec<FrbPqcPreKey>> {
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,31 @@
|
||||||
|
use crate::database::app::{AppDatabase, APP_DATABASE_FILE};
|
||||||
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::{
|
use crate::{
|
||||||
bridge::{
|
bridge::InitConfig,
|
||||||
callbacks::user_discovery::{UserDiscoveryStoreFlutter, UserDiscoveryUtilsFlutter},
|
database::signal::Database,
|
||||||
InitConfig,
|
|
||||||
},
|
|
||||||
database::Database,
|
|
||||||
error::{Result, TwonlyError},
|
error::{Result, TwonlyError},
|
||||||
keys::{DatabaseKey, KeyManager},
|
keys::{DatabaseKey, KeyManager},
|
||||||
log::init_tracing,
|
log::init_tracing,
|
||||||
utils::Shared,
|
utils::Shared,
|
||||||
};
|
};
|
||||||
use std::{path::PathBuf, sync::Arc};
|
use std::{path::PathBuf, sync::Arc};
|
||||||
use tokio::sync::{Mutex, OnceCell};
|
use tokio::sync::{Mutex, OnceCell, RwLock};
|
||||||
use zeroize::Zeroize;
|
use zeroize::Zeroize;
|
||||||
|
|
||||||
use crate::{bridge::TwonlyFlutter, secure_storage::SecureStorage, standalone::TwonlyStandalone};
|
use crate::{bridge::TwonlyFlutter, secure_storage::SecureStorage};
|
||||||
|
|
||||||
|
pub(crate) struct TwonlyStandalone {
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub(crate) config: InitConfig,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub(crate) rust_db: Arc<RwLock<Arc<Database>>>,
|
||||||
|
pub(crate) app_db: Arc<RwLock<Arc<AppDatabase>>>,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub(crate) secure_storage: SecureStorage,
|
||||||
|
pub(crate) key_manager: Arc<Mutex<KeyManager>>,
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) enum Context {
|
pub(crate) enum Context {
|
||||||
Flutter(TwonlyFlutter),
|
Flutter(TwonlyFlutter),
|
||||||
|
|
@ -73,9 +83,20 @@ impl Context {
|
||||||
rust_db.run_migrations().await?;
|
rust_db.run_migrations().await?;
|
||||||
let rust_db = Arc::new(rust_db);
|
let rust_db = Arc::new(rust_db);
|
||||||
|
|
||||||
|
let app_db_path = database_dir.join(APP_DATABASE_FILE);
|
||||||
|
let app_db = AppDatabase::new(
|
||||||
|
&app_db_path.display().to_string(),
|
||||||
|
Some(&key_manager.main_key.get_database_key(DatabaseKey::AppDb)),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
app_db.run_migrations().await?;
|
||||||
|
let app_db = Arc::new(RwLock::new(Arc::new(app_db)));
|
||||||
|
|
||||||
Ok(Context::from_standalone(TwonlyStandalone {
|
Ok(Context::from_standalone(TwonlyStandalone {
|
||||||
config,
|
config,
|
||||||
rust_db,
|
rust_db: Arc::new(RwLock::new(rust_db)),
|
||||||
|
app_db,
|
||||||
secure_storage,
|
secure_storage,
|
||||||
key_manager: Arc::new(Mutex::new(key_manager)),
|
key_manager: Arc::new(Mutex::new(key_manager)),
|
||||||
}))
|
}))
|
||||||
|
|
@ -100,6 +121,7 @@ impl Context {
|
||||||
|
|
||||||
let database_dir = PathBuf::from(&config.database_dir.clone());
|
let database_dir = PathBuf::from(&config.database_dir.clone());
|
||||||
let rust_db_path = database_dir.join("rust_db.sqlite");
|
let rust_db_path = database_dir.join("rust_db.sqlite");
|
||||||
|
let app_db_path = database_dir.join(APP_DATABASE_FILE);
|
||||||
|
|
||||||
tracing::info!("Initialized twonly workspace.");
|
tracing::info!("Initialized twonly workspace.");
|
||||||
let res: Result<&'static Context> = GLOBAL_CONTEXT
|
let res: Result<&'static Context> = GLOBAL_CONTEXT
|
||||||
|
|
@ -129,36 +151,59 @@ impl Context {
|
||||||
.await?;
|
.await?;
|
||||||
rust_db.run_migrations().await?;
|
rust_db.run_migrations().await?;
|
||||||
let rust_db = Arc::new(rust_db);
|
let rust_db = Arc::new(rust_db);
|
||||||
|
let rust_db_handle = Arc::new(RwLock::new(rust_db));
|
||||||
|
|
||||||
|
let mut app_db_key = key_manager.main_key.get_database_key(DatabaseKey::AppDb);
|
||||||
|
let app_db = AppDatabase::new(
|
||||||
|
&app_db_path.display().to_string(),
|
||||||
|
Some(app_db_key.as_str()),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
app_db.run_migrations().await?;
|
||||||
|
let app_db = Arc::new(RwLock::new(Arc::new(app_db)));
|
||||||
|
app_db_key.zeroize();
|
||||||
|
|
||||||
rust_db_key.zeroize();
|
rust_db_key.zeroize();
|
||||||
|
|
||||||
if is_flutter {
|
if is_flutter {
|
||||||
let mut signal_engine = Arc::default();
|
let key_manager = Arc::new(Mutex::new(key_manager));
|
||||||
if let Some(user_id) = key_manager.user_id {
|
let signal_engine = {
|
||||||
if let Some(signal_identity) = &key_manager.signal_identity {
|
let key_manager_guard = key_manager.lock().await;
|
||||||
signal_engine = Arc::new(Mutex::new(Some(RustSignalEngine::new_with_pool(
|
let engine = match (
|
||||||
rust_db.pool.clone(),
|
key_manager_guard.user_id,
|
||||||
|
&key_manager_guard.signal_identity,
|
||||||
|
) {
|
||||||
|
(Some(user_id), Some(signal_identity)) => {
|
||||||
|
Some(RustSignalEngine::new_with_pool(
|
||||||
|
rust_db_handle.read().await.pool.clone(),
|
||||||
signal_identity.identity_key_pair_structure.clone(),
|
signal_identity.identity_key_pair_structure.clone(),
|
||||||
signal_identity.registration_id as u32,
|
signal_identity.registration_id as u32,
|
||||||
user_id.to_string(),
|
user_id.to_string(),
|
||||||
)?)));
|
)?)
|
||||||
}
|
}
|
||||||
}
|
_ => None,
|
||||||
|
};
|
||||||
|
Arc::new(Mutex::new(engine))
|
||||||
|
};
|
||||||
|
let user_discovery = Shared::new(UserDiscovery::new(
|
||||||
|
NativeUserDiscoveryStore::new(app_db.clone(), &config.data_dir),
|
||||||
|
NativeUserDiscoveryUtils::new(key_manager.clone(), rust_db_handle.clone()),
|
||||||
|
)?);
|
||||||
Ok(Context::Flutter(TwonlyFlutter {
|
Ok(Context::Flutter(TwonlyFlutter {
|
||||||
config,
|
config,
|
||||||
secure_storage,
|
secure_storage,
|
||||||
rust_db,
|
rust_db: rust_db_handle,
|
||||||
key_manager: Arc::new(Mutex::new(key_manager)),
|
app_db,
|
||||||
user_discovery: Shared::new(UserDiscovery::new(
|
key_manager,
|
||||||
UserDiscoveryStoreFlutter {},
|
user_discovery,
|
||||||
UserDiscoveryUtilsFlutter {},
|
|
||||||
)?),
|
|
||||||
signal_engine,
|
signal_engine,
|
||||||
}))
|
}))
|
||||||
} else {
|
} else {
|
||||||
Ok(Context::Standalone(TwonlyStandalone {
|
Ok(Context::Standalone(TwonlyStandalone {
|
||||||
config,
|
config,
|
||||||
rust_db,
|
rust_db: rust_db_handle,
|
||||||
|
app_db,
|
||||||
key_manager: Arc::new(Mutex::new(key_manager)),
|
key_manager: Arc::new(Mutex::new(key_manager)),
|
||||||
secure_storage,
|
secure_storage,
|
||||||
}))
|
}))
|
||||||
|
|
@ -193,4 +238,50 @@ impl Context {
|
||||||
Self::Standalone(twonly) => Ok(twonly.key_manager.lock().await),
|
Self::Standalone(twonly) => Ok(twonly.key_manager.lock().await),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn get_app_database(&self) -> Arc<AppDatabase> {
|
||||||
|
match self {
|
||||||
|
Self::Flutter(twonly) => twonly.app_db.read().await.clone(),
|
||||||
|
Self::Standalone(twonly) => twonly.app_db.read().await.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn get_rust_database(&self) -> Arc<Database> {
|
||||||
|
match self {
|
||||||
|
Self::Flutter(twonly) => twonly.rust_db.read().await.clone(),
|
||||||
|
Self::Standalone(twonly) => twonly.rust_db.read().await.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn replace_rust_database(
|
||||||
|
&self,
|
||||||
|
database: Database,
|
||||||
|
key_manager: &KeyManager,
|
||||||
|
) -> Result<()> {
|
||||||
|
let database = Arc::new(database);
|
||||||
|
match self {
|
||||||
|
Self::Flutter(twonly) => {
|
||||||
|
*twonly.rust_db.write().await = database.clone();
|
||||||
|
let engine = match (key_manager.user_id, &key_manager.signal_identity) {
|
||||||
|
(Some(user_id), Some(identity)) => Some(RustSignalEngine::new_with_pool(
|
||||||
|
database.pool.clone(),
|
||||||
|
identity.identity_key_pair_structure.clone(),
|
||||||
|
identity.registration_id as u32,
|
||||||
|
user_id.to_string(),
|
||||||
|
)?),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
*twonly.signal_engine.lock().await = engine;
|
||||||
|
}
|
||||||
|
Self::Standalone(twonly) => *twonly.rust_db.write().await = database,
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn replace_app_database(&self, database: AppDatabase) {
|
||||||
|
match self {
|
||||||
|
Self::Flutter(twonly) => *twonly.app_db.write().await = Arc::new(database),
|
||||||
|
Self::Standalone(twonly) => *twonly.app_db.write().await = Arc::new(database),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
354
rust/src/database/app/legacy_import.rs
Normal file
354
rust/src/database/app/legacy_import.rs
Normal file
|
|
@ -0,0 +1,354 @@
|
||||||
|
use super::{AppDatabase, MigrationReport, TableMigrationCount, APPLICATION_TABLES};
|
||||||
|
use crate::error::{Result, TwonlyError};
|
||||||
|
use sqlx::{Acquire, AssertSqlSafe, Row};
|
||||||
|
use std::collections::BTreeSet;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
impl AppDatabase {
|
||||||
|
pub async fn is_legacy_import_complete(&self) -> Result<bool> {
|
||||||
|
let value = sqlx::query_scalar::<_, String>(
|
||||||
|
"SELECT value FROM app_metadata WHERE key = 'legacy_import_complete'",
|
||||||
|
)
|
||||||
|
.fetch_optional(&self.pool)
|
||||||
|
.await?;
|
||||||
|
Ok(value.as_deref() == Some("1"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn complete_empty_legacy_import(&self) -> Result<MigrationReport> {
|
||||||
|
let mut transaction = self.pool.begin().await?;
|
||||||
|
sqlx::query("INSERT INTO app_metadata(key, value) VALUES('legacy_schema_version', '25') ON CONFLICT(key) DO UPDATE SET value = excluded.value").execute(&mut *transaction).await?;
|
||||||
|
sqlx::query("INSERT INTO app_metadata(key, value) VALUES('legacy_import_complete', '1') ON CONFLICT(key) DO UPDATE SET value = excluded.value").execute(&mut *transaction).await?;
|
||||||
|
transaction.commit().await?;
|
||||||
|
self.migration_report().await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn import_legacy(&self, legacy_path: &Path) -> Result<MigrationReport> {
|
||||||
|
if self.is_legacy_import_complete().await? {
|
||||||
|
return self.migration_report().await;
|
||||||
|
}
|
||||||
|
if !legacy_path.exists() {
|
||||||
|
return Err(TwonlyError::DatabaseNotFound);
|
||||||
|
}
|
||||||
|
let mut connection = self.pool.acquire().await?;
|
||||||
|
sqlx::query("ATTACH DATABASE ? AS legacy KEY ''")
|
||||||
|
.bind(legacy_path.to_string_lossy().as_ref())
|
||||||
|
.execute(&mut *connection)
|
||||||
|
.await?;
|
||||||
|
let import_result = import(&mut connection).await;
|
||||||
|
let _ = sqlx::query("DETACH DATABASE legacy")
|
||||||
|
.execute(&mut *connection)
|
||||||
|
.await;
|
||||||
|
let report = import_result?;
|
||||||
|
self.notify_committed(APPLICATION_TABLES.iter().copied());
|
||||||
|
Ok(report)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn migration_report(&self) -> Result<MigrationReport> {
|
||||||
|
let legacy_version = sqlx::query_scalar::<_, String>(
|
||||||
|
"SELECT value FROM app_metadata WHERE key = 'legacy_schema_version'",
|
||||||
|
)
|
||||||
|
.fetch_optional(&self.pool)
|
||||||
|
.await?
|
||||||
|
.and_then(|value| value.parse().ok())
|
||||||
|
.unwrap_or(25);
|
||||||
|
let mut tables = Vec::with_capacity(APPLICATION_TABLES.len());
|
||||||
|
for table in APPLICATION_TABLES {
|
||||||
|
let rows = sqlx::query_scalar::<_, i64>(AssertSqlSafe(format!(
|
||||||
|
"SELECT COUNT(*) FROM \"{table}\""
|
||||||
|
)))
|
||||||
|
.fetch_one(&self.pool)
|
||||||
|
.await?;
|
||||||
|
tables.push(TableMigrationCount {
|
||||||
|
table: (*table).to_owned(),
|
||||||
|
rows,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(MigrationReport {
|
||||||
|
legacy_version,
|
||||||
|
tables,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn import(
|
||||||
|
connection: &mut sqlx::pool::PoolConnection<sqlx::Sqlite>,
|
||||||
|
) -> Result<MigrationReport> {
|
||||||
|
let legacy_version: i64 = sqlx::query_scalar("PRAGMA legacy.user_version")
|
||||||
|
.fetch_one(&mut **connection)
|
||||||
|
.await?;
|
||||||
|
if legacy_version != 25 {
|
||||||
|
return Err(TwonlyError::Generic(format!("Legacy database must be upgraded to Drift schema 25 before import; found {legacy_version}")));
|
||||||
|
}
|
||||||
|
let mut tx = connection.begin().await?;
|
||||||
|
sqlx::query("PRAGMA defer_foreign_keys = ON")
|
||||||
|
.execute(&mut *tx)
|
||||||
|
.await?;
|
||||||
|
let mut counts = Vec::with_capacity(APPLICATION_TABLES.len());
|
||||||
|
for table in APPLICATION_TABLES {
|
||||||
|
let columns = common_columns(&mut tx, table).await?;
|
||||||
|
if columns.is_empty() {
|
||||||
|
return Err(TwonlyError::Generic(format!(
|
||||||
|
"No importable columns found for {table}"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
let quoted = columns
|
||||||
|
.iter()
|
||||||
|
.map(|column| format!("\"{}\"", column.replace('"', "\"\"")))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ");
|
||||||
|
sqlx::query(AssertSqlSafe(format!("INSERT OR REPLACE INTO main.\"{table}\" ({quoted}) SELECT {quoted} FROM legacy.\"{table}\""))).execute(&mut *tx).await?;
|
||||||
|
let source_count: i64 = sqlx::query_scalar(AssertSqlSafe(format!(
|
||||||
|
"SELECT COUNT(*) FROM legacy.\"{table}\""
|
||||||
|
)))
|
||||||
|
.fetch_one(&mut *tx)
|
||||||
|
.await?;
|
||||||
|
let target_count: i64 = sqlx::query_scalar(AssertSqlSafe(format!(
|
||||||
|
"SELECT COUNT(*) FROM main.\"{table}\""
|
||||||
|
)))
|
||||||
|
.fetch_one(&mut *tx)
|
||||||
|
.await?;
|
||||||
|
if source_count != target_count {
|
||||||
|
return Err(TwonlyError::Generic(format!(
|
||||||
|
"Row count mismatch for {table}: source={source_count}, target={target_count}"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
let mismatch: Option<i64> = sqlx::query_scalar(AssertSqlSafe(format!("SELECT 1 FROM (SELECT {quoted} FROM main.\"{table}\" EXCEPT SELECT {quoted} FROM legacy.\"{table}\") LIMIT 1"))).fetch_optional(&mut *tx).await?;
|
||||||
|
let reverse_mismatch: Option<i64> = sqlx::query_scalar(AssertSqlSafe(format!("SELECT 1 FROM (SELECT {quoted} FROM legacy.\"{table}\" EXCEPT SELECT {quoted} FROM main.\"{table}\") LIMIT 1"))).fetch_optional(&mut *tx).await?;
|
||||||
|
if mismatch.is_some() || reverse_mismatch.is_some() {
|
||||||
|
return Err(TwonlyError::Generic(format!(
|
||||||
|
"Data mismatch while importing {table}"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
counts.push(TableMigrationCount {
|
||||||
|
table: (*table).to_owned(),
|
||||||
|
rows: source_count,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
copy_sequences(&mut tx).await?;
|
||||||
|
let foreign_key_errors: i64 =
|
||||||
|
sqlx::query_scalar("SELECT COUNT(*) FROM pragma_foreign_key_check")
|
||||||
|
.fetch_one(&mut *tx)
|
||||||
|
.await?;
|
||||||
|
if foreign_key_errors != 0 {
|
||||||
|
return Err(TwonlyError::Generic(format!(
|
||||||
|
"Imported database has {foreign_key_errors} foreign-key violations"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
sqlx::query("INSERT INTO app_metadata(key, value) VALUES('legacy_schema_version', ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").bind(legacy_version.to_string()).execute(&mut *tx).await?;
|
||||||
|
sqlx::query("INSERT INTO app_metadata(key, value) VALUES('legacy_import_complete', '1') ON CONFLICT(key) DO UPDATE SET value = '1'").execute(&mut *tx).await?;
|
||||||
|
tx.commit().await?;
|
||||||
|
Ok(MigrationReport {
|
||||||
|
legacy_version,
|
||||||
|
tables: counts,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn common_columns(
|
||||||
|
connection: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
|
||||||
|
table: &str,
|
||||||
|
) -> Result<Vec<String>> {
|
||||||
|
let source_rows = sqlx::query(AssertSqlSafe(format!(
|
||||||
|
"PRAGMA legacy.table_info(\"{table}\")"
|
||||||
|
)))
|
||||||
|
.fetch_all(&mut **connection)
|
||||||
|
.await?;
|
||||||
|
if source_rows.is_empty() {
|
||||||
|
return Err(TwonlyError::Generic(format!(
|
||||||
|
"Legacy database is missing table {table}"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
let source: BTreeSet<String> = source_rows
|
||||||
|
.iter()
|
||||||
|
.map(|row| row.get::<String, _>("name"))
|
||||||
|
.collect();
|
||||||
|
let target_rows = sqlx::query(AssertSqlSafe(format!(
|
||||||
|
"PRAGMA main.table_info(\"{table}\")"
|
||||||
|
)))
|
||||||
|
.fetch_all(&mut **connection)
|
||||||
|
.await?;
|
||||||
|
Ok(target_rows
|
||||||
|
.iter()
|
||||||
|
.map(|row| row.get::<String, _>("name"))
|
||||||
|
.filter(|column| source.contains(column))
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn copy_sequences(connection: &mut sqlx::Transaction<'_, sqlx::Sqlite>) -> Result<()> {
|
||||||
|
for table in [
|
||||||
|
"message_histories",
|
||||||
|
"key_verifications",
|
||||||
|
"verification_tokens",
|
||||||
|
"user_discovery_own_promotions",
|
||||||
|
"user_discovery_shares",
|
||||||
|
"shortcuts",
|
||||||
|
"labels",
|
||||||
|
] {
|
||||||
|
let source_sequence: Option<i64> =
|
||||||
|
sqlx::query_scalar("SELECT seq FROM legacy.sqlite_sequence WHERE name = ?")
|
||||||
|
.bind(table)
|
||||||
|
.fetch_optional(&mut **connection)
|
||||||
|
.await?;
|
||||||
|
if let Some(sequence) = source_sequence {
|
||||||
|
sqlx::query("DELETE FROM main.sqlite_sequence WHERE name = ?")
|
||||||
|
.bind(table)
|
||||||
|
.execute(&mut **connection)
|
||||||
|
.await?;
|
||||||
|
sqlx::query("INSERT INTO main.sqlite_sequence(name, seq) VALUES(?, ?)")
|
||||||
|
.bind(table)
|
||||||
|
.bind(sequence)
|
||||||
|
.execute(&mut **connection)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::database::app::{SqlValue, APPLICATION_TABLES};
|
||||||
|
use sqlx::{AssertSqlSafe, SqlitePool};
|
||||||
|
use tempfile::tempdir;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn imports_all_application_tables_and_is_idempotent() {
|
||||||
|
let directory = tempdir().unwrap();
|
||||||
|
let legacy_path = directory.path().join("twonly.sqlite");
|
||||||
|
let target_path = directory.path().join("app_db.sqlite");
|
||||||
|
|
||||||
|
let legacy = AppDatabase::new(legacy_path.to_str().unwrap(), None, false)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
legacy.run_migrations().await.unwrap();
|
||||||
|
sqlx::query("PRAGMA user_version = 25")
|
||||||
|
.execute(&legacy.pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
populate_every_table(&legacy.pool).await;
|
||||||
|
create_and_populate_drift_signal_tables(&legacy.pool).await;
|
||||||
|
legacy.pool.close().await;
|
||||||
|
|
||||||
|
let target = AppDatabase::new(target_path.to_str().unwrap(), Some("test-key"), false)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
target.run_migrations().await.unwrap();
|
||||||
|
let first = target.import_legacy(&legacy_path).await.unwrap();
|
||||||
|
assert_eq!(first.tables.len(), APPLICATION_TABLES.len());
|
||||||
|
assert!(first.tables.iter().all(|entry| entry.rows == 1));
|
||||||
|
let second = target.import_legacy(&legacy_path).await.unwrap();
|
||||||
|
assert_eq!(first, second);
|
||||||
|
assert!(target.is_legacy_import_complete().await.unwrap());
|
||||||
|
|
||||||
|
let selected = target
|
||||||
|
.raw_select(
|
||||||
|
"SELECT username, avatar_svg_compressed FROM contacts WHERE user_id = ?".to_owned(),
|
||||||
|
vec![SqlValue::integer(7)],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(selected.columns, vec!["username", "avatar_svg_compressed"]);
|
||||||
|
assert_eq!(
|
||||||
|
selected.rows[0].values[0],
|
||||||
|
SqlValue::text("alice".to_owned())
|
||||||
|
);
|
||||||
|
assert_eq!(selected.rows[0].values[1], SqlValue::blob(vec![0, 1, 255]));
|
||||||
|
|
||||||
|
target
|
||||||
|
.raw_execute("BEGIN TRANSACTION".to_owned(), vec![])
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
target
|
||||||
|
.raw_execute(
|
||||||
|
"UPDATE contacts SET username = ? WHERE user_id = ?".to_owned(),
|
||||||
|
vec![SqlValue::text("changed".to_owned()), SqlValue::integer(7)],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
target
|
||||||
|
.raw_execute("ROLLBACK".to_owned(), vec![])
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let username: String =
|
||||||
|
sqlx::query_scalar("SELECT username FROM contacts WHERE user_id = 7")
|
||||||
|
.fetch_one(&target.pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(username, "alice");
|
||||||
|
|
||||||
|
let legacy_check = AppDatabase::new(legacy_path.to_str().unwrap(), None, true)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let signal_checks = [
|
||||||
|
(
|
||||||
|
"signal_identity_key_stores",
|
||||||
|
"identity_key",
|
||||||
|
vec![0, 1, 2, 255],
|
||||||
|
),
|
||||||
|
("signal_pre_key_stores", "pre_key", vec![3, 4, 5]),
|
||||||
|
("signal_sender_key_stores", "sender_key", vec![6, 7, 8]),
|
||||||
|
("signal_session_stores", "session_record", vec![9, 10, 11]),
|
||||||
|
(
|
||||||
|
"signal_signed_pre_key_stores",
|
||||||
|
"signed_pre_key",
|
||||||
|
vec![12, 13, 14],
|
||||||
|
),
|
||||||
|
];
|
||||||
|
for (table, column, expected) in signal_checks {
|
||||||
|
let actual: Vec<u8> = sqlx::query_scalar(AssertSqlSafe(format!(
|
||||||
|
"SELECT {column} FROM {table} LIMIT 1"
|
||||||
|
)))
|
||||||
|
.fetch_one(&legacy_check.pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(actual, expected, "Signal data changed in {table}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn populate_every_table(pool: &SqlitePool) {
|
||||||
|
let statements = [
|
||||||
|
"INSERT INTO contacts(user_id, username, avatar_svg_compressed, signal_version) VALUES(7, 'alice', x'0001FF', 'v2')",
|
||||||
|
"INSERT INTO groups(group_id, group_name) VALUES('group-1', 'Friends')",
|
||||||
|
"INSERT INTO media_files(media_id, type, encryption_key) VALUES('media-1', 'image', x'1020')",
|
||||||
|
"INSERT INTO messages(group_id, message_id, sender_id, type, content, media_id) VALUES('group-1', 'message-1', 7, 'media', '', 'media-1')",
|
||||||
|
"INSERT INTO message_histories(id, message_id, contact_id, content) VALUES(11, 'message-1', 7, NULL)",
|
||||||
|
"INSERT INTO reactions(message_id, emoji, sender_id) VALUES('message-1', '👍', 7)",
|
||||||
|
"INSERT INTO group_members(group_id, contact_id, member_state) VALUES('group-1', 7, 'admin')",
|
||||||
|
"INSERT INTO receipts(receipt_id, contact_id, message_id, message) VALUES('receipt-1', 7, 'message-1', x'00FF')",
|
||||||
|
"INSERT INTO received_receipts(receipt_id) VALUES('received-1')",
|
||||||
|
"INSERT INTO message_actions(message_id, contact_id, type) VALUES('message-1', 7, 'openedAt')",
|
||||||
|
"INSERT INTO group_histories(group_history_id, group_id, contact_id, type) VALUES('history-1', 'group-1', 7, 'createdGroup')",
|
||||||
|
"INSERT INTO key_verifications(verification_id, contact_id, type, verified_by) VALUES(13, 7, 'qrScanned', NULL)",
|
||||||
|
"INSERT INTO verification_tokens(token_id, token) VALUES(17, x'ABCDEF')",
|
||||||
|
"INSERT INTO user_discovery_announced_users(announced_user_id, announced_public_key, public_id, username) VALUES(19, x'01', 23, 'bob')",
|
||||||
|
"INSERT INTO user_discovery_user_relations(announced_user_id, from_contact_id) VALUES(19, 7)",
|
||||||
|
"INSERT INTO user_discovery_other_promotions(from_contact_id, promotion_id, public_id, threshold, announcement_share) VALUES(7, 29, 31, 3, x'02')",
|
||||||
|
"INSERT INTO user_discovery_own_promotions(version_id, contact_id, promotion) VALUES(37, 7, x'03')",
|
||||||
|
"INSERT INTO user_discovery_shares(share_id, share, contact_id) VALUES(41, x'04', 7)",
|
||||||
|
"INSERT INTO shortcuts(id, emoji, usage_counter) VALUES(43, '🔥', 5)",
|
||||||
|
"INSERT INTO shortcut_members(shortcut_id, group_id) VALUES(43, 'group-1')",
|
||||||
|
"INSERT INTO labels(id, name, text_color, background_color) VALUES(47, 'Family', 1, 2)",
|
||||||
|
"INSERT INTO contact_labels(contact_id, label_id) VALUES(7, 47)",
|
||||||
|
];
|
||||||
|
for statement in statements {
|
||||||
|
sqlx::query(statement).execute(pool).await.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_and_populate_drift_signal_tables(pool: &SqlitePool) {
|
||||||
|
let statements = [
|
||||||
|
"CREATE TABLE signal_identity_key_stores(device_id INTEGER NOT NULL, name TEXT NOT NULL, identity_key BLOB NOT NULL, created_at INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)), PRIMARY KEY(device_id, name))",
|
||||||
|
"CREATE TABLE signal_pre_key_stores(pre_key_id INTEGER NOT NULL PRIMARY KEY, pre_key BLOB NOT NULL, created_at INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)))",
|
||||||
|
"CREATE TABLE signal_sender_key_stores(sender_key_name TEXT NOT NULL PRIMARY KEY, sender_key BLOB NOT NULL)",
|
||||||
|
"CREATE TABLE signal_session_stores(device_id INTEGER NOT NULL, name TEXT NOT NULL, session_record BLOB NOT NULL, created_at INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)), PRIMARY KEY(device_id, name))",
|
||||||
|
"CREATE TABLE signal_signed_pre_key_stores(signed_pre_key_id INTEGER NOT NULL PRIMARY KEY, signed_pre_key BLOB NOT NULL, created_at INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)))",
|
||||||
|
"INSERT INTO signal_identity_key_stores(device_id, name, identity_key) VALUES(1, 'alice', x'000102FF')",
|
||||||
|
"INSERT INTO signal_pre_key_stores(pre_key_id, pre_key) VALUES(2, x'030405')",
|
||||||
|
"INSERT INTO signal_sender_key_stores(sender_key_name, sender_key) VALUES('group', x'060708')",
|
||||||
|
"INSERT INTO signal_session_stores(device_id, name, session_record) VALUES(3, 'bob', x'090A0B')",
|
||||||
|
"INSERT INTO signal_signed_pre_key_stores(signed_pre_key_id, signed_pre_key) VALUES(4, x'0C0D0E')",
|
||||||
|
];
|
||||||
|
for statement in statements {
|
||||||
|
sqlx::query(statement).execute(pool).await.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
258
rust/src/database/app/migrations/0001_initial.sql
Normal file
258
rust/src/database/app/migrations/0001_initial.sql
Normal file
|
|
@ -0,0 +1,258 @@
|
||||||
|
CREATE TABLE contacts (
|
||||||
|
user_id INTEGER NOT NULL PRIMARY KEY,
|
||||||
|
username TEXT NOT NULL,
|
||||||
|
display_name TEXT,
|
||||||
|
nick_name TEXT,
|
||||||
|
avatar_svg_compressed BLOB,
|
||||||
|
sender_profile_counter INTEGER NOT NULL DEFAULT 0,
|
||||||
|
accepted INTEGER NOT NULL DEFAULT 0 CHECK (accepted IN (0, 1)),
|
||||||
|
deleted_by_user INTEGER NOT NULL DEFAULT 0 CHECK (deleted_by_user IN (0, 1)),
|
||||||
|
requested INTEGER NOT NULL DEFAULT 0 CHECK (requested IN (0, 1)),
|
||||||
|
blocked INTEGER NOT NULL DEFAULT 0 CHECK (blocked IN (0, 1)),
|
||||||
|
verified INTEGER NOT NULL DEFAULT 0 CHECK (verified IN (0, 1)),
|
||||||
|
account_deleted INTEGER NOT NULL DEFAULT 0 CHECK (account_deleted IN (0, 1)),
|
||||||
|
created_at INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)),
|
||||||
|
signal_version TEXT NOT NULL DEFAULT 'v1',
|
||||||
|
user_discovery_version BLOB,
|
||||||
|
user_discovery_excluded INTEGER NOT NULL DEFAULT 0 CHECK (user_discovery_excluded IN (0, 1)),
|
||||||
|
user_discovery_manual_approved INTEGER DEFAULT 0 CHECK (user_discovery_manual_approved IN (0, 1)),
|
||||||
|
recovery_is_trusted_friend INTEGER NOT NULL DEFAULT 0 CHECK (recovery_is_trusted_friend IN (0, 1)),
|
||||||
|
recovery_last_heartbeat INTEGER,
|
||||||
|
recovery_secret_share BLOB,
|
||||||
|
recovery_contacts_secret_share BLOB,
|
||||||
|
recovery_contacts_last_heartbeat INTEGER,
|
||||||
|
recovery_contacts_threshold INTEGER,
|
||||||
|
ask_for_friend_promotions INTEGER CHECK (ask_for_friend_promotions IN (0, 1)),
|
||||||
|
media_send_counter INTEGER NOT NULL DEFAULT 0,
|
||||||
|
media_received_counter INTEGER NOT NULL DEFAULT 0
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE groups (
|
||||||
|
group_id TEXT NOT NULL PRIMARY KEY,
|
||||||
|
is_group_admin INTEGER NOT NULL DEFAULT 0 CHECK (is_group_admin IN (0, 1)),
|
||||||
|
is_direct_chat INTEGER NOT NULL DEFAULT 0 CHECK (is_direct_chat IN (0, 1)),
|
||||||
|
pinned INTEGER NOT NULL DEFAULT 0 CHECK (pinned IN (0, 1)),
|
||||||
|
archived INTEGER NOT NULL DEFAULT 0 CHECK (archived IN (0, 1)),
|
||||||
|
joined_group INTEGER NOT NULL DEFAULT 0 CHECK (joined_group IN (0, 1)),
|
||||||
|
left_group INTEGER NOT NULL DEFAULT 0 CHECK (left_group IN (0, 1)),
|
||||||
|
deleted_content INTEGER NOT NULL DEFAULT 0 CHECK (deleted_content IN (0, 1)),
|
||||||
|
state_version_id INTEGER NOT NULL DEFAULT 0,
|
||||||
|
state_encryption_key BLOB,
|
||||||
|
my_group_private_key BLOB,
|
||||||
|
group_name TEXT NOT NULL,
|
||||||
|
draft_message TEXT,
|
||||||
|
total_media_counter INTEGER NOT NULL DEFAULT 0,
|
||||||
|
also_best_friend INTEGER NOT NULL DEFAULT 0 CHECK (also_best_friend IN (0, 1)),
|
||||||
|
delete_messages_after_milliseconds INTEGER NOT NULL DEFAULT 86400000,
|
||||||
|
created_at INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)),
|
||||||
|
last_message_send INTEGER,
|
||||||
|
last_message_received INTEGER,
|
||||||
|
last_flame_counter_change INTEGER,
|
||||||
|
last_flame_sync INTEGER,
|
||||||
|
flame_counter INTEGER NOT NULL DEFAULT 0,
|
||||||
|
max_flame_counter INTEGER NOT NULL DEFAULT 0,
|
||||||
|
max_flame_counter_from INTEGER,
|
||||||
|
last_message_exchange INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER))
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE media_files (
|
||||||
|
media_id TEXT NOT NULL PRIMARY KEY,
|
||||||
|
type TEXT NOT NULL,
|
||||||
|
upload_state TEXT,
|
||||||
|
cloud_state TEXT NOT NULL DEFAULT 'none',
|
||||||
|
blurhash TEXT,
|
||||||
|
download_state TEXT,
|
||||||
|
requires_authentication INTEGER NOT NULL DEFAULT 0 CHECK (requires_authentication IN (0, 1)),
|
||||||
|
stored INTEGER NOT NULL DEFAULT 0 CHECK (stored IN (0, 1)),
|
||||||
|
is_draft_media INTEGER NOT NULL DEFAULT 0 CHECK (is_draft_media IN (0, 1)),
|
||||||
|
is_favorite INTEGER NOT NULL DEFAULT 0 CHECK (is_favorite IN (0, 1)),
|
||||||
|
has_crop_analyzed INTEGER NOT NULL DEFAULT 0 CHECK (has_crop_analyzed IN (0, 1)),
|
||||||
|
pre_progressing_process INTEGER,
|
||||||
|
reupload_requested_by TEXT,
|
||||||
|
display_limit_in_milliseconds INTEGER,
|
||||||
|
remove_audio INTEGER CHECK (remove_audio IN (0, 1)),
|
||||||
|
download_token BLOB,
|
||||||
|
encryption_key BLOB,
|
||||||
|
encryption_mac BLOB,
|
||||||
|
encryption_nonce BLOB,
|
||||||
|
stored_file_hash BLOB,
|
||||||
|
has_thumbnail INTEGER NOT NULL DEFAULT 0 CHECK (has_thumbnail IN (0, 1)),
|
||||||
|
size_in_bytes INTEGER,
|
||||||
|
created_at INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)),
|
||||||
|
created_at_month TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE messages (
|
||||||
|
group_id TEXT NOT NULL REFERENCES groups(group_id) ON DELETE CASCADE,
|
||||||
|
message_id TEXT NOT NULL PRIMARY KEY,
|
||||||
|
sender_id INTEGER REFERENCES contacts(user_id),
|
||||||
|
type TEXT NOT NULL,
|
||||||
|
content TEXT,
|
||||||
|
media_id TEXT REFERENCES media_files(media_id) ON DELETE SET NULL,
|
||||||
|
additional_message_data BLOB,
|
||||||
|
media_stored INTEGER NOT NULL DEFAULT 0 CHECK (media_stored IN (0, 1)),
|
||||||
|
media_reopened INTEGER NOT NULL DEFAULT 0 CHECK (media_reopened IN (0, 1)),
|
||||||
|
download_token BLOB,
|
||||||
|
quotes_message_id TEXT,
|
||||||
|
is_deleted_from_sender INTEGER NOT NULL DEFAULT 0 CHECK (is_deleted_from_sender IN (0, 1)),
|
||||||
|
opened_at INTEGER,
|
||||||
|
opened_by_all INTEGER,
|
||||||
|
created_at INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)),
|
||||||
|
modified_at INTEGER,
|
||||||
|
ack_by_user INTEGER,
|
||||||
|
ack_by_server INTEGER
|
||||||
|
);
|
||||||
|
CREATE INDEX idx_messages_group_id_created_at ON messages(group_id, created_at);
|
||||||
|
|
||||||
|
CREATE TABLE message_histories (
|
||||||
|
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
message_id TEXT NOT NULL REFERENCES messages(message_id) ON DELETE CASCADE,
|
||||||
|
contact_id INTEGER REFERENCES contacts(user_id) ON DELETE CASCADE,
|
||||||
|
content TEXT,
|
||||||
|
created_at INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER))
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE reactions (
|
||||||
|
message_id TEXT NOT NULL REFERENCES messages(message_id) ON DELETE CASCADE,
|
||||||
|
emoji TEXT NOT NULL,
|
||||||
|
sender_id INTEGER REFERENCES contacts(user_id) ON DELETE CASCADE,
|
||||||
|
created_at INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)),
|
||||||
|
PRIMARY KEY (message_id, sender_id, emoji)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE group_members (
|
||||||
|
group_id TEXT NOT NULL REFERENCES groups(group_id) ON DELETE CASCADE,
|
||||||
|
contact_id INTEGER NOT NULL REFERENCES contacts(user_id),
|
||||||
|
member_state TEXT,
|
||||||
|
group_public_key BLOB,
|
||||||
|
last_chat_opened INTEGER,
|
||||||
|
last_type_indicator INTEGER,
|
||||||
|
last_message INTEGER,
|
||||||
|
created_at INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)),
|
||||||
|
PRIMARY KEY (group_id, contact_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE receipts (
|
||||||
|
receipt_id TEXT NOT NULL PRIMARY KEY,
|
||||||
|
contact_id INTEGER NOT NULL REFERENCES contacts(user_id) ON DELETE CASCADE,
|
||||||
|
message_id TEXT REFERENCES messages(message_id) ON DELETE CASCADE,
|
||||||
|
message BLOB NOT NULL,
|
||||||
|
contact_will_sends_receipt INTEGER NOT NULL DEFAULT 1 CHECK (contact_will_sends_receipt IN (0, 1)),
|
||||||
|
will_be_retried_by_media_upload INTEGER NOT NULL DEFAULT 0 CHECK (will_be_retried_by_media_upload IN (0, 1)),
|
||||||
|
mark_for_retry INTEGER,
|
||||||
|
mark_for_retry_after_accepted INTEGER,
|
||||||
|
ack_by_server_at INTEGER,
|
||||||
|
retry_count INTEGER NOT NULL DEFAULT 0,
|
||||||
|
last_retry INTEGER,
|
||||||
|
created_at INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER))
|
||||||
|
);
|
||||||
|
CREATE INDEX idx_receipts_message_id ON receipts(message_id);
|
||||||
|
|
||||||
|
CREATE TABLE received_receipts (
|
||||||
|
receipt_id TEXT NOT NULL PRIMARY KEY,
|
||||||
|
created_at INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER))
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE message_actions (
|
||||||
|
message_id TEXT NOT NULL REFERENCES messages(message_id) ON DELETE CASCADE,
|
||||||
|
contact_id INTEGER NOT NULL REFERENCES contacts(user_id) ON DELETE CASCADE,
|
||||||
|
type TEXT NOT NULL,
|
||||||
|
action_at INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)),
|
||||||
|
PRIMARY KEY (message_id, contact_id, type)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE group_histories (
|
||||||
|
group_history_id TEXT NOT NULL PRIMARY KEY,
|
||||||
|
group_id TEXT NOT NULL REFERENCES groups(group_id) ON DELETE CASCADE,
|
||||||
|
contact_id INTEGER REFERENCES contacts(user_id),
|
||||||
|
affected_contact_id INTEGER,
|
||||||
|
old_group_name TEXT,
|
||||||
|
new_group_name TEXT,
|
||||||
|
new_delete_messages_after_milliseconds INTEGER,
|
||||||
|
type TEXT NOT NULL,
|
||||||
|
action_at INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER))
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE key_verifications (
|
||||||
|
verification_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
contact_id INTEGER NOT NULL REFERENCES contacts(user_id) ON DELETE CASCADE,
|
||||||
|
type TEXT NOT NULL,
|
||||||
|
verified_by INTEGER REFERENCES contacts(user_id) ON DELETE CASCADE,
|
||||||
|
created_at INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER))
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE verification_tokens (
|
||||||
|
token_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
token BLOB NOT NULL,
|
||||||
|
created_at INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER))
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE user_discovery_announced_users (
|
||||||
|
announced_user_id INTEGER NOT NULL PRIMARY KEY,
|
||||||
|
announced_public_key BLOB NOT NULL,
|
||||||
|
public_id INTEGER NOT NULL UNIQUE,
|
||||||
|
username TEXT,
|
||||||
|
was_shown_to_the_user INTEGER NOT NULL DEFAULT 0 CHECK (was_shown_to_the_user IN (0, 1)),
|
||||||
|
is_hidden INTEGER NOT NULL DEFAULT 0 CHECK (is_hidden IN (0, 1)),
|
||||||
|
was_asked_friends INTEGER NOT NULL DEFAULT 0 CHECK (was_asked_friends IN (0, 1))
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE user_discovery_user_relations (
|
||||||
|
announced_user_id INTEGER NOT NULL REFERENCES user_discovery_announced_users(announced_user_id) ON DELETE CASCADE,
|
||||||
|
from_contact_id INTEGER NOT NULL REFERENCES contacts(user_id) ON DELETE CASCADE,
|
||||||
|
public_key_verified_timestamp INTEGER,
|
||||||
|
PRIMARY KEY (announced_user_id, from_contact_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE user_discovery_other_promotions (
|
||||||
|
from_contact_id INTEGER NOT NULL REFERENCES contacts(user_id) ON DELETE CASCADE,
|
||||||
|
promotion_id INTEGER NOT NULL,
|
||||||
|
public_id INTEGER NOT NULL,
|
||||||
|
threshold INTEGER NOT NULL,
|
||||||
|
announcement_share BLOB NOT NULL,
|
||||||
|
public_key_verified_timestamp INTEGER,
|
||||||
|
PRIMARY KEY (from_contact_id, public_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE user_discovery_own_promotions (
|
||||||
|
version_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
contact_id INTEGER NOT NULL REFERENCES contacts(user_id) ON DELETE CASCADE,
|
||||||
|
promotion BLOB NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE user_discovery_shares (
|
||||||
|
share_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
share BLOB NOT NULL,
|
||||||
|
contact_id INTEGER REFERENCES contacts(user_id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE shortcuts (
|
||||||
|
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
emoji TEXT NOT NULL UNIQUE,
|
||||||
|
usage_counter INTEGER NOT NULL DEFAULT 0
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE shortcut_members (
|
||||||
|
shortcut_id INTEGER NOT NULL REFERENCES shortcuts(id) ON DELETE CASCADE,
|
||||||
|
group_id TEXT NOT NULL REFERENCES groups(group_id) ON DELETE CASCADE,
|
||||||
|
PRIMARY KEY (shortcut_id, group_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE labels (
|
||||||
|
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
text_color INTEGER NOT NULL,
|
||||||
|
background_color INTEGER NOT NULL,
|
||||||
|
created_at INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER))
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE contact_labels (
|
||||||
|
contact_id INTEGER NOT NULL REFERENCES contacts(user_id) ON DELETE CASCADE,
|
||||||
|
label_id INTEGER NOT NULL REFERENCES labels(id) ON DELETE CASCADE,
|
||||||
|
PRIMARY KEY (contact_id, label_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE app_metadata (
|
||||||
|
key TEXT NOT NULL PRIMARY KEY,
|
||||||
|
value TEXT NOT NULL
|
||||||
|
);
|
||||||
300
rust/src/database/app/mod.rs
Normal file
300
rust/src/database/app/mod.rs
Normal file
|
|
@ -0,0 +1,300 @@
|
||||||
|
use crate::error::{Result, TwonlyError};
|
||||||
|
use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
|
||||||
|
use sqlx::{AssertSqlSafe, Column, ConnectOptions, Row, SqlitePool, TypeInfo, ValueRef};
|
||||||
|
use std::collections::BTreeSet;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use std::time::Duration;
|
||||||
|
use tokio::sync::broadcast;
|
||||||
|
|
||||||
|
mod legacy_import;
|
||||||
|
|
||||||
|
pub const APP_DATABASE_FILE: &str = "app_db.sqlite";
|
||||||
|
pub const APP_SCHEMA_VERSION: i64 = 1;
|
||||||
|
|
||||||
|
pub const APPLICATION_TABLES: &[&str] = &[
|
||||||
|
"contacts",
|
||||||
|
"groups",
|
||||||
|
"media_files",
|
||||||
|
"messages",
|
||||||
|
"message_histories",
|
||||||
|
"reactions",
|
||||||
|
"group_members",
|
||||||
|
"receipts",
|
||||||
|
"received_receipts",
|
||||||
|
"message_actions",
|
||||||
|
"group_histories",
|
||||||
|
"key_verifications",
|
||||||
|
"verification_tokens",
|
||||||
|
"user_discovery_announced_users",
|
||||||
|
"user_discovery_user_relations",
|
||||||
|
"user_discovery_other_promotions",
|
||||||
|
"user_discovery_own_promotions",
|
||||||
|
"user_discovery_shares",
|
||||||
|
"shortcuts",
|
||||||
|
"shortcut_members",
|
||||||
|
"labels",
|
||||||
|
"contact_labels",
|
||||||
|
];
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct DatabaseChange {
|
||||||
|
pub tables: BTreeSet<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AppDatabase {
|
||||||
|
pub pool: SqlitePool,
|
||||||
|
changes: broadcast::Sender<DatabaseChange>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppDatabase {
|
||||||
|
pub async fn new(db_path: &str, encryption_key: Option<&str>, read_only: bool) -> Result<Self> {
|
||||||
|
let db_url = format!("sqlite://{db_path}");
|
||||||
|
let mut options = SqliteConnectOptions::from_str(&format!("{db_url}?mode=rwc"))?
|
||||||
|
.create_if_missing(!read_only)
|
||||||
|
.journal_mode(sqlx::sqlite::SqliteJournalMode::Delete)
|
||||||
|
.foreign_keys(true)
|
||||||
|
.read_only(read_only)
|
||||||
|
.busy_timeout(Duration::from_secs(30))
|
||||||
|
.pragma("synchronous", "FULL")
|
||||||
|
.log_statements(tracing::log::LevelFilter::Off)
|
||||||
|
.log_slow_statements(tracing::log::LevelFilter::Warn, Duration::from_millis(500));
|
||||||
|
if let Some(key) = encryption_key {
|
||||||
|
options = options.pragma("key", format!("'{key}'"));
|
||||||
|
}
|
||||||
|
let pool = SqlitePoolOptions::new()
|
||||||
|
// The compatibility executor uses statement-based transactions.
|
||||||
|
// Keeping one connection guarantees BEGIN, all statements, and
|
||||||
|
// COMMIT are executed on that same native connection.
|
||||||
|
.max_connections(1)
|
||||||
|
.acquire_timeout(Duration::from_secs(30))
|
||||||
|
.connect_with(options)
|
||||||
|
.await?;
|
||||||
|
let (changes, _) = broadcast::channel(256);
|
||||||
|
Ok(Self { pool, changes })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run_migrations(&self) -> Result<()> {
|
||||||
|
sqlx::migrate!("./src/database/app/migrations")
|
||||||
|
.run(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(|error| {
|
||||||
|
TwonlyError::Generic(format!("App database migration failed: {error}"))
|
||||||
|
})?;
|
||||||
|
sqlx::query("INSERT INTO app_metadata(key, value) VALUES('schema_version', ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value")
|
||||||
|
.bind(APP_SCHEMA_VERSION.to_string())
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn subscribe(&self) -> broadcast::Receiver<DatabaseChange> {
|
||||||
|
self.changes.subscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn notify_committed<'a>(&self, tables: impl IntoIterator<Item = &'a str>) {
|
||||||
|
let tables = tables.into_iter().map(str::to_owned).collect();
|
||||||
|
let _ = self.changes.send(DatabaseChange { tables });
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn raw_select(&self, statement: String, arguments: Vec<SqlValue>) -> Result<SqlRows> {
|
||||||
|
let rows = bind_arguments(sqlx::query(AssertSqlSafe(statement)), arguments)?
|
||||||
|
.fetch_all(&self.pool)
|
||||||
|
.await?;
|
||||||
|
let columns = rows
|
||||||
|
.first()
|
||||||
|
.map(|row| {
|
||||||
|
row.columns()
|
||||||
|
.iter()
|
||||||
|
.map(|column| column.name().to_owned())
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
let mut output_rows = Vec::with_capacity(rows.len());
|
||||||
|
for row in rows {
|
||||||
|
let mut values = Vec::with_capacity(row.len());
|
||||||
|
for index in 0..row.len() {
|
||||||
|
let raw = row.try_get_raw(index)?;
|
||||||
|
let value = if raw.is_null() {
|
||||||
|
SqlValue::null()
|
||||||
|
} else {
|
||||||
|
match raw.type_info().name() {
|
||||||
|
"INTEGER" | "INT" | "BOOLEAN" | "DATETIME" => {
|
||||||
|
SqlValue::integer(row.try_get(index)?)
|
||||||
|
}
|
||||||
|
"REAL" | "FLOAT" | "DOUBLE" => SqlValue::real(row.try_get(index)?),
|
||||||
|
"BLOB" => SqlValue::blob(row.try_get(index)?),
|
||||||
|
_ => SqlValue::text(row.try_get(index)?),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
values.push(value);
|
||||||
|
}
|
||||||
|
output_rows.push(SqlRow { values });
|
||||||
|
}
|
||||||
|
Ok(SqlRows {
|
||||||
|
columns,
|
||||||
|
rows: output_rows,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn raw_execute(
|
||||||
|
&self,
|
||||||
|
statement: String,
|
||||||
|
arguments: Vec<SqlValue>,
|
||||||
|
) -> Result<SqlExecutionResult> {
|
||||||
|
let result = bind_arguments(sqlx::query(AssertSqlSafe(statement)), arguments)?
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await?;
|
||||||
|
Ok(SqlExecutionResult {
|
||||||
|
affected_rows: result.rows_affected() as i64,
|
||||||
|
last_insert_row_id: result.last_insert_rowid(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_backup(&self, output_path: &str, encryption_key: &str) -> Result<()> {
|
||||||
|
let mut connection = self.pool.acquire().await?;
|
||||||
|
sqlx::query("ATTACH DATABASE ? AS backup KEY ?")
|
||||||
|
.bind(output_path)
|
||||||
|
.bind(encryption_key)
|
||||||
|
.execute(&mut *connection)
|
||||||
|
.await
|
||||||
|
.map_err(|error| TwonlyError::Generic(format!("Attach app backup failed: {error}")))?;
|
||||||
|
let export = sqlx::query("SELECT sqlcipher_export('backup')")
|
||||||
|
.execute(&mut *connection)
|
||||||
|
.await;
|
||||||
|
let detach = sqlx::query("DETACH DATABASE backup")
|
||||||
|
.execute(&mut *connection)
|
||||||
|
.await;
|
||||||
|
export.map_err(|error| TwonlyError::Generic(format!("App export failed: {error}")))?;
|
||||||
|
detach
|
||||||
|
.map_err(|error| TwonlyError::Generic(format!("Detach app backup failed: {error}")))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct SqlValue {
|
||||||
|
/// 0 = null, 1 = integer, 2 = real, 3 = text, 4 = blob.
|
||||||
|
pub kind: u8,
|
||||||
|
pub integer_value: Option<i64>,
|
||||||
|
pub real_value: Option<f64>,
|
||||||
|
pub text_value: Option<String>,
|
||||||
|
pub blob_value: Option<Vec<u8>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SqlValue {
|
||||||
|
fn null() -> Self {
|
||||||
|
Self {
|
||||||
|
kind: 0,
|
||||||
|
integer_value: None,
|
||||||
|
real_value: None,
|
||||||
|
text_value: None,
|
||||||
|
blob_value: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn integer(value: i64) -> Self {
|
||||||
|
Self {
|
||||||
|
kind: 1,
|
||||||
|
integer_value: Some(value),
|
||||||
|
real_value: None,
|
||||||
|
text_value: None,
|
||||||
|
blob_value: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn real(value: f64) -> Self {
|
||||||
|
Self {
|
||||||
|
kind: 2,
|
||||||
|
integer_value: None,
|
||||||
|
real_value: Some(value),
|
||||||
|
text_value: None,
|
||||||
|
blob_value: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn text(value: String) -> Self {
|
||||||
|
Self {
|
||||||
|
kind: 3,
|
||||||
|
integer_value: None,
|
||||||
|
real_value: None,
|
||||||
|
text_value: Some(value),
|
||||||
|
blob_value: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn blob(value: Vec<u8>) -> Self {
|
||||||
|
Self {
|
||||||
|
kind: 4,
|
||||||
|
integer_value: None,
|
||||||
|
real_value: None,
|
||||||
|
text_value: None,
|
||||||
|
blob_value: Some(value),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct SqlRows {
|
||||||
|
pub columns: Vec<String>,
|
||||||
|
pub rows: Vec<SqlRow>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct SqlRow {
|
||||||
|
pub values: Vec<SqlValue>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct SqlExecutionResult {
|
||||||
|
pub affected_rows: i64,
|
||||||
|
pub last_insert_row_id: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bind_arguments<'q>(
|
||||||
|
mut query: sqlx::query::Query<'q, sqlx::Sqlite, sqlx::sqlite::SqliteArguments>,
|
||||||
|
arguments: Vec<SqlValue>,
|
||||||
|
) -> Result<sqlx::query::Query<'q, sqlx::Sqlite, sqlx::sqlite::SqliteArguments>> {
|
||||||
|
for argument in arguments {
|
||||||
|
query = match argument {
|
||||||
|
SqlValue { kind: 0, .. } => query.bind(Option::<i64>::None),
|
||||||
|
SqlValue {
|
||||||
|
kind: 1,
|
||||||
|
integer_value: Some(value),
|
||||||
|
..
|
||||||
|
} => query.bind(value),
|
||||||
|
SqlValue {
|
||||||
|
kind: 2,
|
||||||
|
real_value: Some(value),
|
||||||
|
..
|
||||||
|
} => query.bind(value),
|
||||||
|
SqlValue {
|
||||||
|
kind: 3,
|
||||||
|
text_value: Some(value),
|
||||||
|
..
|
||||||
|
} => query.bind(value),
|
||||||
|
SqlValue {
|
||||||
|
kind: 4,
|
||||||
|
blob_value: Some(value),
|
||||||
|
..
|
||||||
|
} => query.bind(value),
|
||||||
|
invalid => {
|
||||||
|
return Err(TwonlyError::Generic(format!(
|
||||||
|
"Invalid SQL value received from bridge: {invalid:?}"
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Ok(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct MigrationReport {
|
||||||
|
pub legacy_version: i64,
|
||||||
|
pub tables: Vec<TableMigrationCount>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct TableMigrationCount {
|
||||||
|
pub table: String,
|
||||||
|
pub rows: i64,
|
||||||
|
}
|
||||||
|
|
@ -1,203 +1,2 @@
|
||||||
use crate::error::{Result, TwonlyError};
|
pub mod app;
|
||||||
use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
|
pub mod signal;
|
||||||
use sqlx::{ConnectOptions, SqlitePool};
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
pub(crate) mod tables;
|
|
||||||
|
|
||||||
pub struct Database {
|
|
||||||
pub pool: SqlitePool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Database {
|
|
||||||
pub async fn new(
|
|
||||||
db_path: &String,
|
|
||||||
encryption_key: Option<&str>,
|
|
||||||
read_only: bool,
|
|
||||||
) -> Result<Self> {
|
|
||||||
let db_url = format!("sqlite://{}", db_path);
|
|
||||||
|
|
||||||
let log_statements_level = if std::env::var("SQLX_LOG_STATEMENTS").is_ok() {
|
|
||||||
tracing::log::LevelFilter::Info
|
|
||||||
} else {
|
|
||||||
tracing::log::LevelFilter::Off
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut connect_options = format!("{db_url}?mode=rwc")
|
|
||||||
.parse::<SqliteConnectOptions>()?
|
|
||||||
.log_statements(log_statements_level)
|
|
||||||
.journal_mode(sqlx::sqlite::SqliteJournalMode::Delete)
|
|
||||||
.foreign_keys(true)
|
|
||||||
.read_only(read_only)
|
|
||||||
.busy_timeout(Duration::from_secs(30))
|
|
||||||
.pragma("synchronous", "FULL")
|
|
||||||
.pragma("recursive_triggers", "ON")
|
|
||||||
.log_slow_statements(tracing::log::LevelFilter::Warn, Duration::from_millis(500));
|
|
||||||
|
|
||||||
if let Some(encryption_key) = encryption_key {
|
|
||||||
connect_options = connect_options.pragma("key", format!("'{}'", encryption_key));
|
|
||||||
}
|
|
||||||
|
|
||||||
let pool = SqlitePoolOptions::new()
|
|
||||||
.acquire_timeout(Duration::from_secs(30))
|
|
||||||
.max_connections(10)
|
|
||||||
.connect_with(connect_options)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(Self { pool })
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn run_migrations(&self) -> Result<()> {
|
|
||||||
sqlx::migrate!("./src/database/migrations")
|
|
||||||
.run(&self.pool)
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
tracing::error!("migration error: {:?}", e);
|
|
||||||
TwonlyError::Generic(format!("Migration error: {}", e))
|
|
||||||
})?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn create_backup(
|
|
||||||
&self,
|
|
||||||
output_path: &str,
|
|
||||||
encryption_key: Option<&str>,
|
|
||||||
) -> Result<()> {
|
|
||||||
if let Some(key) = encryption_key {
|
|
||||||
let mut conn = self
|
|
||||||
.pool
|
|
||||||
.acquire()
|
|
||||||
.await
|
|
||||||
.map_err(|e| TwonlyError::Generic(e.to_string()))?;
|
|
||||||
|
|
||||||
sqlx::query("ATTACH DATABASE ? AS backup KEY ?")
|
|
||||||
.bind(output_path)
|
|
||||||
.bind(key)
|
|
||||||
.execute(&mut *conn)
|
|
||||||
.await
|
|
||||||
.map_err(|e| TwonlyError::Generic(format!("Attach failed: {}", e)))?;
|
|
||||||
|
|
||||||
sqlx::query("SELECT sqlcipher_export('backup')")
|
|
||||||
.execute(&mut *conn)
|
|
||||||
.await
|
|
||||||
.map_err(|e| TwonlyError::Generic(format!("Export failed: {}", e)))?;
|
|
||||||
|
|
||||||
sqlx::query("DETACH DATABASE backup")
|
|
||||||
.execute(&mut *conn)
|
|
||||||
.await
|
|
||||||
.map_err(|e| TwonlyError::Generic(format!("Detach failed: {}", e)))?;
|
|
||||||
} else {
|
|
||||||
sqlx::query("VACUUM INTO ?")
|
|
||||||
.bind(output_path)
|
|
||||||
.execute(&self.pool)
|
|
||||||
.await
|
|
||||||
.map_err(|e| TwonlyError::Generic(format!("Backup failed: {}", e)))?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn check_integrity(&self) -> Result<()> {
|
|
||||||
let row: (String,) = sqlx::query_as("PRAGMA integrity_check")
|
|
||||||
.fetch_one(&self.pool)
|
|
||||||
.await
|
|
||||||
.map_err(|e| TwonlyError::Generic(format!("Integrity check query failed: {}", e)))?;
|
|
||||||
|
|
||||||
if row.0.to_lowercase() == "ok" {
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(TwonlyError::Generic(format!(
|
|
||||||
"Database integrity check failed: {}",
|
|
||||||
row.0
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use crate::database::tables::received_messages::ReceivedMessage;
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
use tempfile::tempdir;
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_database_encryption_and_migrations() {
|
|
||||||
let _ = pretty_env_logger::try_init();
|
|
||||||
let dir = tempdir().unwrap();
|
|
||||||
let db_path = dir.path().join("test.sqlite").display().to_string();
|
|
||||||
let key = "secure_password";
|
|
||||||
|
|
||||||
// 1. Create and initialize database with key
|
|
||||||
let db = Database::new(&db_path, Some(key), false).await.unwrap();
|
|
||||||
db.run_migrations().await.unwrap();
|
|
||||||
ReceivedMessage::insert(&db.pool, 1, b"hello world")
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// 2. Try to open with WRONG key
|
|
||||||
let result = Database::new(&db_path, Some("wrong_password"), false).await;
|
|
||||||
assert!(
|
|
||||||
result.is_err(),
|
|
||||||
"Opening with wrong key should fail. If this passes, the database might not be encrypted!"
|
|
||||||
);
|
|
||||||
|
|
||||||
// 3. Open with CORRECT key again
|
|
||||||
let db = Database::new(&db_path, Some(key), false).await.unwrap();
|
|
||||||
let messages = ReceivedMessage::get_all(&db.pool).await.unwrap();
|
|
||||||
assert_eq!(messages.len(), 1);
|
|
||||||
assert_eq!(messages[0].sender_id, 1);
|
|
||||||
assert_eq!(messages[0].content, b"hello world");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_database_backup_encrypted() {
|
|
||||||
let _ = pretty_env_logger::try_init();
|
|
||||||
let dir = tempdir().unwrap();
|
|
||||||
let db_path = dir.path().join("test_enc.sqlite").display().to_string();
|
|
||||||
let backup_path = dir.path().join("backup_enc.sqlite").display().to_string();
|
|
||||||
let key = "secure_password";
|
|
||||||
|
|
||||||
let db = Database::new(&db_path, Some(key), false).await.unwrap();
|
|
||||||
db.run_migrations().await.unwrap();
|
|
||||||
ReceivedMessage::insert(&db.pool, 1, b"hello world")
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
db.create_backup(&backup_path, Some(key)).await.unwrap();
|
|
||||||
|
|
||||||
// 1. Verify it cannot be opened with wrong key
|
|
||||||
let result = Database::new(&backup_path, Some("wrong_password"), false).await;
|
|
||||||
assert!(
|
|
||||||
result.is_err(),
|
|
||||||
"Encrypted backup should fail with wrong key"
|
|
||||||
);
|
|
||||||
|
|
||||||
// 2. Open backup with correct key and verify data
|
|
||||||
let backup_db = Database::new(&backup_path, Some(key), false).await.unwrap();
|
|
||||||
let messages = ReceivedMessage::get_all(&backup_db.pool).await.unwrap();
|
|
||||||
assert_eq!(messages.len(), 1);
|
|
||||||
assert_eq!(messages[0].sender_id, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_database_backup_plaintext() {
|
|
||||||
let _ = pretty_env_logger::try_init();
|
|
||||||
let dir = tempdir().unwrap();
|
|
||||||
let db_path = dir.path().join("test_plain.sqlite").display().to_string();
|
|
||||||
let backup_path = dir.path().join("backup_plain.sqlite").display().to_string();
|
|
||||||
|
|
||||||
let db = Database::new(&db_path, None, false).await.unwrap();
|
|
||||||
db.run_migrations().await.unwrap();
|
|
||||||
ReceivedMessage::insert(&db.pool, 1, b"hello world")
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
db.create_backup(&backup_path, None).await.unwrap();
|
|
||||||
|
|
||||||
// Open backup and verify
|
|
||||||
let backup_db = Database::new(&backup_path, None, false).await.unwrap();
|
|
||||||
let messages = ReceivedMessage::get_all(&backup_db.pool).await.unwrap();
|
|
||||||
assert_eq!(messages.len(), 1);
|
|
||||||
assert_eq!(messages[0].sender_id, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
DROP TABLE IF EXISTS received_messages;
|
||||||
180
rust/src/database/signal/mod.rs
Normal file
180
rust/src/database/signal/mod.rs
Normal file
|
|
@ -0,0 +1,180 @@
|
||||||
|
use crate::error::{Result, TwonlyError};
|
||||||
|
use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
|
||||||
|
use sqlx::{ConnectOptions, SqlitePool};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
pub struct Database {
|
||||||
|
pub pool: SqlitePool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Database {
|
||||||
|
pub async fn new(
|
||||||
|
db_path: &String,
|
||||||
|
encryption_key: Option<&str>,
|
||||||
|
read_only: bool,
|
||||||
|
) -> Result<Self> {
|
||||||
|
let db_url = format!("sqlite://{}", db_path);
|
||||||
|
|
||||||
|
let log_statements_level = if std::env::var("SQLX_LOG_STATEMENTS").is_ok() {
|
||||||
|
tracing::log::LevelFilter::Info
|
||||||
|
} else {
|
||||||
|
tracing::log::LevelFilter::Off
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut connect_options = format!("{db_url}?mode=rwc")
|
||||||
|
.parse::<SqliteConnectOptions>()?
|
||||||
|
.log_statements(log_statements_level)
|
||||||
|
.journal_mode(sqlx::sqlite::SqliteJournalMode::Delete)
|
||||||
|
.foreign_keys(true)
|
||||||
|
.read_only(read_only)
|
||||||
|
.busy_timeout(Duration::from_secs(30))
|
||||||
|
.pragma("synchronous", "FULL")
|
||||||
|
.pragma("recursive_triggers", "ON")
|
||||||
|
.log_slow_statements(tracing::log::LevelFilter::Warn, Duration::from_millis(500));
|
||||||
|
|
||||||
|
if let Some(encryption_key) = encryption_key {
|
||||||
|
connect_options = connect_options.pragma("key", format!("'{}'", encryption_key));
|
||||||
|
}
|
||||||
|
|
||||||
|
let pool = SqlitePoolOptions::new()
|
||||||
|
.acquire_timeout(Duration::from_secs(30))
|
||||||
|
.max_connections(10)
|
||||||
|
.connect_with(connect_options)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(Self { pool })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run_migrations(&self) -> Result<()> {
|
||||||
|
sqlx::migrate!("./src/database/signal/migrations")
|
||||||
|
.run(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
tracing::error!("migration error: {:?}", e);
|
||||||
|
TwonlyError::Generic(format!("Migration error: {}", e))
|
||||||
|
})?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn create_backup(
|
||||||
|
&self,
|
||||||
|
output_path: &str,
|
||||||
|
encryption_key: Option<&str>,
|
||||||
|
) -> Result<()> {
|
||||||
|
if let Some(key) = encryption_key {
|
||||||
|
let mut conn = self
|
||||||
|
.pool
|
||||||
|
.acquire()
|
||||||
|
.await
|
||||||
|
.map_err(|e| TwonlyError::Generic(e.to_string()))?;
|
||||||
|
|
||||||
|
sqlx::query("ATTACH DATABASE ? AS backup KEY ?")
|
||||||
|
.bind(output_path)
|
||||||
|
.bind(key)
|
||||||
|
.execute(&mut *conn)
|
||||||
|
.await
|
||||||
|
.map_err(|e| TwonlyError::Generic(format!("Attach failed: {}", e)))?;
|
||||||
|
|
||||||
|
sqlx::query("SELECT sqlcipher_export('backup')")
|
||||||
|
.execute(&mut *conn)
|
||||||
|
.await
|
||||||
|
.map_err(|e| TwonlyError::Generic(format!("Export failed: {}", e)))?;
|
||||||
|
|
||||||
|
sqlx::query("DETACH DATABASE backup")
|
||||||
|
.execute(&mut *conn)
|
||||||
|
.await
|
||||||
|
.map_err(|e| TwonlyError::Generic(format!("Detach failed: {}", e)))?;
|
||||||
|
} else {
|
||||||
|
sqlx::query("VACUUM INTO ?")
|
||||||
|
.bind(output_path)
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| TwonlyError::Generic(format!("Backup failed: {}", e)))?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn check_integrity(&self) -> Result<()> {
|
||||||
|
let row: (String,) = sqlx::query_as("PRAGMA integrity_check")
|
||||||
|
.fetch_one(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| TwonlyError::Generic(format!("Integrity check query failed: {}", e)))?;
|
||||||
|
|
||||||
|
if row.0.to_lowercase() == "ok" {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(TwonlyError::Generic(format!(
|
||||||
|
"Database integrity check failed: {}",
|
||||||
|
row.0
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use tempfile::tempdir;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_database_encryption_and_migrations() {
|
||||||
|
let _ = pretty_env_logger::try_init();
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
let db_path = dir.path().join("test.sqlite").display().to_string();
|
||||||
|
let key = "secure_password";
|
||||||
|
|
||||||
|
// 1. Create and initialize database with key
|
||||||
|
let db = Database::new(&db_path, Some(key), false).await.unwrap();
|
||||||
|
db.run_migrations().await.unwrap();
|
||||||
|
|
||||||
|
// 2. Try to open with WRONG key
|
||||||
|
let result = Database::new(&db_path, Some("wrong_password"), false).await;
|
||||||
|
assert!(
|
||||||
|
result.is_err(),
|
||||||
|
"Opening with wrong key should fail. If this passes, the database might not be encrypted!"
|
||||||
|
);
|
||||||
|
|
||||||
|
// 3. Open with CORRECT key again
|
||||||
|
Database::new(&db_path, Some(key), false).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_database_backup_encrypted() {
|
||||||
|
let _ = pretty_env_logger::try_init();
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
let db_path = dir.path().join("test_enc.sqlite").display().to_string();
|
||||||
|
let backup_path = dir.path().join("backup_enc.sqlite").display().to_string();
|
||||||
|
let key = "secure_password";
|
||||||
|
|
||||||
|
let db = Database::new(&db_path, Some(key), false).await.unwrap();
|
||||||
|
db.run_migrations().await.unwrap();
|
||||||
|
|
||||||
|
db.create_backup(&backup_path, Some(key)).await.unwrap();
|
||||||
|
|
||||||
|
// 1. Verify it cannot be opened with wrong key
|
||||||
|
let result = Database::new(&backup_path, Some("wrong_password"), false).await;
|
||||||
|
assert!(
|
||||||
|
result.is_err(),
|
||||||
|
"Encrypted backup should fail with wrong key"
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2. Open backup with correct key and verify data
|
||||||
|
Database::new(&backup_path, Some(key), false).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_database_backup_plaintext() {
|
||||||
|
let _ = pretty_env_logger::try_init();
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
let db_path = dir.path().join("test_plain.sqlite").display().to_string();
|
||||||
|
let backup_path = dir.path().join("backup_plain.sqlite").display().to_string();
|
||||||
|
|
||||||
|
let db = Database::new(&db_path, None, false).await.unwrap();
|
||||||
|
db.run_migrations().await.unwrap();
|
||||||
|
|
||||||
|
db.create_backup(&backup_path, None).await.unwrap();
|
||||||
|
|
||||||
|
// Open backup and verify
|
||||||
|
Database::new(&backup_path, None, false).await.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,103 +0,0 @@
|
||||||
pub mod received_messages;
|
|
||||||
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! generate_insert {
|
|
||||||
($table:literal, $fn_name:ident, $($field:ident : $ty:ty),+) => {
|
|
||||||
pub async fn $fn_name(
|
|
||||||
pool: &sqlx::SqlitePool,
|
|
||||||
$($field: $ty),+
|
|
||||||
) -> $crate::error::Result<i64> {
|
|
||||||
let sql = format!(
|
|
||||||
"INSERT INTO {} ({}) VALUES ({}) RETURNING id",
|
|
||||||
$table,
|
|
||||||
vec![$(stringify!($field)),+].join(", "),
|
|
||||||
vec!["?"; [$({stringify!($field); 1}),+].len()].join(", ")
|
|
||||||
);
|
|
||||||
|
|
||||||
let row: (i64,) = sqlx::query_as(sqlx::AssertSqlSafe(sql))
|
|
||||||
$(.bind($field))+
|
|
||||||
.fetch_one(pool)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(row.0)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! generate_select {
|
|
||||||
($table:literal, $fn_name:ident) => {
|
|
||||||
pub async fn $fn_name(pool: &sqlx::SqlitePool) -> $crate::error::Result<Vec<Self>> {
|
|
||||||
let sql = format!("SELECT * FROM {}", $table);
|
|
||||||
let results = sqlx::query_as::<_, Self>(sqlx::AssertSqlSafe(sql))
|
|
||||||
.fetch_all(pool)
|
|
||||||
.await?;
|
|
||||||
Ok(results)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
($table:literal, $fn_name:ident, $($field:ident : $ty:ty),+) => {
|
|
||||||
pub async fn $fn_name(pool: &sqlx::SqlitePool, $($field: $ty),+) -> $crate::error::Result<Vec<Self>> {
|
|
||||||
let mut sql = format!("SELECT * FROM {} WHERE ", $table);
|
|
||||||
let mut filters = Vec::new();
|
|
||||||
$(
|
|
||||||
filters.push(format!("{} = ?", stringify!($field)));
|
|
||||||
)+
|
|
||||||
sql.push_str(&filters.join(" AND "));
|
|
||||||
|
|
||||||
let results = sqlx::query_as::<_, Self>(sqlx::AssertSqlSafe(sql))
|
|
||||||
$(.bind($field))+
|
|
||||||
.fetch_all(pool)
|
|
||||||
.await?;
|
|
||||||
Ok(results)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! generate_table_tests {
|
|
||||||
(
|
|
||||||
$struct:ident,
|
|
||||||
$insert_fn:ident ($($arg:expr),+),
|
|
||||||
$select_all_fn:ident
|
|
||||||
) => {
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use $crate::database::Database;
|
|
||||||
use tempfile::tempdir;
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_generated_basic() {
|
|
||||||
let dir = tempdir().unwrap();
|
|
||||||
let db_path = dir.path().join("test.sqlite").display().to_string();
|
|
||||||
let db = Database::new(&db_path, None, false).await.unwrap();
|
|
||||||
db.run_migrations().await.unwrap();
|
|
||||||
|
|
||||||
let _id = $struct::$insert_fn(&db.pool, $($arg),+).await.unwrap();
|
|
||||||
let all = $struct::$select_all_fn(&db.pool).await.unwrap();
|
|
||||||
assert_eq!(all.len(), 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! generate_test_select {
|
|
||||||
($struct:ident, $insert_fn:ident ($($arg:expr),+), $select_fn:ident ($($sel_arg:expr),+)) => {
|
|
||||||
paste::paste! {
|
|
||||||
#[cfg(test)]
|
|
||||||
#[tokio::test]
|
|
||||||
async fn [<test_ $select_fn>]() {
|
|
||||||
use tempfile::tempdir;
|
|
||||||
let dir = tempdir().unwrap();
|
|
||||||
let db_path = dir.path().join("test.sqlite").display().to_string();
|
|
||||||
let db = $crate::database::Database::new(&db_path, None, false).await.unwrap();
|
|
||||||
db.run_migrations().await.unwrap();
|
|
||||||
|
|
||||||
$struct::$insert_fn(&db.pool, $($arg),+).await.unwrap();
|
|
||||||
let results = $struct::$select_fn(&db.pool, $($sel_arg),+).await.unwrap();
|
|
||||||
assert_eq!(results.len(), 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
#![allow(dead_code)]
|
|
||||||
use chrono::{DateTime, Utc};
|
|
||||||
use sqlx::FromRow;
|
|
||||||
|
|
||||||
#[derive(Debug, FromRow, PartialEq, Clone)]
|
|
||||||
pub struct ReceivedMessage {
|
|
||||||
pub id: i64,
|
|
||||||
pub sender_id: i64,
|
|
||||||
pub content: Vec<u8>,
|
|
||||||
pub timestamp: DateTime<Utc>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ReceivedMessage {
|
|
||||||
crate::generate_insert!(
|
|
||||||
"received_messages",
|
|
||||||
insert,
|
|
||||||
sender_id: i64,
|
|
||||||
content: &[u8]
|
|
||||||
);
|
|
||||||
crate::generate_select!("received_messages", get_all);
|
|
||||||
crate::generate_select!("received_messages", get_by_sender, sender_id: i64);
|
|
||||||
}
|
|
||||||
|
|
||||||
crate::generate_table_tests!(ReceivedMessage, insert(1, b"hello world"), get_all);
|
|
||||||
|
|
||||||
crate::generate_test_select!(ReceivedMessage, insert(1, b"hello world"), get_by_sender(1));
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,73 +0,0 @@
|
||||||
# Cryptographic Architecture
|
|
||||||
|
|
||||||
## 1. Main Key
|
|
||||||
A cryptographically secure, immutable master key. Loss of this key, in the absence of valid backups, results in permanent loss of account access.
|
|
||||||
|
|
||||||
*Key Derivation*: Utilizes HKDF to derive subordinate keys.
|
|
||||||
|
|
||||||
- Authentication Token: Uploaded to the server for session authentication.
|
|
||||||
- Backup Key: Used to encrypt a backup
|
|
||||||
- Backup Content: The encrypted backup encompasses
|
|
||||||
- Main Key
|
|
||||||
- Identity keys
|
|
||||||
- Local database (including contacts, public identities, memories (only references), and messages).
|
|
||||||
- Lifecycle: Backups are refreshed daily and deleted after one year.
|
|
||||||
- Media Main Key: Used to wrap media-specific keys.
|
|
||||||
- A new, cryptographically secure key is generated for every media file.
|
|
||||||
- The media key is wrapped using AES-GCM and the Main Media Key and stored in the online database along side to the uploaded media file database entry.
|
|
||||||
- The original media file is encrypted using AES-GCM and uploaded to the designated storage bucket.
|
|
||||||
|
|
||||||
## 3. Identity Keys
|
|
||||||
- Signal Identity
|
|
||||||
- Generates a private and public key pair for secure communication.
|
|
||||||
- Nostr Identity
|
|
||||||
- Generates a private and public key pair for Nostr network interactions.
|
|
||||||
|
|
||||||
|
|
||||||
## 1. Backup Keys
|
|
||||||
Independent, securely generated keys used to wrap the primary backup key.
|
|
||||||
|
|
||||||
### 1.1. Password-Based Backup
|
|
||||||
1. Derivation
|
|
||||||
- Utilizes scrypt with the username as the salt (cost 65536) to derive a 64-byte sequence.
|
|
||||||
2. Allocation:
|
|
||||||
- 32 bytes: Backup ID, used as the identifier to locate the backup on the server.
|
|
||||||
- 32 bytes: Backup wrapper key.
|
|
||||||
3. Content
|
|
||||||
- The payload contains the main key required to generate the auth token and the backup key.
|
|
||||||
4. Operation
|
|
||||||
- The backup wrapper key encrypts the main key. The ciphertext is uploaded anonymously to the server, indexed by the Backup ID.
|
|
||||||
5. Security Measures
|
|
||||||
- The server enforces strict rate limiting per IP address to prevent brute-force attacks.
|
|
||||||
6. Lifecycle
|
|
||||||
- These backup keys require a monthly refresh; otherwise, they are scheduled for deletion after two years.
|
|
||||||
|
|
||||||
### 1.2. Trusted Friends Keys (Passwordless Recovery)
|
|
||||||
1. Initiation
|
|
||||||
- The recovering user generates a temporary ID (TempID) and a new ephemeral asymmetric key pair.
|
|
||||||
2. Request
|
|
||||||
- A recovery request containing the TempID and the public key is transmitted to a trusted contact via a secure link.
|
|
||||||
3. Verification
|
|
||||||
- The contact manually verifies the requestor's identity within their application to mitigate phishing risks.
|
|
||||||
4. Share Transmission
|
|
||||||
- The contact encrypts a trusted friend share using the provided public key. This share includes the user IDs, the minimum threshold required for decryption, and the cryptographic share (utilizing Shamir's Secret Sharing).
|
|
||||||
5. Reconstruction
|
|
||||||
- Upon receiving the required threshold of shares, the user reconstructs the shared secret data.
|
|
||||||
6. Second Factor (Optional)
|
|
||||||
- The shared secret data may mandate an additional factor (PIN or Email). For a PIN factor, an unlock token and a PIN seed are used to securely retrieve the remaining share from the server without exposing the raw PIN.
|
|
||||||
7. Final Recovery
|
|
||||||
- The decrypted recovery data provides the User ID, private key, and the backup master key necessary to restore the account and its backups.
|
|
||||||
|
|
||||||
## 4. Web Portal Upload Protocol
|
|
||||||
1. Initialization
|
|
||||||
- The web portal generates a cryptographically secure symmetric key for end-to-end encrypted (E2EE) communication with the mobile application, alongside a newly registered session token.
|
|
||||||
2. Handshake
|
|
||||||
- The mobile application scans the QR code containing the session token and the symmetric key.
|
|
||||||
3. Authorization
|
|
||||||
- The application signals readiness via the server using the session token and securely provisions a temporary authentication token for media uploads over the established symmetric E2EE channel.
|
|
||||||
4. Key Exchange
|
|
||||||
- The web portal encrypts the media file using a newly generated media key. It transmits this media key to the application (encrypted via the E2EE symmetric key) and receives the wrapped media key in return.
|
|
||||||
5. Upload
|
|
||||||
- The web portal uploads the encrypted media file to the server, assigning it a device ID of 0.
|
|
||||||
6. Synchronization
|
|
||||||
- Finally, the application requests all memories with a device ID lower than its current one (the device ID increments after a backup restoration).
|
|
||||||
|
|
@ -18,6 +18,7 @@ pub struct MainKey {
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub(crate) enum DatabaseKey {
|
pub(crate) enum DatabaseKey {
|
||||||
RustDb,
|
RustDb,
|
||||||
|
AppDb,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MainKey {
|
impl MainKey {
|
||||||
|
|
@ -42,8 +43,9 @@ impl MainKey {
|
||||||
|
|
||||||
/// Derives the database encryption key.
|
/// Derives the database encryption key.
|
||||||
pub(crate) fn get_database_key(&self, db: DatabaseKey) -> String {
|
pub(crate) fn get_database_key(&self, db: DatabaseKey) -> String {
|
||||||
let db_name = match db {
|
let db_name: &[u8] = match db {
|
||||||
DatabaseKey::RustDb => b"rust_db",
|
DatabaseKey::RustDb => b"rust_db",
|
||||||
|
DatabaseKey::AppDb => b"app_db",
|
||||||
};
|
};
|
||||||
let info = [b"database_key_", db_name as &[u8]].concat();
|
let info = [b"database_key_", db_name as &[u8]].concat();
|
||||||
let key = self.derive_key(&info);
|
let key = self.derive_key(&info);
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,5 @@ mod keys;
|
||||||
mod log;
|
mod log;
|
||||||
mod secure_storage;
|
mod secure_storage;
|
||||||
pub mod signal;
|
pub mod signal;
|
||||||
mod standalone;
|
|
||||||
mod user_discovery;
|
mod user_discovery;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ pub struct FrbPqcPreKey {
|
||||||
impl RustSignalEngine {
|
impl RustSignalEngine {
|
||||||
pub async fn new(local_name: String) -> Result<Self> {
|
pub async fn new(local_name: String) -> Result<Self> {
|
||||||
let twonly = get_twonly_flutter()?;
|
let twonly = get_twonly_flutter()?;
|
||||||
let pool = twonly.rust_db.pool.clone();
|
let pool = twonly.rust_db.read().await.pool.clone();
|
||||||
|
|
||||||
let km = twonly.key_manager.lock().await;
|
let km = twonly.key_manager.lock().await;
|
||||||
let signal_identity = km
|
let signal_identity = km
|
||||||
|
|
@ -541,10 +541,13 @@ mod tests {
|
||||||
// create the file manually just to be sure
|
// create the file manually just to be sure
|
||||||
std::fs::File::create(&db_path).unwrap();
|
std::fs::File::create(&db_path).unwrap();
|
||||||
|
|
||||||
let db =
|
let db = crate::database::signal::Database::new(
|
||||||
crate::database::Database::new(&db_path.to_str().unwrap().to_string(), None, false)
|
&db_path.to_str().unwrap().to_string(),
|
||||||
.await
|
None,
|
||||||
.unwrap();
|
false,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
db.run_migrations().await.unwrap();
|
db.run_migrations().await.unwrap();
|
||||||
let pool = db.pool.clone();
|
let pool = db.pool.clone();
|
||||||
|
|
||||||
|
|
@ -567,7 +570,7 @@ mod tests {
|
||||||
async fn test_generate_pqc_prekeys() {
|
async fn test_generate_pqc_prekeys() {
|
||||||
let (engine, _dir) = create_test_engine("alice").await;
|
let (engine, _dir) = create_test_engine("alice").await;
|
||||||
let prekeys = engine.generate_pqc_prekeys().await.unwrap();
|
let prekeys = engine.generate_pqc_prekeys().await.unwrap();
|
||||||
assert_eq!(prekeys.len(), 50);
|
assert_eq!(prekeys.len(), 30);
|
||||||
for prekey in prekeys {
|
for prekey in prekeys {
|
||||||
assert!(prekey.ecc_pre_key.len() > 0);
|
assert!(prekey.ecc_pre_key.len() > 0);
|
||||||
assert!(prekey.kyber_pre_key.len() > 0);
|
assert!(prekey.kyber_pre_key.len() > 0);
|
||||||
|
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
use tokio::sync::Mutex;
|
|
||||||
|
|
||||||
use crate::bridge::InitConfig;
|
|
||||||
use crate::database::Database;
|
|
||||||
use crate::keys::KeyManager;
|
|
||||||
use crate::secure_storage::SecureStorage;
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
pub(crate) struct TwonlyStandalone {
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub(crate) config: InitConfig,
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub(crate) rust_db: Arc<Database>,
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub(crate) secure_storage: SecureStorage,
|
|
||||||
pub(crate) key_manager: Arc<Mutex<KeyManager>>,
|
|
||||||
}
|
|
||||||
|
|
@ -2,3 +2,6 @@
|
||||||
mod in_memory_store;
|
mod in_memory_store;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(super) use in_memory_store::InMemoryStore;
|
pub(super) use in_memory_store::InMemoryStore;
|
||||||
|
|
||||||
|
mod native;
|
||||||
|
pub(crate) use native::{NativeUserDiscoveryStore, NativeUserDiscoveryUtils};
|
||||||
|
|
|
||||||
552
rust/src/user_discovery/stores/native.rs
Normal file
552
rust/src/user_discovery/stores/native.rs
Normal file
|
|
@ -0,0 +1,552 @@
|
||||||
|
use crate::database::{app::AppDatabase, signal::Database};
|
||||||
|
use crate::keys::KeyManager;
|
||||||
|
use crate::user_discovery::error::{Result, UserDiscoveryError};
|
||||||
|
use crate::user_discovery::traits::{
|
||||||
|
AnnouncedUser, OtherPromotion, UserDiscoveryStore, UserDiscoveryUtils,
|
||||||
|
};
|
||||||
|
use crate::user_discovery::UserID;
|
||||||
|
use libsignal_protocol::{IdentityKey, IdentityKeyPair};
|
||||||
|
use rand::SeedableRng;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::{Mutex, RwLock};
|
||||||
|
|
||||||
|
fn store_error(error: impl std::fmt::Display) -> UserDiscoveryError {
|
||||||
|
UserDiscoveryError::Store(error.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub(crate) struct NativeUserDiscoveryStore {
|
||||||
|
app_db: Arc<RwLock<Arc<AppDatabase>>>,
|
||||||
|
config_path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NativeUserDiscoveryStore {
|
||||||
|
pub(crate) fn new(app_db: Arc<RwLock<Arc<AppDatabase>>>, data_dir: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
app_db,
|
||||||
|
config_path: PathBuf::from(data_dir).join("user_discovery_config.json"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn database(&self) -> Arc<AppDatabase> {
|
||||||
|
self.app_db.read().await.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UserDiscoveryStore for NativeUserDiscoveryStore {
|
||||||
|
async fn get_config(&self) -> Result<String> {
|
||||||
|
if !self.config_path.is_file() {
|
||||||
|
return Err(UserDiscoveryError::NotInitialized);
|
||||||
|
}
|
||||||
|
Ok(std::fs::read_to_string(&self.config_path)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_config(&self, update: String) -> Result<()> {
|
||||||
|
std::fs::write(&self.config_path, update)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn set_shares(&self, shares: Vec<Vec<u8>>) -> Result<()> {
|
||||||
|
let db = self.database().await;
|
||||||
|
let mut tx = db.pool.begin().await.map_err(store_error)?;
|
||||||
|
sqlx::query!("DELETE FROM user_discovery_shares")
|
||||||
|
.execute(&mut *tx)
|
||||||
|
.await
|
||||||
|
.map_err(store_error)?;
|
||||||
|
for share in shares {
|
||||||
|
sqlx::query!(
|
||||||
|
"INSERT INTO user_discovery_shares (share) VALUES (?)",
|
||||||
|
share
|
||||||
|
)
|
||||||
|
.execute(&mut *tx)
|
||||||
|
.await
|
||||||
|
.map_err(store_error)?;
|
||||||
|
}
|
||||||
|
tx.commit().await.map_err(store_error)?;
|
||||||
|
db.notify_committed(["user_discovery_shares"]);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_share_for_contact(&self, contact_id: UserID) -> Result<Vec<u8>> {
|
||||||
|
let db = self.database().await;
|
||||||
|
let mut tx = db.pool.begin().await.map_err(store_error)?;
|
||||||
|
if let Some(share) = sqlx::query_scalar!(
|
||||||
|
r#"
|
||||||
|
SELECT share
|
||||||
|
FROM user_discovery_shares
|
||||||
|
WHERE contact_id = ?
|
||||||
|
LIMIT 1
|
||||||
|
"#,
|
||||||
|
contact_id,
|
||||||
|
)
|
||||||
|
.fetch_optional(&mut *tx)
|
||||||
|
.await
|
||||||
|
.map_err(store_error)?
|
||||||
|
{
|
||||||
|
tx.commit().await.map_err(store_error)?;
|
||||||
|
return Ok(share);
|
||||||
|
}
|
||||||
|
let available = sqlx::query!(
|
||||||
|
r#"
|
||||||
|
SELECT share_id, share
|
||||||
|
FROM user_discovery_shares
|
||||||
|
WHERE contact_id IS NULL
|
||||||
|
LIMIT 1
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.fetch_optional(&mut *tx)
|
||||||
|
.await
|
||||||
|
.map_err(store_error)?;
|
||||||
|
let Some(row) = available else {
|
||||||
|
tx.rollback().await.map_err(store_error)?;
|
||||||
|
return Err(UserDiscoveryError::NoSharesLeft);
|
||||||
|
};
|
||||||
|
let share_id = row.share_id;
|
||||||
|
let share = row.share;
|
||||||
|
sqlx::query!(
|
||||||
|
r#"
|
||||||
|
UPDATE user_discovery_shares
|
||||||
|
SET contact_id = ?
|
||||||
|
WHERE share_id = ?
|
||||||
|
"#,
|
||||||
|
contact_id,
|
||||||
|
share_id,
|
||||||
|
)
|
||||||
|
.execute(&mut *tx)
|
||||||
|
.await
|
||||||
|
.map_err(store_error)?;
|
||||||
|
tx.commit().await.map_err(store_error)?;
|
||||||
|
db.notify_committed(["user_discovery_shares"]);
|
||||||
|
Ok(share)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn push_own_promotion_and_clear_old_version(
|
||||||
|
&self,
|
||||||
|
contact_id: UserID,
|
||||||
|
_version: u32,
|
||||||
|
promotion: Vec<u8>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let db = self.database().await;
|
||||||
|
let mut tx = db.pool.begin().await.map_err(store_error)?;
|
||||||
|
sqlx::query!(
|
||||||
|
r#"
|
||||||
|
UPDATE user_discovery_own_promotions
|
||||||
|
SET promotion = X''
|
||||||
|
WHERE contact_id = ?
|
||||||
|
"#,
|
||||||
|
contact_id,
|
||||||
|
)
|
||||||
|
.execute(&mut *tx)
|
||||||
|
.await
|
||||||
|
.map_err(store_error)?;
|
||||||
|
sqlx::query!(
|
||||||
|
r#"
|
||||||
|
INSERT INTO user_discovery_own_promotions (
|
||||||
|
contact_id,
|
||||||
|
promotion
|
||||||
|
) VALUES (?, ?)
|
||||||
|
"#,
|
||||||
|
contact_id,
|
||||||
|
promotion,
|
||||||
|
)
|
||||||
|
.execute(&mut *tx)
|
||||||
|
.await
|
||||||
|
.map_err(store_error)?;
|
||||||
|
tx.commit().await.map_err(store_error)?;
|
||||||
|
db.notify_committed(["user_discovery_own_promotions"]);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_own_promotions_after_version(&self, version: u32) -> Result<Vec<Vec<u8>>> {
|
||||||
|
let db = self.database().await;
|
||||||
|
sqlx::query_scalar!(
|
||||||
|
r#"
|
||||||
|
SELECT promotion
|
||||||
|
FROM user_discovery_own_promotions
|
||||||
|
WHERE version_id > ?
|
||||||
|
"#,
|
||||||
|
version,
|
||||||
|
)
|
||||||
|
.fetch_all(&db.pool)
|
||||||
|
.await
|
||||||
|
.map_err(store_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn store_other_promotion(&self, promotion: OtherPromotion) -> Result<()> {
|
||||||
|
let db = self.database().await;
|
||||||
|
sqlx::query!(
|
||||||
|
r#"
|
||||||
|
INSERT INTO user_discovery_other_promotions (
|
||||||
|
from_contact_id,
|
||||||
|
promotion_id,
|
||||||
|
public_id,
|
||||||
|
threshold,
|
||||||
|
announcement_share,
|
||||||
|
public_key_verified_timestamp
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?)
|
||||||
|
ON CONFLICT(from_contact_id, public_id) DO UPDATE SET
|
||||||
|
promotion_id = excluded.promotion_id,
|
||||||
|
threshold = excluded.threshold,
|
||||||
|
announcement_share = excluded.announcement_share,
|
||||||
|
public_key_verified_timestamp = excluded.public_key_verified_timestamp
|
||||||
|
"#,
|
||||||
|
promotion.from_contact_id,
|
||||||
|
promotion.promotion_id,
|
||||||
|
promotion.public_id,
|
||||||
|
promotion.threshold,
|
||||||
|
promotion.announcement_share,
|
||||||
|
promotion.public_key_verified_timestamp,
|
||||||
|
)
|
||||||
|
.execute(&db.pool)
|
||||||
|
.await
|
||||||
|
.map_err(store_error)?;
|
||||||
|
db.notify_committed(["user_discovery_other_promotions"]);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_other_promotions_by_public_id(
|
||||||
|
&self,
|
||||||
|
public_id: i64,
|
||||||
|
) -> Result<Vec<OtherPromotion>> {
|
||||||
|
let db = self.database().await;
|
||||||
|
let rows = sqlx::query!(
|
||||||
|
r#"
|
||||||
|
SELECT
|
||||||
|
promotion_id,
|
||||||
|
public_id,
|
||||||
|
from_contact_id,
|
||||||
|
threshold,
|
||||||
|
announcement_share,
|
||||||
|
public_key_verified_timestamp
|
||||||
|
FROM user_discovery_other_promotions
|
||||||
|
WHERE public_id = ?
|
||||||
|
"#,
|
||||||
|
public_id,
|
||||||
|
)
|
||||||
|
.fetch_all(&db.pool)
|
||||||
|
.await
|
||||||
|
.map_err(store_error)?;
|
||||||
|
rows.into_iter()
|
||||||
|
.map(|row| {
|
||||||
|
Ok(OtherPromotion {
|
||||||
|
promotion_id: u32::try_from(row.promotion_id).map_err(store_error)?,
|
||||||
|
public_id: row.public_id,
|
||||||
|
from_contact_id: row.from_contact_id,
|
||||||
|
threshold: u8::try_from(row.threshold).map_err(store_error)?,
|
||||||
|
announcement_share: row.announcement_share,
|
||||||
|
public_key_verified_timestamp: row.public_key_verified_timestamp,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_announced_user_by_public_id(
|
||||||
|
&self,
|
||||||
|
public_id: i64,
|
||||||
|
) -> Result<Option<AnnouncedUser>> {
|
||||||
|
let db = self.database().await;
|
||||||
|
let row = sqlx::query!(
|
||||||
|
r#"
|
||||||
|
SELECT
|
||||||
|
announced_user_id,
|
||||||
|
announced_public_key,
|
||||||
|
public_id
|
||||||
|
FROM user_discovery_announced_users
|
||||||
|
WHERE public_id = ?
|
||||||
|
"#,
|
||||||
|
public_id,
|
||||||
|
)
|
||||||
|
.fetch_optional(&db.pool)
|
||||||
|
.await
|
||||||
|
.map_err(store_error)?;
|
||||||
|
row.map(|row| {
|
||||||
|
Ok(AnnouncedUser {
|
||||||
|
user_id: row.announced_user_id,
|
||||||
|
public_key: row.announced_public_key,
|
||||||
|
public_id: row.public_id,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.transpose()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn push_new_user_relation(
|
||||||
|
&self,
|
||||||
|
from_contact_id: UserID,
|
||||||
|
announced_user: AnnouncedUser,
|
||||||
|
public_key_verified_timestamp: Option<i64>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let db = self.database().await;
|
||||||
|
let mut tx = db.pool.begin().await.map_err(store_error)?;
|
||||||
|
sqlx::query!(
|
||||||
|
r#"
|
||||||
|
INSERT INTO user_discovery_announced_users (
|
||||||
|
announced_user_id,
|
||||||
|
announced_public_key,
|
||||||
|
public_id
|
||||||
|
) VALUES (?, ?, ?)
|
||||||
|
ON CONFLICT DO UPDATE SET
|
||||||
|
announced_user_id = excluded.announced_user_id,
|
||||||
|
announced_public_key = excluded.announced_public_key,
|
||||||
|
public_id = excluded.public_id
|
||||||
|
"#,
|
||||||
|
announced_user.user_id,
|
||||||
|
announced_user.public_key,
|
||||||
|
announced_user.public_id,
|
||||||
|
)
|
||||||
|
.execute(&mut *tx)
|
||||||
|
.await
|
||||||
|
.map_err(store_error)?;
|
||||||
|
sqlx::query!(
|
||||||
|
r#"
|
||||||
|
INSERT INTO user_discovery_user_relations (
|
||||||
|
announced_user_id,
|
||||||
|
from_contact_id,
|
||||||
|
public_key_verified_timestamp
|
||||||
|
) VALUES (?, ?, ?)
|
||||||
|
ON CONFLICT(announced_user_id, from_contact_id) DO UPDATE SET
|
||||||
|
public_key_verified_timestamp = excluded.public_key_verified_timestamp
|
||||||
|
"#,
|
||||||
|
announced_user.user_id,
|
||||||
|
from_contact_id,
|
||||||
|
public_key_verified_timestamp,
|
||||||
|
)
|
||||||
|
.execute(&mut *tx)
|
||||||
|
.await
|
||||||
|
.map_err(store_error)?;
|
||||||
|
tx.commit().await.map_err(store_error)?;
|
||||||
|
db.notify_committed([
|
||||||
|
"user_discovery_announced_users",
|
||||||
|
"user_discovery_user_relations",
|
||||||
|
]);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
async fn get_all_announced_users(
|
||||||
|
&self,
|
||||||
|
) -> Result<std::collections::HashMap<AnnouncedUser, Vec<(UserID, Option<i64>)>>> {
|
||||||
|
Err(UserDiscoveryError::Store("not used by native store".into()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_contact_promotion(&self, contact_id: UserID) -> Result<Option<Vec<u8>>> {
|
||||||
|
let db = self.database().await;
|
||||||
|
sqlx::query_scalar!(
|
||||||
|
r#"
|
||||||
|
SELECT promotion
|
||||||
|
FROM user_discovery_own_promotions
|
||||||
|
WHERE contact_id = ?
|
||||||
|
ORDER BY version_id DESC
|
||||||
|
LIMIT 1
|
||||||
|
"#,
|
||||||
|
contact_id,
|
||||||
|
)
|
||||||
|
.fetch_optional(&db.pool)
|
||||||
|
.await
|
||||||
|
.map_err(store_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_contact_version(&self, contact_id: UserID) -> Result<Option<Vec<u8>>> {
|
||||||
|
let db = self.database().await;
|
||||||
|
sqlx::query_scalar!(
|
||||||
|
r#"
|
||||||
|
SELECT user_discovery_version
|
||||||
|
FROM contacts
|
||||||
|
WHERE user_id = ?
|
||||||
|
"#,
|
||||||
|
contact_id,
|
||||||
|
)
|
||||||
|
.fetch_optional(&db.pool)
|
||||||
|
.await
|
||||||
|
.map_err(store_error)
|
||||||
|
.map(Option::flatten)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn set_contact_version(&self, contact_id: UserID, update: Vec<u8>) -> Result<()> {
|
||||||
|
let db = self.database().await;
|
||||||
|
sqlx::query!(
|
||||||
|
r#"
|
||||||
|
UPDATE contacts
|
||||||
|
SET user_discovery_version = ?
|
||||||
|
WHERE user_id = ?
|
||||||
|
"#,
|
||||||
|
update,
|
||||||
|
contact_id,
|
||||||
|
)
|
||||||
|
.execute(&db.pool)
|
||||||
|
.await
|
||||||
|
.map_err(store_error)?;
|
||||||
|
db.notify_committed(["contacts"]);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct NativeUserDiscoveryUtils {
|
||||||
|
key_manager: Arc<Mutex<KeyManager>>,
|
||||||
|
rust_db: Arc<RwLock<Arc<Database>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NativeUserDiscoveryUtils {
|
||||||
|
pub(crate) fn new(
|
||||||
|
key_manager: Arc<Mutex<KeyManager>>,
|
||||||
|
rust_db: Arc<RwLock<Arc<Database>>>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
key_manager,
|
||||||
|
rust_db,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UserDiscoveryUtils for NativeUserDiscoveryUtils {
|
||||||
|
async fn sign_data(&self, input_data: &[u8]) -> Result<Vec<u8>> {
|
||||||
|
let key_manager = self.key_manager.lock().await;
|
||||||
|
let identity = key_manager
|
||||||
|
.signal_identity
|
||||||
|
.as_ref()
|
||||||
|
.ok_or_else(|| UserDiscoveryError::Store("no Signal identity found".into()))?;
|
||||||
|
let key_pair = IdentityKeyPair::try_from(identity.identity_key_pair_structure.as_slice())
|
||||||
|
.map_err(store_error)?;
|
||||||
|
let mut csprng = rand::rngs::StdRng::from_os_rng();
|
||||||
|
key_pair
|
||||||
|
.private_key()
|
||||||
|
.calculate_signature_for_multipart_message(&[input_data], &mut csprng)
|
||||||
|
.map(|signature| signature.to_vec())
|
||||||
|
.map_err(store_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn verify_signature(
|
||||||
|
&self,
|
||||||
|
input_data: &[u8],
|
||||||
|
pubkey: &[u8],
|
||||||
|
signature: &[u8],
|
||||||
|
) -> Result<bool> {
|
||||||
|
let identity = match IdentityKey::decode(pubkey) {
|
||||||
|
Ok(identity) => identity,
|
||||||
|
Err(_) => return Ok(false),
|
||||||
|
};
|
||||||
|
Ok(identity
|
||||||
|
.public_key()
|
||||||
|
.verify_signature(input_data, signature))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn verify_stored_pubkey(&self, from_contact_id: UserID, pubkey: &[u8]) -> Result<bool> {
|
||||||
|
let db = self.rust_db.read().await.clone();
|
||||||
|
let stored = sqlx::query_scalar!(
|
||||||
|
r#"
|
||||||
|
SELECT identity_key
|
||||||
|
FROM signal_identities
|
||||||
|
WHERE name = ?
|
||||||
|
"#,
|
||||||
|
from_contact_id.to_string(),
|
||||||
|
)
|
||||||
|
.fetch_optional(&db.pool)
|
||||||
|
.await
|
||||||
|
.map_err(store_error)?;
|
||||||
|
Ok(stored.as_deref() == Some(pubkey))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::keys::SignalIdentityKey;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn native_store_and_signal_utils_use_rust_owned_state() {
|
||||||
|
let temp = tempfile::tempdir().unwrap();
|
||||||
|
let app_path = temp.path().join("app.sqlite");
|
||||||
|
let app_db = AppDatabase::new(app_path.to_str().unwrap(), None, false)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
app_db.run_migrations().await.unwrap();
|
||||||
|
sqlx::query("INSERT INTO contacts (user_id, username) VALUES (1, 'one'), (2, 'two')")
|
||||||
|
.execute(&app_db.pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let app_handle = Arc::new(RwLock::new(Arc::new(app_db)));
|
||||||
|
let store = NativeUserDiscoveryStore::new(app_handle, temp.path().to_str().unwrap());
|
||||||
|
|
||||||
|
store.update_config("config".into()).await.unwrap();
|
||||||
|
assert_eq!(store.get_config().await.unwrap(), "config");
|
||||||
|
store.set_shares(vec![vec![1], vec![2]]).await.unwrap();
|
||||||
|
let assigned = store.get_share_for_contact(1).await.unwrap();
|
||||||
|
assert_eq!(store.get_share_for_contact(1).await.unwrap(), assigned);
|
||||||
|
store.set_contact_version(1, vec![7]).await.unwrap();
|
||||||
|
assert_eq!(store.get_contact_version(1).await.unwrap(), Some(vec![7]));
|
||||||
|
store
|
||||||
|
.push_own_promotion_and_clear_old_version(1, 1, vec![8])
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(store.get_contact_promotion(1).await.unwrap(), Some(vec![8]));
|
||||||
|
|
||||||
|
let promotion = OtherPromotion {
|
||||||
|
promotion_id: 3,
|
||||||
|
public_id: 4,
|
||||||
|
from_contact_id: 1,
|
||||||
|
threshold: 2,
|
||||||
|
announcement_share: vec![5],
|
||||||
|
public_key_verified_timestamp: Some(6),
|
||||||
|
};
|
||||||
|
store.store_other_promotion(promotion).await.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
store
|
||||||
|
.get_other_promotions_by_public_id(4)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.len(),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
let announced = AnnouncedUser {
|
||||||
|
user_id: 2,
|
||||||
|
public_key: vec![9],
|
||||||
|
public_id: 10,
|
||||||
|
};
|
||||||
|
store
|
||||||
|
.push_new_user_relation(1, announced, Some(11))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
store
|
||||||
|
.get_announced_user_by_public_id(10)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.unwrap()
|
||||||
|
.user_id,
|
||||||
|
2
|
||||||
|
);
|
||||||
|
|
||||||
|
let rust_path = temp.path().join("rust.sqlite");
|
||||||
|
let rust_path_string = rust_path.display().to_string();
|
||||||
|
let rust_db = Database::new(&rust_path_string, None, false).await.unwrap();
|
||||||
|
rust_db.run_migrations().await.unwrap();
|
||||||
|
let rust_handle = Arc::new(RwLock::new(Arc::new(rust_db)));
|
||||||
|
|
||||||
|
let mut rng = rand::rng();
|
||||||
|
let identity = IdentityKeyPair::generate(&mut rng);
|
||||||
|
let public_key = identity.identity_key().serialize().to_vec();
|
||||||
|
sqlx::query(
|
||||||
|
"INSERT INTO signal_identities (name, identity_key, timestamp) VALUES ('1', ?, 0)",
|
||||||
|
)
|
||||||
|
.bind(&public_key)
|
||||||
|
.execute(&rust_handle.read().await.pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let mut key_manager = KeyManager::generate().unwrap();
|
||||||
|
key_manager.signal_identity = Some(SignalIdentityKey {
|
||||||
|
identity_key_pair_structure: identity.serialize().to_vec(),
|
||||||
|
registration_id: 1,
|
||||||
|
pre_key_store: HashMap::new(),
|
||||||
|
});
|
||||||
|
let utils = NativeUserDiscoveryUtils::new(Arc::new(Mutex::new(key_manager)), rust_handle);
|
||||||
|
let message = b"native discovery";
|
||||||
|
let signature = utils.sign_data(message).await.unwrap();
|
||||||
|
assert!(utils
|
||||||
|
.verify_signature(message, &public_key, &signature)
|
||||||
|
.await
|
||||||
|
.unwrap());
|
||||||
|
assert!(utils.verify_stored_pubkey(1, &public_key).await.unwrap());
|
||||||
|
assert!(!utils.verify_stored_pubkey(2, &public_key).await.unwrap());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_twonly_api_100_messages() -> Result<(), Box<dyn std::error::Error>> {
|
async fn test_twonly_api_100_messages() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
use rust_lib_twonly::database::Database;
|
use rust_lib_twonly::database::signal::Database;
|
||||||
use rust_lib_twonly::signal::engine::RustSignalEngine;
|
use rust_lib_twonly::signal::engine::RustSignalEngine;
|
||||||
|
|
||||||
let _ = pretty_env_logger::try_init();
|
let _ = pretty_env_logger::try_init();
|
||||||
|
|
|
||||||
|
|
@ -53,19 +53,19 @@ abstract class CargoKitBuildTask extends DefaultTask {
|
||||||
}
|
}
|
||||||
|
|
||||||
def executableName = Os.isFamily(Os.FAMILY_WINDOWS) ? "run_build_tool.cmd" : "run_build_tool.sh"
|
def executableName = Os.isFamily(Os.FAMILY_WINDOWS) ? "run_build_tool.cmd" : "run_build_tool.sh"
|
||||||
def path = Paths.get(new File(pluginFile).parent, "..", executableName);
|
def path = Paths.get(new File(pluginFile).parent, "..", executableName).toString()
|
||||||
|
|
||||||
def manifestDir = Paths.get(project.buildscript.sourceFile.parent, project.cargokit.manifestDir)
|
def manifestDir = Paths.get(project.buildscript.sourceFile.parent, project.cargokit.manifestDir).toString()
|
||||||
|
|
||||||
def rootProjectDir = project.rootProject.projectDir
|
def rootProjectDir = project.rootProject.projectDir.absolutePath
|
||||||
|
|
||||||
if (!Os.isFamily(Os.FAMILY_WINDOWS)) {
|
if (!Os.isFamily(Os.FAMILY_WINDOWS)) {
|
||||||
project.exec {
|
project.providers.exec {
|
||||||
commandLine 'chmod', '+x', path
|
commandLine 'chmod', '+x', path
|
||||||
}
|
}.result.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
project.exec {
|
project.providers.exec {
|
||||||
executable path
|
executable path
|
||||||
args "build-gradle"
|
args "build-gradle"
|
||||||
environment "CARGOKIT_ROOT_PROJECT_DIR", rootProjectDir
|
environment "CARGOKIT_ROOT_PROJECT_DIR", rootProjectDir
|
||||||
|
|
@ -80,7 +80,7 @@ abstract class CargoKitBuildTask extends DefaultTask {
|
||||||
environment "CARGOKIT_MIN_SDK_VERSION", minSdkVersion
|
environment "CARGOKIT_MIN_SDK_VERSION", minSdkVersion
|
||||||
environment "CARGOKIT_TARGET_PLATFORMS", targetPlatforms.join(",")
|
environment "CARGOKIT_TARGET_PLATFORMS", targetPlatforms.join(",")
|
||||||
environment "CARGOKIT_JAVA_HOME", System.properties['java.home']
|
environment "CARGOKIT_JAVA_HOME", System.properties['java.home']
|
||||||
}
|
}.result.get()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue