diff --git a/docs/TRUST_AND_BLOCK_FEATURE.md b/docs/TRUST_AND_BLOCK_FEATURE.md index 3a29b73..351c7e5 100644 --- a/docs/TRUST_AND_BLOCK_FEATURE.md +++ b/docs/TRUST_AND_BLOCK_FEATURE.md @@ -1,160 +1,152 @@ -# Trust & Block Feature Plan +# Trust, Mute, and Web-of-Trust (WoT) Plan -## Overview -Add web-of-trust filtering and user block lists to the community template feed so users can -hide content from bad actors and prioritize templates from trusted sources. Because the app -already uses Nostr for identity and publishing, this feature can be built entirely on existing -Nostr protocol standards (NIPs) with no backend required. +## Goals +Build an **abuse-resistant community feed** that still feels open and discoverable, using only Nostr-native data and client-side logic. -## Complexity -**Moderate–High** — Estimated 4–8 hours to implement (phased; Phase 1 alone is ~2 hours) +### Product goals +1. Reduce spam and low-quality content quickly. +2. Keep control in users’ hands (local + portable moderation). +3. Preserve feed performance for both logged-out and logged-in users. +4. Ship in small phases with measurable outcomes. + +### Non-goals (v1) +- Centralized server moderation. +- Perfect Sybil resistance. +- Cross-relay consensus on reports. --- -## Background: Nostr Primitives Available - -| NIP | Kind | Purpose | -|-----|------|---------| -| NIP-02 | 3 | Contact / follow list — the source of truth for the social graph | -| NIP-51 | 10000 | Mute list — pubkeys, event IDs, keywords to suppress | -| NIP-56 | 1984 | Content report — flag a pubkey or event as spam/NSFW/illegal | - -These events are already signed with the user's key (via NIP-07 extension) and stored on -Nostr relays, so the block/mute list automatically syncs across any Nostr-compatible client -the user signs in to. +## Current architecture assumptions +- Identity and signing via **NIP-07** browser extension. +- Community templates are already rendered from Nostr events. +- App can query multiple relays and currently has a feed render pipeline. --- -## Phased Plan +## Key Nostr primitives -### Phase 1 — Local Block List (no login required) - -The simplest layer: any visitor can block a pubkey locally without signing in. - -#### Storage -- `localStorage` key: `casewrap-blocked-pubkeys` → JSON array of hex pubkeys -- Survives page refresh; cleared only if the user explicitly unblocks or clears browser data - -#### UI -- Add a "Block this author" option to each template card menu (⋮ or right-click) -- Blocked templates are hidden from the feed immediately (no reload needed) -- Settings panel shows blocked pubkey list with per-entry "Unblock" buttons -- Blocked count shown as a subtle badge: "3 authors hidden" - -#### Core Functions -```javascript -function blockPubkey(pubkey) - // Add to localStorage block list, re-filter visible templates - -function unblockPubkey(pubkey) - // Remove from localStorage block list, re-filter - -function isBlocked(pubkey) - // Returns true if pubkey is in the local block list - -function applyBlockFilter(templates) - // Filter template array, removing any whose author is blocked -``` +| NIP | Kind | Usage in this feature | +|-----|------|------------------------| +| NIP-02 | `3` | Follow graph (hop-1, optional hop-2) | +| NIP-51 | `10000` | User mute list (`p` tags, optional words/events) | +| NIP-56 | `1984` | Abuse reports (advisory to relays/tools) | --- -### Phase 2 — Nostr Mute List Sync (requires NIP-07 login) +## Phased delivery plan -Upgrade the local block list to a Nostr-native mute list (NIP-51 kind 10000) so it persists -across devices and syncs with other Nostr apps (Damus, Primal, Snort, etc.). +## Phase 1 — Local block list (works logged out) -#### On Login -1. Fetch the user's existing kind 10000 mute list from relays -2. Merge with any locally blocked pubkeys (union, no duplicates) -3. Save merged list back to the relay (signed via `window.nostr.signEvent`) +### Why first +Immediate abuse relief, zero protocol dependencies, and easy rollback. -#### On Block -1. Add pubkey to the in-memory mute list -2. Publish updated kind 10000 event to relays -3. Update `localStorage` as a local cache +### Data model +- `localStorage['casewrap:block:list:v1'] = string[]` (hex pubkeys) +- Normalize pubkeys to lowercase hex before insert. +- Keep a companion metadata object for analytics/debug (optional): + - `localStorage['casewrap:block:meta:v1'] = { [pubkey]: { createdAt, source } }` -#### Event Schema +### UX +- Card menu action: **Block author**. +- Settings section: list blocked pubkeys + unblock button. +- Inline feed toast: “Author blocked. Undo”. +- Badge in settings: “N authors hidden”. + +### Engineering notes +- Block filter is applied as the first feed filter stage. +- Use a `Set` in memory to avoid repeated array scans. +- Add defensive guard for malformed pubkeys. + +### Acceptance criteria +- Blocking hides author content immediately in all feed views. +- Unblock restores content without full refresh. +- Block state persists across refresh. + +--- + +## Phase 2 — Mute list sync (NIP-51) + +### Why second +Makes moderation portable across devices and compatible with other Nostr clients. + +### Sync strategy +On successful NIP-07 login: +1. Fetch latest kind `10000` authored by viewer pubkey. +2. Parse `p` tags into `remoteMuted` set. +3. Read local block set `localMuted`. +4. Merge union -> `effectiveMuted`. +5. If union differs from remote, publish updated kind `10000`. +6. Cache `effectiveMuted` locally. + +### Conflict policy +- **Union wins** by default (safer for abuse prevention). +- Never silently remove remote mutes. +- If publish fails, keep local behavior and surface non-blocking warning. + +### Event shape ```json { "kind": 10000, - "tags": [ - ["p", ""], - ["p", ""] - ], + "tags": [["p", ""]], "content": "" } ``` -#### Core Functions -```javascript -async function fetchMuteList(pubkey) - // Query relays for kind 10000 authored by pubkey, return p-tag array - -async function publishMuteList(mutedPubkeys) - // Sign and publish updated kind 10000 event - -async function syncMuteListOnLogin() - // Merge relay mute list with localStorage block list on sign-in -``` +### Acceptance criteria +- Existing remote mute list is respected on login. +- New block actions publish updated mute list. +- Local filter still works if relays/signing fail. --- -### Phase 3 — Web-of-Trust Feed Filter (requires NIP-07 login) +## Phase 3 — WoT-aware ranking and filtering -Only show templates from pubkeys within your social graph — the most effective spam defense. +### Why third +Highest feed-quality impact for signed-in users. -#### How It Works -1. Fetch the signed-in user's follow list (NIP-02 kind 3) → set of followed pubkeys ("hop 1") -2. Optionally fetch follows-of-follows → "hop 2" (wider but noisier) -3. In the community feed, templates are ranked/filtered by trust tier: +### Trust tiers +- **Blocked**: in effective mute set -> hidden. +- **Trusted (hop-1)**: directly followed by viewer. +- **Extended (hop-2)**: followed by someone viewer follows. +- **Unknown**: no graph evidence. -| Tier | Source | Display | -|------|--------|---------| -| Trusted | Pubkey is in your follow list (hop 1) | Full display, shown first | -| Extended | Pubkey is in follows-of-follows (hop 2) | Shown normally | -| Unknown | No social graph overlap | Shown with a "low trust" badge | -| Blocked | In mute list | Hidden entirely | +### Modes +- Default mode: **rank by trust**, don’t hard-hide unknown. +- Optional strict mode toggle: **show trusted only**. +- Optional toggle: enable/disable hop-2 expansion. -#### Trust Score (optional enhancement) -For each template author, count how many of your follows also follow them. -This score (0–N) can be shown as a small indicator on the template card. +### Ranking algorithm (v1) +Sort key tuple: +1. Tier priority (`trusted` > `extended` > `unknown`) +2. Optional social score (# of hop-1 followers who follow author) +3. Existing recency score (keep current feed freshness behavior) -```javascript -async function fetchFollowList(pubkey) - // Query relays for kind 3, return array of followed pubkeys +### Performance constraints +- Fetch hop-1 first; render immediately with partial trust state. +- Compute hop-2 only if user enables it. +- Cache graph in `sessionStorage` with TTL (e.g., 15 min). +- Hard cap hop-2 expansion fanout (e.g., max 500 hop-1 nodes queried). +- Use batched relay subscriptions; close on EOSE. -function buildTrustSet(myFollows, followsOfFollows) - // Returns { hop1: Set, hop2: Set } - -function getTrustTier(authorPubkey, trustSet) - // Returns 'trusted' | 'extended' | 'unknown' | 'blocked' - -function sortByTrust(templates, trustSet) - // Put hop-1 templates first, then hop-2, then unknown -``` - -#### Caching -- Follow lists are large — cache in `sessionStorage` keyed by pubkey -- Re-fetch at most once per session to avoid relay hammering -- Show a loading indicator while the graph is being fetched - -#### UI Controls -- Toggle in settings: "Show only trusted authors" (default: off — non-breaking) -- Toggle: "Show low-trust badge on unknown authors" (default: on when logged in) -- Trust tier shown as a subtle icon on template cards (★ trusted, ~ extended, ? unknown) +### Acceptance criteria +- Trust tiers display correctly for sampled known authors. +- “Trusted only” mode hides extended + unknown. +- Feed first paint is not blocked by hop-2 queries. --- -### Phase 4 — Content Reporting (NIP-56) +## Phase 4 — Reporting (NIP-56) -Allow users to flag templates as spam, NSFW, or abusive so relay operators can act. +### Why fourth +Valuable ecosystem signal but lower immediate user impact than blocking/filtering. -#### Report Flow -1. User clicks "Report" in the template card menu -2. Modal asks for report type: `spam` | `nudity` | `illegal` | `impersonation` -3. App publishes a NIP-56 kind 1984 event referencing the template event ID and author pubkey +### UX +- Card menu -> **Report**. +- Reason required (`spam`, `nudity`, `illegal`, `impersonation`, `other`). +- Optional note. +- Post-submit option: “Also block this author”. -#### Event Schema +### Event shape ```json { "kind": 1984, @@ -162,86 +154,95 @@ Allow users to flag templates as spam, NSFW, or abusive so relay operators can a ["e", "", "spam"], ["p", ""] ], - "content": "Optional description" + "content": "Optional detail" } ``` -#### Notes -- Reports are published to relays but have no local effect by themselves -- Optionally: auto-hide locally after reporting (user already chose to flag it) -- Reports help relay operators and moderation tools (like Spam Scout) build block lists - -```javascript -async function reportTemplate(eventId, authorPubkey, reason, description) - // Sign and publish NIP-56 kind 1984 event -``` +### Acceptance criteria +- Signed event is published with valid target tags. +- Failures are surfaced to user; no silent drop. --- -## Integration Points +## Cross-cutting concerns -All four phases hook into the community feed render loop. The minimal change is: +### Security & abuse hardening +- Validate all incoming pubkeys/event ids before use. +- Debounce block/report UI actions to avoid duplicate publishes. +- Protect against relay poisoning by taking latest replaceable event only. -```javascript -// In the function that renders community template cards: -function renderCommunityFeed(templates) { - const filtered = applyBlockFilter(templates); // Phase 1 - const sorted = sortByTrust(filtered, trustSet); // Phase 3 - filtered.forEach(t => renderCard(t, getTrustTier(t.author, trustSet))); -} -``` +### Privacy +- WoT computation occurs client-side. +- Do not log raw social graph by default. +- If telemetry is added, aggregate counts only. -Block/report actions live in the template card's action menu alongside existing -"Copy link" / "Import" options. +### Observability (recommended) +Track coarse metrics: +- `% feed hidden by block list` +- `% users enabling trusted-only` +- `median feed render time (logged in/out)` +- `mute sync success rate` + +### Failure handling +- If NIP-07 unavailable: Phase 1 fully operational; hide sync/report controls. +- If relay read fails: preserve local cache and degrade gracefully. +- If relay publish fails: keep local state, show retry affordance. --- -## Performance Considerations -- Follow list fetches are async and cached — they don't block the initial feed render -- Local block filter runs synchronously on the already-fetched template array (~1ms) -- Follows-of-follows (hop 2) can be 10,000+ pubkeys — use a `Set` for O(1) lookups -- Only fetch hop-2 if the user enables the extended trust option (off by default) -- Relay queries use `REQ` filters with `kinds: [3]` — single-use subscriptions, close after `EOSE` +## Implementation slices (suggested task breakdown) +1. **Data layer**: block list store + normalization + helpers. +2. **Feed pipeline**: filter + tiering + ranking hooks. +3. **UI**: card menu actions, settings controls, trust badges. +4. **Nostr sync**: kind 10000 read/merge/publish. +5. **Reporting**: kind 1984 modal and publish flow. +6. **Instrumentation**: lightweight counters/timers. --- -## Limitations -- Web-of-trust only works when the user is signed in via NIP-07 -- Users with small follow lists get little benefit from WoT filtering -- Relay queries for follows-of-follows can be slow on large social graphs (~5–15s) -- NIP-56 reports are advisory only — relay operators decide whether to act -- No cross-relay mute list deduplication (if user uses multiple relay sets) +## Revised testing plan + +### Unit tests +- Pubkey normalization and dedupe logic. +- Union merge behavior for local vs remote mute lists. +- Trust-tier classifier and rank sort stability. + +### Integration tests +- Logged-out: block/unblock lifecycle across refresh. +- Logged-in: mute sync read/merge/publish happy path. +- Logged-in degraded: relay failure + signer rejection handling. +- Trusted-only toggle behavior with mixed-tier fixture feed. + +### Performance tests +- Feed render timing with: + - 0 blocks + - 500 blocked pubkeys + - hop-1 only + - hop-1 + capped hop-2 + +### Manual QA checklist +- [ ] Block author from card and confirm immediate hide. +- [ ] Undo block toast restores card. +- [ ] Login merges local + remote mute sets without loss. +- [ ] Trust badges are accurate for controlled fixture accounts. +- [ ] Trusted-only mode does not show unknown authors. +- [ ] Report flow publishes kind 1984 and handles failure visibly. --- -## Future Enhancements -- Subscribe to community-maintained block lists (NIP-51 kind 30000 "follow sets" used as shared deny lists) -- Import block list from Nostr clients (Damus, Primal) via relay sync -- NSFW blur overlay instead of full hide (user can click to reveal) -- Relay-level WoT filtering: connect to WoT-curated relays (e.g. relay.nostr.band WoT endpoint) for pre-filtered feeds -- Keyword/hashtag muting (NIP-51 supports `["word", "badterm"]` tags in kind 10000) +## Is the original plan good enough? +Short answer: **good foundation, but not quite “as good as it could be” for production**. ---- +### What it already did well +- Correct NIP choices. +- Practical phased rollout. +- Good focus on no-backend architecture. -## Implementation Order (recommended) +### Biggest gaps this revision closes +- Explicit conflict/merge policy for mute sync. +- Concrete degradation behavior on relay/signing failure. +- Performance guardrails for hop-2 fanout. +- Better UX defaults (rank-first vs hide-first). +- Test strategy split by unit/integration/perf. +- Observability so you can prove impact after launch. -1. **Phase 1** — Local block list. Zero dependencies, works for logged-out users, shippable alone. -2. **Phase 3** — WoT feed sort/filter. High value for logged-in users; Phase 2 not required first. -3. **Phase 2** — Nostr mute list sync. Upgrades Phase 1 to be cross-device. -4. **Phase 4** — Reporting. Lowest urgency; depends on relay operator buy-in. - ---- - -## Testing Checklist -- [ ] Blocking a pubkey hides all their templates immediately -- [ ] Unblocking restores their templates -- [ ] Block list persists across page refresh (localStorage) -- [ ] Blocked pubkeys are excluded from all feed views (not just main) -- [ ] Mute list sync fetches existing kind 10000 on login (Phase 2) -- [ ] Publishing updated mute list signs correctly via NIP-07 (Phase 2) -- [ ] Follow list fetch correctly identifies hop-1 pubkeys (Phase 3) -- [ ] Trust tier badges display correctly on template cards (Phase 3) -- [ ] "Show only trusted" toggle hides unknown-tier templates (Phase 3) -- [ ] Report publishes a valid kind 1984 event to relays (Phase 4) -- [ ] No performance regression on feed load (follow list fetched async) -- [ ] Logged-out users see Phase 1 block UI only (no WoT controls)