2025-08-08 23:52:23 -05:00

627 lines
14 KiB
Go

/*
Package forum is the active record implementation for forum posts and threads in the game.
The forum package provides a complete forum system with thread and reply functionality, search capabilities, and comprehensive post management. It supports hierarchical discussions with parent/child relationships between posts.
# Basic Usage
To retrieve a forum post by ID:
post, err := forum.Find(db, 1)
if err != nil {
log.Fatal(err)
}
fmt.Printf("[%s] %s by user %d\n",
post.PostedTime().Format("Jan 2"), post.Title, post.Author)
To get all forum threads (top-level posts):
threads, err := forum.Threads(db)
if err != nil {
log.Fatal(err)
}
for _, thread := range threads {
fmt.Printf("Thread: %s (%d replies)\n", thread.Title, thread.Replies)
}
To get replies to a specific thread:
replies, err := forum.ByParent(db, threadID)
if err != nil {
log.Fatal(err)
}
To search forum posts:
results, err := forum.Search(db, "strategy")
if err != nil {
log.Fatal(err)
}
# Creating Posts with Builder Pattern
The package provides a fluent builder interface for creating new forum posts:
## Creating a New Thread
thread, err := forum.NewBuilder(db).
WithAuthor(userID).
WithTitle("New Strategy Discussion").
WithContent("What are your thoughts on the best character builds?").
AsThread().
Create()
if err != nil {
log.Fatal(err)
}
fmt.Printf("Created thread with ID: %d\n", thread.ID)
## Creating a Reply
reply, err := forum.NewBuilder(db).
WithAuthor(userID).
WithTitle("Re: Strategy Discussion").
WithContent("I think mage builds are overpowered right now.").
AsReply(parentThreadID).
Create()
if err != nil {
log.Fatal(err)
}
The builder automatically sets timestamps to the current time if not specified.
# Updating Posts
Forum posts can be modified and saved back to the database:
post, _ := forum.Find(db, 1)
post.Title = "[UPDATED] " + post.Title
post.Content = post.Content + "\n\nEDIT: Added clarification."
post.UpdateLastPost() // Update last activity timestamp
err := post.Save()
if err != nil {
log.Fatal(err)
}
# Deleting Posts
Posts can be removed from the database:
post, _ := forum.Find(db, 1)
err := post.Delete()
if err != nil {
log.Fatal(err)
}
# Database Schema
The forum table has the following structure:
CREATE TABLE forum (
id INTEGER PRIMARY KEY AUTOINCREMENT,
posted INTEGER NOT NULL DEFAULT (unixepoch()),
last_post INTEGER NOT NULL DEFAULT (unixepoch()),
author INTEGER NOT NULL,
parent INTEGER NOT NULL DEFAULT 0,
replies INTEGER NOT NULL DEFAULT 0,
title TEXT NOT NULL,
content TEXT NOT NULL
)
Where:
- id: Unique identifier for the forum post
- posted: Unix timestamp when the post was originally created
- last_post: Unix timestamp of the most recent activity on this thread/post
- author: User ID of the post author
- parent: Parent post ID (0 for top-level threads, >0 for replies)
- replies: Number of direct replies to this post
- title: Post title/subject line
- content: Post content/body text
# Thread and Reply System
## Thread Structure
The forum uses a parent/child hierarchy:
// Top-level threads have parent = 0
if post.IsThread() {
fmt.Println("This is a main thread")
}
// Replies have parent > 0
if post.IsReply() {
fmt.Printf("This is a reply to post %d\n", post.Parent)
}
## Working with Threads
Get all top-level threads:
threads, _ := forum.Threads(db)
fmt.Println("=== Forum Threads ===")
for _, thread := range threads {
age := thread.ActivityAge()
var activityStr string
if age < time.Hour {
activityStr = fmt.Sprintf("%d minutes ago", int(age.Minutes()))
} else if age < 24*time.Hour {
activityStr = fmt.Sprintf("%d hours ago", int(age.Hours()))
} else {
activityStr = fmt.Sprintf("%d days ago", int(age.Hours()/24))
}
fmt.Printf("📌 %s (%d replies, last activity %s)\n",
thread.Title, thread.Replies, activityStr)
}
## Working with Replies
Get all replies to a thread:
thread, _ := forum.Find(db, threadID)
replies, _ := thread.GetReplies()
fmt.Printf("=== %s ===\n", thread.Title)
fmt.Printf("Posted by user %d on %s\n\n",
thread.Author, thread.PostedTime().Format("Jan 2, 2006"))
fmt.Println(thread.Content)
if len(replies) > 0 {
fmt.Printf("\n--- %d Replies ---\n", len(replies))
for i, reply := range replies {
fmt.Printf("[%d] by user %d on %s:\n%s\n\n",
i+1, reply.Author, reply.PostedTime().Format("Jan 2 15:04"),
reply.Content)
}
}
## Navigation Between Posts
Navigate the thread hierarchy:
reply, _ := forum.Find(db, replyID)
// Get the parent thread
thread, err := reply.GetThread()
if err == nil {
fmt.Printf("This reply belongs to thread: %s\n", thread.Title)
}
// Get all sibling replies
siblings, _ := forum.ByParent(db, reply.Parent)
fmt.Printf("This thread has %d total replies\n", len(siblings))
# Search and Filtering
## Text Search
Search within titles and content:
// Search for posts about "pvp"
pvpPosts, err := forum.Search(db, "pvp")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Found %d posts about PvP:\n", len(pvpPosts))
for _, post := range pvpPosts {
fmt.Printf("- %s: %s\n", post.Title, post.Preview(60))
}
Search is case-insensitive and searches both titles and content.
## Author-Based Queries
Find posts by specific users:
// Get all posts by a user
userPosts, err := forum.ByAuthor(db, userID)
if err != nil {
log.Fatal(err)
}
fmt.Printf("User %d has made %d posts:\n", userID, len(userPosts))
for _, post := range userPosts {
postType := "thread"
if post.IsReply() {
postType = "reply"
}
fmt.Printf("- [%s] %s (%s ago)\n",
postType, post.Title, post.PostAge())
}
## Activity-Based Queries
Find recent activity:
// Get recent forum activity
recentPosts, err := forum.Recent(db, 20)
if err != nil {
log.Fatal(err)
}
// Get activity since user's last visit
lastVisit := getUserLastVisit(userID)
newActivity, err := forum.Since(db, lastVisit)
if len(newActivity) > 0 {
fmt.Printf("There have been %d new posts since your last visit\n",
len(newActivity))
}
# Post Management
## Reply Count Management
Manage reply counts when posts are created or deleted:
// When creating a reply
reply, err := forum.NewBuilder(db).
WithAuthor(userID).
WithContent("Great point!").
AsReply(parentID).
Create()
if err == nil {
// Update parent thread's reply count and last activity
parentThread, _ := forum.Find(db, parentID)
parentThread.IncrementReplies()
parentThread.UpdateLastPost()
parentThread.Save()
}
// When deleting a reply
replyToDelete, _ := forum.Find(db, replyID)
parentID := replyToDelete.Parent
replyToDelete.Delete()
// Update parent thread
if parentID > 0 {
parentThread, _ := forum.Find(db, parentID)
parentThread.DecrementReplies()
parentThread.Save()
}
## Content Analysis
Analyze post content:
post, _ := forum.Find(db, postID)
// Basic content metrics
fmt.Printf("Post length: %d characters\n", post.Length())
fmt.Printf("Word count: %d words\n", post.WordCount())
// Check for specific terms
if post.Contains("bug") {
fmt.Println("This post mentions a bug")
}
// Generate preview for listings
preview := post.Preview(100)
fmt.Printf("Preview: %s\n", preview)
## Time Analysis
Track posting and activity patterns:
post, _ := forum.Find(db, postID)
postAge := post.PostAge()
activityAge := post.ActivityAge()
fmt.Printf("Post created %v ago\n", postAge)
fmt.Printf("Last activity %v ago\n", activityAge)
if post.IsRecentActivity() {
fmt.Println("This thread has recent activity")
}
# Forum Display Patterns
## Thread Listing
Display forum index:
func displayForumIndex(db *database.DB) {
threads, _ := forum.Threads(db)
fmt.Println("=== Game Forum ===")
fmt.Printf("%-40s %-8s %-15s\n", "Thread", "Replies", "Last Activity")
fmt.Println(strings.Repeat("-", 65))
for _, thread := range threads {
title := thread.Title
if len(title) > 37 {
title = title[:37] + "..."
}
age := thread.ActivityAge()
var ageStr string
if age < time.Hour {
ageStr = fmt.Sprintf("%dm ago", int(age.Minutes()))
} else if age < 24*time.Hour {
ageStr = fmt.Sprintf("%dh ago", int(age.Hours()))
} else {
ageStr = fmt.Sprintf("%dd ago", int(age.Hours()/24))
}
fmt.Printf("%-40s %-8d %-15s\n", title, thread.Replies, ageStr)
}
}
## Thread View
Display a complete thread with replies:
func displayThread(db *database.DB, threadID int) error {
thread, err := forum.Find(db, threadID)
if err != nil {
return err
}
if !thread.IsThread() {
return fmt.Errorf("post %d is not a thread", threadID)
}
// Display thread
fmt.Printf("=== %s ===\n", thread.Title)
fmt.Printf("By user %d on %s\n\n",
thread.Author, thread.PostedTime().Format("January 2, 2006 at 3:04 PM"))
fmt.Println(thread.Content)
// Display replies
replies, _ := thread.GetReplies()
if len(replies) > 0 {
fmt.Printf("\n--- %d Replies ---\n\n", len(replies))
for i, reply := range replies {
fmt.Printf("#%d by user %d on %s:\n",
i+1, reply.Author, reply.PostedTime().Format("Jan 2 at 3:04 PM"))
fmt.Println(reply.Content)
fmt.Println()
}
}
return nil
}
# Moderation Features
## Content Moderation
Tools for forum moderation:
// Flag posts for review
func moderatePost(db *database.DB, postID int) {
post, err := forum.Find(db, postID)
if err != nil {
return
}
// Check for very short posts (potential spam)
if post.WordCount() < 3 {
fmt.Printf("Short post flagged: %s\n", post.Preview(30))
}
// Check for very long posts
if post.Length() > 5000 {
fmt.Printf("Very long post from user %d\n", post.Author)
}
// Check for specific terms
suspiciousTerms := []string{"spam", "hack", "cheat"}
for _, term := range suspiciousTerms {
if post.Contains(term) {
fmt.Printf("Post contains suspicious term '%s'\n", term)
}
}
}
## User Activity Analysis
Analyze user forum behavior:
func analyzeUserActivity(db *database.DB, userID int) {
posts, _ := forum.ByAuthor(db, userID)
fmt.Printf("User %d forum activity:\n", userID)
fmt.Printf("- Total posts: %d\n", len(posts))
threadCount := 0
replyCount := 0
totalWords := 0
for _, post := range posts {
if post.IsThread() {
threadCount++
} else {
replyCount++
}
totalWords += post.WordCount()
}
fmt.Printf("- Threads started: %d\n", threadCount)
fmt.Printf("- Replies posted: %d\n", replyCount)
if len(posts) > 0 {
avgWords := totalWords / len(posts)
fmt.Printf("- Average words per post: %d\n", avgWords)
latest := posts[0] // ByAuthor returns newest first
fmt.Printf("- Last post: %s (%s ago)\n",
latest.Title, latest.PostAge())
}
}
# Performance Considerations
## Efficient Queries
Optimize database queries for forum performance:
// Use specific queries for common operations
threads, _ := forum.Threads(db) // More efficient than filtering All()
// Limit results for pagination
recentPosts, _ := forum.Recent(db, 25) // Get page worth of data
// Use specific parent queries
replies, _ := forum.ByParent(db, threadID) // Efficient for thread display
## Caching Strategies
Cache frequently accessed data:
// Cache popular threads
var popularThreadsCache []*forum.Forum
var cacheTime time.Time
func getPopularThreads(db *database.DB) []*forum.Forum {
// Refresh cache every 5 minutes
if time.Since(cacheTime) > 5*time.Minute {
threads, _ := forum.Threads(db)
// Sort by activity (replies + recent posts)
sort.Slice(threads, func(i, j int) bool {
scoreI := threads[i].Replies
scoreJ := threads[j].Replies
// Bonus for recent activity
if threads[i].IsRecentActivity() {
scoreI += 10
}
if threads[j].IsRecentActivity() {
scoreJ += 10
}
return scoreI > scoreJ
})
// Cache top 10
if len(threads) > 10 {
threads = threads[:10]
}
popularThreadsCache = threads
cacheTime = time.Now()
}
return popularThreadsCache
}
# Integration Examples
## User Notifications
Integrate with notification system:
func notifyNewReply(db *database.DB, replyID int) {
reply, err := forum.Find(db, replyID)
if err != nil {
return
}
// Get the parent thread
thread, err := reply.GetThread()
if err != nil {
return
}
// Notify thread author if different from reply author
if thread.Author != reply.Author {
message := fmt.Sprintf("New reply to your thread '%s'", thread.Title)
sendNotification(thread.Author, message)
}
// Notify other participants in the thread
allReplies, _ := thread.GetReplies()
participants := make(map[int]bool)
participants[thread.Author] = true
for _, r := range allReplies {
if r.Author != reply.Author && !participants[r.Author] {
message := fmt.Sprintf("New activity in thread '%s'", thread.Title)
sendNotification(r.Author, message)
participants[r.Author] = true
}
}
}
## Search Integration
Provide advanced search capabilities:
func advancedSearch(db *database.DB, query string, authorID int,
onlyThreads bool, since time.Time) []*forum.Forum {
var results []*forum.Forum
// Start with text search
if query != "" {
textResults, _ := forum.Search(db, query)
results = append(results, textResults...)
} else {
allPosts, _ := forum.All(db)
results = allPosts
}
// Apply filters
var filtered []*forum.Forum
sinceUnix := since.Unix()
for _, post := range results {
// Author filter
if authorID > 0 && post.Author != authorID {
continue
}
// Thread-only filter
if onlyThreads && !post.IsThread() {
continue
}
// Time filter
if post.LastPost < sinceUnix {
continue
}
filtered = append(filtered, post)
}
return filtered
}
# Error Handling
Common error scenarios and handling:
post, err := forum.Find(db, postID)
if err != nil {
// Handle post not found or database issues
log.Printf("Failed to find post %d: %v", postID, err)
return
}
// Validate post relationships
if post.IsReply() {
parentThread, err := post.GetThread()
if err != nil {
log.Printf("Warning: Reply %d has invalid parent %d",
post.ID, post.Parent)
}
}
// Save with error handling
if err := post.Save(); err != nil {
log.Printf("Failed to save post %d: %v", post.ID, err)
}
The forum package provides a complete forum system with hierarchical discussions, search capabilities, and comprehensive post management suitable for game communities.
*/
package forum