curating-acl.go raw

   1  //go:build !(js && wasm)
   2  
   3  package database
   4  
   5  import (
   6  	"bytes"
   7  	"context"
   8  	"encoding/json"
   9  	"fmt"
  10  	"sort"
  11  	"time"
  12  
  13  	"github.com/dgraph-io/badger/v4"
  14  	"github.com/minio/sha256-simd"
  15  	"next.orly.dev/pkg/nostr/encoders/filter"
  16  	"next.orly.dev/pkg/nostr/encoders/hex"
  17  	"next.orly.dev/pkg/nostr/encoders/tag"
  18  )
  19  
  20  // CuratingConfig represents the configuration for curating ACL mode
  21  // This is parsed from a kind 30078 event with d-tag "curating-config"
  22  type CuratingConfig struct {
  23  	DailyLimit       int      `json:"daily_limit"`        // Max events per day for unclassified users
  24  	IPDailyLimit     int      `json:"ip_daily_limit"`     // Max events per day from a single IP (flood protection)
  25  	FirstBanHours    int      `json:"first_ban_hours"`    // IP ban duration for first offense
  26  	SecondBanHours   int      `json:"second_ban_hours"`   // IP ban duration for second+ offense
  27  	AllowedKinds     []int    `json:"allowed_kinds"`      // Explicit kind numbers
  28  	AllowedRanges    []string `json:"allowed_ranges"`     // Kind ranges like "1000-1999"
  29  	KindCategories   []string `json:"kind_categories"`    // Category IDs like "social", "dm"
  30  	ConfigEventID    string   `json:"config_event_id"`    // ID of the config event
  31  	ConfigPubkey     string   `json:"config_pubkey"`      // Pubkey that published config
  32  	ConfiguredAt     int64    `json:"configured_at"`      // Timestamp of config event
  33  }
  34  
  35  // TrustedPubkey represents an explicitly trusted publisher
  36  type TrustedPubkey struct {
  37  	Pubkey string    `json:"pubkey"`
  38  	Note   string    `json:"note,omitempty"`
  39  	Added  time.Time `json:"added"`
  40  }
  41  
  42  // BlacklistedPubkey represents a blacklisted publisher
  43  type BlacklistedPubkey struct {
  44  	Pubkey string    `json:"pubkey"`
  45  	Reason string    `json:"reason,omitempty"`
  46  	Added  time.Time `json:"added"`
  47  }
  48  
  49  // PubkeyEventCount tracks daily event counts for rate limiting
  50  type PubkeyEventCount struct {
  51  	Pubkey    string    `json:"pubkey"`
  52  	Date      string    `json:"date"` // YYYY-MM-DD format
  53  	Count     int       `json:"count"`
  54  	LastEvent time.Time `json:"last_event"`
  55  }
  56  
  57  // IPOffense tracks rate limit violations from IPs
  58  type IPOffense struct {
  59  	IP           string    `json:"ip"`
  60  	OffenseCount int       `json:"offense_count"`
  61  	PubkeysHit   []string  `json:"pubkeys_hit"` // Pubkeys that hit rate limit from this IP
  62  	LastOffense  time.Time `json:"last_offense"`
  63  }
  64  
  65  // CuratingBlockedIP represents a temporarily blocked IP with expiration
  66  type CuratingBlockedIP struct {
  67  	IP        string    `json:"ip"`
  68  	Reason    string    `json:"reason"`
  69  	ExpiresAt time.Time `json:"expires_at"`
  70  	Added     time.Time `json:"added"`
  71  }
  72  
  73  // SpamEvent represents an event flagged as spam
  74  type SpamEvent struct {
  75  	EventID string    `json:"event_id"`
  76  	Pubkey  string    `json:"pubkey"`
  77  	Reason  string    `json:"reason,omitempty"`
  78  	Added   time.Time `json:"added"`
  79  }
  80  
  81  // UnclassifiedUser represents a user who hasn't been trusted or blacklisted
  82  type UnclassifiedUser struct {
  83  	Pubkey     string    `json:"pubkey"`
  84  	EventCount int       `json:"event_count"`
  85  	LastEvent  time.Time `json:"last_event"`
  86  }
  87  
  88  // CuratingACL database operations
  89  type CuratingACL struct {
  90  	*D
  91  }
  92  
  93  // NewCuratingACL creates a new CuratingACL instance
  94  func NewCuratingACL(db *D) *CuratingACL {
  95  	return &CuratingACL{D: db}
  96  }
  97  
  98  // ==================== Configuration ====================
  99  
 100  // SaveConfig saves the curating configuration
 101  func (c *CuratingACL) SaveConfig(config CuratingConfig) error {
 102  	return c.Update(func(txn *badger.Txn) error {
 103  		key := c.getConfigKey()
 104  		data, err := json.Marshal(config)
 105  		if err != nil {
 106  			return err
 107  		}
 108  		return txn.Set(key, data)
 109  	})
 110  }
 111  
 112  // GetConfig returns the curating configuration
 113  func (c *CuratingACL) GetConfig() (CuratingConfig, error) {
 114  	var config CuratingConfig
 115  	err := c.View(func(txn *badger.Txn) error {
 116  		key := c.getConfigKey()
 117  		item, err := txn.Get(key)
 118  		if err != nil {
 119  			if err == badger.ErrKeyNotFound {
 120  				return nil // Return empty config
 121  			}
 122  			return err
 123  		}
 124  		val, err := item.ValueCopy(nil)
 125  		if err != nil {
 126  			return err
 127  		}
 128  		return json.Unmarshal(val, &config)
 129  	})
 130  	return config, err
 131  }
 132  
 133  // IsConfigured returns true if a configuration event has been set
 134  func (c *CuratingACL) IsConfigured() (bool, error) {
 135  	config, err := c.GetConfig()
 136  	if err != nil {
 137  		return false, err
 138  	}
 139  	return config.ConfigEventID != "", nil
 140  }
 141  
 142  // ==================== Trusted Pubkeys ====================
 143  
 144  // SaveTrustedPubkey saves a trusted pubkey to the database
 145  func (c *CuratingACL) SaveTrustedPubkey(pubkey string, note string) error {
 146  	return c.Update(func(txn *badger.Txn) error {
 147  		key := c.getTrustedPubkeyKey(pubkey)
 148  		trusted := TrustedPubkey{
 149  			Pubkey: pubkey,
 150  			Note:   note,
 151  			Added:  time.Now(),
 152  		}
 153  		data, err := json.Marshal(trusted)
 154  		if err != nil {
 155  			return err
 156  		}
 157  		return txn.Set(key, data)
 158  	})
 159  }
 160  
 161  // RemoveTrustedPubkey removes a trusted pubkey from the database
 162  func (c *CuratingACL) RemoveTrustedPubkey(pubkey string) error {
 163  	return c.Update(func(txn *badger.Txn) error {
 164  		key := c.getTrustedPubkeyKey(pubkey)
 165  		return txn.Delete(key)
 166  	})
 167  }
 168  
 169  // ListTrustedPubkeys returns all trusted pubkeys
 170  func (c *CuratingACL) ListTrustedPubkeys() ([]TrustedPubkey, error) {
 171  	var trusted []TrustedPubkey
 172  	err := c.View(func(txn *badger.Txn) error {
 173  		prefix := c.getTrustedPubkeyPrefix()
 174  		it := txn.NewIterator(badger.IteratorOptions{Prefix: prefix})
 175  		defer it.Close()
 176  
 177  		for it.Rewind(); it.Valid(); it.Next() {
 178  			item := it.Item()
 179  			val, err := item.ValueCopy(nil)
 180  			if err != nil {
 181  				continue
 182  			}
 183  			var t TrustedPubkey
 184  			if err := json.Unmarshal(val, &t); err != nil {
 185  				continue
 186  			}
 187  			trusted = append(trusted, t)
 188  		}
 189  		return nil
 190  	})
 191  	return trusted, err
 192  }
 193  
 194  // IsPubkeyTrusted checks if a pubkey is trusted
 195  func (c *CuratingACL) IsPubkeyTrusted(pubkey string) (bool, error) {
 196  	var trusted bool
 197  	err := c.View(func(txn *badger.Txn) error {
 198  		key := c.getTrustedPubkeyKey(pubkey)
 199  		_, err := txn.Get(key)
 200  		if err == badger.ErrKeyNotFound {
 201  			trusted = false
 202  			return nil
 203  		}
 204  		if err != nil {
 205  			return err
 206  		}
 207  		trusted = true
 208  		return nil
 209  	})
 210  	return trusted, err
 211  }
 212  
 213  // ==================== Blacklisted Pubkeys ====================
 214  
 215  // SaveBlacklistedPubkey saves a blacklisted pubkey to the database
 216  func (c *CuratingACL) SaveBlacklistedPubkey(pubkey string, reason string) error {
 217  	return c.Update(func(txn *badger.Txn) error {
 218  		key := c.getBlacklistedPubkeyKey(pubkey)
 219  		blacklisted := BlacklistedPubkey{
 220  			Pubkey: pubkey,
 221  			Reason: reason,
 222  			Added:  time.Now(),
 223  		}
 224  		data, err := json.Marshal(blacklisted)
 225  		if err != nil {
 226  			return err
 227  		}
 228  		return txn.Set(key, data)
 229  	})
 230  }
 231  
 232  // RemoveBlacklistedPubkey removes a blacklisted pubkey from the database
 233  func (c *CuratingACL) RemoveBlacklistedPubkey(pubkey string) error {
 234  	return c.Update(func(txn *badger.Txn) error {
 235  		key := c.getBlacklistedPubkeyKey(pubkey)
 236  		return txn.Delete(key)
 237  	})
 238  }
 239  
 240  // ListBlacklistedPubkeys returns all blacklisted pubkeys
 241  func (c *CuratingACL) ListBlacklistedPubkeys() ([]BlacklistedPubkey, error) {
 242  	var blacklisted []BlacklistedPubkey
 243  	err := c.View(func(txn *badger.Txn) error {
 244  		prefix := c.getBlacklistedPubkeyPrefix()
 245  		it := txn.NewIterator(badger.IteratorOptions{Prefix: prefix})
 246  		defer it.Close()
 247  
 248  		for it.Rewind(); it.Valid(); it.Next() {
 249  			item := it.Item()
 250  			val, err := item.ValueCopy(nil)
 251  			if err != nil {
 252  				continue
 253  			}
 254  			var b BlacklistedPubkey
 255  			if err := json.Unmarshal(val, &b); err != nil {
 256  				continue
 257  			}
 258  			blacklisted = append(blacklisted, b)
 259  		}
 260  		return nil
 261  	})
 262  	return blacklisted, err
 263  }
 264  
 265  // IsPubkeyBlacklisted checks if a pubkey is blacklisted
 266  func (c *CuratingACL) IsPubkeyBlacklisted(pubkey string) (bool, error) {
 267  	var blacklisted bool
 268  	err := c.View(func(txn *badger.Txn) error {
 269  		key := c.getBlacklistedPubkeyKey(pubkey)
 270  		_, err := txn.Get(key)
 271  		if err == badger.ErrKeyNotFound {
 272  			blacklisted = false
 273  			return nil
 274  		}
 275  		if err != nil {
 276  			return err
 277  		}
 278  		blacklisted = true
 279  		return nil
 280  	})
 281  	return blacklisted, err
 282  }
 283  
 284  // ==================== Event Counting ====================
 285  
 286  // GetEventCount returns the event count for a pubkey on a specific date
 287  func (c *CuratingACL) GetEventCount(pubkey, date string) (int, error) {
 288  	var count int
 289  	err := c.View(func(txn *badger.Txn) error {
 290  		key := c.getEventCountKey(pubkey, date)
 291  		item, err := txn.Get(key)
 292  		if err == badger.ErrKeyNotFound {
 293  			count = 0
 294  			return nil
 295  		}
 296  		if err != nil {
 297  			return err
 298  		}
 299  		val, err := item.ValueCopy(nil)
 300  		if err != nil {
 301  			return err
 302  		}
 303  		var ec PubkeyEventCount
 304  		if err := json.Unmarshal(val, &ec); err != nil {
 305  			return err
 306  		}
 307  		count = ec.Count
 308  		return nil
 309  	})
 310  	return count, err
 311  }
 312  
 313  // IncrementEventCount increments and returns the new event count for a pubkey
 314  func (c *CuratingACL) IncrementEventCount(pubkey, date string) (int, error) {
 315  	var newCount int
 316  	err := c.Update(func(txn *badger.Txn) error {
 317  		key := c.getEventCountKey(pubkey, date)
 318  		var ec PubkeyEventCount
 319  
 320  		item, err := txn.Get(key)
 321  		if err == badger.ErrKeyNotFound {
 322  			ec = PubkeyEventCount{
 323  				Pubkey:    pubkey,
 324  				Date:      date,
 325  				Count:     0,
 326  				LastEvent: time.Now(),
 327  			}
 328  		} else if err != nil {
 329  			return err
 330  		} else {
 331  			val, err := item.ValueCopy(nil)
 332  			if err != nil {
 333  				return err
 334  			}
 335  			if err := json.Unmarshal(val, &ec); err != nil {
 336  				return err
 337  			}
 338  		}
 339  
 340  		ec.Count++
 341  		ec.LastEvent = time.Now()
 342  		newCount = ec.Count
 343  
 344  		data, err := json.Marshal(ec)
 345  		if err != nil {
 346  			return err
 347  		}
 348  		return txn.Set(key, data)
 349  	})
 350  	return newCount, err
 351  }
 352  
 353  // CleanupOldEventCounts removes event counts older than the specified date
 354  func (c *CuratingACL) CleanupOldEventCounts(beforeDate string) error {
 355  	return c.Update(func(txn *badger.Txn) error {
 356  		prefix := c.getEventCountPrefix()
 357  		it := txn.NewIterator(badger.IteratorOptions{Prefix: prefix})
 358  		defer it.Close()
 359  
 360  		var keysToDelete [][]byte
 361  		for it.Rewind(); it.Valid(); it.Next() {
 362  			item := it.Item()
 363  			val, err := item.ValueCopy(nil)
 364  			if err != nil {
 365  				continue
 366  			}
 367  			var ec PubkeyEventCount
 368  			if err := json.Unmarshal(val, &ec); err != nil {
 369  				continue
 370  			}
 371  			if ec.Date < beforeDate {
 372  				keysToDelete = append(keysToDelete, item.KeyCopy(nil))
 373  			}
 374  		}
 375  
 376  		for _, key := range keysToDelete {
 377  			if err := txn.Delete(key); err != nil {
 378  				return err
 379  			}
 380  		}
 381  		return nil
 382  	})
 383  }
 384  
 385  // ==================== IP Event Counting ====================
 386  
 387  // IPEventCount tracks events from an IP address per day (flood protection)
 388  type IPEventCount struct {
 389  	IP        string    `json:"ip"`
 390  	Date      string    `json:"date"`
 391  	Count     int       `json:"count"`
 392  	LastEvent time.Time `json:"last_event"`
 393  }
 394  
 395  // GetIPEventCount returns the total event count for an IP on a specific date
 396  func (c *CuratingACL) GetIPEventCount(ip, date string) (int, error) {
 397  	var count int
 398  	err := c.View(func(txn *badger.Txn) error {
 399  		key := c.getIPEventCountKey(ip, date)
 400  		item, err := txn.Get(key)
 401  		if err == badger.ErrKeyNotFound {
 402  			count = 0
 403  			return nil
 404  		}
 405  		if err != nil {
 406  			return err
 407  		}
 408  		val, err := item.ValueCopy(nil)
 409  		if err != nil {
 410  			return err
 411  		}
 412  		var ec IPEventCount
 413  		if err := json.Unmarshal(val, &ec); err != nil {
 414  			return err
 415  		}
 416  		count = ec.Count
 417  		return nil
 418  	})
 419  	return count, err
 420  }
 421  
 422  // IncrementIPEventCount increments and returns the new event count for an IP
 423  func (c *CuratingACL) IncrementIPEventCount(ip, date string) (int, error) {
 424  	var newCount int
 425  	err := c.Update(func(txn *badger.Txn) error {
 426  		key := c.getIPEventCountKey(ip, date)
 427  		var ec IPEventCount
 428  
 429  		item, err := txn.Get(key)
 430  		if err == badger.ErrKeyNotFound {
 431  			ec = IPEventCount{
 432  				IP:        ip,
 433  				Date:      date,
 434  				Count:     0,
 435  				LastEvent: time.Now(),
 436  			}
 437  		} else if err != nil {
 438  			return err
 439  		} else {
 440  			val, err := item.ValueCopy(nil)
 441  			if err != nil {
 442  				return err
 443  			}
 444  			if err := json.Unmarshal(val, &ec); err != nil {
 445  				return err
 446  			}
 447  		}
 448  
 449  		ec.Count++
 450  		ec.LastEvent = time.Now()
 451  		newCount = ec.Count
 452  
 453  		data, err := json.Marshal(ec)
 454  		if err != nil {
 455  			return err
 456  		}
 457  		return txn.Set(key, data)
 458  	})
 459  	return newCount, err
 460  }
 461  
 462  // CleanupOldIPEventCounts removes IP event counts older than the specified date
 463  func (c *CuratingACL) CleanupOldIPEventCounts(beforeDate string) error {
 464  	return c.Update(func(txn *badger.Txn) error {
 465  		prefix := c.getIPEventCountPrefix()
 466  		it := txn.NewIterator(badger.IteratorOptions{Prefix: prefix})
 467  		defer it.Close()
 468  
 469  		var keysToDelete [][]byte
 470  		for it.Rewind(); it.Valid(); it.Next() {
 471  			item := it.Item()
 472  			val, err := item.ValueCopy(nil)
 473  			if err != nil {
 474  				continue
 475  			}
 476  			var ec IPEventCount
 477  			if err := json.Unmarshal(val, &ec); err != nil {
 478  				continue
 479  			}
 480  			if ec.Date < beforeDate {
 481  				keysToDelete = append(keysToDelete, item.KeyCopy(nil))
 482  			}
 483  		}
 484  
 485  		for _, key := range keysToDelete {
 486  			if err := txn.Delete(key); err != nil {
 487  				return err
 488  			}
 489  		}
 490  		return nil
 491  	})
 492  }
 493  
 494  func (c *CuratingACL) getIPEventCountKey(ip, date string) []byte {
 495  	buf := new(bytes.Buffer)
 496  	buf.WriteString("CURATING_ACL_IP_EVENT_COUNT_")
 497  	buf.WriteString(ip)
 498  	buf.WriteString("_")
 499  	buf.WriteString(date)
 500  	return buf.Bytes()
 501  }
 502  
 503  func (c *CuratingACL) getIPEventCountPrefix() []byte {
 504  	return []byte("CURATING_ACL_IP_EVENT_COUNT_")
 505  }
 506  
 507  // ==================== IP Offense Tracking ====================
 508  
 509  // GetIPOffense returns the offense record for an IP
 510  func (c *CuratingACL) GetIPOffense(ip string) (*IPOffense, error) {
 511  	var offense *IPOffense
 512  	err := c.View(func(txn *badger.Txn) error {
 513  		key := c.getIPOffenseKey(ip)
 514  		item, err := txn.Get(key)
 515  		if err == badger.ErrKeyNotFound {
 516  			return nil
 517  		}
 518  		if err != nil {
 519  			return err
 520  		}
 521  		val, err := item.ValueCopy(nil)
 522  		if err != nil {
 523  			return err
 524  		}
 525  		offense = new(IPOffense)
 526  		return json.Unmarshal(val, offense)
 527  	})
 528  	return offense, err
 529  }
 530  
 531  // RecordIPOffense records a rate limit violation from an IP for a pubkey
 532  // Returns the new offense count
 533  func (c *CuratingACL) RecordIPOffense(ip, pubkey string) (int, error) {
 534  	var newCount int
 535  	err := c.Update(func(txn *badger.Txn) error {
 536  		key := c.getIPOffenseKey(ip)
 537  		var offense IPOffense
 538  
 539  		item, err := txn.Get(key)
 540  		if err == badger.ErrKeyNotFound {
 541  			offense = IPOffense{
 542  				IP:           ip,
 543  				OffenseCount: 0,
 544  				PubkeysHit:   []string{},
 545  				LastOffense:  time.Now(),
 546  			}
 547  		} else if err != nil {
 548  			return err
 549  		} else {
 550  			val, err := item.ValueCopy(nil)
 551  			if err != nil {
 552  				return err
 553  			}
 554  			if err := json.Unmarshal(val, &offense); err != nil {
 555  				return err
 556  			}
 557  		}
 558  
 559  		// Add pubkey if not already in list
 560  		found := false
 561  		for _, p := range offense.PubkeysHit {
 562  			if p == pubkey {
 563  				found = true
 564  				break
 565  			}
 566  		}
 567  		if !found {
 568  			offense.PubkeysHit = append(offense.PubkeysHit, pubkey)
 569  			offense.OffenseCount++
 570  		}
 571  		offense.LastOffense = time.Now()
 572  		newCount = offense.OffenseCount
 573  
 574  		data, err := json.Marshal(offense)
 575  		if err != nil {
 576  			return err
 577  		}
 578  		return txn.Set(key, data)
 579  	})
 580  	return newCount, err
 581  }
 582  
 583  // ==================== IP Blocking ====================
 584  
 585  // BlockIP blocks an IP for a specified duration
 586  func (c *CuratingACL) BlockIP(ip string, duration time.Duration, reason string) error {
 587  	return c.Update(func(txn *badger.Txn) error {
 588  		key := c.getBlockedIPKey(ip)
 589  		blocked := CuratingBlockedIP{
 590  			IP:        ip,
 591  			Reason:    reason,
 592  			ExpiresAt: time.Now().Add(duration),
 593  			Added:     time.Now(),
 594  		}
 595  		data, err := json.Marshal(blocked)
 596  		if err != nil {
 597  			return err
 598  		}
 599  		return txn.Set(key, data)
 600  	})
 601  }
 602  
 603  // UnblockIP removes an IP from the blocked list
 604  func (c *CuratingACL) UnblockIP(ip string) error {
 605  	return c.Update(func(txn *badger.Txn) error {
 606  		key := c.getBlockedIPKey(ip)
 607  		return txn.Delete(key)
 608  	})
 609  }
 610  
 611  // IsIPBlocked checks if an IP is blocked and returns expiration time
 612  func (c *CuratingACL) IsIPBlocked(ip string) (bool, time.Time, error) {
 613  	var blocked bool
 614  	var expiresAt time.Time
 615  	err := c.View(func(txn *badger.Txn) error {
 616  		key := c.getBlockedIPKey(ip)
 617  		item, err := txn.Get(key)
 618  		if err == badger.ErrKeyNotFound {
 619  			blocked = false
 620  			return nil
 621  		}
 622  		if err != nil {
 623  			return err
 624  		}
 625  		val, err := item.ValueCopy(nil)
 626  		if err != nil {
 627  			return err
 628  		}
 629  		var b CuratingBlockedIP
 630  		if err := json.Unmarshal(val, &b); err != nil {
 631  			return err
 632  		}
 633  		if time.Now().After(b.ExpiresAt) {
 634  			// Block has expired
 635  			blocked = false
 636  			return nil
 637  		}
 638  		blocked = true
 639  		expiresAt = b.ExpiresAt
 640  		return nil
 641  	})
 642  	return blocked, expiresAt, err
 643  }
 644  
 645  // ListBlockedIPs returns all blocked IPs (including expired ones)
 646  func (c *CuratingACL) ListBlockedIPs() ([]CuratingBlockedIP, error) {
 647  	var blocked []CuratingBlockedIP
 648  	err := c.View(func(txn *badger.Txn) error {
 649  		prefix := c.getBlockedIPPrefix()
 650  		it := txn.NewIterator(badger.IteratorOptions{Prefix: prefix})
 651  		defer it.Close()
 652  
 653  		for it.Rewind(); it.Valid(); it.Next() {
 654  			item := it.Item()
 655  			val, err := item.ValueCopy(nil)
 656  			if err != nil {
 657  				continue
 658  			}
 659  			var b CuratingBlockedIP
 660  			if err := json.Unmarshal(val, &b); err != nil {
 661  				continue
 662  			}
 663  			blocked = append(blocked, b)
 664  		}
 665  		return nil
 666  	})
 667  	return blocked, err
 668  }
 669  
 670  // CleanupExpiredIPBlocks removes expired IP blocks
 671  func (c *CuratingACL) CleanupExpiredIPBlocks() error {
 672  	return c.Update(func(txn *badger.Txn) error {
 673  		prefix := c.getBlockedIPPrefix()
 674  		it := txn.NewIterator(badger.IteratorOptions{Prefix: prefix})
 675  		defer it.Close()
 676  
 677  		now := time.Now()
 678  		var keysToDelete [][]byte
 679  		for it.Rewind(); it.Valid(); it.Next() {
 680  			item := it.Item()
 681  			val, err := item.ValueCopy(nil)
 682  			if err != nil {
 683  				continue
 684  			}
 685  			var b CuratingBlockedIP
 686  			if err := json.Unmarshal(val, &b); err != nil {
 687  				continue
 688  			}
 689  			if now.After(b.ExpiresAt) {
 690  				keysToDelete = append(keysToDelete, item.KeyCopy(nil))
 691  			}
 692  		}
 693  
 694  		for _, key := range keysToDelete {
 695  			if err := txn.Delete(key); err != nil {
 696  				return err
 697  			}
 698  		}
 699  		return nil
 700  	})
 701  }
 702  
 703  // ==================== Spam Events ====================
 704  
 705  // MarkEventAsSpam marks an event as spam
 706  func (c *CuratingACL) MarkEventAsSpam(eventID, pubkey, reason string) error {
 707  	return c.Update(func(txn *badger.Txn) error {
 708  		key := c.getSpamEventKey(eventID)
 709  		spam := SpamEvent{
 710  			EventID: eventID,
 711  			Pubkey:  pubkey,
 712  			Reason:  reason,
 713  			Added:   time.Now(),
 714  		}
 715  		data, err := json.Marshal(spam)
 716  		if err != nil {
 717  			return err
 718  		}
 719  		return txn.Set(key, data)
 720  	})
 721  }
 722  
 723  // UnmarkEventAsSpam removes the spam flag from an event
 724  func (c *CuratingACL) UnmarkEventAsSpam(eventID string) error {
 725  	return c.Update(func(txn *badger.Txn) error {
 726  		key := c.getSpamEventKey(eventID)
 727  		return txn.Delete(key)
 728  	})
 729  }
 730  
 731  // IsEventSpam checks if an event is marked as spam
 732  func (c *CuratingACL) IsEventSpam(eventID string) (bool, error) {
 733  	var spam bool
 734  	err := c.View(func(txn *badger.Txn) error {
 735  		key := c.getSpamEventKey(eventID)
 736  		_, err := txn.Get(key)
 737  		if err == badger.ErrKeyNotFound {
 738  			spam = false
 739  			return nil
 740  		}
 741  		if err != nil {
 742  			return err
 743  		}
 744  		spam = true
 745  		return nil
 746  	})
 747  	return spam, err
 748  }
 749  
 750  // ListSpamEvents returns all spam events
 751  func (c *CuratingACL) ListSpamEvents() ([]SpamEvent, error) {
 752  	var spam []SpamEvent
 753  	err := c.View(func(txn *badger.Txn) error {
 754  		prefix := c.getSpamEventPrefix()
 755  		it := txn.NewIterator(badger.IteratorOptions{Prefix: prefix})
 756  		defer it.Close()
 757  
 758  		for it.Rewind(); it.Valid(); it.Next() {
 759  			item := it.Item()
 760  			val, err := item.ValueCopy(nil)
 761  			if err != nil {
 762  				continue
 763  			}
 764  			var s SpamEvent
 765  			if err := json.Unmarshal(val, &s); err != nil {
 766  				continue
 767  			}
 768  			spam = append(spam, s)
 769  		}
 770  		return nil
 771  	})
 772  	return spam, err
 773  }
 774  
 775  // ==================== Unclassified Users ====================
 776  
 777  // ListUnclassifiedUsers returns users who are neither trusted nor blacklisted
 778  // sorted by event count descending
 779  func (c *CuratingACL) ListUnclassifiedUsers(limit int) ([]UnclassifiedUser, error) {
 780  	// First, get all trusted and blacklisted pubkeys to exclude
 781  	trusted, err := c.ListTrustedPubkeys()
 782  	if err != nil {
 783  		return nil, err
 784  	}
 785  	blacklisted, err := c.ListBlacklistedPubkeys()
 786  	if err != nil {
 787  		return nil, err
 788  	}
 789  
 790  	excludeSet := make(map[string]struct{})
 791  	for _, t := range trusted {
 792  		excludeSet[t.Pubkey] = struct{}{}
 793  	}
 794  	for _, b := range blacklisted {
 795  		excludeSet[b.Pubkey] = struct{}{}
 796  	}
 797  
 798  	// Now iterate through event counts and aggregate by pubkey
 799  	pubkeyCounts := make(map[string]*UnclassifiedUser)
 800  
 801  	err = c.View(func(txn *badger.Txn) error {
 802  		prefix := c.getEventCountPrefix()
 803  		it := txn.NewIterator(badger.IteratorOptions{Prefix: prefix})
 804  		defer it.Close()
 805  
 806  		for it.Rewind(); it.Valid(); it.Next() {
 807  			item := it.Item()
 808  			val, err := item.ValueCopy(nil)
 809  			if err != nil {
 810  				continue
 811  			}
 812  			var ec PubkeyEventCount
 813  			if err := json.Unmarshal(val, &ec); err != nil {
 814  				continue
 815  			}
 816  
 817  			// Skip if trusted or blacklisted
 818  			if _, excluded := excludeSet[ec.Pubkey]; excluded {
 819  				continue
 820  			}
 821  
 822  			if existing, ok := pubkeyCounts[ec.Pubkey]; ok {
 823  				existing.EventCount += ec.Count
 824  				if ec.LastEvent.After(existing.LastEvent) {
 825  					existing.LastEvent = ec.LastEvent
 826  				}
 827  			} else {
 828  				pubkeyCounts[ec.Pubkey] = &UnclassifiedUser{
 829  					Pubkey:     ec.Pubkey,
 830  					EventCount: ec.Count,
 831  					LastEvent:  ec.LastEvent,
 832  				}
 833  			}
 834  		}
 835  		return nil
 836  	})
 837  	if err != nil {
 838  		return nil, err
 839  	}
 840  
 841  	// Convert to slice and sort by event count descending
 842  	var users []UnclassifiedUser
 843  	for _, u := range pubkeyCounts {
 844  		users = append(users, *u)
 845  	}
 846  	sort.Slice(users, func(i, j int) bool {
 847  		return users[i].EventCount > users[j].EventCount
 848  	})
 849  
 850  	// Apply limit
 851  	if limit > 0 && len(users) > limit {
 852  		users = users[:limit]
 853  	}
 854  
 855  	return users, nil
 856  }
 857  
 858  // ==================== Key Generation ====================
 859  
 860  func (c *CuratingACL) getConfigKey() []byte {
 861  	return []byte("CURATING_ACL_CONFIG")
 862  }
 863  
 864  func (c *CuratingACL) getTrustedPubkeyKey(pubkey string) []byte {
 865  	buf := new(bytes.Buffer)
 866  	buf.WriteString("CURATING_ACL_TRUSTED_PUBKEY_")
 867  	buf.WriteString(pubkey)
 868  	return buf.Bytes()
 869  }
 870  
 871  func (c *CuratingACL) getTrustedPubkeyPrefix() []byte {
 872  	return []byte("CURATING_ACL_TRUSTED_PUBKEY_")
 873  }
 874  
 875  func (c *CuratingACL) getBlacklistedPubkeyKey(pubkey string) []byte {
 876  	buf := new(bytes.Buffer)
 877  	buf.WriteString("CURATING_ACL_BLACKLISTED_PUBKEY_")
 878  	buf.WriteString(pubkey)
 879  	return buf.Bytes()
 880  }
 881  
 882  func (c *CuratingACL) getBlacklistedPubkeyPrefix() []byte {
 883  	return []byte("CURATING_ACL_BLACKLISTED_PUBKEY_")
 884  }
 885  
 886  func (c *CuratingACL) getEventCountKey(pubkey, date string) []byte {
 887  	buf := new(bytes.Buffer)
 888  	buf.WriteString("CURATING_ACL_EVENT_COUNT_")
 889  	buf.WriteString(pubkey)
 890  	buf.WriteString("_")
 891  	buf.WriteString(date)
 892  	return buf.Bytes()
 893  }
 894  
 895  func (c *CuratingACL) getEventCountPrefix() []byte {
 896  	return []byte("CURATING_ACL_EVENT_COUNT_")
 897  }
 898  
 899  func (c *CuratingACL) getIPOffenseKey(ip string) []byte {
 900  	buf := new(bytes.Buffer)
 901  	buf.WriteString("CURATING_ACL_IP_OFFENSE_")
 902  	buf.WriteString(ip)
 903  	return buf.Bytes()
 904  }
 905  
 906  func (c *CuratingACL) getBlockedIPKey(ip string) []byte {
 907  	buf := new(bytes.Buffer)
 908  	buf.WriteString("CURATING_ACL_BLOCKED_IP_")
 909  	buf.WriteString(ip)
 910  	return buf.Bytes()
 911  }
 912  
 913  func (c *CuratingACL) getBlockedIPPrefix() []byte {
 914  	return []byte("CURATING_ACL_BLOCKED_IP_")
 915  }
 916  
 917  func (c *CuratingACL) getSpamEventKey(eventID string) []byte {
 918  	buf := new(bytes.Buffer)
 919  	buf.WriteString("CURATING_ACL_SPAM_EVENT_")
 920  	buf.WriteString(eventID)
 921  	return buf.Bytes()
 922  }
 923  
 924  func (c *CuratingACL) getSpamEventPrefix() []byte {
 925  	return []byte("CURATING_ACL_SPAM_EVENT_")
 926  }
 927  
 928  // ==================== Kind Checking Helpers ====================
 929  
 930  // IsKindAllowed checks if an event kind is allowed based on config
 931  func (c *CuratingACL) IsKindAllowed(kind int, config *CuratingConfig) bool {
 932  	if config == nil {
 933  		return false
 934  	}
 935  
 936  	// Check explicit kinds
 937  	for _, k := range config.AllowedKinds {
 938  		if k == kind {
 939  			return true
 940  		}
 941  	}
 942  
 943  	// Check ranges
 944  	for _, rangeStr := range config.AllowedRanges {
 945  		if kindInRange(kind, rangeStr) {
 946  			return true
 947  		}
 948  	}
 949  
 950  	// Check categories
 951  	for _, cat := range config.KindCategories {
 952  		if kindInCategory(kind, cat) {
 953  			return true
 954  		}
 955  	}
 956  
 957  	return false
 958  }
 959  
 960  // kindInRange checks if a kind is within a range string like "1000-1999"
 961  func kindInRange(kind int, rangeStr string) bool {
 962  	var start, end int
 963  	n, err := fmt.Sscanf(rangeStr, "%d-%d", &start, &end)
 964  	if err != nil || n != 2 {
 965  		return false
 966  	}
 967  	return kind >= start && kind <= end
 968  }
 969  
 970  // kindInCategory checks if a kind belongs to a predefined category
 971  func kindInCategory(kind int, category string) bool {
 972  	categories := map[string][]int{
 973  		"social":              {0, 1, 3, 6, 7, 10002},
 974  		"dm":                  {4, 14, 1059},
 975  		"longform":            {30023, 30024},
 976  		"media":               {1063, 20, 21, 22},
 977  		"marketplace":         {30017, 30018, 30019, 30020, 1021, 1022}, // Legacy alias
 978  		"marketplace_nip15":   {30017, 30018, 30019, 30020, 1021, 1022},
 979  		"marketplace_nip99":   {30402, 30403, 30405, 30406, 31555}, // NIP-99/Gamma Markets (Plebeian Market)
 980  		"order_communication": {16, 17},                            // Gamma Markets order messages
 981  		"groups_nip29":        {9, 10, 11, 12, 9000, 9001, 9002, 39000, 39001, 39002},
 982  		"groups_nip72":        {34550, 1111, 4550},
 983  		"lists":               {10000, 10001, 10003, 30000, 30001, 30003},
 984  	}
 985  
 986  	kinds, ok := categories[category]
 987  	if !ok {
 988  		return false
 989  	}
 990  
 991  	for _, k := range kinds {
 992  		if k == kind {
 993  			return true
 994  		}
 995  	}
 996  	return false
 997  }
 998  
 999  // ==================== Database Scanning ====================
