import 'package:commons_core/commons_core.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; /// Where a peer stands, from strongest to weakest signal. Trust is /// ego-centric: computed from YOUR position in the public vouch graph, /// never from a global membership verdict. enum TrustTier { /// In your personal circle (you vouch, or a friend-of-a-friend does). inYourCircle, /// Vouched for by someone, but not (yet) in your circle. vouched, /// No certifications seen. unknown, } /// Trust standing of one peer, seen from this user's position: your own /// vouch, your circle (friend-of-a-friend), and the raw certifier count — /// computed from the public certification graph. 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) currently 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 (you, or a /// friend-of-a-friend, vouch). Spam-resistant and works from day one. final bool knownToYou; final bool loading; final bool busy; /// The strongest applicable signal, for the badge. TrustTier get tier { if (knownToYou) return TrustTier.inYourCircle; if (certifierCount > 0) return TrustTier.vouched; return TrustTier.unknown; } 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 get props => [ certifierCount, iVouch, knownToYou, loading, busy, ]; } /// Reads and toggles this user's vouch for [peerPubkey] over a /// [TrustTransport], and computes the peer's standing from this user's own /// position in the vouch graph (ego-centric — no referents, no parameters). class TrustCubit extends Cubit { TrustCubit( this._transport, { required this.peerPubkey, required this.selfPubkey, Future Function()? onDispose, }) : _onDispose = onDispose, super(const TrustState()); final TrustTransport? _transport; final String peerPubkey; final String selfPubkey; final Future Function()? _onDispose; bool get isOnline => _transport != null; /// Personal "circle" rule: one vouch from your side, out to a friend-of-a- /// friend. Loose by design — "people near you". static const _circleThreshold = 1; static const _circleDistance = 2; /// Vouches expire and must be renewed, so stale trust prunes itself. static const _vouchValidity = Duration(days: 365); /// Loads the certification graph and computes: the peer's certifier count, /// whether you vouch, and whether they're in your circle. Future 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. /// The certification is issued with a validity so it expires and must be /// renewed. Future 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, validity: _vouchValidity, ); } await load(); emit(state.copyWith(busy: false)); } @override Future close() async { await _onDispose?.call(); return super.close(); } }