package forum import ( "fmt" "strings" "time" "dk/internal/database" "zombiezen.com/go/sqlite" ) // Forum represents a forum post or thread in the database type Forum struct { ID int `json:"id"` Posted int64 `json:"posted"` LastPost int64 `json:"last_post"` Author int `json:"author"` Parent int `json:"parent"` Replies int `json:"replies"` Title string `json:"title"` Content string `json:"content"` db *database.DB } // Find retrieves a forum post by ID func Find(db *database.DB, id int) (*Forum, error) { forum := &Forum{db: db} query := "SELECT id, posted, last_post, author, parent, replies, title, content FROM forum WHERE id = ?" err := db.Query(query, func(stmt *sqlite.Stmt) error { forum.ID = stmt.ColumnInt(0) forum.Posted = stmt.ColumnInt64(1) forum.LastPost = stmt.ColumnInt64(2) forum.Author = stmt.ColumnInt(3) forum.Parent = stmt.ColumnInt(4) forum.Replies = stmt.ColumnInt(5) forum.Title = stmt.ColumnText(6) forum.Content = stmt.ColumnText(7) return nil }, id) if err != nil { return nil, fmt.Errorf("failed to find forum post: %w", err) } if forum.ID == 0 { return nil, fmt.Errorf("forum post with ID %d not found", id) } return forum, nil } // All retrieves all forum posts ordered by last post time (most recent first) func All(db *database.DB) ([]*Forum, error) { var forums []*Forum query := "SELECT id, posted, last_post, author, parent, replies, title, content FROM forum ORDER BY last_post DESC, id DESC" err := db.Query(query, func(stmt *sqlite.Stmt) error { forum := &Forum{ ID: stmt.ColumnInt(0), Posted: stmt.ColumnInt64(1), LastPost: stmt.ColumnInt64(2), Author: stmt.ColumnInt(3), Parent: stmt.ColumnInt(4), Replies: stmt.ColumnInt(5), Title: stmt.ColumnText(6), Content: stmt.ColumnText(7), db: db, } forums = append(forums, forum) return nil }) if err != nil { return nil, fmt.Errorf("failed to retrieve all forum posts: %w", err) } return forums, nil } // Threads retrieves all top-level forum threads (parent = 0) func Threads(db *database.DB) ([]*Forum, error) { var forums []*Forum query := "SELECT id, posted, last_post, author, parent, replies, title, content FROM forum WHERE parent = 0 ORDER BY last_post DESC, id DESC" err := db.Query(query, func(stmt *sqlite.Stmt) error { forum := &Forum{ ID: stmt.ColumnInt(0), Posted: stmt.ColumnInt64(1), LastPost: stmt.ColumnInt64(2), Author: stmt.ColumnInt(3), Parent: stmt.ColumnInt(4), Replies: stmt.ColumnInt(5), Title: stmt.ColumnText(6), Content: stmt.ColumnText(7), db: db, } forums = append(forums, forum) return nil }) if err != nil { return nil, fmt.Errorf("failed to retrieve forum threads: %w", err) } return forums, nil } // ByParent retrieves all replies to a specific thread/post func ByParent(db *database.DB, parentID int) ([]*Forum, error) { var forums []*Forum query := "SELECT id, posted, last_post, author, parent, replies, title, content FROM forum WHERE parent = ? ORDER BY posted ASC, id ASC" err := db.Query(query, func(stmt *sqlite.Stmt) error { forum := &Forum{ ID: stmt.ColumnInt(0), Posted: stmt.ColumnInt64(1), LastPost: stmt.ColumnInt64(2), Author: stmt.ColumnInt(3), Parent: stmt.ColumnInt(4), Replies: stmt.ColumnInt(5), Title: stmt.ColumnText(6), Content: stmt.ColumnText(7), db: db, } forums = append(forums, forum) return nil }, parentID) if err != nil { return nil, fmt.Errorf("failed to retrieve forum replies: %w", err) } return forums, nil } // ByAuthor retrieves forum posts by a specific author func ByAuthor(db *database.DB, authorID int) ([]*Forum, error) { var forums []*Forum query := "SELECT id, posted, last_post, author, parent, replies, title, content FROM forum WHERE author = ? ORDER BY posted DESC, id DESC" err := db.Query(query, func(stmt *sqlite.Stmt) error { forum := &Forum{ ID: stmt.ColumnInt(0), Posted: stmt.ColumnInt64(1), LastPost: stmt.ColumnInt64(2), Author: stmt.ColumnInt(3), Parent: stmt.ColumnInt(4), Replies: stmt.ColumnInt(5), Title: stmt.ColumnText(6), Content: stmt.ColumnText(7), db: db, } forums = append(forums, forum) return nil }, authorID) if err != nil { return nil, fmt.Errorf("failed to retrieve forum posts by author: %w", err) } return forums, nil } // Recent retrieves the most recent forum activity (limited by count) func Recent(db *database.DB, limit int) ([]*Forum, error) { var forums []*Forum query := "SELECT id, posted, last_post, author, parent, replies, title, content FROM forum ORDER BY last_post DESC, id DESC LIMIT ?" err := db.Query(query, func(stmt *sqlite.Stmt) error { forum := &Forum{ ID: stmt.ColumnInt(0), Posted: stmt.ColumnInt64(1), LastPost: stmt.ColumnInt64(2), Author: stmt.ColumnInt(3), Parent: stmt.ColumnInt(4), Replies: stmt.ColumnInt(5), Title: stmt.ColumnText(6), Content: stmt.ColumnText(7), db: db, } forums = append(forums, forum) return nil }, limit) if err != nil { return nil, fmt.Errorf("failed to retrieve recent forum posts: %w", err) } return forums, nil } // Search retrieves forum posts containing the search term in title or content func Search(db *database.DB, term string) ([]*Forum, error) { var forums []*Forum query := "SELECT id, posted, last_post, author, parent, replies, title, content FROM forum WHERE LOWER(title) LIKE LOWER(?) OR LOWER(content) LIKE LOWER(?) ORDER BY last_post DESC, id DESC" searchTerm := "%" + term + "%" err := db.Query(query, func(stmt *sqlite.Stmt) error { forum := &Forum{ ID: stmt.ColumnInt(0), Posted: stmt.ColumnInt64(1), LastPost: stmt.ColumnInt64(2), Author: stmt.ColumnInt(3), Parent: stmt.ColumnInt(4), Replies: stmt.ColumnInt(5), Title: stmt.ColumnText(6), Content: stmt.ColumnText(7), db: db, } forums = append(forums, forum) return nil }, searchTerm, searchTerm) if err != nil { return nil, fmt.Errorf("failed to search forum posts: %w", err) } return forums, nil } // Since retrieves forum posts with activity since a specific timestamp func Since(db *database.DB, since int64) ([]*Forum, error) { var forums []*Forum query := "SELECT id, posted, last_post, author, parent, replies, title, content FROM forum WHERE last_post >= ? ORDER BY last_post DESC, id DESC" err := db.Query(query, func(stmt *sqlite.Stmt) error { forum := &Forum{ ID: stmt.ColumnInt(0), Posted: stmt.ColumnInt64(1), LastPost: stmt.ColumnInt64(2), Author: stmt.ColumnInt(3), Parent: stmt.ColumnInt(4), Replies: stmt.ColumnInt(5), Title: stmt.ColumnText(6), Content: stmt.ColumnText(7), db: db, } forums = append(forums, forum) return nil }, since) if err != nil { return nil, fmt.Errorf("failed to retrieve forum posts since timestamp: %w", err) } return forums, nil } // Save updates an existing forum post in the database func (f *Forum) Save() error { if f.ID == 0 { return fmt.Errorf("cannot save forum post without ID") } query := `UPDATE forum SET posted = ?, last_post = ?, author = ?, parent = ?, replies = ?, title = ?, content = ? WHERE id = ?` return f.db.Exec(query, f.Posted, f.LastPost, f.Author, f.Parent, f.Replies, f.Title, f.Content, f.ID) } // Delete removes the forum post from the database func (f *Forum) Delete() error { if f.ID == 0 { return fmt.Errorf("cannot delete forum post without ID") } query := "DELETE FROM forum WHERE id = ?" return f.db.Exec(query, f.ID) } // PostedTime returns the posted timestamp as a time.Time func (f *Forum) PostedTime() time.Time { return time.Unix(f.Posted, 0) } // LastPostTime returns the last post timestamp as a time.Time func (f *Forum) LastPostTime() time.Time { return time.Unix(f.LastPost, 0) } // SetPostedTime sets the posted timestamp from a time.Time func (f *Forum) SetPostedTime(t time.Time) { f.Posted = t.Unix() } // SetLastPostTime sets the last post timestamp from a time.Time func (f *Forum) SetLastPostTime(t time.Time) { f.LastPost = t.Unix() } // IsThread returns true if this is a top-level thread (parent = 0) func (f *Forum) IsThread() bool { return f.Parent == 0 } // IsReply returns true if this is a reply to another post (parent > 0) func (f *Forum) IsReply() bool { return f.Parent > 0 } // HasReplies returns true if this post has replies func (f *Forum) HasReplies() bool { return f.Replies > 0 } // IsRecentActivity returns true if there has been activity within the last 24 hours func (f *Forum) IsRecentActivity() bool { return time.Since(f.LastPostTime()) < 24*time.Hour } // ActivityAge returns how long ago the last activity occurred func (f *Forum) ActivityAge() time.Duration { return time.Since(f.LastPostTime()) } // PostAge returns how long ago the post was originally made func (f *Forum) PostAge() time.Duration { return time.Since(f.PostedTime()) } // IsAuthor returns true if the given user ID is the author of this post func (f *Forum) IsAuthor(userID int) bool { return f.Author == userID } // Preview returns a truncated version of the content for previews func (f *Forum) Preview(maxLength int) string { if len(f.Content) <= maxLength { return f.Content } if maxLength < 3 { return f.Content[:maxLength] } return f.Content[:maxLength-3] + "..." } // WordCount returns the number of words in the content func (f *Forum) WordCount() int { if f.Content == "" { return 0 } // Simple word count by splitting on whitespace words := 0 inWord := false for _, char := range f.Content { if char == ' ' || char == '\t' || char == '\n' || char == '\r' { if inWord { words++ inWord = false } } else { inWord = true } } if inWord { words++ } return words } // Length returns the character length of the content func (f *Forum) Length() int { return len(f.Content) } // Contains returns true if the title or content contains the given term (case-insensitive) func (f *Forum) Contains(term string) bool { lowerTerm := strings.ToLower(term) return strings.Contains(strings.ToLower(f.Title), lowerTerm) || strings.Contains(strings.ToLower(f.Content), lowerTerm) } // UpdateLastPost updates the last_post timestamp to current time func (f *Forum) UpdateLastPost() { f.LastPost = time.Now().Unix() } // IncrementReplies increments the reply count func (f *Forum) IncrementReplies() { f.Replies++ } // DecrementReplies decrements the reply count (minimum 0) func (f *Forum) DecrementReplies() { if f.Replies > 0 { f.Replies-- } } // GetReplies retrieves all direct replies to this post func (f *Forum) GetReplies() ([]*Forum, error) { return ByParent(f.db, f.ID) } // GetThread retrieves the parent thread (if this is a reply) or returns self (if this is a thread) func (f *Forum) GetThread() (*Forum, error) { if f.IsThread() { return f, nil } return Find(f.db, f.Parent) }