290 lines
8.8 KiB
Go
290 lines
8.8 KiB
Go
package handler
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/artalkjs/artalk/v2/internal/core"
|
|
"github.com/artalkjs/artalk/v2/internal/dao"
|
|
"github.com/artalkjs/artalk/v2/internal/entity"
|
|
"github.com/artalkjs/artalk/v2/internal/i18n"
|
|
"github.com/artalkjs/artalk/v2/server/common"
|
|
"github.com/gofiber/fiber/v2"
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/clause"
|
|
)
|
|
|
|
type ResponseVote struct {
|
|
Up int `json:"up"`
|
|
Down int `json:"down"`
|
|
IsUp bool `json:"is_up"`
|
|
IsDown bool `json:"is_down"`
|
|
}
|
|
|
|
// @Id GetVote
|
|
// @Summary Get Vote Status
|
|
// @Description Get vote status for a specific comment or page
|
|
// @Tags Vote
|
|
// @Param target_name path string true "The name of vote target" Enums(comment, page)
|
|
// @Param target_id path int true "The target comment or page ID"
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Success 200 {object} ResponseVote
|
|
// @Failure 400 {object} Map{msg=string}
|
|
// @Failure 403 {object} Map{msg=string}
|
|
// @Failure 404 {object} Map{msg=string}
|
|
// @Failure 500 {object} Map{msg=string}
|
|
// @Router /votes/{target_name}/{target_id} [get]
|
|
func VoteGet(app *core.App, router fiber.Router) {
|
|
router.Get("/votes/:target_name/:target_id", func(c *fiber.Ctx) error {
|
|
targetName := c.Params("target_name")
|
|
targetID, err := c.ParamsInt("target_id")
|
|
if err != nil || targetID <= 0 {
|
|
return common.RespError(c, 400, "invalid vote target id")
|
|
}
|
|
|
|
switch targetName {
|
|
case "comment":
|
|
if app.Dao().FindComment(uint(targetID)).IsEmpty() {
|
|
return common.RespError(c, 404, i18n.T("{{name}} not found", Map{"name": i18n.T("Comment")}))
|
|
}
|
|
case "page":
|
|
if app.Dao().FindPageByID(uint(targetID)).IsEmpty() {
|
|
return common.RespError(c, 404, i18n.T("{{name}} not found", Map{"name": i18n.T("Page")}))
|
|
}
|
|
default:
|
|
return common.RespError(c, 404, "unknown vote target name")
|
|
}
|
|
|
|
var result ResponseVote
|
|
result.Up, result.Down = app.Dao().GetVoteNumUpDown(targetName, uint(targetID))
|
|
exitsVotes := getExistsVotesByIP(app.Dao(), c.IP(), targetName, uint(targetID))
|
|
if len(exitsVotes) > 0 {
|
|
choice := getVoteChoice(string(exitsVotes[0].Type))
|
|
result.IsUp = choice == "up"
|
|
result.IsDown = choice == "down"
|
|
}
|
|
|
|
return common.RespData(c, result)
|
|
})
|
|
}
|
|
|
|
type ParamsVoteCreate struct {
|
|
Name string `json:"name" validate:"optional"` // The username
|
|
Email string `json:"email" validate:"optional"` // The user email
|
|
}
|
|
|
|
// @Id CreateVote
|
|
// @Summary Create Vote
|
|
// @Description Create a new vote for a specific comment or page
|
|
// @Tags Vote
|
|
// @Param target_name path string true "The name of vote target" Enums(comment, page)
|
|
// @Param target_id path int true "The target comment or page ID"
|
|
// @Param choice path string true "The vote choice" Enums(up, down)
|
|
// @Param vote body ParamsVoteCreate true "The vote data"
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Success 200 {object} ResponseVote
|
|
// @Failure 403 {object} Map{msg=string}
|
|
// @Failure 404 {object} Map{msg=string}
|
|
// @Failure 500 {object} Map{msg=string}
|
|
// @Router /votes/{target_name}/{target_id}/{choice} [post]
|
|
func VoteCreate(app *core.App, router fiber.Router) {
|
|
router.Post("/votes/:target_name/:target_id/:choice", common.LimiterGuard(app, func(c *fiber.Ctx) error {
|
|
targetName := c.Params("target_name")
|
|
targetID, _ := c.ParamsInt("target_id")
|
|
choice := c.Params("choice")
|
|
ip := c.IP()
|
|
|
|
var p ParamsVoteCreate
|
|
if isOK, resp := common.ParamsDecode(c, &p); !isOK {
|
|
return resp
|
|
}
|
|
|
|
if choice != "up" && choice != "down" {
|
|
return common.RespError(c, 404, "unknown vote choice")
|
|
}
|
|
|
|
// Find the target model
|
|
var (
|
|
comment entity.Comment
|
|
page entity.Page
|
|
user entity.User
|
|
)
|
|
|
|
switch targetName {
|
|
case "comment":
|
|
comment = app.Dao().FindComment(uint(targetID))
|
|
if comment.IsEmpty() {
|
|
return common.RespError(c, 404, i18n.T("{{name}} not found", Map{"name": i18n.T("Comment")}))
|
|
}
|
|
case "page":
|
|
page = app.Dao().FindPageByID(uint(targetID))
|
|
if page.IsEmpty() {
|
|
return common.RespError(c, 404, i18n.T("{{name}} not found", Map{"name": i18n.T("Page")}))
|
|
}
|
|
default:
|
|
return common.RespError(c, 404, "unknown vote target name")
|
|
}
|
|
|
|
// Find user
|
|
if p.Name != "" && p.Email != "" {
|
|
var err error
|
|
user, err = app.Dao().FindCreateUser(p.Name, p.Email, "")
|
|
if err != nil {
|
|
return common.RespError(c, 500, "Failed to create user")
|
|
}
|
|
}
|
|
|
|
result, err := updateVote(app.Dao(), createNewVoteParams{
|
|
ip: ip,
|
|
ua: string(c.Request().Header.UserAgent()),
|
|
userID: user.ID,
|
|
targetName: targetName,
|
|
targetID: uint(targetID),
|
|
choice: choice,
|
|
})
|
|
if err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
target := i18n.T("Comment")
|
|
if targetName == "page" {
|
|
target = i18n.T("Page")
|
|
}
|
|
return common.RespError(c, 404, i18n.T("{{name}} not found", Map{"name": target}))
|
|
}
|
|
return common.RespError(c, 500, "Failed to update vote")
|
|
}
|
|
|
|
return common.RespData(c, result)
|
|
}))
|
|
}
|
|
|
|
// VoteChoice is `up` or `down`
|
|
func getVoteChoice(voteType string) string {
|
|
choice := strings.TrimPrefix(strings.TrimPrefix(voteType, "comment_"), "page_")
|
|
if choice != "up" && choice != "down" {
|
|
return ""
|
|
}
|
|
return choice
|
|
}
|
|
|
|
// VoteTarget is `comment` or `page`
|
|
func getVoteTargetName(voteType string) string {
|
|
return strings.TrimSuffix(strings.TrimSuffix(voteType, "_up"), "_down")
|
|
}
|
|
|
|
func getExistsVotesByIP(dao *dao.Dao, ip string, targetName string, targetID uint) []entity.Vote {
|
|
var existsVotes []entity.Vote
|
|
dao.DB().Where("type LIKE ? AND target_id = ? AND ip = ?", targetName+"%", uint(targetID), ip).Find(&existsVotes)
|
|
return existsVotes
|
|
}
|
|
|
|
type createNewVoteParams struct {
|
|
ip string
|
|
ua string
|
|
userID uint
|
|
targetName string
|
|
targetID uint
|
|
choice string
|
|
}
|
|
|
|
func updateVote(d *dao.Dao, opts createNewVoteParams) (ResponseVote, error) {
|
|
var (
|
|
result ResponseVote
|
|
comment entity.Comment
|
|
page entity.Page
|
|
)
|
|
|
|
err := d.DB().Transaction(func(tx *gorm.DB) error {
|
|
switch opts.targetName {
|
|
case "comment":
|
|
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&comment, opts.targetID).Error; err != nil {
|
|
return fmt.Errorf("lock vote comment: %w", err)
|
|
}
|
|
case "page":
|
|
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&page, opts.targetID).Error; err != nil {
|
|
return fmt.Errorf("lock vote page: %w", err)
|
|
}
|
|
default:
|
|
return fmt.Errorf("unknown vote target %q", opts.targetName)
|
|
}
|
|
|
|
voteTypes := []entity.VoteType{
|
|
entity.VoteType(opts.targetName + "_up"),
|
|
entity.VoteType(opts.targetName + "_down"),
|
|
}
|
|
var existingVotes []entity.Vote
|
|
if err := tx.Where("type IN ? AND target_id = ? AND ip = ?", voteTypes, opts.targetID, opts.ip).Find(&existingVotes).Error; err != nil {
|
|
return fmt.Errorf("find existing votes: %w", err)
|
|
}
|
|
|
|
selectedChoice := opts.choice
|
|
if len(existingVotes) > 0 {
|
|
existingChoice := getVoteChoice(string(existingVotes[0].Type))
|
|
if err := tx.Unscoped().Delete(&existingVotes).Error; err != nil {
|
|
return fmt.Errorf("delete existing votes: %w", err)
|
|
}
|
|
if selectedChoice == existingChoice {
|
|
selectedChoice = ""
|
|
}
|
|
}
|
|
|
|
if selectedChoice != "" {
|
|
vote := entity.Vote{
|
|
TargetID: opts.targetID,
|
|
Type: entity.VoteType(opts.targetName + "_" + selectedChoice),
|
|
UserID: opts.userID,
|
|
UA: opts.ua,
|
|
IP: opts.ip,
|
|
}
|
|
if err := tx.Create(&vote).Error; err != nil {
|
|
return fmt.Errorf("create vote: %w", err)
|
|
}
|
|
}
|
|
|
|
var up, down int64
|
|
if err := tx.Model(&entity.Vote{}).Where("target_id = ? AND type = ?", opts.targetID, voteTypes[0]).Count(&up).Error; err != nil {
|
|
return fmt.Errorf("count up votes: %w", err)
|
|
}
|
|
if err := tx.Model(&entity.Vote{}).Where("target_id = ? AND type = ?", opts.targetID, voteTypes[1]).Count(&down).Error; err != nil {
|
|
return fmt.Errorf("count down votes: %w", err)
|
|
}
|
|
|
|
updates := map[string]any{"vote_up": int(up), "vote_down": int(down)}
|
|
switch opts.targetName {
|
|
case "comment":
|
|
comment.VoteUp, comment.VoteDown = int(up), int(down)
|
|
if err := tx.Model(&comment).Updates(updates).Error; err != nil {
|
|
return fmt.Errorf("update comment vote counts: %w", err)
|
|
}
|
|
case "page":
|
|
page.VoteUp, page.VoteDown = int(up), int(down)
|
|
if err := tx.Model(&page).Updates(updates).Error; err != nil {
|
|
return fmt.Errorf("update page vote counts: %w", err)
|
|
}
|
|
}
|
|
|
|
result = ResponseVote{
|
|
Up: int(up),
|
|
Down: int(down),
|
|
IsUp: selectedChoice == "up",
|
|
IsDown: selectedChoice == "down",
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return ResponseVote{}, err
|
|
}
|
|
|
|
d.CacheAction(func(cache *dao.DaoCache) {
|
|
if opts.targetName == "comment" {
|
|
_ = cache.CommentCacheSave(&comment)
|
|
} else {
|
|
_ = cache.PageCacheSave(&page)
|
|
}
|
|
})
|
|
|
|
return result, nil
|
|
}
|