initialization of user discovery works

This commit is contained in:
otsmr 2026-04-19 17:13:05 +02:00
parent 6516c4564c
commit e1956c9807
47 changed files with 4435 additions and 884 deletions

View file

@ -5,20 +5,36 @@
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
import 'database/contact.dart';
import 'frb_generated.dart'; import 'frb_generated.dart';
// These functions are ignored because they are not marked as `pub`: `get_workspace` // 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)]`: `Twonly` // These types are ignored because they are neither used by any `pub` functions nor (for structs and enums) marked `#[frb(unignore)]`: `TwonlyFlutter`
Future<void> initializeTwonly({required TwonlyConfig config}) => Future<void> initializeTwonlyFlutter({required TwonlyConfig config}) =>
RustLib.instance.api.crateBridgeInitializeTwonly(config: config); RustLib.instance.api.crateBridgeInitializeTwonlyFlutter(config: config);
Future<List<Contact>> getAllContacts() => class AnnouncedUser {
RustLib.instance.api.crateBridgeGetAllContacts(); const AnnouncedUser({
required this.userId,
required this.publicKey,
required this.publicId,
});
final PlatformInt64 userId;
final Uint8List publicKey;
final PlatformInt64 publicId;
Future<OtherPromotion> loadPromotions() => @override
RustLib.instance.api.crateBridgeLoadPromotions(); 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 OtherPromotion { class OtherPromotion {
const OtherPromotion({ const OtherPromotion({

View file

@ -0,0 +1,58 @@
// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.12.0.
// ignore_for_file: invalid_use_of_internal_member
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
import '../bridge.dart';
import '../frb_generated.dart';
// These functions are ignored because they are not marked as `pub`: `get_callbacks`
// These types are ignored because they are neither used by any `pub` functions nor (for structs and enums) marked `#[frb(unignore)]`: `FlutterCallbacks`, `Logging`, `UserDiscoveryCallbacks`
Future<void> initFlutterCallbacks({
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)
userDiscoveryPushOwnPromotion,
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,
}) => RustLib.instance.api.crateBridgeCallbacksInitFlutterCallbacks(
loggingGetStreamSink: loggingGetStreamSink,
userDiscoverySignData: userDiscoverySignData,
userDiscoveryVerifySignature: userDiscoveryVerifySignature,
userDiscoveryVerifyStoredPubkey: userDiscoveryVerifyStoredPubkey,
userDiscoverySetShares: userDiscoverySetShares,
userDiscoveryGetShareForContact: userDiscoveryGetShareForContact,
userDiscoveryPushOwnPromotion: userDiscoveryPushOwnPromotion,
userDiscoveryGetOwnPromotionsAfterVersion:
userDiscoveryGetOwnPromotionsAfterVersion,
userDiscoveryStoreOtherPromotion: userDiscoveryStoreOtherPromotion,
userDiscoveryGetOtherPromotionsByPublicId:
userDiscoveryGetOtherPromotionsByPublicId,
userDiscoveryGetAnnouncedUserByPublicId:
userDiscoveryGetAnnouncedUserByPublicId,
userDiscoveryGetContactVersion: userDiscoveryGetContactVersion,
userDiscoverySetContactVersion: userDiscoverySetContactVersion,
userDiscoveryPushNewUserRelation: userDiscoveryPushNewUserRelation,
);

View file

@ -0,0 +1,61 @@
// 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
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
import '../../frb_generated.dart';
class FlutterUserDiscovery {
const FlutterUserDiscovery();
static Future<Uint8List> getCurrentVersion() => RustLib.instance.api
.crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetCurrentVersion();
static Future<List<Uint8List>> getNewMessages({
required PlatformInt64 contactId,
required List<int> receivedVersion,
}) => RustLib.instance.api
.crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetNewMessages(
contactId: contactId,
receivedVersion: receivedVersion,
);
static Future<void> handleUserDiscoveryMessages({
required PlatformInt64 contactId,
required List<Uint8List> messages,
}) => RustLib.instance.api
.crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryHandleUserDiscoveryMessages(
contactId: contactId,
messages: messages,
);
static Future<void> initializeOrUpdate({
required int threshold,
required PlatformInt64 userId,
required List<int> publicKey,
}) => RustLib.instance.api
.crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryInitializeOrUpdate(
threshold: threshold,
userId: userId,
publicKey: publicKey,
);
static Future<bool> shouldRequestNewMessages({
required PlatformInt64 contactId,
required List<int> version,
}) => RustLib.instance.api
.crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryShouldRequestNewMessages(
contactId: contactId,
version: version,
);
@override
int get hashCode => 0;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is FlutterUserDiscovery && runtimeType == other.runtimeType;
}

View file

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

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
// This file is automatically generated, so please do not edit it. // This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.12.0. // @generated by `flutter_rust_bridge`@ 2.12.0.
// ignore_for_file: unused_import, non_constant_identifier_names, unused_field // ignore_for_file: unused_import, unnecessary_import, non_constant_identifier_names, unused_field
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
@ -10,7 +10,8 @@ import 'dart:ffi' as ffi;
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_io.dart'; import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_io.dart';
import 'bridge.dart'; import 'bridge.dart';
import 'database/contact.dart'; import 'bridge/callbacks.dart';
import 'bridge/wrapper/user_discovery.dart';
import 'frb_generated.dart'; import 'frb_generated.dart';
abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> { abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@ -24,9 +25,96 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
AnyhowException dco_decode_AnyhowException(dynamic raw); AnyhowException dco_decode_AnyhowException(dynamic raw);
@protected
FutureOr<RustStreamSink<String>> Function()
dco_decode_DartFn_Inputs__Output_StreamSink_String_Sse_AnyhowException(
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
Object dco_decode_DartOpaque(dynamic raw);
@protected
RustStreamSink<String> dco_decode_StreamSink_String_Sse(dynamic raw);
@protected @protected
String dco_decode_String(dynamic raw); String dco_decode_String(dynamic raw);
@protected
AnnouncedUser dco_decode_announced_user(dynamic raw);
@protected
bool dco_decode_bool(dynamic raw);
@protected
AnnouncedUser dco_decode_box_autoadd_announced_user(dynamic raw);
@protected @protected
PlatformInt64 dco_decode_box_autoadd_i_64(dynamic raw); PlatformInt64 dco_decode_box_autoadd_i_64(dynamic raw);
@ -34,20 +122,41 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
TwonlyConfig dco_decode_box_autoadd_twonly_config(dynamic raw); TwonlyConfig dco_decode_box_autoadd_twonly_config(dynamic raw);
@protected @protected
Contact dco_decode_contact(dynamic raw); FlutterUserDiscovery dco_decode_flutter_user_discovery(dynamic raw);
@protected @protected
PlatformInt64 dco_decode_i_64(dynamic raw); PlatformInt64 dco_decode_i_64(dynamic raw);
@protected @protected
List<Contact> dco_decode_list_contact(dynamic raw); PlatformInt64 dco_decode_isize(dynamic raw);
@protected
List<Uint8List> dco_decode_list_list_prim_u_8_strict(dynamic raw);
@protected
List<OtherPromotion> dco_decode_list_other_promotion(dynamic raw);
@protected
List<int> dco_decode_list_prim_u_8_loose(dynamic raw);
@protected @protected
Uint8List dco_decode_list_prim_u_8_strict(dynamic raw); Uint8List dco_decode_list_prim_u_8_strict(dynamic raw);
@protected
AnnouncedUser? dco_decode_opt_box_autoadd_announced_user(dynamic raw);
@protected @protected
PlatformInt64? dco_decode_opt_box_autoadd_i_64(dynamic raw); PlatformInt64? dco_decode_opt_box_autoadd_i_64(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
Uint8List? dco_decode_opt_list_prim_u_8_strict(dynamic raw);
@protected @protected
OtherPromotion dco_decode_other_promotion(dynamic raw); OtherPromotion dco_decode_other_promotion(dynamic raw);
@ -63,12 +172,34 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
void dco_decode_unit(dynamic raw); void dco_decode_unit(dynamic raw);
@protected
BigInt dco_decode_usize(dynamic raw);
@protected @protected
AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer); AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer);
@protected
Object sse_decode_DartOpaque(SseDeserializer deserializer);
@protected
RustStreamSink<String> sse_decode_StreamSink_String_Sse(
SseDeserializer deserializer,
);
@protected @protected
String sse_decode_String(SseDeserializer deserializer); String sse_decode_String(SseDeserializer deserializer);
@protected
AnnouncedUser sse_decode_announced_user(SseDeserializer deserializer);
@protected
bool sse_decode_bool(SseDeserializer deserializer);
@protected
AnnouncedUser sse_decode_box_autoadd_announced_user(
SseDeserializer deserializer,
);
@protected @protected
PlatformInt64 sse_decode_box_autoadd_i_64(SseDeserializer deserializer); PlatformInt64 sse_decode_box_autoadd_i_64(SseDeserializer deserializer);
@ -78,20 +209,53 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
); );
@protected @protected
Contact sse_decode_contact(SseDeserializer deserializer); FlutterUserDiscovery sse_decode_flutter_user_discovery(
SseDeserializer deserializer,
);
@protected @protected
PlatformInt64 sse_decode_i_64(SseDeserializer deserializer); PlatformInt64 sse_decode_i_64(SseDeserializer deserializer);
@protected @protected
List<Contact> sse_decode_list_contact(SseDeserializer deserializer); PlatformInt64 sse_decode_isize(SseDeserializer deserializer);
@protected
List<Uint8List> sse_decode_list_list_prim_u_8_strict(
SseDeserializer deserializer,
);
@protected
List<OtherPromotion> sse_decode_list_other_promotion(
SseDeserializer deserializer,
);
@protected
List<int> sse_decode_list_prim_u_8_loose(SseDeserializer deserializer);
@protected @protected
Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer); Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer);
@protected
AnnouncedUser? sse_decode_opt_box_autoadd_announced_user(
SseDeserializer deserializer,
);
@protected @protected
PlatformInt64? sse_decode_opt_box_autoadd_i_64(SseDeserializer deserializer); PlatformInt64? sse_decode_opt_box_autoadd_i_64(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
Uint8List? sse_decode_opt_list_prim_u_8_strict(SseDeserializer deserializer);
@protected @protected
OtherPromotion sse_decode_other_promotion(SseDeserializer deserializer); OtherPromotion sse_decode_other_promotion(SseDeserializer deserializer);
@ -108,10 +272,10 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
void sse_decode_unit(SseDeserializer deserializer); void sse_decode_unit(SseDeserializer deserializer);
@protected @protected
int sse_decode_i_32(SseDeserializer deserializer); BigInt sse_decode_usize(SseDeserializer deserializer);
@protected @protected
bool sse_decode_bool(SseDeserializer deserializer); int sse_decode_i_32(SseDeserializer deserializer);
@protected @protected
void sse_encode_AnyhowException( void sse_encode_AnyhowException(
@ -119,9 +283,112 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer, SseSerializer serializer,
); );
@protected
void sse_encode_DartFn_Inputs__Output_StreamSink_String_Sse_AnyhowException(
FutureOr<RustStreamSink<String>> Function() self,
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
void sse_encode_DartOpaque(Object self, SseSerializer serializer);
@protected
void sse_encode_StreamSink_String_Sse(
RustStreamSink<String> self,
SseSerializer serializer,
);
@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
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_i_64( void sse_encode_box_autoadd_i_64(
PlatformInt64 self, PlatformInt64 self,
@ -135,13 +402,31 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
); );
@protected @protected
void sse_encode_contact(Contact self, SseSerializer serializer); void sse_encode_flutter_user_discovery(
FlutterUserDiscovery self,
SseSerializer serializer,
);
@protected @protected
void sse_encode_i_64(PlatformInt64 self, SseSerializer serializer); void sse_encode_i_64(PlatformInt64 self, SseSerializer serializer);
@protected @protected
void sse_encode_list_contact(List<Contact> self, SseSerializer serializer); void sse_encode_isize(PlatformInt64 self, SseSerializer serializer);
@protected
void sse_encode_list_list_prim_u_8_strict(
List<Uint8List> self,
SseSerializer serializer,
);
@protected
void sse_encode_list_other_promotion(
List<OtherPromotion> self,
SseSerializer serializer,
);
@protected
void sse_encode_list_prim_u_8_loose(List<int> self, SseSerializer serializer);
@protected @protected
void sse_encode_list_prim_u_8_strict( void sse_encode_list_prim_u_8_strict(
@ -149,12 +434,36 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer, SseSerializer serializer,
); );
@protected
void sse_encode_opt_box_autoadd_announced_user(
AnnouncedUser? self,
SseSerializer serializer,
);
@protected @protected
void sse_encode_opt_box_autoadd_i_64( void sse_encode_opt_box_autoadd_i_64(
PlatformInt64? self, PlatformInt64? self,
SseSerializer serializer, 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
void sse_encode_opt_list_prim_u_8_strict(
Uint8List? self,
SseSerializer serializer,
);
@protected @protected
void sse_encode_other_promotion( void sse_encode_other_promotion(
OtherPromotion self, OtherPromotion self,
@ -174,10 +483,10 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
void sse_encode_unit(void self, SseSerializer serializer); void sse_encode_unit(void self, SseSerializer serializer);
@protected @protected
void sse_encode_i_32(int self, SseSerializer serializer); void sse_encode_usize(BigInt self, SseSerializer serializer);
@protected @protected
void sse_encode_bool(bool self, SseSerializer serializer); void sse_encode_i_32(int self, SseSerializer serializer);
} }
// Section: wire_class // Section: wire_class

View file

@ -1,7 +1,7 @@
// This file is automatically generated, so please do not edit it. // This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.12.0. // @generated by `flutter_rust_bridge`@ 2.12.0.
// ignore_for_file: unused_import, non_constant_identifier_names // ignore_for_file: unused_import, unnecessary_import, non_constant_identifier_names
// Static analysis wrongly picks the IO variant, thus ignore this // Static analysis wrongly picks the IO variant, thus ignore this
@ -11,7 +11,8 @@ import 'dart:convert';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_web.dart'; import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_web.dart';
import 'bridge.dart'; import 'bridge.dart';
import 'database/contact.dart'; import 'bridge/callbacks.dart';
import 'bridge/wrapper/user_discovery.dart';
import 'frb_generated.dart'; import 'frb_generated.dart';
abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> { abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@ -25,9 +26,96 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
AnyhowException dco_decode_AnyhowException(dynamic raw); AnyhowException dco_decode_AnyhowException(dynamic raw);
@protected
FutureOr<RustStreamSink<String>> Function()
dco_decode_DartFn_Inputs__Output_StreamSink_String_Sse_AnyhowException(
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
Object dco_decode_DartOpaque(dynamic raw);
@protected
RustStreamSink<String> dco_decode_StreamSink_String_Sse(dynamic raw);
@protected @protected
String dco_decode_String(dynamic raw); String dco_decode_String(dynamic raw);
@protected
AnnouncedUser dco_decode_announced_user(dynamic raw);
@protected
bool dco_decode_bool(dynamic raw);
@protected
AnnouncedUser dco_decode_box_autoadd_announced_user(dynamic raw);
@protected @protected
PlatformInt64 dco_decode_box_autoadd_i_64(dynamic raw); PlatformInt64 dco_decode_box_autoadd_i_64(dynamic raw);
@ -35,20 +123,41 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
TwonlyConfig dco_decode_box_autoadd_twonly_config(dynamic raw); TwonlyConfig dco_decode_box_autoadd_twonly_config(dynamic raw);
@protected @protected
Contact dco_decode_contact(dynamic raw); FlutterUserDiscovery dco_decode_flutter_user_discovery(dynamic raw);
@protected @protected
PlatformInt64 dco_decode_i_64(dynamic raw); PlatformInt64 dco_decode_i_64(dynamic raw);
@protected @protected
List<Contact> dco_decode_list_contact(dynamic raw); PlatformInt64 dco_decode_isize(dynamic raw);
@protected
List<Uint8List> dco_decode_list_list_prim_u_8_strict(dynamic raw);
@protected
List<OtherPromotion> dco_decode_list_other_promotion(dynamic raw);
@protected
List<int> dco_decode_list_prim_u_8_loose(dynamic raw);
@protected @protected
Uint8List dco_decode_list_prim_u_8_strict(dynamic raw); Uint8List dco_decode_list_prim_u_8_strict(dynamic raw);
@protected
AnnouncedUser? dco_decode_opt_box_autoadd_announced_user(dynamic raw);
@protected @protected
PlatformInt64? dco_decode_opt_box_autoadd_i_64(dynamic raw); PlatformInt64? dco_decode_opt_box_autoadd_i_64(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
Uint8List? dco_decode_opt_list_prim_u_8_strict(dynamic raw);
@protected @protected
OtherPromotion dco_decode_other_promotion(dynamic raw); OtherPromotion dco_decode_other_promotion(dynamic raw);
@ -64,12 +173,34 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
void dco_decode_unit(dynamic raw); void dco_decode_unit(dynamic raw);
@protected
BigInt dco_decode_usize(dynamic raw);
@protected @protected
AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer); AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer);
@protected
Object sse_decode_DartOpaque(SseDeserializer deserializer);
@protected
RustStreamSink<String> sse_decode_StreamSink_String_Sse(
SseDeserializer deserializer,
);
@protected @protected
String sse_decode_String(SseDeserializer deserializer); String sse_decode_String(SseDeserializer deserializer);
@protected
AnnouncedUser sse_decode_announced_user(SseDeserializer deserializer);
@protected
bool sse_decode_bool(SseDeserializer deserializer);
@protected
AnnouncedUser sse_decode_box_autoadd_announced_user(
SseDeserializer deserializer,
);
@protected @protected
PlatformInt64 sse_decode_box_autoadd_i_64(SseDeserializer deserializer); PlatformInt64 sse_decode_box_autoadd_i_64(SseDeserializer deserializer);
@ -79,20 +210,53 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
); );
@protected @protected
Contact sse_decode_contact(SseDeserializer deserializer); FlutterUserDiscovery sse_decode_flutter_user_discovery(
SseDeserializer deserializer,
);
@protected @protected
PlatformInt64 sse_decode_i_64(SseDeserializer deserializer); PlatformInt64 sse_decode_i_64(SseDeserializer deserializer);
@protected @protected
List<Contact> sse_decode_list_contact(SseDeserializer deserializer); PlatformInt64 sse_decode_isize(SseDeserializer deserializer);
@protected
List<Uint8List> sse_decode_list_list_prim_u_8_strict(
SseDeserializer deserializer,
);
@protected
List<OtherPromotion> sse_decode_list_other_promotion(
SseDeserializer deserializer,
);
@protected
List<int> sse_decode_list_prim_u_8_loose(SseDeserializer deserializer);
@protected @protected
Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer); Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer);
@protected
AnnouncedUser? sse_decode_opt_box_autoadd_announced_user(
SseDeserializer deserializer,
);
@protected @protected
PlatformInt64? sse_decode_opt_box_autoadd_i_64(SseDeserializer deserializer); PlatformInt64? sse_decode_opt_box_autoadd_i_64(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
Uint8List? sse_decode_opt_list_prim_u_8_strict(SseDeserializer deserializer);
@protected @protected
OtherPromotion sse_decode_other_promotion(SseDeserializer deserializer); OtherPromotion sse_decode_other_promotion(SseDeserializer deserializer);
@ -109,10 +273,10 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
void sse_decode_unit(SseDeserializer deserializer); void sse_decode_unit(SseDeserializer deserializer);
@protected @protected
int sse_decode_i_32(SseDeserializer deserializer); BigInt sse_decode_usize(SseDeserializer deserializer);
@protected @protected
bool sse_decode_bool(SseDeserializer deserializer); int sse_decode_i_32(SseDeserializer deserializer);
@protected @protected
void sse_encode_AnyhowException( void sse_encode_AnyhowException(
@ -120,9 +284,112 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer, SseSerializer serializer,
); );
@protected
void sse_encode_DartFn_Inputs__Output_StreamSink_String_Sse_AnyhowException(
FutureOr<RustStreamSink<String>> Function() self,
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
void sse_encode_DartOpaque(Object self, SseSerializer serializer);
@protected
void sse_encode_StreamSink_String_Sse(
RustStreamSink<String> self,
SseSerializer serializer,
);
@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
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_i_64( void sse_encode_box_autoadd_i_64(
PlatformInt64 self, PlatformInt64 self,
@ -136,13 +403,31 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
); );
@protected @protected
void sse_encode_contact(Contact self, SseSerializer serializer); void sse_encode_flutter_user_discovery(
FlutterUserDiscovery self,
SseSerializer serializer,
);
@protected @protected
void sse_encode_i_64(PlatformInt64 self, SseSerializer serializer); void sse_encode_i_64(PlatformInt64 self, SseSerializer serializer);
@protected @protected
void sse_encode_list_contact(List<Contact> self, SseSerializer serializer); void sse_encode_isize(PlatformInt64 self, SseSerializer serializer);
@protected
void sse_encode_list_list_prim_u_8_strict(
List<Uint8List> self,
SseSerializer serializer,
);
@protected
void sse_encode_list_other_promotion(
List<OtherPromotion> self,
SseSerializer serializer,
);
@protected
void sse_encode_list_prim_u_8_loose(List<int> self, SseSerializer serializer);
@protected @protected
void sse_encode_list_prim_u_8_strict( void sse_encode_list_prim_u_8_strict(
@ -150,12 +435,36 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer, SseSerializer serializer,
); );
@protected
void sse_encode_opt_box_autoadd_announced_user(
AnnouncedUser? self,
SseSerializer serializer,
);
@protected @protected
void sse_encode_opt_box_autoadd_i_64( void sse_encode_opt_box_autoadd_i_64(
PlatformInt64? self, PlatformInt64? self,
SseSerializer serializer, 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
void sse_encode_opt_list_prim_u_8_strict(
Uint8List? self,
SseSerializer serializer,
);
@protected @protected
void sse_encode_other_promotion( void sse_encode_other_promotion(
OtherPromotion self, OtherPromotion self,
@ -175,10 +484,10 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
void sse_encode_unit(void self, SseSerializer serializer); void sse_encode_unit(void self, SseSerializer serializer);
@protected @protected
void sse_encode_i_32(int self, SseSerializer serializer); void sse_encode_usize(BigInt self, SseSerializer serializer);
@protected @protected
void sse_encode_bool(bool self, SseSerializer serializer); void sse_encode_i_32(int self, SseSerializer serializer);
} }
// Section: wire_class // Section: wire_class

View file

@ -10,6 +10,7 @@ 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/frb_generated.dart'; import 'package:twonly/core/frb_generated.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/callbacks/callbacks.dart';
import 'package:twonly/src/database/twonly.db.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';
@ -37,7 +38,11 @@ void main() async {
globalApplicationSupportDirectory = globalApplicationSupportDirectory =
(await getApplicationSupportDirectory()).path; (await getApplicationSupportDirectory()).path;
await bridge.initializeTwonly( initLogger();
await initFlutterCallbacksForRust();
await bridge.initializeTwonlyFlutter(
config: bridge.TwonlyConfig( config: bridge.TwonlyConfig(
databasePath: '$globalApplicationSupportDirectory/twonly.sqlite', databasePath: '$globalApplicationSupportDirectory/twonly.sqlite',
dataDirectory: globalApplicationSupportDirectory, dataDirectory: globalApplicationSupportDirectory,
@ -68,8 +73,6 @@ void main() async {
await deleteLocalUserData(); await deleteLocalUserData();
} }
initLogger();
final settingsController = SettingsChangeProvider(); final settingsController = SettingsChangeProvider();
await settingsController.loadSettings(); await settingsController.loadSettings();

View file

@ -0,0 +1,28 @@
import 'package:twonly/core/bridge/callbacks.dart';
import 'package:twonly/src/callbacks/logging.callbacks.dart';
import 'package:twonly/src/callbacks/user_discovery.callbacks.dart';
Future<void> initFlutterCallbacksForRust() async {
await initFlutterCallbacks(
loggingGetStreamSink: LoggingCallbacks.getStreamSink,
userDiscoverySetShares: UserDiscoveryCallbacks.setShares,
userDiscoveryGetShareForContact:
UserDiscoveryCallbacks.userDiscoveryGetShareForContact,
userDiscoveryPushOwnPromotion: UserDiscoveryCallbacks.pushOwnPromotion,
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,
);
}

View file

@ -0,0 +1,32 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
import 'package:twonly/src/utils/log.dart';
class LoggingCallbacks {
static Future<RustStreamSink<String>> getStreamSink() async {
final dartLogSink = RustStreamSink<String>();
Timer.periodic(const Duration(milliseconds: 100), (timer) {
try {
dartLogSink.stream.listen(
(log) {
if (log.contains('INFO ')) {
Log.info(log.split('INFO ')[1]);
} else if (log.contains('DEBUG ')) {
Log.info(log.split('DEBUG ')[1]);
} else if (kDebugMode) {
print(log);
}
},
onDone: () => Log.error('Log stream closed'),
);
timer.cancel();
} catch (e) {
// stream not yet initialized
}
});
return dartLogSink;
}
}

View file

@ -0,0 +1,294 @@
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/globals.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 {
var privKey = (await getSignalIdentityKeyPair())?.getPrivateKey();
if (privKey == null) return null;
final random = getRandomUint8List(32);
final signature = sign(
privKey.serialize(),
inputData,
random,
);
privKey = null;
return signature;
}
static Future<bool> verifySignature(
Uint8List inputData,
Uint8List pubKey,
Uint8List signature,
) async {
return Curve.verifySignature(
IdentityKey.fromBytes(pubKey, 0).publicKey,
inputData,
signature,
);
}
static Future<bool> verifyStoredPubKey(
int contactId,
Uint8List pubKey,
) async {
final storedPublicKey = await getPublicKeyFromContact(contactId);
if (storedPublicKey != null) {
return storedPublicKey == pubKey;
} else {
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> pushOwnPromotion(
int contactId,
int version, // Maps to versionId or logic control
Uint8List promotion,
) async {
try {
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(
UserDiscoveryAnnouncedUser(
announcedUserId: announcedUser.userId,
announcedPublicKey: announcedUser.publicKey,
publicId: 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;
}
}
}

View file

@ -34,6 +34,8 @@ class Routes {
static const String settingsPrivacy = '/settings/privacy'; static const String settingsPrivacy = '/settings/privacy';
static const String settingsPrivacyBlockUsers = static const String settingsPrivacyBlockUsers =
'/settings/privacy/block_users'; '/settings/privacy/block_users';
static const String settingsPrivacyUserDiscovery =
'/settings/privacy/user_discovery';
static const String settingsNotification = '/settings/notification'; static const String settingsNotification = '/settings/notification';
static const String settingsStorage = '/settings/storage_data'; static const String settingsStorage = '/settings/storage_data';
static const String settingsStorageImport = '/settings/storage_data/import'; static const String settingsStorageImport = '/settings/storage_data/import';

View file

@ -96,6 +96,9 @@ class UserData {
@JsonKey(defaultValue: false) @JsonKey(defaultValue: false)
bool isUserDiscoveryEnabled = false; bool isUserDiscoveryEnabled = false;
@JsonKey(defaultValue: false)
int minimumRequiredImagesExchanged = 4;
// -- Custom DATA -- // -- Custom DATA --
@JsonKey(defaultValue: 100_000) @JsonKey(defaultValue: 100_000)

View file

@ -37,7 +37,8 @@ import 'package:twonly/src/views/settings/help/faq/verifybadge.dart';
import 'package:twonly/src/views/settings/help/help.view.dart'; import 'package:twonly/src/views/settings/help/help.view.dart';
import 'package:twonly/src/views/settings/notification.view.dart'; import 'package:twonly/src/views/settings/notification.view.dart';
import 'package:twonly/src/views/settings/privacy.view.dart'; import 'package:twonly/src/views/settings/privacy.view.dart';
import 'package:twonly/src/views/settings/privacy_view_block.view.dart'; import 'package:twonly/src/views/settings/privacy/block_users.view.dart';
import 'package:twonly/src/views/settings/privacy/user_discovery.view.dart';
import 'package:twonly/src/views/settings/profile/modify_avatar.view.dart'; import 'package:twonly/src/views/settings/profile/modify_avatar.view.dart';
import 'package:twonly/src/views/settings/profile/profile.view.dart'; import 'package:twonly/src/views/settings/profile/profile.view.dart';
import 'package:twonly/src/views/settings/settings_main.view.dart'; import 'package:twonly/src/views/settings/settings_main.view.dart';
@ -200,7 +201,11 @@ final routerProvider = GoRouter(
routes: [ routes: [
GoRoute( GoRoute(
path: 'block_users', path: 'block_users',
builder: (context, state) => const PrivacyViewBlockUsersView(), builder: (context, state) => const BlockUsersView(),
),
GoRoute(
path: 'user_discovery',
builder: (context, state) => const UserDiscoverySettingsView(),
), ),
], ],
), ),

View file

@ -0,0 +1,26 @@
import 'package:twonly/core/bridge/wrapper/user_discovery.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/qr.dart';
import 'package:twonly/src/utils/storage.dart';
Future<void> initializeOrUpdateUserDiscovery({
required int threshold,
required int minimumRequiredImagesExchanged,
}) async {
try {
await FlutterUserDiscovery.initializeOrUpdate(
threshold: threshold,
userId: gUser.userId,
publicKey: await getUserPublicKey(),
);
await updateUserdata((u) {
u
..isUserDiscoveryEnabled = true
..minimumRequiredImagesExchanged = minimumRequiredImagesExchanged;
return u;
});
} catch (e) {
Log.error(e);
}
}

View file

@ -1,10 +1,32 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
final primaryColor = const Color(0xFF57CC99);
final ThemeData lightTheme = ThemeData( final ThemeData lightTheme = ThemeData(
colorScheme: ColorScheme.fromSeed( colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF57CC99), seedColor: primaryColor,
), ),
inputDecorationTheme: const InputDecorationTheme( inputDecorationTheme: const InputDecorationTheme(
border: OutlineInputBorder(), border: OutlineInputBorder(),
), ),
); );
final ButtonStyle primaryColorButtonStyle = FilledButton.styleFrom(
backgroundColor: primaryColor,
foregroundColor: Colors.black87,
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16),
// Adjusting the border radius (default is usually 20+)
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8), // Lower number = sharper corners
),
);
final ButtonStyle secondaryGreyButtonStyle = FilledButton.styleFrom(
backgroundColor: Colors.grey[200],
foregroundColor: Colors.black87,
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16),
// Adjusting the border radius (default is usually 20+)
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8), // Lower number = sharper corners
),
);

View file

@ -404,3 +404,50 @@ Future<List<int>> sha256File(File file) async {
converter.close(); converter.close();
return sha256Sink.events.single.bytes; return sha256Sink.events.single.bytes;
} }
List<TextSpan> formattedText(String input) {
// Pattern to find text between asterisks
final regex = RegExp(r'\*(.*?)\*');
final List<TextSpan> spans = [];
// Track the current position in the string
int lastMatchEnd = 0;
for (final match in regex.allMatches(input)) {
// Add text before the match (Normal style)
if (match.start > lastMatchEnd) {
spans.add(
TextSpan(
text: input.substring(lastMatchEnd, match.start),
style: const TextStyle(color: Colors.black),
),
);
}
// Add the matched text (Bold style)
// match.group(1) is the text without the asterisks
spans.add(
TextSpan(
text: match.group(1),
style: const TextStyle(
fontWeight: FontWeight.bold,
color: Colors.black,
),
),
);
lastMatchEnd = match.end;
}
// Add any remaining text after the last match
if (lastMatchEnd < input.length) {
spans.add(
TextSpan(
text: input.substring(lastMatchEnd),
style: const TextStyle(color: Colors.black),
),
);
}
return spans;
}

View file

@ -73,6 +73,13 @@ class _PrivacyViewState extends State<PrivacyView> {
setState(() {}); setState(() {});
}, },
), ),
ListTile(
title: const Text('Freunde finden'),
onTap: () async {
await context.push(Routes.settingsPrivacyUserDiscovery);
setState(() {});
},
),
const Divider(), const Divider(),
ListTile( ListTile(
title: Text(context.lang.settingsTypingIndication), title: Text(context.lang.settingsTypingIndication),

View file

@ -7,14 +7,14 @@ import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/components/avatar_icon.component.dart'; import 'package:twonly/src/views/components/avatar_icon.component.dart';
import 'package:twonly/src/views/components/user_context_menu.component.dart'; import 'package:twonly/src/views/components/user_context_menu.component.dart';
class PrivacyViewBlockUsersView extends StatefulWidget { class BlockUsersView extends StatefulWidget {
const PrivacyViewBlockUsersView({super.key}); const BlockUsersView({super.key});
@override @override
State<PrivacyViewBlockUsersView> createState() => _PrivacyViewBlockUsers(); State<BlockUsersView> createState() => _PrivacyViewBlockUsers();
} }
class _PrivacyViewBlockUsers extends State<PrivacyViewBlockUsersView> { class _PrivacyViewBlockUsers extends State<BlockUsersView> {
late Stream<List<Contact>> allUsers; late Stream<List<Contact>> allUsers;
List<Contact> filteredUsers = []; List<Contact> filteredUsers = [];
String filter = ''; String filter = '';

View file

@ -0,0 +1,26 @@
import 'package:flutter/material.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/views/settings/privacy/user_discovery/user_discovery_disabled.component.dart';
import 'package:twonly/src/views/settings/privacy/user_discovery/user_discovery_enabled.component.dart';
class UserDiscoverySettingsView extends StatefulWidget {
const UserDiscoverySettingsView({super.key});
@override
State<UserDiscoverySettingsView> createState() =>
_UserDiscoverySettingsViewState();
}
class _UserDiscoverySettingsViewState extends State<UserDiscoverySettingsView> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Freunde finden'),
),
body: gUser.isUserDiscoveryEnabled
? UserDiscoveryEnabledComponent(onUpdate: () => setState(() {}))
: UserDiscoveryDisabledComponent(onUpdate: () => setState(() {})),
);
}
}

View file

@ -0,0 +1,93 @@
import 'package:flutter/material.dart';
import 'package:twonly/src/services/user_discovery.service.dart';
import 'package:twonly/src/themes/light.dart';
import 'package:twonly/src/utils/misc.dart';
class UserDiscoveryDisabledComponent extends StatefulWidget {
const UserDiscoveryDisabledComponent({required this.onUpdate, super.key});
final VoidCallback onUpdate;
@override
State<UserDiscoveryDisabledComponent> createState() =>
_UserDiscoveryDisabledComponentState();
}
class _UserDiscoveryDisabledComponentState
extends State<UserDiscoveryDisabledComponent> {
Future<void> initializeUserDiscoveryWithDefaultSettings() async {
await initializeOrUpdateUserDiscovery(
threshold: 2,
minimumRequiredImagesExchanged: 4,
);
widget.onUpdate();
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 40),
child: ListView(
children: [
const SizedBox(height: 45),
const Text(
'twonly verzichten auf Telefonnummern, daher schlagen wir dir Freunde stattdessen über gemeinsame Kontakte vor sicher und privat.',
textAlign: TextAlign.center,
),
const SizedBox(height: 30),
RichText(
text: TextSpan(
children: formattedText(
'Deine Freundesliste ist für *Fremde komplett unsichtbar*. Nur deine Freunde können Teile davon sehen und zwar nur die Personen, mit denen sie selbst *gemeinsame Freunde* haben.',
),
),
textAlign: TextAlign.center,
),
const SizedBox(height: 35),
const Text(
'Du hast die Kontrolle',
style: TextStyle(fontSize: 17),
textAlign: TextAlign.center,
),
const SizedBox(height: 20),
const Text(
'Entscheide selbst, wer deine Freunde sehen darf. Du kannst deine Meinung jederzeit ändern oder bestimmte Personen verstecken.',
textAlign: TextAlign.center,
),
const SizedBox(height: 50),
FilledButton(
onPressed: initializeUserDiscoveryWithDefaultSettings,
style: primaryColorButtonStyle.merge(
FilledButton.styleFrom(
padding: EdgeInsets.symmetric(horizontal: 32, vertical: 24),
),
),
child: const Text('Mit Standardeinstellungen aktivieren'),
),
const SizedBox(height: 20),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: FilledButton(
onPressed: () {},
style: secondaryGreyButtonStyle,
child: const Text('Einstellungen anpassen'),
),
),
const SizedBox(height: 15),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: FilledButton(
onPressed: () {},
style: secondaryGreyButtonStyle,
child: const Text('Mehr erfahren'),
),
),
],
),
);
}
}

