hmn/src/website/threads_and_posts_helper.go

220 lines
5.8 KiB
Go

package website
import (
"context"
"errors"
"math"
"git.handmade.network/hmn/hmn/src/db"
"git.handmade.network/hmn/hmn/src/models"
"git.handmade.network/hmn/hmn/src/oops"
)
type postAndRelatedModels struct {
Thread models.Thread
Post models.Post
CurrentVersion models.PostVersion
Author *models.User
Editor *models.User
ReplyPost *models.Post
ReplyAuthor *models.User
}
/*
Fetches the thread defined by your (already parsed) path params.
YOU MUST VERIFY THAT THE THREAD ID IS VALID BEFORE CALLING THIS FUNCTION. It will
not check, for example, that the thread belongs to the correct subforum.
*/
func FetchThread(ctx context.Context, connOrTx db.ConnOrTx, threadId int) models.Thread {
type threadQueryResult struct {
Thread models.Thread `db:"thread"`
}
irow, err := db.QueryOne(ctx, connOrTx, threadQueryResult{},
`
SELECT $columns
FROM
handmade_thread AS thread
WHERE
id = $1
AND NOT deleted
`,
threadId,
)
if err != nil {
// We shouldn't encounter db.ErrNoMatchingRows, because validation should have verified that everything exists.
panic(oops.New(err, "failed to fetch thread"))
}
thread := irow.(*threadQueryResult).Thread
return thread
}
/*
Fetches the post, the thread, and author / editor information for the post defined in
your path params.
YOU MUST VERIFY THAT THE THREAD ID AND POST ID ARE VALID BEFORE CALLING THIS FUNCTION.
It will not check that the post belongs to the correct subforum, for example, or the
correct project blog. This logic varies per route and per use of threads, so it doesn't
happen here.
*/
func FetchPostAndStuff(
ctx context.Context,
connOrTx db.ConnOrTx,
threadId, postId int,
) postAndRelatedModels {
type resultRow struct {
Thread models.Thread `db:"thread"`
Post models.Post `db:"post"`
CurrentVersion models.PostVersion `db:"ver"`
Author *models.User `db:"author"`
Editor *models.User `db:"editor"`
ReplyPost *models.Post `db:"reply"`
ReplyAuthor *models.User `db:"reply_author"`
}
postQueryResult, err := db.QueryOne(ctx, connOrTx, resultRow{},
`
SELECT $columns
FROM
handmade_thread AS thread
JOIN handmade_post AS post ON post.thread_id = thread.id
JOIN handmade_postversion AS ver ON post.current_id = ver.id
LEFT JOIN auth_user AS author ON post.author_id = author.id
LEFT JOIN auth_user AS editor ON ver.editor_id = editor.id
LEFT JOIN handmade_post AS reply ON post.reply_id = reply.id
LEFT JOIN auth_user AS reply_author ON reply.author_id = reply_author.id
WHERE
post.thread_id = $1
AND post.id = $2
AND NOT post.deleted
`,
threadId,
postId,
)
if err != nil {
// We shouldn't encounter db.ErrNoMatchingRows, because validation should have verified that everything exists.
panic(oops.New(err, "failed to fetch post and related data"))
}
result := postQueryResult.(*resultRow)
return postAndRelatedModels{
Thread: result.Thread,
Post: result.Post,
CurrentVersion: result.CurrentVersion,
Author: result.Author,
Editor: result.Editor,
ReplyPost: result.ReplyPost,
ReplyAuthor: result.ReplyAuthor,
}
}
/*
Fetches all the posts (and related models) for a given thread.
YOU MUST VERIFY THAT THE THREAD ID IS VALID BEFORE CALLING THIS FUNCTION. It will
not check, for example, that the thread belongs to the correct subforum.
*/
func FetchThreadPostsAndStuff(
ctx context.Context,
connOrTx db.ConnOrTx,
threadId int,
page, postsPerPage int,
) (models.Thread, []postAndRelatedModels) {
limit := postsPerPage
offset := (page - 1) * postsPerPage
if postsPerPage == 0 {
limit = math.MaxInt32
offset = 0
}
thread := FetchThread(ctx, connOrTx, threadId)
type postResult struct {
Post models.Post `db:"post"`
CurrentVersion models.PostVersion `db:"ver"`
Author *models.User `db:"author"`
Editor *models.User `db:"editor"`
ReplyPost *models.Post `db:"reply"`
ReplyAuthor *models.User `db:"reply_author"`
}
itPosts, err := db.Query(ctx, connOrTx, postResult{},
`
SELECT $columns
FROM
handmade_post AS post
JOIN handmade_postversion AS ver ON post.current_id = ver.id
LEFT JOIN auth_user AS author ON post.author_id = author.id
LEFT JOIN auth_user AS editor ON ver.editor_id = editor.id
LEFT JOIN handmade_post AS reply ON post.reply_id = reply.id
LEFT JOIN auth_user AS reply_author ON reply.author_id = reply_author.id
WHERE
post.thread_id = $1
AND NOT post.deleted
ORDER BY post.postdate
LIMIT $2 OFFSET $3
`,
thread.ID,
limit,
offset,
)
if err != nil {
panic(oops.New(err, "failed to fetch posts for thread"))
}
defer itPosts.Close()
var posts []postAndRelatedModels
for {
irow, hasNext := itPosts.Next()
if !hasNext {
break
}
row := irow.(*postResult)
posts = append(posts, postAndRelatedModels{
Thread: thread,
Post: row.Post,
CurrentVersion: row.CurrentVersion,
Author: row.Author,
Editor: row.Editor,
ReplyPost: row.ReplyPost,
ReplyAuthor: row.ReplyAuthor,
})
}
return thread, posts
}
func (cd *commonForumData) UserCanEditPost(ctx context.Context, connOrTx db.ConnOrTx, user models.User) bool {
if user.IsStaff {
return true
}
type postResult struct {
AuthorID *int `db:"post.author_id"`
}
iresult, err := db.QueryOne(ctx, connOrTx, postResult{},
`
SELECT $columns
FROM
handmade_post AS post
WHERE
post.id = $1
AND NOT post.deleted
`,
cd.PostID,
)
if err != nil {
if errors.Is(err, db.ErrNoMatchingRows) {
return false
} else {
panic(oops.New(err, "failed to get author of post when checking permissions"))
}
}
result := iresult.(*postResult)
return result.AuthorID != nil && *result.AuthorID == user.ID
}