package config import ( "encoding/json" "fmt" "os" "sync" ) // BlocklistMode is either "blacklist" or "whitelist". type BlocklistSettings struct { Mode string `json:"mode"` // "blacklist" (default) or "whitelist" GroupIDs []string `json:"group_ids"` // group IDs to block/allow UserIDs []string `json:"user_ids"` // private chat user IDs to block/allow } // BlocklistStore manages persistence of blocklist settings. type BlocklistStore struct { mu sync.RWMutex path string settings BlocklistSettings } // NewBlocklistStore loads or creates blocklist settings file. func NewBlocklistStore(path string) (*BlocklistStore, error) { s := &BlocklistStore{ path: path, settings: BlocklistSettings{ Mode: "blacklist", GroupIDs: []string{}, UserIDs: []string{}, }, } if err := s.load(); err != nil { return nil, err } return s, nil } func (s *BlocklistStore) load() error { data, err := os.ReadFile(s.path) if err != nil { if os.IsNotExist(err) { return s.save() // write defaults } return fmt.Errorf("read blocklist file: %w", err) } if len(data) == 0 { return nil } s.mu.Lock() defer s.mu.Unlock() if err := json.Unmarshal(data, &s.settings); err != nil { return fmt.Errorf("parse blocklist file: %w", err) } if s.settings.Mode == "" { s.settings.Mode = "blacklist" } if s.settings.GroupIDs == nil { s.settings.GroupIDs = []string{} } if s.settings.UserIDs == nil { s.settings.UserIDs = []string{} } return nil } func (s *BlocklistStore) save() error { s.mu.RLock() data, err := json.MarshalIndent(s.settings, "", " ") s.mu.RUnlock() if err != nil { return fmt.Errorf("marshal blocklist: %w", err) } tmpPath := s.path + ".tmp" if err := os.WriteFile(tmpPath, data, 0640); err != nil { return fmt.Errorf("write blocklist: %w", err) } return os.Rename(tmpPath, s.path) } // Get returns current blocklist settings. func (s *BlocklistStore) Get() BlocklistSettings { s.mu.RLock() defer s.mu.RUnlock() cp := BlocklistSettings{ Mode: s.settings.Mode, GroupIDs: make([]string, len(s.settings.GroupIDs)), UserIDs: make([]string, len(s.settings.UserIDs)), } copy(cp.GroupIDs, s.settings.GroupIDs) copy(cp.UserIDs, s.settings.UserIDs) return cp } // Set updates and persists blocklist settings. func (s *BlocklistStore) Set(bs BlocklistSettings) error { if bs.Mode != "blacklist" && bs.Mode != "whitelist" { return fmt.Errorf("invalid mode: %s (must be blacklist or whitelist)", bs.Mode) } if bs.GroupIDs == nil { bs.GroupIDs = []string{} } if bs.UserIDs == nil { bs.UserIDs = []string{} } s.mu.Lock() s.settings = bs s.mu.Unlock() return s.save() } // IsBlocked checks whether a message should be blocked based on channel type and ID. // In blacklist mode: returns true if the id is IN the list. // In whitelist mode: returns true if the id is NOT in the list. // Admin users should call this with isAdmin=true to always bypass. func (s *BlocklistStore) IsBlocked(channelType, channelID, senderID string, isAdmin bool) bool { if isAdmin { return false } s.mu.RLock() defer s.mu.RUnlock() switch s.settings.Mode { case "whitelist": // Block if NOT in the whitelist. if channelType == "group" { return !contains(s.settings.GroupIDs, channelID) } return !contains(s.settings.UserIDs, senderID) case "blacklist": fallthrough default: // Block if IN the blacklist. if channelType == "group" { return contains(s.settings.GroupIDs, channelID) } return contains(s.settings.UserIDs, senderID) } } func contains(list []string, val string) bool { for _, v := range list { if v == val { return true } } return false }