mirror of
https://github.com/PR0M3TH3AN/Artstr-Studio.git
synced 2026-10-27 08:40:39 +00:00
Merge pull request #6 from PR0M3TH3AN/codex/improve-web-of-trust-documentation-plan
docs: strengthen web-of-trust feature development plan
This commit is contained in:
1 file changed
+182
-181
+182
-181
@@ -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", "<hex-pubkey>"],
|
||||
["p", "<hex-pubkey>"]
|
||||
],
|
||||
"tags": [["p", "<hex-pubkey>"]],
|
||||
"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<pubkey>, hop2: Set<pubkey> }
|
||||
|
||||
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", "<template-event-id>", "spam"],
|
||||
["p", "<author-pubkey>"]
|
||||
],
|
||||
"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)
|
||||
Reference in new issue
Block a user