1000  
1001  // ScanResult contains the results of scanning all pubkeys in the database
1002  type ScanResult struct {
1003  	TotalPubkeys int `json:"total_pubkeys"`
1004  	TotalEvents  int `json:"total_events"`
1005  	Skipped      int `json:"skipped"` // Trusted/blacklisted users skipped
1006  }
1007  
1008  // ScanAllPubkeys scans the database to find all unique pubkeys and count their events.
1009  // This populates the event count data needed for the unclassified users list.
1010  // It uses the SerialPubkey index to find all pubkeys, then counts events for each.
1011  func (c *CuratingACL) ScanAllPubkeys() (*ScanResult, error) {
1012  	result := &ScanResult{}
1013  
1014  	// First, get all trusted and blacklisted pubkeys to skip
1015  	trusted, err := c.ListTrustedPubkeys()
1016  	if err != nil {
1017  		return nil, err
1018  	}
1019  	blacklisted, err := c.ListBlacklistedPubkeys()
1020  	if err != nil {
1021  		return nil, err
1022  	}
1023  
1024  	excludeSet := make(map[string]struct{})
1025  	for _, t := range trusted {
1026  		excludeSet[t.Pubkey] = struct{}{}
1027  	}
1028  	for _, b := range blacklisted {
1029  		excludeSet[b.Pubkey] = struct{}{}
1030  	}
1031  
1032  	// Scan the SerialPubkey index to get all pubkeys
1033  	pubkeys := make(map[string]struct{})
1034  
1035  	err = c.View(func(txn *badger.Txn) error {
1036  		// SerialPubkey prefix is "spk"
1037  		prefix := []byte("spk")
1038  		it := txn.NewIterator(badger.IteratorOptions{Prefix: prefix})
1039  		defer it.Close()
1040  
1041  		for it.Rewind(); it.Valid(); it.Next() {
1042  			item := it.Item()
1043  			// The value contains the 32-byte pubkey
1044  			val, err := item.ValueCopy(nil)
1045  			if err != nil {
1046  				continue
1047  			}
1048  			if len(val) == 32 {
1049  				// Convert to hex
1050  				pubkeyHex := fmt.Sprintf("%x", val)
1051  				pubkeys[pubkeyHex] = struct{}{}
1052  			}
1053  		}
1054  		return nil
1055  	})
1056  	if err != nil {
1057  		return nil, err
1058  	}
1059  
1060  	result.TotalPubkeys = len(pubkeys)
1061  
1062  	// For each pubkey, count events and store the count
1063  	today := time.Now().Format("2006-01-02")
1064  
1065  	for pubkeyHex := range pubkeys {
1066  		// Skip if trusted or blacklisted
1067  		if _, excluded := excludeSet[pubkeyHex]; excluded {
1068  			result.Skipped++
1069  			continue
1070  		}
1071  
1072  		// Count events for this pubkey using the Pubkey index
1073  		count, err := c.countEventsForPubkey(pubkeyHex)
1074  		if err != nil {
1075  			continue
1076  		}
1077  
1078  		if count > 0 {
1079  			result.TotalEvents += count
1080  
1081  			// Store the event count
1082  			ec := PubkeyEventCount{
1083  				Pubkey:    pubkeyHex,
1084  				Date:      today,
1085  				Count:     count,
1086  				LastEvent: time.Now(),
1087  			}
1088  
1089  			err = c.Update(func(txn *badger.Txn) error {
1090  				key := c.getEventCountKey(pubkeyHex, today)
1091  				data, err := json.Marshal(ec)
1092  				if err != nil {
1093  					return err
1094  				}
1095  				return txn.Set(key, data)
1096  			})
1097  			if err != nil {
1098  				continue
1099  			}
1100  		}
1101  	}
1102  
1103  	return result, nil
1104  }
1105  
1106  // EventSummary represents a simplified event for display in the UI
1107  type EventSummary struct {
1108  	ID        string `json:"id"`
1109  	Kind      int    `json:"kind"`
1110  	Content   string `json:"content"`
1111  	CreatedAt int64  `json:"created_at"`
1112  }
1113  
1114  // GetEventsForPubkey fetches events for a pubkey, returning simplified event data
1115  // limit specifies max events to return, offset is for pagination
1116  func (c *CuratingACL) GetEventsForPubkey(pubkeyHex string, limit, offset int) ([]EventSummary, int, error) {
1117  	var events []EventSummary
1118  
1119  	// First, count total events for this pubkey
1120  	totalCount, err := c.countEventsForPubkey(pubkeyHex)
1121  	if err != nil {
1122  		return nil, 0, err
1123  	}
1124  
1125  	// Decode the pubkey hex to bytes
1126  	pubkeyBytes, err := hex.DecAppend(nil, []byte(pubkeyHex))
1127  	if err != nil {
1128  		return nil, 0, fmt.Errorf("invalid pubkey hex: %w", err)
1129  	}
1130  
1131  	// Create a filter to query events by author
1132  	// Use a larger limit to account for offset, then slice
1133  	queryLimit := uint(limit + offset)
1134  	f := &filter.F{
1135  		Authors: tag.NewFromBytesSlice(pubkeyBytes),
1136  		Limit:   &queryLimit,
1137  	}
1138  
1139  	// Query events using the database's QueryEvents method
1140  	ctx := context.Background()
1141  	evs, err := c.D.QueryEvents(ctx, f)
1142  	if err != nil {
1143  		return nil, 0, err
1144  	}
1145  
1146  	// Apply offset and convert to EventSummary
1147  	for i, ev := range evs {
1148  		if i < offset {
1149  			continue
1150  		}
1151  		if len(events) >= limit {
1152  			break
1153  		}
1154  		events = append(events, EventSummary{
1155  			ID:        hex.Enc(ev.ID),
1156  			Kind:      int(ev.Kind),
1157  			Content:   string(ev.Content),
1158  			CreatedAt: ev.CreatedAt,
1159  		})
1160  	}
1161  
1162  	return events, totalCount, nil
1163  }
1164  
1165  // DeleteEventsForPubkey deletes all events for a given pubkey
1166  // Returns the number of events deleted
1167  func (c *CuratingACL) DeleteEventsForPubkey(pubkeyHex string) (int, error) {
1168  	// Decode the pubkey hex to bytes
1169  	pubkeyBytes, err := hex.DecAppend(nil, []byte(pubkeyHex))
1170  	if err != nil {
1171  		return 0, fmt.Errorf("invalid pubkey hex: %w", err)
1172  	}
1173  
1174  	// Create a filter to find all events by this author
1175  	f := &filter.F{
1176  		Authors: tag.NewFromBytesSlice(pubkeyBytes),
1177  	}
1178  
1179  	// Query all events for this pubkey
1180  	ctx := context.Background()
1181  	evs, err := c.D.QueryEvents(ctx, f)
1182  	if err != nil {
1183  		return 0, err
1184  	}
1185  
1186  	// Delete each event
1187  	deleted := 0
1188  	for _, ev := range evs {
1189  		if err := c.D.DeleteEvent(ctx, ev.ID); err != nil {
1190  			// Log error but continue deleting
1191  			continue
1192  		}
1193  		deleted++
1194  	}
1195  
1196  	return deleted, nil
1197  }
1198  
1199  // countEventsForPubkey counts events in the database for a given pubkey hex string
1200  func (c *CuratingACL) countEventsForPubkey(pubkeyHex string) (int, error) {
1201  	count := 0
1202  
1203  	// Decode the pubkey hex to bytes
1204  	pubkeyBytes := make([]byte, 32)
1205  	for i := 0; i < 32 && i*2+1 < len(pubkeyHex); i++ {
1206  		fmt.Sscanf(pubkeyHex[i*2:i*2+2], "%02x", &pubkeyBytes[i])
1207  	}
1208  
1209  	// Compute the pubkey hash (SHA256 of pubkey, first 8 bytes)
1210  	// This matches the PubHash type in indexes/types/pubhash.go
1211  	pkh := sha256.Sum256(pubkeyBytes)
1212  
1213  	// Scan the Pubkey index (prefix "pc-") for this pubkey
1214  	err := c.View(func(txn *badger.Txn) error {
1215  		// Build prefix: "pc-" + 8-byte SHA256 hash of pubkey
1216  		prefix := make([]byte, 3+8)
1217  		copy(prefix[:3], []byte("pc-"))
1218  		copy(prefix[3:], pkh[:8])
1219  
1220  		it := txn.NewIterator(badger.IteratorOptions{Prefix: prefix})
1221  		defer it.Close()
1222  
1223  		for it.Rewind(); it.Valid(); it.Next() {
1224  			count++
1225  		}
1226  		return nil
1227  	})
1228  
1229  	return count, err
1230  }
1231