display transferred trust

This commit is contained in:
otsmr 2026-04-23 18:52:48 +02:00
parent f8649298e0
commit a29af4c914
23 changed files with 695 additions and 90 deletions

View file

@ -36,6 +36,8 @@ Future<void> initFlutterCallbacks({
userDiscoverySetContactVersion,
required FutureOr<bool> Function(PlatformInt64, AnnouncedUser, PlatformInt64?)
userDiscoveryPushNewUserRelation,
required FutureOr<Uint8List?> Function(PlatformInt64)
userDiscoveryGetContactPromotion,
}) => RustLib.instance.api.crateBridgeCallbacksInitFlutterCallbacks(
loggingGetStreamSink: loggingGetStreamSink,
userDiscoverySignData: userDiscoverySignData,
@ -55,4 +57,5 @@ Future<void> initFlutterCallbacks({
userDiscoveryGetContactVersion: userDiscoveryGetContactVersion,
userDiscoverySetContactVersion: userDiscoverySetContactVersion,
userDiscoveryPushNewUserRelation: userDiscoveryPushNewUserRelation,
userDiscoveryGetContactPromotion: userDiscoveryGetContactPromotion,
);

View file

@ -23,10 +23,12 @@ class FlutterUserDiscovery {
static Future<void> handleNewMessages({
required PlatformInt64 contactId,
PlatformInt64? publicKeyVerifiedTimestamp,
required List<Uint8List> messages,
}) => RustLib.instance.api
.crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryHandleNewMessages(
contactId: contactId,
publicKeyVerifiedTimestamp: publicKeyVerifiedTimestamp,
messages: messages,
);
@ -50,6 +52,15 @@ class FlutterUserDiscovery {
version: version,
);
static Future<void> updateVerificationStateForUser({
required PlatformInt64 contactId,
PlatformInt64? publicKeyVerifiedTimestamp,
}) => RustLib.instance.api
.crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryUpdateVerificationStateForUser(
contactId: contactId,
publicKeyVerifiedTimestamp: publicKeyVerifiedTimestamp,
);
@override
int get hashCode => 0;

View file

@ -70,7 +70,7 @@ class RustLib extends BaseEntrypoint<RustLibApi, RustLibApiImpl, RustLibWire> {
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<void>
crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryHandleNewMessages({
required PlatformInt64 contactId,
PlatformInt64? publicKeyVerifiedTimestamp,
required List<Uint8List> messages,
});
@ -110,6 +111,12 @@ abstract class RustLibApi extends BaseApi {
required List<int> version,
});
Future<void>
crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryUpdateVerificationStateForUser({
required PlatformInt64 contactId,
PlatformInt64? publicKeyVerifiedTimestamp,
});
Future<void> crateBridgeCallbacksInitFlutterCallbacks({
required FutureOr<RustStreamSink<String>> Function() loggingGetStreamSink,
required FutureOr<Uint8List?> Function(Uint8List) userDiscoverySignData,
@ -140,6 +147,8 @@ abstract class RustLibApi extends BaseApi {
PlatformInt64?,
)
userDiscoveryPushNewUserRelation,
required FutureOr<Uint8List?> Function(PlatformInt64)
userDiscoveryGetContactPromotion,
});
Future<void> crateBridgeInitializeTwonlyFlutter({
@ -230,6 +239,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
Future<void>
crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryHandleNewMessages({
required PlatformInt64 contactId,
PlatformInt64? publicKeyVerifiedTimestamp,
required List<Uint8List> 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<void>
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<void> crateBridgeCallbacksInitFlutterCallbacks({
required FutureOr<RustStreamSink<String>> Function() loggingGetStreamSink,
@ -373,6 +428,8 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
PlatformInt64?,
)
userDiscoveryPushNewUserRelation,
required FutureOr<Uint8List?> 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_,
);
},

View file

@ -25,5 +25,7 @@ Future<void> initFlutterCallbacksForRust() async {
userDiscoverySignData: UserDiscoveryCallbacks.signData,
userDiscoveryVerifySignature: UserDiscoveryCallbacks.verifySignature,
userDiscoveryVerifyStoredPubkey: UserDiscoveryCallbacks.verifyStoredPubKey,
userDiscoveryGetContactPromotion:
UserDiscoveryCallbacks.getContactPromotion,
);
}

View file

@ -298,4 +298,16 @@ class UserDiscoveryCallbacks {
return false;
}
}
static Future<Uint8List?> 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;
}
}
}

View file

@ -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<TwonlyDB>
with _$KeyVerificationDaoMixin {
@ -56,18 +68,89 @@ class KeyVerificationDao extends DatabaseAccessor<TwonlyDB>
)..where((kv) => kv.contactId.equals(contactId))).watch();
}
Stream<bool> watchAllGroupMembersVerified(String groupId) {
final gm = groupMembers;
Future<List<KeyVerification>> getContactVerification(int contactId) async {
return (select(
keyVerifications,
)..where((kv) => kv.contactId.equals(contactId))).get();
}
Stream<List<(Contact, DateTime)>> 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)),
]);
return query.watch().map(
(rows) =>
rows.isNotEmpty && rows.every((r) => r.readTableOrNull(kv) != null),
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) {
return rows.map((row) {
final contact = row.readTable(contacts);
final timestamp = row.readTable(ur).publicKeyVerifiedTimestamp!;
return (contact, timestamp);
}).toList();
});
}
Stream<VerificationStatus> 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 = <int, ({bool direct, bool partial})>{};
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<void> addKeyVerification(int contactId, VerificationType type) async {
@ -77,5 +160,11 @@ class KeyVerificationDao extends DatabaseAccessor<TwonlyDB>
type: Value(type),
),
);
if (userService.currentUser.isUserDiscoveryEnabled) {
await FlutterUserDiscovery.updateVerificationStateForUser(
contactId: contactId,
publicKeyVerifiedTimestamp: clock.now().millisecondsSinceEpoch,
);
}
}
}