View file

@ -0,0 +1,45 @@
import 'package:flutter/material.dart';
class UserDiscoveryEnabledComponent extends StatefulWidget {
const UserDiscoveryEnabledComponent({required this.onUpdate, super.key});
final VoidCallback onUpdate;
@override
State<UserDiscoveryEnabledComponent> createState() =>
_UserDiscoveryEnabledComponentState();
}
class _UserDiscoveryEnabledComponentState
extends State<UserDiscoveryEnabledComponent> {
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(top: 10),
child: ListView(
children: [
const ExpansionTile(
shape: RoundedRectangleBorder(),
collapsedShape: RoundedRectangleBorder(),
tilePadding: EdgeInsets.symmetric(horizontal: 17),
title: Text('Freunde die du teilst'),
subtitle: Text(
'Du teilst nur Freunde, die diese Funktion ebenfalls aktiviert haben und die den von dir festgelegten Schwellenwert erreicht haben.',
style: TextStyle(fontSize: 10),
),
children: [],
),
ListTile(
title: Text('Einstellungen ändern'),
// onTap: () {},
),
const Divider(),
ListTile(
title: Text('Deaktivieren'),
onTap: () {},
),
],
),
);
}
}

View file

@ -412,26 +412,26 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: drift name: drift
sha256: "970cd188fddb111b26ea6a9b07a62bf5c2432d74147b8122c67044ae3b97e99e" sha256: "055c249d1f91be5a47fe447f88afc24c4ca6f4cd6c5ed66767b4797d48acc2e5"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.31.0" version: "2.32.1"
drift_dev: drift_dev:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: drift_dev name: drift_dev
sha256: "917184b2fb867b70a548a83bf0d36268423b38d39968c06cce4905683da49587" sha256: "88a9de3af8571518148a6d8a513b57779fd1e60a026d3ab8a481a878fba01d91"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.31.0" version: "2.32.1"
drift_flutter: drift_flutter:
dependency: "direct main" dependency: "direct main"
description: description:
name: drift_flutter name: drift_flutter
sha256: c07120854742a0cae2f7501a0da02493addde550db6641d284983c08762e60a7 sha256: "887fdec622174dc7eaefd0048403e34ee07cc18626ac8a7544cc3b8a4a172166"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.2.8" version: "0.3.0"
ed25519_edwards: ed25519_edwards:
dependency: "direct overridden" dependency: "direct overridden"
description: description:
@ -1809,30 +1809,38 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.0" version: "2.4.0"
sqlite3: sqlcipher_flutter_libs:
dependency: "direct main" dependency: transitive
description: description:
name: sqlite3 name: sqlcipher_flutter_libs
sha256: "3145bd74dcdb4fd6f5c6dda4d4e4490a8087d7f286a14dee5d37087290f0f8a2" sha256: "38d62d659d2fb8739bf25a42c9a350d1fdd6c29a5a61f13a946778ec75d27929"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.9.4" version: "0.7.0+eol"
sqlite3:
dependency: transitive
description:
name: sqlite3
sha256: "56da3e13ed7d28a66f930aa2b2b29db6736a233f08283326e96321dd812030f5"
url: "https://pub.dev"
source: hosted
version: "3.3.1"
sqlite3_flutter_libs: sqlite3_flutter_libs:
dependency: transitive dependency: transitive
description: description:
name: sqlite3_flutter_libs name: sqlite3_flutter_libs
sha256: eeb9e3a45207649076b808f8a5a74d68770d0b7f26ccef6d5f43106eee5375ad sha256: "3ed7553eee7bb368f8950f58ba29f634e06e813c029aff6a0d60862b96de8454"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.5.42" version: "0.6.0+eol"
sqlparser: sqlparser:
dependency: transitive dependency: transitive
description: description:
name: sqlparser name: sqlparser
sha256: "337e9997f7141ffdd054259128553c348635fa318f7ca492f07a4ab76f850d19" sha256: ab2b467425f1d4f3acfa5fd11a08226f7d6c26ff102c06be1807e1dff34e050b
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.43.1" version: "0.44.3"
stack_trace: stack_trace:
dependency: transitive dependency: transitive
description: description:

