mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-05-25 05:22: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,
|
userDiscoverySetContactVersion,
|
||||||
required FutureOr<bool> Function(PlatformInt64, AnnouncedUser, PlatformInt64?)
|
required FutureOr<bool> Function(PlatformInt64, AnnouncedUser, PlatformInt64?)
|
||||||
userDiscoveryPushNewUserRelation,
|
userDiscoveryPushNewUserRelation,
|
||||||
|
required FutureOr<Uint8List?> Function(PlatformInt64)
|
||||||
|
userDiscoveryGetContactPromotion,
|
||||||
}) => RustLib.instance.api.crateBridgeCallbacksInitFlutterCallbacks(
|
}) => RustLib.instance.api.crateBridgeCallbacksInitFlutterCallbacks(
|
||||||
loggingGetStreamSink: loggingGetStreamSink,
|
loggingGetStreamSink: loggingGetStreamSink,
|
||||||
userDiscoverySignData: userDiscoverySignData,
|
userDiscoverySignData: userDiscoverySignData,
|
||||||
|
|
@ -55,4 +57,5 @@ Future<void> initFlutterCallbacks({
|
||||||
userDiscoveryGetContactVersion: userDiscoveryGetContactVersion,
|
userDiscoveryGetContactVersion: userDiscoveryGetContactVersion,
|
||||||
userDiscoverySetContactVersion: userDiscoverySetContactVersion,
|
userDiscoverySetContactVersion: userDiscoverySetContactVersion,
|
||||||
userDiscoveryPushNewUserRelation: userDiscoveryPushNewUserRelation,
|
userDiscoveryPushNewUserRelation: userDiscoveryPushNewUserRelation,
|
||||||
|
userDiscoveryGetContactPromotion: userDiscoveryGetContactPromotion,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -23,10 +23,12 @@ class FlutterUserDiscovery {
|
||||||
|
|
||||||
static Future<void> handleNewMessages({
|
static Future<void> handleNewMessages({
|
||||||
required PlatformInt64 contactId,
|
required PlatformInt64 contactId,
|
||||||
|
PlatformInt64? publicKeyVerifiedTimestamp,
|
||||||
required List<Uint8List> messages,
|
required List<Uint8List> messages,
|
||||||
}) => RustLib.instance.api
|
}) => RustLib.instance.api
|
||||||
.crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryHandleNewMessages(
|
.crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryHandleNewMessages(
|
||||||
contactId: contactId,
|
contactId: contactId,
|
||||||
|
publicKeyVerifiedTimestamp: publicKeyVerifiedTimestamp,
|
||||||
messages: messages,
|
messages: messages,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -50,6 +52,15 @@ class FlutterUserDiscovery {
|
||||||
version: version,
|
version: version,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
static Future<void> updateVerificationStateForUser({
|
||||||
|
required PlatformInt64 contactId,
|
||||||
|
PlatformInt64? publicKeyVerifiedTimestamp,
|
||||||
|
}) => RustLib.instance.api
|
||||||
|
.crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryUpdateVerificationStateForUser(
|
||||||
|
contactId: contactId,
|
||||||
|
publicKeyVerifiedTimestamp: publicKeyVerifiedTimestamp,
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => 0;
|
int get hashCode => 0;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@ class RustLib extends BaseEntrypoint<RustLibApi, RustLibApiImpl, RustLibWire> {
|
||||||
String get codegenVersion => '2.12.0';
|
String get codegenVersion => '2.12.0';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get rustContentHash => -630534473;
|
int get rustContentHash => 1680338106;
|
||||||
|
|
||||||
static const kDefaultExternalLibraryLoaderConfig =
|
static const kDefaultExternalLibraryLoaderConfig =
|
||||||
ExternalLibraryLoaderConfig(
|
ExternalLibraryLoaderConfig(
|
||||||
|
|
@ -94,6 +94,7 @@ abstract class RustLibApi extends BaseApi {
|
||||||
Future<void>
|
Future<void>
|
||||||
crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryHandleNewMessages({
|
crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryHandleNewMessages({
|
||||||
required PlatformInt64 contactId,
|
required PlatformInt64 contactId,
|
||||||
|
PlatformInt64? publicKeyVerifiedTimestamp,
|
||||||
required List<Uint8List> messages,
|
required List<Uint8List> messages,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -110,6 +111,12 @@ abstract class RustLibApi extends BaseApi {
|
||||||
required List<int> version,
|
required List<int> version,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Future<void>
|
||||||
|
crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryUpdateVerificationStateForUser({
|
||||||
|
required PlatformInt64 contactId,
|
||||||
|
PlatformInt64? publicKeyVerifiedTimestamp,
|
||||||
|
});
|
||||||
|
|
||||||
Future<void> crateBridgeCallbacksInitFlutterCallbacks({
|
Future<void> crateBridgeCallbacksInitFlutterCallbacks({
|
||||||
required FutureOr<RustStreamSink<String>> Function() loggingGetStreamSink,
|
required FutureOr<RustStreamSink<String>> Function() loggingGetStreamSink,
|
||||||
required FutureOr<Uint8List?> Function(Uint8List) userDiscoverySignData,
|
required FutureOr<Uint8List?> Function(Uint8List) userDiscoverySignData,
|
||||||
|
|
@ -140,6 +147,8 @@ abstract class RustLibApi extends BaseApi {
|
||||||
PlatformInt64?,
|
PlatformInt64?,
|
||||||
)
|
)
|
||||||
userDiscoveryPushNewUserRelation,
|
userDiscoveryPushNewUserRelation,
|
||||||
|
required FutureOr<Uint8List?> Function(PlatformInt64)
|
||||||
|
userDiscoveryGetContactPromotion,
|
||||||
});
|
});
|
||||||
|
|
||||||
Future<void> crateBridgeInitializeTwonlyFlutter({
|
Future<void> crateBridgeInitializeTwonlyFlutter({
|
||||||
|
|
@ -230,6 +239,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
Future<void>
|
Future<void>
|
||||||
crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryHandleNewMessages({
|
crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryHandleNewMessages({
|
||||||
required PlatformInt64 contactId,
|
required PlatformInt64 contactId,
|
||||||
|
PlatformInt64? publicKeyVerifiedTimestamp,
|
||||||
required List<Uint8List> messages,
|
required List<Uint8List> messages,
|
||||||
}) {
|
}) {
|
||||||
return handler.executeNormal(
|
return handler.executeNormal(
|
||||||
|
|
@ -237,6 +247,10 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
callFfi: (port_) {
|
callFfi: (port_) {
|
||||||
final serializer = SseSerializer(generalizedFrbRustBinding);
|
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||||
sse_encode_i_64(contactId, serializer);
|
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);
|
sse_encode_list_list_prim_u_8_strict(messages, serializer);
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
|
|
@ -251,7 +265,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
),
|
),
|
||||||
constMeta:
|
constMeta:
|
||||||
kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryHandleNewMessagesConstMeta,
|
kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryHandleNewMessagesConstMeta,
|
||||||
argValues: [contactId, messages],
|
argValues: [contactId, publicKeyVerifiedTimestamp, messages],
|
||||||
apiImpl: this,
|
apiImpl: this,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -261,7 +275,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
get kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryHandleNewMessagesConstMeta =>
|
get kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryHandleNewMessagesConstMeta =>
|
||||||
const TaskConstMeta(
|
const TaskConstMeta(
|
||||||
debugName: "flutter_user_discovery_handle_new_messages",
|
debugName: "flutter_user_discovery_handle_new_messages",
|
||||||
argNames: ["contactId", "messages"],
|
argNames: ["contactId", "publicKeyVerifiedTimestamp", "messages"],
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -342,6 +356,47 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
argNames: ["contactId", "version"],
|
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
|
@override
|
||||||
Future<void> crateBridgeCallbacksInitFlutterCallbacks({
|
Future<void> crateBridgeCallbacksInitFlutterCallbacks({
|
||||||
required FutureOr<RustStreamSink<String>> Function() loggingGetStreamSink,
|
required FutureOr<RustStreamSink<String>> Function() loggingGetStreamSink,
|
||||||
|
|
@ -373,6 +428,8 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
PlatformInt64?,
|
PlatformInt64?,
|
||||||
)
|
)
|
||||||
userDiscoveryPushNewUserRelation,
|
userDiscoveryPushNewUserRelation,
|
||||||
|
required FutureOr<Uint8List?> Function(PlatformInt64)
|
||||||
|
userDiscoveryGetContactPromotion,
|
||||||
}) {
|
}) {
|
||||||
return handler.executeNormal(
|
return handler.executeNormal(
|
||||||
NormalTask(
|
NormalTask(
|
||||||
|
|
@ -434,10 +491,14 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
userDiscoveryPushNewUserRelation,
|
userDiscoveryPushNewUserRelation,
|
||||||
serializer,
|
serializer,
|
||||||
);
|
);
|
||||||
|
sse_encode_DartFn_Inputs_i_64_Output_opt_list_prim_u_8_strict_AnyhowException(
|
||||||
|
userDiscoveryGetContactPromotion,
|
||||||
|
serializer,
|
||||||
|
);
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 6,
|
funcId: 7,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -461,6 +522,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
userDiscoveryGetContactVersion,
|
userDiscoveryGetContactVersion,
|
||||||
userDiscoverySetContactVersion,
|
userDiscoverySetContactVersion,
|
||||||
userDiscoveryPushNewUserRelation,
|
userDiscoveryPushNewUserRelation,
|
||||||
|
userDiscoveryGetContactPromotion,
|
||||||
],
|
],
|
||||||
apiImpl: this,
|
apiImpl: this,
|
||||||
),
|
),
|
||||||
|
|
@ -485,6 +547,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
"userDiscoveryGetContactVersion",
|
"userDiscoveryGetContactVersion",
|
||||||
"userDiscoverySetContactVersion",
|
"userDiscoverySetContactVersion",
|
||||||
"userDiscoveryPushNewUserRelation",
|
"userDiscoveryPushNewUserRelation",
|
||||||
|
"userDiscoveryGetContactPromotion",
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -500,7 +563,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 7,
|
funcId: 8,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -25,5 +25,7 @@ Future<void> initFlutterCallbacksForRust() async {
|
||||||
userDiscoverySignData: UserDiscoveryCallbacks.signData,
|
userDiscoverySignData: UserDiscoveryCallbacks.signData,
|
||||||
userDiscoveryVerifySignature: UserDiscoveryCallbacks.verifySignature,
|
userDiscoveryVerifySignature: UserDiscoveryCallbacks.verifySignature,
|
||||||
userDiscoveryVerifyStoredPubkey: UserDiscoveryCallbacks.verifyStoredPubKey,
|
userDiscoveryVerifyStoredPubkey: UserDiscoveryCallbacks.verifyStoredPubKey,
|
||||||
|
userDiscoveryGetContactPromotion:
|
||||||
|
UserDiscoveryCallbacks.getContactPromotion,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -298,4 +298,16 @@ class UserDiscoveryCallbacks {
|
||||||
return false;
|
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: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/contacts.table.dart';
|
||||||
import 'package:twonly/src/database/tables/groups.table.dart';
|
import 'package:twonly/src/database/tables/groups.table.dart';
|
||||||
|
import 'package:twonly/src/database/tables/user_discovery.table.dart';
|
||||||
import 'package:twonly/src/database/twonly.db.dart';
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
|
|
||||||
part 'key_verification.dao.g.dart';
|
part 'key_verification.dao.g.dart';
|
||||||
|
|
||||||
|
enum VerificationStatus { trusted, partialTrusted, notTrusted }
|
||||||
|
|
||||||
@DriftAccessor(
|
@DriftAccessor(
|
||||||
tables: [Contacts, VerificationTokens, KeyVerifications, GroupMembers],
|
tables: [
|
||||||
|
Contacts,
|
||||||
|
VerificationTokens,
|
||||||
|
KeyVerifications,
|
||||||
|
GroupMembers,
|
||||||
|
UserDiscoveryUserRelations,
|
||||||
|
],
|
||||||
)
|
)
|
||||||
class KeyVerificationDao extends DatabaseAccessor<TwonlyDB>
|
class KeyVerificationDao extends DatabaseAccessor<TwonlyDB>
|
||||||
with _$KeyVerificationDaoMixin {
|
with _$KeyVerificationDaoMixin {
|
||||||
|
|
@ -56,18 +68,89 @@ class KeyVerificationDao extends DatabaseAccessor<TwonlyDB>
|
||||||
)..where((kv) => kv.contactId.equals(contactId))).watch();
|
)..where((kv) => kv.contactId.equals(contactId))).watch();
|
||||||
}
|
}
|
||||||
|
|
||||||
Stream<bool> watchAllGroupMembersVerified(String groupId) {
|
Future<List<KeyVerification>> getContactVerification(int contactId) async {
|
||||||
final gm = groupMembers;
|
return (select(
|
||||||
|
keyVerifications,
|
||||||
|
)..where((kv) => kv.contactId.equals(contactId))).get();
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream<List<(Contact, DateTime)>> watchTransferredTrustVerifications(
|
||||||
|
int contactId,
|
||||||
|
) {
|
||||||
final kv = keyVerifications;
|
final kv = keyVerifications;
|
||||||
|
final ur = userDiscoveryUserRelations;
|
||||||
|
|
||||||
final query = (select(gm)..where((m) => m.groupId.equals(groupId))).join([
|
final query =
|
||||||
leftOuterJoin(kv, kv.contactId.equalsExp(gm.contactId)),
|
(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(
|
return query.watch().map((rows) {
|
||||||
(rows) =>
|
return rows.map((row) {
|
||||||
rows.isNotEmpty && rows.every((r) => r.readTableOrNull(kv) != null),
|
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 {
|
Future<void> addKeyVerification(int contactId, VerificationType type) async {
|
||||||
|
|
@ -77,5 +160,11 @@ class KeyVerificationDao extends DatabaseAccessor<TwonlyDB>
|
||||||
type: Value(type),
|
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;
|
attachedDatabase.keyVerifications;
|
||||||
$GroupsTable get groups => attachedDatabase.groups;
|
$GroupsTable get groups => attachedDatabase.groups;
|
||||||
$GroupMembersTable get groupMembers => attachedDatabase.groupMembers;
|
$GroupMembersTable get groupMembers => attachedDatabase.groupMembers;
|
||||||
|
$UserDiscoveryAnnouncedUsersTable get userDiscoveryAnnouncedUsers =>
|
||||||
|
attachedDatabase.userDiscoveryAnnouncedUsers;
|
||||||
|
$UserDiscoveryUserRelationsTable get userDiscoveryUserRelations =>
|
||||||
|
attachedDatabase.userDiscoveryUserRelations;
|
||||||
KeyVerificationDaoManager get managers => KeyVerificationDaoManager(this);
|
KeyVerificationDaoManager get managers => KeyVerificationDaoManager(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -33,4 +37,16 @@ class KeyVerificationDaoManager {
|
||||||
$$GroupsTableTableManager(_db.attachedDatabase, _db.groups);
|
$$GroupsTableTableManager(_db.attachedDatabase, _db.groups);
|
||||||
$$GroupMembersTableTableManager get groupMembers =>
|
$$GroupMembersTableTableManager get groupMembers =>
|
||||||
$$GroupMembersTableTableManager(_db.attachedDatabase, _db.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/contacts.table.dart';
|
||||||
import 'package:twonly/src/database/tables/user_discovery.table.dart';
|
import 'package:twonly/src/database/tables/user_discovery.table.dart';
|
||||||
import 'package:twonly/src/database/twonly.db.dart';
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
|
import 'package:twonly/src/utils/log.dart';
|
||||||
|
|
||||||
part 'user_discovery.dao.g.dart';
|
part 'user_discovery.dao.g.dart';
|
||||||
|
|
||||||
|
|
@ -13,6 +14,7 @@ typedef AnnouncedUsersWithRelations =
|
||||||
UserDiscoveryAnnouncedUsers,
|
UserDiscoveryAnnouncedUsers,
|
||||||
UserDiscoveryUserRelations,
|
UserDiscoveryUserRelations,
|
||||||
UserDiscoveryOwnPromotions,
|
UserDiscoveryOwnPromotions,
|
||||||
|
UserDiscoveryOtherPromotions,
|
||||||
UserDiscoveryShares,
|
UserDiscoveryShares,
|
||||||
Contacts,
|
Contacts,
|
||||||
],
|
],
|
||||||
|
|
@ -127,6 +129,8 @@ class UserDiscoveryDao extends DatabaseAccessor<TwonlyDB>
|
||||||
results[user]!.add(relationData);
|
results[user]!.add(relationData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Log.info('results = ${results.length}');
|
||||||
|
|
||||||
return results;
|
return results;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -223,4 +227,19 @@ class UserDiscoveryDao extends DatabaseAccessor<TwonlyDB>
|
||||||
updatedValues,
|
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;
|
attachedDatabase.userDiscoveryUserRelations;
|
||||||
$UserDiscoveryOwnPromotionsTable get userDiscoveryOwnPromotions =>
|
$UserDiscoveryOwnPromotionsTable get userDiscoveryOwnPromotions =>
|
||||||
attachedDatabase.userDiscoveryOwnPromotions;
|
attachedDatabase.userDiscoveryOwnPromotions;
|
||||||
|
$UserDiscoveryOtherPromotionsTable get userDiscoveryOtherPromotions =>
|
||||||
|
attachedDatabase.userDiscoveryOtherPromotions;
|
||||||
$UserDiscoverySharesTable get userDiscoveryShares =>
|
$UserDiscoverySharesTable get userDiscoveryShares =>
|
||||||
attachedDatabase.userDiscoveryShares;
|
attachedDatabase.userDiscoveryShares;
|
||||||
UserDiscoveryDaoManager get managers => UserDiscoveryDaoManager(this);
|
UserDiscoveryDaoManager get managers => UserDiscoveryDaoManager(this);
|
||||||
|
|
@ -39,6 +41,12 @@ class UserDiscoveryDaoManager {
|
||||||
_db.attachedDatabase,
|
_db.attachedDatabase,
|
||||||
_db.userDiscoveryOwnPromotions,
|
_db.userDiscoveryOwnPromotions,
|
||||||
);
|
);
|
||||||
|
$$UserDiscoveryOtherPromotionsTableTableManager
|
||||||
|
get userDiscoveryOtherPromotions =>
|
||||||
|
$$UserDiscoveryOtherPromotionsTableTableManager(
|
||||||
|
_db.attachedDatabase,
|
||||||
|
_db.userDiscoveryOtherPromotions,
|
||||||
|
);
|
||||||
$$UserDiscoverySharesTableTableManager get userDiscoveryShares =>
|
$$UserDiscoverySharesTableTableManager get userDiscoveryShares =>
|
||||||
$$UserDiscoverySharesTableTableManager(
|
$$UserDiscoverySharesTableTableManager(
|
||||||
_db.attachedDatabase,
|
_db.attachedDatabase,
|
||||||
|
|
|
||||||
|
|
@ -131,9 +131,14 @@ class UserDiscoveryService {
|
||||||
List<Uint8List> messages,
|
List<Uint8List> messages,
|
||||||
) async {
|
) async {
|
||||||
try {
|
try {
|
||||||
|
final verifications = await twonlyDB.keyVerificationDao
|
||||||
|
.getContactVerification(fromUserId);
|
||||||
|
|
||||||
return await FlutterUserDiscovery.handleNewMessages(
|
return await FlutterUserDiscovery.handleNewMessages(
|
||||||
contactId: fromUserId,
|
contactId: fromUserId,
|
||||||
messages: messages,
|
messages: messages,
|
||||||
|
publicKeyVerifiedTimestamp:
|
||||||
|
verifications.lastOrNull?.createdAt.millisecondsSinceEpoch,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Log.error(e);
|
Log.error(e);
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,11 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/constants/routes.keys.dart';
|
import 'package:twonly/src/constants/routes.keys.dart';
|
||||||
|
import 'package:twonly/src/database/daos/key_verification.dao.dart';
|
||||||
import 'package:twonly/src/database/twonly.db.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/elements/svg_icon.element.dart';
|
||||||
|
import 'package:twonly/src/visual/views/settings/help/faq/verification_bade_faq.view.dart';
|
||||||
|
|
||||||
class VerificationBadgeComp extends StatefulWidget {
|
class VerificationBadgeComp extends StatefulWidget {
|
||||||
const VerificationBadgeComp({
|
const VerificationBadgeComp({
|
||||||
|
|
@ -29,9 +32,11 @@ class VerificationBadgeComp extends StatefulWidget {
|
||||||
|
|
||||||
class _VerificationBadgeCompState extends State<VerificationBadgeComp> {
|
class _VerificationBadgeCompState extends State<VerificationBadgeComp> {
|
||||||
bool _isVerified = false;
|
bool _isVerified = false;
|
||||||
|
bool _isVerifiedByTransferredTrust = false;
|
||||||
|
|
||||||
StreamSubscription<bool>? _streamAllVerified;
|
StreamSubscription<VerificationStatus>? _streamAllVerified;
|
||||||
StreamSubscription<List<KeyVerification>>? _streamContactVerification;
|
StreamSubscription<List<KeyVerification>>? _streamContactVerification;
|
||||||
|
StreamSubscription<List<(Contact, DateTime)>>? _streamTransferredTrust;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
|
@ -42,7 +47,14 @@ class _VerificationBadgeCompState extends State<VerificationBadgeComp> {
|
||||||
.listen((update) {
|
.listen((update) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
_isVerified = update;
|
_isVerified = false;
|
||||||
|
_isVerifiedByTransferredTrust = false;
|
||||||
|
if (update == VerificationStatus.trusted) {
|
||||||
|
_isVerified = true;
|
||||||
|
}
|
||||||
|
if (update == VerificationStatus.partialTrusted) {
|
||||||
|
_isVerifiedByTransferredTrust = true;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else if (widget.contact != null) {
|
} else if (widget.contact != null) {
|
||||||
|
|
@ -51,9 +63,19 @@ class _VerificationBadgeCompState extends State<VerificationBadgeComp> {
|
||||||
.listen((update) {
|
.listen((update) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
|
Log.info('Update: ${update.length}');
|
||||||
_isVerified = update.isNotEmpty;
|
_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() {
|
void dispose() {
|
||||||
_streamAllVerified?.cancel();
|
_streamAllVerified?.cancel();
|
||||||
_streamContactVerification?.cancel();
|
_streamContactVerification?.cancel();
|
||||||
|
_streamTransferredTrust?.cancel();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -81,9 +104,12 @@ class _VerificationBadgeCompState extends State<VerificationBadgeComp> {
|
||||||
bottom: 3,
|
bottom: 3,
|
||||||
),
|
),
|
||||||
child: SvgIcon(
|
child: SvgIcon(
|
||||||
assetPath: _isVerified
|
assetPath: (_isVerified || _isVerifiedByTransferredTrust)
|
||||||
? SvgIcons.verifiedGreen
|
? SvgIcons.verifiedGreen
|
||||||
: SvgIcons.verifiedRed,
|
: SvgIcons.verifiedRed,
|
||||||
|
color: (_isVerifiedByTransferredTrust && !_isVerified)
|
||||||
|
? colorVerificationBadgeYellow
|
||||||
|
: null,
|
||||||
size: widget.size,
|
size: widget.size,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -122,6 +122,16 @@ class _ContactRowState extends State<_ContactRow> {
|
||||||
);
|
);
|
||||||
if (userdata == null) return;
|
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)) {
|
if (userdata.publicIdentityKey.equals(widget.contact.publicIdentityKey)) {
|
||||||
final verified = await twonlyDB.keyVerificationDao.isContactVerified(
|
final verified = await twonlyDB.keyVerificationDao.isContactVerified(
|
||||||
widget.message.senderId!,
|
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);
|
if (added > 0) await importSignalContactAndCreateRequest(userdata);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Log.error(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/daos/contacts.dao.dart';
|
||||||
import 'package:twonly/src/database/tables/contacts.table.dart';
|
import 'package:twonly/src/database/tables/contacts.table.dart';
|
||||||
import 'package:twonly/src/database/twonly.db.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/utils/misc.dart';
|
||||||
import 'package:twonly/src/visual/components/alert.dialog.dart';
|
import 'package:twonly/src/visual/components/alert.dialog.dart';
|
||||||
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
|
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
|
||||||
|
|
@ -34,15 +33,17 @@ class _ContactViewState extends State<ContactView> {
|
||||||
Contact? _contact;
|
Contact? _contact;
|
||||||
List<GroupMember> _memberOfGroups = [];
|
List<GroupMember> _memberOfGroups = [];
|
||||||
List<KeyVerification> _keyVerifications = [];
|
List<KeyVerification> _keyVerifications = [];
|
||||||
|
List<(Contact, DateTime)> _transferredTrust = [];
|
||||||
|
|
||||||
late StreamSubscription<Contact?> _contactSub;
|
late StreamSubscription<Contact?> _streamContact;
|
||||||
late StreamSubscription<List<GroupMember>> _groupMemberSub;
|
late StreamSubscription<List<GroupMember>> _streamMemberOfGroups;
|
||||||
late StreamSubscription<List<KeyVerification>> _streamKeyVerifications;
|
late StreamSubscription<List<KeyVerification>> _streamKeyVerifications;
|
||||||
|
late StreamSubscription<List<(Contact, DateTime)>> _streamTransferredTrust;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_contactSub = twonlyDB.contactsDao.watchContact(widget.userId).listen((
|
_streamContact = twonlyDB.contactsDao.watchContact(widget.userId).listen((
|
||||||
update,
|
update,
|
||||||
) {
|
) {
|
||||||
if (update != null) {
|
if (update != null) {
|
||||||
|
|
@ -51,26 +52,38 @@ class _ContactViewState extends State<ContactView> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
_groupMemberSub = twonlyDB.groupsDao
|
_streamMemberOfGroups = twonlyDB.groupsDao
|
||||||
.watchContactGroupMember(widget.userId)
|
.watchContactGroupMember(widget.userId)
|
||||||
.listen((groups) async {
|
.listen((groups) async {
|
||||||
_memberOfGroups = groups;
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_memberOfGroups = groups;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
_streamKeyVerifications = twonlyDB.keyVerificationDao
|
_streamKeyVerifications = twonlyDB.keyVerificationDao
|
||||||
.watchContactVerification(widget.userId)
|
.watchContactVerification(widget.userId)
|
||||||
.listen((update) {
|
.listen((update) {
|
||||||
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
Log.info('Verifications: ${update.length}');
|
|
||||||
_keyVerifications = update;
|
_keyVerifications = update;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
_streamTransferredTrust = twonlyDB.keyVerificationDao
|
||||||
|
.watchTransferredTrustVerifications(widget.userId)
|
||||||
|
.listen((update) {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_transferredTrust = update;
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_contactSub.cancel();
|
_streamContact.cancel();
|
||||||
_groupMemberSub.cancel();
|
_streamMemberOfGroups.cancel();
|
||||||
_streamKeyVerifications.cancel();
|
_streamKeyVerifications.cancel();
|
||||||
|
_streamTransferredTrust.cancel();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -175,7 +188,6 @@ class _ContactViewState extends State<ContactView> {
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(right: 10),
|
padding: const EdgeInsets.only(right: 10),
|
||||||
child: VerificationBadgeComp(
|
child: VerificationBadgeComp(
|
||||||
key: GlobalKey(),
|
|
||||||
contact: contact,
|
contact: contact,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -230,7 +242,7 @@ class _ContactViewState extends State<ContactView> {
|
||||||
RestoreFlameComp(
|
RestoreFlameComp(
|
||||||
contactId: widget.userId,
|
contactId: widget.userId,
|
||||||
),
|
),
|
||||||
if (_keyVerifications.isEmpty)
|
if (_keyVerifications.isEmpty && _transferredTrust.isEmpty)
|
||||||
BetterListTile(
|
BetterListTile(
|
||||||
leading: VerificationBadgeComp(
|
leading: VerificationBadgeComp(
|
||||||
contact: contact,
|
contact: contact,
|
||||||
|
|
@ -242,7 +254,7 @@ class _ContactViewState extends State<ContactView> {
|
||||||
setState(() {});
|
setState(() {});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (_keyVerifications.isNotEmpty)
|
if (_keyVerifications.isNotEmpty || _transferredTrust.isNotEmpty)
|
||||||
ExpansionTile(
|
ExpansionTile(
|
||||||
shape: const RoundedRectangleBorder(),
|
shape: const RoundedRectangleBorder(),
|
||||||
backgroundColor: context.color.surfaceContainer,
|
backgroundColor: context.color.surfaceContainer,
|
||||||
|
|
@ -255,23 +267,40 @@ class _ContactViewState extends State<ContactView> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
title: Text(context.lang.userVerifiedTitle),
|
title: Text(context.lang.userVerifiedTitle),
|
||||||
children: _keyVerifications
|
children: [
|
||||||
.map(
|
..._keyVerifications.map(
|
||||||
(kv) => ListTile(
|
(kv) => ListTile(
|
||||||
dense: true,
|
dense: true,
|
||||||
title: Text(_verificationTypeLabel(context, kv.type)),
|
title: Text(_verificationTypeLabel(context, kv.type)),
|
||||||
trailing: Text(
|
trailing: Text(
|
||||||
DateFormat.yMd(
|
DateFormat.yMd(
|
||||||
Localizations.localeOf(context).toString(),
|
Localizations.localeOf(context).toString(),
|
||||||
).format(kv.createdAt),
|
).format(kv.createdAt),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: context.color.onSurfaceVariant,
|
color: context.color.onSurfaceVariant,
|
||||||
fontSize: 13,
|
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)
|
if (userService.currentUser.isUserDiscoveryEnabled)
|
||||||
BetterListTile(
|
BetterListTile(
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,10 @@ import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/constants/routes.keys.dart';
|
import 'package:twonly/src/constants/routes.keys.dart';
|
||||||
import 'package:twonly/src/database/twonly.db.dart';
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
import 'package:twonly/src/services/user.service.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/utils/storage.dart';
|
||||||
import 'package:twonly/src/visual/components/alert.dialog.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 {
|
class DeveloperSettingsView extends StatefulWidget {
|
||||||
const DeveloperSettingsView({super.key});
|
const DeveloperSettingsView({super.key});
|
||||||
|
|
@ -56,12 +58,21 @@ class _DeveloperSettingsViewState extends State<DeveloperSettingsView> {
|
||||||
onChanged: (_) => toggleDeveloperSettings(),
|
onChanged: (_) => toggleDeveloperSettings(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
ListTile(
|
||||||
|
title: const Text('User ID'),
|
||||||
|
subtitle: Text(userService.currentUser.userId.toString()),
|
||||||
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: const Text('Show Retransmission Database'),
|
title: const Text('Show Retransmission Database'),
|
||||||
onTap: () => context.push(
|
onTap: () => context.push(
|
||||||
Routes.settingsDeveloperRetransmissionDatabase,
|
Routes.settingsDeveloperRetransmissionDatabase,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
ListTile(
|
||||||
|
title: const Text('Show User Discovery Database'),
|
||||||
|
onTap: () =>
|
||||||
|
context.navPush(const UserDiscoveryDeveloperView()),
|
||||||
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: const Text('Toggle Video Stabilization'),
|
title: const Text('Toggle Video Stabilization'),
|
||||||
onTap: toggleVideoStabilization,
|
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>>,
|
get_contact_version: (i64) => Option<Vec<u8>>,
|
||||||
set_contact_version: (i64, Vec<u8>) => bool,
|
set_contact_version: (i64, Vec<u8>) => bool,
|
||||||
push_new_user_relation: (i64, AnnouncedUser, Option<i64>) => 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,
|
version: u32,
|
||||||
promotion: Vec<u8>,
|
promotion: Vec<u8>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
(get_callbacks()?.user_discovery.push_own_promotion_and_clear_old_version)(contact_id, version as i64, promotion)
|
(get_callbacks()?
|
||||||
.await
|
.user_discovery
|
||||||
.then_some(())
|
.push_own_promotion_and_clear_old_version)(contact_id, version as i64, promotion)
|
||||||
.ok_or(TwonlyError::DartError.into())
|
.await
|
||||||
|
.then_some(())
|
||||||
|
.ok_or(TwonlyError::DartError.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_own_promotions_after_version(&self, version: u32) -> Result<Vec<Vec<u8>>> {
|
async fn get_own_promotions_after_version(&self, version: u32) -> Result<Vec<Vec<u8>>> {
|
||||||
|
|
@ -166,4 +168,8 @@ impl UserDiscoveryStore for UserDiscoveryStoreFlutter {
|
||||||
.then_some(())
|
.then_some(())
|
||||||
.ok_or(TwonlyError::DartError.into())
|
.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?)
|
.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()?
|
Ok(get_twonly_flutter()?
|
||||||
.user_discovery
|
.user_discovery
|
||||||
.get()
|
.get()
|
||||||
.await
|
.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?)
|
.await?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ flutter_rust_bridge::frb_generated_boilerplate!(
|
||||||
default_rust_auto_opaque = RustAutoOpaqueMoi,
|
default_rust_auto_opaque = RustAutoOpaqueMoi,
|
||||||
);
|
);
|
||||||
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.12.0";
|
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
|
// 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 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 mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message);
|
||||||
let api_contact_id = <i64>::sse_decode(&mut deserializer);
|
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 {
|
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 {
|
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)
|
})().await)
|
||||||
} })
|
} })
|
||||||
}
|
}
|
||||||
|
|
@ -126,6 +127,22 @@ let api_version = <Vec<u8>>::sse_decode(&mut deserializer);deserializer.end(); m
|
||||||
})().await)
|
})().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(
|
fn wire__crate__bridge__callbacks__init_flutter_callbacks_impl(
|
||||||
port_: flutter_rust_bridge::for_generated::MessagePort,
|
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||||
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
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_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_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_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 || {
|
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),
|
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),
|
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),
|
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),
|
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__initialize_twonly_flutter_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!(),
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,12 @@ pub mod tests;
|
||||||
pub mod traits;
|
pub mod traits;
|
||||||
|
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
|
use std::sync::Arc;
|
||||||
use std::u8;
|
use std::u8;
|
||||||
use blahaj::{Share, Sharks};
|
use blahaj::{Share, Sharks};
|
||||||
use prost::Message;
|
use prost::Message;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tokio::sync::{Mutex, MutexGuard};
|
||||||
use crate::user_discovery::error::{Result, UserDiscoveryError};
|
use crate::user_discovery::error::{Result, UserDiscoveryError};
|
||||||
use crate::user_discovery::traits::{AnnouncedUser, OtherPromotion, UserDiscoveryUtils};
|
use crate::user_discovery::traits::{AnnouncedUser, OtherPromotion, UserDiscoveryUtils};
|
||||||
use crate::user_discovery::user_discovery_message::{UserDiscoveryAnnouncement, UserDiscoveryPromotion};
|
use crate::user_discovery::user_discovery_message::{UserDiscoveryAnnouncement, UserDiscoveryPromotion};
|
||||||
|
|
@ -52,12 +54,17 @@ where
|
||||||
{
|
{
|
||||||
store: Store,
|
store: Store,
|
||||||
utils: Utils,
|
utils: Utils,
|
||||||
|
config_lock: Arc<Mutex<bool>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<Store: UserDiscoveryStore, Utils: UserDiscoveryUtils> UserDiscovery<Store, Utils> {
|
impl<Store: UserDiscoveryStore, Utils: UserDiscoveryUtils> UserDiscovery<Store, Utils> {
|
||||||
/// Creates a new instance of the user discovery.
|
/// Creates a new instance of the user discovery.
|
||||||
pub fn new(store: Store, utils: Utils) -> Result<Self> {
|
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.
|
/// Initializes or updates the user discovery.
|
||||||
|
|
@ -81,6 +88,7 @@ impl<Store: UserDiscoveryStore, Utils: UserDiscoveryUtils> UserDiscovery<Store,
|
||||||
user_id: UserID,
|
user_id: UserID,
|
||||||
public_key: Vec<u8>,
|
public_key: Vec<u8>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
let config_lock = self.config_lock.lock().await;
|
||||||
let mut config = match self.store.get_config().await {
|
let mut config = match self.store.get_config().await {
|
||||||
Ok(config) => {
|
Ok(config) => {
|
||||||
let mut config: UserDiscoveryConfig = serde_json::from_str(&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.announcement_version += 1;
|
||||||
config.verification_shares = verification_shares;
|
config.verification_shares = verification_shares;
|
||||||
|
|
||||||
self.store
|
self.update_config(config, config_lock).await?;
|
||||||
.update_config(serde_json::to_string_pretty(&config)?)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -138,7 +144,7 @@ impl<Store: UserDiscoveryStore, Utils: UserDiscoveryUtils> UserDiscovery<Store,
|
||||||
/// * `Err(UserDiscoveryError)` - If there where errors in the store.
|
/// * `Err(UserDiscoveryError)` - If there where errors in the store.
|
||||||
///
|
///
|
||||||
pub async fn get_current_version(&self) -> Result<Vec<u8>> {
|
pub async fn get_current_version(&self) -> Result<Vec<u8>> {
|
||||||
let config = self.get_config().await?;
|
let (config, _) = self.get_config().await?;
|
||||||
Ok(UserDiscoveryVersion {
|
Ok(UserDiscoveryVersion {
|
||||||
announcement: config.announcement_version,
|
announcement: config.announcement_version,
|
||||||
promotion: config.promotion_version,
|
promotion: config.promotion_version,
|
||||||
|
|
@ -181,7 +187,7 @@ impl<Store: UserDiscoveryStore, Utils: UserDiscoveryUtils> UserDiscovery<Store,
|
||||||
let mut messages = vec![];
|
let mut messages = vec![];
|
||||||
let received_version = UserDiscoveryVersion::decode(received_version)?;
|
let received_version = UserDiscoveryVersion::decode(received_version)?;
|
||||||
|
|
||||||
let config = self.get_config().await?;
|
let (config, _) = self.get_config().await?;
|
||||||
let version = Some(UserDiscoveryVersion {
|
let version = Some(UserDiscoveryVersion {
|
||||||
announcement: config.announcement_version,
|
announcement: config.announcement_version,
|
||||||
promotion: config.promotion_version,
|
promotion: config.promotion_version,
|
||||||
|
|
@ -271,6 +277,7 @@ impl<Store: UserDiscoveryStore, Utils: UserDiscoveryUtils> UserDiscovery<Store,
|
||||||
pub async fn handle_new_messages(
|
pub async fn handle_new_messages(
|
||||||
&self,
|
&self,
|
||||||
contact_id: UserID,
|
contact_id: UserID,
|
||||||
|
public_key_verified_timestamp: Option<i64>,
|
||||||
messages: Vec<Vec<u8>>,
|
messages: Vec<Vec<u8>>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
for message in messages {
|
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 Some(uda) = message.user_discovery_announcement {
|
||||||
if let Err(err) = self
|
if let Err(err) = self
|
||||||
.handle_user_discovery_announcement(contact_id, uda)
|
.handle_user_discovery_announcement(
|
||||||
|
contact_id,
|
||||||
|
public_key_verified_timestamp,
|
||||||
|
uda,
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
tracing::warn!("Ignoring: {err}");
|
tracing::warn!("Ignoring: {err}");
|
||||||
|
|
@ -303,6 +314,54 @@ impl<Store: UserDiscoveryStore, Utils: UserDiscoveryUtils> UserDiscovery<Store,
|
||||||
Ok(())
|
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(
|
async fn setup_announcements(
|
||||||
&self,
|
&self,
|
||||||
config: &UserDiscoveryConfig,
|
config: &UserDiscoveryConfig,
|
||||||
|
|
@ -354,13 +413,28 @@ impl<Store: UserDiscoveryStore, Utils: UserDiscoveryUtils> UserDiscovery<Store,
|
||||||
Ok(verification_shares)
|
Ok(verification_shares)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_config(&self) -> Result<UserDiscoveryConfig> {
|
async fn get_config(&self) -> Result<(UserDiscoveryConfig, MutexGuard<'_, bool>)> {
|
||||||
Ok(serde_json::from_str(&self.store.get_config().await?)?)
|
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(
|
async fn handle_user_discovery_announcement(
|
||||||
&self,
|
&self,
|
||||||
contact_id: UserID,
|
contact_id: UserID,
|
||||||
|
public_key_verified_timestamp: Option<i64>,
|
||||||
uda: UserDiscoveryAnnouncement,
|
uda: UserDiscoveryAnnouncement,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
tracing::info!("Got a user discovery announcement from {contact_id}.");
|
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, config_lock) = self.get_config().await?;
|
||||||
let mut config = self.get_config().await?;
|
|
||||||
config.promotion_version += 1;
|
config.promotion_version += 1;
|
||||||
self.store
|
|
||||||
.update_config(serde_json::to_string_pretty(&config)?)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let message = UserDiscoveryMessage {
|
let message = UserDiscoveryMessage {
|
||||||
version: Some(UserDiscoveryVersion {
|
version: Some(UserDiscoveryVersion {
|
||||||
|
|
@ -441,7 +511,7 @@ impl<Store: UserDiscoveryStore, Utils: UserDiscoveryUtils> UserDiscovery<Store,
|
||||||
public_id: signed_data.public_id,
|
public_id: signed_data.public_id,
|
||||||
threshold: uda.threshold,
|
threshold: uda.threshold,
|
||||||
announcement_share: uda.announcement_share,
|
announcement_share: uda.announcement_share,
|
||||||
public_key_verified_timestamp: None,
|
public_key_verified_timestamp,
|
||||||
}),
|
}),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
@ -454,6 +524,8 @@ impl<Store: UserDiscoveryStore, Utils: UserDiscoveryUtils> UserDiscovery<Store,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
self.update_config(config, config_lock).await?;
|
||||||
|
|
||||||
let announced_user = AnnouncedUser {
|
let announced_user = AnnouncedUser {
|
||||||
user_id: signed_data.user_id,
|
user_id: signed_data.user_id,
|
||||||
public_key: signed_data.public_key,
|
public_key: signed_data.public_key,
|
||||||
|
|
@ -470,7 +542,7 @@ impl<Store: UserDiscoveryStore, Utils: UserDiscoveryUtils> UserDiscovery<Store,
|
||||||
.push_new_user_relation(
|
.push_new_user_relation(
|
||||||
contact_id,
|
contact_id,
|
||||||
announced_user,
|
announced_user,
|
||||||
None, // This flag mus be handled by the applications as this comes from an announcement.
|
public_key_verified_timestamp,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
|
@ -587,7 +659,9 @@ impl<Store: UserDiscoveryStore, Utils: UserDiscoveryUtils> UserDiscovery<Store,
|
||||||
public_id: udp.public_id,
|
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 {
|
for promotion in unique_promotions {
|
||||||
// Do not store the announcement of the users itself.
|
// Do not store the announcement of the users itself.
|
||||||
// Or in case the promotion promotes myself
|
// Or in case the promotion promotes myself
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ pub(crate) struct Storage {
|
||||||
unused_shares: Vec<Vec<u8>>,
|
unused_shares: Vec<Vec<u8>>,
|
||||||
used_shares: HashMap<UserID, Vec<u8>>,
|
used_shares: HashMap<UserID, Vec<u8>>,
|
||||||
contact_versions: 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>)>>,
|
announced_users: HashMap<AnnouncedUser, Vec<(UserID, Option<i64>)>>,
|
||||||
own_promotions: Vec<(UserID, Vec<u8>)>,
|
own_promotions: Vec<(UserID, Vec<u8>)>,
|
||||||
}
|
}
|
||||||
|
|
@ -97,8 +97,23 @@ impl UserDiscoveryStore for InMemoryStore {
|
||||||
Ok(elements)
|
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<()> {
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -109,9 +124,9 @@ impl UserDiscoveryStore for InMemoryStore {
|
||||||
Ok(self
|
Ok(self
|
||||||
.storage()
|
.storage()
|
||||||
.other_promotions
|
.other_promotions
|
||||||
.iter()
|
.clone()
|
||||||
|
.into_values()
|
||||||
.filter(|other| other.public_id == public_id)
|
.filter(|other| other.public_id == public_id)
|
||||||
.map(OtherPromotion::to_owned)
|
|
||||||
.collect())
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -145,7 +160,11 @@ impl UserDiscoveryStore for InMemoryStore {
|
||||||
.entry(announced_user.clone())
|
.entry(announced_user.clone())
|
||||||
.or_insert(vec![]);
|
.or_insert(vec![]);
|
||||||
if announced_user.user_id != from_contact_id {
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -282,6 +282,40 @@ async fn test_user_discovery_in_memory_store() {
|
||||||
step0_exchange_in_order::<InMemoryStore>(&users).await;
|
step0_exchange_in_order::<InMemoryStore>(&users).await;
|
||||||
step1_verify_no_new_messages::<InMemoryStore>(&users).await;
|
step1_verify_no_new_messages::<InMemoryStore>(&users).await;
|
||||||
step2_verify_announced_users_expected::<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]
|
#[tokio::test]
|
||||||
|
|
@ -441,7 +475,7 @@ async fn request_and_handle_messages<S: UserDiscoveryStore>(
|
||||||
assert!(new_messages.len() <= messages_count);
|
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
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@ pub trait UserDiscoveryStore {
|
||||||
&self,
|
&self,
|
||||||
promotion: OtherPromotion,
|
promotion: OtherPromotion,
|
||||||
) -> impl Future<Output = Result<()>> + Send;
|
) -> impl Future<Output = Result<()>> + Send;
|
||||||
|
|
||||||
fn get_other_promotions_by_public_id(
|
fn get_other_promotions_by_public_id(
|
||||||
&self,
|
&self,
|
||||||
public_id: i64,
|
public_id: i64,
|
||||||
|
|
@ -68,10 +69,16 @@ pub trait UserDiscoveryStore {
|
||||||
&self,
|
&self,
|
||||||
) -> impl Future<Output = Result<HashMap<AnnouncedUser, Vec<(UserID, Option<i64>)>>>> + Send;
|
) -> 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(
|
fn get_contact_version(
|
||||||
&self,
|
&self,
|
||||||
contact_id: UserID,
|
contact_id: UserID,
|
||||||
) -> impl Future<Output = Result<Option<Vec<u8>>>> + Send;
|
) -> impl Future<Output = Result<Option<Vec<u8>>>> + Send;
|
||||||
|
|
||||||
fn set_contact_version(
|
fn set_contact_version(
|
||||||
&self,
|
&self,
|
||||||
contact_id: UserID,
|
contact_id: UserID,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue