220 lines
5.8 KiB
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
|
||
|
}
|