View file

@ -58,9 +58,8 @@ dependencies:
json_annotation: ^4.9.0 # google.dev json_annotation: ^4.9.0 # google.dev
protobuf: ^4.0.0 # google.dev protobuf: ^4.0.0 # google.dev
scrollable_positioned_list: ^0.3.8 # google.dev scrollable_positioned_list: ^0.3.8 # google.dev
drift: ^2.25.1 drift: ^2.32.0
drift_flutter: ^0.2.4 drift_flutter: ^0.3.0
sqlite3: ^2.9.4
# Flutter Favorite # Flutter Favorite

166
rust/Cargo.lock generated
View file

@ -305,6 +305,15 @@ version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
[[package]]
name = "crossbeam-channel"
version = "0.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
dependencies = [
"crossbeam-utils",
]
[[package]] [[package]]
name = "crossbeam-queue" name = "crossbeam-queue"
version = "0.3.12" version = "0.3.12"
@ -392,6 +401,15 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "deranged"
version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
dependencies = [
"powerfmt",
]
[[package]] [[package]]
name = "digest" name = "digest"
version = "0.10.7" version = "0.10.7"
@ -1071,9 +1089,9 @@ dependencies = [
[[package]] [[package]]
name = "libsqlite3-sys" name = "libsqlite3-sys"
version = "0.35.0" version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "133c182a6a2c87864fe97778797e46c7e999672690dc9fa3ee8e241aa4a9c13f" checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
dependencies = [ dependencies = [
"cc", "cc",
"pkg-config", "pkg-config",
@ -1107,6 +1125,15 @@ version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "matchers"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
dependencies = [
"regex-automata",
]
[[package]] [[package]]
name = "md-5" name = "md-5"
version = "0.10.6" version = "0.10.6"
@ -1149,6 +1176,15 @@ version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084"
[[package]]
name = "nu-ansi-term"
version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [
"windows-sys 0.61.2",
]
[[package]] [[package]]
name = "num-bigint-dig" name = "num-bigint-dig"
version = "0.8.6" version = "0.8.6"
@ -1165,6 +1201,12 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "num-conv"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967"
[[package]] [[package]]
name = "num-integer" name = "num-integer"
version = "0.1.46" version = "0.1.46"
@ -1260,6 +1302,12 @@ dependencies = [
"windows-link", "windows-link",
] ]
[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]] [[package]]
name = "pem-rfc7468" name = "pem-rfc7468"
version = "0.7.0" version = "0.7.0"
@ -1340,6 +1388,12 @@ dependencies = [
"zerovec", "zerovec",
] ]
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]] [[package]]
name = "ppv-lite86" name = "ppv-lite86"
version = "0.2.21" version = "0.2.21"
@ -1582,7 +1636,7 @@ name = "rust_lib_twonly"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"flutter_rust_bridge", "flutter_rust_bridge",
"parking_lot", "paste",
"pretty_env_logger", "pretty_env_logger",
"prost-build", "prost-build",
"protocols", "protocols",
@ -1592,6 +1646,8 @@ dependencies = [
"thiserror", "thiserror",
"tokio", "tokio",
"tracing", "tracing",
"tracing-appender",
"tracing-subscriber",
] ]
[[package]] [[package]]
@ -1725,6 +1781,15 @@ dependencies = [
"digest 0.11.2", "digest 0.11.2",
] ]
[[package]]
name = "sharded-slab"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
dependencies = [
"lazy_static",
]
[[package]] [[package]]
name = "shlex" name = "shlex"
version = "1.3.0" version = "1.3.0"
@ -2009,6 +2074,12 @@ version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "symlink"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a"
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.117" version = "2.0.117"
@ -2073,6 +2144,15 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "thread_local"
version = "1.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
dependencies = [
"cfg-if",
]
[[package]] [[package]]
name = "threadpool" name = "threadpool"
version = "1.8.1" version = "1.8.1"
@ -2082,6 +2162,37 @@ dependencies = [
"num_cpus", "num_cpus",
] ]
[[package]]
name = "time"
version = "0.3.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
dependencies = [
"deranged",
"itoa",
"num-conv",
"powerfmt",
"serde_core",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
[[package]]
name = "time-macros"
version = "0.2.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215"
dependencies = [
"num-conv",
"time-core",
]
[[package]] [[package]]
name = "tinystr" name = "tinystr"
version = "0.8.3" version = "0.8.3"
@ -2158,6 +2269,19 @@ dependencies = [
"tracing-core", "tracing-core",
] ]
[[package]]
name = "tracing-appender"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "050686193eb999b4bb3bc2acfa891a13da00f79734704c4b8b4ef1a10b368a3c"
dependencies = [
"crossbeam-channel",
"symlink",
"thiserror",
"time",
"tracing-subscriber",
]
[[package]] [[package]]
name = "tracing-attributes" name = "tracing-attributes"
version = "0.1.31" version = "0.1.31"
@ -2176,6 +2300,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"valuable",
]
[[package]]
name = "tracing-log"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
dependencies = [
"log",
"once_cell",
"tracing-core",
]
[[package]]
name = "tracing-subscriber"
version = "0.3.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
dependencies = [
"matchers",
"nu-ansi-term",
"once_cell",
"regex-automata",
"sharded-slab",
"smallvec",
"thread_local",
"tracing",
"tracing-core",
"tracing-log",
] ]
[[package]] [[package]]
@ -2235,6 +2389,12 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "valuable"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
[[package]] [[package]]
name = "vcpkg" name = "vcpkg"
version = "0.2.15" version = "0.2.15"

