From 8b03ddc0d4a505ae7bb89ef4aff98289ffd8cbf1 Mon Sep 17 00:00:00 2001
From: Rootspring <cheesycod@outlook.com>
Date: Mon, 3 Jun 2024 22:29:21 +0200
Subject: [PATCH 1/8] Add initial support for blogs

---
 votes/common.go | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/votes/common.go b/votes/common.go
index f7564059..2049b401 100644
--- a/votes/common.go
+++ b/votes/common.go
@@ -25,6 +25,8 @@ func GetVoteTime() uint16 {
 // Returns core vote info about the entity (such as the amount of cooldown time the entity has)
 //
 // If user id is specified, then in the future special perks for the user will be returned as well
+//
+// If vote time is negative, then it is not possible to revote
 func EntityVoteInfo(ctx context.Context, userId, targetId, targetType string) (*types.VoteInfo, error) {
 	var defaultVoteEntity = types.VoteInfo{
 		PerUser: func() int {
@@ -63,6 +65,9 @@ func EntityVoteInfo(ctx context.Context, userId, targetId, targetType string) (*
 		if premium {
 			defaultVoteEntity.VoteTime = 4
 		}
+	case "blog":
+		defaultVoteEntity.PerUser = 1 // Only 1 vote per blog post
+	        defaultVoteEntity.VoteTime = -1 // No revotes allowed
 	}
 
 	return &defaultVoteEntity, nil
@@ -109,7 +114,7 @@ func EntityVoteCheck(ctx context.Context, userId, targetId, targetType string) (
 
 	var vw *types.VoteWait
 
-	if len(validVotes) > 0 {
+	if len(validVotes) > 0 && vi.VoteTime > 0 {
 		timeElapsed := time.Since(validVotes[0].CreatedAt)
 
 		timeToWait := int64(vi.VoteTime)*60*60*1000 - timeElapsed.Milliseconds()

From fca329aee6e277e155a4c8d5f78ffc0aba63f294 Mon Sep 17 00:00:00 2001
From: Rootspring <cheesycod@outlook.com>
Date: Mon, 3 Jun 2024 22:39:01 +0200
Subject: [PATCH 2/8] Add blog to entityinfo

---
 votes/helpers.go | 18 ++++++++++++------
 1 file changed, 12 insertions(+), 6 deletions(-)

diff --git a/votes/helpers.go b/votes/helpers.go
index fc7ccc72..c1419d45 100644
--- a/votes/helpers.go
+++ b/votes/helpers.go
@@ -97,8 +97,8 @@ func GetEntityInfo(ctx context.Context, targetId, targetType string) (*EntityInf
 
 		// Set entityInfo for log
 		return &EntityInfo{
-			URL:     "https://botlist.site/team/" + targetId,
-			VoteURL: "https://botlist.site/team/" + targetId + "/vote",
+			URL:     state.Config.Sites.Frontend.Parse() + "/team/" + targetId,
+			VoteURL: state.Config.Sites.Frontend.Parse() + "/team/" + targetId + "/vote",
 			Name:    name,
 			Avatar:  avatarPath,
 		}, nil
@@ -120,13 +120,19 @@ func GetEntityInfo(ctx context.Context, targetId, targetType string) (*EntityInf
 			return nil, errors.New("server is vote banned and cannot be voted for right now")
 		}
 
-		// Set entityInfo for log
-		return &EntityInfo{
-			URL:     "https://botlist.site/server/" + targetId,
-			VoteURL: "https://botlist.site/server/" + targetId + "/vote",
+   		// Set entityInfo for log
+                return &EntityInfo{
+			URL: state.Config.Sites.Frontend.Parse()+ "/server/" + targetId,
+			VoteURL: state.Config.Sites.Frontend.Parse() + "/server/" + targetId + "/vote",
 			Name:    name,
 			Avatar:  avatar,
 		}, nil
+	case "blog":
+	        return &EntityInfo{
+			URL:     state.Config.Sites.Frontend.Parse() + "/blog/" + targetId,
+			VoteURL: state.Config.Sites.Frontend.Parse() + "/blog/" + targetId,
+			Name:    targetId,
+		}, nil
 	default:
 		return nil, errors.New("unimplemented target type:" + targetType)
 	}

From 194c76b29fd6116d040d05da4198103c010e3888 Mon Sep 17 00:00:00 2001
From: Rootspring <cheesycod@outlook.com>
Date: Mon, 3 Jun 2024 22:41:43 +0200
Subject: [PATCH 3/8] Add avatar support to blog entity as default avatar

---
 votes/helpers.go | 1 +
 1 file changed, 1 insertion(+)

diff --git a/votes/helpers.go b/votes/helpers.go
index c1419d45..9445fa8f 100644
--- a/votes/helpers.go
+++ b/votes/helpers.go
@@ -132,6 +132,7 @@ func GetEntityInfo(ctx context.Context, targetId, targetType string) (*EntityInf
 			URL:     state.Config.Sites.Frontend.Parse() + "/blog/" + targetId,
 			VoteURL: state.Config.Sites.Frontend.Parse() + "/blog/" + targetId,
 			Name:    targetId,
+			Avatar:  state.Config.Sites.CDN.Parse() + "/avatars/default.webp",
 		}, nil
 	default:
 		return nil, errors.New("unimplemented target type:" + targetType)

From 887ab08cab17a4fa2c99d7d12db2dc4f339c397f Mon Sep 17 00:00:00 2001
From: Rootspring <cheesycod@outlook.com>
Date: Mon, 3 Jun 2024 23:07:35 +0200
Subject: [PATCH 4/8] Update helpers.go

---
 votes/helpers.go | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/votes/helpers.go b/votes/helpers.go
index 9445fa8f..026da936 100644
--- a/votes/helpers.go
+++ b/votes/helpers.go
@@ -55,17 +55,17 @@ func GetEntityInfo(ctx context.Context, targetId, targetType string) (*EntityInf
 
 		// Set entityInfo for log
 		return &EntityInfo{
-			URL:     "https://botlist.site/" + targetId,
-			VoteURL: "https://botlist.site/" + targetId + "/vote",
+			URL:     state.Config.Sites.Frontend.Parse() + "/bot/" + targetId,
+			VoteURL: state.Config.Sites.Frontend.Parse() + "/bot/" + targetId + "/vote",
 			Name:    botObj.Username,
 			Avatar:  botObj.Avatar,
 		}, nil
 	case "pack":
 		return &EntityInfo{
-			URL:     "https://botlist.site/pack/" + targetId,
-			VoteURL: "https://botlist.site/pack/" + targetId,
+			URL:     state.Config.Sites.Frontend.Parse() + "/pack/" + targetId,
+			VoteURL: state.Config.Sites.Frontend.Parse() + "/pack/" + targetId,
 			Name:    targetId,
-			Avatar:  "",
+			Avatar:  state.Config.Sites.CDN.Parse() + "/avatars/default.webp",
 		}, nil
 	case "team":
 		var name string
@@ -122,7 +122,7 @@ func GetEntityInfo(ctx context.Context, targetId, targetType string) (*EntityInf
 
    		// Set entityInfo for log
                 return &EntityInfo{
-			URL: state.Config.Sites.Frontend.Parse()+ "/server/" + targetId,
+			URL:     state.Config.Sites.Frontend.Parse()+ "/server/" + targetId,
 			VoteURL: state.Config.Sites.Frontend.Parse() + "/server/" + targetId + "/vote",
 			Name:    name,
 			Avatar:  avatar,

From 14c59b4240a08c5b29e20a09872055cca74ce97c Mon Sep 17 00:00:00 2001
From: Rootspring <cheesycod@outlook.com>
Date: Tue, 4 Jun 2024 21:54:28 +0200
Subject: [PATCH 5/8] Add vote credits optin for entities part 1

---
 votes/helpers.go | 27 +++++++++++++++------------
 1 file changed, 15 insertions(+), 12 deletions(-)

diff --git a/votes/helpers.go b/votes/helpers.go
index 026da936..a12e0e31 100644
--- a/votes/helpers.go
+++ b/votes/helpers.go
@@ -13,10 +13,11 @@ import (
 )
 
 type EntityInfo struct {
-	Name    string
-	URL     string
-	VoteURL string
-	Avatar  string
+	Name        string
+	URL         string
+	VoteURL     string
+	Avatar      string
+	VoteCredits bool
 }
 
 // GetEntityInfo returns information about the entity that is being voted for including vote bans etc.
@@ -55,10 +56,11 @@ func GetEntityInfo(ctx context.Context, targetId, targetType string) (*EntityInf
 
 		// Set entityInfo for log
 		return &EntityInfo{
-			URL:     state.Config.Sites.Frontend.Parse() + "/bot/" + targetId,
-			VoteURL: state.Config.Sites.Frontend.Parse() + "/bot/" + targetId + "/vote",
-			Name:    botObj.Username,
-			Avatar:  botObj.Avatar,
+			URL:         state.Config.Sites.Frontend.Parse() + "/bot/" + targetId,
+			VoteURL:     state.Config.Sites.Frontend.Parse() + "/bot/" + targetId + "/vote",
+			Name:        botObj.Username,
+			Avatar:      botObj.Avatar,
+                        VoteCredits: true,
 		}, nil
 	case "pack":
 		return &EntityInfo{
@@ -122,10 +124,11 @@ func GetEntityInfo(ctx context.Context, targetId, targetType string) (*EntityInf
 
    		// Set entityInfo for log
                 return &EntityInfo{
-			URL:     state.Config.Sites.Frontend.Parse()+ "/server/" + targetId,
-			VoteURL: state.Config.Sites.Frontend.Parse() + "/server/" + targetId + "/vote",
-			Name:    name,
-			Avatar:  avatar,
+			URL:         state.Config.Sites.Frontend.Parse()+ "/server/" + targetId,
+			VoteURL:     state.Config.Sites.Frontend.Parse() + "/server/" + targetId + "/vote",
+			Name:        name,
+			Avatar:      avatar,
+			VoteCredits: true,
 		}, nil
 	case "blog":
 	        return &EntityInfo{

From fb6313f6ac5a9588b4437566079d94d72e3de5bf Mon Sep 17 00:00:00 2001
From: cheesycod <cheesycod@outlook.com>
Date: Sun, 9 Jun 2024 20:26:51 +0530
Subject: [PATCH 6/8] fix: vote refactors and more multiple vote type stuff

---
 notifications/vote_reminders.go               |   4 +-
 .../endpoints/put_user_reminders/route.go     |   2 +-
 .../endpoints/get_user_entity_votes/route.go  |   4 +-
 .../endpoints/put_user_entity_votes/route.go  | 165 +++++++++++-------
 types/vote.go                                 |  10 +-
 votes/common.go                               | 136 +++++++++------
 votes/helpers.go                              |  47 +++--
 7 files changed, 216 insertions(+), 152 deletions(-)

diff --git a/notifications/vote_reminders.go b/notifications/vote_reminders.go
index 9e752133..3611a22c 100644
--- a/notifications/vote_reminders.go
+++ b/notifications/vote_reminders.go
@@ -52,7 +52,7 @@ 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))
@@ -60,7 +60,7 @@ func vrCheck() error {
 		}
 
 		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))
diff --git a/routes/reminders/endpoints/put_user_reminders/route.go b/routes/reminders/endpoints/put_user_reminders/route.go
index 19e6bb3c..171689d0 100644
--- a/routes/reminders/endpoints/put_user_reminders/route.go
+++ b/routes/reminders/endpoints/put_user_reminders/route.go
@@ -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))
diff --git a/routes/votes/endpoints/get_user_entity_votes/route.go b/routes/votes/endpoints/get_user_entity_votes/route.go
index 0c6fec46..4de55d90 100644
--- a/routes/votes/endpoints/get_user_entity_votes/route.go
+++ b/routes/votes/endpoints/get_user_entity_votes/route.go
@@ -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",
@@ -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))
diff --git a/routes/votes/endpoints/put_user_entity_votes/route.go b/routes/votes/endpoints/put_user_entity_votes/route.go
index 5658d497..4f1a8323 100644
--- a/routes/votes/endpoints/put_user_entity_votes/route.go
+++ b/routes/votes/endpoints/put_user_entity_votes/route.go
@@ -132,15 +132,18 @@ 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
 
@@ -148,7 +151,10 @@ func Route(d uapi.RouteData, r *http.Request) uapi.HttpResponse {
 
 	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 {
@@ -158,18 +164,7 @@ 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"},
-			}
-		}
-	}
-
-	entityInfo, err := votes.GetEntityInfo(d.Context, targetId, targetType)
+	entityInfo, err := votes.GetEntityInfo(d.Context, state.Pool, 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))
@@ -180,20 +175,47 @@ 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, state.Pool, 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.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: "This entity does not support upvotes"},
+		}
+	}
+
 	if vi.HasVoted {
-		timeStr := fmt.Sprintf("%02d hours, %02d minutes. %02d seconds", vi.Wait.Hours, vi.Wait.Minutes, vi.Wait.Seconds)
+		// If !Multiple Votes
+		if !vi.VoteInfo.MultipleVotes {
+			return uapi.HttpResponse{
+				Status: http.StatusBadRequest,
+				Json:   types.ApiError{Message: "You have already voted for this entity before!"},
+			}
+		}
+
+		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 len(vi.ValidVotes) > 1 {
 			return uapi.HttpResponse{
 				Status: http.StatusBadRequest,
-				Json:   types.ApiError{Message: "Your last vote was a double vote, calm down?: " + timeStr},
+				Json:   types.ApiError{Message: "Your last vote was a double vote, calm down for " + timeStr + "?"},
 			}
 		}
 
@@ -208,17 +230,20 @@ func Route(d uapi.RouteData, r *http.Request) uapi.HttpResponse {
 
 	if err != nil {
 		state.Logger.Error("Failed to create transaction [put_user_entity_votes]", zap.Error(err))
-		return uapi.DefaultResponse(http.StatusInternalServerError)
+		return uapi.HttpResponse{
+			Status: http.StatusInternalServerError,
+			Json:   types.ApiError{Message: "Failed to create transaction: " + err.Error()},
+		}
 	}
 
 	defer tx.Rollback(d.Context)
 
 	// 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 = 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, i)
 
 		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))
+			state.Logger.Error("Failed to insert vote", zap.Error(err), zap.String("userId", uid), zap.String("targetId", targetId), zap.String("targetType", targetType), zap.Bool("upvote", upvote))
 			return uapi.DefaultResponse(http.StatusInternalServerError)
 		}
 	}
