feat: add support for basic web of trust #65
Generated
+29
-30
@@ -933,9 +933,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.8.0"
|
version = "1.9.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6651c9ed80effdc7db0ff72512157f901af5e3549e341e24b1dd4887d836d838"
|
checksum = "877bf67ac8e52449eb2f9e98a344ea163beb70dd1f9eebe3cc5357dcfeaee4df"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"find-msvc-tools",
|
"find-msvc-tools",
|
||||||
"jobserver",
|
"jobserver",
|
||||||
@@ -1066,7 +1066,7 @@ checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"block-buffer 0.12.1",
|
"block-buffer 0.12.1",
|
||||||
"crypto-common 0.2.2",
|
"crypto-common 0.2.2",
|
||||||
"inout 0.2.4",
|
"inout 0.2.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2013,9 +2013,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "find-msvc-tools"
|
name = "find-msvc-tools"
|
||||||
version = "0.1.14"
|
version = "0.1.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484"
|
checksum = "1c66915cc5391097de7a2b0b972e8505aa33c0df401fd3a6811dbdb32881ba48"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "flatbuffers"
|
name = "flatbuffers"
|
||||||
@@ -2696,7 +2696,7 @@ dependencies = [
|
|||||||
"log",
|
"log",
|
||||||
"rustls",
|
"rustls",
|
||||||
"rustls-platform-verifier",
|
"rustls-platform-verifier",
|
||||||
"webpki-roots 1.0.9",
|
"webpki-roots 1.0.10",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3392,7 +3392,7 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
"tokio-rustls",
|
"tokio-rustls",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"webpki-roots 1.0.9",
|
"webpki-roots 1.0.10",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3616,9 +3616,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "inout"
|
name = "inout"
|
||||||
version = "0.2.4"
|
version = "0.2.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3ce983bb6fb399067d101fbfc9e74174e07e9f29a2bc679efefeb8c645819c3f"
|
checksum = "3b5cf4372d022092674a83b8febf5865f7a573cc0c5baec5e49c879b6a20978f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hybrid-array",
|
"hybrid-array",
|
||||||
]
|
]
|
||||||
@@ -4352,7 +4352,7 @@ checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr"
|
name = "nostr"
|
||||||
version = "0.45.5"
|
version = "0.45.5"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#de3031aa99986eb29b3f34580355f9a099e4c927"
|
source = "git+https://github.com/rust-nostr/nostr#20096765b95f4840dd3df66cad49db35e9634462"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
@@ -4378,7 +4378,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-blossom"
|
name = "nostr-blossom"
|
||||||
version = "0.45.1"
|
version = "0.45.1"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#de3031aa99986eb29b3f34580355f9a099e4c927"
|
source = "git+https://github.com/rust-nostr/nostr#20096765b95f4840dd3df66cad49db35e9634462"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"bitcoin_hashes 1.2.0",
|
"bitcoin_hashes 1.2.0",
|
||||||
@@ -4391,10 +4391,9 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-connect"
|
name = "nostr-connect"
|
||||||
version = "0.45.2"
|
version = "0.45.2"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#de3031aa99986eb29b3f34580355f9a099e4c927"
|
source = "git+https://github.com/rust-nostr/nostr#20096765b95f4840dd3df66cad49db35e9634462"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-utility",
|
"async-utility",
|
||||||
"futures-core",
|
|
||||||
"nostr",
|
"nostr",
|
||||||
"nostr-sdk",
|
"nostr-sdk",
|
||||||
"opaquerr 0.2.0",
|
"opaquerr 0.2.0",
|
||||||
@@ -4405,7 +4404,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-database"
|
name = "nostr-database"
|
||||||
version = "0.45.2"
|
version = "0.45.2"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#de3031aa99986eb29b3f34580355f9a099e4c927"
|
source = "git+https://github.com/rust-nostr/nostr#20096765b95f4840dd3df66cad49db35e9634462"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"nostr",
|
"nostr",
|
||||||
"opaquerr 0.2.0",
|
"opaquerr 0.2.0",
|
||||||
@@ -4414,7 +4413,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-gossip"
|
name = "nostr-gossip"
|
||||||
version = "0.45.1"
|
version = "0.45.1"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#de3031aa99986eb29b3f34580355f9a099e4c927"
|
source = "git+https://github.com/rust-nostr/nostr#20096765b95f4840dd3df66cad49db35e9634462"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"nostr",
|
"nostr",
|
||||||
"opaquerr 0.2.0",
|
"opaquerr 0.2.0",
|
||||||
@@ -4423,7 +4422,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-gossip-memory"
|
name = "nostr-gossip-memory"
|
||||||
version = "0.45.1"
|
version = "0.45.1"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#de3031aa99986eb29b3f34580355f9a099e4c927"
|
source = "git+https://github.com/rust-nostr/nostr#20096765b95f4840dd3df66cad49db35e9634462"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"lru",
|
"lru",
|
||||||
@@ -4435,7 +4434,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-lmdb"
|
name = "nostr-lmdb"
|
||||||
version = "0.45.3"
|
version = "0.45.3"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#de3031aa99986eb29b3f34580355f9a099e4c927"
|
source = "git+https://github.com/rust-nostr/nostr#20096765b95f4840dd3df66cad49db35e9634462"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-utility",
|
"async-utility",
|
||||||
"flatbuffers",
|
"flatbuffers",
|
||||||
@@ -4450,7 +4449,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-memory"
|
name = "nostr-memory"
|
||||||
version = "0.45.2"
|
version = "0.45.2"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#de3031aa99986eb29b3f34580355f9a099e4c927"
|
source = "git+https://github.com/rust-nostr/nostr#20096765b95f4840dd3df66cad49db35e9634462"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"btreecap",
|
"btreecap",
|
||||||
"nostr",
|
"nostr",
|
||||||
@@ -4461,7 +4460,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-sdk"
|
name = "nostr-sdk"
|
||||||
version = "0.45.4"
|
version = "0.45.4"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#de3031aa99986eb29b3f34580355f9a099e4c927"
|
source = "git+https://github.com/rust-nostr/nostr#20096765b95f4840dd3df66cad49db35e9634462"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-utility",
|
"async-utility",
|
||||||
"async-wsocket",
|
"async-wsocket",
|
||||||
@@ -5943,7 +5942,7 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"wasm-bindgen-futures",
|
"wasm-bindgen-futures",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
"webpki-roots 1.0.9",
|
"webpki-roots 1.0.10",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -7164,9 +7163,9 @@ checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.53.3"
|
version = "1.54.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ce3335fa71841cda333a58d7615b03901380ecf09d59b3296d21f8bbac0dde4e"
|
checksum = "a0c168a474b95bfecfe908a65d4bdffc523144b649a74c09c286afb35d53e0bc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"libc",
|
"libc",
|
||||||
@@ -7656,14 +7655,14 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"ureq-proto",
|
"ureq-proto",
|
||||||
"utf8-zero",
|
"utf8-zero",
|
||||||
"webpki-roots 1.0.9",
|
"webpki-roots 1.0.10",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ureq-proto"
|
name = "ureq-proto"
|
||||||
version = "0.6.4"
|
version = "0.6.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f86fd172ccca569e458f61b6bdd6220965a9ef36e672a6852953b51a0e1583be"
|
checksum = "841583fc36089d7d59419568ae157f43a5a03a2e5a81ba87c77d1596ef46e922"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.23.1",
|
"base64 0.23.1",
|
||||||
"http",
|
"http",
|
||||||
@@ -8043,9 +8042,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "webpki-root-certs"
|
name = "webpki-root-certs"
|
||||||
version = "1.0.9"
|
version = "1.0.10"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b"
|
checksum = "58ec80e73fd8baf57bf0d9615554c75348bda990d6d5bbeab964dd068319fecb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
]
|
]
|
||||||
@@ -8056,14 +8055,14 @@ version = "0.26.11"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9"
|
checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"webpki-roots 1.0.9",
|
"webpki-roots 1.0.10",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "webpki-roots"
|
name = "webpki-roots"
|
||||||
version = "1.0.9"
|
version = "1.0.10"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a"
|
checksum = "1c1c8e20c8a38fc7d65abfe74eb7280e4aa667e8eec8bc84996d2874810748d9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
]
|
]
|
||||||
|
|||||||
+7
-19
@@ -1,4 +1,5 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use anyhow::{Error, anyhow};
|
use anyhow::{Error, anyhow};
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
@@ -260,15 +261,9 @@ impl NostrRegistry {
|
|||||||
let uri = NostrConnectUri::parse(base)?;
|
let uri = NostrConnectUri::parse(base)?;
|
||||||
|
|
||||||
// Construct the nostr connect signer
|
// Construct the nostr connect signer
|
||||||
let mut signer = NostrConnect::new(
|
let signer = NostrConnect::builder(uri, keys)
|
||||||
uri,
|
.auth_url_handler(Arc::new(CoopAuthUrlHandler))
|
||||||
keys,
|
.build()?;
|
||||||
Duration::from_secs(NOSTR_CONNECT_TIMEOUT),
|
|
||||||
None,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// Handle auth url with the default browser
|
|
||||||
signer.auth_url_handler(CoopAuthUrlHandler);
|
|
||||||
|
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
this.set_signer(signer, cx);
|
this.set_signer(signer, cx);
|
||||||
@@ -447,16 +442,9 @@ impl NostrRegistry {
|
|||||||
let write = cx.write_credentials(USER_KEYRING, "bunker", credential.as_bytes());
|
let write = cx.write_credentials(USER_KEYRING, "bunker", credential.as_bytes());
|
||||||
|
|
||||||
cx.spawn(async move |this, cx| {
|
cx.spawn(async move |this, cx| {
|
||||||
// Construct the nostr connect signer
|
let signer = NostrConnect::builder(connect_uri, keys)
|
||||||
let mut signer = NostrConnect::new(
|
.auth_url_handler(Arc::new(CoopAuthUrlHandler))
|
||||||
connect_uri,
|
.build()?;
|
||||||
keys,
|
|
||||||
Duration::from_secs(NOSTR_CONNECT_TIMEOUT),
|
|
||||||
None,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// Handle auth url with the default browser
|
|
||||||
signer.auth_url_handler(CoopAuthUrlHandler);
|
|
||||||
|
|
||||||
// Verify the bunker responds before persisting the credential
|
// Verify the bunker responds before persisting the credential
|
||||||
let public_key = signer.get_public_key_async().await?;
|
let public_key = signer.get_public_key_async().await?;
|
||||||
|
|||||||
@@ -0,0 +1,201 @@
|
|||||||
|
# Web of Trust — implementation plan
|
||||||
|
|
||||||
|
A plan for scoring strangers by the user's follow graph, based on how Amethyst
|
||||||
|
implements web of trust. The goal is to make the screening dialog and the chat
|
||||||
|
requests list more useful: "3 of your contacts follow this person" instead of a
|
||||||
|
bare npub, and a way to let trusted strangers skip screening.
|
||||||
|
|
||||||
|
## 1. How Amethyst does it
|
||||||
|
|
||||||
|
Amethyst (vitorpamplona/amethyst) has two independent WoT layers:
|
||||||
|
|
||||||
|
**Layer 1 — follow-graph score (`WoTService`)**, in
|
||||||
|
`commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/wot/WoTService.kt`:
|
||||||
|
|
||||||
|
- For every pubkey `X`, the score is the number of people the user follows who
|
||||||
|
also follow `X` (friends-of-friends, computed only from kind 3 contact lists).
|
||||||
|
- The user's own follow set and each followed user's kind 3 feed the graph; the
|
||||||
|
service keeps a reverse index `target -> {followers}` and derives scores from
|
||||||
|
set sizes.
|
||||||
|
- Purely in memory, rebuilt on start; a single writer coroutine serializes
|
||||||
|
updates via an op channel and diffs each follower's list on republish, so a
|
||||||
|
changed kind 3 only touches the entries it added/removed.
|
||||||
|
- Guardrails: WoT is disabled when the user follows more than `MAX_FOLLOWS`
|
||||||
|
(2000) accounts, and each ingested contact list is capped
|
||||||
|
(`MAX_FOLLOWS_PER_EVENT`, 5000) to bound hostile publishers.
|
||||||
|
- A `ready` flag (set on first EOSE) tells the UI when scores are usable.
|
||||||
|
- Consumers sort people follows-first, then by score (`PeopleOrder.kt`) and show
|
||||||
|
a desktop badge.
|
||||||
|
|
||||||
|
**Layer 2 — NIP-85 trust-provider index**, in
|
||||||
|
`commons/.../wot/network/` and `quartz/.../nip85TrustedAssertions/users/index/`:
|
||||||
|
|
||||||
|
- The user names a "trust provider" (e.g. Brainstorm/GrapeRank) in a kind 10040
|
||||||
|
event; the provider publishes a signed rank card (kind 30382) for every member
|
||||||
|
of the user's extended network (~150–300k events).
|
||||||
|
- The client downloads the whole set once, verifies every signature, and stores
|
||||||
|
a compact binary index (~6 MB: 128-bit key prefixes + rank/hops/followers
|
||||||
|
arrays) with cursor-based incremental sync (NIP-45 COUNT + NIP-77 negentropy).
|
||||||
|
- The gate is `accepted(pk) = pk == me || pk in follows || rank(pk) >= minScore`
|
||||||
|
and it drives the DM Known/New split, the "Curated" notification filter, push
|
||||||
|
filtering, and collapsing out-of-network replies. Exemptions live in one
|
||||||
|
predicate (`TrustVerdicts.kt` / `CuratedTrustFilter.kt`) so in-app and push
|
||||||
|
can't drift: zaps always pass, chats follow the DM rule, everything else is
|
||||||
|
gated on network membership.
|
||||||
|
|
||||||
|
### What we take, what we skip
|
||||||
|
|
||||||
|
| Amethyst piece | Take? |
|
||||||
|
|---|---|
|
||||||
|
| Friends-of-friends score, reverse index, diffing, caps, ready flag | Yes — this is the whole feature for coop |
|
||||||
|
| Follows-first ordering helper | Only if we ever list non-contacts; not needed now |
|
||||||
|
| NIP-85 provider onboarding, kind 30382 index, negentropy sync, binary files | No — massive sync + storage complexity for a DM client |
|
||||||
|
| One shared exemption predicate per surface | Yes as a principle: gate requests in one place |
|
||||||
|
|
||||||
|
Layer 2 exists because Amethyst gates a 300k-user social graph. Coop gates
|
||||||
|
incoming DMs; the follow graph (one hop of contact lists) is enough signal and
|
||||||
|
needs no new events, relays, or storage formats.
|
||||||
|
|
||||||
|
## 2. Design: `WebOfTrust` in the `person` crate
|
||||||
|
|
||||||
|
One new logical component, one new file: `crates/person/src/wot.rs`, re-exported
|
||||||
|
from `crates/person/src/lib.rs` and initialized in `person::init` as a global
|
||||||
|
entity, exactly like `PersonRegistry`.
|
||||||
|
|
||||||
|
### State
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct WebOfTrust {
|
||||||
|
/// The signed-in user's contacts (latest kind 3)
|
||||||
|
follows: HashSet<PublicKey>,
|
||||||
|
/// The current user, excluded from credit
|
||||||
|
self_key: Option<PublicKey>,
|
||||||
|
/// Per-follower cached follow set, for diffing on republish
|
||||||
|
lists: HashMap<PublicKey, HashSet<PublicKey>>,
|
||||||
|
/// Reverse index: target -> contacts who follow them
|
||||||
|
reverse: HashMap<PublicKey, HashSet<PublicKey>>,
|
||||||
|
/// Disabled when the follow set exceeds MAX_FOLLOWS
|
||||||
|
disabled: bool,
|
||||||
|
ready: bool,
|
||||||
|
tasks, subscriptions, ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
API (all reads are plain methods; writes go through the entity):
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub fn score(&self, key: &PublicKey) -> usize // reverse[key].len()
|
||||||
|
pub fn is_in_network(&self, key: &PublicKey) -> bool // follows || score >= 1
|
||||||
|
pub fn mutuals(&self, key: &PublicKey) -> Vec<PublicKey> // who vouches, for the dialog
|
||||||
|
pub fn is_ready(&self) -> bool
|
||||||
|
pub fn is_disabled(&self) -> bool
|
||||||
|
```
|
||||||
|
|
||||||
|
Events: `WoTEvent::Updated` and `WoTEvent::Ready`, emitted after hydration and
|
||||||
|
on every graph change; consumers subscribe like they do to `PersonRegistry`.
|
||||||
|
|
||||||
|
### Data flow — reuses the existing pattern end to end
|
||||||
|
|
||||||
|
Same shape as `PersonRegistry::new` (background listener -> flume channel ->
|
||||||
|
entity update -> `cx.emit`):
|
||||||
|
|
||||||
|
1. **Hydrate from the local database** on creation and on
|
||||||
|
`StateEvent::SignerChanged`:
|
||||||
|
- own kind 3 (newest event, authors sorted by `Reverse(created_at)` like
|
||||||
|
`Directory::track` does) -> `follows`;
|
||||||
|
- one batch query `Filter::new().kind(Kind::ContactList).authors(follows)`
|
||||||
|
-> dedupe per author keeping the newest -> seed `lists`/`reverse`.
|
||||||
|
2. **Stay fresh**: one persistent subscription with a dedicated
|
||||||
|
`SubscriptionId` (add `WOT_CONTACTS` beside `DEVICE_GIFTWRAP`/`USER_GIFTWRAP`
|
||||||
|
in `state::constants`), filter `kind(Kind::ContactList).authors(follows)`.
|
||||||
|
The background listener accepts any kind 3 whose author is in `follows`
|
||||||
|
(don't couple to subscription ids), diffs it against the cached list, and
|
||||||
|
updates the reverse index. Re-issue the subscription when the user's own
|
||||||
|
contact list changes.
|
||||||
|
3. **Rules ported from `WoTService`**:
|
||||||
|
- exclude the follower and `self_key` from every ingested list;
|
||||||
|
- on follow-set change, un-credit removed followers from the reverse index;
|
||||||
|
- `MAX_FOLLOWS = 2000` disables the service (clear state, emit ready);
|
||||||
|
- `MAX_TAGS_PER_LIST = 5000` caps one follower's ingested list;
|
||||||
|
- reset everything on sign-out / account switch.
|
||||||
|
|
||||||
|
No persistence: the graph rebuilds from the LMDB cache on every start, the same
|
||||||
|
way `ChatRegistry` rebuilds rooms. No new relay events, no settings file, no new
|
||||||
|
dependencies.
|
||||||
|
|
||||||
|
## 3. Consumers (in order)
|
||||||
|
|
||||||
|
### Phase 1 — screening dialog (`crates/workspace/src/dialogs/screening.rs`)
|
||||||
|
|
||||||
|
Replace `check_wot`'s database query (which only sees contact lists that happen
|
||||||
|
to be cached, so "mutual contacts" is usually empty) with the registry:
|
||||||
|
|
||||||
|
- `mutual_contacts = wot.mutuals(&public_key)` — the people **the user follows**
|
||||||
|
who follow the stranger, which is the correct WoT semantic and matches
|
||||||
|
Amethyst's score.
|
||||||
|
- The existing "Mutual contacts" dialog keeps working unchanged: it already
|
||||||
|
renders a `Vec<PublicKey>` through a `Directory`.
|
||||||
|
- When `wot.is_disabled()` or not ready, fall back to the current query so the
|
||||||
|
dialog degrades to today's behavior.
|
||||||
|
|
||||||
|
### Phase 2 — requests list badge
|
||||||
|
|
||||||
|
Where request rooms are rendered, show the mutual count for the sender:
|
||||||
|
|
||||||
|
- `crates/workspace/src/sidebar/inbox.rs` (`requests_rows`) and
|
||||||
|
`crates/workspace/src/panels/greeter.rs` (`load`): one caption
|
||||||
|
`"N mutual contacts"` under the request preview, via the room's other member.
|
||||||
|
- Subscribe to `WoTEvent::Updated` where `PersonEvent` is already subscribed so
|
||||||
|
rows re-render when the graph changes.
|
||||||
|
|
||||||
|
### Phase 3 — opt-in auto-trust (behind a setting)
|
||||||
|
|
||||||
|
The payoff: strangers vouched for by the user's contacts can skip screening.
|
||||||
|
|
||||||
|
- Add `pub wot_auto_trust: bool` to `setting_accessors!` in
|
||||||
|
`crates/settings/src/lib.rs` (default `false`) and a toggle in the existing
|
||||||
|
preferences screen (`crates/workspace/src/dialogs/settings.rs`).
|
||||||
|
- When enabled, apply it in one place — `ChatRegistry::new_message` in
|
||||||
|
`crates/chat/src/lib.rs`, next to the existing promotion rule — so in-app and
|
||||||
|
any future consumer can't drift (Amethyst's lesson from `TrustVerdicts`):
|
||||||
|
|
||||||
|
```rust
|
||||||
|
if this.kind == RoomKind::Request
|
||||||
|
&& (message.rumor.pubkey == public_key
|
||||||
|
|| (AppSettings::get_wot_auto_trust(cx) && wot.is_in_network(&message.rumor.pubkey)))
|
||||||
|
{
|
||||||
|
this.set_ongoing(cx);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Group requests stay conservative: every member must be in the network (or
|
||||||
|
already a contact) to promote, mirroring how `is_contact` works today.
|
||||||
|
|
||||||
|
Deliberately not planned: sorting contacts (the new-chat dialog lists only
|
||||||
|
follows already), notification feeds (coop has none), muting/spam scoring.
|
||||||
|
|
||||||
|
## 4. Non-goals
|
||||||
|
|
||||||
|
- NIP-85 trust providers (kind 10040/30382), Brainstorm onboarding, rank
|
||||||
|
thresholds — revisit only if coop ever needs a network-scale gate.
|
||||||
|
- Persisting the graph or multi-hop (3+) scores — kind 3s are one LMDB query
|
||||||
|
away on restart.
|
||||||
|
- Background jobs / WorkManager equivalents — hydration is two local queries.
|
||||||
|
|
||||||
|
## 5. Testing
|
||||||
|
|
||||||
|
- Unit tests for the pure graph logic in `wot.rs` (no `cx` needed): seeding,
|
||||||
|
per-follower diffing, follow-set removal un-crediting, self/follower
|
||||||
|
exclusion, both caps and the disabled transition.
|
||||||
|
- One GPUI test for hydration ordering (newest kind 3 wins) using
|
||||||
|
`TestAppContext`; use `cx.background_executor().timer(...)` for any waits per
|
||||||
|
the project guidelines.
|
||||||
|
|
||||||
|
## 6. References
|
||||||
|
|
||||||
|
- `WoTService.kt` — the model this plan ports:
|
||||||
|
https://github.com/vitorpamplona/amethyst/blob/main/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/wot/WoTService.kt
|
||||||
|
- `PeopleOrder.kt`, `CuratedTrustFilter.kt`, `TrustVerdicts.kt` — consumers and
|
||||||
|
the shared-predicate pattern.
|
||||||
|
- Design doc for Amethyst's NIP-85 index (the layer we skip, with the reasoning
|
||||||
|
and measurements): `commons/plans/2026-10-07-wot-network-index.md` on main.
|
||||||
Reference in New Issue
Block a user