mute.mx raw
1 // Package mute manages the relay's mute blacklist. The admin nominates one
2 // pubkey whose kind-10000 mute list is treated as the authoritative blocklist.
3 // Any event from a muted pubkey is rejected at dispatch time.
4 package mute
5
6 import (
7 "encoding/hex"
8 "fmt"
9
10 "git.smesh.lol/nostr/pkg/filter"
11 "git.smesh.lol/nostr/pkg/kind"
12 "git.smesh.lol/nostr/pkg/tag"
13 "git.smesh.lol/morly/pkg/store"
14 )
15
16 // Blacklist holds the mute blacklist state for one configured admin pubkey.
17 type Blacklist struct {
18 adminPK []byte
19 pubkeys map[string]bool
20 }
21
22 // New creates a Blacklist for the given admin pubkey (hex-encoded).
23 // Returns nil if adminHexPK is empty or invalid.
24 func New(adminHexPK string) (b *Blacklist) {
25 if adminHexPK == "" {
26 return nil
27 }
28 pk, err := hex.DecodeString(adminHexPK)
29 if err != nil || len(pk) != 32 {
30 return nil
31 }
32 return &Blacklist{adminPK: pk, pubkeys: map[string]bool{}}
33 }
34
35 // Check returns true if pubkey (hex string) is on the mute list.
36 func (b *Blacklist) Check(pubkeyHex string) (ok bool) {
37 return b.pubkeys[pubkeyHex]
38 }
39
40 // AdminPK returns the raw 32-byte admin pubkey.
41 func (b *Blacklist) AdminPK() (buf []byte) { return b.adminPK }
42
43 // Load reads the admin's latest kind-10000 event from the store and refreshes
44 // the in-memory mute set.
45 func (b *Blacklist) Load(eng *store.Engine) {
46 kind.Ensure()
47 one := uint32(1)
48 filt := &filter.F{
49 Authors: tag.NewFromBytesSlice(b.adminPK),
50 Kinds: kind.NewS(kind.MuteList),
51 Limit: &one,
52 }
53 events, err := eng.QueryEvents(filt)
54 if err != nil || len(events) == 0 {
55 return
56 }
57 m := map[string]bool{}
58 ev := events[0]
59 if ev.Tags != nil {
60 var v []byte
61 for _, t := range ev.Tags.GetAll([]byte("p")) {
62 if t.Len() < 2 {
63 continue
64 }
65 v = t.Value()
66 // Three shapes reach here: the 32-byte hash, the 64-byte hex text a
67 // client writes, and the 33-byte binary-optimized form (hash plus
68 // the NUL marker tagFieldBinary appends) that the relay's own JSON
69 // parse produces and the store round trip preserves. Only the first
70 // two were accepted, so every real kind-10000 mute list was silently
71 // ignored.
72 if len(v) == 32 {
73 m[hex.EncodeToString(v)] = true
74 } else if len(v) == 64 {
75 m[string(v)] = true
76 } else if len(v) == 33 && v[32] == 0 {
77 m[hex.EncodeToString(v[:32])] = true
78 }
79 }
80 }
81 b.pubkeys = m
82 fmt.Printf("mute blacklist: loaded %d pubkeys from %s\n", len(m), hex.EncodeToString(b.adminPK))
83 }
84
85 // Purge deletes all events in the store authored by muted pubkeys.
86 func (b *Blacklist) Purge(eng *store.Engine) {
87 total := 0
88 for pkHex := range b.pubkeys {
89 pk, _ := hex.DecodeString(pkHex)
90 if len(pk) != 32 {
91 continue
92 }
93 filt := &filter.F{Authors: tag.NewFromBytesSlice(pk)}
94 events, err := eng.QueryEvents(filt)
95 if err != nil {
96 continue
97 }
98 for _, ev := range events {
99 eng.DeleteEvent(ev.ID)
100 total++
101 }
102 }
103 if total > 0 {
104 fmt.Printf("mute blacklist: purged %d events from blacklisted authors\n", total)
105 }
106 }
107