package forum import ( "fmt" "strings" "time" "dk/internal/database" "dk/internal/helpers/scanner" "zombiezen.com/go/sqlite" ) // Forum represents a forum post or thread in the database type Forum struct { database.BaseModel ID int `db:"id" json:"id"` Posted int64 `db:"posted" json:"posted"` LastPost int64 `db:"last_post" json:"last_post"` Author int `db:"author" json:"author"` Parent int `db:"parent" json:"parent"` Replies int `db:"replies" json:"replies"` Title string `db:"title" json:"title"` Content string `db:"content" json:"content"` } func (f *Forum) GetTableName() string { return "forum" } func (f *Forum) GetID() int { return f.ID } func (f *Forum) SetID(id int) { f.ID = id } func (f *Forum) Set(field string, value any) error { return database.Set(f, field, value) } func (f *Forum) Save() error { return database.Save(f) } func (f *Forum) Delete() error { return database.Delete(f) } // Creates a new Forum with sensible defaults func New() *Forum { now := time.Now().Unix() return &Forum{ Posted: now, LastPost: now, Author: 0, Parent: 0, // Default to thread (not reply) Replies: 0, Title: "", Content: "", } } var forumScanner = scanner.New[Forum]() // Returns the column list for forum queries func forumColumns() string { return forumScanner.Columns() } // Populates a Forum struct using the fast scanner func scanForum(stmt *sqlite.Stmt) *Forum { forum := &Forum{} forumScanner.Scan(stmt, forum) return forum } // Retrieves a forum post by ID func Find(id int) (*Forum, error) { var forum *Forum query := `SELECT ` + forumColumns() + ` FROM forum WHERE id = ?` err := database.Query(query, func(stmt *sqlite.Stmt) error { forum = scanForum(stmt) return nil }, id) if err != nil { return nil, fmt.Errorf("failed to find forum post: %w", err) } if forum == nil { return nil, fmt.Errorf("forum post with ID %d not found", id) } return forum, nil } // Retrieves all forum posts ordered by last post time (most recent first) func All() ([]*Forum, error) { var forums []*Forum query := `SELECT ` + forumColumns() + ` FROM forum ORDER BY last_post DESC, id DESC` err := database.Query(query, func(stmt *sqlite.Stmt) error { forum := scanForum(stmt) forums = append(forums, forum) return nil }) if err != nil { return nil, fmt.Errorf("failed to retrieve all forum posts: %w", err) } return forums, nil } // Retrieves all top-level forum threads (parent = 0) func Threads() ([]*Forum, error) { var forums []*Forum query := `SELECT ` + forumColumns() + ` FROM forum WHERE parent = 0 ORDER BY last_post DESC, id DESC` err := database.Query(query, func(stmt *sqlite.Stmt) error { forum := scanForum(stmt) forums = append(forums, forum) return nil }) if err != nil { return nil, fmt.Errorf("failed to retrieve forum threads: %w", err) } return forums, nil } // Retrieves all replies to a specific thread/post func ByParent(parentID int) ([]*Forum, error) { var forums []*Forum query := `SELECT ` + forumColumns() + ` FROM forum WHERE parent = ? ORDER BY posted ASC, id ASC` err := database.Query(query, func(stmt *sqlite.Stmt) error { forum := scanForum(stmt) forums = append(forums, forum) return nil }, parentID) if err != nil { return nil, fmt.Errorf("failed to retrieve forum replies: %w", err) } return forums, nil } // Retrieves forum posts by a specific author func ByAuthor(authorID int) ([]*Forum, error) { var forums []*Forum query := `SELECT ` + forumColumns() + ` FROM forum WHERE author = ? ORDER BY posted DESC, id DESC` err := database.Query(query, func(stmt *sqlite.Stmt) error { forum := scanForum(stmt) 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 } // Retrieves the most recent forum activity (limited by count) func Recent(limit int) ([]*Forum, error) { var forums []*Forum query := `SELECT ` + forumColumns() + ` FROM forum ORDER BY last_post DESC, id DESC LIMIT ?` err := database.Query(query, func(stmt *sqlite.Stmt) error { forum := scanForum(stmt) 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 } // Retrieves forum posts containing the search term in title or content func Search(term string) ([]*Forum, error) { var forums []*Forum query := `SELECT ` + forumColumns() + ` FROM forum WHERE LOWER(title) LIKE LOWER(?) OR LOWER(content) LIKE LOWER(?) ORDER BY last_post DESC, id DESC` searchTerm := "%" + term + "%" err := database.Query(query, func(stmt *sqlite.Stmt) error { forum := scanForum(stmt) 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 } // Retrieves forum posts with activity since a specific timestamp func Since(since int64) ([]*Forum, error) { var forums []*Forum query := `SELECT ` + forumColumns() + ` FROM forum WHERE last_post >= ? ORDER BY last_post DESC, id DESC` err := database.Query(query, func(stmt *sqlite.Stmt) error { forum := scanForum(stmt) 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 } // Saves a new forum post to the database and sets the ID func (f *Forum) Insert() error { columns := `posted, last_post, author, parent, replies, title, content` values := []any{f.Posted, f.LastPost, f.Author, f.Parent, f.Replies, f.Title, f.Content} return database.Insert(f, columns, values...) } // Returns the posted timestamp as a time.Time func (f *Forum) PostedTime() time.Time { return time.Unix(f.Posted, 0) } // Returns the last post timestamp as a time.Time func (f *Forum) LastPostTime() time.Time { return time.Unix(f.LastPost, 0) } // Sets the posted timestamp from a time.Time func (f *Forum) SetPostedTime(t time.Time) { f.Set("Posted", t.Unix()) } // Sets the last post timestamp from a time.Time func (f *Forum) SetLastPostTime(t time.Time) { f.Set("LastPost", t.Unix()) } // Returns true if this is a top-level thread (parent = 0) func (f *Forum) IsThread() bool { return f.Parent == 0 } // Returns true if this is a reply to another post (parent > 0) func (f *Forum) IsReply() bool { return f.Parent > 0 } // Returns true if this post has replies func (f *Forum) HasReplies() bool { return f.Replies > 0 } // 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 } // Returns how long ago the last activity occurred func (f *Forum) ActivityAge() time.Duration { return time.Since(f.LastPostTime()) } // Returns how long ago the post was originally made func (f *Forum) PostAge() time.Duration { return time.Since(f.PostedTime()) } // Returns true if the given user ID is the author of this post func (f *Forum) IsAuthor(userID int) bool { return f.Author == userID } // 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] + "..." } // 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 } // Returns the character length of the content func (f *Forum) Length() int { return len(f.Content) } // 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) } // Updates the last_post timestamp to current time func (f *Forum) UpdateLastPost() { f.Set("LastPost", time.Now().Unix()) } // Increments the reply count func (f *Forum) IncrementReplies() { f.Set("Replies", f.Replies+1) } // Decrements the reply count (minimum 0) func (f *Forum) DecrementReplies() { if f.Replies > 0 { f.Set("Replies", f.Replies-1) } } // Retrieves all direct replies to this post func (f *Forum) GetReplies() ([]*Forum, error) { return ByParent(f.ID) } // 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.Parent) }