mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-05-25 02:12:13 +00:00
display transferred trust
This commit is contained in:
parent
f8649298e0
commit
a29af4c914
23 changed files with 695 additions and 90 deletions
|
|
@ -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,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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_,
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -25,5 +25,7 @@ Future<void> initFlutterCallbacksForRust() async {
|
|||
userDiscoverySignData: UserDiscoveryCallbacks.signData,
|
||||
userDiscoveryVerifySignature: UserDiscoveryCallbacks.verifySignature,
|
||||
userDiscoveryVerifyStoredPubkey: UserDiscoveryCallbacks.verifyStoredPubKey,
|
||||
userDiscoveryGetContactPromotion:
|
||||
UserDiscoveryCallbacks.getContactPromotion,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
]);
|
||||
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<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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
_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<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,23 +267,40 @@ class _ContactViewState extends State<ContactView> {
|
|||
),
|
||||
),
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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>>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,10 +85,12 @@ 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)
|
||||
.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<Vec<Vec<u8>>> {
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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?)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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!(),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,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(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue