Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cmd/api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,8 @@ func initializeRoutes(engine *gin.Engine) {
engine.POST("/v2/map/:id/mods", middleware.RequireAuth, handlers.CreateHandler(handlers.SubmitMapMod))
engine.POST("/v2/map/:id/mods/:mod_id/status", middleware.RequireAuth, handlers.CreateHandler(handlers.UpdateMapModStatus))
engine.POST("/v2/map/:id/mods/:mod_id/comment", middleware.RequireAuth, handlers.CreateHandler(handlers.SubmitMapModComment))
engine.POST("/v2/map/mod/:id/edit", middleware.RequireAuth, handlers.CreateHandler(handlers.EditMapMod))
engine.POST("/v2/map/mod/comment/:id/edit", middleware.RequireAuth, handlers.CreateHandler(handlers.EditMapModComment))

// Mapsets
engine.GET("/v2/mapset/search", handlers.CreateHandler(handlers.GetMapsetsSearch))
Expand Down
15 changes: 15 additions & 0 deletions db/map_mods.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,21 @@ func (mod *MapMod) Insert() error {
return nil
}

// Edit Updates the editable content of a pending map mod.
func (mod *MapMod) Edit(comment string, mapTimestamp *string) error {
mod.Comment = comment
mod.MapTimestamp = mapTimestamp

result := SQL.Model(&MapMod{}).
Where("id = ?", mod.Id).
Updates(map[string]interface{}{
"comment": mod.Comment,
"map_timestamp": mod.MapTimestamp,
})

return result.Error
}

// UpdateStatus Updates the status of a map mod
func (mod *MapMod) UpdateStatus(status MapModStatus) error {
mod.Status = status
Expand Down
26 changes: 26 additions & 0 deletions db/map_mods_comments.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,29 @@ func (comment *MapModComment) Insert() error {

return nil
}

// GetMapModCommentById Gets a map mod comment by its id.
func GetMapModCommentById(id int) (*MapModComment, error) {
var comment *MapModComment

result := SQL.
Where("id = ?", id).
First(&comment)

if result.Error != nil {
return nil, result.Error
}

return comment, nil
}

// Edit Updates the content of a map mod comment.
func (comment *MapModComment) Edit(content string) error {
comment.Comment = content

result := SQL.Model(&MapModComment{}).
Where("id = ?", comment.Id).
Update("comment", comment.Comment)

return result.Error
}
148 changes: 148 additions & 0 deletions handlers/map_mods.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,154 @@ func SubmitMapModComment(c *gin.Context) *APIError {
return nil
}

// EditMapMod Edits the content of a pending map mod.
// Endpoint: POST /v2/map/mod/:id/edit
func EditMapMod(c *gin.Context) *APIError {
modId, err := strconv.Atoi(c.Param("id"))

if err != nil {
return APIErrorBadRequest("Invalid mod id")
}

user := getAuthedUser(c)

if user == nil {
return nil
}

body := struct {
MapTimestamp *string `form:"map_timestamp" json:"map_timestamp"`
Comment string `form:"comment" json:"comment" binding:"required"`
}{}

if err := c.ShouldBind(&body); err != nil {
return APIErrorBadRequest("Invalid request body")
}

if apiErr := validateEditableMapModComment(body.Comment); apiErr != nil {
return apiErr
}

var apiErr *APIError
body.MapTimestamp, apiErr = normalizeEditableMapModTimestamp(body.MapTimestamp)

if apiErr != nil {
return apiErr
}

mod, err := db.GetModById(modId)

if err != nil && err != gorm.ErrRecordNotFound {
return APIErrorServerError("Error retrieving map mod from database", err)
}

if mod == nil {
return APIErrorNotFound("Mod")
}

if mod.AuthorId != user.Id {
return APIErrorForbidden("You are not the author of this mod.")
}

if mod.Status != db.ModStatusPending {
return APIErrorForbidden("You can only edit pending mods.")
}

if err := mod.Edit(body.Comment, body.MapTimestamp); err != nil {
return APIErrorServerError("Error updating map mod in the database", err)
}

c.JSON(http.StatusOK, gin.H{"message": "Your mod has been successfully edited."})
return nil
}

// EditMapModComment Edits the content of a comment on a pending map mod.
// Endpoint: POST /v2/map/mod/comment/:id/edit
func EditMapModComment(c *gin.Context) *APIError {
commentId, err := strconv.Atoi(c.Param("id"))

if err != nil {
return APIErrorBadRequest("Invalid comment id")
}

user := getAuthedUser(c)

if user == nil {
return nil
}

body := struct {
Comment string `form:"comment" json:"comment" binding:"required"`
}{}

if err := c.ShouldBind(&body); err != nil {
return APIErrorBadRequest("Invalid request body")
}

if apiErr := validateEditableMapModComment(body.Comment); apiErr != nil {
return apiErr
}

comment, err := db.GetMapModCommentById(commentId)

if err != nil && err != gorm.ErrRecordNotFound {
return APIErrorServerError("Error retrieving map mod comment from database", err)
}

if comment == nil {
return APIErrorNotFound("Comment")
}

if comment.AuthorId != user.Id {
return APIErrorForbidden("You are not the author of this comment.")
}

mod, err := db.GetModById(comment.MapModId)

if err != nil && err != gorm.ErrRecordNotFound {
return APIErrorServerError("Error retrieving map mod from database", err)
}

if mod == nil {
return APIErrorNotFound("Mod")
}

if mod.Status != db.ModStatusPending {
return APIErrorForbidden("You can only edit comments on pending mods.")
}

if err := comment.Edit(body.Comment); err != nil {
return APIErrorServerError("Error updating map mod comment in the database", err)
}

c.JSON(http.StatusOK, gin.H{"message": "Your comment has been successfully edited."})
return nil
}

func validateEditableMapModComment(comment string) *APIError {
if len(comment) == 0 || len(comment) > 5000 {
return APIErrorBadRequest("Your comment must be between 1 and 5,000 characters")
}

return nil
}

func normalizeEditableMapModTimestamp(timestamp *string) (*string, *APIError) {
if timestamp == nil || len(*timestamp) == 0 {
return nil, nil
}

if len(*timestamp) > 5000 {
return nil, APIErrorBadRequest("The map timestamp can't be greater than 5,000 characters")
}

if !isMapTimestampValid(*timestamp) {
return nil, APIErrorBadRequest("You have provided an invalid map timestamp")
}

return timestamp, nil
}

// Returns if a map timestamp has valid syntax
// Time OR Time|Lane,Time|Lane,...
func isMapTimestampValid(str string) bool {
Expand Down
72 changes: 72 additions & 0 deletions handlers/map_mods_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package handlers

import "testing"

func TestValidateEditableMapModComment(t *testing.T) {
tests := []struct {
name string
comment string
valid bool
}{
{name: "valid", comment: "Updated explanation", valid: true},
{name: "empty", comment: "", valid: false},
{name: "too long", comment: string(make([]byte, 5001)), valid: false},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
apiErr := validateEditableMapModComment(test.comment)

if test.valid && apiErr != nil {
t.Fatalf("expected comment to be valid, got error: %v", apiErr.Message)
}

if !test.valid && apiErr == nil {
t.Fatal("expected comment validation to fail")
}
})
}
}

func TestNormalizeEditableMapModTimestamp(t *testing.T) {
validTimestamp := "12345|2,23456|4"

tests := []struct {
name string
timestamp *string
want *string
valid bool
}{
{name: "omitted", timestamp: nil, want: nil, valid: true},
{name: "empty", timestamp: stringPointer(""), want: nil, valid: true},
{name: "valid", timestamp: &validTimestamp, want: &validTimestamp, valid: true},
{name: "invalid syntax", timestamp: stringPointer("12345|"), valid: false},
{name: "too long", timestamp: stringPointer(string(make([]byte, 5001))), valid: false},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got, apiErr := normalizeEditableMapModTimestamp(test.timestamp)

if test.valid && apiErr != nil {
t.Fatalf("expected timestamp to be valid, got error: %v", apiErr.Message)
}

if !test.valid && apiErr == nil {
t.Fatal("expected timestamp validation to fail")
}

if test.want == nil && got != nil {
t.Fatalf("expected timestamp to be cleared, got %q", *got)
}

if test.want != nil && (got == nil || *got != *test.want) {
t.Fatalf("expected timestamp %q, got %v", *test.want, got)
}
})
}
}

func stringPointer(value string) *string {
return &value
}
Loading