You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
989 lines
24 KiB
989 lines
24 KiB
//go:build !(js && wasm) |
|
|
|
package database |
|
|
|
import ( |
|
"bytes" |
|
"encoding/json" |
|
"fmt" |
|
"sort" |
|
"time" |
|
|
|
"github.com/dgraph-io/badger/v4" |
|
) |
|
|
|
// CuratingConfig represents the configuration for curating ACL mode |
|
// This is parsed from a kind 30078 event with d-tag "curating-config" |
|
type CuratingConfig struct { |
|
DailyLimit int `json:"daily_limit"` // Max events per day for unclassified users |
|
IPDailyLimit int `json:"ip_daily_limit"` // Max events per day from a single IP (flood protection) |
|
FirstBanHours int `json:"first_ban_hours"` // IP ban duration for first offense |
|
SecondBanHours int `json:"second_ban_hours"` // IP ban duration for second+ offense |
|
AllowedKinds []int `json:"allowed_kinds"` // Explicit kind numbers |
|
AllowedRanges []string `json:"allowed_ranges"` // Kind ranges like "1000-1999" |
|
KindCategories []string `json:"kind_categories"` // Category IDs like "social", "dm" |
|
ConfigEventID string `json:"config_event_id"` // ID of the config event |
|
ConfigPubkey string `json:"config_pubkey"` // Pubkey that published config |
|
ConfiguredAt int64 `json:"configured_at"` // Timestamp of config event |
|
} |
|
|
|
// TrustedPubkey represents an explicitly trusted publisher |
|
type TrustedPubkey struct { |
|
Pubkey string `json:"pubkey"` |
|
Note string `json:"note,omitempty"` |
|
Added time.Time `json:"added"` |
|
} |
|
|
|
// BlacklistedPubkey represents a blacklisted publisher |
|
type BlacklistedPubkey struct { |
|
Pubkey string `json:"pubkey"` |
|
Reason string `json:"reason,omitempty"` |
|
Added time.Time `json:"added"` |
|
} |
|
|
|
// PubkeyEventCount tracks daily event counts for rate limiting |
|
type PubkeyEventCount struct { |
|
Pubkey string `json:"pubkey"` |
|
Date string `json:"date"` // YYYY-MM-DD format |
|
Count int `json:"count"` |
|
LastEvent time.Time `json:"last_event"` |
|
} |
|
|
|
// IPOffense tracks rate limit violations from IPs |
|
type IPOffense struct { |
|
IP string `json:"ip"` |
|
OffenseCount int `json:"offense_count"` |
|
PubkeysHit []string `json:"pubkeys_hit"` // Pubkeys that hit rate limit from this IP |
|
LastOffense time.Time `json:"last_offense"` |
|
} |
|
|
|
// CuratingBlockedIP represents a temporarily blocked IP with expiration |
|
type CuratingBlockedIP struct { |
|
IP string `json:"ip"` |
|
Reason string `json:"reason"` |
|
ExpiresAt time.Time `json:"expires_at"` |
|
Added time.Time `json:"added"` |
|
} |
|
|
|
// SpamEvent represents an event flagged as spam |
|
type SpamEvent struct { |
|
EventID string `json:"event_id"` |
|
Pubkey string `json:"pubkey"` |
|
Reason string `json:"reason,omitempty"` |
|
Added time.Time `json:"added"` |
|
} |
|
|
|
// UnclassifiedUser represents a user who hasn't been trusted or blacklisted |
|
type UnclassifiedUser struct { |
|
Pubkey string `json:"pubkey"` |
|
EventCount int `json:"event_count"` |
|
LastEvent time.Time `json:"last_event"` |
|
} |
|
|
|
// CuratingACL database operations |
|
type CuratingACL struct { |
|
*D |
|
} |
|
|
|
// NewCuratingACL creates a new CuratingACL instance |
|
func NewCuratingACL(db *D) *CuratingACL { |
|
return &CuratingACL{D: db} |
|
} |
|
|
|
// ==================== Configuration ==================== |
|
|
|
// SaveConfig saves the curating configuration |
|
func (c *CuratingACL) SaveConfig(config CuratingConfig) error { |
|
return c.Update(func(txn *badger.Txn) error { |
|
key := c.getConfigKey() |
|
data, err := json.Marshal(config) |
|
if err != nil { |
|
return err |
|
} |
|
return txn.Set(key, data) |
|
}) |
|
} |
|
|
|
// GetConfig returns the curating configuration |
|
func (c *CuratingACL) GetConfig() (CuratingConfig, error) { |
|
var config CuratingConfig |
|
err := c.View(func(txn *badger.Txn) error { |
|
key := c.getConfigKey() |
|
item, err := txn.Get(key) |
|
if err != nil { |
|
if err == badger.ErrKeyNotFound { |
|
return nil // Return empty config |
|
} |
|
return err |
|
} |
|
val, err := item.ValueCopy(nil) |
|
if err != nil { |
|
return err |
|
} |
|
return json.Unmarshal(val, &config) |
|
}) |
|
return config, err |
|
} |
|
|
|
// IsConfigured returns true if a configuration event has been set |
|
func (c *CuratingACL) IsConfigured() (bool, error) { |
|
config, err := c.GetConfig() |
|
if err != nil { |
|
return false, err |
|
} |
|
return config.ConfigEventID != "", nil |
|
} |
|
|
|
// ==================== Trusted Pubkeys ==================== |
|
|
|
// SaveTrustedPubkey saves a trusted pubkey to the database |
|
func (c *CuratingACL) SaveTrustedPubkey(pubkey string, note string) error { |
|
return c.Update(func(txn *badger.Txn) error { |
|
key := c.getTrustedPubkeyKey(pubkey) |
|
trusted := TrustedPubkey{ |
|
Pubkey: pubkey, |
|
Note: note, |
|
Added: time.Now(), |
|
} |
|
data, err := json.Marshal(trusted) |
|
if err != nil { |
|
return err |
|
} |
|
return txn.Set(key, data) |
|
}) |
|
} |
|
|
|
// RemoveTrustedPubkey removes a trusted pubkey from the database |
|
func (c *CuratingACL) RemoveTrustedPubkey(pubkey string) error { |
|
return c.Update(func(txn *badger.Txn) error { |
|
key := c.getTrustedPubkeyKey(pubkey) |
|
return txn.Delete(key) |
|
}) |
|
} |
|
|
|
// ListTrustedPubkeys returns all trusted pubkeys |
|
func (c *CuratingACL) ListTrustedPubkeys() ([]TrustedPubkey, error) { |
|
var trusted []TrustedPubkey |
|
err := c.View(func(txn *badger.Txn) error { |
|
prefix := c.getTrustedPubkeyPrefix() |
|
it := txn.NewIterator(badger.IteratorOptions{Prefix: prefix}) |
|
defer it.Close() |
|
|
|
for it.Rewind(); it.Valid(); it.Next() { |
|
item := it.Item() |
|
val, err := item.ValueCopy(nil) |
|
if err != nil { |
|
continue |
|
} |
|
var t TrustedPubkey |
|
if err := json.Unmarshal(val, &t); err != nil { |
|
continue |
|
} |
|
trusted = append(trusted, t) |
|
} |
|
return nil |
|
}) |
|
return trusted, err |
|
} |
|
|
|
// IsPubkeyTrusted checks if a pubkey is trusted |
|
func (c *CuratingACL) IsPubkeyTrusted(pubkey string) (bool, error) { |
|
var trusted bool |
|
err := c.View(func(txn *badger.Txn) error { |
|
key := c.getTrustedPubkeyKey(pubkey) |
|
_, err := txn.Get(key) |
|
if err == badger.ErrKeyNotFound { |
|
trusted = false |
|
return nil |
|
} |
|
if err != nil { |
|
return err |
|
} |
|
trusted = true |
|
return nil |
|
}) |
|
return trusted, err |
|
} |
|
|
|
// ==================== Blacklisted Pubkeys ==================== |
|
|
|
// SaveBlacklistedPubkey saves a blacklisted pubkey to the database |
|
func (c *CuratingACL) SaveBlacklistedPubkey(pubkey string, reason string) error { |
|
return c.Update(func(txn *badger.Txn) error { |
|
key := c.getBlacklistedPubkeyKey(pubkey) |
|
blacklisted := BlacklistedPubkey{ |
|
Pubkey: pubkey, |
|
Reason: reason, |
|
Added: time.Now(), |
|
} |
|
data, err := json.Marshal(blacklisted) |
|
if err != nil { |
|
return err |
|
} |
|
return txn.Set(key, data) |
|
}) |
|
} |
|
|
|
// RemoveBlacklistedPubkey removes a blacklisted pubkey from the database |
|
func (c *CuratingACL) RemoveBlacklistedPubkey(pubkey string) error { |
|
return c.Update(func(txn *badger.Txn) error { |
|
key := c.getBlacklistedPubkeyKey(pubkey) |
|
return txn.Delete(key) |
|
}) |
|
} |
|
|
|
// ListBlacklistedPubkeys returns all blacklisted pubkeys |
|
func (c *CuratingACL) ListBlacklistedPubkeys() ([]BlacklistedPubkey, error) { |
|
var blacklisted []BlacklistedPubkey |
|
err := c.View(func(txn *badger.Txn) error { |
|
prefix := c.getBlacklistedPubkeyPrefix() |
|
it := txn.NewIterator(badger.IteratorOptions{Prefix: prefix}) |
|
defer it.Close() |
|
|
|
for it.Rewind(); it.Valid(); it.Next() { |
|
item := it.Item() |
|
val, err := item.ValueCopy(nil) |
|
if err != nil { |
|
continue |
|
} |
|
var b BlacklistedPubkey |
|
if err := json.Unmarshal(val, &b); err != nil { |
|
continue |
|
} |
|
blacklisted = append(blacklisted, b) |
|
} |
|
return nil |
|
}) |
|
return blacklisted, err |
|
} |
|
|
|
// IsPubkeyBlacklisted checks if a pubkey is blacklisted |
|
func (c *CuratingACL) IsPubkeyBlacklisted(pubkey string) (bool, error) { |
|
var blacklisted bool |
|
err := c.View(func(txn *badger.Txn) error { |
|
key := c.getBlacklistedPubkeyKey(pubkey) |
|
_, err := txn.Get(key) |
|
if err == badger.ErrKeyNotFound { |
|
blacklisted = false |
|
return nil |
|
} |
|
if err != nil { |
|
return err |
|
} |
|
blacklisted = true |
|
return nil |
|
}) |
|
return blacklisted, err |
|
} |
|
|
|
// ==================== Event Counting ==================== |
|
|
|
// GetEventCount returns the event count for a pubkey on a specific date |
|
func (c *CuratingACL) GetEventCount(pubkey, date string) (int, error) { |
|
var count int |
|
err := c.View(func(txn *badger.Txn) error { |
|
key := c.getEventCountKey(pubkey, date) |
|
item, err := txn.Get(key) |
|
if err == badger.ErrKeyNotFound { |
|
count = 0 |
|
return nil |
|
} |
|
if err != nil { |
|
return err |
|
} |
|
val, err := item.ValueCopy(nil) |
|
if err != nil { |
|
return err |
|
} |
|
var ec PubkeyEventCount |
|
if err := json.Unmarshal(val, &ec); err != nil { |
|
return err |
|
} |
|
count = ec.Count |
|
return nil |
|
}) |
|
return count, err |
|
} |
|
|
|
// IncrementEventCount increments and returns the new event count for a pubkey |
|
func (c *CuratingACL) IncrementEventCount(pubkey, date string) (int, error) { |
|
var newCount int |
|
err := c.Update(func(txn *badger.Txn) error { |
|
key := c.getEventCountKey(pubkey, date) |
|
var ec PubkeyEventCount |
|
|
|
item, err := txn.Get(key) |
|
if err == badger.ErrKeyNotFound { |
|
ec = PubkeyEventCount{ |
|
Pubkey: pubkey, |
|
Date: date, |
|
Count: 0, |
|
LastEvent: time.Now(), |
|
} |
|
} else if err != nil { |
|
return err |
|
} else { |
|
val, err := item.ValueCopy(nil) |
|
if err != nil { |
|
return err |
|
} |
|
if err := json.Unmarshal(val, &ec); err != nil { |
|
return err |
|
} |
|
} |
|
|
|
ec.Count++ |
|
ec.LastEvent = time.Now() |
|
newCount = ec.Count |
|
|
|
data, err := json.Marshal(ec) |
|
if err != nil { |
|
return err |
|
} |
|
return txn.Set(key, data) |
|
}) |
|
return newCount, err |
|
} |
|
|
|
// CleanupOldEventCounts removes event counts older than the specified date |
|
func (c *CuratingACL) CleanupOldEventCounts(beforeDate string) error { |
|
return c.Update(func(txn *badger.Txn) error { |
|
prefix := c.getEventCountPrefix() |
|
it := txn.NewIterator(badger.IteratorOptions{Prefix: prefix}) |
|
defer it.Close() |
|
|
|
var keysToDelete [][]byte |
|
for it.Rewind(); it.Valid(); it.Next() { |
|
item := it.Item() |
|
val, err := item.ValueCopy(nil) |
|
if err != nil { |
|
continue |
|
} |
|
var ec PubkeyEventCount |
|
if err := json.Unmarshal(val, &ec); err != nil { |
|
continue |
|
} |
|
if ec.Date < beforeDate { |
|
keysToDelete = append(keysToDelete, item.KeyCopy(nil)) |
|
} |
|
} |
|
|
|
for _, key := range keysToDelete { |
|
if err := txn.Delete(key); err != nil { |
|
return err |
|
} |
|
} |
|
return nil |
|
}) |
|
} |
|
|
|
// ==================== IP Event Counting ==================== |
|
|
|
// IPEventCount tracks events from an IP address per day (flood protection) |
|
type IPEventCount struct { |
|
IP string `json:"ip"` |
|
Date string `json:"date"` |
|
Count int `json:"count"` |
|
LastEvent time.Time `json:"last_event"` |
|
} |
|
|
|
// GetIPEventCount returns the total event count for an IP on a specific date |
|
func (c *CuratingACL) GetIPEventCount(ip, date string) (int, error) { |
|
var count int |
|
err := c.View(func(txn *badger.Txn) error { |
|
key := c.getIPEventCountKey(ip, date) |
|
item, err := txn.Get(key) |
|
if err == badger.ErrKeyNotFound { |
|
count = 0 |
|
return nil |
|
} |
|
if err != nil { |
|
return err |
|
} |
|
val, err := item.ValueCopy(nil) |
|
if err != nil { |
|
return err |
|
} |
|
var ec IPEventCount |
|
if err := json.Unmarshal(val, &ec); err != nil { |
|
return err |
|
} |
|
count = ec.Count |
|
return nil |
|
}) |
|
return count, err |
|
} |
|
|
|
// IncrementIPEventCount increments and returns the new event count for an IP |
|
func (c *CuratingACL) IncrementIPEventCount(ip, date string) (int, error) { |
|
var newCount int |
|
err := c.Update(func(txn *badger.Txn) error { |
|
key := c.getIPEventCountKey(ip, date) |
|
var ec IPEventCount |
|
|
|
item, err := txn.Get(key) |
|
if err == badger.ErrKeyNotFound { |
|
ec = IPEventCount{ |
|
IP: ip, |
|
Date: date, |
|
Count: 0, |
|
LastEvent: time.Now(), |
|
} |
|
} else if err != nil { |
|
return err |
|
} else { |
|
val, err := item.ValueCopy(nil) |
|
if err != nil { |
|
return err |
|
} |
|
if err := json.Unmarshal(val, &ec); err != nil { |
|
return err |
|
} |
|
} |
|
|
|
ec.Count++ |
|
ec.LastEvent = time.Now() |
|
newCount = ec.Count |
|
|
|
data, err := json.Marshal(ec) |
|
if err != nil { |
|
return err |
|
} |
|
return txn.Set(key, data) |
|
}) |
|
return newCount, err |
|
} |
|
|
|
// CleanupOldIPEventCounts removes IP event counts older than the specified date |
|
func (c *CuratingACL) CleanupOldIPEventCounts(beforeDate string) error { |
|
return c.Update(func(txn *badger.Txn) error { |
|
prefix := c.getIPEventCountPrefix() |
|
it := txn.NewIterator(badger.IteratorOptions{Prefix: prefix}) |
|
defer it.Close() |
|
|
|
var keysToDelete [][]byte |
|
for it.Rewind(); it.Valid(); it.Next() { |
|
item := it.Item() |
|
val, err := item.ValueCopy(nil) |
|
if err != nil { |
|
continue |
|
} |
|
var ec IPEventCount |
|
if err := json.Unmarshal(val, &ec); err != nil { |
|
continue |
|
} |
|
if ec.Date < beforeDate { |
|
keysToDelete = append(keysToDelete, item.KeyCopy(nil)) |
|
} |
|
} |
|
|
|
for _, key := range keysToDelete { |
|
if err := txn.Delete(key); err != nil { |
|
return err |
|
} |
|
} |
|
return nil |
|
}) |
|
} |
|
|
|
func (c *CuratingACL) getIPEventCountKey(ip, date string) []byte { |
|
buf := new(bytes.Buffer) |
|
buf.WriteString("CURATING_ACL_IP_EVENT_COUNT_") |
|
buf.WriteString(ip) |
|
buf.WriteString("_") |
|
buf.WriteString(date) |
|
return buf.Bytes() |
|
} |
|
|
|
func (c *CuratingACL) getIPEventCountPrefix() []byte { |
|
return []byte("CURATING_ACL_IP_EVENT_COUNT_") |
|
} |
|
|
|
// ==================== IP Offense Tracking ==================== |
|
|
|
// GetIPOffense returns the offense record for an IP |
|
func (c *CuratingACL) GetIPOffense(ip string) (*IPOffense, error) { |
|
var offense *IPOffense |
|
err := c.View(func(txn *badger.Txn) error { |
|
key := c.getIPOffenseKey(ip) |
|
item, err := txn.Get(key) |
|
if err == badger.ErrKeyNotFound { |
|
return nil |
|
} |
|
if err != nil { |
|
return err |
|
} |
|
val, err := item.ValueCopy(nil) |
|
if err != nil { |
|
return err |
|
} |
|
offense = new(IPOffense) |
|
return json.Unmarshal(val, offense) |
|
}) |
|
return offense, err |
|
} |
|
|
|
// RecordIPOffense records a rate limit violation from an IP for a pubkey |
|
// Returns the new offense count |
|
func (c *CuratingACL) RecordIPOffense(ip, pubkey string) (int, error) { |
|
var newCount int |
|
err := c.Update(func(txn *badger.Txn) error { |
|
key := c.getIPOffenseKey(ip) |
|
var offense IPOffense |
|
|
|
item, err := txn.Get(key) |
|
if err == badger.ErrKeyNotFound { |
|
offense = IPOffense{ |
|
IP: ip, |
|
OffenseCount: 0, |
|
PubkeysHit: []string{}, |
|
LastOffense: time.Now(), |
|
} |
|
} else if err != nil { |
|
return err |
|
} else { |
|
val, err := item.ValueCopy(nil) |
|
if err != nil { |
|
return err |
|
} |
|
if err := json.Unmarshal(val, &offense); err != nil { |
|
return err |
|
} |
|
} |
|
|
|
// Add pubkey if not already in list |
|
found := false |
|
for _, p := range offense.PubkeysHit { |
|
if p == pubkey { |
|
found = true |
|
break |
|
} |
|
} |
|
if !found { |
|
offense.PubkeysHit = append(offense.PubkeysHit, pubkey) |
|
offense.OffenseCount++ |
|
} |
|
offense.LastOffense = time.Now() |
|
newCount = offense.OffenseCount |
|
|
|
data, err := json.Marshal(offense) |
|
if err != nil { |
|
return err |
|
} |
|
return txn.Set(key, data) |
|
}) |
|
return newCount, err |
|
} |
|
|
|
// ==================== IP Blocking ==================== |
|
|
|
// BlockIP blocks an IP for a specified duration |
|
func (c *CuratingACL) BlockIP(ip string, duration time.Duration, reason string) error { |
|
return c.Update(func(txn *badger.Txn) error { |
|
key := c.getBlockedIPKey(ip) |
|
blocked := CuratingBlockedIP{ |
|
IP: ip, |
|
Reason: reason, |
|
ExpiresAt: time.Now().Add(duration), |
|
Added: time.Now(), |
|
} |
|
data, err := json.Marshal(blocked) |
|
if err != nil { |
|
return err |
|
} |
|
return txn.Set(key, data) |
|
}) |
|
} |
|
|
|
// UnblockIP removes an IP from the blocked list |
|
func (c *CuratingACL) UnblockIP(ip string) error { |
|
return c.Update(func(txn *badger.Txn) error { |
|
key := c.getBlockedIPKey(ip) |
|
return txn.Delete(key) |
|
}) |
|
} |
|
|
|
// IsIPBlocked checks if an IP is blocked and returns expiration time |
|
func (c *CuratingACL) IsIPBlocked(ip string) (bool, time.Time, error) { |
|
var blocked bool |
|
var expiresAt time.Time |
|
err := c.View(func(txn *badger.Txn) error { |
|
key := c.getBlockedIPKey(ip) |
|
item, err := txn.Get(key) |
|
if err == badger.ErrKeyNotFound { |
|
blocked = false |
|
return nil |
|
} |
|
if err != nil { |
|
return err |
|
} |
|
val, err := item.ValueCopy(nil) |
|
if err != nil { |
|
return err |
|
} |
|
var b CuratingBlockedIP |
|
if err := json.Unmarshal(val, &b); err != nil { |
|
return err |
|
} |
|
if time.Now().After(b.ExpiresAt) { |
|
// Block has expired |
|
blocked = false |
|
return nil |
|
} |
|
blocked = true |
|
expiresAt = b.ExpiresAt |
|
return nil |
|
}) |
|
return blocked, expiresAt, err |
|
} |
|
|
|
// ListBlockedIPs returns all blocked IPs (including expired ones) |
|
func (c *CuratingACL) ListBlockedIPs() ([]CuratingBlockedIP, error) { |
|
var blocked []CuratingBlockedIP |
|
err := c.View(func(txn *badger.Txn) error { |
|
prefix := c.getBlockedIPPrefix() |
|
it := txn.NewIterator(badger.IteratorOptions{Prefix: prefix}) |
|
defer it.Close() |
|
|
|
for it.Rewind(); it.Valid(); it.Next() { |
|
item := it.Item() |
|
val, err := item.ValueCopy(nil) |
|
if err != nil { |
|
continue |
|
} |
|
var b CuratingBlockedIP |
|
if err := json.Unmarshal(val, &b); err != nil { |
|
continue |
|
} |
|
blocked = append(blocked, b) |
|
} |
|
return nil |
|
}) |
|
return blocked, err |
|
} |
|
|
|
// CleanupExpiredIPBlocks removes expired IP blocks |
|
func (c *CuratingACL) CleanupExpiredIPBlocks() error { |
|
return c.Update(func(txn *badger.Txn) error { |
|
prefix := c.getBlockedIPPrefix() |
|
it := txn.NewIterator(badger.IteratorOptions{Prefix: prefix}) |
|
defer it.Close() |
|
|
|
now := time.Now() |
|
var keysToDelete [][]byte |
|
for it.Rewind(); it.Valid(); it.Next() { |
|
item := it.Item() |
|
val, err := item.ValueCopy(nil) |
|
if err != nil { |
|
continue |
|
} |
|
var b CuratingBlockedIP |
|
if err := json.Unmarshal(val, &b); err != nil { |
|
continue |
|
} |
|
if now.After(b.ExpiresAt) { |
|
keysToDelete = append(keysToDelete, item.KeyCopy(nil)) |
|
} |
|
} |
|
|
|
for _, key := range keysToDelete { |
|
if err := txn.Delete(key); err != nil { |
|
return err |
|
} |
|
} |
|
return nil |
|
}) |
|
} |
|
|
|
// ==================== Spam Events ==================== |
|
|
|
// MarkEventAsSpam marks an event as spam |
|
func (c *CuratingACL) MarkEventAsSpam(eventID, pubkey, reason string) error { |
|
return c.Update(func(txn *badger.Txn) error { |
|
key := c.getSpamEventKey(eventID) |
|
spam := SpamEvent{ |
|
EventID: eventID, |
|
Pubkey: pubkey, |
|
Reason: reason, |
|
Added: time.Now(), |
|
} |
|
data, err := json.Marshal(spam) |
|
if err != nil { |
|
return err |
|
} |
|
return txn.Set(key, data) |
|
}) |
|
} |
|
|
|
// UnmarkEventAsSpam removes the spam flag from an event |
|
func (c *CuratingACL) UnmarkEventAsSpam(eventID string) error { |
|
return c.Update(func(txn *badger.Txn) error { |
|
key := c.getSpamEventKey(eventID) |
|
return txn.Delete(key) |
|
}) |
|
} |
|
|
|
// IsEventSpam checks if an event is marked as spam |
|
func (c *CuratingACL) IsEventSpam(eventID string) (bool, error) { |
|
var spam bool |
|
err := c.View(func(txn *badger.Txn) error { |
|
key := c.getSpamEventKey(eventID) |
|
_, err := txn.Get(key) |
|
if err == badger.ErrKeyNotFound { |
|
spam = false |
|
return nil |
|
} |
|
if err != nil { |
|
return err |
|
} |
|
spam = true |
|
return nil |
|
}) |
|
return spam, err |
|
} |
|
|
|
// ListSpamEvents returns all spam events |
|
func (c *CuratingACL) ListSpamEvents() ([]SpamEvent, error) { |
|
var spam []SpamEvent |
|
err := c.View(func(txn *badger.Txn) error { |
|
prefix := c.getSpamEventPrefix() |
|
it := txn.NewIterator(badger.IteratorOptions{Prefix: prefix}) |
|
defer it.Close() |
|
|
|
for it.Rewind(); it.Valid(); it.Next() { |
|
item := it.Item() |
|
val, err := item.ValueCopy(nil) |
|
if err != nil { |
|
continue |
|
} |
|
var s SpamEvent |
|
if err := json.Unmarshal(val, &s); err != nil { |
|
continue |
|
} |
|
spam = append(spam, s) |
|
} |
|
return nil |
|
}) |
|
return spam, err |
|
} |
|
|
|
// ==================== Unclassified Users ==================== |
|
|
|
// ListUnclassifiedUsers returns users who are neither trusted nor blacklisted |
|
// sorted by event count descending |
|
func (c *CuratingACL) ListUnclassifiedUsers(limit int) ([]UnclassifiedUser, error) { |
|
// First, get all trusted and blacklisted pubkeys to exclude |
|
trusted, err := c.ListTrustedPubkeys() |
|
if err != nil { |
|
return nil, err |
|
} |
|
blacklisted, err := c.ListBlacklistedPubkeys() |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
excludeSet := make(map[string]struct{}) |
|
for _, t := range trusted { |
|
excludeSet[t.Pubkey] = struct{}{} |
|
} |
|
for _, b := range blacklisted { |
|
excludeSet[b.Pubkey] = struct{}{} |
|
} |
|
|
|
// Now iterate through event counts and aggregate by pubkey |
|
pubkeyCounts := make(map[string]*UnclassifiedUser) |
|
|
|
err = c.View(func(txn *badger.Txn) error { |
|
prefix := c.getEventCountPrefix() |
|
it := txn.NewIterator(badger.IteratorOptions{Prefix: prefix}) |
|
defer it.Close() |
|
|
|
for it.Rewind(); it.Valid(); it.Next() { |
|
item := it.Item() |
|
val, err := item.ValueCopy(nil) |
|
if err != nil { |
|
continue |
|
} |
|
var ec PubkeyEventCount |
|
if err := json.Unmarshal(val, &ec); err != nil { |
|
continue |
|
} |
|
|
|
// Skip if trusted or blacklisted |
|
if _, excluded := excludeSet[ec.Pubkey]; excluded { |
|
continue |
|
} |
|
|
|
if existing, ok := pubkeyCounts[ec.Pubkey]; ok { |
|
existing.EventCount += ec.Count |
|
if ec.LastEvent.After(existing.LastEvent) { |
|
existing.LastEvent = ec.LastEvent |
|
} |
|
} else { |
|
pubkeyCounts[ec.Pubkey] = &UnclassifiedUser{ |
|
Pubkey: ec.Pubkey, |
|
EventCount: ec.Count, |
|
LastEvent: ec.LastEvent, |
|
} |
|
} |
|
} |
|
return nil |
|
}) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
// Convert to slice and sort by event count descending |
|
var users []UnclassifiedUser |
|
for _, u := range pubkeyCounts { |
|
users = append(users, *u) |
|
} |
|
sort.Slice(users, func(i, j int) bool { |
|
return users[i].EventCount > users[j].EventCount |
|
}) |
|
|
|
// Apply limit |
|
if limit > 0 && len(users) > limit { |
|
users = users[:limit] |
|
} |
|
|
|
return users, nil |
|
} |
|
|
|
// ==================== Key Generation ==================== |
|
|
|
func (c *CuratingACL) getConfigKey() []byte { |
|
return []byte("CURATING_ACL_CONFIG") |
|
} |
|
|
|
func (c *CuratingACL) getTrustedPubkeyKey(pubkey string) []byte { |
|
buf := new(bytes.Buffer) |
|
buf.WriteString("CURATING_ACL_TRUSTED_PUBKEY_") |
|
buf.WriteString(pubkey) |
|
return buf.Bytes() |
|
} |
|
|
|
func (c *CuratingACL) getTrustedPubkeyPrefix() []byte { |
|
return []byte("CURATING_ACL_TRUSTED_PUBKEY_") |
|
} |
|
|
|
func (c *CuratingACL) getBlacklistedPubkeyKey(pubkey string) []byte { |
|
buf := new(bytes.Buffer) |
|
buf.WriteString("CURATING_ACL_BLACKLISTED_PUBKEY_") |
|
buf.WriteString(pubkey) |
|
return buf.Bytes() |
|
} |
|
|
|
func (c *CuratingACL) getBlacklistedPubkeyPrefix() []byte { |
|
return []byte("CURATING_ACL_BLACKLISTED_PUBKEY_") |
|
} |
|
|
|
func (c *CuratingACL) getEventCountKey(pubkey, date string) []byte { |
|
buf := new(bytes.Buffer) |
|
buf.WriteString("CURATING_ACL_EVENT_COUNT_") |
|
buf.WriteString(pubkey) |
|
buf.WriteString("_") |
|
buf.WriteString(date) |
|
return buf.Bytes() |
|
} |
|
|
|
func (c *CuratingACL) getEventCountPrefix() []byte { |
|
return []byte("CURATING_ACL_EVENT_COUNT_") |
|
} |
|
|
|
func (c *CuratingACL) getIPOffenseKey(ip string) []byte { |
|
buf := new(bytes.Buffer) |
|
buf.WriteString("CURATING_ACL_IP_OFFENSE_") |
|
buf.WriteString(ip) |
|
return buf.Bytes() |
|
} |
|
|
|
func (c *CuratingACL) getBlockedIPKey(ip string) []byte { |
|
buf := new(bytes.Buffer) |
|
buf.WriteString("CURATING_ACL_BLOCKED_IP_") |
|
buf.WriteString(ip) |
|
return buf.Bytes() |
|
} |
|
|
|
func (c *CuratingACL) getBlockedIPPrefix() []byte { |
|
return []byte("CURATING_ACL_BLOCKED_IP_") |
|
} |
|
|
|
func (c *CuratingACL) getSpamEventKey(eventID string) []byte { |
|
buf := new(bytes.Buffer) |
|
buf.WriteString("CURATING_ACL_SPAM_EVENT_") |
|
buf.WriteString(eventID) |
|
return buf.Bytes() |
|
} |
|
|
|
func (c *CuratingACL) getSpamEventPrefix() []byte { |
|
return []byte("CURATING_ACL_SPAM_EVENT_") |
|
} |
|
|
|
// ==================== Kind Checking Helpers ==================== |
|
|
|
// IsKindAllowed checks if an event kind is allowed based on config |
|
func (c *CuratingACL) IsKindAllowed(kind int, config *CuratingConfig) bool { |
|
if config == nil { |
|
return false |
|
} |
|
|
|
// Check explicit kinds |
|
for _, k := range config.AllowedKinds { |
|
if k == kind { |
|
return true |
|
} |
|
} |
|
|
|
// Check ranges |
|
for _, rangeStr := range config.AllowedRanges { |
|
if kindInRange(kind, rangeStr) { |
|
return true |
|
} |
|
} |
|
|
|
// Check categories |
|
for _, cat := range config.KindCategories { |
|
if kindInCategory(kind, cat) { |
|
return true |
|
} |
|
} |
|
|
|
return false |
|
} |
|
|
|
// kindInRange checks if a kind is within a range string like "1000-1999" |
|
func kindInRange(kind int, rangeStr string) bool { |
|
var start, end int |
|
n, err := fmt.Sscanf(rangeStr, "%d-%d", &start, &end) |
|
if err != nil || n != 2 { |
|
return false |
|
} |
|
return kind >= start && kind <= end |
|
} |
|
|
|
// kindInCategory checks if a kind belongs to a predefined category |
|
func kindInCategory(kind int, category string) bool { |
|
categories := map[string][]int{ |
|
"social": {0, 1, 3, 6, 7, 10002}, |
|
"dm": {4, 14, 1059}, |
|
"longform": {30023, 30024}, |
|
"media": {1063, 20, 21, 22}, |
|
"marketplace": {30017, 30018, 30019, 30020, 1021, 1022}, |
|
"groups_nip29": {9, 10, 11, 12, 9000, 9001, 9002, 39000, 39001, 39002}, |
|
"groups_nip72": {34550, 1111, 4550}, |
|
"lists": {10000, 10001, 10003, 30000, 30001, 30003}, |
|
} |
|
|
|
kinds, ok := categories[category] |
|
if !ok { |
|
return false |
|
} |
|
|
|
for _, k := range kinds { |
|
if k == kind { |
|
return true |
|
} |
|
} |
|
return false |
|
}
|
|
|