View file

@ -1,3 +1,35 @@
[workspace] [package]
members = ["protocols", "core"] name = "rust_lib_twonly"
resolver = "3" version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "staticlib"]
[dependencies]
flutter_rust_bridge = "=2.12.0"
thiserror = "2.0.18"
sqlx = { version = "0.9.0-alpha.1", default-features = false, features = [
"runtime-tokio",
"sqlite",
"migrate",
"macros",
"chrono",
"derive",
"json",
] }
tokio = { version = "1.44", features = ["full"] }
tracing = "0.1.44"
rand = "0.10.1"
protocols = { path = "../rust_dependencies/protocols" }
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
tracing-appender = "0.2.5"
paste = "1.0.15"
[dev-dependencies]
pretty_env_logger = "0.5.0"
tempfile = "3.27.0"
[build-dependencies]
prost-build = "0.14.1"

View file

@ -0,0 +1,47 @@
pub(crate) mod log;
mod macros;
pub(crate) mod user_discovery;
use flutter_rust_bridge::DartFnFuture;
use protocols::user_discovery::traits::{AnnouncedUser, OtherPromotion};
use super::error::Result;
use crate::{callback_generator, frb_generated::StreamSink};
use std::sync::{Arc, OnceLock};
use crate::bridge::error::TwonlyError;
static FLUTTER_CALLBACKS: OnceLock<FlutterCallbacks> = OnceLock::new();
// This will also generate the function init_flutter_callbacks which MUST be called from Flutter to initialize the callbacks
callback_generator! {
FlutterCallbacks {
Logging logging {
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: (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,
}
}
}
pub(crate) fn get_callbacks() -> Result<&'static FlutterCallbacks> {
FLUTTER_CALLBACKS
.get()
.ok_or(TwonlyError::MissingCallbackInitialization)
}

