diff --git a/internal/database/handlers.go b/internal/database/handlers.go index ea8ab27..83a378c 100644 --- a/internal/database/handlers.go +++ b/internal/database/handlers.go @@ -58,7 +58,7 @@ func GetBoard(db *sql.DB, slug string) (Board, error) { func GetThreads(db *sql.DB, boardSlug string) ([]Thread, error) { rows, err := db.Query(` - SELECT id, board_slug, subject, created_at, bumped_at + SELECT id, board_slug, subject, created_at, bumped_at, pinned, locked FROM threads WHERE board_slug = ?`, boardSlug) @@ -70,7 +70,9 @@ func GetThreads(db *sql.DB, boardSlug string) ([]Thread, error) { var result []Thread for rows.Next() { var t Thread - err := rows.Scan(&t.Id, &t.BoardSlug, &t.Subject, &t.CreatedAt, &t.BumpedAt) + err := rows.Scan( + &t.Id, &t.BoardSlug, &t.Subject, &t.CreatedAt, &t.BumpedAt, + &t.Pinned, &t.Locked) if err != nil { return nil, err } @@ -82,79 +84,19 @@ func GetThreads(db *sql.DB, boardSlug string) ([]Thread, error) { func GetThread(db *sql.DB, threadId int) (Thread, error) { row := db.QueryRow(` - SELECT id, board_slug, subject, created_at, bumped_at + SELECT id, board_slug, subject, created_at, bumped_at, pinned, locked FROM threads WHERE id = ?`, threadId) - var thread Thread - err := row.Scan(&thread.Id, &thread.BoardSlug, &thread.Subject, &thread.CreatedAt, &thread.BumpedAt) + var t Thread + err := row.Scan( + &t.Id, &t.BoardSlug, &t.Subject, &t.CreatedAt, &t.BumpedAt, + &t.Pinned, &t.Locked) if err != nil { return Thread{}, err } - return thread, row.Err() -} - -func GetPosts(db *sql.DB, threadId int) ([]Post, error) { - rows, err := db.Query(` - SELECT id, thread_id, author, body, created_at, media_path, - ip_hash, number, thumb_path, banned - FROM posts - WHERE thread_id = ?`, threadId) - - if err != nil { - return nil, err - } - defer rows.Close() - - var result []Post - for rows.Next() { - var p Post - err := rows.Scan( - &p.Id, &p.ThreadId, &p.Author, &p.Body, &p.CreatedAt, &p.MediaPath, - &p.IpHash, &p.Number, &p.ThumbPath, &p.Banned) - if err != nil { - return nil, err - } - result = append(result, p) - } - - return result, rows.Err() -} - -func GetPost(db *sql.DB, postId int) (Post, error) { - row := db.QueryRow(` - SELECT id, thread_id, author, body, created_at, media_path, - ip_hash, number, thumb_path, banned - FROM posts - WHERE id = ?`, postId) - - var r Post - err := row.Scan( - &r.Id, &r.ThreadId, &r.Author, &r.Body, &r.CreatedAt, &r.MediaPath, - &r.IpHash, &r.Number, &r.ThumbPath, &r.Banned) - if err != nil { - return Post{}, err - } - return r, row.Err() -} - -func GetOriginalPost(db *sql.DB, threadId int) (Post, error) { - row := db.QueryRow(` - SELECT id, thread_id, author, body, created_at, media_path, - ip_hash, number, thumb_path, banned - FROM posts - WHERE thread_id = ? - ORDER BY created_at ASC LIMIT 1`, threadId) - - var r Post - err := row.Scan( - &r.Id, &r.ThreadId, &r.Author, &r.Body, &r.CreatedAt, &r.MediaPath, - &r.IpHash, &r.Number, &r.ThumbPath, &r.Banned) - if err != nil { - return Post{}, err - } - return r, row.Err() + return t, row.Err() } func PutThread(db *sql.DB, boardSlug string, subject string, body string, mediaPath string, thumbPath string, ip_hash string) (int, error) { @@ -215,38 +157,6 @@ func PutThread(db *sql.DB, boardSlug string, subject string, body string, mediaP return int(threadIdStr), nil } -func PutPost(db Queryer, boardSlug string, threadId int, body string, mediaPath string, thumbPath string, ip_hash string) error { - row := db.QueryRow(` - SELECT MAX(p.number) - FROM posts p - INNER JOIN threads t ON p.thread_id = t.id - WHERE t.board_slug = ?`, boardSlug) - - var latestPostNumber sql.NullInt64 - if err := row.Scan(&latestPostNumber); err != nil { - return err - } - - newPostNumber := 1 - if latestPostNumber.Valid { - newPostNumber = int(latestPostNumber.Int64) + 1 - } - - _, err := db.Exec(` - INSERT INTO posts (thread_id, body, media_path, ip_hash, number, thumb_path) - VALUES (?, ?, ?, ?, ?, ?)`, threadId, body, mediaPath, ip_hash, newPostNumber, thumbPath) - if err != nil { - return err - } - - _, err = db.Exec(`UPDATE threads SET bumped_at = CURRENT_TIMESTAMP where id = ?`, threadId) - if err != nil { - return err - } - - return nil -} - func DeleteThread(db Queryer, threadId int) error { // cleanup images rows, err := db.Query(` @@ -294,6 +204,100 @@ func DeleteThread(db Queryer, threadId int) error { return nil } +func GetPosts(db *sql.DB, threadId int) ([]Post, error) { + rows, err := db.Query(` + SELECT id, thread_id, author, body, created_at, media_path, + ip_hash, number, thumb_path, banned + FROM posts + WHERE thread_id = ?`, threadId) + + if err != nil { + return nil, err + } + defer rows.Close() + + var result []Post + for rows.Next() { + var p Post + err := rows.Scan( + &p.Id, &p.ThreadId, &p.Author, &p.Body, &p.CreatedAt, &p.MediaPath, + &p.IpHash, &p.Number, &p.ThumbPath, &p.Banned) + if err != nil { + return nil, err + } + result = append(result, p) + } + + return result, rows.Err() +} + +func GetOriginalPost(db *sql.DB, threadId int) (Post, error) { + row := db.QueryRow(` + SELECT id, thread_id, author, body, created_at, media_path, + ip_hash, number, thumb_path, banned + FROM posts + WHERE thread_id = ? + ORDER BY created_at ASC LIMIT 1`, threadId) + + var r Post + err := row.Scan( + &r.Id, &r.ThreadId, &r.Author, &r.Body, &r.CreatedAt, &r.MediaPath, + &r.IpHash, &r.Number, &r.ThumbPath, &r.Banned) + if err != nil { + return Post{}, err + } + return r, row.Err() +} + +func GetPost(db *sql.DB, postId int) (Post, error) { + row := db.QueryRow(` + SELECT id, thread_id, author, body, created_at, media_path, + ip_hash, number, thumb_path, banned + FROM posts + WHERE id = ?`, postId) + + var r Post + err := row.Scan( + &r.Id, &r.ThreadId, &r.Author, &r.Body, &r.CreatedAt, &r.MediaPath, + &r.IpHash, &r.Number, &r.ThumbPath, &r.Banned) + if err != nil { + return Post{}, err + } + return r, row.Err() +} + +func PutPost(db Queryer, boardSlug string, threadId int, body string, mediaPath string, thumbPath string, ip_hash string) error { + row := db.QueryRow(` + SELECT MAX(p.number) + FROM posts p + INNER JOIN threads t ON p.thread_id = t.id + WHERE t.board_slug = ?`, boardSlug) + + var latestPostNumber sql.NullInt64 + if err := row.Scan(&latestPostNumber); err != nil { + return err + } + + newPostNumber := 1 + if latestPostNumber.Valid { + newPostNumber = int(latestPostNumber.Int64) + 1 + } + + _, err := db.Exec(` + INSERT INTO posts (thread_id, body, media_path, ip_hash, number, thumb_path) + VALUES (?, ?, ?, ?, ?, ?)`, threadId, body, mediaPath, ip_hash, newPostNumber, thumbPath) + if err != nil { + return err + } + + _, err = db.Exec(`UPDATE threads SET bumped_at = CURRENT_TIMESTAMP where id = ?`, threadId) + if err != nil { + return err + } + + return nil +} + func DeletePost(db *sql.DB, postId int) error { // cleanup images row := db.QueryRow(` @@ -336,20 +340,6 @@ func DeletePost(db *sql.DB, postId int) error { return nil } -func GetAdmin(db *sql.DB, username string) (Admin, error) { - row := db.QueryRow(` - SELECT username, password - FROM admins - WHERE username = ?`, username) - - var result Admin - if err := row.Scan(&result.Username, &result.Password); err != nil { - return Admin{}, nil - } - - return result, nil -} - func BanIp(db *sql.DB, ipHash string, reason string, expiration time.Time) error { log.Printf("IP: %s, reason: %s, expiration: %v", ipHash, reason, expiration) // if the ban already exists for ip, only update if its greater than existing @@ -393,3 +383,17 @@ func GetBan(db *sql.DB, ip string) (Ban, error) { return r, nil } + +func GetAdmin(db *sql.DB, username string) (Admin, error) { + row := db.QueryRow(` + SELECT username, password + FROM admins + WHERE username = ?`, username) + + var result Admin + if err := row.Scan(&result.Username, &result.Password); err != nil { + return Admin{}, nil + } + + return result, nil +} diff --git a/internal/database/models.go b/internal/database/models.go index 3f5c402..0a07bc8 100644 --- a/internal/database/models.go +++ b/internal/database/models.go @@ -15,6 +15,8 @@ type Thread struct { Subject string CreatedAt time.Time BumpedAt time.Time + Pinned bool + Locked bool } type Post struct { diff --git a/internal/database/seed.sql b/internal/database/seed.sql index 578a1bc..874b61e 100644 --- a/internal/database/seed.sql +++ b/internal/database/seed.sql @@ -17,6 +17,8 @@ CREATE TABLE IF NOT EXISTS threads ( subject TEXT NOT NULL DEFAULT '', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, bumped_at DATETIME DEFAULT CURRENT_TIMESTAMP, + pinned BOOLEAN NOT NULL DEFAULT 0, + locked BOOLEAN NOT NULL DEFAULT 0, FOREIGN KEY (board_slug) REFERENCES boards(slug) ON DELETE CASCADE ON UPDATE CASCADE ); diff --git a/web/main.go b/web/main.go index e66c91c..3f4eba9 100644 --- a/web/main.go +++ b/web/main.go @@ -428,6 +428,8 @@ func main() { ThumbPath: op.ThumbPath, ReplyCount: len(posts), IpCount: len(uniqueIpHashes), + Pinned: thread.Pinned, + Locked: thread.Locked, }) } @@ -526,19 +528,50 @@ func main() { r.Route("/admin", func(r chi.Router) { r.Use(AdminOnlyMiddleware) - r.Post("/logout", func(w http.ResponseWriter, r *http.Request) { - if c, err := r.Cookie("comfy_admin"); err == nil { - util.DeleteAdminSession(c.Value) + r.Patch("/threads/{threadId}/lock", func(w http.ResponseWriter, r *http.Request) { + threadIdStr := chi.URLParam(r, "threadId") + threadId, err := strconv.Atoi(threadIdStr) + if err != nil { + http.Error(w, "Invalid thread id", http.StatusBadRequest) + return + } + + lockedStr := r.URL.Query().Get("locked") + locked, err := strconv.ParseBool(lockedStr) + if err != nil { + http.Error(w, "Invalid values for 'locked'", http.StatusBadRequest) + return + } + + _, err = db.Exec(`UPDATE threads SET locked = ? WHERE id = ?`, locked, threadId) + if err != nil { + log.Println("Updating thread 'locked': ", err) + http.Error(w, "Failed to lock thread: "+threadIdStr, http.StatusInternalServerError) + return + } + }) + + r.Patch("/threads/{threadId}/pin", func(w http.ResponseWriter, r *http.Request) { + threadIdStr := chi.URLParam(r, "threadId") + threadId, err := strconv.Atoi(threadIdStr) + if err != nil { + http.Error(w, "Invalid thread id", http.StatusBadRequest) + return + } + + pinnedStr := r.URL.Query().Get("pinned") + pinned, err := strconv.ParseBool(pinnedStr) + if err != nil { + http.Error(w, "Invalid values for 'pinned'", http.StatusBadRequest) + return + } + + _, err = db.Exec(`UPDATE threads SET pinned = ? WHERE id = ?`, pinned, threadId) + if err != nil { + log.Println("Updating thread 'pinned': ", err) + http.Error(w, "Failed to pin thread: "+threadIdStr, http.StatusInternalServerError) + return } - http.SetCookie(w, &http.Cookie{ - Name: "comfy_admin", - Value: "", - HttpOnly: true, - Secure: !util.DevMode, - Expires: time.Now(), - SameSite: http.SameSiteStrictMode, - Path: "/", - }) }) r.Delete("/threads/{threadId}", func(w http.ResponseWriter, r *http.Request) { @@ -612,6 +645,21 @@ func main() { return } }) + + r.Post("/logout", func(w http.ResponseWriter, r *http.Request) { + if c, err := r.Cookie("comfy_admin"); err == nil { + util.DeleteAdminSession(c.Value) + } + http.SetCookie(w, &http.Cookie{ + Name: "comfy_admin", + Value: "", + HttpOnly: true, + Secure: !util.DevMode, + Expires: time.Now(), + SameSite: http.SameSiteStrictMode, + Path: "/", + }) + }) }) // ----------------- diff --git a/web/views/board.templ b/web/views/board.templ index 22e64ea..7b46e54 100644 --- a/web/views/board.templ +++ b/web/views/board.templ @@ -37,7 +37,7 @@ templ Board(board database.Board, isAdmin bool) { - [Refresh] + [Refresh]
@templ.Raw(util.EnrichPost(preview.Body)) @@ -111,6 +119,39 @@ templ ThreadsCatalog(previews []CatalogThreadPreview, catalogContext CatalogCont _="on htmx:afterRequest trigger refreshPosts on body" hx-confirm="Are you sure you wish to delete this thread?" >Delete + + +