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.
699 lines
18 KiB
699 lines
18 KiB
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)) |
|
}
|
|
|