diff --git a/cmd/api/server.go b/cmd/api/server.go index 2f12625..c33cadd 100644 --- a/cmd/api/server.go +++ b/cmd/api/server.go @@ -233,8 +233,9 @@ func initializeRoutes(engine *gin.Engine) { engine.GET("/v2/ranking/queue/:id", handlers.CreateHandler(handlers.GetRankingQueueMapset)) engine.POST("/v2/ranking/queue/:id/submit", middleware.RequireAuth, handlers.CreateHandler(handlers.SubmitMapsetToRankingQueue)) engine.POST("/v2/ranking/queue/:id/remove", middleware.RequireAuth, handlers.CreateHandler(handlers.RemoveFromRankingQueue)) - engine.GET("/v2/ranking/queue/:id/comments", handlers.CreateHandler(handlers.GetRankingQueueComments)) + engine.GET("/v2/ranking/queue/:id/comments", middleware.AllowAuth, handlers.CreateHandler(handlers.GetRankingQueueComments)) engine.POST("/v2/ranking/queue/:id/comment", middleware.RequireAuth, handlers.CreateHandler(handlers.AddRankingQueueComment)) + engine.POST("/v2/ranking/queue/:id/private-comment", middleware.RequireAuth, handlers.CreateHandler(handlers.AddPrivateRankingQueueComment)) engine.POST("/v2/ranking/queue/comment/:id/edit", middleware.RequireAuth, handlers.CreateHandler(handlers.EditRankingQueueComment)) engine.POST("/v2/ranking/queue/:id/vote", middleware.RequireAuth, handlers.CreateHandler(handlers.VoteForRankingQueueMapset)) engine.POST("/v2/ranking/queue/:id/deny", middleware.RequireAuth, handlers.CreateHandler(handlers.DenyRankingQueueMapset)) diff --git a/cmd/database/migrations/29_private_ranking_messages.down.sql b/cmd/database/migrations/29_private_ranking_messages.down.sql new file mode 100644 index 0000000..a9f4318 --- /dev/null +++ b/cmd/database/migrations/29_private_ranking_messages.down.sql @@ -0,0 +1,5 @@ +ALTER TABLE mapset_ranking_queue_comments + DROP COLUMN is_anonymous; + +ALTER TABLE mapset_ranking_queue_comments + DROP COLUMN is_private; diff --git a/cmd/database/migrations/29_private_ranking_messages.up.sql b/cmd/database/migrations/29_private_ranking_messages.up.sql new file mode 100644 index 0000000..9e2f5bd --- /dev/null +++ b/cmd/database/migrations/29_private_ranking_messages.up.sql @@ -0,0 +1,5 @@ +ALTER TABLE mapset_ranking_queue_comments + ADD COLUMN is_anonymous TINYINT(1) NOT NULL DEFAULT 0 AFTER game_mode; + +ALTER TABLE mapset_ranking_queue_comments + ADD COLUMN is_private TINYINT(1) NOT NULL DEFAULT 0 AFTER game_mode; diff --git a/db/mapset_ranking_queue_comments.go b/db/mapset_ranking_queue_comments.go index e084e44..884bf75 100644 --- a/db/mapset_ranking_queue_comments.go +++ b/db/mapset_ranking_queue_comments.go @@ -1,9 +1,10 @@ package db import ( + "time" + "github.com/Quaver/api2/enums" "gorm.io/gorm" - "time" ) type RankingQueueAction int8 @@ -29,7 +30,10 @@ type MapsetRankingQueueComment struct { DateLastUpdated int64 `gorm:"date_last_updated" json:"-"` DateLastUpdatedJSON time.Time `gorm:"-:all" json:"date_last_updated"` GameMode *enums.GameMode `gorm:"column:game_mode" json:"game_mode"` + IsPrivate bool `gorm:"column:is_private" json:"is_private"` + IsAnonymous bool `gorm:"column:is_anonymous" json:"is_anonymous"` User *User `gorm:"foreignKey:UserId; references:Id" json:"user,omitempty"` + AnonymousAuthor *User `gorm:"-" json:"anonymous_author,omitempty"` } func (*MapsetRankingQueueComment) TableName() string { @@ -73,12 +77,12 @@ func (c *MapsetRankingQueueComment) Edit(comment string) error { } // GetRankingQueueComments Retrieves the ranking queue comments for a given mapset -func GetRankingQueueComments(mapsetId int) ([]*MapsetRankingQueueComment, error) { +func GetRankingQueueComments(mapsetId int, includePrivate bool) ([]*MapsetRankingQueueComment, error) { var comments = make([]*MapsetRankingQueueComment, 0) result := SQL. Joins("User"). - Where("mapset_id = ?", mapsetId). + Where("mapset_id = ? AND is_private = ?", mapsetId, includePrivate). Order("id DESC"). Find(&comments) diff --git a/db/user_notifications.go b/db/user_notifications.go index d7dbcc1..eee91c2 100644 --- a/db/user_notifications.go +++ b/db/user_notifications.go @@ -208,8 +208,13 @@ func NewMapsetRankedNotification(mapset *Mapset) *UserNotification { // Returns a new mapset ranking queue action notification func NewMapsetActionNotification(mapset *Mapset, comment *MapsetRankingQueueComment) *UserNotification { + senderId := comment.UserId + if comment.IsAnonymous { + senderId = QuaverBotId + } + notif := &UserNotification{ - SenderId: comment.UserId, + SenderId: senderId, ReceiverId: mapset.CreatorID, Type: NotificationMapsetAction, Category: NotificationCategoryRankingQueue, diff --git a/handlers/ranking.go b/handlers/ranking.go index 2059b91..8a52793 100644 --- a/handlers/ranking.go +++ b/handlers/ranking.go @@ -57,6 +57,11 @@ func GetRankingQueue(c *gin.Context) *APIError { return APIErrorServerError("Error retrieving ranking queue count", err) } + for _, mapset := range rankingQueue { + mapset.Votes = prepareRankingQueueCommentsForResponse(mapset.Votes, false) + mapset.Denies = prepareRankingQueueCommentsForResponse(mapset.Denies, false) + } + c.JSON(http.StatusOK, gin.H{ "count": count, "ranking_queue": rankingQueue, diff --git a/handlers/ranking_actions.go b/handlers/ranking_actions.go index fd934fc..566cfc2 100644 --- a/handlers/ranking_actions.go +++ b/handlers/ranking_actions.go @@ -19,6 +19,7 @@ type rankingQueueRequestData struct { QueueMapset *db.RankingQueueMapset Comment string GameMode enums.GameMode + Anonymous bool } // Validates and returns common data used for ranking queue action requests @@ -36,8 +37,9 @@ func validateRankingQueueRequest(c *gin.Context) (*rankingQueueRequestData, *API } body := struct { - Comment string `form:"comment" json:"comment" binding:"required"` - GameMode enums.GameMode `form:"game_mode" json:"game_mode"` + Comment string `form:"comment" json:"comment" binding:"required"` + GameMode enums.GameMode `form:"game_mode" json:"game_mode"` + Anonymous bool `form:"anonymous" json:"anonymous"` }{} if err := c.ShouldBind(&body); err != nil { @@ -52,6 +54,10 @@ func validateRankingQueueRequest(c *gin.Context) (*rankingQueueRequestData, *API return nil, APIErrorForbidden("You do not have permission to perform this action.") } + if body.Anonymous && !canUserAccessSupervisorRoute(c) { + return nil, APIErrorForbidden("Only ranking supervisors can take ranking actions anonymously.") + } + queueMapset, err := db.GetRankingQueueMapset(id) if err != nil && err != gorm.ErrRecordNotFound { @@ -68,6 +74,7 @@ func validateRankingQueueRequest(c *gin.Context) (*rankingQueueRequestData, *API QueueMapset: queueMapset, Comment: body.Comment, GameMode: body.GameMode, + Anonymous: body.Anonymous, }, nil } @@ -111,13 +118,14 @@ func VoteForRankingQueueMapset(c *gin.Context) *APIError { } newVoteAction := &db.MapsetRankingQueueComment{ - UserId: data.User.Id, - User: data.User, - MapsetId: data.MapsetId, - ActionType: db.RankingQueueActionVote, - IsActive: true, - Comment: data.Comment, - GameMode: &data.GameMode, + UserId: data.User.Id, + User: data.User, + MapsetId: data.MapsetId, + ActionType: db.RankingQueueActionVote, + IsActive: true, + Comment: data.Comment, + GameMode: &data.GameMode, + IsAnonymous: data.Anonymous, } existingVotes = append(existingVotes, newVoteAction) @@ -168,7 +176,7 @@ func VoteForRankingQueueMapset(c *gin.Context) *APIError { return APIErrorServerError("Error updating vote count for queue mapset", err) } - _ = webhooks.SendQueueWebhook(data.User, queueMapset.Mapset, db.RankingQueueActionVote) + sendRankingQueueActionWebhook(data, db.RankingQueueActionVote) c.JSON(http.StatusOK, gin.H{"message": "You have successfully added a vote to this mapset."}) return nil } @@ -209,12 +217,13 @@ func DenyRankingQueueMapset(c *gin.Context) *APIError { } denyAction := &db.MapsetRankingQueueComment{ - UserId: data.User.Id, - MapsetId: data.MapsetId, - ActionType: db.RankingQueueActionDeny, - IsActive: true, - Comment: data.Comment, - GameMode: &data.GameMode, + UserId: data.User.Id, + MapsetId: data.MapsetId, + ActionType: db.RankingQueueActionDeny, + IsActive: true, + Comment: data.Comment, + GameMode: &data.GameMode, + IsAnonymous: data.Anonymous, } if err := denyAction.Insert(); err != nil { @@ -240,7 +249,7 @@ func DenyRankingQueueMapset(c *gin.Context) *APIError { } } - _ = webhooks.SendQueueWebhook(data.User, queueMapset.Mapset, db.RankingQueueActionDeny) + sendRankingQueueActionWebhook(data, db.RankingQueueActionDeny) c.JSON(http.StatusOK, gin.H{"message": "You have successfully added a deny to this mapset."}) return nil } @@ -265,12 +274,13 @@ func BlacklistRankingQueueMapset(c *gin.Context) *APIError { } blacklistAction := &db.MapsetRankingQueueComment{ - UserId: data.User.Id, - MapsetId: data.MapsetId, - ActionType: db.RankingQueueActionBlacklist, - IsActive: true, - Comment: data.Comment, - GameMode: &data.GameMode, + UserId: data.User.Id, + MapsetId: data.MapsetId, + ActionType: db.RankingQueueActionBlacklist, + IsActive: true, + Comment: data.Comment, + GameMode: &data.GameMode, + IsAnonymous: data.Anonymous, } if err := blacklistAction.Insert(); err != nil { @@ -289,7 +299,7 @@ func BlacklistRankingQueueMapset(c *gin.Context) *APIError { return APIErrorServerError("Error updating vote count for queue mapset", err) } - _ = webhooks.SendQueueWebhook(data.User, queueMapset.Mapset, db.RankingQueueActionBlacklist) + sendRankingQueueActionWebhook(data, db.RankingQueueActionBlacklist) c.JSON(http.StatusOK, gin.H{"message": "You have successfully blacklisted this mapset."}) return nil } @@ -318,12 +328,13 @@ func OnHoldRankingQueueMapset(c *gin.Context) *APIError { } onHoldAction := &db.MapsetRankingQueueComment{ - UserId: data.User.Id, - MapsetId: data.MapsetId, - ActionType: db.RankingQueueActionOnHold, - IsActive: true, - Comment: data.Comment, - GameMode: &data.GameMode, + UserId: data.User.Id, + MapsetId: data.MapsetId, + ActionType: db.RankingQueueActionOnHold, + IsActive: true, + Comment: data.Comment, + GameMode: &data.GameMode, + IsAnonymous: data.Anonymous, } if err := onHoldAction.Insert(); err != nil { @@ -342,7 +353,16 @@ func OnHoldRankingQueueMapset(c *gin.Context) *APIError { return APIErrorServerError("Error updating vote count for queue mapset", err) } - _ = webhooks.SendQueueWebhook(data.User, queueMapset.Mapset, db.RankingQueueActionOnHold) + sendRankingQueueActionWebhook(data, db.RankingQueueActionOnHold) c.JSON(http.StatusOK, gin.H{"message": "You have successfully placed this mapset on hold."}) return nil } + +func sendRankingQueueActionWebhook(data *rankingQueueRequestData, action db.RankingQueueAction) { + if data.Anonymous { + _ = webhooks.SendAnonymousQueueWebhook(data.QueueMapset.Mapset, action) + return + } + + _ = webhooks.SendQueueWebhook(data.User, data.QueueMapset.Mapset, action) +} diff --git a/handlers/ranking_comments.go b/handlers/ranking_comments.go index 511be48..f23e221 100644 --- a/handlers/ranking_comments.go +++ b/handlers/ranking_comments.go @@ -1,13 +1,14 @@ package handlers import ( + "net/http" + "strconv" + "github.com/Quaver/api2/db" "github.com/Quaver/api2/enums" "github.com/Quaver/api2/webhooks" "github.com/gin-gonic/gin" "gorm.io/gorm" - "net/http" - "strconv" ) // GetRankingQueueComments Returns all of the comments for a mapset in the ranking queue @@ -19,19 +20,69 @@ func GetRankingQueueComments(c *gin.Context) *APIError { return APIErrorBadRequest("You must provide a valid mapset id") } - comments, err := db.GetRankingQueueComments(id) + canViewPrivate := canUserAccessSupervisorRoute(c) + comments, err := db.GetRankingQueueComments(id, canViewPrivate) if err != nil { return APIErrorServerError("Error getting ranking queue comments", err) } - c.JSON(http.StatusOK, gin.H{"comments": comments}) + c.JSON(http.StatusOK, gin.H{"comments": prepareRankingQueueCommentsForResponse(comments, canViewPrivate)}) return nil } +func prepareRankingQueueCommentsForResponse(comments []*db.MapsetRankingQueueComment, canViewPrivate bool) []*db.MapsetRankingQueueComment { + prepared := make([]*db.MapsetRankingQueueComment, 0, len(comments)) + + for _, comment := range comments { + if !canViewPrivate { + continue + } + + if comment.IsAnonymous { + avatarUrl := webhooks.QuaverLogo + redacted := *comment + redacted.User = &db.User{ + Id: db.QuaverBotId, + Username: "QuaverBot", + UserGroups: enums.UserGroupBot, + AvatarUrl: &avatarUrl, + } + + if canViewPrivate { + redacted.AnonymousAuthor = comment.User + } else { + redacted.UserId = db.QuaverBotId + } + + prepared = append(prepared, &redacted) + continue + } + + prepared = append(prepared, comment) + } + + return prepared +} + // AddRankingQueueComment Inserts a ranking queue comment to the database // Endpoint: POST /v2/ranking/queue/:id/comment func AddRankingQueueComment(c *gin.Context) *APIError { + return addRankingQueueComment(c, db.RankingQueueActionComment, false) +} + +// AddPrivateRankingQueueComment inserts an attributed comment only ranking supervisors can retrieve. +// Endpoint: POST /v2/ranking/queue/:id/private-comment +func AddPrivateRankingQueueComment(c *gin.Context) *APIError { + canUsePrivate := canUserAccessSupervisorRoute(c) + if !canUsePrivate { + return APIErrorForbidden("Only ranking supervisors can add a private ranking comment.") + } + + return addRankingQueueComment(c, db.RankingQueueActionComment, true) +} + +func addRankingQueueComment(c *gin.Context, action db.RankingQueueAction, isPrivate bool) *APIError { id, err := strconv.Atoi(c.Param("id")) if err != nil { @@ -72,23 +123,33 @@ func AddRankingQueueComment(c *gin.Context) *APIError { } comment := &db.MapsetRankingQueueComment{ - UserId: user.Id, - MapsetId: queueMapset.MapsetId, - Comment: body.Comment, - GameMode: &body.GameMode, - IsActive: true, + UserId: user.Id, + MapsetId: queueMapset.MapsetId, + ActionType: action, + Comment: body.Comment, + GameMode: &body.GameMode, + IsPrivate: isPrivate, + IsActive: true, } if err := comment.Insert(); err != nil { return APIErrorServerError("Error inserting comment into DB", err) } - if err := db.NewMapsetActionNotification(queueMapset.Mapset, comment).Insert(); err != nil { - return APIErrorServerError("Error inserting comment notification", err) + if action == db.RankingQueueActionComment { + if err := db.NewMapsetActionNotification(queueMapset.Mapset, comment).Insert(); err != nil { + return APIErrorServerError("Error inserting comment notification", err) + } + + _ = webhooks.SendQueueWebhook(user, queueMapset.Mapset, db.RankingQueueActionComment) + } + + message := "Your comment has been successfully added." + if isPrivate { + message = "Your private ranking comment has been successfully added." } - _ = webhooks.SendQueueWebhook(user, queueMapset.Mapset, db.RankingQueueActionComment) - c.JSON(http.StatusOK, gin.H{"message": "Your comment has been successfully added."}) + c.JSON(http.StatusOK, gin.H{"message": message}) return nil } diff --git a/webhooks/webhooks.go b/webhooks/webhooks.go index 5a6d000..23921b8 100644 --- a/webhooks/webhooks.go +++ b/webhooks/webhooks.go @@ -124,6 +124,19 @@ func SendQueueWebhook(user *db.User, mapset *db.Mapset, action db.RankingQueueAc return nil } +// SendAnonymousQueueWebhook sends a ranking action as QuaverBot without exposing its author. +func SendAnonymousQueueWebhook(mapset *db.Mapset, action db.RankingQueueAction) error { + avatarUrl := QuaverLogo + quaverBot := &db.User{ + Id: db.QuaverBotId, + Username: "QuaverBot", + UserGroups: enums.UserGroupBot, + AvatarUrl: &avatarUrl, + } + + return SendQueueWebhook(quaverBot, mapset, action) +} + // SendRankedWebhook Sends a webhook that a new mapset was ranked func SendRankedWebhook(mapset *db.Mapset, votes []*db.MapsetRankingQueueComment) error { if rankedMapsets == nil { @@ -133,7 +146,14 @@ func SendRankedWebhook(mapset *db.Mapset, votes []*db.MapsetRankingQueueComment) votedBy := "" for index, voter := range votes { - votedBy += fmt.Sprintf("[%v](https://quavergame.com/user/%v)", voter.User.Username, voter.UserId) + username := voter.User.Username + userId := voter.UserId + if voter.IsAnonymous { + username = "QuaverBot" + userId = db.QuaverBotId + } + + votedBy += fmt.Sprintf("[%v](https://quavergame.com/user/%v)", username, userId) if index != len(votes)-1 { votedBy += ", "