// emoji_favorites.go handles persistence of the user's favorite emoji list, // stored as a JSON file in the cache directory. package chat import ( "encoding/json" "log/slog" "os" "path/filepath" "sync" "github.com/ayn2op/discordo/internal/consts" ) const maxFavoriteEmoji = 10 // maxFavoritesFileSize is the upper bound on the emoji favorites JSON file (4 KiB). const maxFavoritesFileSize = 4096 // maxFavoriteEmojiLen is the maximum byte length of a single emoji favorite string. const maxFavoriteEmojiLen = 64 type emojiFavorites struct { Favorites []string `json:"favorites"` mu sync.RWMutex } var emojiFavoritesPath = filepath.Join(consts.CacheDir(), "emoji_favorites.json") func loadEmojiFavorites() *emojiFavorites { ef := &emojiFavorites{} data, err := os.ReadFile(emojiFavoritesPath) if err != nil { return ef } if len(data) > maxFavoritesFileSize { slog.Warn("emoji favorites file too large, ignoring", "size", len(data)) return ef } if err := json.Unmarshal(data, ef); err != nil { slog.Warn("failed to parse emoji favorites", "err", err) return &emojiFavorites{} } // Reject any favorite strings that exceed the length limit. valid := ef.Favorites[:0] for _, e := range ef.Favorites { if len(e) <= maxFavoriteEmojiLen { valid = append(valid, e) } else { slog.Warn("emoji favorite too long, skipping", "len", len(e)) } } ef.Favorites = valid return ef } // save persists favorites to disk. The caller must hold ef.mu. func (ef *emojiFavorites) save() { atomicSaveJSON(emojiFavoritesPath, ef) } func (ef *emojiFavorites) toggle(emoji string) { ef.mu.Lock() defer ef.mu.Unlock() for i, e := range ef.Favorites { if e == emoji { ef.Favorites = append(ef.Favorites[:i], ef.Favorites[i+1:]...) ef.save() return } } if len(ef.Favorites) >= maxFavoriteEmoji { return } ef.Favorites = append(ef.Favorites, emoji) ef.save() } func (ef *emojiFavorites) list() []string { ef.mu.RLock() defer ef.mu.RUnlock() out := make([]string, len(ef.Favorites)) copy(out, ef.Favorites) return out }