diff --git a/context/CONTEXT_2026-02-16_docs-code-investigator.md b/context/CONTEXT_2026-02-16_docs-code-investigator.md new file mode 100644 index 00000000..4b6e3b3a --- /dev/null +++ b/context/CONTEXT_2026-02-16_docs-code-investigator.md @@ -0,0 +1,25 @@ +# Context: Documentation of js/services/moderationService.js + +## Selection +- **File**: `js/services/moderationService.js` +- **Line Count**: ~1350 lines +- **Reason**: This is a critical Trust & Safety module implementing the "Web of Trust" model. It is large, complex, and central to the user experience (filtering, blurring). It lacks comprehensive high-level documentation and detailed JSDoc for its public API. +- **Goal**: Analyze the file, add in-code JSDoc, and create a `docs/moderationService-overview.md` to explain the trust graph and report aggregation logic. + +## Analysis Plan +1. **High-level summary**: Web of Trust implementation using NIP-56 (Reports) and NIP-51 (Mutes), filtered by the user's social graph (Kind 3 contacts). +2. **Public Surface**: `ModerationService` class, `submitReport`, `recomputeSummaryForEvent`, `refreshViewerFromClient`, `setViewerPubkey`. +3. **Key Flows**: + - **Initialization**: `setViewerPubkey` -> `fetchTrustedContacts` -> `rebuildTrustedContacts`. + - **Reporting**: `submitReport` -> sign -> publish -> ingest. + - **Aggregation**: `subscribeToReports` -> `ingestReportEvent` -> `recomputeSummaryForEvent` (checks trust). +4. **Invariants**: + - Reports are only counted if the reporter is in `trustedContacts` (or whitelist). + - Admin actions (blacklist/whitelist) override the trust graph. + - Viewer mutes are synchronized with `userBlocks`. + +## Tasks +- [ ] Add file-header JSDoc explaining the module. +- [ ] Add JSDoc to `ModerationService` methods (`constructor`, `setViewerPubkey`, `submitReport`, etc.). +- [ ] Create `docs/moderationService-overview.md` with architecture diagrams (text) and usage examples. +- [ ] Verify with `npm run lint` and `npm run test:unit`. diff --git a/docs/agents/task-logs/daily/2026-02-16_docs-code-investigator_completed.md b/docs/agents/task-logs/daily/2026-02-16_docs-code-investigator_completed.md new file mode 100644 index 00000000..3d432cab --- /dev/null +++ b/docs/agents/task-logs/daily/2026-02-16_docs-code-investigator_completed.md @@ -0,0 +1,21 @@ +# Agent Task Log: docs-code-investigator + +- **Status**: Completed +- **Agent**: docs-code-investigator +- **Date**: 2026-02-16 +- **Target**: `js/services/moderationService.js` + +## Summary +Analyzed and documented `js/services/moderationService.js`, the core Trust & Safety module implementing the "Web of Trust" model. Added JSDoc to the class and public methods, and created a high-level overview document. + +## Actions +- Analyzed `js/services/moderationService.js` (2560 lines). +- Added JSDoc for `ModerationService` class and methods: `constructor`, `setViewerPubkey`, `subscribeToReports`, `getTrustedReportSummary`, `refreshViewerFromClient`. +- Added file-level architectural summary. +- Created `docs/moderationService-overview.md` with usage examples and data flow description. +- Verified with `npm run lint` and `tests/moderation-service.test.mjs` + `tests/moderation/*.test.mjs`. + +## Artifacts +- `js/services/moderationService.js` (modified) +- `docs/moderationService-overview.md` (created) +- `context/CONTEXT_2026-02-16_docs-code-investigator.md` (created) diff --git a/docs/moderationService-overview.md b/docs/moderationService-overview.md new file mode 100644 index 00000000..a5c0beda --- /dev/null +++ b/docs/moderationService-overview.md @@ -0,0 +1,99 @@ +# Moderation Service Overview + +**File**: `js/services/moderationService.js` + +## Purpose + +The `ModerationService` implements the decentralized Trust & Safety model for bitvid. It filters content based on a "Web of Trust" derived from the current user's social graph (NIP-02 Contact List, Kind 3) and administrative overrides. + +Instead of a central authority deciding what is "safe," the service aggregates reports (NIP-56) only from users the viewer follows (their "trusted contacts"). This creates a personalized moderation layer where spam and abuse are filtered by community consensus within the user's network. + +## Key Concepts + +### Web of Trust +- **Viewer Context**: The service tracks the currently logged-in user (`viewerPubkey`). +- **Trusted Contacts**: The set of pubkeys the viewer follows (from their Kind 3 event). +- **Trust Score**: The number of unique trusted users who have reported a specific piece of content. +- **Thresholds**: Actions (e.g., blurring, disabling autoplay) are triggered when the Trust Score exceeds defined limits (`AUTOPLAY_TRUST_THRESHOLD`, `BLUR_TRUST_THRESHOLD`). + +### NIP-56 Reports (Kind 1984) +- Users can report content for reasons like `nudity`, `spam`, `illegal`, etc. +- Reports are public events signed by the reporter. +- The service subscribes to reports for active content and aggregates them locally. +- **Crucially**: Reports from users *outside* the trusted contact list are ignored (unless whitelisted by admin). + +### NIP-51 Mutes (Kind 10000) +- Users can mute other users to hide their content entirely. +- The service syncs with `UserBlocks` to enforce mutes. +- It also supports "Trusted Mutes" where mutes from trusted contacts can influence visibility (though this feature is evolving). + +### Admin Overrides +- The service integrates with `AccessControl` to respect an instance-wide **Blacklist** (always hidden) and **Whitelist** (always trusted). + +## Architecture & Data Flow + +### 1. Initialization (Setting the Viewer) +When a user logs in, `setViewerPubkey(pubkey)` is called: +1. The service fetches the user's latest Kind 3 Contact List. +2. It extracts all followed pubkeys into `trustedContacts`. +3. It merges any admin-defined "Trust Seeds" (default trusted users). +4. It recomputes trust scores for all currently loaded content based on the new graph. + +### 2. Report Aggregation +When viewing a video, the app calls `subscribeToReports(eventId)`: +1. The service subscribes to Kind 1984 events referencing the video ID. +2. Incoming reports are ingested via `ingestReportEvent`. +3. `recomputeSummaryForEvent` checks if the reporter is in `trustedContacts`. +4. If trusted, the report increments the score for its category. +5. If the score crosses a threshold, the service emits a `summary` event, updating the UI (e.g., to blur the video). + +## Usage Example + +```javascript +import moderationService from './services/moderationService.js'; + +// 1. Initialize with the logged-in user +await moderationService.setViewerPubkey(myPubkey); + +// 2. Subscribe to reports for a video +await moderationService.subscribeToReports(videoEventId); + +// 3. Listen for trust score updates +const unsubscribe = moderationService.on('summary', ({ eventId, summary }) => { + if (summary.totalTrusted >= 1) { + console.log(`Video ${eventId} is flagged by trusted users!`); + // UI logic: blur thumbnail, show warning + } +}); + +// 4. Submit a report (as the viewer) +await moderationService.submitReport({ + eventId: videoEventId, + type: 'nudity', + targetPubkey: videoAuthorPubkey, + content: 'Contains explicit content' +}); +``` + +## Public API Summary + +| Method | Description | +| :--- | :--- | +| `setViewerPubkey(pubkey)` | Sets the active user and rebuilds the trust graph from their contacts. | +| `subscribeToReports(eventId)` | Starts listening for NIP-56 reports for the given event ID. | +| `submitReport(params)` | Publishes a signed Kind 1984 report event to relays. | +| `getTrustedReportSummary(eventId)` | Returns the current aggregated trust score and report types. | +| `isPubkeyBlockedByViewer(pubkey)` | Checks if the author is blocked/muted by the viewer. | +| `refreshViewerFromClient()` | Helper to sync the viewer with the active Nostr client state. | + +## Invariants + +- **Trust is Subjective**: A report only counts if the *viewer* follows the reporter. Changing the viewer changes all trust scores immediately. +- **Admin Trumps Trust**: Blacklisted users are ignored even if followed. Whitelisted users are trusted even if not followed. +- **Reactive**: The service emits events (`summary`, `contacts`, `user-blocks`) to drive UI updates; it does not directly manipulate the DOM. + +## When to Change + +- **Refactoring**: Logic for "Trusted Mutes" (NIP-51 lists from others) is complex and partially deprecated/evolved. This area may need cleanup. +- **Performance**: If the number of reports or contacts grows large, the in-memory aggregation (`reportEvents` map) might need optimization or worker offloading. +- **Protocol Updates**: Changes to NIP-56 or NIP-51 should be reflected here. diff --git a/js/services/moderationService.js b/js/services/moderationService.js index a41c7ebd..86c6e906 100644 --- a/js/services/moderationService.js +++ b/js/services/moderationService.js @@ -1,3 +1,19 @@ +/** + * @fileoverview + * Moderation Service - Decentralized Trust & Safety. + * + * Implements a "Web of Trust" model where content visibility and safety actions (blur, hide) + * are determined by aggregating reports (NIP-56) and mutes (NIP-51) from the user's + * trusted social graph (follows). + * + * Flow: + * 1. setViewerPubkey(pubkey) loads the user's Kind 3 contacts -> trustedContacts. + * 2. subscribeToReports(eventId) listens for Kind 1984 reports. + * 3. ingestReportEvent(event) stores reports in memory. + * 4. recomputeSummaryForEvent(eventId) checks if reporter is in trustedContacts. + * 5. If trusted reports > threshold, emits 'summary' event to update UI. + */ + import { getActiveSigner, nostrClient, @@ -528,22 +544,16 @@ function resolveStorage() { * Implements a "Web of Trust" model where content visibility and safety actions (blur, hide) * are determined by aggregating reports (NIP-56) and mutes (NIP-51) from the user's * trusted social graph (follows). - * - * Architecture: - * - **Viewer Context**: Tracks the currently logged-in user and their Kind 3 Contact List. - * - **Trust Graph**: Combines contacts + admin seeds + whitelists. - * - **Report Aggregation**: Counts reports only from trusted users. - * - **Thresholds**: Triggers actions (blur, block autoplay) when trusted report counts exceed limits. */ export class ModerationService { /** * @param {object} options - * @param {object} [options.nostrClient] - The Nostr client instance (dependency injection). - * @param {object} [options.logger] - System logger. - * @param {object} [options.userBlocks] - User block manager instance. - * @param {object} [options.accessControl] - Admin whitelist/blacklist controller. - * @param {object} [options.userLogger] - User-facing logger for notifications. - * @param {function} [options.requestExtensionPermissions] - Helper to request NIP-07 permissions. + * @param {object} [options.nostrClient] Nostr client. + * @param {object} [options.logger] System logger. + * @param {object} [options.userBlocks] User block manager. + * @param {object} [options.accessControl] Admin access control. + * @param {object} [options.userLogger] User-facing logger. + * @param {function} [options.requestExtensionPermissions] NIP-07 helper. */ constructor({ nostrClient: client = null, @@ -1568,6 +1578,12 @@ export class ModerationService { throw new Error("Mute list management is delegated to userBlocks."); } + /** + * Checks if an author is muted by any of the trusted contacts. + * + * @param {string} pubkey - The author's pubkey. + * @returns {boolean} True if at least one trusted contact mutes this author. + */ isAuthorMutedByTrusted(pubkey) { const muters = this.getActiveTrustedMutersForAuthor(pubkey); return muters.length > 0; @@ -1577,6 +1593,13 @@ export class ModerationService { return this.getActiveTrustedMutersForAuthor(pubkey); } + /** + * Checks if a pubkey is blocked or muted by the current viewer. + * Delegates to the UserBlocks manager. + * + * @param {string} pubkey - The pubkey to check. + * @returns {boolean} True if blocked or muted by viewer. + */ isPubkeyBlockedByViewer(pubkey) { if (!this.userBlocks || typeof this.userBlocks.isBlocked !== "function") { return false; @@ -1728,6 +1751,12 @@ export class ModerationService { return this.nostrClient.ensurePool(); } + /** + * Checks the current Nostr client state and updates the viewer if changed. + * Useful when the user logs in or switches accounts externally. + * + * @returns {Promise} The active viewer pubkey. + */ async refreshViewerFromClient() { const clientPubkey = normalizeHex( this.nostrClient?.pubkey || this.nostrClient?.sessionActor?.pubkey, @@ -2019,6 +2048,15 @@ export class ModerationService { this.activeSubscriptions.delete(normalized); } + /** + * Subscribes to Kind 1984 reports for a specific event. + * + * This fetches recent history (backfill) and opens a live subscription. + * Reports are ingested and trigger summary recomputation. + * + * @param {string} eventId - The ID of the event to monitor. + * @returns {Promise} + */ async subscribeToReports(eventId) { const normalized = normalizeEventId(eventId); if (!normalized) { @@ -2271,6 +2309,12 @@ export class ModerationService { this.emit("summary", { eventId: normalized, summary: cloneSummary(summary) }); } + /** + * Retrieves the current aggregated trust score for an event. + * + * @param {string} eventId - The event ID. + * @returns {object} The summary object ({ totalTrusted, types, ... }). + */ getTrustedReportSummary(eventId) { const normalized = normalizeEventId(eventId); if (!normalized) {