View file

@ -0,0 +1,28 @@
use crate::frb_generated::StreamSink;
use tracing_subscriber::fmt::MakeWriter;
#[derive(Clone)]
pub(crate) struct DartWriter {
pub(crate) sink: StreamSink<String>,
}
impl std::io::Write for DartWriter {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
if let Ok(msg) = std::str::from_utf8(buf) {
let _ = self.sink.add(msg.trim_end().to_string());
}
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
impl<'a> MakeWriter<'a> for DartWriter {
type Writer = DartWriter;
fn make_writer(&'a self) -> Self::Writer {
self.clone()
}
}

View file

@ -0,0 +1,57 @@
#[macro_export]
macro_rules! callback_generator {
(
$struct_name:ident {
$(
$sub_struct_name:ident $sub_struct_field:ident {
$(
$fn_name:ident : ($($input:ty),*) => $output:ty
),* $(,)?
}
),* $(,)?
}
) => {
// 1. Generate the Nested Sub-Structs
$(
pub(crate) struct $sub_struct_name {
$(
pub(crate) $fn_name: Arc<dyn Fn($($input),*) -> DartFnFuture<$output> + Send + Sync + 'static>,
)*
}
)*
// 2. Generate the Main Container Struct
pub(crate) struct $struct_name {
$(
pub(crate) $sub_struct_field: $sub_struct_name,
)*
}
// 3. Generate the Automated Init Function
paste::paste! {
pub async fn init_flutter_callbacks(
$(
$(
// Parameters: sub-struct_field + _ + fn_name
[<$sub_struct_field _ $fn_name>]: impl Fn($($input),*) -> DartFnFuture<$output> + Send + Sync + 'static,
)*
)*
) {
let callbacks = $struct_name {
$(
$sub_struct_field: $sub_struct_name {
$(
$fn_name: Arc::new([<$sub_struct_field _ $fn_name>]),
)*
},
)*
};
// Use the static global strictly named FLUTTER_CALLBACKS
FLUTTER_CALLBACKS.set(callbacks).unwrap_or_else(|_| {
println!("Callbacks were already initialized!");
});
}
}
};
}

View file

@ -0,0 +1,169 @@
use crate::bridge::callbacks::get_callbacks;
use crate::bridge::error::TwonlyError;
use crate::bridge::get_twonly_flutter;
use protocols::user_discovery::error::{Result, UserDiscoveryError};
use protocols::user_discovery::traits::UserDiscoveryUtils;
use protocols::user_discovery::traits::{AnnouncedUser, OtherPromotion, UserDiscoveryStore};
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().unwrap();
let config_path =
PathBuf::from(&ws.config.data_directory).join("user_discovery_config.json");
if !config_path.is_file() {
return Err(UserDiscoveryError::NotInitialized);
}
tracing::debug!("Loading Config from {}", config_path.display());
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().unwrap();
let config_path =
PathBuf::from(&ws.config.data_directory).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(
&self,
contact_id: i64,
version: u32,
promotion: Vec<u8>,
) -> Result<()> {
(get_callbacks()?.user_discovery.push_own_promotion)(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())
}
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())
}
}

View file

