diff --git a/lib/core/bridge/callbacks.dart b/lib/core/bridge/callbacks.dart index b0948ced..c7bdf17e 100644 --- a/lib/core/bridge/callbacks.dart +++ b/lib/core/bridge/callbacks.dart @@ -36,6 +36,8 @@ Future initFlutterCallbacks({ userDiscoverySetContactVersion, required FutureOr Function(PlatformInt64, AnnouncedUser, PlatformInt64?) userDiscoveryPushNewUserRelation, + required FutureOr Function(PlatformInt64) + userDiscoveryGetContactPromotion, }) => RustLib.instance.api.crateBridgeCallbacksInitFlutterCallbacks( loggingGetStreamSink: loggingGetStreamSink, userDiscoverySignData: userDiscoverySignData, @@ -55,4 +57,5 @@ Future initFlutterCallbacks({ userDiscoveryGetContactVersion: userDiscoveryGetContactVersion, userDiscoverySetContactVersion: userDiscoverySetContactVersion, userDiscoveryPushNewUserRelation: userDiscoveryPushNewUserRelation, + userDiscoveryGetContactPromotion: userDiscoveryGetContactPromotion, ); diff --git a/lib/core/bridge/wrapper/user_discovery.dart b/lib/core/bridge/wrapper/user_discovery.dart index 628492ce..db1a53e4 100644 --- a/lib/core/bridge/wrapper/user_discovery.dart +++ b/lib/core/bridge/wrapper/user_discovery.dart @@ -23,10 +23,12 @@ class FlutterUserDiscovery { static Future handleNewMessages({ required PlatformInt64 contactId, + PlatformInt64? publicKeyVerifiedTimestamp, required List messages, }) => RustLib.instance.api .crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryHandleNewMessages( contactId: contactId, + publicKeyVerifiedTimestamp: publicKeyVerifiedTimestamp, messages: messages, ); @@ -50,6 +52,15 @@ class FlutterUserDiscovery { version: version, ); + static Future updateVerificationStateForUser({ + required PlatformInt64 contactId, + PlatformInt64? publicKeyVerifiedTimestamp, + }) => RustLib.instance.api + .crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryUpdateVerificationStateForUser( + contactId: contactId, + publicKeyVerifiedTimestamp: publicKeyVerifiedTimestamp, + ); + @override int get hashCode => 0; diff --git a/lib/core/frb_generated.dart b/lib/core/frb_generated.dart index deec8bfb..83f38520 100644 --- a/lib/core/frb_generated.dart +++ b/lib/core/frb_generated.dart @@ -70,7 +70,7 @@ class RustLib extends BaseEntrypoint { String get codegenVersion => '2.12.0'; @override - int get rustContentHash => -630534473; + int get rustContentHash => 1680338106; static const kDefaultExternalLibraryLoaderConfig = ExternalLibraryLoaderConfig( @@ -94,6 +94,7 @@ abstract class RustLibApi extends BaseApi { Future crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryHandleNewMessages({ required PlatformInt64 contactId, + PlatformInt64? publicKeyVerifiedTimestamp, required List messages, }); @@ -110,6 +111,12 @@ abstract class RustLibApi extends BaseApi { required List version, }); + Future + crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryUpdateVerificationStateForUser({ + required PlatformInt64 contactId, + PlatformInt64? publicKeyVerifiedTimestamp, + }); + Future crateBridgeCallbacksInitFlutterCallbacks({ required FutureOr> Function() loggingGetStreamSink, required FutureOr Function(Uint8List) userDiscoverySignData, @@ -140,6 +147,8 @@ abstract class RustLibApi extends BaseApi { PlatformInt64?, ) userDiscoveryPushNewUserRelation, + required FutureOr Function(PlatformInt64) + userDiscoveryGetContactPromotion, }); Future crateBridgeInitializeTwonlyFlutter({ @@ -230,6 +239,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { Future crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryHandleNewMessages({ required PlatformInt64 contactId, + PlatformInt64? publicKeyVerifiedTimestamp, required List messages, }) { return handler.executeNormal( @@ -237,6 +247,10 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { callFfi: (port_) { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_i_64(contactId, serializer); + sse_encode_opt_box_autoadd_i_64( + publicKeyVerifiedTimestamp, + serializer, + ); sse_encode_list_list_prim_u_8_strict(messages, serializer); pdeCallFfi( generalizedFrbRustBinding, @@ -251,7 +265,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ), constMeta: kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryHandleNewMessagesConstMeta, - argValues: [contactId, messages], + argValues: [contactId, publicKeyVerifiedTimestamp, messages], apiImpl: this, ), ); @@ -261,7 +275,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { get kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryHandleNewMessagesConstMeta => const TaskConstMeta( debugName: "flutter_user_discovery_handle_new_messages", - argNames: ["contactId", "messages"], + argNames: ["contactId", "publicKeyVerifiedTimestamp", "messages"], ); @override @@ -342,6 +356,47 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { argNames: ["contactId", "version"], ); + @override + Future + crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryUpdateVerificationStateForUser({ + required PlatformInt64 contactId, + PlatformInt64? publicKeyVerifiedTimestamp, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_i_64(contactId, serializer); + sse_encode_opt_box_autoadd_i_64( + publicKeyVerifiedTimestamp, + serializer, + ); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 6, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: + kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryUpdateVerificationStateForUserConstMeta, + argValues: [contactId, publicKeyVerifiedTimestamp], + apiImpl: this, + ), + ); + } + + TaskConstMeta + get kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryUpdateVerificationStateForUserConstMeta => + const TaskConstMeta( + debugName: "flutter_user_discovery_update_verification_state_for_user", + argNames: ["contactId", "publicKeyVerifiedTimestamp"], + ); + @override Future crateBridgeCallbacksInitFlutterCallbacks({ required FutureOr> Function() loggingGetStreamSink, @@ -373,6 +428,8 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { PlatformInt64?, ) userDiscoveryPushNewUserRelation, + required FutureOr Function(PlatformInt64) + userDiscoveryGetContactPromotion, }) { return handler.executeNormal( NormalTask( @@ -434,10 +491,14 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { userDiscoveryPushNewUserRelation, serializer, ); + sse_encode_DartFn_Inputs_i_64_Output_opt_list_prim_u_8_strict_AnyhowException( + userDiscoveryGetContactPromotion, + serializer, + ); pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 6, + funcId: 7, port: port_, ); }, @@ -461,6 +522,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { userDiscoveryGetContactVersion, userDiscoverySetContactVersion, userDiscoveryPushNewUserRelation, + userDiscoveryGetContactPromotion, ], apiImpl: this, ), @@ -485,6 +547,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { "userDiscoveryGetContactVersion", "userDiscoverySetContactVersion", "userDiscoveryPushNewUserRelation", + "userDiscoveryGetContactPromotion", ], ); @@ -500,7 +563,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 7, + funcId: 8, port: port_, ); }, diff --git a/lib/src/callbacks/callbacks.dart b/lib/src/callbacks/callbacks.dart index 9e62fcd0..07862ace 100644 --- a/lib/src/callbacks/callbacks.dart +++ b/lib/src/callbacks/callbacks.dart @@ -25,5 +25,7 @@ Future initFlutterCallbacksForRust() async { userDiscoverySignData: UserDiscoveryCallbacks.signData, userDiscoveryVerifySignature: UserDiscoveryCallbacks.verifySignature, userDiscoveryVerifyStoredPubkey: UserDiscoveryCallbacks.verifyStoredPubKey, + userDiscoveryGetContactPromotion: + UserDiscoveryCallbacks.getContactPromotion, ); } diff --git a/lib/src/callbacks/user_discovery.callbacks.dart b/lib/src/callbacks/user_discovery.callbacks.dart index fe435104..8542616b 100644 --- a/lib/src/callbacks/user_discovery.callbacks.dart +++ b/lib/src/callbacks/user_discovery.callbacks.dart @@ -298,4 +298,16 @@ class UserDiscoveryCallbacks { return false; } } + + static Future getContactPromotion(int contactId) async { + try { + final row = await (twonlyDB.select( + twonlyDB.userDiscoveryOwnPromotions, + )..where((tbl) => tbl.contactId.equals(contactId))).getSingleOrNull(); + return row?.promotion; + } catch (e) { + Log.error(e); + return null; + } + } } diff --git a/lib/src/database/daos/key_verification.dao.dart b/lib/src/database/daos/key_verification.dao.dart index b71ac5dc..f4693ff4 100644 --- a/lib/src/database/daos/key_verification.dao.dart +++ b/lib/src/database/daos/key_verification.dao.dart @@ -1,12 +1,24 @@ +import 'package:clock/clock.dart'; import 'package:drift/drift.dart'; +import 'package:twonly/core/bridge/wrapper/user_discovery.dart'; +import 'package:twonly/locator.dart'; import 'package:twonly/src/database/tables/contacts.table.dart'; import 'package:twonly/src/database/tables/groups.table.dart'; +import 'package:twonly/src/database/tables/user_discovery.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; part 'key_verification.dao.g.dart'; +enum VerificationStatus { trusted, partialTrusted, notTrusted } + @DriftAccessor( - tables: [Contacts, VerificationTokens, KeyVerifications, GroupMembers], + tables: [ + Contacts, + VerificationTokens, + KeyVerifications, + GroupMembers, + UserDiscoveryUserRelations, + ], ) class KeyVerificationDao extends DatabaseAccessor with _$KeyVerificationDaoMixin { @@ -56,18 +68,89 @@ class KeyVerificationDao extends DatabaseAccessor )..where((kv) => kv.contactId.equals(contactId))).watch(); } - Stream watchAllGroupMembersVerified(String groupId) { - final gm = groupMembers; + Future> getContactVerification(int contactId) async { + return (select( + keyVerifications, + )..where((kv) => kv.contactId.equals(contactId))).get(); + } + + Stream> watchTransferredTrustVerifications( + int contactId, + ) { final kv = keyVerifications; + final ur = userDiscoveryUserRelations; - final query = (select(gm)..where((m) => m.groupId.equals(groupId))).join([ - leftOuterJoin(kv, kv.contactId.equalsExp(gm.contactId)), - ]); + final query = + (select(contacts)..where((u) => u.userId.equals(contactId).not())).join( + [ + innerJoin( + ur, + ur.fromContactId.equalsExp(contacts.userId), + ), + innerJoin(kv, kv.contactId.equalsExp(ur.fromContactId)), + ], + )..where( + ur.announcedUserId.equals(contactId) & + ur.publicKeyVerifiedTimestamp.isNotNull(), + ); - return query.watch().map( - (rows) => - rows.isNotEmpty && rows.every((r) => r.readTableOrNull(kv) != null), - ); + return query.watch().map((rows) { + return rows.map((row) { + final contact = row.readTable(contacts); + final timestamp = row.readTable(ur).publicKeyVerifiedTimestamp!; + return (contact, timestamp); + }).toList(); + }); + } + + Stream watchAllGroupMembersVerified(String groupId) { + final gm = groupMembers; + final directKv = alias(keyVerifications, 'directKv'); + final ur = userDiscoveryUserRelations; + final verifierKv = alias(keyVerifications, 'verifierKv'); + + final query = select(gm).join([ + leftOuterJoin(directKv, directKv.contactId.equalsExp(gm.contactId)), + leftOuterJoin( + ur, + ur.announcedUserId.equalsExp(gm.contactId) & + ur.publicKeyVerifiedTimestamp.isNotNull() & + ur.fromContactId.equalsExp(gm.contactId).not(), + ), + leftOuterJoin( + verifierKv, + verifierKv.contactId.equalsExp(ur.fromContactId), + ), + ])..where(gm.groupId.equals(groupId)); + + return query.watch().map((rows) { + if (rows.isEmpty) return VerificationStatus.notTrusted; + + final memberTrustMap = {}; + + for (final row in rows) { + final contactId = row.readTable(gm).contactId; + final isDirect = row.readTableOrNull(directKv) != null; + final isPartial = row.readTableOrNull(verifierKv) != null; + + final current = + memberTrustMap[contactId] ?? (direct: false, partial: false); + memberTrustMap[contactId] = ( + direct: current.direct || isDirect, + partial: current.partial || isPartial, + ); + } + + final allDirect = memberTrustMap.values.every((m) => m.direct); + if (allDirect) return VerificationStatus.trusted; + + final allAtLeastPartial = memberTrustMap.values.every( + (m) => m.direct || m.partial, + ); + if (allAtLeastPartial) return VerificationStatus.partialTrusted; + + return VerificationStatus.notTrusted; + }); } Future addKeyVerification(int contactId, VerificationType type) async { @@ -77,5 +160,11 @@ class KeyVerificationDao extends DatabaseAccessor type: Value(type), ), ); + if (userService.currentUser.isUserDiscoveryEnabled) { + await FlutterUserDiscovery.updateVerificationStateForUser( + contactId: contactId, + publicKeyVerifiedTimestamp: clock.now().millisecondsSinceEpoch, + ); + } } } diff --git a/lib/src/database/daos/key_verification.dao.g.dart b/lib/src/database/daos/key_verification.dao.g.dart index 6d69eb21..816c44c3 100644 --- a/lib/src/database/daos/key_verification.dao.g.dart +++ b/lib/src/database/daos/key_verification.dao.g.dart @@ -11,6 +11,10 @@ mixin _$KeyVerificationDaoMixin on DatabaseAccessor { attachedDatabase.keyVerifications; $GroupsTable get groups => attachedDatabase.groups; $GroupMembersTable get groupMembers => attachedDatabase.groupMembers; + $UserDiscoveryAnnouncedUsersTable get userDiscoveryAnnouncedUsers => + attachedDatabase.userDiscoveryAnnouncedUsers; + $UserDiscoveryUserRelationsTable get userDiscoveryUserRelations => + attachedDatabase.userDiscoveryUserRelations; KeyVerificationDaoManager get managers => KeyVerificationDaoManager(this); } @@ -33,4 +37,16 @@ class KeyVerificationDaoManager { $$GroupsTableTableManager(_db.attachedDatabase, _db.groups); $$GroupMembersTableTableManager get groupMembers => $$GroupMembersTableTableManager(_db.attachedDatabase, _db.groupMembers); + $$UserDiscoveryAnnouncedUsersTableTableManager + get userDiscoveryAnnouncedUsers => + $$UserDiscoveryAnnouncedUsersTableTableManager( + _db.attachedDatabase, + _db.userDiscoveryAnnouncedUsers, + ); + $$UserDiscoveryUserRelationsTableTableManager + get userDiscoveryUserRelations => + $$UserDiscoveryUserRelationsTableTableManager( + _db.attachedDatabase, + _db.userDiscoveryUserRelations, + ); } diff --git a/lib/src/database/daos/user_discovery.dao.dart b/lib/src/database/daos/user_discovery.dao.dart index 2d21c2bd..d2ab5b26 100644 --- a/lib/src/database/daos/user_discovery.dao.dart +++ b/lib/src/database/daos/user_discovery.dao.dart @@ -2,6 +2,7 @@ import 'package:drift/drift.dart'; import 'package:twonly/src/database/tables/contacts.table.dart'; import 'package:twonly/src/database/tables/user_discovery.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/utils/log.dart'; part 'user_discovery.dao.g.dart'; @@ -13,6 +14,7 @@ typedef AnnouncedUsersWithRelations = UserDiscoveryAnnouncedUsers, UserDiscoveryUserRelations, UserDiscoveryOwnPromotions, + UserDiscoveryOtherPromotions, UserDiscoveryShares, Contacts, ], @@ -127,6 +129,8 @@ class UserDiscoveryDao extends DatabaseAccessor results[user]!.add(relationData); } + Log.info('results = ${results.length}'); + return results; }); } @@ -223,4 +227,19 @@ class UserDiscoveryDao extends DatabaseAccessor updatedValues, ); } + + Stream> watchAllAnnouncedUsers() => + select(userDiscoveryAnnouncedUsers).watch(); + + Stream> watchAllUserRelations() => + select(userDiscoveryUserRelations).watch(); + + Stream> watchAllOwnPromotions() => + select(userDiscoveryOwnPromotions).watch(); + + Stream> watchAllOtherPromotions() => + select(userDiscoveryOtherPromotions).watch(); + + Stream> watchAllShares() => + select(userDiscoveryShares).watch(); } diff --git a/lib/src/database/daos/user_discovery.dao.g.dart b/lib/src/database/daos/user_discovery.dao.g.dart index f2103528..ef38cf62 100644 --- a/lib/src/database/daos/user_discovery.dao.g.dart +++ b/lib/src/database/daos/user_discovery.dao.g.dart @@ -11,6 +11,8 @@ mixin _$UserDiscoveryDaoMixin on DatabaseAccessor { attachedDatabase.userDiscoveryUserRelations; $UserDiscoveryOwnPromotionsTable get userDiscoveryOwnPromotions => attachedDatabase.userDiscoveryOwnPromotions; + $UserDiscoveryOtherPromotionsTable get userDiscoveryOtherPromotions => + attachedDatabase.userDiscoveryOtherPromotions; $UserDiscoverySharesTable get userDiscoveryShares => attachedDatabase.userDiscoveryShares; UserDiscoveryDaoManager get managers => UserDiscoveryDaoManager(this); @@ -39,6 +41,12 @@ class UserDiscoveryDaoManager { _db.attachedDatabase, _db.userDiscoveryOwnPromotions, ); + $$UserDiscoveryOtherPromotionsTableTableManager + get userDiscoveryOtherPromotions => + $$UserDiscoveryOtherPromotionsTableTableManager( + _db.attachedDatabase, + _db.userDiscoveryOtherPromotions, + ); $$UserDiscoverySharesTableTableManager get userDiscoveryShares => $$UserDiscoverySharesTableTableManager( _db.attachedDatabase, diff --git a/lib/src/services/user_discovery.service.dart b/lib/src/services/user_discovery.service.dart index 2ae8cb87..dc9b8914 100644 --- a/lib/src/services/user_discovery.service.dart +++ b/lib/src/services/user_discovery.service.dart @@ -131,9 +131,14 @@ class UserDiscoveryService { List messages, ) async { try { + final verifications = await twonlyDB.keyVerificationDao + .getContactVerification(fromUserId); + return await FlutterUserDiscovery.handleNewMessages( contactId: fromUserId, messages: messages, + publicKeyVerifiedTimestamp: + verifications.lastOrNull?.createdAt.millisecondsSinceEpoch, ); } catch (e) { Log.error(e); diff --git a/lib/src/visual/components/verification_badge.comp.dart b/lib/src/visual/components/verification_badge.comp.dart index 6f8f116d..2971b2bf 100644 --- a/lib/src/visual/components/verification_badge.comp.dart +++ b/lib/src/visual/components/verification_badge.comp.dart @@ -4,8 +4,11 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/constants/routes.keys.dart'; +import 'package:twonly/src/database/daos/key_verification.dao.dart'; import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/visual/elements/svg_icon.element.dart'; +import 'package:twonly/src/visual/views/settings/help/faq/verification_bade_faq.view.dart'; class VerificationBadgeComp extends StatefulWidget { const VerificationBadgeComp({ @@ -29,9 +32,11 @@ class VerificationBadgeComp extends StatefulWidget { class _VerificationBadgeCompState extends State { bool _isVerified = false; + bool _isVerifiedByTransferredTrust = false; - StreamSubscription? _streamAllVerified; + StreamSubscription? _streamAllVerified; StreamSubscription>? _streamContactVerification; + StreamSubscription>? _streamTransferredTrust; @override void initState() { @@ -42,7 +47,14 @@ class _VerificationBadgeCompState extends State { .listen((update) { if (!mounted) return; setState(() { - _isVerified = update; + _isVerified = false; + _isVerifiedByTransferredTrust = false; + if (update == VerificationStatus.trusted) { + _isVerified = true; + } + if (update == VerificationStatus.partialTrusted) { + _isVerifiedByTransferredTrust = true; + } }); }); } else if (widget.contact != null) { @@ -51,9 +63,19 @@ class _VerificationBadgeCompState extends State { .listen((update) { if (!mounted) return; setState(() { + Log.info('Update: ${update.length}'); _isVerified = update.isNotEmpty; }); }); + + _streamTransferredTrust = twonlyDB.keyVerificationDao + .watchTransferredTrustVerifications(widget.contact!.userId) + .listen((update) { + if (!mounted) return; + setState(() { + _isVerifiedByTransferredTrust = update.isNotEmpty; + }); + }); } } @@ -61,6 +83,7 @@ class _VerificationBadgeCompState extends State { void dispose() { _streamAllVerified?.cancel(); _streamContactVerification?.cancel(); + _streamTransferredTrust?.cancel(); super.dispose(); } @@ -81,9 +104,12 @@ class _VerificationBadgeCompState extends State { bottom: 3, ), child: SvgIcon( - assetPath: _isVerified + assetPath: (_isVerified || _isVerifiedByTransferredTrust) ? SvgIcons.verifiedGreen : SvgIcons.verifiedRed, + color: (_isVerifiedByTransferredTrust && !_isVerified) + ? colorVerificationBadgeYellow + : null, size: widget.size, ), ), diff --git a/lib/src/visual/views/chats/chat_messages_components/entries/chat_contacts.entry.dart b/lib/src/visual/views/chats/chat_messages_components/entries/chat_contacts.entry.dart index d605a3d6..e8c69569 100644 --- a/lib/src/visual/views/chats/chat_messages_components/entries/chat_contacts.entry.dart +++ b/lib/src/visual/views/chats/chat_messages_components/entries/chat_contacts.entry.dart @@ -122,6 +122,16 @@ class _ContactRowState extends State<_ContactRow> { ); if (userdata == null) return; + final added = await twonlyDB.contactsDao.insertOnConflictUpdate( + ContactsCompanion( + username: Value(utf8.decode(userdata.username)), + userId: Value(userdata.userId.toInt()), + requested: const Value(false), + blocked: const Value(false), + deletedByUser: const Value(false), + ), + ); + if (userdata.publicIdentityKey.equals(widget.contact.publicIdentityKey)) { final verified = await twonlyDB.keyVerificationDao.isContactVerified( widget.message.senderId!, @@ -136,16 +146,6 @@ class _ContactRowState extends State<_ContactRow> { } } - final added = await twonlyDB.contactsDao.insertOnConflictUpdate( - ContactsCompanion( - username: Value(utf8.decode(userdata.username)), - userId: Value(userdata.userId.toInt()), - requested: const Value(false), - blocked: const Value(false), - deletedByUser: const Value(false), - ), - ); - if (added > 0) await importSignalContactAndCreateRequest(userdata); } catch (e) { Log.error(e); diff --git a/lib/src/visual/views/contact/contact.view.dart b/lib/src/visual/views/contact/contact.view.dart index 5d2d8500..b800ee05 100644 --- a/lib/src/visual/views/contact/contact.view.dart +++ b/lib/src/visual/views/contact/contact.view.dart @@ -10,7 +10,6 @@ import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/tables/contacts.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; -import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/components/alert.dialog.dart'; import 'package:twonly/src/visual/components/avatar_icon.comp.dart'; @@ -34,15 +33,17 @@ class _ContactViewState extends State { Contact? _contact; List _memberOfGroups = []; List _keyVerifications = []; + List<(Contact, DateTime)> _transferredTrust = []; - late StreamSubscription _contactSub; - late StreamSubscription> _groupMemberSub; + late StreamSubscription _streamContact; + late StreamSubscription> _streamMemberOfGroups; late StreamSubscription> _streamKeyVerifications; + late StreamSubscription> _streamTransferredTrust; @override void initState() { super.initState(); - _contactSub = twonlyDB.contactsDao.watchContact(widget.userId).listen(( + _streamContact = twonlyDB.contactsDao.watchContact(widget.userId).listen(( update, ) { if (update != null) { @@ -51,26 +52,38 @@ class _ContactViewState extends State { }); } }); - _groupMemberSub = twonlyDB.groupsDao + _streamMemberOfGroups = twonlyDB.groupsDao .watchContactGroupMember(widget.userId) .listen((groups) async { - _memberOfGroups = groups; + if (!mounted) return; + setState(() { + _memberOfGroups = groups; + }); }); _streamKeyVerifications = twonlyDB.keyVerificationDao .watchContactVerification(widget.userId) .listen((update) { + if (!mounted) return; setState(() { - Log.info('Verifications: ${update.length}'); _keyVerifications = update; }); }); + _streamTransferredTrust = twonlyDB.keyVerificationDao + .watchTransferredTrustVerifications(widget.userId) + .listen((update) { + if (!mounted) return; + setState(() { + _transferredTrust = update; + }); + }); } @override void dispose() { - _contactSub.cancel(); - _groupMemberSub.cancel(); + _streamContact.cancel(); + _streamMemberOfGroups.cancel(); _streamKeyVerifications.cancel(); + _streamTransferredTrust.cancel(); super.dispose(); } @@ -175,7 +188,6 @@ class _ContactViewState extends State { Padding( padding: const EdgeInsets.only(right: 10), child: VerificationBadgeComp( - key: GlobalKey(), contact: contact, ), ), @@ -230,7 +242,7 @@ class _ContactViewState extends State { RestoreFlameComp( contactId: widget.userId, ), - if (_keyVerifications.isEmpty) + if (_keyVerifications.isEmpty && _transferredTrust.isEmpty) BetterListTile( leading: VerificationBadgeComp( contact: contact, @@ -242,7 +254,7 @@ class _ContactViewState extends State { setState(() {}); }, ), - if (_keyVerifications.isNotEmpty) + if (_keyVerifications.isNotEmpty || _transferredTrust.isNotEmpty) ExpansionTile( shape: const RoundedRectangleBorder(), backgroundColor: context.color.surfaceContainer, @@ -255,23 +267,40 @@ class _ContactViewState extends State { ), ), title: Text(context.lang.userVerifiedTitle), - children: _keyVerifications - .map( - (kv) => ListTile( - dense: true, - title: Text(_verificationTypeLabel(context, kv.type)), - trailing: Text( - DateFormat.yMd( - Localizations.localeOf(context).toString(), - ).format(kv.createdAt), - style: TextStyle( - color: context.color.onSurfaceVariant, - fontSize: 13, - ), + children: [ + ..._keyVerifications.map( + (kv) => ListTile( + dense: true, + title: Text(_verificationTypeLabel(context, kv.type)), + trailing: Text( + DateFormat.yMd( + Localizations.localeOf(context).toString(), + ).format(kv.createdAt), + style: TextStyle( + color: context.color.onSurfaceVariant, + fontSize: 13, ), ), - ) - .toList(), + ), + ), + ..._transferredTrust.map( + (tt) => ListTile( + dense: true, + title: Text( + 'Verifiziert von ${getContactDisplayName(tt.$1)}', + ), + trailing: Text( + DateFormat.yMd( + Localizations.localeOf(context).toString(), + ).format(tt.$2), + style: TextStyle( + color: context.color.onSurfaceVariant, + fontSize: 13, + ), + ), + ), + ), + ], ), if (userService.currentUser.isUserDiscoveryEnabled) BetterListTile( diff --git a/lib/src/visual/views/settings/developer/developer.view.dart b/lib/src/visual/views/settings/developer/developer.view.dart index f8244bf9..7b5807e4 100644 --- a/lib/src/visual/views/settings/developer/developer.view.dart +++ b/lib/src/visual/views/settings/developer/developer.view.dart @@ -11,8 +11,10 @@ import 'package:twonly/locator.dart'; import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/services/user.service.dart'; +import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/visual/components/alert.dialog.dart'; +import 'package:twonly/src/visual/views/settings/developer/user_discovery_developer.view.dart'; class DeveloperSettingsView extends StatefulWidget { const DeveloperSettingsView({super.key}); @@ -56,12 +58,21 @@ class _DeveloperSettingsViewState extends State { onChanged: (_) => toggleDeveloperSettings(), ), ), + ListTile( + title: const Text('User ID'), + subtitle: Text(userService.currentUser.userId.toString()), + ), ListTile( title: const Text('Show Retransmission Database'), onTap: () => context.push( Routes.settingsDeveloperRetransmissionDatabase, ), ), + ListTile( + title: const Text('Show User Discovery Database'), + onTap: () => + context.navPush(const UserDiscoveryDeveloperView()), + ), ListTile( title: const Text('Toggle Video Stabilization'), onTap: toggleVideoStabilization, diff --git a/lib/src/visual/views/settings/developer/user_discovery_developer.view.dart b/lib/src/visual/views/settings/developer/user_discovery_developer.view.dart new file mode 100644 index 00000000..0b3af71b --- /dev/null +++ b/lib/src/visual/views/settings/developer/user_discovery_developer.view.dart @@ -0,0 +1,135 @@ +import 'package:flutter/material.dart'; +import 'package:twonly/locator.dart'; +import 'package:twonly/src/database/daos/user_discovery.dao.dart'; +import 'package:twonly/src/database/twonly.db.dart'; + +class UserDiscoveryDeveloperView extends StatefulWidget { + const UserDiscoveryDeveloperView({super.key}); + + @override + State createState() => + _UserDiscoveryDeveloperViewState(); +} + +class _UserDiscoveryDeveloperViewState + extends State { + UserDiscoveryDao get dao => twonlyDB.userDiscoveryDao; + + @override + Widget build(BuildContext context) { + return StreamBuilder>( + stream: twonlyDB.contactsDao.watchAllContacts(), + builder: (context, contactSnapshot) { + final contacts = contactSnapshot.data ?? []; + final contactNames = {for (final c in contacts) c.userId: c.username}; + + String getName(int? id) { + if (id == null) return 'None'; + return contactNames[id] ?? 'ID: $id'; + } + + return Scaffold( + appBar: AppBar( + title: const Text('User Discovery Debug'), + ), + body: ListView( + children: [ + _TableExpansionTile( + title: 'Announced Users', + stream: dao.watchAllAnnouncedUsers(), + itemBuilder: (context, user) => ListTile( + title: Text(user.username ?? 'No Username'), + subtitle: Text( + 'ID: ${getName(user.announcedUserId)}\n' + 'Public ID: ${user.publicId}\n' + 'Shown: ${user.wasShownToTheUser}, Hidden: ${user.isHidden}', + ), + isThreeLine: true, + ), + ), + _TableExpansionTile( + title: 'User Relations', + stream: dao.watchAllUserRelations(), + itemBuilder: (context, relation) => ListTile( + title: Text( + 'From: ${getName(relation.fromContactId)} → To: ${getName(relation.announcedUserId)}', + ), + subtitle: Text( + 'Verified: ${relation.publicKeyVerifiedTimestamp}', + ), + ), + ), + _TableExpansionTile( + title: 'Other Promotions', + stream: dao.watchAllOtherPromotions(), + itemBuilder: (context, promotion) => ListTile( + title: Text( + 'From: ${getName(promotion.fromContactId)}, Public ID: ${promotion.publicId}', + ), + subtitle: Text( + 'ID: ${promotion.promotionId}, Threshold: ${promotion.threshold}\n' + 'Verified: ${promotion.publicKeyVerifiedTimestamp}', + ), + isThreeLine: true, + ), + ), + _TableExpansionTile( + title: 'Own Promotions', + stream: dao.watchAllOwnPromotions(), + itemBuilder: (context, promotion) => ListTile( + title: Text('Contact: ${getName(promotion.contactId)}'), + subtitle: Text('Version: ${promotion.versionId}'), + ), + ), + _TableExpansionTile( + title: 'Shares', + stream: dao.watchAllShares(), + itemBuilder: (context, share) => ListTile( + title: Text('Share ID: ${share.shareId}'), + subtitle: Text('Contact: ${getName(share.contactId)}'), + ), + ), + ], + ), + ); + }, + ); + } +} + +class _TableExpansionTile extends StatelessWidget { + const _TableExpansionTile({ + required this.title, + required this.stream, + required this.itemBuilder, + }); + final String title; + final Stream> stream; + final Widget Function(BuildContext, T) itemBuilder; + + @override + Widget build(BuildContext context) { + return StreamBuilder>( + stream: stream, + builder: (context, snapshot) { + final data = snapshot.data ?? []; + return ExpansionTile( + title: Text( + '$title (${data.length})', + style: const TextStyle(fontWeight: FontWeight.bold), + ), + children: data.isNotEmpty + ? data.map((item) => itemBuilder(context, item)).toList() + : [ + const ListTile( + title: Text( + 'No entries', + style: TextStyle(color: Colors.grey), + ), + ), + ], + ); + }, + ); + } +} diff --git a/rust/src/bridge/callbacks.rs b/rust/src/bridge/callbacks.rs index bdfa9aab..a2c81111 100644 --- a/rust/src/bridge/callbacks.rs +++ b/rust/src/bridge/callbacks.rs @@ -36,6 +36,7 @@ callback_generator! { get_contact_version: (i64) => Option>, set_contact_version: (i64, Vec) => bool, push_new_user_relation: (i64, AnnouncedUser, Option) => bool, + get_contact_promotion: (i64) => Option> } } } diff --git a/rust/src/bridge/callbacks/user_discovery.rs b/rust/src/bridge/callbacks/user_discovery.rs index dfa7cc0d..68fd03cf 100644 --- a/rust/src/bridge/callbacks/user_discovery.rs +++ b/rust/src/bridge/callbacks/user_discovery.rs @@ -85,10 +85,12 @@ impl UserDiscoveryStore for UserDiscoveryStoreFlutter { version: u32, promotion: Vec, ) -> Result<()> { - (get_callbacks()?.user_discovery.push_own_promotion_and_clear_old_version)(contact_id, version as i64, promotion) - .await - .then_some(()) - .ok_or(TwonlyError::DartError.into()) + (get_callbacks()? + .user_discovery + .push_own_promotion_and_clear_old_version)(contact_id, version as i64, promotion) + .await + .then_some(()) + .ok_or(TwonlyError::DartError.into()) } async fn get_own_promotions_after_version(&self, version: u32) -> Result>> { @@ -166,4 +168,8 @@ impl UserDiscoveryStore for UserDiscoveryStoreFlutter { .then_some(()) .ok_or(TwonlyError::DartError.into()) } + + async fn get_contact_promotion(&self, contact_id: i64) -> Result>> { + Ok((get_callbacks()?.user_discovery.get_contact_promotion)(contact_id).await) + } } diff --git a/rust/src/bridge/wrapper/user_discovery.rs b/rust/src/bridge/wrapper/user_discovery.rs index 6c4f35f3..4ce07990 100644 --- a/rust/src/bridge/wrapper/user_discovery.rs +++ b/rust/src/bridge/wrapper/user_discovery.rs @@ -50,12 +50,28 @@ impl FlutterUserDiscovery { .await?) } - pub async fn handle_new_messages(contact_id: i64, messages: Vec>) -> Result<()> { + pub async fn handle_new_messages( + contact_id: i64, + public_key_verified_timestamp: Option, + messages: Vec>, + ) -> Result<()> { Ok(get_twonly_flutter()? .user_discovery .get() .await - .handle_new_messages(contact_id, messages) + .handle_new_messages(contact_id, public_key_verified_timestamp, messages) + .await?) + } + + pub async fn update_verification_state_for_user( + contact_id: i64, + public_key_verified_timestamp: Option, + ) -> Result<()> { + Ok(get_twonly_flutter()? + .user_discovery + .get() + .await + .update_verification_state_for_user(contact_id, public_key_verified_timestamp) .await?) } } diff --git a/rust/src/frb_generated.rs b/rust/src/frb_generated.rs index 96963de4..104bd87c 100644 --- a/rust/src/frb_generated.rs +++ b/rust/src/frb_generated.rs @@ -38,7 +38,7 @@ flutter_rust_bridge::frb_generated_boilerplate!( default_rust_auto_opaque = RustAutoOpaqueMoi, ); pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.12.0"; -pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -630534473; +pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 1680338106; // Section: executor @@ -87,9 +87,10 @@ fn wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_handle_n let message = unsafe { flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(ptr_, rust_vec_len_, data_len_) }; let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); let api_contact_id = ::sse_decode(&mut deserializer); +let api_public_key_verified_timestamp = >::sse_decode(&mut deserializer); let api_messages = >>::sse_decode(&mut deserializer);deserializer.end(); move |context| async move { transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>((move || async move { - let output_ok = crate::bridge::wrapper::user_discovery::FlutterUserDiscovery::handle_new_messages(api_contact_id, api_messages).await?; Ok(output_ok) + let output_ok = crate::bridge::wrapper::user_discovery::FlutterUserDiscovery::handle_new_messages(api_contact_id, api_public_key_verified_timestamp, api_messages).await?; Ok(output_ok) })().await) } }) } @@ -126,6 +127,22 @@ let api_version = >::sse_decode(&mut deserializer);deserializer.end(); m })().await) } }) } +fn wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_update_verification_state_for_user_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::(flutter_rust_bridge::for_generated::TaskInfo{ debug_name: "flutter_user_discovery_update_verification_state_for_user", port: Some(port_), mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal }, move || { + let message = unsafe { flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(ptr_, rust_vec_len_, data_len_) }; + let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_contact_id = ::sse_decode(&mut deserializer); +let api_public_key_verified_timestamp = >::sse_decode(&mut deserializer);deserializer.end(); move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>((move || async move { + let output_ok = crate::bridge::wrapper::user_discovery::FlutterUserDiscovery::update_verification_state_for_user(api_contact_id, api_public_key_verified_timestamp).await?; Ok(output_ok) + })().await) + } }) +} fn wire__crate__bridge__callbacks__init_flutter_callbacks_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, @@ -148,9 +165,10 @@ let api_user_discovery_get_other_promotions_by_public_id = decode_DartFn_Inputs_ let api_user_discovery_get_announced_user_by_public_id = decode_DartFn_Inputs_i_64_Output_opt_box_autoadd_announced_user_AnyhowException(::sse_decode(&mut deserializer)); let api_user_discovery_get_contact_version = decode_DartFn_Inputs_i_64_Output_opt_list_prim_u_8_strict_AnyhowException(::sse_decode(&mut deserializer)); let api_user_discovery_set_contact_version = decode_DartFn_Inputs_i_64_list_prim_u_8_strict_Output_bool_AnyhowException(::sse_decode(&mut deserializer)); -let api_user_discovery_push_new_user_relation = decode_DartFn_Inputs_i_64_announced_user_opt_box_autoadd_i_64_Output_bool_AnyhowException(::sse_decode(&mut deserializer));deserializer.end(); move |context| { +let api_user_discovery_push_new_user_relation = decode_DartFn_Inputs_i_64_announced_user_opt_box_autoadd_i_64_Output_bool_AnyhowException(::sse_decode(&mut deserializer)); +let api_user_discovery_get_contact_promotion = decode_DartFn_Inputs_i_64_Output_opt_list_prim_u_8_strict_AnyhowException(::sse_decode(&mut deserializer));deserializer.end(); move |context| { transform_result_sse::<_, ()>((move || { - let output_ok = Result::<_,()>::Ok({ crate::bridge::callbacks::init_flutter_callbacks(api_logging_get_stream_sink, api_user_discovery_sign_data, api_user_discovery_verify_signature, api_user_discovery_verify_stored_pubkey, api_user_discovery_set_shares, api_user_discovery_get_share_for_contact, api_user_discovery_push_own_promotion_and_clear_old_version, api_user_discovery_get_own_promotions_after_version, api_user_discovery_store_other_promotion, api_user_discovery_get_other_promotions_by_public_id, api_user_discovery_get_announced_user_by_public_id, api_user_discovery_get_contact_version, api_user_discovery_set_contact_version, api_user_discovery_push_new_user_relation); })?; Ok(output_ok) + let output_ok = Result::<_,()>::Ok({ crate::bridge::callbacks::init_flutter_callbacks(api_logging_get_stream_sink, api_user_discovery_sign_data, api_user_discovery_verify_signature, api_user_discovery_verify_stored_pubkey, api_user_discovery_set_shares, api_user_discovery_get_share_for_contact, api_user_discovery_push_own_promotion_and_clear_old_version, api_user_discovery_get_own_promotions_after_version, api_user_discovery_store_other_promotion, api_user_discovery_get_other_promotions_by_public_id, api_user_discovery_get_announced_user_by_public_id, api_user_discovery_get_contact_version, api_user_discovery_set_contact_version, api_user_discovery_push_new_user_relation, api_user_discovery_get_contact_promotion); })?; Ok(output_ok) })()) } }) } @@ -903,8 +921,9 @@ fn pde_ffi_dispatcher_primary_impl( 3 => wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_handle_new_messages_impl(port, ptr, rust_vec_len, data_len), 4 => wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_initialize_or_update_impl(port, ptr, rust_vec_len, data_len), 5 => wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_should_request_new_messages_impl(port, ptr, rust_vec_len, data_len), -6 => wire__crate__bridge__callbacks__init_flutter_callbacks_impl(port, ptr, rust_vec_len, data_len), -7 => wire__crate__bridge__initialize_twonly_flutter_impl(port, ptr, rust_vec_len, data_len), +6 => wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_update_verification_state_for_user_impl(port, ptr, rust_vec_len, data_len), +7 => wire__crate__bridge__callbacks__init_flutter_callbacks_impl(port, ptr, rust_vec_len, data_len), +8 => wire__crate__bridge__initialize_twonly_flutter_impl(port, ptr, rust_vec_len, data_len), _ => unreachable!(), } } diff --git a/rust_dependencies/protocols/src/user_discovery.rs b/rust_dependencies/protocols/src/user_discovery.rs index 9ae3d2fe..833adff7 100644 --- a/rust_dependencies/protocols/src/user_discovery.rs +++ b/rust_dependencies/protocols/src/user_discovery.rs @@ -5,10 +5,12 @@ pub mod tests; pub mod traits; use std::collections::{HashMap, HashSet}; +use std::sync::Arc; use std::u8; use blahaj::{Share, Sharks}; use prost::Message; use serde::{Deserialize, Serialize}; +use tokio::sync::{Mutex, MutexGuard}; use crate::user_discovery::error::{Result, UserDiscoveryError}; use crate::user_discovery::traits::{AnnouncedUser, OtherPromotion, UserDiscoveryUtils}; use crate::user_discovery::user_discovery_message::{UserDiscoveryAnnouncement, UserDiscoveryPromotion}; @@ -52,12 +54,17 @@ where { store: Store, utils: Utils, + config_lock: Arc>, } impl UserDiscovery { /// Creates a new instance of the user discovery. pub fn new(store: Store, utils: Utils) -> Result { - Ok(Self { store, utils }) + Ok(Self { + store, + utils, + config_lock: Arc::default(), + }) } /// Initializes or updates the user discovery. @@ -81,6 +88,7 @@ impl UserDiscovery, ) -> Result<()> { + let config_lock = self.config_lock.lock().await; let mut config = match self.store.get_config().await { Ok(config) => { let mut config: UserDiscoveryConfig = serde_json::from_str(&config)?; @@ -116,9 +124,7 @@ impl UserDiscovery UserDiscovery Result> { - let config = self.get_config().await?; + let (config, _) = self.get_config().await?; Ok(UserDiscoveryVersion { announcement: config.announcement_version, promotion: config.promotion_version, @@ -181,7 +187,7 @@ impl UserDiscovery UserDiscovery, messages: Vec>, ) -> Result<()> { for message in messages { @@ -281,7 +288,11 @@ impl UserDiscovery UserDiscovery, + ) -> Result<()> { + let (mut config, config_lock) = self.get_config().await?; + config.promotion_version += 1; + + let Some(current_promotion) = self.store.get_contact_promotion(contact_id).await? else { + // User does not participate... + return Ok(()); + }; + + let old_message = UserDiscoveryMessage::decode(current_promotion.as_slice())?; + + let Some(old_promotion) = old_message.user_discovery_promotion else { + tracing::error!("A contact should only have a promotion message..."); + return Ok(()); + }; + + let message = UserDiscoveryMessage { + version: Some(UserDiscoveryVersion { + announcement: config.announcement_version, + promotion: config.promotion_version, + }), + user_discovery_promotion: Some(UserDiscoveryPromotion { + promotion_id: rand::random(), + public_id: old_promotion.public_id, + threshold: old_promotion.threshold, + announcement_share: old_promotion.announcement_share, + public_key_verified_timestamp, + }), + ..Default::default() + }; + + self.store + .push_own_promotion_and_clear_old_version( + contact_id, + config.promotion_version, + message.encode_to_vec(), + ) + .await?; + + self.update_config(config, config_lock).await?; + + Ok(()) + } + async fn setup_announcements( &self, config: &UserDiscoveryConfig, @@ -354,13 +413,28 @@ impl UserDiscovery Result { - Ok(serde_json::from_str(&self.store.get_config().await?)?) + async fn get_config(&self) -> Result<(UserDiscoveryConfig, MutexGuard<'_, bool>)> { + let mut lock = self.config_lock.lock().await; + *lock = true; + Ok((serde_json::from_str(&self.store.get_config().await?)?, lock)) + } + + async fn update_config( + &self, + config: UserDiscoveryConfig, + mut config_lock: MutexGuard<'_, bool>, + ) -> Result<()> { + self.store + .update_config(serde_json::to_string_pretty(&config)?) + .await?; + *config_lock = false; + Ok(()) } async fn handle_user_discovery_announcement( &self, contact_id: UserID, + public_key_verified_timestamp: Option, uda: UserDiscoveryAnnouncement, ) -> Result<()> { tracing::info!("Got a user discovery announcement from {contact_id}."); @@ -424,12 +498,8 @@ impl UserDiscovery UserDiscovery UserDiscovery UserDiscovery UserDiscovery>, used_shares: HashMap>, contact_versions: HashMap>, - other_promotions: Vec, + other_promotions: HashMap<(UserID, i64), OtherPromotion>, announced_users: HashMap)>>, own_promotions: Vec<(UserID, Vec)>, } @@ -97,8 +97,23 @@ impl UserDiscoveryStore for InMemoryStore { Ok(elements) } + async fn get_contact_promotion(&self, contact_id: UserID) -> Result>> { + let storage = self.storage(); + let element = storage + .own_promotions + .iter() + .rev() + .find(|(c_id, _)| *c_id == contact_id); + if let Some(element) = element { + return Ok(Some(element.1.to_owned())); + } + return Ok(None); + } + async fn store_other_promotion(&self, promotion: OtherPromotion) -> Result<()> { - self.storage().other_promotions.push(promotion); + self.storage() + .other_promotions + .insert((promotion.from_contact_id, promotion.public_id), promotion); Ok(()) } @@ -109,9 +124,9 @@ impl UserDiscoveryStore for InMemoryStore { Ok(self .storage() .other_promotions - .iter() + .clone() + .into_values() .filter(|other| other.public_id == public_id) - .map(OtherPromotion::to_owned) .collect()) } @@ -145,7 +160,11 @@ impl UserDiscoveryStore for InMemoryStore { .entry(announced_user.clone()) .or_insert(vec![]); if announced_user.user_id != from_contact_id { - entry.push((from_contact_id, public_key_verified_timestamp)); + if let Some(found) = entry.iter_mut().find(|x| x.0 == from_contact_id) { + found.1 = public_key_verified_timestamp; + } else { + entry.push((from_contact_id, public_key_verified_timestamp)); + } } Ok(()) } diff --git a/rust_dependencies/protocols/src/user_discovery/tests.rs b/rust_dependencies/protocols/src/user_discovery/tests.rs index 6cd80b26..2ecaf1b0 100644 --- a/rust_dependencies/protocols/src/user_discovery/tests.rs +++ b/rust_dependencies/protocols/src/user_discovery/tests.rs @@ -282,6 +282,40 @@ async fn test_user_discovery_in_memory_store() { step0_exchange_in_order::(&users).await; step1_verify_no_new_messages::(&users).await; step2_verify_announced_users_expected::(&users).await; + + let alice_idx = users.ids_by_name["ALICE"]; + let bob_idx = users.ids_by_name["BOB"]; + let david_idx = users.ids_by_name["DAVID"]; + + users.uds[bob_idx] + .update_verification_state_for_user(alice_idx as UserID, Some(10)) + .await + .unwrap(); + + step0_exchange_random::(&users).await; + step1_verify_no_new_messages::(&users).await; + + { + let david_knows = users.uds[david_idx] + .get_all_announced_users() + .await + .unwrap(); + + let knows_alice = david_knows + .iter() + .find(|(u, _)| u.user_id == alice_idx as UserID); + + assert!(knows_alice.is_some(), "David should know Alice"); + + let bob_has_verified = knows_alice + .unwrap() + .1 + .iter() + .find(|(user_id, _)| *user_id == bob_idx as UserID) + .unwrap(); + + assert_eq!(bob_has_verified.1, Some(10)); + } } #[tokio::test] @@ -441,7 +475,7 @@ async fn request_and_handle_messages( assert!(new_messages.len() <= messages_count); } - to.1.handle_new_messages(from.0 as UserID, new_messages) + to.1.handle_new_messages(from.0 as UserID, None, new_messages) .await .unwrap(); diff --git a/rust_dependencies/protocols/src/user_discovery/traits.rs b/rust_dependencies/protocols/src/user_discovery/traits.rs index 736df8fb..c9dcb507 100644 --- a/rust_dependencies/protocols/src/user_discovery/traits.rs +++ b/rust_dependencies/protocols/src/user_discovery/traits.rs @@ -47,6 +47,7 @@ pub trait UserDiscoveryStore { &self, promotion: OtherPromotion, ) -> impl Future> + Send; + fn get_other_promotions_by_public_id( &self, public_id: i64, @@ -68,10 +69,16 @@ pub trait UserDiscoveryStore { &self, ) -> impl Future)>>>> + Send; + fn get_contact_promotion( + &self, + contact_id: UserID, + ) -> impl Future>>> + Send; + fn get_contact_version( &self, contact_id: UserID, ) -> impl Future>>> + Send; + fn set_contact_version( &self, contact_id: UserID,