package news import ( "dk/internal/store" "fmt" "sort" "strings" "sync" "time" ) // News represents a news post in the game type News struct { ID int `json:"id"` Author int `json:"author"` Posted int64 `json:"posted"` Content string `json:"content"` } func (n *News) Save() error { newsStore := GetStore() newsStore.UpdateNews(n) return nil } func (n *News) Delete() error { newsStore := GetStore() newsStore.RemoveNews(n.ID) return nil } // Creates a new News with sensible defaults func New() *News { return &News{ Author: 0, // No author by default Posted: time.Now().Unix(), // Current time Content: "", // Empty content } } // Validate checks if news has valid values func (n *News) Validate() error { if n.Posted < 0 { return fmt.Errorf("news Posted timestamp cannot be negative") } if strings.TrimSpace(n.Content) == "" { return fmt.Errorf("news Content cannot be empty") } return nil } // NewsStore provides in-memory storage with O(1) lookups and news-specific indices type NewsStore struct { *store.BaseStore[News] // Embedded generic store byAuthor map[int][]int // Author -> []ID allByPosted []int // All IDs sorted by posted DESC, id DESC mu sync.RWMutex // Protects indices } // Global in-memory store var newsStore *NewsStore var storeOnce sync.Once // Initialize the in-memory store func initStore() { newsStore = &NewsStore{ BaseStore: store.NewBaseStore[News](), byAuthor: make(map[int][]int), allByPosted: make([]int, 0), } } // GetStore returns the global news store func GetStore() *NewsStore { storeOnce.Do(initStore) return newsStore } // AddNews adds a news post to the in-memory store and updates all indices func (ns *NewsStore) AddNews(news *News) { ns.mu.Lock() defer ns.mu.Unlock() // Validate news if err := news.Validate(); err != nil { return } // Add to base store ns.Add(news.ID, news) // Rebuild indices ns.rebuildIndicesUnsafe() } // RemoveNews removes a news post from the store and updates indices func (ns *NewsStore) RemoveNews(id int) { ns.mu.Lock() defer ns.mu.Unlock() // Remove from base store ns.Remove(id) // Rebuild indices ns.rebuildIndicesUnsafe() } // UpdateNews updates a news post efficiently func (ns *NewsStore) UpdateNews(news *News) { ns.mu.Lock() defer ns.mu.Unlock() // Validate news if err := news.Validate(); err != nil { return } // Update base store ns.Add(news.ID, news) // Rebuild indices ns.rebuildIndicesUnsafe() } // LoadData loads news data from JSON file, or starts with empty store func LoadData(dataPath string) error { ns := GetStore() // Load from base store, which handles JSON loading if err := ns.BaseStore.LoadData(dataPath); err != nil { return err } // Rebuild indices from loaded data ns.rebuildIndices() return nil } // SaveData saves news data to JSON file func SaveData(dataPath string) error { ns := GetStore() return ns.BaseStore.SaveData(dataPath) } // rebuildIndicesUnsafe rebuilds all indices from base store data (caller must hold lock) func (ns *NewsStore) rebuildIndicesUnsafe() { // Clear indices ns.byAuthor = make(map[int][]int) ns.allByPosted = make([]int, 0) // Collect all news and build indices allNews := ns.GetAll() for id, news := range allNews { // Author index ns.byAuthor[news.Author] = append(ns.byAuthor[news.Author], id) // All IDs ns.allByPosted = append(ns.allByPosted, id) } // Sort allByPosted by posted DESC, then ID DESC sort.Slice(ns.allByPosted, func(i, j int) bool { newsI, _ := ns.GetByID(ns.allByPosted[i]) newsJ, _ := ns.GetByID(ns.allByPosted[j]) if newsI.Posted != newsJ.Posted { return newsI.Posted > newsJ.Posted // DESC } return ns.allByPosted[i] > ns.allByPosted[j] // DESC }) // Sort author indices by posted DESC, then ID DESC for author := range ns.byAuthor { sort.Slice(ns.byAuthor[author], func(i, j int) bool { newsI, _ := ns.GetByID(ns.byAuthor[author][i]) newsJ, _ := ns.GetByID(ns.byAuthor[author][j]) if newsI.Posted != newsJ.Posted { return newsI.Posted > newsJ.Posted // DESC } return ns.byAuthor[author][i] > ns.byAuthor[author][j] // DESC }) } } // rebuildIndices rebuilds all news-specific indices from base store data func (ns *NewsStore) rebuildIndices() { ns.mu.Lock() defer ns.mu.Unlock() ns.rebuildIndicesUnsafe() } // Retrieves a news post by ID func Find(id int) (*News, error) { ns := GetStore() news, exists := ns.GetByID(id) if !exists { return nil, fmt.Errorf("news with ID %d not found", id) } return news, nil } // Retrieves all news posts ordered by posted date (newest first) func All() ([]*News, error) { ns := GetStore() ns.mu.RLock() defer ns.mu.RUnlock() result := make([]*News, 0, len(ns.allByPosted)) for _, id := range ns.allByPosted { if news, exists := ns.GetByID(id); exists { result = append(result, news) } } return result, nil } // Retrieves news posts by a specific author func ByAuthor(authorID int) ([]*News, error) { ns := GetStore() ns.mu.RLock() defer ns.mu.RUnlock() ids, exists := ns.byAuthor[authorID] if !exists { return []*News{}, nil } result := make([]*News, 0, len(ids)) for _, id := range ids { if news, exists := ns.GetByID(id); exists { result = append(result, news) } } return result, nil } // Retrieves the most recent news posts (limited by count) func Recent(limit int) ([]*News, error) { ns := GetStore() ns.mu.RLock() defer ns.mu.RUnlock() if limit > len(ns.allByPosted) { limit = len(ns.allByPosted) } result := make([]*News, 0, limit) for i := 0; i < limit; i++ { if news, exists := ns.GetByID(ns.allByPosted[i]); exists { result = append(result, news) } } return result, nil } // Retrieves news posts since a specific timestamp func Since(since int64) ([]*News, error) { ns := GetStore() ns.mu.RLock() defer ns.mu.RUnlock() var result []*News for _, id := range ns.allByPosted { if news, exists := ns.GetByID(id); exists && news.Posted >= since { result = append(result, news) } } return result, nil } // Retrieves news posts between two timestamps (inclusive) func Between(start, end int64) ([]*News, error) { ns := GetStore() ns.mu.RLock() defer ns.mu.RUnlock() var result []*News for _, id := range ns.allByPosted { if news, exists := ns.GetByID(id); exists && news.Posted >= start && news.Posted <= end { result = append(result, news) } } return result, nil } // Retrieves news posts containing the search term in content func Search(term string) ([]*News, error) { ns := GetStore() ns.mu.RLock() defer ns.mu.RUnlock() var result []*News lowerTerm := strings.ToLower(term) for _, id := range ns.allByPosted { if news, exists := ns.GetByID(id); exists { if strings.Contains(strings.ToLower(news.Content), lowerTerm) { result = append(result, news) } } } return result, nil } // Saves a new news post to the in-memory store and sets the ID func (n *News) Insert() error { ns := GetStore() // Validate before insertion if err := n.Validate(); err != nil { return fmt.Errorf("validation failed: %w", err) } // Assign new ID if not set if n.ID == 0 { n.ID = ns.GetNextID() } // Add to store ns.AddNews(n) return nil } // Returns the posted timestamp as a time.Time func (n *News) PostedTime() time.Time { return time.Unix(n.Posted, 0) } // Sets the posted timestamp from a time.Time func (n *News) SetPostedTime(t time.Time) { n.Posted = t.Unix() } // Returns true if the news post was made within the last 24 hours func (n *News) IsRecent() bool { return time.Since(n.PostedTime()) < 24*time.Hour } // Returns how long ago the news post was made func (n *News) Age() time.Duration { return time.Since(n.PostedTime()) } // Converts a time.Time to a human-readable date string func (n *News) ReadableTime() string { return n.PostedTime().Format("Jan 2, 2006 3:04 PM") } // Returns true if the given user ID is the author of this news post func (n *News) IsAuthor(userID int) bool { return n.Author == userID } // Returns a truncated version of the content for previews func (n *News) Preview(maxLength int) string { if len(n.Content) <= maxLength { return n.Content } if maxLength < 3 { return n.Content[:maxLength] } return n.Content[:maxLength-3] + "..." } // Returns the number of words in the content func (n *News) WordCount() int { if n.Content == "" { return 0 } // Simple word count by splitting on spaces words := 0 inWord := false for _, char := range n.Content { if char == ' ' || char == '\t' || char == '\n' || char == '\r' { if inWord { words++ inWord = false } } else { inWord = true } } if inWord { words++ } return words } // Returns the character length of the content func (n *News) Length() int { return len(n.Content) } // Returns true if the content contains the given term (case-insensitive) func (n *News) Contains(term string) bool { return strings.Contains(strings.ToLower(n.Content), strings.ToLower(term)) } // Returns true if the content is empty or whitespace-only func (n *News) IsEmpty() bool { return strings.TrimSpace(n.Content) == "" }