// Package store remembers which post each output produced for a given Discord // message, so a later delete or reply on Discord can be mirrored. package store import ( "encoding/json" "fmt" "os" "sync" "tweetdistributor/output" ) // maxEntries bounds how far back a delete or reply can reach; the oldest // message is forgotten once the limit is passed. const maxEntries = 1000 // Refs maps an output name to the post that output published. type Refs map[string]output.Ref type Store struct { mu sync.Mutex path string refs map[string]Refs order []string } // New returns a store backed by the JSON file at path, loading it if it // already exists. An empty path keeps everything in memory only, in which // case the mapping is lost on restart. func New(path string) (*Store, error) { s := &Store{ path: path, refs: map[string]Refs{}, } if path == "" { return s, nil } data, err := os.ReadFile(path) if os.IsNotExist(err) { return s, nil } if err != nil { return nil, fmt.Errorf("reading %s: %w", path, err) } var saved struct { Order []string `json:"order"` Refs map[string]Refs `json:"refs"` } if err := json.Unmarshal(data, &saved); err != nil { return nil, fmt.Errorf("parsing %s: %w", path, err) } if saved.Refs != nil { s.refs = saved.Refs } s.order = saved.Order return s, nil } func (s *Store) Put(messageID string, refs Refs) error { s.mu.Lock() defer s.mu.Unlock() if _, exists := s.refs[messageID]; !exists { s.order = append(s.order, messageID) } s.refs[messageID] = refs for len(s.order) > maxEntries { delete(s.refs, s.order[0]) s.order = s.order[1:] } return s.save() } func (s *Store) Get(messageID string) (Refs, bool) { s.mu.Lock() defer s.mu.Unlock() refs, ok := s.refs[messageID] return refs, ok } func (s *Store) Delete(messageID string) error { s.mu.Lock() defer s.mu.Unlock() if _, ok := s.refs[messageID]; !ok { return nil } delete(s.refs, messageID) for i, id := range s.order { if id == messageID { s.order = append(s.order[:i], s.order[i+1:]...) break } } return s.save() } // save writes the whole store out; the caller must hold the lock. func (s *Store) save() error { if s.path == "" { return nil } data, err := json.Marshal(struct { Order []string `json:"order"` Refs map[string]Refs `json:"refs"` }{Order: s.order, Refs: s.refs}) if err != nil { return err } tmp := s.path + ".tmp" if err := os.WriteFile(tmp, data, 0o600); err != nil { return fmt.Errorf("writing %s: %w", tmp, err) } if err := os.Rename(tmp, s.path); err != nil { return fmt.Errorf("replacing %s: %w", s.path, err) } return nil }