@ -7,9 +7,15 @@ pub type Result<T> = core::result::Result<T, TwonlyError>;
pub enum TwonlyError { pub enum TwonlyError {
#[error("global twonly is not initialized")] #[error("global twonly is not initialized")]
Initialization, Initialization,
#[error("init_flutter_callbacks was not called")]
MissingCallbackInitialization,
#[error("Could not find the given database")] #[error("Could not find the given database")]
DatabaseNotFound, DatabaseNotFound,
#[error("{0}")] #[error("{0}")]
UserDiscoveryError(#[from] UserDiscoveryError),
#[error("Error in dart callback")]
DartError,
#[error("{0}")]
SqliteError(#[from] sqlx::Error), SqliteError(#[from] sqlx::Error),
} }

73
rust/src/bridge/log.rs Normal file
View file

@ -0,0 +1,73 @@
use crate::bridge::callbacks::{get_callbacks, log::DartWriter};
use std::sync::{Mutex, OnceLock};
use tracing_appender::non_blocking::{NonBlocking, WorkerGuard};
use tracing_subscriber::{
fmt::Layer, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Registry,
};
static TRACING_GUARDS: OnceLock<Mutex<Option<(WorkerGuard, WorkerGuard)>>> = OnceLock::new();
static TRACING_INIT: OnceLock<()> = OnceLock::new();
pub(crate) async fn init_tracing(logs_dir: &std::path::Path, is_dart_available: bool) {
let _ = std::fs::create_dir_all(logs_dir);
let mut dart_sink = None;
if is_dart_available {
if let Ok(callbacks) = get_callbacks() {
dart_sink = Some((callbacks.logging.get_stream_sink)().await);
}
}
TRACING_INIT.get_or_init(|| {
let (non_blocking_stdout, _non_blocking_file) = build_writers(logs_dir);
let stdout_layer = Layer::new()
.with_writer(non_blocking_stdout)
.with_ansi(true)
.with_target(true);
// let file_layer = Layer::new()
// .with_writer(non_blocking_file)
// .with_ansi(false)
// .with_target(true);
// Replace stdout with our new DartWriter!
let registry = Registry::default()
.with(
EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new("debug,refinery_core=warn,refinery=warn")),
)
.with(stdout_layer);
if let Some(sink) = dart_sink {
let dart_writer = DartWriter { sink };
let dart_layer = tracing_subscriber::fmt::Layer::new()
.with_writer(dart_writer)
.with_ansi(false)
.with_target(true);
let _ = registry.with(dart_layer).try_init();
} else {
let _ = registry.try_init();
}
});
}
fn build_writers(logs_dir: &std::path::Path) -> (NonBlocking, NonBlocking) {
let file_appender = tracing_appender::rolling::RollingFileAppender::builder()
.rotation(tracing_appender::rolling::Rotation::DAILY)
.filename_prefix("twonly")
.filename_suffix("log")
.build(logs_dir)
.expect("Failed to create file appender");
let (non_blocking_file, file_guard) = tracing_appender::non_blocking(file_appender);
let (non_blocking_stdout, stdout_guard) = tracing_appender::non_blocking(std::io::stdout());
TRACING_GUARDS
.set(Mutex::new(Some((file_guard, stdout_guard))))
.ok();
(non_blocking_stdout, non_blocking_file)
}

View file

@ -1,44 +0,0 @@
static TRACING_INIT: OnceLock<()> = OnceLock::new();
pub(crate) fn init_tracing(logs_dir: &std::path::Path) {
TRACING_INIT.get_or_init(|| {
let (non_blocking_stdout, non_blocking_file) = build_writers(logs_dir);
let stdout_layer = Layer::new()
.with_writer(non_blocking_stdout)
.with_ansi(true)
.with_target(true);
let file_layer = Layer::new()
.with_writer(non_blocking_file)
.with_ansi(false)
.with_target(true);
Registry::default()
.with(
EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new("info,refinery_core=warn,refinery=warn")),
)
.with(stdout_layer)
.with(file_layer)
.init();
});
}
fn build_writers(logs_dir: &std::path::Path) -> (NonBlocking, NonBlocking) {
let file_appender = tracing_appender::rolling::RollingFileAppender::builder()
.rotation(tracing_appender::rolling::Rotation::DAILY)
.filename_prefix("twonly")
.filename_suffix("log")
.build(logs_dir)
.expect("Failed to create file appender");
let (non_blocking_file, file_guard) = tracing_appender::non_blocking(file_appender);
let (non_blocking_stdout, stdout_guard) = tracing_appender::non_blocking(std::io::stdout());
TRACING_GUARDS
.set(Mutex::new(Some((file_guard, stdout_guard))))
.ok();
(non_blocking_stdout, non_blocking_file)
}

View file

@ -1,18 +1,22 @@
#![allow(unexpected_cfgs)] #![allow(unexpected_cfgs)]
pub mod callbacks;
pub mod error; pub mod error;
mod user_discovery_utils; pub mod log;
use crate::bridge::user_discovery_utils::UserDiscoveryUtilsFlutter; pub mod wrapper;
use crate::database::contact::Contact;
use crate::database::Database; use crate::bridge::callbacks::user_discovery::{
use crate::user_discovery_store::UserDiscoveryDatabaseStore; UserDiscoveryStoreFlutter, UserDiscoveryUtilsFlutter,
};
use crate::bridge::log::init_tracing;
use crate::utils::Shared; use crate::utils::Shared;
use error::Result; use error::Result;
use error::TwonlyError; use error::TwonlyError;
use flutter_rust_bridge::frb; use flutter_rust_bridge::frb;
use protocols::user_discovery::UserDiscovery; use protocols::user_discovery::UserDiscovery;
use std::sync::Arc; use std::path::PathBuf;
use tokio::sync::OnceCell; use tokio::sync::OnceCell;
pub use protocols::user_discovery::traits::AnnouncedUser;
pub use protocols::user_discovery::traits::OtherPromotion; pub use protocols::user_discovery::traits::OtherPromotion;
#[frb(mirror(OtherPromotion))] #[frb(mirror(OtherPromotion))]
@ -25,34 +29,56 @@ pub struct _OtherPromotion {
pub public_key_verified_timestamp: Option<i64>, pub public_key_verified_timestamp: Option<i64>,
} }
#[frb(mirror(AnnouncedUser))]
pub struct _AnnouncedUser {
pub user_id: i64,
pub public_key: Vec<u8>,
pub public_id: i64,
}
pub struct TwonlyConfig { pub struct TwonlyConfig {
pub database_path: String, pub database_path: String,
pub data_directory: String, pub data_directory: String,
} }
pub(crate) struct Twonly { pub(crate) struct TwonlyFlutter {
#[allow(dead_code)] #[allow(dead_code)]
pub(crate) config: TwonlyConfig, pub(crate) config: TwonlyConfig,
pub(crate) database: Arc<Database>, // /// Rust runs in the same process as drift, the database can only be opened in readonly mode
// pub(crate) twonly_db_readonly: Arc<Database>,
pub(crate) user_discovery: pub(crate) user_discovery:
Shared<Option<UserDiscovery<UserDiscoveryDatabaseStore, UserDiscoveryUtilsFlutter>>>, Shared<UserDiscovery<UserDiscoveryStoreFlutter, UserDiscoveryUtilsFlutter>>,
} }
static GLOBAL_TWONLY: OnceCell<Twonly> = OnceCell::const_new(); static GLOBAL_TWONLY: OnceCell<TwonlyFlutter> = OnceCell::const_new();
pub(crate) fn get_workspace() -> Result<&'static Twonly> { pub(super) fn get_twonly_flutter() -> Result<&'static TwonlyFlutter> {
GLOBAL_TWONLY.get().ok_or(TwonlyError::Initialization) GLOBAL_TWONLY.get().ok_or(TwonlyError::Initialization)
} }
pub async fn initialize_twonly(config: TwonlyConfig) -> Result<()> { pub async fn initialize_twonly_flutter(config: TwonlyConfig) -> Result<()> {
tracing::debug!("Initialized twonly workspace."); let log_dir = PathBuf::from(&config.data_directory).join("log");
let twonly_res: Result<&'static Twonly> = GLOBAL_TWONLY init_tracing(&log_dir, true).await;
tracing::info!("Initialized twonly workspace.");
let twonly_res: Result<&'static TwonlyFlutter> = GLOBAL_TWONLY
.get_or_try_init(|| async { .get_or_try_init(|| async {
let database = Arc::new(Database::new(&config.database_path).await?); // let database_dir = PathBuf::from(&config.database_path.clone());
Ok(Twonly { // let Some(rust_db_path) = database_dir.parent() else {
// return Err(TwonlyError::DatabaseNotFound);
// };
// let rust_db_path = rust_db_path.join("rust_db.sqlite").display().to_string();
// let twonly_db_readonly = Arc::new(Database::new(&config.database_path, true).await?);
// let rust_db = Arc::new(Database::new(&rust_db_path, false).await?);
Ok(TwonlyFlutter {
config, config,
database, // twonly_db_readonly,
user_discovery: Shared::default(), // rust_db,
user_discovery: Shared::new(UserDiscovery::new(
UserDiscoveryStoreFlutter {},
UserDiscoveryUtilsFlutter {},
)?),
}) })
}) })
.await; .await;
@ -61,102 +87,3 @@ pub async fn initialize_twonly(config: TwonlyConfig) -> Result<()> {
Ok(()) Ok(())
} }
pub async fn get_all_contacts() -> Result<Vec<Contact>> {
let twonly = get_workspace()?;
Contact::get_all_contacts(twonly.database.as_ref()).await
}
pub fn load_promotions() -> OtherPromotion {
todo!()
}
#[cfg(test)]
pub(crate) mod tests {
use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
use tempfile::{NamedTempFile, TempDir};
use tokio::sync::OnceCell;
use crate::{database::Database, utils::Shared};
use super::error::Result;
use super::Twonly;
use std::{path::PathBuf, sync::Arc};
use tokio::sync::Mutex;
use super::{get_workspace, initialize_twonly, TwonlyConfig};
static TWONLY_TESTING: [OnceCell<Twonly>; 10] = [
OnceCell::const_new(),
OnceCell::const_new(),
OnceCell::const_new(),
OnceCell::const_new(),
OnceCell::const_new(),
OnceCell::const_new(),
OnceCell::const_new(),
OnceCell::const_new(),
OnceCell::const_new(),
OnceCell::const_new(),
];
static TWONLY_TESTING_INDEX: OnceCell<Arc<Mutex<usize>>> = OnceCell::const_new();
pub(crate) async fn initialize_twonly_for_testing(use_global: bool) -> Result<&'static Twonly> {
let default_twonly_database = PathBuf::from("tests/testing.db");
if !default_twonly_database.is_file() {
panic!("{} not found!", default_twonly_database.display())
}
let temp_file = NamedTempFile::new().unwrap().path().to_owned();
tracing::info!("Crated db copy: {}", temp_file.display());
let conn = SqlitePoolOptions::new()
.connect_with(
format!("sqlite://{}", default_twonly_database.display())
.parse::<SqliteConnectOptions>()
.unwrap(),
)
.await
.unwrap();
let path_str = temp_file.display().to_string();
sqlx::query("VACUUM INTO $1")
.bind(path_str)
.execute(&conn)
.await
.expect("Failed to backup database");
let tmp_dir = TempDir::new().unwrap().path().to_owned();
std::fs::create_dir_all(&tmp_dir).unwrap();
let config = TwonlyConfig {
database_path: temp_file.display().to_string(),
data_directory: tmp_dir.to_str().unwrap().to_string(),
};
if use_global {
initialize_twonly(config).await.unwrap();
get_workspace()
} else {
let index = TWONLY_TESTING_INDEX
.get_or_init(|| async { Arc::default() })
.await;
let mut index = index.lock().await;
let res: Result<&'static Twonly> = TWONLY_TESTING[*index]
.get_or_try_init(|| async {
let database = Arc::new(Database::new(&config.database_path).await?);
Ok(Twonly {
config,
database,
user_discovery: Shared::default(),
})
})
.await;
tracing::debug!("TWONLY_TESTING_INDEX: {index}");
*index += 1;
res
}
}
}

View file

