A network-wide 'N people vouch' count invites spam and means little. Now the
trust banner computes whether the peer is within YOUR circle — you vouch for
them, or someone you vouch for does (friend-of-a-friend) — via the pure
WebOfTrust rule (seeds={you}, threshold 1, distance 2). When known, the banner
turns green with 'In your circle' (else it falls back to the count).
The full member policy (threshold/distance/bootstrap) stays open (network-trust
§2); this is a deliberately loose 'people near you' heuristic.
Analyzer clean; trust cubit test adds a friend-of-a-friend vs stranger case
(run flutter test locally to confirm — the widget suite is too slow to run here).
150 lines
4.5 KiB
Dart
150 lines
4.5 KiB
Dart
import 'package:commons_core/commons_core.dart';
|
|
import 'package:equatable/equatable.dart';
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
|
|
import '../services/social_service.dart';
|
|
import '../services/social_settings.dart';
|
|
|
|
/// Trust standing of one peer: how many people vouch for them, and whether YOU
|
|
/// do. Kept simple on purpose — the full "known member" rule (threshold +
|
|
/// distance) and its bootstrap set are a policy decision still open
|
|
/// (network-trust.md §2). Vouching + a live count is the useful, honest first
|
|
/// step.
|
|
class TrustState extends Equatable {
|
|
const TrustState({
|
|
this.certifierCount = 0,
|
|
this.iVouch = false,
|
|
this.knownToYou = false,
|
|
this.loading = true,
|
|
this.busy = false,
|
|
});
|
|
|
|
/// How many people (network-wide) vouch for the peer.
|
|
final int certifierCount;
|
|
|
|
/// Whether this user vouches for the peer.
|
|
final bool iVouch;
|
|
|
|
/// Whether the peer is within the user's own circle of trust — you vouch for
|
|
/// them, or someone you vouch for does (friend-of-a-friend). This is the
|
|
/// meaningful signal; a raw network count invites spam.
|
|
final bool knownToYou;
|
|
|
|
final bool loading;
|
|
final bool busy;
|
|
|
|
TrustState copyWith({
|
|
int? certifierCount,
|
|
bool? iVouch,
|
|
bool? knownToYou,
|
|
bool? loading,
|
|
bool? busy,
|
|
}) =>
|
|
TrustState(
|
|
certifierCount: certifierCount ?? this.certifierCount,
|
|
iVouch: iVouch ?? this.iVouch,
|
|
knownToYou: knownToYou ?? this.knownToYou,
|
|
loading: loading ?? this.loading,
|
|
busy: busy ?? this.busy,
|
|
);
|
|
|
|
@override
|
|
List<Object?> get props =>
|
|
[certifierCount, iVouch, knownToYou, loading, busy];
|
|
}
|
|
|
|
/// Reads and toggles this user's vouch for [peerPubkey] over a [TrustTransport].
|
|
class TrustCubit extends Cubit<TrustState> {
|
|
TrustCubit(
|
|
this._transport, {
|
|
required this.peerPubkey,
|
|
required this.selfPubkey,
|
|
Future<void> Function()? onDispose,
|
|
}) : _onDispose = onDispose,
|
|
super(const TrustState());
|
|
|
|
final TrustTransport? _transport;
|
|
final String peerPubkey;
|
|
final String selfPubkey;
|
|
final Future<void> Function()? _onDispose;
|
|
|
|
bool get isOnline => _transport != null;
|
|
|
|
/// Bootstrap "circle" rule: one vouch from your side, out to a friend-of-a-
|
|
/// friend. Deliberately loose (the full member policy — threshold/distance —
|
|
/// is still open, network-trust.md §2); this is just "people near you".
|
|
static const _circleThreshold = 1;
|
|
static const _circleDistance = 2;
|
|
|
|
/// Loads the peer's certifiers and whether they're within your circle.
|
|
Future<void> load() async {
|
|
final transport = _transport;
|
|
if (transport == null) {
|
|
emit(state.copyWith(loading: false));
|
|
return;
|
|
}
|
|
emit(state.copyWith(loading: true));
|
|
final wot = WebOfTrust.fromCertifications(
|
|
await transport.allCertifications(),
|
|
now: DateTime.now(),
|
|
);
|
|
final certifiers = wot.certifiersOf(peerPubkey);
|
|
final circle = wot.members(
|
|
seeds: {selfPubkey},
|
|
threshold: _circleThreshold,
|
|
maxDistance: _circleDistance,
|
|
);
|
|
emit(state.copyWith(
|
|
certifierCount: certifiers.length,
|
|
iVouch: certifiers.contains(selfPubkey),
|
|
knownToYou: circle.contains(peerPubkey),
|
|
loading: false,
|
|
));
|
|
}
|
|
|
|
/// Adds or removes this user's vouch, then reloads. Never vouches for self.
|
|
Future<void> toggleVouch() async {
|
|
final transport = _transport;
|
|
if (transport == null || peerPubkey == selfPubkey) return;
|
|
emit(state.copyWith(busy: true));
|
|
if (state.iVouch) {
|
|
await transport.revoke(subjectPubkey: peerPubkey);
|
|
} else {
|
|
await transport.certify(subjectPubkey: peerPubkey);
|
|
}
|
|
await load();
|
|
emit(state.copyWith(busy: false));
|
|
}
|
|
|
|
@override
|
|
Future<void> close() async {
|
|
await _onDispose?.call();
|
|
return super.close();
|
|
}
|
|
}
|
|
|
|
/// Opens a [TrustCubit] wired to the social layer for [peerPubkey], or an
|
|
/// offline one.
|
|
Future<TrustCubit> createTrustCubit(
|
|
SocialService social,
|
|
SocialSettings settings, {
|
|
required String peerPubkey,
|
|
}) async {
|
|
final relays = await settings.relayUrls();
|
|
if (relays.isEmpty) {
|
|
return TrustCubit(null,
|
|
peerPubkey: peerPubkey, selfPubkey: social.publicKeyHex);
|
|
}
|
|
try {
|
|
final session = await social.openSession(relays);
|
|
return TrustCubit(
|
|
session.trust,
|
|
peerPubkey: peerPubkey,
|
|
selfPubkey: social.publicKeyHex,
|
|
onDispose: session.close,
|
|
);
|
|
} catch (_) {
|
|
return TrustCubit(null,
|
|
peerPubkey: peerPubkey, selfPubkey: social.publicKeyHex);
|
|
}
|
|
}
|