Skip to content

Commit

Permalink
Merge pull request #8 from InfinityBotList/blog-likes
Browse files Browse the repository at this point in the history
Blog likes
  • Loading branch information
cheesycod authored Jun 9, 2024
2 parents 786d80b + 6940e76 commit 51df85a
Show file tree
Hide file tree
Showing 7 changed files with 376 additions and 271 deletions.
4 changes: 2 additions & 2 deletions notifications/vote_reminders.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,15 @@ func vrCheck() error {
continue
}

vi, err := votes.EntityVoteCheck(state.Context, userId, targetId, targetType)
vi, err := votes.EntityVoteCheck(state.Context, state.Pool, userId, targetId, targetType)

if err != nil {
state.Logger.Error("Error checking votes of entity", zap.Error(err), zap.String("userId", userId), zap.String("targetId", targetId), zap.String("targetType", targetType))
continue
}

if !vi.HasVoted {
entityInfo, err := votes.GetEntityInfo(state.Context, targetId, targetType)
entityInfo, err := votes.GetEntityInfo(state.Context, state.Pool, targetId, targetType)

if err != nil {
state.Logger.Error("Error finding bot info", zap.Error(err), zap.String("targetId", targetId), zap.String("targetType", targetType))
Expand Down
2 changes: 1 addition & 1 deletion routes/reminders/endpoints/put_user_reminders/route.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ func Route(d uapi.RouteData, r *http.Request) uapi.HttpResponse {
return uapi.DefaultResponse(http.StatusBadRequest)
}

entityInfo, err := votes.GetEntityInfo(d.Context, targetId, targetType)
entityInfo, err := votes.GetEntityInfo(d.Context, state.Pool, targetId, targetType)

if err != nil {
state.Logger.Error("Error getting entity info", zap.Error(err), zap.String("target_id", targetId), zap.String("target_type", targetType))
Expand Down
4 changes: 2 additions & 2 deletions routes/votes/endpoints/get_user_entity_votes/route.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import (
func Docs() *docs.Doc {
return &docs.Doc{
Summary: "Get User Entity Votes",
Description: "Gets a vote a user has made for an entity. Note that for compatibility, a trailing 's' is removed",
Description: "Gets all votes a user has made for an entity. Note that for compatibility, a trailing 's' is removed",
Params: []docs.Parameter{
{
Name: "uid",
Expand Down Expand Up @@ -60,7 +60,7 @@ func Route(d uapi.RouteData, r *http.Request) uapi.HttpResponse {

targetType = strings.TrimSuffix(targetType, "s")

uv, err := votes.EntityVoteCheck(d.Context, uid, targetId, targetType)
uv, err := votes.EntityVoteCheck(d.Context, state.Pool, uid, targetId, targetType)

if err != nil {
state.Logger.Error("Failed to get user entity votes", zap.Error(err), zap.String("userId", uid), zap.String("targetId", targetId), zap.String("targetType", targetType))
Expand Down
210 changes: 129 additions & 81 deletions routes/votes/endpoints/put_user_entity_votes/route.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,23 +132,29 @@ func Route(d uapi.RouteData, r *http.Request) uapi.HttpResponse {

targetType = strings.TrimSuffix(targetType, "s")

upvote := r.URL.Query().Get("upvote")
// Check if upvote query parameter is valid
upvoteStr := r.URL.Query().Get("upvote")

if upvote != "true" && upvote != "false" {
if upvoteStr != "true" && upvoteStr != "false" {
return uapi.HttpResponse{
Status: http.StatusBadRequest,
Json: types.ApiError{Message: "upvote must be either true or false"},
Json: types.ApiError{Message: "upvote must be either `true` or `false`"},
}
}

upvote := upvoteStr == "true"

// Check if user is allowed to even make a vote right now.
var voteBanned bool

err = state.Pool.QueryRow(d.Context, "SELECT vote_banned FROM users WHERE user_id = $1", uid).Scan(&voteBanned)

if err != nil {
state.Logger.Error("Failed to check if user is vote banned", zap.Error(err), zap.String("userId", uid))
return uapi.DefaultResponse(http.StatusInternalServerError)
return uapi.HttpResponse{
Status: http.StatusBadRequest,
Json: types.ApiError{Message: "Error checking if user is vote banned: " + err.Error()},
}
}

if voteBanned {
Expand All @@ -158,18 +164,20 @@ func Route(d uapi.RouteData, r *http.Request) uapi.HttpResponse {
}
}

// Handle entity specific checks here, such as ensuring the entity actually exists
switch targetType {
case "bot":
if upvote == "false" {
return uapi.HttpResponse{
Status: http.StatusNotImplemented,
Json: types.ApiError{Message: "Downvoting bots is not implemented yet"},
}
// Create a new entity vote
tx, err := state.Pool.Begin(d.Context)

if err != nil {
state.Logger.Error("Failed to create transaction [put_user_entity_votes]", zap.Error(err))
return uapi.HttpResponse{
Status: http.StatusInternalServerError,
Json: types.ApiError{Message: "Failed to create transaction: " + err.Error()},
}
}

entityInfo, err := votes.GetEntityInfo(d.Context, targetId, targetType)
defer tx.Rollback(d.Context)

entityInfo, err := votes.GetEntityInfo(d.Context, tx, targetId, targetType)

if err != nil {
state.Logger.Error("Failed to fetch entity info", zap.Error(err), zap.String("userId", uid), zap.String("targetId", targetId), zap.String("targetType", targetType))
Expand All @@ -180,46 +188,77 @@ func Route(d uapi.RouteData, r *http.Request) uapi.HttpResponse {
}

// Now check the vote
vi, err := votes.EntityVoteCheck(d.Context, uid, targetId, targetType)
vi, err := votes.EntityVoteCheck(d.Context, tx, uid, targetId, targetType)

if err != nil {
state.Logger.Error("Failed to check vote", zap.Error(err), zap.String("userId", uid), zap.String("targetId", targetId), zap.String("targetType", targetType))
return uapi.DefaultResponse(http.StatusInternalServerError)
}

if vi.HasVoted {
timeStr := fmt.Sprintf("%02d hours, %02d minutes. %02d seconds", vi.Wait.Hours, vi.Wait.Minutes, vi.Wait.Seconds)

if len(vi.ValidVotes) > 1 {
return uapi.HttpResponse{
Status: http.StatusBadRequest,
Json: types.ApiError{Message: "Your last vote was a double vote, calm down?: " + timeStr},
}
if !vi.VoteInfo.SupportsDownvotes && !upvote {
return uapi.HttpResponse{
Status: http.StatusBadRequest,
Json: types.ApiError{Message: "This entity does not support downvotes"},
}
}

if !vi.VoteInfo.SupportsUpvotes && upvote {
return uapi.HttpResponse{
Status: http.StatusBadRequest,
Json: types.ApiError{Message: "Please wait " + timeStr + " before voting again"},
Json: types.ApiError{Message: "This entity does not support upvotes"},
}
}

// Create a new entity vote
tx, err := state.Pool.Begin(d.Context)
if vi.HasVoted {
// lacking MultipleVotes means that there can only be one vote per user for the entity
if !vi.VoteInfo.MultipleVotes {
if vi.ValidVotes[0].Upvote == upvote {
return uapi.HttpResponse{
Status: http.StatusBadRequest,
Json: types.ApiError{Message: "You have already voted for this entity before!"},
}
} else {
// Remove all old votes by said user
_, err = tx.Exec(d.Context, "DELETE FROM entity_votes WHERE author = $1 AND target_id = $2 AND target_type = $3", uid, targetId, targetType)

if err != nil {
state.Logger.Error("Failed to delete old vote", zap.Error(err), zap.String("userId", uid), zap.String("targetId", targetId), zap.String("targetType", targetType))
return uapi.HttpResponse{
Status: http.StatusInternalServerError,
Json: types.ApiError{Message: "Failed to delete old vote: " + err.Error()},
}
}
}
} else {
var timeStr string
if vi.Wait != nil {
timeStr = fmt.Sprintf("%02d hours, %02d minutes. %02d seconds", vi.Wait.Hours, vi.Wait.Minutes, vi.Wait.Seconds)
} else {
timeStr = "a while"
}

if err != nil {
state.Logger.Error("Failed to create transaction [put_user_entity_votes]", zap.Error(err))
return uapi.DefaultResponse(http.StatusInternalServerError)
}
if len(vi.ValidVotes) > 1 {
return uapi.HttpResponse{
Status: http.StatusBadRequest,
Json: types.ApiError{Message: "Your last vote was a double vote, calm down for " + timeStr + "?"},
}
}

defer tx.Rollback(d.Context)
return uapi.HttpResponse{
Status: http.StatusBadRequest,
Json: types.ApiError{Message: "Please wait " + timeStr + " before voting again"},
}
}
}

// Keep adding votes until, but not including vi.VoteInfo.PerUser
for i := 0; i < vi.VoteInfo.PerUser; i++ {
_, err = tx.Exec(d.Context, "INSERT INTO entity_votes (author, target_id, target_type, upvote, vote_num) VALUES ($1, $2, $3, $4, $5)", uid, targetId, targetType, upvote == "true", i)
err = votes.EntityGiveVotes(d.Context, tx, upvote, uid, targetType, targetId, vi.VoteInfo)

if err != nil {
state.Logger.Error("Failed to insert vote", zap.Error(err), zap.String("userId", uid), zap.String("targetId", targetId), zap.String("targetType", targetType), zap.String("upvote", upvote))
return uapi.DefaultResponse(http.StatusInternalServerError)
if err != nil {
state.Logger.Error("Failed to give votes", zap.Error(err), zap.String("userId", uid), zap.String("targetId", targetId), zap.String("targetType", targetType))
return uapi.HttpResponse{
Status: http.StatusInternalServerError,
Json: types.ApiError{Message: "Failed to give votes: " + err.Error()},
}
}

Expand All @@ -228,7 +267,10 @@ func Route(d uapi.RouteData, r *http.Request) uapi.HttpResponse {

if err != nil {
state.Logger.Error("Failed to fetch new vote count", zap.Error(err), zap.String("userId", uid), zap.String("targetId", targetId), zap.String("targetType", targetType))
return uapi.DefaultResponse(http.StatusInternalServerError)
return uapi.HttpResponse{
Status: http.StatusInternalServerError,
Json: types.ApiError{Message: "Failed to fetch new vote count: " + err.Error()},
}
}

// Commit transaction
Expand All @@ -240,59 +282,61 @@ func Route(d uapi.RouteData, r *http.Request) uapi.HttpResponse {
}

// Fetch user info to log it to server
userObj, err := dovewing.GetUser(d.Context, uid, state.DovewingPlatformDiscord)
go func() {
userObj, err := dovewing.GetUser(d.Context, uid, state.DovewingPlatformDiscord)

if err != nil {
state.Logger.Error("Failed to fetch user info", zap.Error(err), zap.String("userId", uid), zap.String("targetId", targetId), zap.String("targetType", targetType))
return uapi.DefaultResponse(http.StatusInternalServerError)
}
if err != nil {
state.Logger.Error("Failed to fetch user info", zap.Error(err), zap.String("userId", uid), zap.String("targetId", targetId), zap.String("targetType", targetType))
return
}

_, err = state.Discord.ChannelMessageSendComplex(state.Config.Channels.VoteLogs, &discordgo.MessageSend{
Embeds: []*discordgo.MessageEmbed{
{
URL: entityInfo.URL,
Thumbnail: &discordgo.MessageEmbedThumbnail{
URL: entityInfo.Avatar,
},
Title: "🎉 Vote Count Updated!",
Description: ":heart:" + userObj.DisplayName + " has voted for " + targetType + ": " + entityInfo.Name,
Color: 0x8A6BFD,
Fields: []*discordgo.MessageEmbedField{
{
Name: "Vote Count:",
Value: strconv.Itoa(nvc),
Inline: true,
},
{
Name: "Votes Added:",
Value: strconv.Itoa(vi.VoteInfo.PerUser),
Inline: true,
_, err = state.Discord.ChannelMessageSendComplex(state.Config.Channels.VoteLogs, &discordgo.MessageSend{
Embeds: []*discordgo.MessageEmbed{
{
URL: entityInfo.URL,
Thumbnail: &discordgo.MessageEmbedThumbnail{
URL: entityInfo.Avatar,
},
{
Name: "User ID:",
Value: userObj.ID,
Inline: true,
},
{
Name: "View " + targetType + "'s page",
Value: "[View " + entityInfo.Name + "](" + entityInfo.URL + ")",
Inline: true,
},
{
Name: "Vote Page",
Value: "[Vote for " + entityInfo.Name + "](" + entityInfo.VoteURL + ")",
Inline: true,
Title: "🎉 Vote Count Updated!",
Description: ":heart:" + userObj.DisplayName + " has voted for " + targetType + ": " + entityInfo.Name,
Color: 0x8A6BFD,
Fields: []*discordgo.MessageEmbedField{
{
Name: "Vote Count:",
Value: strconv.Itoa(nvc),
Inline: true,
},
{
Name: "Votes Added:",
Value: strconv.Itoa(vi.VoteInfo.PerUser),
Inline: true,
},
{
Name: "User ID:",
Value: userObj.ID,
Inline: true,
},
{
Name: "View " + targetType + "'s page",
Value: "[View " + entityInfo.Name + "](" + entityInfo.URL + ")",
Inline: true,
},
{
Name: "Vote Page",
Value: "[Vote for " + entityInfo.Name + "](" + entityInfo.VoteURL + ")",
Inline: true,
},
},
},
},
},
})
})

if err != nil {
state.Logger.Error("Failed to send vote log message", zap.Error(err), zap.String("userId", uid), zap.String("targetId", targetId), zap.String("targetType", targetType))
}
if err != nil {
state.Logger.Error("Failed to send vote log message", zap.Error(err), zap.String("userId", uid), zap.String("targetId", targetId), zap.String("targetType", targetType))
}
}()

// Send webhook in a goroutine refunding the vote if it failed
// Send webhook in a goroutine
go func() {
err = nil // Be sure error is empty before we start

Expand All @@ -305,6 +349,10 @@ func Route(d uapi.RouteData, r *http.Request) uapi.HttpResponse {
PerUser: vi.VoteInfo.PerUser,
},
})

if err != nil {
state.Logger.Error("Failed to send webhook", zap.Error(err), zap.String("userId", uid), zap.String("targetId", targetId), zap.String("targetType", targetType))
}
}()

return uapi.DefaultResponse(http.StatusNoContent)
Expand Down
15 changes: 10 additions & 5 deletions types/vote.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,12 @@ type EntityVote struct {

// Vote Info
type VoteInfo struct {
PerUser int `json:"per_user" description:"The amount of votes a single vote creates on this entity"`
VoteTime uint16 `json:"vote_time" description:"The amount of time in hours until a user can vote again"`
PerUser int `json:"per_user" description:"The amount of votes a single vote creates on this entity"`
VoteTime uint16 `json:"vote_time" description:"The amount of time in hours until a user can vote again"`
VoteCredits bool `json:"vote_credits" description:"Whether or not the entity supports vote credits"`
MultipleVotes bool `json:"multiple_votes" description:"Whether or not the entity supports multiple votes per time interval"`
SupportsUpvotes bool `json:"supports_upvotes" description:"Whether or not the entity supports upvotes"`
SupportsDownvotes bool `json:"supports_downvotes" description:"Whether or not the entity supports downvotes"`
}

// Stores the hours, minutes and seconds until the user can vote again
Expand All @@ -38,13 +42,14 @@ type VoteWait struct {
}

type ValidVote struct {
Upvote bool `json:"upvote" description:"Whether or not the vote was an upvote"`
CreatedAt time.Time `json:"created_at" description:"The time the vote was created"`
ID pgtype.UUID `json:"id" description:"The ID of the vote"`
Upvote bool `json:"upvote" description:"Whether or not the vote was an upvote"`
CreatedAt time.Time `json:"created_at" description:"The time the vote was created"`
}

// A user vote is a struct containing basic info on a users vote
type UserVote struct {
HasVoted bool `json:"has_voted" description:"Whether or not the user has voted"`
HasVoted bool `json:"has_voted" description:"Whether or not the user has voted for the entity. If an entity supports multiple votes, this will be true if the user has voted in the last vote time, otherwise, it will be true if the user has voted at all"`
ValidVotes []*ValidVote `json:"valid_votes" description:"Some information about a valid vote"`
VoteInfo *VoteInfo `json:"vote_info" description:"Some information about the vote"`
Wait *VoteWait `json:"wait" description:"The time until the user can vote again"`
Expand Down
Loading

0 comments on commit 51df85a

Please sign in to comment.