twonly-app/lib/src/callbacks/user_discovery.callbacks.dart
2026-04-21 17:29:01 +02:00

295 lines
8.9 KiB
Dart

import 'package:collection/collection.dart';
import 'package:drift/drift.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'
show Curve, IdentityKey;
// ignore: implementation_imports
import 'package:libsignal_protocol_dart/src/ecc/ed25519.dart';
import 'package:twonly/core/bridge.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/signal/identity.signal.dart';
import 'package:twonly/src/services/signal/session.signal.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
class UserDiscoveryCallbacks {
static Future<Uint8List?> signData(
Uint8List inputData,
) async {
var privKey = (await getSignalIdentityKeyPair())?.getPrivateKey();
if (privKey == null) return null;
final random = getRandomUint8List(32);
final signature = sign(
privKey.serialize(),
inputData,
random,
);
privKey = null;
return signature;
}
static Future<bool> verifySignature(
Uint8List inputData,
Uint8List pubKey,
Uint8List signature,
) async {
return Curve.verifySignature(
IdentityKey.fromBytes(pubKey, 0).publicKey,
inputData,
signature,
);
}
static Future<bool> verifyStoredPubKey(
int contactId,
Uint8List pubKey,
) async {
final storedPublicKey = await getPublicKeyFromContact(contactId);
if (storedPublicKey != null) {
return storedPublicKey.equals(pubKey);
} else {
return false;
}
}
static Future<bool> setShares(List<Uint8List> shares) async {
try {
// First remove all old shares then insert all the new shares
await twonlyDB.delete(twonlyDB.userDiscoveryShares).go();
await twonlyDB.batch((b) {
b.insertAll(
twonlyDB.userDiscoveryShares,
shares
.map((s) => UserDiscoverySharesCompanion(share: Value(s)))
.toList(),
);
});
return true;
} catch (e) {
Log.error(e);
return false;
}
}
static Future<Uint8List?> userDiscoveryGetShareForContact(
int contactId,
) async {
return twonlyDB.transaction(() async {
// 1. Check if this contact already has a share assigned
final existing =
await (twonlyDB.select(twonlyDB.userDiscoveryShares)
..where((tbl) => tbl.contactId.equals(contactId))
..limit(1))
.getSingleOrNull();
if (existing != null) {
return existing.share;
}
// 2. No share found. Find an available one (where contactId is null)
final available =
await (twonlyDB.select(twonlyDB.userDiscoveryShares)
..where((tbl) => tbl.contactId.isNull())
..limit(1))
.getSingleOrNull();
if (available != null) {
// 3. Assign the contactId to this available share
await (twonlyDB.update(
twonlyDB.userDiscoveryShares,
)..where((tbl) => tbl.shareId.equals(available.shareId))).write(
UserDiscoverySharesCompanion(
contactId: Value(contactId),
),
);
return available.share;
}
return null; // 4. No existing or available shares found
});
}
static Future<bool> pushOwnPromotion(
int contactId,
int version, // Maps to versionId or logic control
Uint8List promotion,
) async {
try {
await twonlyDB
.into(twonlyDB.userDiscoveryOwnPromotions)
.insert(
UserDiscoveryOwnPromotionsCompanion.insert(
contactId: contactId,
promotion: promotion,
),
);
return true;
} catch (e) {
Log.error(e);
return false;
}
}
static Future<List<Uint8List>> getOwnPromotionsAfterVersion(
int version,
) async {
final query = twonlyDB.select(twonlyDB.userDiscoveryOwnPromotions)
..where((tbl) => tbl.versionId.isBiggerThanValue(version));
final rows = await query.get();
return rows.map((r) => r.promotion).toList();
}
static Future<bool> storeOtherPromotion(
OtherPromotion promotion,
) async {
try {
await twonlyDB
.into(twonlyDB.userDiscoveryOtherPromotions)
.insertOnConflictUpdate(
UserDiscoveryOtherPromotionsCompanion(
promotionId: Value(promotion.promotionId),
publicId: Value(promotion.publicId),
fromContactId: Value(promotion.fromContactId),
threshold: Value(promotion.threshold),
announcementShare: Value(promotion.announcementShare),
publicKeyVerifiedTimestamp: Value(
promotion.publicKeyVerifiedTimestamp == null
? null
: DateTime.fromMillisecondsSinceEpoch(
promotion.publicKeyVerifiedTimestamp!,
),
),
),
);
return true;
} catch (e) {
Log.error(e);
return false;
}
}
static Future<List<OtherPromotion>> getOtherPromotionsByPublicId(
int publicId,
) async {
final rows = await (twonlyDB.select(
twonlyDB.userDiscoveryOtherPromotions,
)..where((tbl) => tbl.publicId.equals(publicId))).get();
return rows
.map(
(row) => OtherPromotion(
promotionId: row.promotionId,
publicId: row.publicId,
fromContactId: row.fromContactId,
threshold: row.threshold,
announcementShare: row.announcementShare,
publicKeyVerifiedTimestamp:
row.publicKeyVerifiedTimestamp?.millisecondsSinceEpoch,
),
)
.toList();
}
static Future<AnnouncedUser?> getAnnouncedUserByPublicId(
int publicId,
) async {
final row = await (twonlyDB.select(
twonlyDB.userDiscoveryAnnouncedUsers,
)..where((tbl) => tbl.publicId.equals(publicId))).getSingleOrNull();
if (row == null) return null;
return AnnouncedUser(
userId: row.announcedUserId,
publicKey: row.announcedPublicKey,
publicId: row.publicId,
);
}
static Future<bool> pushNewUserRelation(
int fromContactId,
AnnouncedUser announcedUser,
int? publicKeyVerifiedTimestamp,
) async {
try {
await twonlyDB.transaction(() async {
// 1. Ensure the user exists in the AnnouncedUsers table
await twonlyDB
.into(twonlyDB.userDiscoveryAnnouncedUsers)
.insertOnConflictUpdate(
UserDiscoveryAnnouncedUsersCompanion(
announcedUserId: Value(announcedUser.userId),
announcedPublicKey: Value(announcedUser.publicKey),
publicId: Value(announcedUser.publicId),
),
);
// 2. Insert or update the relation
await twonlyDB
.into(twonlyDB.userDiscoveryUserRelations)
.insertOnConflictUpdate(
UserDiscoveryUserRelationsCompanion.insert(
announcedUserId: announcedUser.userId,
fromContactId: fromContactId,
publicKeyVerifiedTimestamp: Value(
publicKeyVerifiedTimestamp != null
? DateTime.fromMillisecondsSinceEpoch(
publicKeyVerifiedTimestamp,
)
: null,
),
),
);
});
return true;
} catch (e) {
Log.error(e);
return false;
}
}
// static Future<Map<AnnouncedUser, List<(int, DateTime?)>>>
// getAllAnnouncedUsers() async {
// final query = twonlyDB.select(twonlyDB.userDiscoveryAnnouncedUsers).join([
// innerJoin(
// twonlyDB.userDiscoveryUserRelations,
// twonlyDB.userDiscoveryUserRelations.announcedUserId.equalsExp(
// twonlyDB.userDiscoveryAnnouncedUsers.announcedUserId,
// ),
// ),
// ]);
// final results = await query.get();
// final map = <UserDiscoveryAnnouncedUser, List<(int, DateTime?)>>{};
// for (final row in results) {
// final user = row.readTable(twonlyDB.userDiscoveryAnnouncedUsers);
// final relation = row.readTable(twonlyDB.userDiscoveryUserRelations);
// map.putIfAbsent(user, () => []).add(
// (relation.fromContactId, relation.publicKeyVerifiedTimestamp),
// );
// }
// return map;
// }
static Future<Uint8List?> getContactVersion(int contactId) async {
final row = await (twonlyDB.select(
twonlyDB.contacts,
)..where((tbl) => tbl.userId.equals(contactId))).getSingleOrNull();
return row?.userDiscoveryVersion;
}
static Future<bool> setContactVersion(int contactId, Uint8List update) async {
try {
await (twonlyDB.update(twonlyDB.contacts)
..where((tbl) => tbl.userId.equals(contactId)))
.write(ContactsCompanion(userDiscoveryVersion: Value(update)));
return true;
} catch (e) {
Log.error(e);
return false;
}
}
}