Add curation ACL mode and complete graph query implementation (v0.47.0)
Some checks failed
Go / build-and-release (push) Has been cancelled
Some checks failed
Go / build-and-release (push) Has been cancelled
Curation Mode: - Three-tier publisher classification: Trusted, Blacklisted, Unclassified - Per-pubkey rate limiting (default 50/day) for unclassified users - IP flood protection (default 500/day) with automatic banning - Event kind allow-listing via categories, ranges, and custom kinds - Query filtering hides blacklisted pubkey events (admin/owner exempt) - Web UI for managing trusted/blacklisted pubkeys and configuration - NIP-86 API endpoints for all curation management operations Graph Query Extension: - Complete reference aggregation for Badger and Neo4j backends - E-tag graph backfill migration (v8) runs automatically on startup - Configuration options: ORLY_GRAPH_QUERIES_ENABLED, MAX_DEPTH, etc. - NIP-11 advertisement of graph query capabilities Files modified: - app/handle-nip86-curating.go: NIP-86 curation API handlers (new) - app/web/src/CurationView.svelte: Curation management UI (new) - app/web/src/kindCategories.js: Kind category definitions (new) - pkg/acl/curating.go: Curating ACL implementation (new) - pkg/database/curating-acl.go: Database layer for curation (new) - pkg/neo4j/graph-refs.go: Neo4j ref collection (new) - pkg/database/migrations.go: E-tag graph backfill migration - pkg/protocol/graph/executor.go: Reference aggregation support - app/handle-event.go: Curation config event processing - app/handle-req.go: Blacklist filtering for queries - docs/GRAPH_QUERIES_REMAINING_PLAN.md: Updated completion status 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
699
pkg/acl/curating.go
Normal file
699
pkg/acl/curating.go
Normal file
@@ -0,0 +1,699 @@
|
||||
package acl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"lol.mleku.dev/chk"
|
||||
"lol.mleku.dev/errorf"
|
||||
"lol.mleku.dev/log"
|
||||
"next.orly.dev/app/config"
|
||||
"next.orly.dev/pkg/database"
|
||||
"git.mleku.dev/mleku/nostr/encoders/bech32encoding"
|
||||
"git.mleku.dev/mleku/nostr/encoders/event"
|
||||
"next.orly.dev/pkg/utils"
|
||||
)
|
||||
|
||||
// Default values for curating mode
|
||||
const (
|
||||
DefaultDailyLimit = 50
|
||||
DefaultIPDailyLimit = 500 // Max events per IP per day (flood protection)
|
||||
DefaultFirstBanHours = 1
|
||||
DefaultSecondBanHours = 168 // 1 week
|
||||
CuratingConfigKind = 30078
|
||||
CuratingConfigDTag = "curating-config"
|
||||
)
|
||||
|
||||
// Curating implements the curating ACL mode with three-tier publisher classification:
|
||||
// - Trusted: Unlimited publishing
|
||||
// - Blacklisted: Cannot publish
|
||||
// - Unclassified: Rate-limited publishing (default 50/day)
|
||||
type Curating struct {
|
||||
Ctx context.Context
|
||||
cfg *config.C
|
||||
db *database.D
|
||||
curatingACL *database.CuratingACL
|
||||
owners [][]byte
|
||||
admins [][]byte
|
||||
mx sync.RWMutex
|
||||
|
||||
// In-memory caches for performance
|
||||
trustedCache map[string]bool
|
||||
blacklistedCache map[string]bool
|
||||
kindCache map[int]bool
|
||||
configCache *database.CuratingConfig
|
||||
cacheMx sync.RWMutex
|
||||
}
|
||||
|
||||
func (c *Curating) Configure(cfg ...any) (err error) {
|
||||
log.I.F("configuring curating ACL")
|
||||
for _, ca := range cfg {
|
||||
switch cv := ca.(type) {
|
||||
case *config.C:
|
||||
c.cfg = cv
|
||||
case *database.D:
|
||||
c.db = cv
|
||||
c.curatingACL = database.NewCuratingACL(cv)
|
||||
case context.Context:
|
||||
c.Ctx = cv
|
||||
default:
|
||||
err = errorf.E("invalid type: %T", reflect.TypeOf(ca))
|
||||
}
|
||||
}
|
||||
if c.cfg == nil || c.db == nil {
|
||||
err = errorf.E("both config and database must be set")
|
||||
return
|
||||
}
|
||||
|
||||
// Initialize caches
|
||||
c.trustedCache = make(map[string]bool)
|
||||
c.blacklistedCache = make(map[string]bool)
|
||||
c.kindCache = make(map[int]bool)
|
||||
|
||||
// Load owners from config
|
||||
for _, owner := range c.cfg.Owners {
|
||||
var own []byte
|
||||
if o, e := bech32encoding.NpubOrHexToPublicKeyBinary(owner); chk.E(e) {
|
||||
continue
|
||||
} else {
|
||||
own = o
|
||||
}
|
||||
c.owners = append(c.owners, own)
|
||||
}
|
||||
|
||||
// Load admins from config
|
||||
for _, admin := range c.cfg.Admins {
|
||||
var adm []byte
|
||||
if a, e := bech32encoding.NpubOrHexToPublicKeyBinary(admin); chk.E(e) {
|
||||
continue
|
||||
} else {
|
||||
adm = a
|
||||
}
|
||||
c.admins = append(c.admins, adm)
|
||||
}
|
||||
|
||||
// Refresh caches from database
|
||||
if err = c.RefreshCaches(); err != nil {
|
||||
log.W.F("curating ACL: failed to refresh caches: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Curating) GetAccessLevel(pub []byte, address string) (level string) {
|
||||
c.mx.RLock()
|
||||
defer c.mx.RUnlock()
|
||||
|
||||
pubkeyHex := hex.EncodeToString(pub)
|
||||
|
||||
// Check owners first
|
||||
for _, v := range c.owners {
|
||||
if utils.FastEqual(v, pub) {
|
||||
return "owner"
|
||||
}
|
||||
}
|
||||
|
||||
// Check admins
|
||||
for _, v := range c.admins {
|
||||
if utils.FastEqual(v, pub) {
|
||||
return "admin"
|
||||
}
|
||||
}
|
||||
|
||||
// Check if IP is blocked
|
||||
if address != "" {
|
||||
blocked, _, err := c.curatingACL.IsIPBlocked(address)
|
||||
if err == nil && blocked {
|
||||
return "blocked"
|
||||
}
|
||||
}
|
||||
|
||||
// Check if pubkey is blacklisted (check cache first)
|
||||
c.cacheMx.RLock()
|
||||
if c.blacklistedCache[pubkeyHex] {
|
||||
c.cacheMx.RUnlock()
|
||||
return "banned"
|
||||
}
|
||||
c.cacheMx.RUnlock()
|
||||
|
||||
// Double-check database for blacklisted
|
||||
blacklisted, _ := c.curatingACL.IsPubkeyBlacklisted(pubkeyHex)
|
||||
if blacklisted {
|
||||
// Update cache
|
||||
c.cacheMx.Lock()
|
||||
c.blacklistedCache[pubkeyHex] = true
|
||||
c.cacheMx.Unlock()
|
||||
return "banned"
|
||||
}
|
||||
|
||||
// All other users get write access (rate limiting handled in CheckPolicy)
|
||||
return "write"
|
||||
}
|
||||
|
||||
// CheckPolicy implements the PolicyChecker interface for event-level filtering
|
||||
func (c *Curating) CheckPolicy(ev *event.E) (allowed bool, err error) {
|
||||
pubkeyHex := hex.EncodeToString(ev.Pubkey)
|
||||
|
||||
// Check if configured
|
||||
config, err := c.GetConfig()
|
||||
if err != nil {
|
||||
return false, errorf.E("failed to get config: %v", err)
|
||||
}
|
||||
if config.ConfigEventID == "" {
|
||||
return false, errorf.E("curating mode not configured: please publish a configuration event")
|
||||
}
|
||||
|
||||
// Check if event is spam-flagged
|
||||
isSpam, _ := c.curatingACL.IsEventSpam(hex.EncodeToString(ev.ID[:]))
|
||||
if isSpam {
|
||||
return false, errorf.E("blocked: event is flagged as spam")
|
||||
}
|
||||
|
||||
// Check if event kind is allowed
|
||||
if !c.curatingACL.IsKindAllowed(int(ev.Kind), &config) {
|
||||
return false, errorf.E("blocked: event kind %d is not in the allow list", ev.Kind)
|
||||
}
|
||||
|
||||
// Check if pubkey is blacklisted
|
||||
c.cacheMx.RLock()
|
||||
isBlacklisted := c.blacklistedCache[pubkeyHex]
|
||||
c.cacheMx.RUnlock()
|
||||
if !isBlacklisted {
|
||||
isBlacklisted, _ = c.curatingACL.IsPubkeyBlacklisted(pubkeyHex)
|
||||
}
|
||||
if isBlacklisted {
|
||||
return false, errorf.E("blocked: pubkey is blacklisted")
|
||||
}
|
||||
|
||||
// Check if pubkey is trusted (bypass rate limiting)
|
||||
c.cacheMx.RLock()
|
||||
isTrusted := c.trustedCache[pubkeyHex]
|
||||
c.cacheMx.RUnlock()
|
||||
if !isTrusted {
|
||||
isTrusted, _ = c.curatingACL.IsPubkeyTrusted(pubkeyHex)
|
||||
if isTrusted {
|
||||
// Update cache
|
||||
c.cacheMx.Lock()
|
||||
c.trustedCache[pubkeyHex] = true
|
||||
c.cacheMx.Unlock()
|
||||
}
|
||||
}
|
||||
if isTrusted {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Check if owner or admin (bypass rate limiting)
|
||||
for _, v := range c.owners {
|
||||
if utils.FastEqual(v, ev.Pubkey) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
for _, v := range c.admins {
|
||||
if utils.FastEqual(v, ev.Pubkey) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
// For unclassified users, check rate limit
|
||||
today := time.Now().Format("2006-01-02")
|
||||
dailyLimit := config.DailyLimit
|
||||
if dailyLimit == 0 {
|
||||
dailyLimit = DefaultDailyLimit
|
||||
}
|
||||
|
||||
count, err := c.curatingACL.GetEventCount(pubkeyHex, today)
|
||||
if err != nil {
|
||||
log.W.F("curating ACL: failed to get event count: %v", err)
|
||||
count = 0
|
||||
}
|
||||
|
||||
if count >= dailyLimit {
|
||||
return false, errorf.E("rate limit exceeded: maximum %d events per day for unclassified users", dailyLimit)
|
||||
}
|
||||
|
||||
// Increment the counter
|
||||
_, err = c.curatingACL.IncrementEventCount(pubkeyHex, today)
|
||||
if err != nil {
|
||||
log.W.F("curating ACL: failed to increment event count: %v", err)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// RateLimitCheck checks if an unclassified user can publish and handles IP tracking
|
||||
// This is called separately when we have access to the IP address
|
||||
func (c *Curating) RateLimitCheck(pubkeyHex, ip string) (allowed bool, message string, err error) {
|
||||
config, err := c.GetConfig()
|
||||
if err != nil {
|
||||
return false, "", errorf.E("failed to get config: %v", err)
|
||||
}
|
||||
|
||||
today := time.Now().Format("2006-01-02")
|
||||
|
||||
// Check IP flood limit first (applies to all non-trusted users from this IP)
|
||||
if ip != "" {
|
||||
ipDailyLimit := config.IPDailyLimit
|
||||
if ipDailyLimit == 0 {
|
||||
ipDailyLimit = DefaultIPDailyLimit
|
||||
}
|
||||
|
||||
ipCount, err := c.curatingACL.GetIPEventCount(ip, today)
|
||||
if err != nil {
|
||||
ipCount = 0
|
||||
}
|
||||
|
||||
if ipCount >= ipDailyLimit {
|
||||
// IP has exceeded flood limit - record offense and ban
|
||||
c.recordIPOffenseAndBan(ip, pubkeyHex, config, "IP flood limit exceeded")
|
||||
return false, "rate limit exceeded: too many events from this IP address", nil
|
||||
}
|
||||
}
|
||||
|
||||
// Check per-pubkey daily limit
|
||||
dailyLimit := config.DailyLimit
|
||||
if dailyLimit == 0 {
|
||||
dailyLimit = DefaultDailyLimit
|
||||
}
|
||||
|
||||
count, err := c.curatingACL.GetEventCount(pubkeyHex, today)
|
||||
if err != nil {
|
||||
count = 0
|
||||
}
|
||||
|
||||
if count >= dailyLimit {
|
||||
// Record IP offense and potentially ban
|
||||
if ip != "" {
|
||||
c.recordIPOffenseAndBan(ip, pubkeyHex, config, "pubkey rate limit exceeded")
|
||||
}
|
||||
return false, "rate limit exceeded: maximum events per day for unclassified users", nil
|
||||
}
|
||||
|
||||
// Increment IP event count for flood tracking (only for non-trusted users)
|
||||
if ip != "" {
|
||||
_, _ = c.curatingACL.IncrementIPEventCount(ip, today)
|
||||
}
|
||||
|
||||
return true, "", nil
|
||||
}
|
||||
|
||||
// recordIPOffenseAndBan records an offense for an IP and applies a ban if warranted
|
||||
func (c *Curating) recordIPOffenseAndBan(ip, pubkeyHex string, config database.CuratingConfig, reason string) {
|
||||
offenseCount, _ := c.curatingACL.RecordIPOffense(ip, pubkeyHex)
|
||||
if offenseCount > 0 {
|
||||
firstBanHours := config.FirstBanHours
|
||||
if firstBanHours == 0 {
|
||||
firstBanHours = DefaultFirstBanHours
|
||||
}
|
||||
secondBanHours := config.SecondBanHours
|
||||
if secondBanHours == 0 {
|
||||
secondBanHours = DefaultSecondBanHours
|
||||
}
|
||||
|
||||
var banDuration time.Duration
|
||||
if offenseCount >= 2 {
|
||||
banDuration = time.Duration(secondBanHours) * time.Hour
|
||||
log.W.F("curating ACL: IP %s banned for %d hours (offense #%d, reason: %s)", ip, secondBanHours, offenseCount, reason)
|
||||
} else {
|
||||
banDuration = time.Duration(firstBanHours) * time.Hour
|
||||
log.W.F("curating ACL: IP %s banned for %d hours (offense #%d, reason: %s)", ip, firstBanHours, offenseCount, reason)
|
||||
}
|
||||
c.curatingACL.BlockIP(ip, banDuration, reason)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Curating) GetACLInfo() (name, description, documentation string) {
|
||||
return "curating", "curated relay with rate-limited unclassified publishers",
|
||||
`Curating ACL mode provides three-tier publisher classification:
|
||||
|
||||
- Trusted: Unlimited publishing, explicitly marked by admin
|
||||
- Blacklisted: Cannot publish, events rejected
|
||||
- Unclassified: Default state, rate-limited (default 50 events/day)
|
||||
|
||||
Features:
|
||||
- Per-pubkey daily rate limiting for unclassified users (default 50/day)
|
||||
- Per-IP daily rate limiting for flood protection (default 500/day)
|
||||
- IP-based spam detection (tracks multiple rate-limited pubkeys)
|
||||
- Automatic IP bans (1-hour first offense, 1-week second offense)
|
||||
- Event kind allow-listing for content control
|
||||
- Spam flagging (events hidden from queries without deletion)
|
||||
|
||||
Configuration via kind 30078 event with d-tag "curating-config".
|
||||
The relay will not accept events until configured.
|
||||
|
||||
Management through NIP-86 API endpoints:
|
||||
- trustpubkey, untrustpubkey, listtrustedpubkeys
|
||||
- blacklistpubkey, unblacklistpubkey, listblacklistedpubkeys
|
||||
- listunclassifiedusers
|
||||
- markspam, unmarkspam, listspamevents
|
||||
- setallowedkindcategories, getallowedkindcategories`
|
||||
}
|
||||
|
||||
func (c *Curating) Type() string { return "curating" }
|
||||
|
||||
// IsEventVisible checks if an event should be visible to the given access level.
|
||||
// Events from blacklisted pubkeys are only visible to admin/owner.
|
||||
func (c *Curating) IsEventVisible(ev *event.E, accessLevel string) bool {
|
||||
// Admin and owner can see all events
|
||||
if accessLevel == "admin" || accessLevel == "owner" {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check if the event author is blacklisted
|
||||
pubkeyHex := hex.EncodeToString(ev.Pubkey)
|
||||
|
||||
// Check cache first
|
||||
c.cacheMx.RLock()
|
||||
isBlacklisted := c.blacklistedCache[pubkeyHex]
|
||||
c.cacheMx.RUnlock()
|
||||
|
||||
if isBlacklisted {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check database if not in cache
|
||||
if blacklisted, _ := c.curatingACL.IsPubkeyBlacklisted(pubkeyHex); blacklisted {
|
||||
c.cacheMx.Lock()
|
||||
c.blacklistedCache[pubkeyHex] = true
|
||||
c.cacheMx.Unlock()
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// FilterVisibleEvents filters a list of events, removing those from blacklisted pubkeys.
|
||||
// Returns only events visible to the given access level.
|
||||
func (c *Curating) FilterVisibleEvents(events []*event.E, accessLevel string) []*event.E {
|
||||
// Admin and owner can see all events
|
||||
if accessLevel == "admin" || accessLevel == "owner" {
|
||||
return events
|
||||
}
|
||||
|
||||
// Filter out events from blacklisted pubkeys
|
||||
visible := make([]*event.E, 0, len(events))
|
||||
for _, ev := range events {
|
||||
if c.IsEventVisible(ev, accessLevel) {
|
||||
visible = append(visible, ev)
|
||||
}
|
||||
}
|
||||
return visible
|
||||
}
|
||||
|
||||
// GetCuratingACL returns the database ACL instance for direct access
|
||||
func (c *Curating) GetCuratingACL() *database.CuratingACL {
|
||||
return c.curatingACL
|
||||
}
|
||||
|
||||
func (c *Curating) Syncer() {
|
||||
log.I.F("starting curating ACL syncer")
|
||||
|
||||
// Start background cleanup goroutine
|
||||
go c.backgroundCleanup()
|
||||
}
|
||||
|
||||
// backgroundCleanup periodically cleans up expired data
|
||||
func (c *Curating) backgroundCleanup() {
|
||||
// Run cleanup every hour
|
||||
ticker := time.NewTicker(time.Hour)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-c.Ctx.Done():
|
||||
log.D.F("curating ACL background cleanup stopped")
|
||||
return
|
||||
case <-ticker.C:
|
||||
c.runCleanup()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Curating) runCleanup() {
|
||||
log.D.F("curating ACL: running background cleanup")
|
||||
|
||||
// Clean up expired IP blocks
|
||||
if err := c.curatingACL.CleanupExpiredIPBlocks(); err != nil {
|
||||
log.W.F("curating ACL: failed to cleanup expired IP blocks: %v", err)
|
||||
}
|
||||
|
||||
// Clean up old event counts (older than 7 days)
|
||||
cutoffDate := time.Now().AddDate(0, 0, -7).Format("2006-01-02")
|
||||
if err := c.curatingACL.CleanupOldEventCounts(cutoffDate); err != nil {
|
||||
log.W.F("curating ACL: failed to cleanup old event counts: %v", err)
|
||||
}
|
||||
|
||||
// Refresh caches
|
||||
if err := c.RefreshCaches(); err != nil {
|
||||
log.W.F("curating ACL: failed to refresh caches: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// RefreshCaches refreshes all in-memory caches from the database
|
||||
func (c *Curating) RefreshCaches() error {
|
||||
c.cacheMx.Lock()
|
||||
defer c.cacheMx.Unlock()
|
||||
|
||||
// Refresh trusted pubkeys cache
|
||||
trusted, err := c.curatingACL.ListTrustedPubkeys()
|
||||
if err != nil {
|
||||
return errorf.E("failed to list trusted pubkeys: %v", err)
|
||||
}
|
||||
c.trustedCache = make(map[string]bool)
|
||||
for _, t := range trusted {
|
||||
c.trustedCache[t.Pubkey] = true
|
||||
}
|
||||
|
||||
// Refresh blacklisted pubkeys cache
|
||||
blacklisted, err := c.curatingACL.ListBlacklistedPubkeys()
|
||||
if err != nil {
|
||||
return errorf.E("failed to list blacklisted pubkeys: %v", err)
|
||||
}
|
||||
c.blacklistedCache = make(map[string]bool)
|
||||
for _, b := range blacklisted {
|
||||
c.blacklistedCache[b.Pubkey] = true
|
||||
}
|
||||
|
||||
// Refresh config cache
|
||||
config, err := c.curatingACL.GetConfig()
|
||||
if err != nil {
|
||||
return errorf.E("failed to get config: %v", err)
|
||||
}
|
||||
c.configCache = &config
|
||||
|
||||
// Refresh allowed kinds cache
|
||||
c.kindCache = make(map[int]bool)
|
||||
for _, k := range config.AllowedKinds {
|
||||
c.kindCache[k] = true
|
||||
}
|
||||
|
||||
log.D.F("curating ACL: caches refreshed - %d trusted, %d blacklisted, %d allowed kinds",
|
||||
len(c.trustedCache), len(c.blacklistedCache), len(c.kindCache))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetConfig returns the current configuration
|
||||
func (c *Curating) GetConfig() (database.CuratingConfig, error) {
|
||||
c.cacheMx.RLock()
|
||||
if c.configCache != nil {
|
||||
config := *c.configCache
|
||||
c.cacheMx.RUnlock()
|
||||
return config, nil
|
||||
}
|
||||
c.cacheMx.RUnlock()
|
||||
|
||||
return c.curatingACL.GetConfig()
|
||||
}
|
||||
|
||||
// IsConfigured returns true if the relay has been configured
|
||||
func (c *Curating) IsConfigured() (bool, error) {
|
||||
return c.curatingACL.IsConfigured()
|
||||
}
|
||||
|
||||
// ProcessConfigEvent processes a kind 30078 event to extract curating configuration
|
||||
func (c *Curating) ProcessConfigEvent(ev *event.E) error {
|
||||
if ev.Kind != CuratingConfigKind {
|
||||
return errorf.E("invalid event kind: expected %d, got %d", CuratingConfigKind, ev.Kind)
|
||||
}
|
||||
|
||||
// Check d-tag
|
||||
dTag := ev.Tags.GetFirst([]byte("d"))
|
||||
if dTag == nil || string(dTag.Value()) != CuratingConfigDTag {
|
||||
return errorf.E("invalid d-tag: expected %s", CuratingConfigDTag)
|
||||
}
|
||||
|
||||
// Check if pubkey is owner or admin
|
||||
pubkeyHex := hex.EncodeToString(ev.Pubkey)
|
||||
isOwner := false
|
||||
isAdmin := false
|
||||
for _, v := range c.owners {
|
||||
if utils.FastEqual(v, ev.Pubkey) {
|
||||
isOwner = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !isOwner {
|
||||
for _, v := range c.admins {
|
||||
if utils.FastEqual(v, ev.Pubkey) {
|
||||
isAdmin = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !isOwner && !isAdmin {
|
||||
return errorf.E("config event must be from owner or admin")
|
||||
}
|
||||
|
||||
// Parse configuration from tags
|
||||
config := database.CuratingConfig{
|
||||
ConfigEventID: hex.EncodeToString(ev.ID[:]),
|
||||
ConfigPubkey: pubkeyHex,
|
||||
ConfiguredAt: ev.CreatedAt,
|
||||
DailyLimit: DefaultDailyLimit,
|
||||
FirstBanHours: DefaultFirstBanHours,
|
||||
SecondBanHours: DefaultSecondBanHours,
|
||||
}
|
||||
|
||||
for _, tag := range *ev.Tags {
|
||||
if tag.Len() < 2 {
|
||||
continue
|
||||
}
|
||||
key := string(tag.Key())
|
||||
value := string(tag.Value())
|
||||
|
||||
switch key {
|
||||
case "daily_limit":
|
||||
if v, err := strconv.Atoi(value); err == nil && v > 0 {
|
||||
config.DailyLimit = v
|
||||
}
|
||||
case "ip_daily_limit":
|
||||
if v, err := strconv.Atoi(value); err == nil && v > 0 {
|
||||
config.IPDailyLimit = v
|
||||
}
|
||||
case "first_ban_hours":
|
||||
if v, err := strconv.Atoi(value); err == nil && v > 0 {
|
||||
config.FirstBanHours = v
|
||||
}
|
||||
case "second_ban_hours":
|
||||
if v, err := strconv.Atoi(value); err == nil && v > 0 {
|
||||
config.SecondBanHours = v
|
||||
}
|
||||
case "kind_category":
|
||||
config.KindCategories = append(config.KindCategories, value)
|
||||
case "kind_range":
|
||||
config.AllowedRanges = append(config.AllowedRanges, value)
|
||||
case "kind":
|
||||
if k, err := strconv.Atoi(value); err == nil {
|
||||
config.AllowedKinds = append(config.AllowedKinds, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save configuration
|
||||
if err := c.curatingACL.SaveConfig(config); err != nil {
|
||||
return errorf.E("failed to save config: %v", err)
|
||||
}
|
||||
|
||||
// Refresh caches
|
||||
c.cacheMx.Lock()
|
||||
c.configCache = &config
|
||||
c.cacheMx.Unlock()
|
||||
|
||||
log.I.F("curating ACL: configuration updated from event %s by %s",
|
||||
config.ConfigEventID, config.ConfigPubkey)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsTrusted checks if a pubkey is trusted
|
||||
func (c *Curating) IsTrusted(pubkeyHex string) bool {
|
||||
c.cacheMx.RLock()
|
||||
if c.trustedCache[pubkeyHex] {
|
||||
c.cacheMx.RUnlock()
|
||||
return true
|
||||
}
|
||||
c.cacheMx.RUnlock()
|
||||
|
||||
trusted, _ := c.curatingACL.IsPubkeyTrusted(pubkeyHex)
|
||||
return trusted
|
||||
}
|
||||
|
||||
// IsBlacklisted checks if a pubkey is blacklisted
|
||||
func (c *Curating) IsBlacklisted(pubkeyHex string) bool {
|
||||
c.cacheMx.RLock()
|
||||
if c.blacklistedCache[pubkeyHex] {
|
||||
c.cacheMx.RUnlock()
|
||||
return true
|
||||
}
|
||||
c.cacheMx.RUnlock()
|
||||
|
||||
blacklisted, _ := c.curatingACL.IsPubkeyBlacklisted(pubkeyHex)
|
||||
return blacklisted
|
||||
}
|
||||
|
||||
// TrustPubkey adds a pubkey to the trusted list
|
||||
func (c *Curating) TrustPubkey(pubkeyHex, note string) error {
|
||||
pubkeyHex = strings.ToLower(pubkeyHex)
|
||||
if err := c.curatingACL.SaveTrustedPubkey(pubkeyHex, note); err != nil {
|
||||
return err
|
||||
}
|
||||
// Update cache
|
||||
c.cacheMx.Lock()
|
||||
c.trustedCache[pubkeyHex] = true
|
||||
delete(c.blacklistedCache, pubkeyHex) // Remove from blacklist cache if present
|
||||
c.cacheMx.Unlock()
|
||||
// Also remove from blacklist in DB
|
||||
c.curatingACL.RemoveBlacklistedPubkey(pubkeyHex)
|
||||
return nil
|
||||
}
|
||||
|
||||
// UntrustPubkey removes a pubkey from the trusted list
|
||||
func (c *Curating) UntrustPubkey(pubkeyHex string) error {
|
||||
pubkeyHex = strings.ToLower(pubkeyHex)
|
||||
if err := c.curatingACL.RemoveTrustedPubkey(pubkeyHex); err != nil {
|
||||
return err
|
||||
}
|
||||
// Update cache
|
||||
c.cacheMx.Lock()
|
||||
delete(c.trustedCache, pubkeyHex)
|
||||
c.cacheMx.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// BlacklistPubkey adds a pubkey to the blacklist
|
||||
func (c *Curating) BlacklistPubkey(pubkeyHex, reason string) error {
|
||||
pubkeyHex = strings.ToLower(pubkeyHex)
|
||||
if err := c.curatingACL.SaveBlacklistedPubkey(pubkeyHex, reason); err != nil {
|
||||
return err
|
||||
}
|
||||
// Update cache
|
||||
c.cacheMx.Lock()
|
||||
c.blacklistedCache[pubkeyHex] = true
|
||||
delete(c.trustedCache, pubkeyHex) // Remove from trusted cache if present
|
||||
c.cacheMx.Unlock()
|
||||
// Also remove from trusted list in DB
|
||||
c.curatingACL.RemoveTrustedPubkey(pubkeyHex)
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnblacklistPubkey removes a pubkey from the blacklist
|
||||
func (c *Curating) UnblacklistPubkey(pubkeyHex string) error {
|
||||
pubkeyHex = strings.ToLower(pubkeyHex)
|
||||
if err := c.curatingACL.RemoveBlacklistedPubkey(pubkeyHex); err != nil {
|
||||
return err
|
||||
}
|
||||
// Update cache
|
||||
c.cacheMx.Lock()
|
||||
delete(c.blacklistedCache, pubkeyHex)
|
||||
c.cacheMx.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
Registry.Register(new(Curating))
|
||||
}
|
||||
Reference in New Issue
Block a user