@ -1,27 +0,0 @@
use protocols::user_discovery::error::Result;
use protocols::user_discovery::traits::UserDiscoveryUtils;
pub(crate) struct UserDiscoveryUtilsFlutter {}
impl UserDiscoveryUtils for UserDiscoveryUtilsFlutter {
async fn sign_data(&self, input_data: &[u8]) -> Result<Vec<u8>> {
todo!()
}
async fn verify_signature(
&self,
input_data: &[u8],
pubkey: &[u8],
signature: &[u8],
) -> Result<bool> {
todo!()
}
async fn verify_stored_pubkey(
&self,
from_contact_id: protocols::user_discovery::UserID,
pubkey: &[u8],
) -> Result<bool> {
todo!()
}
}

View file

@ -0,0 +1 @@
pub mod user_discovery;

View file

@ -0,0 +1,61 @@
use crate::bridge::error::Result;
use crate::bridge::get_twonly_flutter;
pub struct FlutterUserDiscovery {}
impl FlutterUserDiscovery {
pub async fn initialize_or_update(
threshold: u8,
user_id: i64,
public_key: Vec<u8>,
) -> Result<()> {
Ok(get_twonly_flutter()?
.user_discovery
.get()
.await
.initialize_or_update(threshold, user_id, public_key)
.await?)
}
pub async fn get_current_version() -> Result<Vec<u8>> {
Ok(get_twonly_flutter()?
.user_discovery
.get()
.await
.get_current_version()
.await?)
}
pub async fn get_new_messages(
contact_id: i64,
received_version: &[u8],
) -> Result<Vec<Vec<u8>>> {
Ok(get_twonly_flutter()?
.user_discovery
.get()
.await
.get_new_messages(contact_id, received_version)
.await?)
}
pub async fn should_request_new_messages(contact_id: i64, version: &[u8]) -> Result<bool> {
Ok(get_twonly_flutter()?
.user_discovery
.get()
.await
.should_request_new_messages(contact_id, version)
.await?)
}
pub async fn handle_user_discovery_messages(
contact_id: i64,
messages: Vec<Vec<u8>>,
) -> Result<()> {
Ok(get_twonly_flutter()?
.user_discovery
.get()
.await
.handle_user_discovery_messages(contact_id, messages)
.await?)
}
}

View file

@ -1,58 +0,0 @@
// use sqlx::types::chrono::{DateTime, Utc};
use sqlx::FromRow;
use super::Database;
use crate::bridge::error::Result;
#[derive(FromRow, Clone, Debug)]
struct ContactRow {
pub(crate) user_id: i64,
pub(crate) username: String,
// pub(crate) created_at: DateTime<Utc>,
}
pub struct Contact {
pub user_id: i64,
pub username: String,
// pub created_at: DateTime<Utc>,
}
impl From<ContactRow> for Contact {
fn from(row: ContactRow) -> Self {
Self {
user_id: row.user_id,
username: row.username,
// created_at: row.created_at,
}
}
}
impl Contact {
pub(crate) async fn get_all_contacts(db: &Database) -> Result<Vec<Contact>> {
let rows = sqlx::query_as::<_, ContactRow>("SELECT * FROM contacts")
.fetch_all(&db.pool)
.await?;
Ok(rows.into_iter().map(Into::into).collect())
}
}
#[cfg(test)]
mod tests {
use crate::bridge::tests::initialize_twonly_for_testing;
use crate::database::contact::Contact;
#[tokio::test]
async fn test_get_all_contacts() {
let twonly = initialize_twonly_for_testing(true).await.unwrap();
let contacts = Contact::get_all_contacts(&twonly.database).await.unwrap();
let users = vec!["alice", "bob", "charlie", "david", "frank"];
assert_eq!(contacts.len(), users.len());
for contact in contacts {
assert_eq!(users[contact.user_id as usize], &contact.username);
}
}
}

View file

