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