diff --git a/handlers/style/ban.go b/handlers/style/ban.go index c23f5603..9a5b24be 100644 --- a/handlers/style/ban.go +++ b/handlers/style/ban.go @@ -46,6 +46,50 @@ func BanGet(c *fiber.Ctx) error { }) } +func BanStyle(db *gorm.DB, style *models.Style, u *models.APIUser, user *storage.User, c *fiber.Ctx) (*models.Log, error) { + event := &models.Log{ + UserID: u.ID, + Username: u.Username, + Kind: models.LogRemoveStyle, + TargetUserName: user.Username, + TargetData: style.Name, + Reason: strings.TrimSpace(c.FormValue("reason")), + Message: strings.TrimSpace(c.FormValue("message")), + Censor: c.FormValue("censor") == "on", + } + + n := &models.Notification{ + Kind: models.KindBannedStyle, + TargetID: int(event.ID), + UserID: int(user.ID), + StyleID: int(style.ID), + } + + i := int(style.ID) + if err := storage.DeleteUserstyle(db, i); err != nil { + return nil, err + } + if err := models.DeleteStats(db, i); err != nil { + return nil, err + } + if err := storage.DeleteSearchData(db, i); err != nil { + return nil, err + } + if err := models.CreateLog(db, event); err != nil { + return nil, err + } + if err := models.CreateNotification(db, n); err != nil { + return nil, err + } + if err := models.RemoveStyleCode(strconv.Itoa(i)); err != nil { + return nil, err + } + + cache.Code.Remove(i) + + return event, nil +} + func BanPost(c *fiber.Ctx) error { u, _ := jwt.User(c) @@ -65,9 +109,8 @@ func BanPost(c *fiber.Ctx) error { "Title": "Invalid style ID", }) } - id := c.Params("id") - style, err := models.GetStyleByID(id) + style, err := models.TempGetStyleByID(i) if err != nil { c.Status(fiber.StatusNotFound) return c.Render("err", fiber.Map{ @@ -85,64 +128,35 @@ func BanPost(c *fiber.Ctx) error { }) } - event := models.Log{ - UserID: u.ID, - Username: u.Username, - Kind: models.LogRemoveStyle, - TargetUserName: style.Username, - TargetData: style.Name, - Reason: strings.TrimSpace(c.FormValue("reason")), - Message: strings.TrimSpace(c.FormValue("message")), - Censor: c.FormValue("censor") == "on", - } - - notification := models.Notification{ - Kind: models.KindBannedStyle, - TargetID: int(event.ID), - UserID: int(user.ID), - StyleID: int(style.ID), - } - - // INSERT INTO `logs` + var event *models.Log err = database.Conn.Transaction(func(tx *gorm.DB) error { - if err = storage.DeleteUserstyle(tx, i); err != nil { - return err - } - if err = models.DeleteStats(tx, i); err != nil { - return err - } - if err = storage.DeleteSearchData(tx, i); err != nil { - return err - } - if err = models.CreateLog(tx, &event); err != nil { + event, err = BanStyle(tx, style, u, user, c) + if err != nil { return err } - if err = models.CreateNotification(tx, ¬ification); err != nil { - return err - } - return models.RemoveStyleCode(id) + + return nil }) if err != nil { log.Database.Printf("Failed to remove %d: %s\n", i, err) + c.Status(fiber.StatusInternalServerError) return c.Render("err", fiber.Map{ "Title": "Failed to remove userstyle", "User": u, }) } - cache.Code.Remove(i) - go sendRemovalEmail(user, style, event) return c.Redirect("/modlog", fiber.StatusSeeOther) } -func sendRemovalEmail(user *storage.User, style *models.APIStyle, entry models.Log) { +func sendRemovalEmail(user *storage.User, style *models.Style, event *models.Log) { args := fiber.Map{ "User": user, "Style": style, - "Log": entry, - "Link": config.BaseURL + "/modlog#id-" + strconv.Itoa(int(entry.ID)), + "Log": event, + "Link": config.BaseURL + "/modlog#id-" + strconv.Itoa(int(event.ID)), } title := "Your style has been removed" diff --git a/handlers/style/bulkban.go b/handlers/style/bulkban.go new file mode 100644 index 00000000..55cfff44 --- /dev/null +++ b/handlers/style/bulkban.go @@ -0,0 +1,149 @@ +package style + +import ( + "strconv" + + "github.com/gofiber/fiber/v2" + "gorm.io/gorm" + + "userstyles.world/handlers/jwt" + "userstyles.world/models" + "userstyles.world/modules/config" + "userstyles.world/modules/database" + "userstyles.world/modules/email" + "userstyles.world/modules/log" + "userstyles.world/modules/storage" +) + +type bulkReq struct { + IDs []string +} + +func BulkBanGet(c *fiber.Ctx) error { + u, _ := jwt.User(c) + c.Locals("User", u) + + if !u.IsModOrAdmin() { + c.Locals("Title", "You are not authorized to perform this action") + return c.Status(fiber.StatusUnauthorized).Render("err", fiber.Map{}) + } + + id, err := c.ParamsInt("userid") + if err != nil || id < 1 { + c.Locals("Title", "Invalid user ID") + return c.Status(fiber.StatusBadRequest).Render("err", fiber.Map{}) + } + + if _, err = storage.FindUser(uint(id)); err != nil { + c.Locals("Title", "Could not find such user") + return c.Status(fiber.StatusNotFound).Render("err", fiber.Map{}) + } + + var styles []models.APIStyle + err = database.Conn.Find(&styles, "user_id = ? AND deleted_at IS NULL", id).Error + if err != nil || len(styles) == 0 { + c.Locals("Title", "Could not find any userstyles") + return c.Status(fiber.StatusNotFound).Render("err", fiber.Map{}) + } + c.Locals("Styles", styles) + + c.Locals("UserID", id) + c.Locals("Title", "Perform a bulk userstyle removal") + + return c.Render("style/bulkban", fiber.Map{}) +} + +func BulkBanPost(c *fiber.Ctx) error { + u, _ := jwt.User(c) + c.Locals("User", u) + + if !u.IsModOrAdmin() { + c.Locals("Title", "You are not authorized to perform this action") + return c.Status(fiber.StatusUnauthorized).Render("err", fiber.Map{}) + } + + uid, err := c.ParamsInt("userid") + if err != nil || uid < 1 { + c.Locals("Title", "Invalid user ID") + return c.Status(fiber.StatusBadRequest).Render("err", fiber.Map{}) + } + + user, err := storage.FindUser(uint(uid)) + if err != nil { + c.Locals("Title", "Could not find such user") + return c.Status(fiber.StatusNotFound).Render("err", fiber.Map{}) + } + + var req bulkReq + if err = c.BodyParser(&req); err != nil { + c.Locals("Title", "Failed to process request body") + return c.Status(fiber.StatusBadRequest).Render("err", fiber.Map{}) + } + + var styles []*models.Style + + // Process all IDs for problems not to have any errors in between of removal + for _, val := range req.IDs { + id, err := strconv.Atoi(val) + if err != nil { + c.Locals("Title", "Operation failed") + c.Locals("ErrTitle", val+" is not a valid number") + return c.Status(fiber.StatusBadRequest).Render("err", fiber.Map{}) + } + + style, err := models.GetStyleFromAuthor(id, uid) + if err != nil { + c.Locals("Title", "Operation failed") + c.Locals("ErrTitle", "User isn't the author of style with ID "+val) + return c.Status(fiber.StatusNotFound).Render("err", fiber.Map{}) + } + + styles = append(styles, &style) + } + + // lastEvent is used to link to the newest event in the modlog + // so the user will be presented with all of them on the screen. + var lastEvent *models.Log + err = database.Conn.Transaction(func(tx *gorm.DB) error { + for index, style := range styles { + event, err := BanStyle(tx, style, u, user, c) + if err != nil { + log.Database.Printf("Failed to remove %d: %s\n", style.ID, err) + return err + } + + if index == len(styles)-1 { + lastEvent = event + } + } + + return nil + }) + if err != nil { + c.Locals("Title", "Failed to ban styles") + return c.Status(fiber.StatusInternalServerError).Render("err", fiber.Map{}) + } + + go sendBulkRemovalEmail(user, styles, lastEvent) + + return c.Redirect("/modlog", fiber.StatusSeeOther) +} + +func sendBulkRemovalEmail(user *storage.User, styles []*models.Style, event *models.Log) { + args := fiber.Map{ + "User": user, + "Styles": styles, + "Log": event, + "Link": config.BaseURL + "/modlog#id-" + strconv.Itoa(int(event.ID)), + } + + var title string + if len(styles) == 1 { + title = strconv.Itoa(len(styles)) + " of your styles has been removed" + } else { + title = strconv.Itoa(len(styles)) + " of your styles have been removed" + } + if err := email.Send("style/bulkban", user.Email, title, args); err != nil { + log.Warn.Printf("Failed to email %d: %s\n", user.ID, err) + } +} diff --git a/handlers/style/style.go b/handlers/style/style.go index a7bf47de..4c886068 100644 --- a/handlers/style/style.go +++ b/handlers/style/style.go @@ -27,6 +27,8 @@ func Routes(app *fiber.App) { r.Get("/styles/promote/:id", jwtware.Protected, Promote) r.Get("/styles/ban/:id", jwtware.Protected, BanGet) r.Post("/styles/ban/:id", jwtware.Protected, BanPost) + r.Get("/styles/bulk-ban/:userid", jwtware.Protected, BulkBanGet) + r.Post("/styles/bulk-ban/:userid", jwtware.Protected, BulkBanPost) r.Static("/preview", config.PublicDir, fiber.Static{ MaxAge: 2678400, // 1 month }) diff --git a/models/style.go b/models/style.go index bd47a52a..6c15041a 100644 --- a/models/style.go +++ b/models/style.go @@ -14,6 +14,7 @@ import ( "gorm.io/gorm" "userstyles.world/modules/config" + "userstyles.world/modules/database" "userstyles.world/modules/errors" "userstyles.world/modules/util" ) @@ -67,6 +68,9 @@ type APIStyle struct { MirrorPrivate bool `json:"-"` } +// TableName returns which table in database to use with GORM. +func (APIStyle) TableName() string { return "styles" } + type StyleSiteMap struct { ID int } @@ -238,6 +242,15 @@ func AbleToReview(uid, sid uint) (string, bool) { return "", true } +func TempGetStyleByID(id int) (s *Style, err error) { + err = database.Conn. + Select("styles.*, u.username"). + Joins("JOIN users u ON u.id = styles.user_id"). + First(&s, "styles.id = ?", id). + Error + return s, err +} + // GetStyleFromAuthor tries to fetch a userstyle made by logged in user. func GetStyleFromAuthor(id, uid int) (Style, error) { var s Style diff --git a/modules/storage/user.go b/modules/storage/user.go index 37abe545..7c298ef6 100644 --- a/modules/storage/user.go +++ b/modules/storage/user.go @@ -32,7 +32,7 @@ func FindUsersCreatedOn(date time.Time) ([]User, error) { // FindUser returns a user. func FindUser(id uint) (u *User, err error) { - err = database.Conn.Find(&u, "id = ?", id).Error + err = database.Conn.First(&u, "id = ?", id).Error if err != nil { return nil, err } diff --git a/web/views/email/regardsmod.text.tmpl b/web/views/email/regardsmod.text.tmpl index 66818484..a1fb4f24 100644 --- a/web/views/email/regardsmod.text.tmpl +++ b/web/views/email/regardsmod.text.tmpl @@ -1,2 +1,2 @@ Regards, -The UserStyles.world Moderation Team +The UserStyles.world Moderation Team diff --git a/web/views/email/style/bulkban.html.tmpl b/web/views/email/style/bulkban.html.tmpl new file mode 100644 index 00000000..ade94964 --- /dev/null +++ b/web/views/email/style/bulkban.html.tmpl @@ -0,0 +1,24 @@ +{{ template "email/greeting.html" . }} + +{{ template "email/noticeaction.html" . }} + +

+ Some of your styles have been removed from our platform for the following reason:
+ {{ .Log.Reason }} +

+ +

Styles that were removed:

+ + + +{{ with .Log.Message -}} +

Additional message from the moderator:
{{ . }}

+{{ end }} + +{{ template "email/actionrecorded.html" . }} + +{{ template "email/getintouch.html" . }} + +{{ template "email/regardsmod.html" . }} diff --git a/web/views/email/style/bulkban.text.tmpl b/web/views/email/style/bulkban.text.tmpl new file mode 100644 index 00000000..a2fcbfbc --- /dev/null +++ b/web/views/email/style/bulkban.text.tmpl @@ -0,0 +1,23 @@ +{{ template "email/greeting.text" . }} + +{{ template "email/noticeaction.text" . }} + +Some of your styles have been removed from our platform for the following reason: +{{ .Log.Reason }} + +Styles that were removed: + +{{ range .Styles -}} + {{- printf "- %s\n" .Name -}} +{{ end -}} + +{{ with .Log.Message }} +Additional message from the moderator: +{{ . }} +{{ end }} + +{{ template "email/actionrecorded.text" . }} + +{{ template "email/getintouch.text" . }} + +{{ template "email/regardsmod.text" . }} diff --git a/web/views/style/bulkban.tmpl b/web/views/style/bulkban.tmpl new file mode 100644 index 00000000..b8dedcb9 --- /dev/null +++ b/web/views/style/bulkban.tmpl @@ -0,0 +1,52 @@ +
+

{{ .Title }}

+

This action is irreversible.

+
+ + + +
+
+ + {{ range .Styles }} +
+ + {{ template "partials/checkboxes" }} + +
+ {{ end }} + + + + Be aware that this reason will be made public alongside this action. + + + + For example, a hint about was done wrong and what can be done now. Will be included in the email. + + +
+ + {{ template "partials/checkboxes" }} + +
+ This will censor the information about the styles with a spoiler, use this if the style has an innapropiate name. + +
+ + Cancel +
+
+
diff --git a/web/views/user/profile.tmpl b/web/views/user/profile.tmpl index 4348d103..4833700a 100644 --- a/web/views/user/profile.tmpl +++ b/web/views/user/profile.tmpl @@ -20,7 +20,8 @@

{{ if ne .Profile.ID .User.ID }} - Ban this user +

Ban this user

+

Style bulk-removal

{{ end }} {{ end }}