From 450c1e445097ba419e80694bfbfaaaf163d61bd5 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Wed, 6 Mar 2024 22:35:10 +0800 Subject: [PATCH] :sparkles: Comments API --- .air.toml | 46 +++++++++++++++ .gitignore | 5 +- pkg/server/comments_api.go | 66 +++++++++++++++++---- pkg/server/feed_api.go | 3 +- pkg/server/posts_api.go | 3 +- pkg/server/startup.go | 3 +- pkg/services/comments.go | 82 +++++++++++++++++++++++++++ pkg/services/posts.go | 12 +++- pkg/views/src/views/posts/details.vue | 12 ++-- 9 files changed, 207 insertions(+), 25 deletions(-) create mode 100644 .air.toml create mode 100644 pkg/services/comments.go diff --git a/.air.toml b/.air.toml new file mode 100644 index 0000000..c3d0898 --- /dev/null +++ b/.air.toml @@ -0,0 +1,46 @@ +root = "." +testdata_dir = "testdata" +tmp_dir = "dist" + +[build] + args_bin = [] + bin = "./dist/server" + cmd = "go build -o ./dist/server ./pkg/cmd/main.go" + delay = 1000 + exclude_dir = ["assets", "tmp", "vendor", "testdata", "pkg/views"] + exclude_file = [] + exclude_regex = ["_test.go"] + exclude_unchanged = false + follow_symlink = false + full_bin = "" + include_dir = [] + include_ext = ["go", "tpl", "tmpl", "html"] + include_file = [] + kill_delay = "0s" + log = "build-errors.log" + poll = false + poll_interval = 0 + post_cmd = [] + pre_cmd = [] + rerun = false + rerun_delay = 500 + send_interrupt = false + stop_on_error = false + +[color] + app = "" + build = "yellow" + main = "magenta" + runner = "green" + watcher = "cyan" + +[log] + main_only = false + time = false + +[misc] + clean_on_exit = false + +[screen] + clear_on_rebuild = false + keep_scroll = true diff --git a/.gitignore b/.gitignore index 2333d60..b9bcb4e 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ -/uploads \ No newline at end of file +/uploads +/dist + +.DS_Store diff --git a/pkg/server/comments_api.go b/pkg/server/comments_api.go index 869c35a..ef32891 100644 --- a/pkg/server/comments_api.go +++ b/pkg/server/comments_api.go @@ -22,6 +22,37 @@ func contextComment() *services.PostTypeContext { } } +func listComment(c *fiber.Ctx) error { + take := c.QueryInt("take", 0) + offset := c.QueryInt("offset", 0) + noReact := c.QueryBool("noReact", false) + + alias := c.Params("postId") + + mx := c.Locals(postContextKey).(*services.PostTypeContext). + FilterPublishedAt(time.Now()) + + item, err := mx.GetViaAlias(alias) + if err != nil { + return fiber.NewError(fiber.StatusNotFound, err.Error()) + } + + data, err := mx.ListComment(item.ID, take, offset, noReact) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, err.Error()) + } + + count, err := mx.CountComment(item.ID) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, err.Error()) + } + + return c.JSON(fiber.Map{ + "count": count, + "data": data, + }) +} + func createComment(c *fiber.Ctx) error { user := c.Locals("principal").(models.Account) @@ -32,8 +63,6 @@ func createComment(c *fiber.Ctx) error { Categories []models.Category `json:"categories"` Attachments []models.Attachment `json:"attachments"` PublishedAt *time.Time `json:"published_at"` - ArticleID *uint `json:"article_id"` - MomentID *uint `json:"moment_id"` ReplyTo uint `json:"reply_to"` } @@ -55,13 +84,29 @@ func createComment(c *fiber.Ctx) error { Content: data.Content, } - if data.ArticleID == nil && data.MomentID == nil { + postType := c.Params("postType") + alias := c.Params("postId") + + var err error + var res models.Feed + + switch postType { + case "moments": + err = database.C.Model(&models.Moment{}).Where("alias = ?", alias).Select("id").First(&res).Error + case "articles": + err = database.C.Model(&models.Article{}).Where("alias = ?", alias).Select("id").First(&res).Error + default: return fiber.NewError(fiber.StatusBadRequest, "comment must belongs to a resource") } - if data.ArticleID != nil { - var article models.Article - if err := database.C.Where("id = ?", data.ArticleID).First(&article).Error; err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("belongs to resource was not found: %v", err)) + + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("belongs to resource was not found: %v", err)) + } else { + switch postType { + case "moments": + item.MomentID = &res.ID + case "articles": + item.ArticleID = &res.ID } } @@ -77,12 +122,11 @@ func createComment(c *fiber.Ctx) error { } } - item, err := services.NewPost(item) - if err != nil { + if item, err := services.NewPost(item); err != nil { return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } else { + return c.JSON(item) } - - return c.JSON(item) } func editComment(c *fiber.Ctx) error { diff --git a/pkg/server/feed_api.go b/pkg/server/feed_api.go index beee2cf..7f71b18 100644 --- a/pkg/server/feed_api.go +++ b/pkg/server/feed_api.go @@ -1,9 +1,10 @@ package server import ( + "fmt" + "code.smartsheep.studio/hydrogen/interactive/pkg/database" "code.smartsheep.studio/hydrogen/interactive/pkg/models" - "fmt" "github.com/gofiber/fiber/v2" "github.com/samber/lo" "github.com/spf13/viper" diff --git a/pkg/server/posts_api.go b/pkg/server/posts_api.go index 26224ed..d0208e0 100644 --- a/pkg/server/posts_api.go +++ b/pkg/server/posts_api.go @@ -31,7 +31,8 @@ func useDynamicContext(c *fiber.Ctx) error { func getPost(c *fiber.Ctx) error { alias := c.Params("postId") - mx := c.Locals(postContextKey).(*services.PostTypeContext) + mx := c.Locals(postContextKey).(*services.PostTypeContext). + FilterPublishedAt(time.Now()) item, err := mx.GetViaAlias(alias) if err != nil { diff --git a/pkg/server/startup.go b/pkg/server/startup.go index aa8b829..97d5767 100644 --- a/pkg/server/startup.go +++ b/pkg/server/startup.go @@ -76,6 +76,8 @@ func NewServer() { posts.Get("/", listPost) posts.Get("/:postId", getPost) posts.Post("/:postId/react", authMiddleware, reactPost) + posts.Get("/:postId/comments", listComment) + posts.Post("/:postId/comments", authMiddleware, createComment) } moments := api.Group("/moments").Name("Moments API") @@ -94,7 +96,6 @@ func NewServer() { comments := api.Group("/p/comments").Name("Comments API") { - comments.Post("/", authMiddleware, createComment) comments.Put("/:commentId", authMiddleware, editComment) comments.Delete("/:commentId", authMiddleware, deleteComment) } diff --git a/pkg/services/comments.go b/pkg/services/comments.go new file mode 100644 index 0000000..e1cb4f1 --- /dev/null +++ b/pkg/services/comments.go @@ -0,0 +1,82 @@ +package services + +import ( + "fmt" + "time" + + "code.smartsheep.studio/hydrogen/interactive/pkg/database" + "code.smartsheep.studio/hydrogen/interactive/pkg/models" + "github.com/samber/lo" + "github.com/spf13/viper" +) + +func (v *PostTypeContext) ListComment(id uint, take int, offset int, noReact ...bool) ([]*models.Feed, error) { + if take > 20 { + take = 20 + } + + var items []*models.Feed + table := viper.GetString("database.prefix") + "comments" + userTable := viper.GetString("database.prefix") + "accounts" + if err := v.Tx. + Table(table). + Select("*, ? as model_type", "comments"). + Where(v.ColumnName+"_id = ?", id). + Joins(fmt.Sprintf("INNER JOIN %s as author ON author_id = author.id", userTable)). + Limit(take).Offset(offset).Find(&items).Error; err != nil { + return items, err + } + + idx := lo.Map(items, func(item *models.Feed, index int) uint { + return item.ID + }) + + if len(noReact) <= 0 || !noReact[0] { + var reactions []struct { + PostID uint + Symbol string + Count int64 + } + + if err := database.C.Model(&models.Reaction{}). + Select("comment_id as post_id, symbol, COUNT(id) as count"). + Where("comment_id IN (?)", idx). + Group("post_id, symbol"). + Scan(&reactions).Error; err != nil { + return items, err + } + + itemMap := lo.SliceToMap(items, func(item *models.Feed) (uint, *models.Feed) { + return item.ID, item + }) + + list := map[uint]map[string]int64{} + for _, info := range reactions { + if _, ok := list[info.PostID]; !ok { + list[info.PostID] = make(map[string]int64) + } + list[info.PostID][info.Symbol] = info.Count + } + + for k, v := range list { + if post, ok := itemMap[k]; ok { + post.ReactionList = v + } + } + } + + return items, nil +} + +func (v *PostTypeContext) CountComment(id uint) (int64, error) { + var count int64 + if err := database.C. + Model(&models.Comment{}). + Where(v.ColumnName+"_id = ?", id). + Where("published_at <= ?", time.Now()). + Count(&count).Error; err != nil { + return count, err + } + + return count, nil +} diff --git a/pkg/services/posts.go b/pkg/services/posts.go index 3e5d6d2..068c462 100644 --- a/pkg/services/posts.go +++ b/pkg/services/posts.go @@ -72,9 +72,13 @@ func (v *PostTypeContext) SortCreatedAt(order string) *PostTypeContext { return v } -func (v *PostTypeContext) GetViaAlias(alias string, noComments ...bool) (models.Feed, error) { +func (v *PostTypeContext) GetViaAlias(alias string) (models.Feed, error) { var item models.Feed - if err := v.Tx.Where("alias = ?", alias).First(&item).Error; err != nil { + table := viper.GetString("database.prefix") + v.TableName + if err := v.Tx. + Table(table). + Where("alias = ?", alias). + First(&item).Error; err != nil { return item, err } @@ -83,7 +87,9 @@ func (v *PostTypeContext) GetViaAlias(alias string, noComments ...bool) (models. func (v *PostTypeContext) Get(id uint, noComments ...bool) (models.Feed, error) { var item models.Feed + table := viper.GetString("database.prefix") + v.TableName if err := v.Tx. + Table(table). Select("*, ? as model_type", v.ColumnName). Where("id = ?", id).First(&item).Error; err != nil { return item, err @@ -131,7 +137,9 @@ func (v *PostTypeContext) List(take int, offset int, noReact ...bool) ([]*models } var items []*models.Feed + table := viper.GetString("database.prefix") + v.TableName if err := v.Tx. + Table(table). Select("*, ? as model_type", v.ColumnName). Limit(take).Offset(offset).Find(&items).Error; err != nil { return items, err diff --git a/pkg/views/src/views/posts/details.vue b/pkg/views/src/views/posts/details.vue index 0650443..247142d 100644 --- a/pkg/views/src/views/posts/details.vue +++ b/pkg/views/src/views/posts/details.vue @@ -24,12 +24,8 @@
- +
@@ -51,7 +47,7 @@ const route = useRoute(); async function readPost() { loading.value = true; - const res = await request(`/api/${route.params.postType}/${route.params.alias}?`); + const res = await request(`/api/p/${route.params.postType}/${route.params.alias}?`); if (res.status !== 200) { error.value = await res.text(); } else { @@ -70,4 +66,4 @@ function updateReactions(symbol: string, num: number) { post.value.reaction_list[symbol] = num; } } - \ No newline at end of file +