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