Skip to content

feat(nip56): add WoT-weighted content reporting - #770

Open
Priyanshubhartistm wants to merge 5 commits into
cameri:mainfrom
Priyanshubhartistm:feat/content-reporting
Open

Priyanshubhartistm wants to merge 5 commits into
cameri:mainfrom
Priyanshubhartistm:feat/content-reporting

Conversation

@Priyanshubhartistm

Copy link
Copy Markdown
Collaborator

Description

Accepts and stores kind-1984 NIP-56 report events (p/e tags identifying the reported pubkey and/or event, plus a standard report type spam, nudity, illegal, impersonation, malware, profanity, other). Each report is scored by the reporter's WoT distance from wot.seedPubkey: a direct follow gets full weight, weight halves each additional hop, and a pubkey outside the trust graph entirely gets zero stored, but scored near zero as intended.

A new nip56.trustedModerators pubkey list gets maximum weight regardless of distance, and a report from one of those pubkeys is flagged actionable in the reports table. This PR records that flag but does not yet execute any automatic action (hiding content, deprioritizing in query results) that's left for a follow-up PR once there's a consumer for it, such as an admin/management API for reviewing and acting on reports.

The report event itself is always stored and broadcast normally, independent of the nip56 feature flag; only the scoring/recording step is gated behind nip56.enabled (disabled by default).

Related Issue

Closes #769

Motivation and Context

The NIP-56 spec itself warns against automated moderation driven by public reports, since a coordinated handful of accounts can game a naive report count. Tying report weight to WoT distance from the operator's trust circle means random/unknown reporters score near zero, while reports from within the trust circle (or an explicitly configured moderator) carry real signal.

Signed-off-by: Priyanshubhartistm <bhartipriyanshustm@gmail.com>
Signed-off-by: Priyanshubhartistm <bhartipriyanshustm@gmail.com>
@changeset-bot

changeset-bot Bot commented Sep 10, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 2dcbba3

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
nostream Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coveralls

coveralls commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator

Coverage Status

coverage: 72.884% (+0.3%) from 72.608% — Priyanshubhartistm:feat/content-reporting into cameri:main

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Five unresolved moderate findings affect validation, actionability, report typing, moderator handling, and scoring.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Adds NIP-56 kind-1984 report ingestion with WoT-weighted scoring, trusted moderators, persistence, configuration, and event handling.

Changes:

  • Adds report parsing, scoring, repository persistence, and migration.
  • Wires reporting through event strategies and factories.
  • Adds settings, documentation, release notes, and unit tests.

Open findings:

  • Avoid WoT lookups for trusted moderators.
  • Require valid targets for actionable reports.
  • Validate 64-character hexadecimal p/e tags.
  • Preserve target-specific report types.
  • Correct scoring to exponential hop decay.
File summaries
File Summary
test/unit/utils/settings.spec.ts Tests NIP-56 settings.
test/unit/utils/report-scoring.spec.ts Tests report weighting.
test/unit/utils/nip56.spec.ts Tests target extraction.
test/unit/repositories/report-repository.spec.ts Tests report persistence and queries.
test/unit/handlers/event-strategies/report-event-strategy.spec.ts Tests report handling.
test/unit/factories/websocket-adapter-factory.spec.ts Tests adapter wiring.
test/unit/factories/message-handler-factory.spec.ts Tests repository wiring.
test/unit/factories/event-strategy-factory.spec.ts Tests report routing.
src/utils/settings-guided-schema.ts Adds guided NIP-56 configuration.
src/utils/report-scoring.ts Calculates WoT report weights.
src/utils/nip56.ts Extracts NIP-56 report targets.
src/repositories/report-repository.ts Persists and queries reports.
src/handlers/event-strategies/report-event-strategy.ts Processes and records reports.
src/factories/worker-factory.ts Instantiates the report repository.
src/factories/websocket-adapter-factory.ts Wires the report repository.
src/factories/message-handler-factory.ts Passes the report repository through.
src/factories/event-strategy-factory.ts Routes report events.
src/constants/base.ts Defines kind 1984.
src/@types/settings.ts Defines NIP-56 settings types.
src/@types/repositories.ts Adds the repository interface.
src/@types/report.ts Defines report models.
resources/default-settings.yaml Adds disabled defaults.
migrations/20260910_120000_create_reports_table.js Creates report storage.
CONFIGURATION.md Documents NIP-56 settings.
.changeset/content-reporting.md Documents the release.
Review details