@@ -240,59 +265,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
 
@@ -305,6 +332,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)
diff --git a/types/vote.go b/types/vote.go
index 13ba80fe..0bc860f9 100644
--- a/types/vote.go
+++ b/types/vote.go
@@ -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
@@ -44,7 +48,7 @@ type ValidVote struct {
 
 // 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"`
diff --git a/votes/common.go b/votes/common.go
index 2049b401..8957ec8a 100644
--- a/votes/common.go
+++ b/votes/common.go
@@ -2,7 +2,6 @@ package votes
 
 import (
 	"context"
-	"popplio/state"
 	"popplio/types"
 	"time"
 
@@ -10,40 +9,40 @@ import (
 	"github.com/jackc/pgx/v5/pgconn"
 )
 
-func GetDoubleVote() bool {
-	return time.Now().Weekday() == time.Friday || time.Now().Weekday() == time.Saturday || time.Now().Weekday() == time.Sunday
+type DbConn interface {
+	QueryRow(ctx context.Context, sql string, args ...any) pgx.Row
+	Query(ctx context.Context, sql string, args ...any) (pgx.Rows, error)
+	Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error)
 }
 
-func GetVoteTime() uint16 {
-	if GetDoubleVote() {
-		return 6
-	} else {
-		return 12
-	}
+func GetDoubleVote() bool {
+	weekday := time.Now().Weekday()
+	return weekday == time.Friday || weekday == time.Saturday || weekday == time.Sunday
 }
 
 // Returns core vote info about the entity (such as the amount of cooldown time the entity has)
 //
-// If user id is specified, then in the future special perks for the user will be returned as well
+// # If user id is specified, then in the future special perks for the user will be returned as well
 //
 // If vote time is negative, then it is not possible to revote
-func EntityVoteInfo(ctx context.Context, userId, targetId, targetType string) (*types.VoteInfo, error) {
-	var defaultVoteEntity = types.VoteInfo{
-		PerUser: func() int {
-			if GetDoubleVote() {
-				return 2
-			} else {
-				return 1
-			}
-		}(),
-		VoteTime: GetVoteTime(),
+func EntityVoteInfo(ctx context.Context, c DbConn, userId, targetId, targetType string) (*types.VoteInfo, error) {
+	var voteEntity = types.VoteInfo{
+		PerUser:           1,     // 1 vote per user
+		VoteTime:          12,    // per day
+		MultipleVotes:     true,  // Multiple votes per time interval
+		VoteCredits:       false, // Vote credits are not supported unless opted in
+		SupportsUpvotes:   true,  // Upvotes are supported (usually)
+		SupportsDownvotes: true,  // Downvotes are supported (usually)
 	}
 
 	// Add other special cases of entities not following the basic voting system rules
 	switch targetType {
 	case "bot":
+		voteEntity.VoteCredits = true        // Bots support vote credits
+		voteEntity.SupportsDownvotes = false // Bots cannot be downvoted
+
 		var premium bool
-		err := state.Pool.QueryRow(ctx, "SELECT premium FROM bots WHERE bot_id = $1", targetId).Scan(&premium)
+		err := c.QueryRow(ctx, "SELECT premium FROM bots WHERE bot_id = $1", targetId).Scan(&premium)
 
 		if err != nil {
 			return nil, err
@@ -51,43 +50,70 @@ func EntityVoteInfo(ctx context.Context, userId, targetId, targetType string) (*
 
 		// Premium bots get vote time of 4
 		if premium {
-			defaultVoteEntity.VoteTime = 4
+			voteEntity.VoteTime = 4
+		} else {
+			// Bot is not premium
+			if GetDoubleVote() {
+				voteEntity.PerUser = 2  // 2 votes per user
+				voteEntity.VoteTime = 6 // Half of the normal vote time
+			}
 		}
 	case "server":
+		voteEntity.VoteCredits = true
+
 		var premium bool
-		err := state.Pool.QueryRow(ctx, "SELECT premium FROM servers WHERE server_id = $1", targetId).Scan(&premium)
+		err := c.QueryRow(ctx, "SELECT premium FROM servers WHERE server_id = $1", targetId).Scan(&premium)
 
 		if err != nil {
 			return nil, err
 		}
 
-		// Premium bots get vote time of 4
+		// Premium servers get vote time of 4
 		if premium {
-			defaultVoteEntity.VoteTime = 4
+			voteEntity.VoteTime = 4
+		} else {
+			// Server is not premium
+			if GetDoubleVote() {
+				voteEntity.PerUser = 2  // 2 votes per user
+				voteEntity.VoteTime = 6 // Half of the normal vote time
+			}
 		}
 	case "blog":
-		defaultVoteEntity.PerUser = 1 // Only 1 vote per blog post
-	        defaultVoteEntity.VoteTime = -1 // No revotes allowed
+		voteEntity.MultipleVotes = false
+		voteEntity.PerUser = 1 // Only 1 vote per blog post
+	case "team":
+		// Teams cannot be premium yet
+		if GetDoubleVote() {
+			voteEntity.PerUser = 2  // 2 votes per user
+			voteEntity.VoteTime = 6 // Half of the normal vote time
+		}
+	case "pack":
+		// Packs cannot be premium yet
+		if GetDoubleVote() {
+			voteEntity.PerUser = 2  // 2 votes per user
+			voteEntity.VoteTime = 6 // Half of the normal vote time
+		}
 	}
 
-	return &defaultVoteEntity, nil
+	return &voteEntity, nil
 }
 
 // Checks whether or not a user has voted for an entity
-func EntityVoteCheck(ctx context.Context, userId, targetId, targetType string) (*types.UserVote, error) {
-	vi, err := EntityVoteInfo(ctx, userId, targetId, targetType)
+func EntityVoteCheck(ctx context.Context, c DbConn, userId, targetId, targetType string) (*types.UserVote, error) {
+	vi, err := EntityVoteInfo(ctx, c, userId, targetId, targetType)
 
 	if err != nil {
 		return nil, err
 	}
 
-	rows, err := state.Pool.Query(
+	var rows pgx.Rows
+
+	rows, err = c.Query(
 		ctx,
-		"SELECT created_at, upvote FROM entity_votes WHERE author = $1 AND target_id = $2 AND target_type = $3 AND void = false AND NOW() - created_at < make_interval(hours => $4) ORDER BY created_at DESC",
+		"SELECT created_at, upvote FROM entity_votes WHERE author = $1 AND target_id = $2 AND target_type = $3 AND void = false ORDER BY created_at DESC",
 		userId,
 		targetId,
 		targetType,
-		vi.VoteTime,
 	)
 
 	if err != nil {
@@ -114,38 +140,46 @@ func EntityVoteCheck(ctx context.Context, userId, targetId, targetType string) (
 
 	var vw *types.VoteWait
 
-	if len(validVotes) > 0 && vi.VoteTime > 0 {
-		timeElapsed := time.Since(validVotes[0].CreatedAt)
+	// If there is a valid vote in this period and the entity supports multiple votes, figure out how long the user has to wait
+	var hasVoted bool
+
+	// Case 1: Multiple votes
+	if vi.MultipleVotes {
+		if len(validVotes) > 0 {
+			// Check if the user has voted in the last vote time
+			hasVoted = validVotes[0].CreatedAt.Add(time.Duration(vi.VoteTime) * time.Hour).After(time.Now())
 
-		timeToWait := int64(vi.VoteTime)*60*60*1000 - timeElapsed.Milliseconds()
+			if hasVoted {
+				timeElapsed := time.Since(validVotes[0].CreatedAt)
 
-		timeToWaitTime := (time.Duration(timeToWait) * time.Millisecond)
+				timeToWait := int64(vi.VoteTime)*60*60*1000 - timeElapsed.Milliseconds()
 
-		hours := timeToWaitTime / time.Hour
-		mins := (timeToWaitTime - (hours * time.Hour)) / time.Minute
-		secs := (timeToWaitTime - (hours*time.Hour + mins*time.Minute)) / time.Second
+				timeToWaitTime := (time.Duration(timeToWait) * time.Millisecond)
 
-		vw = &types.VoteWait{
-			Hours:   int(hours),
-			Minutes: int(mins),
-			Seconds: int(secs),
+				hours := timeToWaitTime / time.Hour
+				mins := (timeToWaitTime - (hours * time.Hour)) / time.Minute
+				secs := (timeToWaitTime - (hours*time.Hour + mins*time.Minute)) / time.Second
+
+				vw = &types.VoteWait{
+					Hours:   int(hours),
+					Minutes: int(mins),
+					Seconds: int(secs),
+				}
+			}
 		}
+	} else {
+		// Case 2: Single vote entity
+		hasVoted = len(validVotes) > 0
 	}
 
 	return &types.UserVote{
-		HasVoted:   len(validVotes) > 0,
+		HasVoted:   hasVoted,
 		ValidVotes: validVotes,
 		VoteInfo:   vi,
 		Wait:       vw,
 	}, nil
 }
 
-type DbConn interface {
-	QueryRow(ctx context.Context, sql string, args ...any) pgx.Row
-	Query(ctx context.Context, sql string, args ...any) (pgx.Rows, error)
-	Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error)
-}
-
 // Returns the exact (non-cached/approximate) vote count for an entity
 func EntityGetVoteCount(ctx context.Context, c DbConn, targetId, targetType string) (int, error) {
 	var upvotes int
diff --git a/votes/helpers.go b/votes/helpers.go
index a12e0e31..bfb91859 100644
--- a/votes/helpers.go
+++ b/votes/helpers.go
@@ -13,24 +13,21 @@ import (
 )
 
 type EntityInfo struct {
-	Name        string
-	URL         string
-	VoteURL     string
-	Avatar      string
-	VoteCredits bool
+	Name    string
+	URL     string
+	VoteURL string
+	Avatar  string
 }
 
 // GetEntityInfo returns information about the entity that is being voted for including vote bans etc.
-//
-// TODO: Refactor vote ban checks to its own function
-func GetEntityInfo(ctx context.Context, targetId, targetType string) (*EntityInfo, error) {
+func GetEntityInfo(ctx context.Context, c DbConn, targetId, targetType string) (*EntityInfo, error) {
 	// Handle entity specific checks here, such as ensuring the entity actually exists
 	switch targetType {
 	case "bot":
 		var botType string
 		var voteBanned bool
 
-		err := state.Pool.QueryRow(ctx, "SELECT type, vote_banned FROM bots WHERE bot_id = $1", targetId).Scan(&botType, &voteBanned)
+		err := c.QueryRow(ctx, "SELECT type, vote_banned FROM bots WHERE bot_id = $1", targetId).Scan(&botType, &voteBanned)
 
 		if errors.Is(err, pgx.ErrNoRows) {
 			return nil, errors.New("bot not found")
@@ -56,24 +53,23 @@ func GetEntityInfo(ctx context.Context, targetId, targetType string) (*EntityInf
 
 		// Set entityInfo for log
 		return &EntityInfo{
-			URL:         state.Config.Sites.Frontend.Parse() + "/bot/" + targetId,
-			VoteURL:     state.Config.Sites.Frontend.Parse() + "/bot/" + targetId + "/vote",
-			Name:        botObj.Username,
-			Avatar:      botObj.Avatar,
-                        VoteCredits: true,
+			URL:     state.Config.Sites.Frontend.Parse() + "/bot/" + targetId,
+			VoteURL: state.Config.Sites.Frontend.Parse() + "/bot/" + targetId + "/vote",
+			Name:    botObj.Username,
+			Avatar:  botObj.Avatar,
 		}, nil
 	case "pack":
 		return &EntityInfo{
 			URL:     state.Config.Sites.Frontend.Parse() + "/pack/" + targetId,
 			VoteURL: state.Config.Sites.Frontend.Parse() + "/pack/" + targetId,
 			Name:    targetId,
-			Avatar:  state.Config.Sites.CDN.Parse() + "/avatars/default.webp",
+			Avatar:  state.Config.Sites.CDN + "/avatars/default.webp",
 		}, nil
 	case "team":
 		var name string
 		var voteBanned bool
 
-		err := state.Pool.QueryRow(ctx, "SELECT name, vote_banned FROM teams WHERE id = $1", targetId).Scan(&name, &voteBanned)
+		err := c.QueryRow(ctx, "SELECT name, vote_banned FROM teams WHERE id = $1", targetId).Scan(&name, &voteBanned)
 
 		if errors.Is(err, pgx.ErrNoRows) {
 			return nil, errors.New("team not found")
@@ -108,7 +104,7 @@ func GetEntityInfo(ctx context.Context, targetId, targetType string) (*EntityInf
 		var name, avatar string
 		var voteBanned bool
 
-		err := state.Pool.QueryRow(ctx, "SELECT name, avatar, vote_banned FROM servers WHERE server_id = $1", targetId).Scan(&name, &avatar, &voteBanned)
+		err := c.QueryRow(ctx, "SELECT name, avatar, vote_banned FROM servers WHERE server_id = $1", targetId).Scan(&name, &avatar, &voteBanned)
 
 		if errors.Is(err, pgx.ErrNoRows) {
 			return nil, errors.New("server not found")
@@ -122,20 +118,19 @@ func GetEntityInfo(ctx context.Context, targetId, targetType string) (*EntityInf
 			return nil, errors.New("server is vote banned and cannot be voted for right now")
 		}
 
-   		// Set entityInfo for log
-                return &EntityInfo{
-			URL:         state.Config.Sites.Frontend.Parse()+ "/server/" + targetId,
-			VoteURL:     state.Config.Sites.Frontend.Parse() + "/server/" + targetId + "/vote",
-			Name:        name,
-			Avatar:      avatar,
-			VoteCredits: true,
+		// Set entityInfo for log
+		return &EntityInfo{
+			URL:     state.Config.Sites.Frontend.Parse() + "/server/" + targetId,
+			VoteURL: state.Config.Sites.Frontend.Parse() + "/server/" + targetId + "/vote",
+			Name:    name,
+			Avatar:  avatar,
 		}, nil
 	case "blog":
-	        return &EntityInfo{
+		return &EntityInfo{
 			URL:     state.Config.Sites.Frontend.Parse() + "/blog/" + targetId,
 			VoteURL: state.Config.Sites.Frontend.Parse() + "/blog/" + targetId,
 			Name:    targetId,
-			Avatar:  state.Config.Sites.CDN.Parse() + "/avatars/default.webp",
+			Avatar:  state.Config.Sites.CDN + "/avatars/default.webp",
 		}, nil
 	default:
 		return nil, errors.New("unimplemented target type:" + targetType)

From f816a625328a0ebbc1f978e36487dc8407e2a728 Mon Sep 17 00:00:00 2001
From: cheesycod <cheesycod@outlook.com>
Date: Sun, 9 Jun 2024 20:42:53 +0530
Subject: [PATCH 7/8] more bug fixes to handle vote changes

---
 .../endpoints/put_user_entity_votes/route.go  |  89 ++++++-----
 types/vote.go                                 |   5 +-
 votes/common.go                               | 150 +++++++++++++++++-
 votes/helpers.go                              | 138 ----------------
 4 files changed, 204 insertions(+), 178 deletions(-)
 delete mode 100644 votes/helpers.go

diff --git a/routes/votes/endpoints/put_user_entity_votes/route.go b/routes/votes/endpoints/put_user_entity_votes/route.go
index 4f1a8323..08aecda9 100644
--- a/routes/votes/endpoints/put_user_entity_votes/route.go
+++ b/routes/votes/endpoints/put_user_entity_votes/route.go
@@ -164,7 +164,20 @@ func Route(d uapi.RouteData, r *http.Request) uapi.HttpResponse {
 		}
 	}
 
-	entityInfo, err := votes.GetEntityInfo(d.Context, state.Pool, targetId, targetType)
+	// 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()},
+		}
+	}
+
+	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))
@@ -175,7 +188,7 @@ func Route(d uapi.RouteData, r *http.Request) uapi.HttpResponse {
 	}
 
 	// Now check the vote
-	vi, err := votes.EntityVoteCheck(d.Context, state.Pool, 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))
@@ -199,52 +212,53 @@ func Route(d uapi.RouteData, r *http.Request) uapi.HttpResponse {
 	if vi.HasVoted {
 		// If !Multiple Votes
 		if !vi.VoteInfo.MultipleVotes {
-			return uapi.HttpResponse{
-				Status: http.StatusBadRequest,
-				Json:   types.ApiError{Message: "You have already voted for this entity before!"},
+			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()},
+					}
+				}
 			}
-		}
-
-		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"
-		}
+			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 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 + "?"},
+				}
+			}
 
-		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 + "?"},
+				Json:   types.ApiError{Message: "Please wait " + timeStr + " before voting again"},
 			}
 		}
-
-		return uapi.HttpResponse{
-			Status: http.StatusBadRequest,
-			Json:   types.ApiError{Message: "Please wait " + timeStr + " before voting again"},
-		}
 	}
 
-	// Create a new entity vote
-	tx, err := state.Pool.Begin(d.Context)
+	// Keep adding votes until, but not including vi.VoteInfo.PerUser
+	err = votes.EntityGiveVotes(d.Context, tx, upvote, uid, targetType, targetId, vi.VoteInfo)
 
 	if err != nil {
-		state.Logger.Error("Failed to create transaction [put_user_entity_votes]", zap.Error(err))
+		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 create transaction: " + err.Error()},
-		}
-	}
-
-	defer tx.Rollback(d.Context)
-
-	// 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, i)
-
-		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.Bool("upvote", upvote))
-			return uapi.DefaultResponse(http.StatusInternalServerError)
+			Json:   types.ApiError{Message: "Failed to give votes: " + err.Error()},
 		}
 	}
 
@@ -253,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
diff --git a/types/vote.go b/types/vote.go
index 0bc860f9..2d3a92b7 100644
--- a/types/vote.go
+++ b/types/vote.go
@@ -42,8 +42,9 @@ 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
diff --git a/votes/common.go b/votes/common.go
index 8957ec8a..b44211cd 100644
--- a/votes/common.go
+++ b/votes/common.go
@@ -2,11 +2,18 @@ package votes
 
 import (
 	"context"
+	"errors"
+	"fmt"
+	"popplio/assetmanager"
+	"popplio/state"
 	"popplio/types"
+	"strconv"
 	"time"
 
+	"github.com/infinitybotlist/eureka/dovewing"
 	"github.com/jackc/pgx/v5"
 	"github.com/jackc/pgx/v5/pgconn"
+	"github.com/jackc/pgx/v5/pgtype"
 )
 
 type DbConn interface {
@@ -20,6 +27,131 @@ func GetDoubleVote() bool {
 	return weekday == time.Friday || weekday == time.Saturday || weekday == time.Sunday
 }
 
+type EntityInfo struct {
+	Name    string
+	URL     string
+	VoteURL string
+	Avatar  string
+}
+
+// GetEntityInfo returns information about the entity that is being voted for including vote bans etc.
+func GetEntityInfo(ctx context.Context, c DbConn, targetId, targetType string) (*EntityInfo, error) {
+	// Handle entity specific checks here, such as ensuring the entity actually exists
+	switch targetType {
+	case "bot":
+		var botType string
+		var voteBanned bool
+
+		err := c.QueryRow(ctx, "SELECT type, vote_banned FROM bots WHERE bot_id = $1", targetId).Scan(&botType, &voteBanned)
+
+		if errors.Is(err, pgx.ErrNoRows) {
+			return nil, errors.New("bot not found")
+		}
+
+		if err != nil {
+			return nil, fmt.Errorf("failed to fetch bot data for this vote: %w", err)
+		}
+
+		if voteBanned {
+			return nil, errors.New("bot is vote banned and cannot be voted for right now")
+		}
+
+		if botType != "approved" && botType != "certified" {
+			return nil, errors.New("bot is not approved or certified and cannot be voted for right now")
+		}
+
+		botObj, err := dovewing.GetUser(ctx, targetId, state.DovewingPlatformDiscord)
+
+		if err != nil {
+			return nil, err
+		}
+
+		// Set entityInfo for log
+		return &EntityInfo{
+			URL:     state.Config.Sites.Frontend.Parse() + "/bot/" + targetId,
+			VoteURL: state.Config.Sites.Frontend.Parse() + "/bot/" + targetId + "/vote",
+			Name:    botObj.Username,
+			Avatar:  botObj.Avatar,
+		}, nil
+	case "pack":
+		return &EntityInfo{
+			URL:     state.Config.Sites.Frontend.Parse() + "/pack/" + targetId,
+			VoteURL: state.Config.Sites.Frontend.Parse() + "/pack/" + targetId,
+			Name:    targetId,
+			Avatar:  state.Config.Sites.CDN + "/avatars/default.webp",
+		}, nil
+	case "team":
+		var name string
+		var voteBanned bool
+
+		err := c.QueryRow(ctx, "SELECT name, vote_banned FROM teams WHERE id = $1", targetId).Scan(&name, &voteBanned)
+
+		if errors.Is(err, pgx.ErrNoRows) {
+			return nil, errors.New("team not found")
+		}
+
+		if err != nil {
+			return nil, fmt.Errorf("failed to fetch team data for this vote: %w", err)
+		}
+
+		if voteBanned {
+			return nil, errors.New("team is vote banned and cannot be voted for right now")
+		}
+
+		avatar := assetmanager.AvatarInfo(assetmanager.AssetTargetTypeTeams, targetId)
+
+		var avatarPath string
+
+		if avatar.Exists {
+			avatarPath = state.Config.Sites.CDN + "/" + avatar.Path + "?ts=" + strconv.FormatInt(avatar.LastModified.Unix(), 10)
+		} else {
+			avatarPath = state.Config.Sites.CDN + "/" + avatar.DefaultPath
+		}
+
+		// Set entityInfo for log
+		return &EntityInfo{
+			URL:     state.Config.Sites.Frontend.Parse() + "/team/" + targetId,
+			VoteURL: state.Config.Sites.Frontend.Parse() + "/team/" + targetId + "/vote",
+			Name:    name,
+			Avatar:  avatarPath,
+		}, nil
+	case "server":
+		var name, avatar string
+		var voteBanned bool
+
+		err := c.QueryRow(ctx, "SELECT name, avatar, vote_banned FROM servers WHERE server_id = $1", targetId).Scan(&name, &avatar, &voteBanned)
+
+		if errors.Is(err, pgx.ErrNoRows) {
+			return nil, errors.New("server not found")
+		}
+
+		if err != nil {
+			return nil, fmt.Errorf("failed to fetch server data for this vote: %w", err)
+		}
+
+		if voteBanned {
+			return nil, errors.New("server is vote banned and cannot be voted for right now")
+		}
+
+		// Set entityInfo for log
+		return &EntityInfo{
+			URL:     state.Config.Sites.Frontend.Parse() + "/server/" + targetId,
+			VoteURL: state.Config.Sites.Frontend.Parse() + "/server/" + targetId + "/vote",
+			Name:    name,
+			Avatar:  avatar,
+		}, nil
+	case "blog":
+		return &EntityInfo{
+			URL:     state.Config.Sites.Frontend.Parse() + "/blog/" + targetId,
+			VoteURL: state.Config.Sites.Frontend.Parse() + "/blog/" + targetId,
+			Name:    targetId,
+			Avatar:  state.Config.Sites.CDN + "/avatars/default.webp",
+		}, nil
+	default:
+		return nil, errors.New("unimplemented target type:" + targetType)
+	}
+}
+
 // Returns core vote info about the entity (such as the amount of cooldown time the entity has)
 //
 // # If user id is specified, then in the future special perks for the user will be returned as well
@@ -110,7 +242,7 @@ func EntityVoteCheck(ctx context.Context, c DbConn, userId, targetId, targetType
 
 	rows, err = c.Query(
 		ctx,
-		"SELECT created_at, upvote FROM entity_votes WHERE author = $1 AND target_id = $2 AND target_type = $3 AND void = false ORDER BY created_at DESC",
+		"SELECT itag, created_at, upvote FROM entity_votes WHERE author = $1 AND target_id = $2 AND target_type = $3 AND void = false ORDER BY created_at DESC",
 		userId,
 		targetId,
 		targetType,
@@ -123,16 +255,18 @@ func EntityVoteCheck(ctx context.Context, c DbConn, userId, targetId, targetType
 	var validVotes []*types.ValidVote
 
 	for rows.Next() {
+		var itag pgtype.UUID
 		var createdAt time.Time
 		var upvote bool
 
-		err = rows.Scan(&createdAt, &upvote)
+		err = rows.Scan(&itag, &createdAt, &upvote)
 
 		if err != nil {
 			return nil, err
 		}
 
 		validVotes = append(validVotes, &types.ValidVote{
+			ID:        itag,
 			Upvote:    upvote,
 			CreatedAt: createdAt,
 		})
@@ -202,3 +336,15 @@ func EntityGetVoteCount(ctx context.Context, c DbConn, targetId, targetType stri
 
 	return upvotes - downvotes, nil
 }
+
+func EntityGiveVotes(ctx context.Context, c DbConn, upvote bool, author, targetType, targetId string, vi *types.VoteInfo) error {
+	// Keep adding votes until, but not including vi.VoteInfo.PerUser
+	for i := 0; i < vi.PerUser; i++ {
+		_, err := c.Exec(ctx, "INSERT INTO entity_votes (author, target_id, target_type, upvote, vote_num) VALUES ($1, $2, $3, $4, $5)", author, targetId, targetType, upvote, i)
+
+		if err != nil {
+			return fmt.Errorf("failed to insert vote: %w", err)
+		}
+	}
+	return nil
+}
diff --git a/votes/helpers.go b/votes/helpers.go
deleted file mode 100644
index bfb91859..00000000
--- a/votes/helpers.go
+++ /dev/null
@@ -1,138 +0,0 @@
-package votes
-
-import (
-	"context"
-	"errors"
-	"fmt"
-	"popplio/assetmanager"
-	"popplio/state"
-	"strconv"
-
-	"github.com/infinitybotlist/eureka/dovewing"
-	"github.com/jackc/pgx/v5"
-)
-
-type EntityInfo struct {
-	Name    string
-	URL     string
-	VoteURL string
-	Avatar  string
-}
-
-// GetEntityInfo returns information about the entity that is being voted for including vote bans etc.
-func GetEntityInfo(ctx context.Context, c DbConn, targetId, targetType string) (*EntityInfo, error) {
-	// Handle entity specific checks here, such as ensuring the entity actually exists
-	switch targetType {
-	case "bot":
-		var botType string
-		var voteBanned bool
-
-		err := c.QueryRow(ctx, "SELECT type, vote_banned FROM bots WHERE bot_id = $1", targetId).Scan(&botType, &voteBanned)
-
-		if errors.Is(err, pgx.ErrNoRows) {
-			return nil, errors.New("bot not found")
-		}
-
-		if err != nil {
-			return nil, fmt.Errorf("failed to fetch bot data for this vote: %w", err)
-		}
-
-		if voteBanned {
-			return nil, errors.New("bot is vote banned and cannot be voted for right now")
-		}
-
-		if botType != "approved" && botType != "certified" {
-			return nil, errors.New("bot is not approved or certified and cannot be voted for right now")
-		}
-
-		botObj, err := dovewing.GetUser(ctx, targetId, state.DovewingPlatformDiscord)
-
-		if err != nil {
-			return nil, err
-		}
-
-		// Set entityInfo for log
-		return &EntityInfo{
-			URL:     state.Config.Sites.Frontend.Parse() + "/bot/" + targetId,
-			VoteURL: state.Config.Sites.Frontend.Parse() + "/bot/" + targetId + "/vote",
-			Name:    botObj.Username,
-			Avatar:  botObj.Avatar,
-		}, nil
-	case "pack":
-		return &EntityInfo{
-			URL:     state.Config.Sites.Frontend.Parse() + "/pack/" + targetId,
-			VoteURL: state.Config.Sites.Frontend.Parse() + "/pack/" + targetId,
-			Name:    targetId,
-			Avatar:  state.Config.Sites.CDN + "/avatars/default.webp",
-		}, nil
-	case "team":
-		var name string
-		var voteBanned bool
-
-		err := c.QueryRow(ctx, "SELECT name, vote_banned FROM teams WHERE id = $1", targetId).Scan(&name, &voteBanned)
-
-		if errors.Is(err, pgx.ErrNoRows) {
-			return nil, errors.New("team not found")
-		}
-
-		if err != nil {
-			return nil, fmt.Errorf("failed to fetch team data for this vote: %w", err)
-		}
-
-		if voteBanned {
-			return nil, errors.New("team is vote banned and cannot be voted for right now")
-		}
-
-		avatar := assetmanager.AvatarInfo(assetmanager.AssetTargetTypeTeams, targetId)
-
-		var avatarPath string
-
-		if avatar.Exists {
-			avatarPath = state.Config.Sites.CDN + "/" + avatar.Path + "?ts=" + strconv.FormatInt(avatar.LastModified.Unix(), 10)
-		} else {
-			avatarPath = state.Config.Sites.CDN + "/" + avatar.DefaultPath
-		}
-
-		// Set entityInfo for log
-		return &EntityInfo{
-			URL:     state.Config.Sites.Frontend.Parse() + "/team/" + targetId,
-			VoteURL: state.Config.Sites.Frontend.Parse() + "/team/" + targetId + "/vote",
-			Name:    name,
-			Avatar:  avatarPath,
-		}, nil
-	case "server":
-		var name, avatar string
-		var voteBanned bool
-
-		err := c.QueryRow(ctx, "SELECT name, avatar, vote_banned FROM servers WHERE server_id = $1", targetId).Scan(&name, &avatar, &voteBanned)
-
-		if errors.Is(err, pgx.ErrNoRows) {
-			return nil, errors.New("server not found")
-		}
-
-		if err != nil {
-			return nil, fmt.Errorf("failed to fetch server data for this vote: %w", err)
-		}
-
-		if voteBanned {
-			return nil, errors.New("server is vote banned and cannot be voted for right now")
-		}
-
-		// Set entityInfo for log
-		return &EntityInfo{
-			URL:     state.Config.Sites.Frontend.Parse() + "/server/" + targetId,
-			VoteURL: state.Config.Sites.Frontend.Parse() + "/server/" + targetId + "/vote",
-			Name:    name,
-			Avatar:  avatar,
-		}, nil
-	case "blog":
-		return &EntityInfo{
-			URL:     state.Config.Sites.Frontend.Parse() + "/blog/" + targetId,
-			VoteURL: state.Config.Sites.Frontend.Parse() + "/blog/" + targetId,
-			Name:    targetId,
-			Avatar:  state.Config.Sites.CDN + "/avatars/default.webp",
-		}, nil
-	default:
-		return nil, errors.New("unimplemented target type:" + targetType)
-	}
-}

From 6940e76ba3b56adbf9e2af8bdc77b7495678508c Mon Sep 17 00:00:00 2001
From: cheesycod <cheesycod@outlook.com>
Date: Sun, 9 Jun 2024 20:43:44 +0530
Subject: [PATCH 8/8] improve comment

---
 routes/votes/endpoints/put_user_entity_votes/route.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/routes/votes/endpoints/put_user_entity_votes/route.go b/routes/votes/endpoints/put_user_entity_votes/route.go
index 08aecda9..a6836d4a 100644
--- a/routes/votes/endpoints/put_user_entity_votes/route.go
+++ b/routes/votes/endpoints/put_user_entity_votes/route.go
@@ -210,7 +210,7 @@ func Route(d uapi.RouteData, r *http.Request) uapi.HttpResponse {
 	}
 
 	if vi.HasVoted {
-		// If !Multiple Votes
+		// 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{