From 2cf24c4724ef3c27f640a8cfedcdcb1421cddad9 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Tue, 23 Jul 2024 16:12:19 +0800 Subject: [PATCH] :sparkles: Recommendation API --- pkg/internal/models/accounts.go | 3 + pkg/internal/server/api/index.go | 6 ++ pkg/internal/server/api/posts_api.go | 4 +- pkg/internal/server/api/recommendation_api.go | 75 ++++++++++++++++ pkg/internal/server/api/replies_api.go | 2 +- pkg/internal/services/accounts.go | 11 +++ pkg/internal/services/posts.go | 86 ++++++++++--------- 7 files changed, 143 insertions(+), 44 deletions(-) create mode 100644 pkg/internal/server/api/recommendation_api.go diff --git a/pkg/internal/models/accounts.go b/pkg/internal/models/accounts.go index 7f77419..be54d5a 100644 --- a/pkg/internal/models/accounts.go +++ b/pkg/internal/models/accounts.go @@ -16,4 +16,7 @@ type Account struct { Posts []Post `json:"posts" gorm:"foreignKey:AuthorID"` Reactions []Reaction `json:"reactions"` ExternalID uint `json:"external_id"` + + TotalUpvote int `json:"total_upvote"` + TotalDownvote int `json:"total_downvote"` } diff --git a/pkg/internal/server/api/index.go b/pkg/internal/server/api/index.go index 5ff97c7..f034bc4 100644 --- a/pkg/internal/server/api/index.go +++ b/pkg/internal/server/api/index.go @@ -10,6 +10,12 @@ func MapAPIs(app *fiber.App, baseURL string) { api.Get("/users/me", getUserinfo) api.Get("/users/:accountId", getOthersInfo) + recommendations := api.Group("/recommendations").Name("Recommendations API") + { + recommendations.Get("/", listRecommendationDefault) + recommendations.Get("/shuffle", listRecommendationShuffle) + } + stories := api.Group("/stories").Name("Story API") { stories.Post("/", createStory) diff --git a/pkg/internal/server/api/posts_api.go b/pkg/internal/server/api/posts_api.go index 6ceaac6..d0d107c 100644 --- a/pkg/internal/server/api/posts_api.go +++ b/pkg/internal/server/api/posts_api.go @@ -66,7 +66,7 @@ func listPost(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusInternalServerError, err.Error()) } - items, err := services.ListPost(tx, take, offset) + items, err := services.ListPost(tx, take, offset, "published_at DESC") if err != nil { return fiber.NewError(fiber.StatusBadRequest, err.Error()) } @@ -93,7 +93,7 @@ func listDraftPost(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusInternalServerError, err.Error()) } - items, err := services.ListPost(tx, take, offset, true) + items, err := services.ListPost(tx, take, offset, "created_at DESC", true) if err != nil { return fiber.NewError(fiber.StatusBadRequest, err.Error()) } diff --git a/pkg/internal/server/api/recommendation_api.go b/pkg/internal/server/api/recommendation_api.go new file mode 100644 index 0000000..cc3513b --- /dev/null +++ b/pkg/internal/server/api/recommendation_api.go @@ -0,0 +1,75 @@ +package api + +import ( + "fmt" + "git.solsynth.dev/hydrogen/interactive/pkg/internal/database" + "git.solsynth.dev/hydrogen/interactive/pkg/internal/services" + "github.com/gofiber/fiber/v2" + "gorm.io/gorm" +) + +func listRecommendationDefault(c *fiber.Ctx) error { + take := c.QueryInt("take", 0) + offset := c.QueryInt("offset", 0) + realmId := c.QueryInt("realmId", 0) + maxDownVote := c.QueryInt("maxDownVote", 3) + + tx := database.C.Joins("Author").Where("accounts.total_downvote <= ?", maxDownVote) + tx = services.FilterPostDraft(tx) + if realmId > 0 { + if realm, err := services.GetRealmWithExtID(uint(realmId)); err != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("realm was not found: %v", err)) + } else { + tx = services.FilterPostWithRealm(tx, realm.ID) + } + } + + countTx := tx + count, err := services.CountPost(countTx) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, err.Error()) + } + + items, err := services.ListPost(tx, take, offset, "published_at DESC") + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } + + return c.JSON(fiber.Map{ + "count": count, + "data": items, + }) +} + +func listRecommendationShuffle(c *fiber.Ctx) error { + take := c.QueryInt("take", 0) + offset := c.QueryInt("offset", 0) + realmId := c.QueryInt("realmId", 0) + maxDownVote := c.QueryInt("maxDownVote", 3) + + tx := database.C.Joins("Author").Where("accounts.total_downvote <= ?", maxDownVote) + tx = services.FilterPostDraft(tx) + if realmId > 0 { + if realm, err := services.GetRealmWithExtID(uint(realmId)); err != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("realm was not found: %v", err)) + } else { + tx = services.FilterPostWithRealm(tx, realm.ID) + } + } + + countTx := tx + count, err := services.CountPost(countTx) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, err.Error()) + } + + items, err := services.ListPost(tx, take, offset, gorm.Expr("RAND()")) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } + + return c.JSON(fiber.Map{ + "count": count, + "data": items, + }) +} diff --git a/pkg/internal/server/api/replies_api.go b/pkg/internal/server/api/replies_api.go index 6a6437a..0d3ef94 100644 --- a/pkg/internal/server/api/replies_api.go +++ b/pkg/internal/server/api/replies_api.go @@ -40,7 +40,7 @@ func listPostReplies(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusInternalServerError, err.Error()) } - items, err := services.ListPost(tx, take, offset) + items, err := services.ListPost(tx, take, offset, "published_at DESC") if err != nil { return fiber.NewError(fiber.StatusBadRequest, err.Error()) } diff --git a/pkg/internal/services/accounts.go b/pkg/internal/services/accounts.go index 12dd5a0..4a22a32 100644 --- a/pkg/internal/services/accounts.go +++ b/pkg/internal/services/accounts.go @@ -4,12 +4,23 @@ import ( "context" "git.solsynth.dev/hydrogen/dealer/pkg/hyper" "git.solsynth.dev/hydrogen/dealer/pkg/proto" + "git.solsynth.dev/hydrogen/interactive/pkg/internal/database" "git.solsynth.dev/hydrogen/interactive/pkg/internal/gap" "git.solsynth.dev/hydrogen/interactive/pkg/internal/models" "github.com/rs/zerolog/log" "time" ) +func ModifyPosterVoteCount(user models.Account, isUpvote bool, delta int) error { + if isUpvote { + user.TotalUpvote += delta + } else { + user.TotalDownvote += delta + } + + return database.C.Save(&user).Error +} + func NotifyPosterAccount(user models.Account, title, body string, subtitle *string) error { ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) defer cancel() diff --git a/pkg/internal/services/posts.go b/pkg/internal/services/posts.go index ca9a52c..6618b66 100644 --- a/pkg/internal/services/posts.go +++ b/pkg/internal/services/posts.go @@ -57,14 +57,8 @@ func FilterPostDraft(tx *gorm.DB) *gorm.DB { return tx.Where("is_draft = ? OR is_draft IS NULL", false) } -func GetPost(tx *gorm.DB, id uint, ignoreLimitation ...bool) (models.Post, error) { - if len(ignoreLimitation) == 0 || !ignoreLimitation[0] { - tx = FilterPostWithPublishedAt(tx, time.Now()) - } - - var item models.Post - if err := tx. - Where("id = ?", id). +func PreloadGeneral(tx *gorm.DB) *gorm.DB { + return tx. Preload("Tags"). Preload("Categories"). Preload("Realm"). @@ -76,7 +70,17 @@ func GetPost(tx *gorm.DB, id uint, ignoreLimitation ...bool) (models.Post, error Preload("RepostTo"). Preload("RepostTo.Author"). Preload("RepostTo.Tags"). - Preload("RepostTo.Categories"). + Preload("RepostTo.Categories") +} + +func GetPost(tx *gorm.DB, id uint, ignoreLimitation ...bool) (models.Post, error) { + if len(ignoreLimitation) == 0 || !ignoreLimitation[0] { + tx = FilterPostWithPublishedAt(tx, time.Now()) + } + + var item models.Post + if err := PreloadGeneral(tx). + Where("id = ?", id). First(&item).Error; err != nil { return item, err } @@ -115,27 +119,15 @@ func CountPostReactions(id uint) int64 { return count } -func ListPost(tx *gorm.DB, take int, offset int, noReact ...bool) ([]*models.Post, error) { +func ListPost(tx *gorm.DB, take int, offset int, order any, noReact ...bool) ([]*models.Post, error) { if take > 100 { take = 100 } var items []*models.Post - if err := tx. + if err := PreloadGeneral(tx). Limit(take).Offset(offset). - Order("published_at DESC"). - Preload("Tags"). - Preload("Categories"). - Preload("Realm"). - Preload("Author"). - Preload("ReplyTo"). - Preload("ReplyTo.Author"). - Preload("ReplyTo.Tags"). - Preload("ReplyTo.Categories"). - Preload("RepostTo"). - Preload("RepostTo.Author"). - Preload("RepostTo.Tags"). - Preload("RepostTo.Categories"). + Order(order). Find(&items).Error; err != nil { return items, err } @@ -278,31 +270,43 @@ func DeletePost(item models.Post) error { } func ReactPost(user models.Account, reaction models.Reaction) (bool, models.Reaction, error) { + var op models.Post + if err := database.C. + Where("id = ?", reaction.PostID). + Preload("Author"). + First(&op).Error; err != nil { + return true, reaction, err + } + if err := database.C.Where(reaction).First(&reaction).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - var op models.Post - if err := database.C. - Where("id = ?", reaction.PostID). - Preload("Author"). - First(&op).Error; err == nil { - if op.Author.ID != user.ID { - err = NotifyPosterAccount( - op.Author, - "Post got reacted", - fmt.Sprintf("%s (%s) reacted your post a %s.", user.Nick, user.Name, reaction.Symbol), - lo.ToPtr(fmt.Sprintf("%s reacted you", user.Nick)), - ) - if err != nil { - log.Error().Err(err).Msg("An error occurred when notifying user...") - } + if op.Author.ID != user.ID { + err = NotifyPosterAccount( + op.Author, + "Post got reacted", + fmt.Sprintf("%s (%s) reacted your post a %s.", user.Nick, user.Name, reaction.Symbol), + lo.ToPtr(fmt.Sprintf("%s reacted you", user.Nick)), + ) + if err != nil { + log.Error().Err(err).Msg("An error occurred when notifying user...") } } - return true, reaction, database.C.Save(&reaction).Error + err = database.C.Save(&reaction).Error + if err == nil { + _ = ModifyPosterVoteCount(op.Author, reaction.Attitude == models.AttitudePositive, 1) + } + + return true, reaction, err } else { return true, reaction, err } } else { - return false, reaction, database.C.Delete(&reaction).Error + err = database.C.Delete(&reaction).Error + if err == nil { + _ = ModifyPosterVoteCount(op.Author, reaction.Attitude == models.AttitudePositive, -1) + } + + return false, reaction, err } }