311 lines
8.1 KiB
Go
311 lines
8.1 KiB
Go
package routes
|
|
|
|
import (
|
|
"dk/internal/components"
|
|
"dk/internal/database"
|
|
"dk/internal/helpers"
|
|
"dk/internal/helpers/markdown"
|
|
"dk/internal/models/forum"
|
|
"dk/internal/models/users"
|
|
"fmt"
|
|
"log"
|
|
"strings"
|
|
"time"
|
|
|
|
sushi "git.sharkk.net/Sharkk/Sushi"
|
|
"git.sharkk.net/Sharkk/Sushi/auth"
|
|
)
|
|
|
|
type threadData struct {
|
|
ID int
|
|
Posted int64
|
|
LastPost int64
|
|
Author int
|
|
Parent int
|
|
Replies int
|
|
Title string
|
|
Content string
|
|
AuthorUsername string
|
|
AuthorLevel int
|
|
AuthorClass string
|
|
LastReplyUsername string
|
|
LastReplyLevel int
|
|
LastReplyClassID int
|
|
HasReplies bool
|
|
}
|
|
|
|
type replyData struct {
|
|
ID int
|
|
Posted int64
|
|
Author int
|
|
Title string
|
|
Content string
|
|
AuthorUsername string
|
|
AuthorLevel int
|
|
AuthorClass string
|
|
}
|
|
|
|
func (t *threadData) PostedTime() time.Time {
|
|
return time.Unix(t.Posted, 0)
|
|
}
|
|
|
|
func (t *threadData) LastPostTime() time.Time {
|
|
return time.Unix(t.LastPost, 0)
|
|
}
|
|
|
|
func (r *replyData) PostedTime() time.Time {
|
|
return time.Unix(r.Posted, 0)
|
|
}
|
|
|
|
func RegisterForumRoutes(app *sushi.App) {
|
|
authed := app.Group("/forum")
|
|
authed.Use(auth.RequireAuth())
|
|
authed.Get("/", forumIndex)
|
|
authed.Get("/new", showNew)
|
|
authed.Post("/new", newThread)
|
|
authed.Get("/:id", showThread)
|
|
authed.Post("/:id/reply", reply)
|
|
}
|
|
|
|
// Helper function to validate thread exists and is actually a thread (not a reply)
|
|
func validateThread(ctx sushi.Ctx, id int) (*forum.Forum, bool) {
|
|
sess := ctx.GetCurrentSession()
|
|
thread, err := forum.Find(id)
|
|
if err != nil {
|
|
sess.SetFlash("error", fmt.Sprintf("Forum thread %d not found", id))
|
|
ctx.Redirect("/forum")
|
|
return nil, false
|
|
}
|
|
|
|
if thread.Parent != 0 {
|
|
sess.SetFlash("error", fmt.Sprintf("Forum post %d is not a thread", id))
|
|
ctx.Redirect("/forum")
|
|
return nil, false
|
|
}
|
|
|
|
return thread, true
|
|
}
|
|
|
|
// Helper function to get pagination params from request
|
|
func getPagination(ctx sushi.Ctx, perPage int) helpers.Pagination {
|
|
page := max(int(ctx.QueryArgs().GetUintOrZero("page")), 1)
|
|
return helpers.Pagination{
|
|
Page: page,
|
|
PerPage: perPage,
|
|
}
|
|
}
|
|
|
|
func forumIndex(ctx sushi.Ctx) {
|
|
pagination := getPagination(ctx, 30)
|
|
|
|
// Get threads with user data
|
|
var threads []*threadData
|
|
query := `
|
|
SELECT f.id, f.posted, f.last_post, f.author, f.parent, f.replies, f.title, f.content,
|
|
COALESCE(u.username, '[Deleted]') as author_username,
|
|
COALESCE(u.level, 1) as author_level,
|
|
COALESCE(c.name, 'Unknown') as author_class
|
|
FROM forum f
|
|
LEFT JOIN users u ON f.author = u.id
|
|
LEFT JOIN classes c ON u.class_id = c.id
|
|
WHERE f.parent = 0
|
|
ORDER BY f.last_post DESC, f.id DESC
|
|
LIMIT %d OFFSET %d`
|
|
|
|
err := database.Select(&threads, query, pagination.PerPage, pagination.Offset())
|
|
if err != nil {
|
|
log.Printf("Error fetching forum threads: %v", err)
|
|
threads = make([]*threadData, 0)
|
|
}
|
|
|
|
// Get last reply info for each thread
|
|
for i := range threads {
|
|
type ReplyCount struct{ Count int }
|
|
var replyCount ReplyCount
|
|
database.Get(&replyCount, "SELECT COUNT(*) as count FROM forum WHERE parent = %d", threads[i].ID)
|
|
|
|
if replyCount.Count > 0 {
|
|
type LastReply struct{ Username string }
|
|
var lastReply LastReply
|
|
err := database.Get(&lastReply, `
|
|
SELECT COALESCE(u.username, '[Deleted]') as username
|
|
FROM forum f
|
|
LEFT JOIN users u ON f.author = u.id
|
|
WHERE f.parent = %d
|
|
ORDER BY f.posted DESC
|
|
LIMIT 1`, threads[i].ID)
|
|
|
|
if err == nil {
|
|
threads[i].LastReplyUsername = lastReply.Username
|
|
threads[i].HasReplies = true
|
|
} else {
|
|
log.Printf("Error getting last reply author for thread %d: %v", threads[i].ID, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Get total count for pagination
|
|
type CountResult struct{ Count int }
|
|
var paginationResult CountResult
|
|
database.Get(&paginationResult, "SELECT COUNT(*) as count FROM forum WHERE parent = 0")
|
|
pagination.Total = paginationResult.Count
|
|
|
|
components.RenderPage(ctx, "Forum", "forum/index.html", map[string]any{
|
|
"threads": threads,
|
|
"currentPage": pagination.Page,
|
|
"totalPages": pagination.TotalPages(),
|
|
"hasNext": pagination.HasNext(),
|
|
"hasPrev": pagination.HasPrev(),
|
|
})
|
|
}
|
|
|
|
func showNew(ctx sushi.Ctx) {
|
|
components.RenderPage(ctx, "New Forum Thread", "forum/new.html", map[string]any{})
|
|
}
|
|
|
|
func newThread(ctx sushi.Ctx) {
|
|
sess := ctx.GetCurrentSession()
|
|
|
|
title := strings.TrimSpace(ctx.Form("title").String())
|
|
content := strings.TrimSpace(ctx.Form("content").String())
|
|
|
|
if title == "" {
|
|
sess.SetFlash("error", "Thread title cannot be empty")
|
|
ctx.Redirect("/forum/new")
|
|
return
|
|
}
|
|
|
|
if content == "" {
|
|
sess.SetFlash("error", "Thread content cannot be empty")
|
|
ctx.Redirect("/forum/new")
|
|
return
|
|
}
|
|
|
|
user := ctx.GetCurrentUser().(*users.User)
|
|
|
|
thread := forum.New()
|
|
thread.Author = user.ID
|
|
thread.Title = title
|
|
thread.Content = content
|
|
|
|
if err := thread.Insert(); err != nil {
|
|
log.Printf("Error creating thread: %v", err)
|
|
sess.SetFlash("error", "Failed to create thread")
|
|
ctx.Redirect("/forum/new")
|
|
return
|
|
}
|
|
|
|
ctx.Redirect(fmt.Sprintf("/forum/%d", thread.ID))
|
|
}
|
|
|
|
func showThread(ctx sushi.Ctx) {
|
|
id := ctx.Param("id").Int()
|
|
thread, ok := validateThread(ctx, id)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
pagination := getPagination(ctx, 10)
|
|
pagination.Total = thread.Replies
|
|
|
|
// Get thread with author data
|
|
var threadData threadData
|
|
err := database.Get(&threadData, `
|
|
SELECT f.id, f.posted, f.last_post, f.author, f.parent, f.replies, f.title, f.content,
|
|
COALESCE(u.username, '[Deleted]') as author_username,
|
|
COALESCE(u.level, 1) as author_level,
|
|
COALESCE(c.name, 'Unknown') as author_class
|
|
FROM forum f
|
|
LEFT JOIN users u ON f.author = u.id
|
|
LEFT JOIN classes c ON u.class_id = c.id
|
|
WHERE f.id = %d`, id)
|
|
|
|
if err != nil {
|
|
log.Printf("Error loading thread %d: %v", id, err)
|
|
sess := ctx.GetCurrentSession()
|
|
sess.SetFlash("error", "Failed to load thread")
|
|
ctx.Redirect("/forum")
|
|
return
|
|
}
|
|
|
|
// Get replies with user data
|
|
var replies []*replyData
|
|
err = database.Select(&replies, `
|
|
SELECT f.id, f.posted, f.author, f.title, f.content,
|
|
COALESCE(u.username, '[Deleted]') as author_username,
|
|
COALESCE(u.level, 1) as author_level,
|
|
COALESCE(c.name, 'Unknown') as author_class
|
|
FROM forum f
|
|
LEFT JOIN users u ON f.author = u.id
|
|
LEFT JOIN classes c ON u.class_id = c.id
|
|
WHERE f.parent = %d
|
|
ORDER BY f.posted ASC
|
|
LIMIT %d OFFSET %d`, id, pagination.PerPage, pagination.Offset())
|
|
|
|
if err != nil {
|
|
log.Printf("Error loading replies for thread %d: %v", id, err)
|
|
replies = make([]*replyData, 0)
|
|
}
|
|
|
|
components.RenderPage(ctx, threadData.Title, "forum/thread.html", map[string]any{
|
|
"thread": &threadData,
|
|
"replies": replies,
|
|
"currentPage": pagination.Page,
|
|
"totalPages": pagination.TotalPages(),
|
|
"hasNext": pagination.HasNext(),
|
|
"hasPrev": pagination.HasPrev(),
|
|
"markdown": markdown.MarkdownToHTML,
|
|
})
|
|
}
|
|
|
|
func reply(ctx sushi.Ctx) {
|
|
sess := ctx.GetCurrentSession()
|
|
id := ctx.Param("id").Int()
|
|
|
|
thread, ok := validateThread(ctx, id)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
content := strings.TrimSpace(ctx.Form("content").String())
|
|
if content == "" {
|
|
sess.SetFlash("error", "Reply content cannot be empty")
|
|
ctx.Redirect(fmt.Sprintf("/forum/%d#reply", id))
|
|
return
|
|
}
|
|
|
|
user := ctx.GetCurrentUser().(*users.User)
|
|
|
|
reply := forum.New()
|
|
reply.Author = user.ID
|
|
reply.Parent = thread.ID
|
|
reply.Title = "Re: " + thread.Title
|
|
reply.Content = content
|
|
|
|
err := database.Transaction(func() error {
|
|
if err := reply.Insert(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Update thread reply count and last post time
|
|
thread.IncrementReplies()
|
|
thread.UpdateLastPost()
|
|
|
|
return database.Exec("UPDATE forum SET replies = %d, last_post = %d WHERE id = %d",
|
|
thread.Replies, thread.LastPost, thread.ID)
|
|
})
|
|
|
|
if err != nil {
|
|
log.Printf("Error posting reply to thread %d: %v", id, err)
|
|
sess.SetFlash("error", "Failed to post reply")
|
|
ctx.Redirect(fmt.Sprintf("/forum/%d#reply", id))
|
|
return
|
|
}
|
|
|
|
// Calculate the last page where the new reply will appear
|
|
const repliesPerPage = 10
|
|
lastPage := max((thread.Replies+repliesPerPage-1)/repliesPerPage, 1)
|
|
|
|
ctx.Redirect(fmt.Sprintf("/forum/%d?page=%d", thread.ID, lastPage))
|
|
}
|