mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-10-09 02:54:14 +00:00
365 lines
12 KiB
Dart
365 lines
12 KiB
Dart
import 'package:clock/clock.dart';
|
|
import 'package:drift/drift.dart';
|
|
import 'package:twonly/core/bridge/wrapper/user_discovery.dart';
|
|
import 'package:twonly/globals.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';
|
|
import 'package:twonly/src/utils/log.dart';
|
|
|
|
part 'key_verification.dao.g.dart';
|
|
|
|
enum VerificationStatus { trusted, partialTrusted, notTrusted }
|
|
|
|
@DriftAccessor(
|
|
tables: [
|
|
Contacts,
|
|
VerificationTokens,
|
|
KeyVerifications,
|
|
GroupMembers,
|
|
UserDiscoveryUserRelations,
|
|
],
|
|
)
|
|
class KeyVerificationDao extends DatabaseAccessor<TwonlyDB>
|
|
with _$KeyVerificationDaoMixin {
|
|
// ignore: matching_super_parameters
|
|
KeyVerificationDao(super.db);
|
|
|
|
Future<List<VerificationToken>> getRecentVerificationTokens() {
|
|
// Tokens are only valid for one hour, so if the users are currently offline, the verification notification will still work later.
|
|
final cutoff = DateTime.now().subtract(const Duration(hours: 1));
|
|
return (select(
|
|
verificationTokens,
|
|
)..where((t) => t.createdAt.isBiggerOrEqualValue(cutoff))).get();
|
|
}
|
|
|
|
Future<int> insertVerificationToken(Uint8List token) {
|
|
return into(verificationTokens).insert(
|
|
VerificationTokensCompanion.insert(token: token),
|
|
);
|
|
}
|
|
|
|
/// Returns a map of contactId → the verification type of the earliest
|
|
/// [KeyVerification] row for that contact.
|
|
Future<Map<int, VerificationType>>
|
|
getFirstVerificationTypeByContacts() async {
|
|
final rows = await (select(
|
|
keyVerifications,
|
|
)..orderBy([(kv) => OrderingTerm.asc(kv.createdAt)])).get();
|
|
|
|
final result = <int, VerificationType>{};
|
|
for (final row in rows) {
|
|
result.putIfAbsent(row.contactId, () => row.type);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
Future<bool> isContactVerified(int contactId) async {
|
|
final verifierKv = alias(keyVerifications, 'verifierKv');
|
|
final query = select(keyVerifications).join([
|
|
leftOuterJoin(
|
|
verifierKv,
|
|
verifierKv.contactId.equalsExp(keyVerifications.verifiedBy),
|
|
),
|
|
])..where(keyVerifications.contactId.equals(contactId));
|
|
|
|
final rows = await query.get();
|
|
for (final row in rows) {
|
|
final kv = row.readTable(keyVerifications);
|
|
final hasVerifierKv = row.readTableOrNull(verifierKv) != null;
|
|
if (kv.type == VerificationType.contactSharedByVerified) {
|
|
if (hasVerifierKv) return true;
|
|
} else {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
Stream<List<(KeyVerification, Contact?)>> watchContactVerification(
|
|
int contactId,
|
|
) {
|
|
final verifier = alias(contacts, 'verifier');
|
|
final verifierKv = alias(keyVerifications, 'verifierKv');
|
|
final query = select(keyVerifications).join([
|
|
leftOuterJoin(
|
|
verifier,
|
|
verifier.userId.equalsExp(keyVerifications.verifiedBy),
|
|
),
|
|
leftOuterJoin(
|
|
verifierKv,
|
|
verifierKv.contactId.equalsExp(keyVerifications.verifiedBy),
|
|
),
|
|
])..where(keyVerifications.contactId.equals(contactId));
|
|
|
|
return query.watch().map((rows) {
|
|
final uniqueKvs =
|
|
<int, (KeyVerification, Contact?, bool isVerifierVerified)>{};
|
|
|
|
for (final row in rows) {
|
|
final kv = row.readTable(keyVerifications);
|
|
final contact = row.readTableOrNull(verifier);
|
|
final hasVerifierKv = row.readTableOrNull(verifierKv) != null;
|
|
|
|
final existing = uniqueKvs[kv.verificationId];
|
|
if (existing == null || hasVerifierKv) {
|
|
uniqueKvs[kv.verificationId] = (kv, contact, hasVerifierKv);
|
|
}
|
|
}
|
|
|
|
return uniqueKvs.values
|
|
.where((item) {
|
|
final kv = item.$1;
|
|
final isVerifierVerified = item.$3;
|
|
if (kv.type == VerificationType.contactSharedByVerified) {
|
|
return isVerifierVerified;
|
|
}
|
|
return true;
|
|
})
|
|
.map((item) => (item.$1, item.$2))
|
|
.toList();
|
|
});
|
|
}
|
|
|
|
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(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(),
|
|
)
|
|
..groupBy([contacts.userId]);
|
|
|
|
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;
|
|
});
|
|
}
|
|
|
|
Stream<Map<String, VerificationStatus>> watchAllGroupsVerificationStatus() {
|
|
final gm = groupMembers;
|
|
final directKv = alias(keyVerifications, 'allGroupsDirectKv');
|
|
final ur = userDiscoveryUserRelations;
|
|
final verifierKv = alias(keyVerifications, 'allGroupsVerifierKv');
|
|
|
|
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),
|
|
),
|
|
]);
|
|
|
|
return query.watch().map((rows) {
|
|
final groups = <String, Map<int, ({bool direct, bool partial})>>{};
|
|
for (final row in rows) {
|
|
final member = row.readTable(gm);
|
|
final members = groups.putIfAbsent(member.groupId, () => {});
|
|
final current =
|
|
members[member.contactId] ?? (direct: false, partial: false);
|
|
members[member.contactId] = (
|
|
direct: current.direct || row.readTableOrNull(directKv) != null,
|
|
partial: current.partial || row.readTableOrNull(verifierKv) != null,
|
|
);
|
|
}
|
|
return {
|
|
for (final entry in groups.entries)
|
|
entry.key: entry.value.values.every((member) => member.direct)
|
|
? VerificationStatus.trusted
|
|
: entry.value.values.every(
|
|
(member) => member.direct || member.partial,
|
|
)
|
|
? VerificationStatus.partialTrusted
|
|
: VerificationStatus.notTrusted,
|
|
};
|
|
});
|
|
}
|
|
|
|
Stream<int> watchUnverifiedGroupMembersCount(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 0;
|
|
|
|
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,
|
|
);
|
|
}
|
|
|
|
var count = 0;
|
|
for (final trust in memberTrustMap.values) {
|
|
if (!trust.direct && !trust.partial) {
|
|
count++;
|
|
}
|
|
}
|
|
return count;
|
|
});
|
|
}
|
|
|
|
Future<void> addKeyVerification(
|
|
int contactId,
|
|
VerificationType type, {
|
|
int? verifiedBy,
|
|
}) async {
|
|
try {
|
|
await into(keyVerifications).insertOnConflictUpdate(
|
|
KeyVerificationsCompanion(
|
|
contactId: Value(contactId),
|
|
type: Value(type),
|
|
verifiedBy: Value(verifiedBy),
|
|
),
|
|
);
|
|
if (userService.currentUser.isUserDiscoveryEnabled) {
|
|
await FlutterUserDiscovery.updateVerificationStateForUser(
|
|
callbackId: isolateCallbackId,
|
|
contactId: contactId,
|
|
publicKeyVerifiedTimestamp: clock.now().millisecondsSinceEpoch,
|
|
);
|
|
}
|
|
} catch (e) {
|
|
Log.error(e);
|
|
}
|
|
}
|
|
|
|
Future<void> deleteKeyVerification(int contactId) async {
|
|
try {
|
|
await (delete(
|
|
keyVerifications,
|
|
)..where((kv) => kv.contactId.equals(contactId))).go();
|
|
if (userService.currentUser.isUserDiscoveryEnabled) {
|
|
await FlutterUserDiscovery.updateVerificationStateForUser(
|
|
callbackId: isolateCallbackId,
|
|
contactId: contactId,
|
|
);
|
|
}
|
|
} catch (e) {
|
|
Log.error(e);
|
|
}
|
|
}
|
|
|
|
Future<void> deleteKeyVerificationById(
|
|
int verificationId,
|
|
int contactId,
|
|
) async {
|
|
try {
|
|
await (delete(
|
|
keyVerifications,
|
|
)..where((kv) => kv.verificationId.equals(verificationId))).go();
|
|
final remaining = await getContactVerification(contactId);
|
|
if (remaining.isEmpty && userService.currentUser.isUserDiscoveryEnabled) {
|
|
await FlutterUserDiscovery.updateVerificationStateForUser(
|
|
callbackId: isolateCallbackId,
|
|
contactId: contactId,
|
|
);
|
|
}
|
|
} catch (e) {
|
|
Log.error(e);
|
|
}
|
|
}
|
|
}
|