View file

@ -11,6 +11,10 @@ mixin _$KeyVerificationDaoMixin on DatabaseAccessor<TwonlyDB> {
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,
);
}

View file

@ -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<TwonlyDB>
results[user]!.add(relationData);
}
Log.info('results = ${results.length}');
return results;
});
}
@ -223,4 +227,19 @@ class UserDiscoveryDao extends DatabaseAccessor<TwonlyDB>
updatedValues,
);
}
Stream<List<UserDiscoveryAnnouncedUser>> watchAllAnnouncedUsers() =>
select(userDiscoveryAnnouncedUsers).watch();
Stream<List<UserDiscoveryUserRelation>> watchAllUserRelations() =>
select(userDiscoveryUserRelations).watch();
Stream<List<UserDiscoveryOwnPromotion>> watchAllOwnPromotions() =>
select(userDiscoveryOwnPromotions).watch();
Stream<List<UserDiscoveryOtherPromotion>> watchAllOtherPromotions() =>
select(userDiscoveryOtherPromotions).watch();
Stream<List<UserDiscoveryShare>> watchAllShares() =>
select(userDiscoveryShares).watch();
}

View file

@ -11,6 +11,8 @@ mixin _$UserDiscoveryDaoMixin on DatabaseAccessor<TwonlyDB> {
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,

View file

@ -131,9 +131,14 @@ class UserDiscoveryService {
List<Uint8List> 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);

View file

@ -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<VerificationBadgeComp> {
bool _isVerified = false;
bool _isVerifiedByTransferredTrust = false;
StreamSubscription<bool>? _streamAllVerified;
StreamSubscription<VerificationStatus>? _streamAllVerified;
StreamSubscription<List<KeyVerification>>? _streamContactVerification;
StreamSubscription<List<(Contact, DateTime)>>? _streamTransferredTrust;
@override
void initState() {
@ -42,7 +47,14 @@ class _VerificationBadgeCompState extends State<VerificationBadgeComp> {
.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<VerificationBadgeComp> {
.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<VerificationBadgeComp> {
void dispose() {
_streamAllVerified?.cancel();
_streamContactVerification?.cancel();
_streamTransferredTrust?.cancel();
super.dispose();
}
@ -81,9 +104,12 @@ class _VerificationBadgeCompState extends State<VerificationBadgeComp> {
bottom: 3,
),
child: SvgIcon(
assetPath: _isVerified
assetPath: (_isVerified || _isVerifiedByTransferredTrust)
? SvgIcons.verifiedGreen
: SvgIcons.verifiedRed,
color: (_isVerifiedByTransferredTrust && !_isVerified)
? colorVerificationBadgeYellow
: null,
size: widget.size,
),
),

View file

@ -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);

View file

@ -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<ContactView> {
Contact? _contact;
List<GroupMember> _memberOfGroups = [];
List<KeyVerification> _keyVerifications = [];
List<(Contact, DateTime)> _transferredTrust = [];
late StreamSubscription<Contact?> _contactSub;
late StreamSubscription<List<GroupMember>> _groupMemberSub;
late StreamSubscription<Contact?> _streamContact;
late StreamSubscription<List<GroupMember>> _streamMemberOfGroups;
late StreamSubscription<List<KeyVerification>> _streamKeyVerifications;
late StreamSubscription<List<(Contact, DateTime)>> _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<ContactView> {
});
}
});
_groupMemberSub = twonlyDB.groupsDao
_streamMemberOfGroups = twonlyDB.groupsDao
.watchContactGroupMember(widget.userId)
.listen((groups) async {
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<ContactView> {
Padding(
padding: const EdgeInsets.only(right: 10),
child: VerificationBadgeComp(
key: GlobalKey(),
contact: contact,
),
),
@ -230,7 +242,7 @@ class _ContactViewState extends State<ContactView> {
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<ContactView> {
setState(() {});
},
),
if (_keyVerifications.isNotEmpty)
if (_keyVerifications.isNotEmpty || _transferredTrust.isNotEmpty)
ExpansionTile(
shape: const RoundedRectangleBorder(),
backgroundColor: context.color.surfaceContainer,
@ -255,8 +267,8 @@ class _ContactViewState extends State<ContactView> {
),
),
title: Text(context.lang.userVerifiedTitle),
children: _keyVerifications
.map(
children: [
..._keyVerifications.map(
(kv) => ListTile(
dense: true,
title: Text(_verificationTypeLabel(context, kv.type)),
@ -270,8 +282,25 @@ class _ContactViewState extends State<ContactView> {
),
),
),
)
.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(

View file

@ -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<DeveloperSettingsView> {
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,

View file

@ -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<UserDiscoveryDeveloperView> createState() =>
_UserDiscoveryDeveloperViewState();
}
class _UserDiscoveryDeveloperViewState
extends State<UserDiscoveryDeveloperView> {
UserDiscoveryDao get dao => twonlyDB.userDiscoveryDao;
@override
Widget build(BuildContext context) {
return StreamBuilder<List<Contact>>(
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<UserDiscoveryAnnouncedUser>(
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<UserDiscoveryUserRelation>(
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<UserDiscoveryOtherPromotion>(
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<UserDiscoveryOwnPromotion>(
title: 'Own Promotions',
stream: dao.watchAllOwnPromotions(),
itemBuilder: (context, promotion) => ListTile(
title: Text('Contact: ${getName(promotion.contactId)}'),
subtitle: Text('Version: ${promotion.versionId}'),
),
),
_TableExpansionTile<UserDiscoveryShare>(
title: 'Shares',
stream: dao.watchAllShares(),
itemBuilder: (context, share) => ListTile(
title: Text('Share ID: ${share.shareId}'),
subtitle: Text('Contact: ${getName(share.contactId)}'),
),
),
],
),
);
},
);
}
}
class _TableExpansionTile<T> extends StatelessWidget {
const _TableExpansionTile({
required this.title,
required this.stream,
required this.itemBuilder,
});
final String title;
final Stream<List<T>> stream;
final Widget Function(BuildContext, T) itemBuilder;
@override
Widget build(BuildContext context) {
return StreamBuilder<List<T>>(
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),
),
),
],
);
},
);
}
}

