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.
1230 lines
31 KiB
1230 lines
31 KiB
//go:build !(js && wasm) |
|
|
|
package database |
|
|
|
import ( |
|
"bytes" |
|
"context" |
|
"encoding/json" |
|
"fmt" |
|
"sort" |
|
"time" |
|
|
|
"github.com/dgraph-io/badger/v4" |
|
"github.com/minio/sha256-simd" |
|
"git.mleku.dev/mleku/nostr/encoders/filter" |
|
"git.mleku.dev/mleku/nostr/encoders/hex" |
|
"git.mleku.dev/mleku/nostr/encoders/tag" |
|
) |
|
|
|
// 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}, // Legacy alias |
|
"marketplace_nip15": {30017, 30018, 30019, 30020, 1021, 1022}, |
|
"marketplace_nip99": {30402, 30403, 30405, 30406, 31555}, // NIP-99/Gamma Markets (Plebeian Market) |
|
"order_communication": {16, 17}, // Gamma Markets order messages |
|
"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 |
|
} |
|
|
|
// ==================== Database Scanning ==================== |
|
|
|
// ScanResult contains the results of scanning all pubkeys in the database |
|
type ScanResult struct { |
|
TotalPubkeys int `json:"total_pubkeys"` |
|
TotalEvents int `json:"total_events"` |
|
Skipped int `json:"skipped"` // Trusted/blacklisted users skipped |
|
} |
|
|
|
// ScanAllPubkeys scans the database to find all unique pubkeys and count their events. |
|
// This populates the event count data needed for the unclassified users list. |
|
// It uses the SerialPubkey index to find all pubkeys, then counts events for each. |
|
func (c *CuratingACL) ScanAllPubkeys() (*ScanResult, error) { |
|
result := &ScanResult{} |
|
|
|
// First, get all trusted and blacklisted pubkeys to skip |
|
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{}{} |
|
} |
|
|
|
// Scan the SerialPubkey index to get all pubkeys |
|
pubkeys := make(map[string]struct{}) |
|
|
|
err = c.View(func(txn *badger.Txn) error { |
|
// SerialPubkey prefix is "spk" |
|
prefix := []byte("spk") |
|
it := txn.NewIterator(badger.IteratorOptions{Prefix: prefix}) |
|
defer it.Close() |
|
|
|
for it.Rewind(); it.Valid(); it.Next() { |
|
item := it.Item() |
|
// The value contains the 32-byte pubkey |
|
val, err := item.ValueCopy(nil) |
|
if err != nil { |
|
continue |
|
} |
|
if len(val) == 32 { |
|
// Convert to hex |
|
pubkeyHex := fmt.Sprintf("%x", val) |
|
pubkeys[pubkeyHex] = struct{}{} |
|
} |
|
} |
|
return nil |
|
}) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
result.TotalPubkeys = len(pubkeys) |
|
|
|
// For each pubkey, count events and store the count |
|
today := time.Now().Format("2006-01-02") |
|
|
|
for pubkeyHex := range pubkeys { |
|
// Skip if trusted or blacklisted |
|
if _, excluded := excludeSet[pubkeyHex]; excluded { |
|
result.Skipped++ |
|
continue |
|
} |
|
|
|
// Count events for this pubkey using the Pubkey index |
|
count, err := c.countEventsForPubkey(pubkeyHex) |
|
if err != nil { |
|
continue |
|
} |
|
|
|
if count > 0 { |
|
result.TotalEvents += count |
|
|
|
// Store the event count |
|
ec := PubkeyEventCount{ |
|
Pubkey: pubkeyHex, |
|
Date: today, |
|
Count: count, |
|
LastEvent: time.Now(), |
|
} |
|
|
|
err = c.Update(func(txn *badger.Txn) error { |
|
key := c.getEventCountKey(pubkeyHex, today) |
|
data, err := json.Marshal(ec) |
|
if err != nil { |
|
return err |
|
} |
|
return txn.Set(key, data) |
|
}) |
|
if err != nil { |
|
continue |
|
} |
|
} |
|
} |
|
|
|
return result, nil |
|
} |
|
|
|
// EventSummary represents a simplified event for display in the UI |
|
type EventSummary struct { |
|
ID string `json:"id"` |
|
Kind int `json:"kind"` |
|
Content string `json:"content"` |
|
CreatedAt int64 `json:"created_at"` |
|
} |
|
|
|
// GetEventsForPubkey fetches events for a pubkey, returning simplified event data |
|
// limit specifies max events to return, offset is for pagination |
|
func (c *CuratingACL) GetEventsForPubkey(pubkeyHex string, limit, offset int) ([]EventSummary, int, error) { |
|
var events []EventSummary |
|
|
|
// First, count total events for this pubkey |
|
totalCount, err := c.countEventsForPubkey(pubkeyHex) |
|
if err != nil { |
|
return nil, 0, err |
|
} |
|
|
|
// Decode the pubkey hex to bytes |
|
pubkeyBytes, err := hex.DecAppend(nil, []byte(pubkeyHex)) |
|
if err != nil { |
|
return nil, 0, fmt.Errorf("invalid pubkey hex: %w", err) |
|
} |
|
|
|
// Create a filter to query events by author |
|
// Use a larger limit to account for offset, then slice |
|
queryLimit := uint(limit + offset) |
|
f := &filter.F{ |
|
Authors: tag.NewFromBytesSlice(pubkeyBytes), |
|
Limit: &queryLimit, |
|
} |
|
|
|
// Query events using the database's QueryEvents method |
|
ctx := context.Background() |
|
evs, err := c.D.QueryEvents(ctx, f) |
|
if err != nil { |
|
return nil, 0, err |
|
} |
|
|
|
// Apply offset and convert to EventSummary |
|
for i, ev := range evs { |
|
if i < offset { |
|
continue |
|
} |
|
if len(events) >= limit { |
|
break |
|
} |
|
events = append(events, EventSummary{ |
|
ID: hex.Enc(ev.ID), |
|
Kind: int(ev.Kind), |
|
Content: string(ev.Content), |
|
CreatedAt: ev.CreatedAt, |
|
}) |
|
} |
|
|
|
return events, totalCount, nil |
|
} |
|
|
|
// DeleteEventsForPubkey deletes all events for a given pubkey |
|
// Returns the number of events deleted |
|
func (c *CuratingACL) DeleteEventsForPubkey(pubkeyHex string) (int, error) { |
|
// Decode the pubkey hex to bytes |
|
pubkeyBytes, err := hex.DecAppend(nil, []byte(pubkeyHex)) |
|
if err != nil { |
|
return 0, fmt.Errorf("invalid pubkey hex: %w", err) |
|
} |
|
|
|
// Create a filter to find all events by this author |
|
f := &filter.F{ |
|
Authors: tag.NewFromBytesSlice(pubkeyBytes), |
|
} |
|
|
|
// Query all events for this pubkey |
|
ctx := context.Background() |
|
evs, err := c.D.QueryEvents(ctx, f) |
|
if err != nil { |
|
return 0, err |
|
} |
|
|
|
// Delete each event |
|
deleted := 0 |
|
for _, ev := range evs { |
|
if err := c.D.DeleteEvent(ctx, ev.ID); err != nil { |
|
// Log error but continue deleting |
|
continue |
|
} |
|
deleted++ |
|
} |
|
|
|
return deleted, nil |
|
} |
|
|
|
// countEventsForPubkey counts events in the database for a given pubkey hex string |
|
func (c *CuratingACL) countEventsForPubkey(pubkeyHex string) (int, error) { |
|
count := 0 |
|
|
|
// Decode the pubkey hex to bytes |
|
pubkeyBytes := make([]byte, 32) |
|
for i := 0; i < 32 && i*2+1 < len(pubkeyHex); i++ { |
|
fmt.Sscanf(pubkeyHex[i*2:i*2+2], "%02x", &pubkeyBytes[i]) |
|
} |
|
|
|
// Compute the pubkey hash (SHA256 of pubkey, first 8 bytes) |
|
// This matches the PubHash type in indexes/types/pubhash.go |
|
pkh := sha256.Sum256(pubkeyBytes) |
|
|
|
// Scan the Pubkey index (prefix "pc-") for this pubkey |
|
err := c.View(func(txn *badger.Txn) error { |
|
// Build prefix: "pc-" + 8-byte SHA256 hash of pubkey |
|
prefix := make([]byte, 3+8) |
|
copy(prefix[:3], []byte("pc-")) |
|
copy(prefix[3:], pkh[:8]) |
|
|
|
it := txn.NewIterator(badger.IteratorOptions{Prefix: prefix}) |
|
defer it.Close() |
|
|
|
for it.Rewind(); it.Valid(); it.Next() { |
|
count++ |
|
} |
|
return nil |
|
}) |
|
|
|
return count, err |
|
}
|
|
|