mirror of
https://github.com/layer-systems/relay.git
synced 2026-06-05 10:01:24 +02:00
WIP
This commit is contained in:
114
REPORTS.md
Normal file
114
REPORTS.md
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
# NIP-56 Reports Implementation
|
||||||
|
|
||||||
|
This relay implements [NIP-56 (Reporting)](https://nips.nostr.com/) for handling content reports and integrates them with the [NIP-86 Management API](https://nips.nostr.com/86).
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### Report Storage
|
||||||
|
- Automatically stores all kind `1984` events (reports) to a dedicated database table
|
||||||
|
- Extracts report metadata:
|
||||||
|
- Reporter pubkey
|
||||||
|
- Reported pubkey (from `p` tag)
|
||||||
|
- Reported event ID (from `e` tag, optional)
|
||||||
|
- Report type (nudity, malware, profanity, illegal, spam, impersonation, other)
|
||||||
|
- Report content/reason
|
||||||
|
|
||||||
|
### Report Management via NIP-86
|
||||||
|
|
||||||
|
The following NIP-86 management methods are available:
|
||||||
|
|
||||||
|
#### `listevents needingmoderation`
|
||||||
|
Returns all unresolved reports showing:
|
||||||
|
- Event ID or pubkey that was reported
|
||||||
|
- Report type, content, and reporter information
|
||||||
|
- Only shows reports that haven't been resolved yet
|
||||||
|
|
||||||
|
#### `allowevent`
|
||||||
|
- Marks all reports about the specified event/pubkey as resolved
|
||||||
|
- Adds the event to the `allowed_events` table
|
||||||
|
- Useful for dismissing false reports
|
||||||
|
|
||||||
|
#### `banevent`
|
||||||
|
- Marks all reports about the specified event/pubkey as resolved
|
||||||
|
- Adds the event to the `banned_events` table
|
||||||
|
- Deletes the actual event from the relay (if it exists)
|
||||||
|
- Prevents the event from being accepted in the future
|
||||||
|
|
||||||
|
#### `listbannedevents`
|
||||||
|
Returns all events that have been banned, with reasons
|
||||||
|
|
||||||
|
#### `listaLlowedevents`
|
||||||
|
Returns all events that have been explicitly allowed (reports dismissed)
|
||||||
|
|
||||||
|
### Event Rejection
|
||||||
|
The relay automatically rejects:
|
||||||
|
- Events from banned pubkeys
|
||||||
|
- Events with IDs in the banned events list
|
||||||
|
|
||||||
|
## Database Schema
|
||||||
|
|
||||||
|
### `reports` table
|
||||||
|
```sql
|
||||||
|
CREATE TABLE reports (
|
||||||
|
id TEXT PRIMARY KEY, -- Report event ID
|
||||||
|
reporter_pubkey TEXT NOT NULL, -- Who submitted the report
|
||||||
|
reported_event_id TEXT, -- Event being reported (optional)
|
||||||
|
reported_pubkey TEXT NOT NULL, -- Pubkey being reported
|
||||||
|
report_type TEXT NOT NULL, -- nudity, malware, profanity, etc.
|
||||||
|
content TEXT, -- Additional details from reporter
|
||||||
|
resolved BOOLEAN DEFAULT FALSE, -- Whether report has been handled
|
||||||
|
resolution TEXT, -- How it was resolved
|
||||||
|
created_at TIMESTAMP DEFAULT NOW()
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### `banned_events` table
|
||||||
|
```sql
|
||||||
|
CREATE TABLE banned_events (
|
||||||
|
event_id TEXT PRIMARY KEY,
|
||||||
|
reason TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT NOW()
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### `allowed_events` table
|
||||||
|
```sql
|
||||||
|
CREATE TABLE allowed_events (
|
||||||
|
event_id TEXT PRIMARY KEY,
|
||||||
|
reason TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT NOW()
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage Example
|
||||||
|
|
||||||
|
### Viewing Reports
|
||||||
|
Use a NIP-86 compatible client to call:
|
||||||
|
```
|
||||||
|
listeventsNeedingModeration
|
||||||
|
```
|
||||||
|
|
||||||
|
### Handling a Report
|
||||||
|
Ban a reported event:
|
||||||
|
```
|
||||||
|
banEvent("event_id_here", "confirmed spam")
|
||||||
|
```
|
||||||
|
|
||||||
|
Or dismiss a false report:
|
||||||
|
```
|
||||||
|
allowEvent("event_id_here", "false report - content is acceptable")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Report Types (NIP-56)
|
||||||
|
|
||||||
|
- `nudity` - depictions of nudity, porn, etc.
|
||||||
|
- `malware` - virus, trojan horse, worm, etc.
|
||||||
|
- `profanity` - profanity, hateful speech, etc.
|
||||||
|
- `illegal` - something which may be illegal in some jurisdiction
|
||||||
|
- `spam` - spam
|
||||||
|
- `impersonation` - someone pretending to be someone else
|
||||||
|
- `other` - for reports that don't fit in the above categories
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
All NIP-86 management endpoints require authentication with the relay owner's pubkey (configured via `RELAY_PUBKEY` environment variable).
|
||||||
216
main.go
216
main.go
@@ -49,6 +49,48 @@ func initManagementDB(db *sql.DB) error {
|
|||||||
return fmt.Errorf("failed to create banned_pubkeys table: %w", err)
|
return fmt.Errorf("failed to create banned_pubkeys table: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// create reports table for NIP-56
|
||||||
|
_, err = db.Exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS reports (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
reporter_pubkey TEXT NOT NULL,
|
||||||
|
reported_event_id TEXT,
|
||||||
|
reported_pubkey TEXT NOT NULL,
|
||||||
|
report_type TEXT NOT NULL,
|
||||||
|
content TEXT,
|
||||||
|
resolved BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
resolution TEXT,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||||
|
)
|
||||||
|
`)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create reports table: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// create banned_events table
|
||||||
|
_, err = db.Exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS banned_events (
|
||||||
|
event_id TEXT PRIMARY KEY,
|
||||||
|
reason TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||||
|
)
|
||||||
|
`)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create banned_events table: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// create allowed_events table
|
||||||
|
_, err = db.Exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS allowed_events (
|
||||||
|
event_id TEXT PRIMARY KEY,
|
||||||
|
reason TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||||
|
)
|
||||||
|
`)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create allowed_events table: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,6 +134,42 @@ func main() {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Store NIP-56 report events
|
||||||
|
relay.StoreEvent = append(relay.StoreEvent, func(ctx context.Context, event *nostr.Event) error {
|
||||||
|
if event.Kind == 1984 {
|
||||||
|
// Parse report tags
|
||||||
|
var reportedEventId, reportedPubkey, reportType string
|
||||||
|
|
||||||
|
// Extract reported pubkey and type from p tag
|
||||||
|
if pTag := event.Tags.Find("p"); len(pTag) >= 2 {
|
||||||
|
reportedPubkey = pTag[1]
|
||||||
|
if len(pTag) >= 3 {
|
||||||
|
reportType = pTag[2]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract reported event id and type from e tag
|
||||||
|
if eTag := event.Tags.Find("e"); len(eTag) >= 2 {
|
||||||
|
reportedEventId = eTag[1]
|
||||||
|
if len(eTag) >= 3 && reportType == "" {
|
||||||
|
reportType = eTag[2]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if reportedPubkey != "" && reportType != "" {
|
||||||
|
_, err := managementDB.ExecContext(ctx, `
|
||||||
|
INSERT INTO reports (id, reporter_pubkey, reported_event_id, reported_pubkey, report_type, content, created_at)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||||
|
ON CONFLICT (id) DO NOTHING
|
||||||
|
`, event.ID, event.PubKey, reportedEventId, reportedPubkey, reportType, event.Content, time.Unix(int64(event.CreatedAt), 0))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to store report: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
relay.RejectEvent = append(relay.RejectEvent,
|
relay.RejectEvent = append(relay.RejectEvent,
|
||||||
func(ctx context.Context, event *nostr.Event) (reject bool, msg string) {
|
func(ctx context.Context, event *nostr.Event) (reject bool, msg string) {
|
||||||
var reason string
|
var reason string
|
||||||
@@ -108,6 +186,23 @@ func main() {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Check for banned events
|
||||||
|
relay.RejectEvent = append(relay.RejectEvent,
|
||||||
|
func(ctx context.Context, event *nostr.Event) (reject bool, msg string) {
|
||||||
|
var reason string
|
||||||
|
row := managementDB.QueryRowContext(ctx, `SELECT reason FROM banned_events WHERE event_id = $1`, event.ID)
|
||||||
|
switch err := row.Scan(&reason); err {
|
||||||
|
case sql.ErrNoRows:
|
||||||
|
return false, ""
|
||||||
|
case nil:
|
||||||
|
return true, fmt.Sprintf("event %s banned: %s", event.ID, reason)
|
||||||
|
default:
|
||||||
|
// on unexpected DB errors, do not reject the event solely because of the failure
|
||||||
|
return false, ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
// // there are many other configurable things you can set
|
// // there are many other configurable things you can set
|
||||||
// relay.RejectEvent = append(relay.RejectEvent,
|
// relay.RejectEvent = append(relay.RejectEvent,
|
||||||
// // built-in policies
|
// // built-in policies
|
||||||
@@ -190,6 +285,127 @@ func main() {
|
|||||||
return result, rows.Err()
|
return result, rows.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
relay.ManagementAPI.ListEventsNeedingModeration = func(ctx context.Context) ([]nip86.IDReason, error) {
|
||||||
|
rows, err := managementDB.Query(`
|
||||||
|
SELECT COALESCE(reported_event_id, reported_pubkey),
|
||||||
|
CONCAT(report_type, ': ', content, ' (reported by ', reporter_pubkey, ')')
|
||||||
|
FROM reports
|
||||||
|
WHERE resolved = FALSE
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
`)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var result []nip86.IDReason
|
||||||
|
for rows.Next() {
|
||||||
|
var ir nip86.IDReason
|
||||||
|
if err := rows.Scan(&ir.ID, &ir.Reason); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result = append(result, ir)
|
||||||
|
}
|
||||||
|
return result, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
relay.ManagementAPI.AllowEvent = func(ctx context.Context, id string, reason string) error {
|
||||||
|
// Mark reports about this event as resolved
|
||||||
|
_, err := managementDB.Exec(`
|
||||||
|
UPDATE reports
|
||||||
|
SET resolved = TRUE, resolution = $2
|
||||||
|
WHERE reported_event_id = $1 OR reported_pubkey = $1
|
||||||
|
`, id, "allowed: "+reason)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add to allowed_events table
|
||||||
|
_, err = managementDB.Exec(`
|
||||||
|
INSERT INTO allowed_events (event_id, reason, created_at)
|
||||||
|
VALUES ($1, $2, $3)
|
||||||
|
ON CONFLICT (event_id) DO UPDATE SET reason = $2, created_at = $3
|
||||||
|
`, id, reason, time.Now())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
relay.ManagementAPI.BanEvent = func(ctx context.Context, id string, reason string) error {
|
||||||
|
// Mark reports about this event as resolved
|
||||||
|
_, err := managementDB.Exec(`
|
||||||
|
UPDATE reports
|
||||||
|
SET resolved = TRUE, resolution = $2
|
||||||
|
WHERE reported_event_id = $1 OR reported_pubkey = $1
|
||||||
|
`, id, "banned: "+reason)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add to banned_events table
|
||||||
|
_, err = managementDB.Exec(`
|
||||||
|
INSERT INTO banned_events (event_id, reason, created_at)
|
||||||
|
VALUES ($1, $2, $3)
|
||||||
|
ON CONFLICT (event_id) DO UPDATE SET reason = $2, created_at = $3
|
||||||
|
`, id, reason, time.Now())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query and delete the event from the main event store
|
||||||
|
for _, query := range relay.QueryEvents {
|
||||||
|
ch, err := query(ctx, nostr.Filter{IDs: []string{id}})
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
evt := <-ch
|
||||||
|
if evt != nil {
|
||||||
|
for _, deleter := range relay.DeleteEvent {
|
||||||
|
if err := deleter(ctx, evt); err != nil {
|
||||||
|
return fmt.Errorf("failed to delete event: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
relay.ManagementAPI.ListBannedEvents = func(ctx context.Context) ([]nip86.IDReason, error) {
|
||||||
|
rows, err := managementDB.Query(`SELECT event_id, reason FROM banned_events ORDER BY created_at DESC`)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var result []nip86.IDReason
|
||||||
|
for rows.Next() {
|
||||||
|
var ir nip86.IDReason
|
||||||
|
if err := rows.Scan(&ir.ID, &ir.Reason); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result = append(result, ir)
|
||||||
|
}
|
||||||
|
return result, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
relay.ManagementAPI.ListAllowedEvents = func(ctx context.Context) ([]nip86.IDReason, error) {
|
||||||
|
rows, err := managementDB.Query(`SELECT event_id, reason FROM allowed_events ORDER BY created_at DESC`)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var result []nip86.IDReason
|
||||||
|
for rows.Next() {
|
||||||
|
var ir nip86.IDReason
|
||||||
|
if err := rows.Scan(&ir.ID, &ir.Reason); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result = append(result, ir)
|
||||||
|
}
|
||||||
|
return result, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
mux := relay.Router()
|
mux := relay.Router()
|
||||||
// set up other http handlers
|
// set up other http handlers
|
||||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|||||||
Reference in New Issue
Block a user