View file

@ -36,6 +36,7 @@ callback_generator! {
get_contact_version: (i64) => Option<Vec<u8>>,
set_contact_version: (i64, Vec<u8>) => bool,
push_new_user_relation: (i64, AnnouncedUser, Option<i64>) => bool,
get_contact_promotion: (i64) => Option<Vec<u8>>
}
}
}

View file

@ -85,7 +85,9 @@ impl UserDiscoveryStore for UserDiscoveryStoreFlutter {
version: u32,
promotion: Vec<u8>,
) -> Result<()> {
(get_callbacks()?.user_discovery.push_own_promotion_and_clear_old_version)(contact_id, version as i64, promotion)
(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())
@ -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<Option<Vec<u8>>> {
Ok((get_callbacks()?.user_discovery.get_contact_promotion)(contact_id).await)
}
}

View file

@ -50,12 +50,28 @@ impl FlutterUserDiscovery {
.await?)
}
pub async fn handle_new_messages(contact_id: i64, messages: Vec<Vec<u8>>) -> Result<()> {
pub async fn handle_new_messages(
contact_id: i64,
public_key_verified_timestamp: Option<i64>,
messages: Vec<Vec<u8>>,
) -> 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<i64>,
) -> Result<()> {
Ok(get_twonly_flutter()?
.user_discovery
.get()
.await
.update_verification_state_for_user(contact_id, public_key_verified_timestamp)
.await?)
}
}