Suppressed comments (2)

src/handlers/event-strategies/report-event-strategy.ts:58

  • When a trusted moderator submits a kind-1984 event with no e/p target, extractReportTarget returns null targets and OTHER, but this still marks the row actionable. findActionable would then expose an actionable report with nothing to act on; make actionability require at least one validated target (or reject the malformed report row).
        actionable: isTrustedModerator,

src/utils/nip56.ts:26

  • NIP-56 attaches the report type to each target tag. If an event contains both p and e tags with different types (as this test does), this stores the pubkey target using the event's type and loses its own type. Preserve one report row per target/type, or reject mixed target types, instead of silently misclassifying a report.
  const rawType = eTag?.[2] ?? pTag?.[2]
  • Files reviewed: 25/25 changed files
  • Comments generated: 3
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/handlers/event-strategies/report-event-strategy.ts Outdated
Comment thread src/utils/nip56.ts Outdated
Comment thread src/utils/report-scoring.ts Outdated
Signed-off-by: Priyanshubhartistm <bhartipriyanshustm@gmail.com>
@phoenix-server

Copy link
Copy Markdown
Collaborator

Reviewed at 635dd1bf — the five open Copilot findings are resolved in this revision (hex validation, exponential hop decay, per-target report types, the moderator lookup shortcut, and actionability requiring a target). Four things I did not see covered, plus two small ones.

1. A report with no valid target still writes a row. src/utils/nip56.ts:42-43 returns [{ reportedPubkey: null, reportedEventId: null, reportType: OTHER }] when neither tag validates, and report-event-strategy.ts:57 inserts it unconditionally. With nip56.enabled on, any pubkey can add a row to reports per free kind-1984 event with nothing in it, and nothing prunes the table. The event itself is already stored and broadcast, so the row adds nothing — if (!hasValidTarget) continue is enough, and it makes this case consistent with the actionability guard at line 66.

2. The first report after startup blocks on a full WoT rebuild. getDistance awaits ensureBuilt()scheduleRebuild(), and the report path awaits that before the handler returns. The service is explicit about not doing this: updateFollowList's comment reads "the event path must not block on a full graph rebuild." To be fair I checked the common path rather than assuming, and it is fine — the service is a process-wide singleton (wot-graph-service-factory.ts) with coalesced rebuilds, and after the first build getDistance is a Map read, so this is not a per-report cost. It is a one-time latency spike on the first report after every restart, on the path that accepts events: warm the graph at boot, or score with a fallback while the graph is not ready.

3. Multi-target reports are silently truncated. tags.find(...) at nip56.ts:36-37 takes only the first p and the first e. A report naming fifty pubkeys records one, with no log line. Either iterate all p/e tags with a sane cap (logging when the cap bites), or document the single-target limit — but it should be one or the other rather than silent.

4. The report rows and their event are not written in a transaction. The event insert happens first, then the target rows one at a time, with failures caught at report-event-strategy.ts:69. A failure part-way through leaves some rows, and the duplicate guard at line 33 means a re-sent event never retries — so the missing rows are lost with only a log line. reportRepository.create(report, client) already accepts a client, so passing a transaction is a small change.

5. Two smaller things. weight is table.float; a ratio that will eventually drive thresholds is better as decimal, or store the hop distance and derive the weight at read. And reports rows reference events and pubkeys with no retention path while events themselves are cleaned up — worth either a pruning story or an explicit note that orphan rows are expected.

Related documentation gap: with nip56.enabled: true but wot.enabled: false, getDistance returns undefined for everyone and every non-moderator report is stored with weight 0. That matches the design (record, never act), but it is not what a reader of CONFIGURATION.md would infer — a sentence there would help.

No objection to the shape of this. The per-event duplicate guard, the singleton graph service, the explicit "untrusted reports score zero and never act", and the record-now-act-later boundary are all the right calls, and the tests are honest about what they do and do not assert.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: WoT-weighted content reporting (NIP-56)

4 participants