1
0
Fork 0
mirror of https://github.com/Eggbertx/gochan.git synced 2025-08-06 21:46:24 -07:00
gochan/pkg/gcsql/boards.go
2023-07-12 10:56:03 -07:00

476 lines
14 KiB
Go

package gcsql
import (
"database/sql"
"errors"
"fmt"
"path"
"time"
"github.com/gochan-org/gochan/pkg/config"
)
const (
// selects all columns from DBPREFIXboards
selectBoardsBaseSQL = `SELECT
DBPREFIXboards.id, section_id, uri, dir, navbar_position, title, subtitle, description,
max_file_size, max_threads, default_style, locked, created_at, anonymous_name, force_anonymous,
autosage_after, no_images_after, max_message_length, min_message_length, allow_embeds, redirect_to_thread,
require_file, enable_catalog
FROM DBPREFIXboards
INNER JOIN (
SELECT id, hidden FROM DBPREFIXsections
) s ON DBPREFIXboards.section_id = s.id `
)
var (
// AllBoards provides a quick and simple way to access a list of all boards in non-hidden sections
// without having to do any SQL queries. It and AllSections are updated by ResetBoardSectionArrays
AllBoards []Board
ErrNilBoard = errors.New("board is nil")
ErrBoardExists = errors.New("board already exists")
ErrBoardDoesNotExist = errors.New("board does not exist")
ErrBoardIsLocked = errors.New("board is locked")
)
// DoesBoardExistByID returns a bool indicating whether a board with a given id exists
func DoesBoardExistByID(ID int) bool {
const query = `SELECT COUNT(id) FROM DBPREFIXboards WHERE id = ?`
var count int
QueryRowSQL(query, interfaceSlice(ID), interfaceSlice(&count))
return count > 0
}
// DoesBoardExistByDir returns a bool indicating whether a board with a given directory exists
func DoesBoardExistByDir(dir string) bool {
const query = `SELECT COUNT(dir) FROM DBPREFIXboards WHERE dir = ?`
var count int
QueryRowSQL(query, interfaceSlice(dir), interfaceSlice(&count))
return count > 0
}
// GetAllBoards gets a list of all existing boards
func GetAllBoards(onlyNonHidden bool) ([]Board, error) {
query := selectBoardsBaseSQL
if onlyNonHidden {
query += " WHERE s.hidden = FALSE"
}
query += " ORDER BY navbar_position ASC, DBPREFIXboards.id ASC"
rows, err := QuerySQL(query)
if err != nil {
return nil, err
}
defer rows.Close()
var boards []Board
for rows.Next() {
var board Board
if err = rows.Scan(
&board.ID, &board.SectionID, &board.URI, &board.Dir, &board.NavbarPosition, &board.Title, &board.Subtitle,
&board.Description, &board.MaxFilesize, &board.MaxThreads, &board.DefaultStyle, &board.Locked,
&board.CreatedAt, &board.AnonymousName, &board.ForceAnonymous, &board.AutosageAfter, &board.NoImagesAfter,
&board.MaxMessageLength, &board.MinMessageLength, &board.AllowEmbeds, &board.RedirectToThread, &board.RequireFile,
&board.EnableCatalog,
); err != nil {
return nil, err
}
boards = append(boards, board)
}
return boards, nil
}
func GetBoardDir(id int) (string, error) {
const query = `SELECT dir FROM DBPREFIXboards WHERE id = ?`
var dir string
err := QueryRowSQL(query, interfaceSlice(id), interfaceSlice(&dir))
return dir, err
}
// GetBoardFromPostID gets the boardURI that a given postid exists on
func GetBoardDirFromPostID(postID int) (string, error) {
const query = `SELECT board.uri FROM DBPREFIXboards as board
JOIN (
SELECT threads.board_id FROM DBPREFIXthreads as threads
JOIN DBPREFIXposts as posts ON posts.thread_id = threads.id
WHERE posts.id = ?
) as threads ON threads.board_id = board.id`
var boardURI string
err := QueryRowSQL(query, interfaceSlice(postID), interfaceSlice(&boardURI))
if errors.Is(err, sql.ErrNoRows) {
err = ErrBoardDoesNotExist
}
return boardURI, err
}
func getBoardBase(where string, whereParameters []interface{}) (*Board, error) {
query := selectBoardsBaseSQL + where
board := new(Board)
err := QueryRowSQL(query, whereParameters, interfaceSlice(
&board.ID, &board.SectionID, &board.URI, &board.Dir, &board.NavbarPosition, &board.Title, &board.Subtitle,
&board.Description, &board.MaxFilesize, &board.MaxThreads, &board.DefaultStyle, &board.Locked,
&board.CreatedAt, &board.AnonymousName, &board.ForceAnonymous, &board.AutosageAfter, &board.NoImagesAfter,
&board.MaxMessageLength, &board.MinMessageLength, &board.AllowEmbeds, &board.RedirectToThread, &board.RequireFile,
&board.EnableCatalog,
))
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrBoardDoesNotExist
}
return board, err
}
// GetBoardFromID returns the board corresponding to a given id
func GetBoardFromID(id int) (*Board, error) {
return getBoardBase("WHERE DBPREFIXboards.id = ?", interfaceSlice(id))
}
// GetBoardFromDir returns the board corresponding to a given dir
func GetBoardFromDir(dir string) (*Board, error) {
return getBoardBase("WHERE DBPREFIXboards.dir = ?", interfaceSlice(dir))
}
// GetIDFromDir returns the id of the board with the given dir value
func GetBoardIDFromDir(dir string) (id int, err error) {
const query = `SELECT id FROM DBPREFIXboards WHERE dir = ?`
err = QueryRowSQL(query, interfaceSlice(dir), interfaceSlice(&id))
if errors.Is(err, sql.ErrNoRows) {
return 0, ErrBoardDoesNotExist
}
return id, err
}
// GetBoardURIs gets a list of all existing board URIs
func GetBoardURIs() (URIS []string, err error) {
const sql = `SELECT uri FROM DBPREFIXboards`
rows, err := QuerySQL(sql)
if err != nil {
return nil, err
}
defer rows.Close()
var uris []string
for rows.Next() {
var uri string
if err = rows.Scan(&uri); err != nil {
return nil, err
}
uris = append(uris, uri)
}
return uris, nil
}
// ResetBoardSectionArrays is run when the board list needs to be changed
// (board/section is added, deleted, etc)
func ResetBoardSectionArrays() error {
allBoardsArr, err := GetAllBoards(true)
if err != nil {
return err
}
AllBoards = nil
AllBoards = append(AllBoards, allBoardsArr...)
for _, board := range AllBoards {
if err = config.UpdateBoardConfig(board.Dir); err != nil {
return fmt.Errorf("unable to update board config for /%s/: %s", board.Dir, err.Error())
}
}
allSectionsArr, err := GetAllSections(true)
if err != nil {
return err
}
AllSections = nil
AllSections = append(AllSections, allSectionsArr...)
return nil
}
// NewBoardSimple creates a new board in the database given the directory, title, subtitle, and description.
// Generic values are used for the other columns to be optionally changed later
func NewBoardSimple(dir string, title string, subtitle string, description string, appendToAllBoards bool) (*Board, error) {
sectionID, err := getOrCreateDefaultSectionID()
if err != nil {
return nil, err
}
board := &Board{
SectionID: sectionID,
URI: dir,
Dir: dir,
NavbarPosition: 3,
Title: title,
Subtitle: subtitle,
Description: description,
MaxFilesize: 15000,
MaxThreads: 300,
DefaultStyle: config.GetBoardConfig("").DefaultStyle,
Locked: false,
AnonymousName: "Anonymous",
ForceAnonymous: false,
AutosageAfter: 500,
NoImagesAfter: -1,
MaxMessageLength: 1500,
MinMessageLength: 0,
AllowEmbeds: false,
RedirectToThread: false,
RequireFile: false,
EnableCatalog: true,
}
// board.ShowID = false
// board.EnableSpoileredImages = true
// board.Worksafe = true
// board.ThreadsPerPage = 20
// board.Cooldowns = BoardCooldowns{
// NewThread: 30,
// Reply: 7,
// ImageReply: 7,
// }
return board, CreateBoard(board, appendToAllBoards)
}
// CreateBoard inserts a new board into the database, using the fields from the given Board pointer.
// It sets board.ID and board.CreatedAt if it is successfull
func CreateBoard(board *Board, appendToAllBoards bool) error {
const sqlINSERT = `INSERT INTO DBPREFIXboards
(section_id, uri, dir, navbar_position, title, subtitle,
description, max_file_size, max_threads, default_style, locked,
anonymous_name, force_anonymous, autosage_after, no_images_after, max_message_length,
min_message_length, allow_embeds, redirect_to_thread, require_file, enable_catalog)
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`
if board == nil {
return ErrNilBoard
}
if DoesBoardExistByDir(board.Dir) {
return ErrBoardExists
}
if board.Dir == "" {
return errors.New("board dir string must not be empty")
}
if board.URI == "" {
board.URI = board.Dir
}
if board.Title == "" {
return errors.New("board title string must not be empty")
}
_, err := ExecSQL(sqlINSERT,
&board.SectionID, &board.URI, &board.Dir, &board.NavbarPosition, &board.Title, &board.Subtitle,
&board.Description, &board.MaxFilesize, &board.MaxThreads, &board.DefaultStyle, &board.Locked,
&board.AnonymousName, &board.ForceAnonymous, &board.AutosageAfter, &board.NoImagesAfter, &board.MaxMessageLength,
&board.MinMessageLength, &board.AllowEmbeds, &board.RedirectToThread, &board.RequireFile, &board.EnableCatalog)
if err != nil {
return err
}
if err = QueryRowSQL(
`SELECT id FROM DBPREFIXboards WHERE dir = ?`,
interfaceSlice(board.Dir), interfaceSlice(&board.ID),
); err != nil {
return err
}
board.CreatedAt = time.Now()
if appendToAllBoards {
AllBoards = append(AllBoards, *board)
}
return nil
}
// createDefaultBoardIfNoneExist creates a default board if no boards exist yet
func createDefaultBoardIfNoneExist() error {
const query = `SELECT COUNT(id) FROM DBPREFIXboards`
var count int
QueryRowSQL(query, interfaceSlice(), interfaceSlice(&count))
if count > 0 {
return nil
}
// create a default generic /test/ board
_, err := NewBoardSimple("test", "Testing Board", "Board for testing stuff", "Board for testing stuff", true)
return err
}
func getBoardIDFromURI(uri string) (int, error) {
const sql = `SELECT id FROM DBPREFIXboards WHERE uri = ?`
var id int
err := QueryRowSQL(sql, interfaceSlice(uri), interfaceSlice(&id))
return id, err
}
func (board *Board) Delete() error {
const query = `DELETE FROM DBPREFIXboards WHERE id = ?`
_, err := ExecSQL(query, board.ID)
if err != nil {
return err
}
config.DeleteBoardConfig(board.Dir)
return nil
}
// DeleteOldThreads deletes old threads that exceed the limit set by board.MaxThreads and returns the posts in those
// threads
func (board *Board) DeleteOldThreads() ([]int, error) {
if board.MaxThreads < 1 {
return nil, nil
}
tx, err := BeginTx()
if err != nil {
return nil, err
}
defer tx.Rollback()
rows, err := QueryTxSQL(tx, `SELECT id FROM DBPREFIXthreads WHERE board_id = ? AND is_deleted = FALSE AND stickied = FALSE ORDER BY last_bump DESC`,
board.ID)
if err != nil {
return nil, err
}
defer rows.Close()
var threadIDs []interface{}
var id int
var threadsProccessed int
for rows.Next() {
threadsProccessed++
if threadsProccessed <= board.MaxThreads {
continue
}
if err = rows.Scan(&id); err != nil {
return nil, err
}
threadIDs = append(threadIDs, id)
}
if threadIDs == nil {
// no threads to trim
return nil, nil
}
idSetStr := createArrayPlaceholder(threadIDs)
if _, err = ExecTxSQL(tx, `UPDATE DBPREFIXthreads SET is_deleted = TRUE WHERE id in `+idSetStr,
threadIDs...); err != nil {
return nil, err
}
if rows, err = QueryTxSQL(tx, `SELECT id FROM DBPREFIXposts WHERE thread_id in `+idSetStr,
threadIDs...); err != nil {
return nil, err
}
defer rows.Close()
var postIDs []int
for rows.Next() {
if err = rows.Scan(&id); err != nil {
return nil, err
}
postIDs = append(postIDs, id)
}
if _, err = ExecTxSQL(tx, `UPDATE DBPREFIXposts SET is_deleted = TRUE WHERE thread_id in `+idSetStr,
threadIDs...); err != nil {
return nil, err
}
return postIDs, tx.Commit()
}
func (board *Board) GetThreads(onlyNotDeleted bool, orderLastByBump bool, stickiedFirst bool) ([]Thread, error) {
query := selectThreadsBaseSQL + " WHERE board_id = ?"
if onlyNotDeleted {
query += " AND is_deleted = FALSE"
}
if orderLastByBump || stickiedFirst {
query += " ORDER BY "
}
if stickiedFirst {
query += "stickied DESC"
if orderLastByBump {
query += ", "
}
}
if orderLastByBump {
query += " last_bump DESC"
}
rows, err := QuerySQL(query, board.ID)
if err != nil {
return nil, err
}
defer rows.Close()
var threads []Thread
for rows.Next() {
var thread Thread
err = rows.Scan(
&thread.ID, &thread.BoardID, &thread.Locked, &thread.Stickied, &thread.Anchored,
&thread.Cyclical, &thread.LastBump, &thread.DeletedAt, &thread.IsDeleted,
)
if err != nil {
return threads, err
}
threads = append(threads, thread)
}
return threads, nil
}
// IsHidden returns true if the board is in a section that is hidden, otherwise false. If it is in a section
// that is not in the AllSections array, it returns defValueIfMissingSection
func (board *Board) IsHidden(defValueIfMissingSection bool) bool {
for s := range AllSections {
if AllSections[s].ID == board.SectionID {
return AllSections[s].Hidden
}
}
return defValueIfMissingSection // board is not in a valid section (or AllSections needs to be reset)
}
// ModifyInDB updates the board dataa in the database with new values
func (board *Board) ModifyInDB() error {
const query = `UPDATE DBPREFIXboards SET
section_id = ?,
navbar_position = ?,
title = ?,
subtitle = ?,
description = ?,
max_file_size = ?,
max_threads = ?,
default_style = ?,
locked = ?,
anonymous_name = ?,
force_anonymous = ?,
autosage_after = ?,
no_images_after = ?,
max_message_length = ?,
min_message_length = ?,
allow_embeds = ?,
redirect_to_thread = ?,
require_file = ?,
enable_catalog = ?
WHERE id = ?`
_, err := ExecSQL(query,
board.SectionID, board.NavbarPosition, board.Title, board.Subtitle, board.Description,
board.MaxFilesize, board.MaxThreads, board.DefaultStyle, board.Locked, board.AnonymousName,
board.ForceAnonymous, board.AutosageAfter, board.NoImagesAfter, board.MaxMessageLength,
board.MinMessageLength, board.AllowEmbeds, board.RedirectToThread, board.RequireFile, board.EnableCatalog,
board.ID)
if err != nil {
return err
}
return ResetBoardSectionArrays()
}
// AbsolutePath returns the full filepath of the board directory
func (board *Board) AbsolutePath(subpath ...string) string {
return path.Join(config.GetSystemCriticalConfig().DocumentRoot, board.Dir, path.Join(subpath...))
}
// WebPath returns a string that represents the file's path as accessible by a browser
// fileType should be "boardPage", "threadPage", "upload", or "thumb"
func (board *Board) WebPath(fileName, fileType string) string {
var filePath string
systemCritical := config.GetSystemCriticalConfig()
switch fileType {
case "":
fallthrough
case "boardPage":
filePath = path.Join(systemCritical.WebRoot, board.Dir, fileName)
case "threadPage":
filePath = path.Join(systemCritical.WebRoot, board.Dir, "res", fileName)
case "upload":
filePath = path.Join(systemCritical.WebRoot, board.Dir, "src", fileName)
case "thumb":
fallthrough
case "thumbnail":
filePath = path.Join(systemCritical.WebRoot, board.Dir, "thumb", fileName)
}
return filePath
}