View file

@ -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 = <i64>::sse_decode(&mut deserializer);
let api_public_key_verified_timestamp = <Option<i64>>::sse_decode(&mut deserializer);
let api_messages = <Vec<Vec<u8>>>::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 = <Vec<u8>>::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::SseCodec,_,_,_>(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 = <i64>::sse_decode(&mut deserializer);
let api_public_key_verified_timestamp = <Option<i64>>::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(<flutter_rust_bridge::DartOpaque>::sse_decode(&mut deserializer));
let api_user_discovery_get_contact_version = decode_DartFn_Inputs_i_64_Output_opt_list_prim_u_8_strict_AnyhowException(<flutter_rust_bridge::DartOpaque>::sse_decode(&mut deserializer));
let api_user_discovery_set_contact_version = decode_DartFn_Inputs_i_64_list_prim_u_8_strict_Output_bool_AnyhowException(<flutter_rust_bridge::DartOpaque>::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(<flutter_rust_bridge::DartOpaque>::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(<flutter_rust_bridge::DartOpaque>::sse_decode(&mut deserializer));
let api_user_discovery_get_contact_promotion = decode_DartFn_Inputs_i_64_Output_opt_list_prim_u_8_strict_AnyhowException(<flutter_rust_bridge::DartOpaque>::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!(),
}
}

View file

@ -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<Mutex<bool>>,
}
impl<Store: UserDiscoveryStore, Utils: UserDiscoveryUtils> UserDiscovery<Store, Utils> {
/// Creates a new instance of the user discovery.
pub fn new(store: Store, utils: Utils) -> Result<Self> {
Ok(Self { store, utils })
Ok(Self {
store,
utils,
config_lock: Arc::default(),
})
}
/// Initializes or updates the user discovery.
@ -81,6 +88,7 @@ impl<Store: UserDiscoveryStore, Utils: UserDiscoveryUtils> UserDiscovery<Store,
user_id: UserID,
public_key: Vec<u8>,
) -> 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<Store: UserDiscoveryStore, Utils: UserDiscoveryUtils> UserDiscovery<Store,
config.announcement_version += 1;
config.verification_shares = verification_shares;
self.store
.update_config(serde_json::to_string_pretty(&config)?)
.await?;
self.update_config(config, config_lock).await?;
Ok(())
}
@ -138,7 +144,7 @@ impl<Store: UserDiscoveryStore, Utils: UserDiscoveryUtils> UserDiscovery<Store,
/// * `Err(UserDiscoveryError)` - If there where errors in the store.
///
pub async fn get_current_version(&self) -> Result<Vec<u8>> {
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<Store: UserDiscoveryStore, Utils: UserDiscoveryUtils> UserDiscovery<Store,
let mut messages = vec![];
let received_version = UserDiscoveryVersion::decode(received_version)?;
let config = self.get_config().await?;
let (config, _) = self.get_config().await?;
let version = Some(UserDiscoveryVersion {
announcement: config.announcement_version,
promotion: config.promotion_version,
@ -271,6 +277,7 @@ impl<Store: UserDiscoveryStore, Utils: UserDiscoveryUtils> UserDiscovery<Store,
pub async fn handle_new_messages(
&self,
contact_id: UserID,
public_key_verified_timestamp: Option<i64>,
messages: Vec<Vec<u8>>,
) -> Result<()> {
for message in messages {
@ -281,7 +288,11 @@ impl<Store: UserDiscoveryStore, Utils: UserDiscoveryUtils> UserDiscovery<Store,
if let Some(uda) = message.user_discovery_announcement {
if let Err(err) = self
.handle_user_discovery_announcement(contact_id, uda)
.handle_user_discovery_announcement(
contact_id,
public_key_verified_timestamp,
uda,
)
.await
{
tracing::warn!("Ignoring: {err}");
@ -303,6 +314,54 @@ impl<Store: UserDiscoveryStore, Utils: UserDiscoveryUtils> UserDiscovery<Store,
Ok(())
}
pub async fn update_verification_state_for_user(
&self,
contact_id: UserID,
public_key_verified_timestamp: Option<i64>,
) -> 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<Store: UserDiscoveryStore, Utils: UserDiscoveryUtils> UserDiscovery<Store,
Ok(verification_shares)
}
async fn get_config(&self) -> Result<UserDiscoveryConfig> {
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<i64>,
uda: UserDiscoveryAnnouncement,
) -> Result<()> {
tracing::info!("Got a user discovery announcement from {contact_id}.");
@ -424,12 +498,8 @@ impl<Store: UserDiscoveryStore, Utils: UserDiscoveryUtils> UserDiscovery<Store,
)));
}
tracing::debug!("Increased promotion version id.");
let mut config = self.get_config().await?;
let (mut config, config_lock) = self.get_config().await?;
config.promotion_version += 1;
self.store
.update_config(serde_json::to_string_pretty(&config)?)
.await?;
let message = UserDiscoveryMessage {
version: Some(UserDiscoveryVersion {
@ -441,7 +511,7 @@ impl<Store: UserDiscoveryStore, Utils: UserDiscoveryUtils> UserDiscovery<Store,
public_id: signed_data.public_id,
threshold: uda.threshold,
announcement_share: uda.announcement_share,
public_key_verified_timestamp: None,
public_key_verified_timestamp,
}),
..Default::default()
};
@ -454,6 +524,8 @@ impl<Store: UserDiscoveryStore, Utils: UserDiscoveryUtils> UserDiscovery<Store,
)
.await?;
self.update_config(config, config_lock).await?;
let announced_user = AnnouncedUser {
user_id: signed_data.user_id,
public_key: signed_data.public_key,
@ -470,7 +542,7 @@ impl<Store: UserDiscoveryStore, Utils: UserDiscoveryUtils> UserDiscovery<Store,
.push_new_user_relation(
contact_id,
announced_user,
None, // This flag mus be handled by the applications as this comes from an announcement.
public_key_verified_timestamp,
)
.await?;
@ -587,7 +659,9 @@ impl<Store: UserDiscoveryStore, Utils: UserDiscoveryUtils> UserDiscovery<Store,
public_id: udp.public_id,
};
let user_id = self.get_config().await?.user_id;
let (config, _) = self.get_config().await?;
let user_id = config.user_id;
for promotion in unique_promotions {
// Do not store the announcement of the users itself.
// Or in case the promotion promotes myself

View file

@ -10,7 +10,7 @@ pub(crate) struct Storage {
unused_shares: Vec<Vec<u8>>,
used_shares: HashMap<UserID, Vec<u8>>,
contact_versions: HashMap<UserID, Vec<u8>>,
other_promotions: Vec<OtherPromotion>,
other_promotions: HashMap<(UserID, i64), OtherPromotion>,
announced_users: HashMap<AnnouncedUser, Vec<(UserID, Option<i64>)>>,
own_promotions: Vec<(UserID, Vec<u8>)>,
}
@ -97,8 +97,23 @@ impl UserDiscoveryStore for InMemoryStore {
Ok(elements)
}
async fn get_contact_promotion(&self, contact_id: UserID) -> Result<Option<Vec<u8>>> {
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,8 +160,12 @@ impl UserDiscoveryStore for InMemoryStore {
.entry(announced_user.clone())
.or_insert(vec![]);
if announced_user.user_id != from_contact_id {
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(())
}
}

View file

@ -282,6 +282,40 @@ async fn test_user_discovery_in_memory_store() {
step0_exchange_in_order::<InMemoryStore>(&users).await;
step1_verify_no_new_messages::<InMemoryStore>(&users).await;
step2_verify_announced_users_expected::<InMemoryStore>(&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::<InMemoryStore>(&users).await;
step1_verify_no_new_messages::<InMemoryStore>(&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<S: UserDiscoveryStore>(
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();

View file

@ -47,6 +47,7 @@ pub trait UserDiscoveryStore {
&self,
promotion: OtherPromotion,
) -> impl Future<Output = Result<()>> + Send;
fn get_other_promotions_by_public_id(
&self,
public_id: i64,
@ -68,10 +69,16 @@ pub trait UserDiscoveryStore {
&self,
) -> impl Future<Output = Result<HashMap<AnnouncedUser, Vec<(UserID, Option<i64>)>>>> + Send;
fn get_contact_promotion(
&self,
contact_id: UserID,
) -> impl Future<Output = Result<Option<Vec<u8>>>> + Send;
fn get_contact_version(
&self,
contact_id: UserID,
) -> impl Future<Output = Result<Option<Vec<u8>>>> + Send;
fn set_contact_version(
&self,
contact_id: UserID,