mirror of
https://github.com/Eggbertx/gochan.git
synced 2025-08-06 21:46:24 -07:00
239 lines
8.1 KiB
Go
239 lines
8.1 KiB
Go
package gcsql
|
|
|
|
import (
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
)
|
|
|
|
const (
|
|
selectThreadsBaseSQL = `SELECT
|
|
id, board_id, locked, stickied, anchored, cyclical, last_bump, deleted_at, is_deleted
|
|
FROM DBPREFIXthreads `
|
|
)
|
|
|
|
var (
|
|
ErrThreadExists = errors.New("thread already exists")
|
|
ErrThreadDoesNotExist = errors.New("thread does not exist")
|
|
ErrThreadLocked = errors.New("thread is locked and cannot be replied to")
|
|
)
|
|
|
|
func createThread(tx *sql.Tx, boardID int, locked bool, stickied bool, anchored bool, cyclical bool) (threadID int, err error) {
|
|
const lockedQuery = `SELECT locked FROM DBPREFIXboards WHERE id = ?`
|
|
const insertQuery = `INSERT INTO DBPREFIXthreads (board_id, locked, stickied, anchored, cyclical) VALUES (?,?,?,?,?)`
|
|
var boardIsLocked bool
|
|
if err = QueryRowTxSQL(tx, lockedQuery, interfaceSlice(boardID), interfaceSlice(&boardIsLocked)); err != nil {
|
|
return 0, err
|
|
}
|
|
if boardIsLocked {
|
|
return 0, ErrBoardIsLocked
|
|
}
|
|
if _, err = ExecTxSQL(tx, insertQuery, boardID, locked, stickied, anchored, cyclical); err != nil {
|
|
return 0, err
|
|
}
|
|
QueryRowTxSQL(tx, "SELECT MAX(id) FROM DBPREFIXthreads", nil, interfaceSlice(&threadID))
|
|
return threadID, err
|
|
}
|
|
|
|
// GetThread returns a a thread object from the database, given its ID
|
|
func GetThread(threadID int) (*Thread, error) {
|
|
const query = selectThreadsBaseSQL + `WHERE id = ?`
|
|
thread := new(Thread)
|
|
err := QueryRowSQL(query, interfaceSlice(threadID), interfaceSlice(
|
|
&thread.ID, &thread.BoardID, &thread.Locked, &thread.Stickied, &thread.Anchored, &thread.Cyclical,
|
|
&thread.LastBump, &thread.DeletedAt, &thread.IsDeleted,
|
|
))
|
|
return thread, err
|
|
}
|
|
|
|
// GetPostThread returns a thread object from the database, given the ID of a post in the thread
|
|
func GetPostThread(opID int) (*Thread, error) {
|
|
const query = selectThreadsBaseSQL + `WHERE id = (SELECT thread_id FROM DBPREFIXposts WHERE id = ? LIMIT 1)`
|
|
thread := new(Thread)
|
|
err := QueryRowSQL(query, interfaceSlice(opID), interfaceSlice(
|
|
&thread.ID, &thread.BoardID, &thread.Locked, &thread.Stickied, &thread.Anchored, &thread.Cyclical,
|
|
&thread.LastBump, &thread.DeletedAt, &thread.IsDeleted,
|
|
))
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
err = ErrThreadDoesNotExist
|
|
}
|
|
return thread, err
|
|
}
|
|
|
|
// GetTopPostThreadID gets the thread ID from the database, given the post ID of a top post
|
|
func GetTopPostThreadID(opID int) (int, error) {
|
|
const query = `SELECT thread_id FROM DBPREFIXposts WHERE id = ? and is_top_post`
|
|
var threadID int
|
|
err := QueryRowSQL(query, interfaceSlice(opID), interfaceSlice(&threadID))
|
|
if err == sql.ErrNoRows {
|
|
err = ErrThreadDoesNotExist
|
|
}
|
|
return threadID, err
|
|
}
|
|
|
|
// GetThreadsWithBoardID queries the database for the threads with the given board ID from the database.
|
|
// If onlyNotDeleted is true, it omits deleted threads and threads that were removed because the max
|
|
// thread limit was reached
|
|
func GetThreadsWithBoardID(boardID int, onlyNotDeleted bool) ([]Thread, error) {
|
|
query := selectThreadsBaseSQL + `WHERE board_id = ?`
|
|
if onlyNotDeleted {
|
|
query += " AND is_deleted = FALSE"
|
|
}
|
|
rows, err := QuerySQL(query, boardID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var threads []Thread
|
|
for rows.Next() {
|
|
var thread Thread
|
|
if err = rows.Scan(
|
|
&thread.ID, &thread.BoardID, &thread.Locked, &thread.Stickied, &thread.Anchored,
|
|
&thread.Cyclical, &thread.LastBump, &thread.DeletedAt, &thread.IsDeleted,
|
|
); err != nil {
|
|
return threads, err
|
|
}
|
|
threads = append(threads, thread)
|
|
}
|
|
return threads, nil
|
|
}
|
|
|
|
func GetThreadReplyCountFromOP(opID int) (int, error) {
|
|
const query = `SELECT COUNT(*) FROM DBPREFIXposts WHERE thread_id = (
|
|
SELECT thread_id FROM DBPREFIXposts WHERE id = ?) AND is_deleted = FALSE AND is_top_post = FALSE`
|
|
var num int
|
|
err := QueryRowSQL(query, interfaceSlice(opID), interfaceSlice(&num))
|
|
return num, err
|
|
}
|
|
|
|
// ChangeThreadBoardID updates the given thread's post ID and the destination board ID
|
|
func ChangeThreadBoardID(threadID int, newBoardID int) error {
|
|
if !DoesBoardExistByID(newBoardID) {
|
|
return ErrBoardDoesNotExist
|
|
}
|
|
_, err := ExecSQL(`UPDATE DBPREFIXthreads SET board_id = ? WHERE id = ?`, newBoardID, threadID)
|
|
return err
|
|
}
|
|
|
|
// ChangeThreadBoardByURI updates a thread's board ID, given the thread's post ID and
|
|
// the destination board's uri
|
|
func ChangeThreadBoardByURI(postID int, uri string) error {
|
|
boardID, err := getBoardIDFromURI(uri)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return ChangeThreadBoardID(postID, boardID)
|
|
}
|
|
|
|
func (t *Thread) GetBoard() (*Board, error) {
|
|
return GetBoardFromID(t.BoardID)
|
|
}
|
|
|
|
func (t *Thread) GetReplyFileCount() (int, error) {
|
|
const query = `SELECT COUNT(filename) FROM DBPREFIXfiles WHERE post_id IN (
|
|
SELECT id FROM DBPREFIXposts WHERE thread_id = ? AND is_deleted = FALSE)`
|
|
var fileCount int
|
|
err := QueryRowSQL(query, interfaceSlice(t.ID), interfaceSlice(&fileCount))
|
|
return fileCount, err
|
|
}
|
|
|
|
// GetReplyCount returns the number of posts in the thread, not including the top post or any deleted posts
|
|
func (t *Thread) GetReplyCount() (int, error) {
|
|
const query = `SELECT COUNT(*) FROM DBPREFIXposts WHERE thread_id = ? AND is_top_post = FALSE AND is_deleted = FALSE`
|
|
var numReplies int
|
|
err := QueryRowSQL(query, interfaceSlice(t.ID), interfaceSlice(&numReplies))
|
|
return numReplies, err
|
|
}
|
|
|
|
// GetPosts returns the posts in the thread, optionally excluding the top post. If limit >= 0, a limit is set.
|
|
// If reversed is true, it is returned in descending order
|
|
func (t *Thread) GetPosts(repliesOnly bool, boardPage bool, limit int) ([]Post, error) {
|
|
query := selectPostsBaseSQL + "WHERE thread_id = ?"
|
|
if boardPage {
|
|
query = "SELECT * FROM (" + query + " AND is_deleted = FALSE ORDER BY id DESC LIMIT " +
|
|
strconv.Itoa(limit+1) + ") AS posts ORDER BY id"
|
|
} else if repliesOnly {
|
|
query += " AND is_top_post = FALSE"
|
|
}
|
|
if !boardPage && limit > 0 {
|
|
query += " LIMIT " + strconv.Itoa(limit)
|
|
}
|
|
|
|
rows, err := QuerySQL(query, t.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var posts []Post
|
|
for rows.Next() {
|
|
var post Post
|
|
if err = rows.Scan(
|
|
&post.ID, &post.ThreadID, &post.IsTopPost, &post.IP, &post.CreatedOn, &post.Name,
|
|
&post.Tripcode, &post.IsRoleSignature, &post.Email, &post.Subject, &post.Message,
|
|
&post.MessageRaw, &post.Password, &post.DeletedAt, &post.IsDeleted,
|
|
&post.BannedMessage, &post.Flag, &post.Country,
|
|
); err != nil {
|
|
return posts, err
|
|
}
|
|
posts = append(posts, post)
|
|
}
|
|
return posts, nil
|
|
}
|
|
|
|
func (t *Thread) GetUploads() ([]Upload, error) {
|
|
const query = selectFilesBaseSQL + ` WHERE post_id IN (
|
|
SELECT id FROM DBPREFIXposts WHERE thread_id = ? and is_deleted = FALSE) AND filename != 'deleted'`
|
|
rows, err := QuerySQL(query, t.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var uploads []Upload
|
|
for rows.Next() {
|
|
var upload Upload
|
|
err = rows.Scan(
|
|
&upload.ID, &upload.PostID, &upload.FileOrder, &upload.OriginalFilename, &upload.Filename,
|
|
&upload.Checksum, &upload.FileSize, &upload.IsSpoilered, &upload.ThumbnailWidth,
|
|
&upload.ThumbnailHeight, &upload.Width, &upload.Height,
|
|
)
|
|
if err != nil {
|
|
return uploads, err
|
|
}
|
|
uploads = append(uploads, upload)
|
|
}
|
|
return uploads, nil
|
|
}
|
|
|
|
// UpdateAttribute updates the given attribute (valid attribute values are "locked", "stickied, "anchored",
|
|
// or "cyclical") for the thread
|
|
func (t *Thread) UpdateAttribute(attribute string, value bool) error {
|
|
updateSQL := "UPDATE DBPREFIXthreads SET "
|
|
switch attribute {
|
|
case "locked":
|
|
t.Locked = value
|
|
case "stickied":
|
|
t.Stickied = value
|
|
case "anchored":
|
|
t.Anchored = value
|
|
case "cyclical":
|
|
t.Cyclical = value
|
|
default:
|
|
return fmt.Errorf("invalid thread attribute %q", attribute)
|
|
}
|
|
updateSQL += attribute + " = ? WHERE id = ?"
|
|
_, err := ExecSQL(updateSQL, value, t.ID)
|
|
return err
|
|
}
|
|
|
|
// deleteThread updates the thread and sets it as deleted, as well as the posts where thread_id = threadID
|
|
func deleteThread(threadID int) error {
|
|
const deletePostsSQL = `UPDATE DBPREFIXposts SET is_deleted = TRUE, deleted_at = CURRENT_TIMESTAMP WHERE thread_id = ?`
|
|
const deleteThreadSQL = `UPDATE DBPREFIXthreads SET is_deleted = TRUE, deleted_at = CURRENT_TIMESTAMP WHERE id = ?`
|
|
_, err := ExecSQL(deletePostsSQL, threadID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = ExecSQL(deleteThreadSQL, threadID)
|
|
return err
|
|
}
|