627 lines
14 KiB
Go
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 |