✨ Recommendation API
This commit is contained in:
		| @@ -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"` | ||||
| } | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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()) | ||||
| 	} | ||||
|   | ||||
							
								
								
									
										75
									
								
								pkg/internal/server/api/recommendation_api.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								pkg/internal/server/api/recommendation_api.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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, | ||||
| 	}) | ||||
| } | ||||
| @@ -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()) | ||||
| 	} | ||||
|   | ||||
| @@ -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() | ||||
|   | ||||
| @@ -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,13 +270,16 @@ func DeletePost(item models.Post) error { | ||||
| } | ||||
|  | ||||
| func ReactPost(user models.Account, reaction models.Reaction) (bool, models.Reaction, error) { | ||||
| 	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 { | ||||
| 		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) { | ||||
| 			if op.Author.ID != user.ID { | ||||
| 				err = NotifyPosterAccount( | ||||
| 					op.Author, | ||||
| @@ -296,13 +291,22 @@ func ReactPost(user models.Account, reaction models.Reaction) (bool, models.Reac | ||||
| 					log.Error().Err(err).Msg("An error occurred when notifying user...") | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			err = database.C.Save(&reaction).Error | ||||
| 			if err == nil { | ||||
| 				_ = ModifyPosterVoteCount(op.Author, reaction.Attitude == models.AttitudePositive, 1) | ||||
| 			} | ||||
|  | ||||
| 			return true, reaction, database.C.Save(&reaction).Error | ||||
| 			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 | ||||
| 	} | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user