@ -1,4 +1,3 @@
pub(crate) mod contact;
use crate::bridge::error::{Result, TwonlyError}; use crate::bridge::error::{Result, TwonlyError};
use sqlx::migrate::MigrateDatabase; use sqlx::migrate::MigrateDatabase;
use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions}; use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
@ -10,7 +9,7 @@ pub(crate) struct Database {
} }
impl Database { impl Database {
pub(crate) async fn new(db_path: &String) -> Result<Self> { pub(crate) async fn new(db_path: &String, read_only: bool) -> Result<Self> {
let db_url = format!("sqlite://{}", db_path); let db_url = format!("sqlite://{}", db_path);
match Sqlite::database_exists(&db_url).await { match Sqlite::database_exists(&db_url).await {
@ -41,6 +40,7 @@ impl Database {
let connect_options = format!("{db_url}?mode=rwc") let connect_options = format!("{db_url}?mode=rwc")
.parse::<SqliteConnectOptions>()? .parse::<SqliteConnectOptions>()?
.log_statements(log_statements_level) .log_statements(log_statements_level)
.read_only(read_only)
.journal_mode(sqlx::sqlite::SqliteJournalMode::Wal) .journal_mode(sqlx::sqlite::SqliteJournalMode::Wal)
.foreign_keys(true) .foreign_keys(true)
.busy_timeout(Duration::from_millis(5000)) .busy_timeout(Duration::from_millis(5000))
@ -53,6 +53,13 @@ impl Database {
.connect_with(connect_options) .connect_with(connect_options)
.await?; .await?;
let row: (String, String) = sqlx::query_as("SELECT sqlite_version(), sqlite_source_id()")
.fetch_one(&pool)
.await?;
tracing::info!("Rust SQLite Version: {}", row.0);
tracing::info!("Rust SQLite Source ID: {}", row.1);
Ok(Self { pool: pool }) Ok(Self { pool: pool })
} }
} }

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,5 @@
pub mod bridge; pub mod bridge;
mod database; mod database;
mod frb_generated; mod frb_generated;
mod user_discovery_store; mod standalone;
mod utils; mod utils;

View file

@ -0,0 +1,9 @@
use crate::{bridge::TwonlyConfig, database::Database};
use std::sync::Arc;
pub(crate) struct TwonlyStandalone {
#[allow(dead_code)]
pub(crate) config: TwonlyConfig,
/// Because Rust is called from a different process it is safe to write to the twonly_db.
pub(crate) twonly_db: Arc<Database>,
}

View file

@ -1,349 +0,0 @@
#[allow(async_fn_in_trait)]
use protocols::user_discovery::error::{Result, UserDiscoveryError};
use protocols::user_discovery::traits::{AnnouncedUser, OtherPromotion, UserDiscoveryStore};
use protocols::user_discovery::UserID;
use sqlx::{QueryBuilder, Row, Sqlite, Transaction};
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use crate::bridge::error::TwonlyError;
use crate::bridge::{get_workspace, Twonly};
#[derive(Clone)]
pub(crate) struct UserDiscoveryDatabaseStore {
ws: Arc<&'static Twonly>,
}
impl UserDiscoveryStore for UserDiscoveryDatabaseStore {
async fn new() -> Self {
#[cfg(test)]
return Self {
ws: Arc::new(
crate::bridge::tests::initialize_twonly_for_testing(false)
.await
.unwrap(),
),
};
#[allow(unreachable_code)]
Self {
ws: Arc::new(get_workspace().unwrap()),
}
}
async fn get_config(&self) -> Result<String> {
let config_path =
PathBuf::from(&self.ws.config.data_directory).join("user_discovery_config.json");
if !config_path.is_file() {
return Err(UserDiscoveryError::NotInitialized);
}
tracing::debug!("Loading Config from {}", config_path.display());
Ok(std::fs::read_to_string(&config_path)?)
}
async fn update_config(&self, update: String) -> Result<()> {
tracing::debug!("Updating configuration file.");
let config_path =
PathBuf::from(&self.ws.config.data_directory).join("user_discovery_config.json");
std::fs::write(config_path, &update)?;
Ok(())
}
async fn set_shares(&self, shares: Vec<Vec<u8>>) -> Result<()> {
let mut query_builder: QueryBuilder<Sqlite> =
QueryBuilder::new("INSERT INTO user_discovery_shares (share) ");
query_builder.push_values(shares, |mut b, share| {
b.push_bind(share);
});
query_builder
.build()
.execute(&self.ws.database.pool)
.await
.map_err(TwonlyError::from)?;
Ok(())
}
async fn get_share_for_contact(&self, contact_id: UserID) -> Result<Vec<u8>> {
let mut tx: Transaction<'_, Sqlite> = self
.ws
.database
.pool
.begin()
.await
.map_err(TwonlyError::from)?;
// 1. Check if the user already has a share assigned
let existing: Option<Vec<u8>> =
sqlx::query_scalar("SELECT share FROM user_discovery_shares WHERE contact_id = ?")
.bind(contact_id)
.fetch_optional(&mut *tx)
.await
.map_err(TwonlyError::from)?;
if let Some(share) = existing {
tx.commit().await.map_err(TwonlyError::from)?;
return Ok(share);
}
// 2. No share found. Try to assign an available one (where contact_id is NULL)
let rows_affected = sqlx::query(
"UPDATE user_discovery_shares
SET contact_id = ?
WHERE share_id = (
SELECT share_id FROM user_discovery_shares
WHERE contact_id IS NULL
LIMIT 1
)",
)
.bind(contact_id)
.execute(&mut *tx)
.await
.map_err(TwonlyError::from)?
.rows_affected();
if rows_affected == 0 {
return Err(UserDiscoveryError::NoSharesLeft);
}
// 3. Retrieve the newly assigned share
let assigned_share: Vec<u8> =
sqlx::query_scalar("SELECT share FROM user_discovery_shares WHERE contact_id = ?")
.bind(contact_id)
.fetch_one(&mut *tx)
.await
.map_err(TwonlyError::from)?;
tx.commit().await.map_err(TwonlyError::from)?;
Ok(assigned_share)
}
async fn push_own_promotion(
&self,
contact_id: UserID,
version: u32,
promotion: Vec<u8>,
) -> Result<()> {
sqlx::query(
r#"
INSERT INTO user_discovery_own_promotions (contact_id, promotion, version_id)
VALUES (?1, ?2, ?3)
"#,
)
.bind(contact_id)
.bind(promotion)
.bind(version as i64) // SQLite integers are usually i32/i64
.execute(&self.ws.database.pool)
.await
.map_err(TwonlyError::from)?;
Ok(())
}
async fn get_own_promotions_after_version(&self, version: u32) -> Result<Vec<Vec<u8>>> {
let promotions: Vec<Vec<u8>> = sqlx::query_scalar(
"SELECT promotion FROM user_discovery_own_promotions
WHERE version_id > ?
ORDER BY version_id ASC",
)
.bind(version as i64)
.fetch_all(&self.ws.database.pool)
.await
.map_err(TwonlyError::from)?;
Ok(promotions)
}
async fn store_other_promotion(&self, promotion: OtherPromotion) -> Result<()> {
sqlx::query(
r"
INSERT INTO user_discovery_other_promotions (
from_contact_id,
promotion_id,
public_id,
threshold,
announcement_share,
public_key_verified_timestamp
)
VALUES (?1, ?2, ?3, ?4, ?5, ?6)
",
)
.bind(promotion.from_contact_id)
.bind(promotion.promotion_id as i64)
.bind(promotion.public_id)
.bind(promotion.threshold as i64)
.bind(promotion.announcement_share)
.bind(promotion.public_key_verified_timestamp) // Option<i64> maps to NULLable
.execute(&self.ws.database.pool)
.await
.map_err(TwonlyError::from)?;
Ok(())
}
async fn get_other_promotions_by_public_id(
&self,
public_id: i64,
) -> Result<Vec<OtherPromotion>> {
let promotions = sqlx::query_as::<_, OtherPromotion>(
"SELECT * FROM user_discovery_other_promotions WHERE public_id = ?",
)
.bind(public_id)
.fetch_all(&self.ws.database.pool)
.await
.map_err(TwonlyError::from)?;
Ok(promotions)
}
async fn get_announced_user_by_public_id(
&self,
public_id: i64,
) -> Result<Option<AnnouncedUser>> {
let row = sqlx::query("SELECT * FROM user_discovery_announced_users WHERE public_id = ?")
.bind(public_id)
.fetch_optional(&self.ws.database.pool)
.await
.map_err(TwonlyError::from)?;
match row {
Some(r) => Ok(Some(AnnouncedUser {
user_id: r.get::<i64, _>("announced_user_id"),
public_key: r.get::<Vec<u8>, _>("announced_public_key"),
public_id: r.get::<i64, _>("public_id"),
})),
None => Ok(None),
}
}
async fn push_new_user_relation(
&self,
from_contact_id: UserID,
announced_user: AnnouncedUser,
public_key_verified_timestamp: Option<i64>,
) -> Result<()> {
let mut tx = self
.ws
.database
.pool
.begin()
.await
.map_err(TwonlyError::from)?;
sqlx::query(
r"
INSERT INTO user_discovery_announced_users (announced_user_id, announced_public_key, public_id)
VALUES (?1, ?2, ?3)
ON CONFLICT(announced_user_id) DO NOTHING
")
.bind(announced_user.user_id)
.bind(announced_user.public_key)
.bind(announced_user.public_id)
.execute(&mut *tx)
.await.map_err(TwonlyError::from)?;
if from_contact_id != announced_user.user_id {
tracing::debug!(
"INSERTING THAT {} KNOWS {}",
from_contact_id,
announced_user.user_id
);
sqlx::query(
r"INSERT INTO user_discovery_user_relations (
announced_user_id,
from_contact_id,
public_key_verified_timestamp
)
VALUES (?1, ?2, ?3)
ON CONFLICT(announced_user_id, from_contact_id) DO UPDATE SET
public_key_verified_timestamp = excluded.public_key_verified_timestamp
",
)
.bind(announced_user.user_id)
.bind(from_contact_id)
.bind(public_key_verified_timestamp)
.execute(&mut *tx)
.await
.map_err(TwonlyError::from)?;
}
tx.commit().await.map_err(TwonlyError::from)?;
Ok(())
}
async fn get_all_announced_users(
&self,
) -> Result<HashMap<AnnouncedUser, Vec<(UserID, Option<i64>)>>> {
let rows = sqlx::query(
r#"
SELECT
u.announced_user_id,
u.announced_public_key,
u.public_id,
r.from_contact_id,
r.public_key_verified_timestamp
FROM user_discovery_announced_users u
LEFT JOIN user_discovery_user_relations r
ON u.announced_user_id = r.announced_user_id
ORDER BY u.announced_user_id
"#,
)
.fetch_all(&self.ws.database.pool)
.await
.map_err(TwonlyError::from)?;
let mut map: HashMap<AnnouncedUser, Vec<(UserID, Option<i64>)>> = HashMap::new();
for row in rows {
let announced_user = AnnouncedUser {
user_id: row.get::<i64, _>("announced_user_id"),
public_key: row.get::<Vec<u8>, _>("announced_public_key"),
public_id: row.get::<i64, _>("public_id"),
};
let relations_list = map.entry(announced_user).or_insert_with(Vec::new);
// SQLX returns NULL for columns in a LEFT JOIN where no match is found.
if let Ok(Some(contact_id)) = row.try_get::<Option<i64>, _>("from_contact_id") {
let timestamp = row.get::<Option<i64>, _>("public_key_verified_timestamp");
relations_list.push((contact_id, timestamp));
}
}
Ok(map)
}
async fn get_contact_version(&self, contact_id: UserID) -> Result<Option<Vec<u8>>> {
let version: Option<Vec<u8>> =
sqlx::query_scalar("SELECT user_discovery_version FROM contacts WHERE user_id = ?")
.bind(contact_id)
.fetch_optional(&self.ws.database.pool)
.await
.map_err(TwonlyError::from)?;
Ok(version)
}
async fn set_contact_version(&self, contact_id: UserID, update: Vec<u8>) -> Result<()> {
sqlx::query("UPDATE contacts SET user_discovery_version = ? WHERE user_id = ?")
.bind(update)
.bind(contact_id)
.execute(&self.ws.database.pool)
.await
.map_err(TwonlyError::from)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use crate::user_discovery_store::UserDiscoveryDatabaseStore;
use protocols::user_discovery::tests::test_initialize_user_discovery;
#[tokio::test]
async fn test_initialize_user_discovery_database_store() {
let _ = pretty_env_logger::try_init();
test_initialize_user_discovery::<UserDiscoveryDatabaseStore>().await;
}
}

View file

@ -1,22 +1,16 @@
use parking_lot::{RwLock, RwLockReadGuard};
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::{RwLock, RwLockReadGuard};
#[derive(Default, Clone)] #[derive(Default, Clone)]
pub(crate) struct Shared<T>(Arc<RwLock<T>>); pub(crate) struct Shared<T>(Arc<RwLock<T>>);
impl<T> Shared<T> impl<T> Shared<T> {
where
T: Clone,
{
pub(crate) fn new(value: T) -> Self { pub(crate) fn new(value: T) -> Self {
Self(Arc::new(RwLock::new(value))) Self(Arc::new(RwLock::new(value)))
} }
pub(crate) fn get(&self) -> RwLockReadGuard<'_, T> { pub(crate) async fn get(&self) -> RwLockReadGuard<'_, T> {
self.0.read() self.0.read().await
}
pub(crate) fn cloned(&self) -> T {
self.0.read().clone()
}
pub(crate) fn set(&self, value: T) {
*self.0.write() = value;
} }
// pub(crate) async fn set(&self, value: T) {
// *self.0.write().await = value;
// }
} }

View file

@ -27,9 +27,6 @@ impl InMemoryStore {
} }
impl UserDiscoveryStore for InMemoryStore { impl UserDiscoveryStore for InMemoryStore {
async fn new() -> Self {
Self::default()
}
async fn get_config(&self) -> Result<String> { async fn get_config(&self) -> Result<String> {
if let Some(storage) = self.storage().config.clone() { if let Some(storage) = self.storage().config.clone() {
return Ok(storage); return Ok(storage);

View file

@ -12,8 +12,10 @@ fn get_version_bytes(announcement: u32, promotion: u32) -> Vec<u8> {
.encode_to_vec() .encode_to_vec()
} }
async fn get_ud<S: UserDiscoveryStore + Clone>(user_id: usize) -> UserDiscovery<S, TestingUtils> { async fn get_ud<S: UserDiscoveryStore + Clone + Default>(
let store = S::new().await; user_id: usize,
) -> UserDiscovery<S, TestingUtils> {
let store = S::default();
let ud = UserDiscovery::new(store.to_owned(), TestingUtils::default()).unwrap(); let ud = UserDiscovery::new(store.to_owned(), TestingUtils::default()).unwrap();
ud.initialize_or_update(2, user_id as UserID, vec![user_id as u8; 32]) ud.initialize_or_update(2, user_id as UserID, vec![user_id as u8; 32])
@ -97,7 +99,7 @@ struct TestUsers<S: UserDiscoveryStore> {
uds: Vec<UserDiscovery<S, TestingUtils>>, uds: Vec<UserDiscovery<S, TestingUtils>>,
} }
impl<S: UserDiscoveryStore + Clone> TestUsers<S> { impl<S: UserDiscoveryStore + Clone + Default> TestUsers<S> {
async fn get() -> Self { async fn get() -> Self {
let names = ["ALICE", "BOB", "CHARLIE", "DAVID", "FRANK"]; let names = ["ALICE", "BOB", "CHARLIE", "DAVID", "FRANK"];
let mut uds = vec![]; let mut uds = vec![];
@ -119,7 +121,7 @@ impl<S: UserDiscoveryStore + Clone> TestUsers<S> {
} }
} }
pub async fn test_initialize_user_discovery<S: UserDiscoveryStore + Clone>() { pub async fn test_initialize_user_discovery<S: UserDiscoveryStore + Clone + Default>() {
#[cfg(test)] #[cfg(test)]
let _ = pretty_env_logger::try_init(); let _ = pretty_env_logger::try_init();

View file

@ -22,7 +22,6 @@ pub struct AnnouncedUser {
} }
pub trait UserDiscoveryStore { pub trait UserDiscoveryStore {
fn new() -> impl std::future::Future<Output = Self> + Send;
fn get_config(&self) -> impl Future<Output = Result<String>> + Send; fn get_config(&self) -> impl Future<Output = Result<String>> + Send;
fn update_config(&self, update: String) -> impl Future<Output = Result<()>> + Send; fn update_config(&self, update: String) -> impl Future<Output = Result<()>> + Send;
fn set_shares(&self, shares: Vec<Vec<u8>>) -> impl Future<Output = Result<()>> + Send; fn set_shares(&self, shares: Vec<Vec<u8>>) -> impl Future<Output = Result<()>> + Send;
@ -88,6 +87,9 @@ pub trait UserDiscoveryUtils {
pubkey: &[u8], pubkey: &[u8],
signature: &[u8], signature: &[u8],
) -> impl Future<Output = Result<bool>> + Send; ) -> impl Future<Output = Result<bool>> + Send;
/// In case the the user does not exists yet return false.
/// If this happens this should trigger an error, as this functions is only when a message was received from this user...
/// This is used to verify that the share of the promotions contains the same public key and the user is not secretly announcing a different one
fn verify_stored_pubkey( fn verify_stored_pubkey(
&self, &self,
from_contact_id: UserID, from_contact_id: UserID,