Skip to content

Commit

Permalink
Merge pull request #272 from a0eoc/bulkremoval
Browse files Browse the repository at this point in the history
Style (spam) bulk removal for moderators

Fix #213
  • Loading branch information
vednoc authored Oct 18, 2023
2 parents fc28b30 + b3ed283 commit a2e4a6f
Show file tree
Hide file tree
Showing 10 changed files with 321 additions and 43 deletions.
94 changes: 54 additions & 40 deletions handlers/style/ban.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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{
Expand All @@ -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, &notification); 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"
Expand Down
149 changes: 149 additions & 0 deletions handlers/style/bulkban.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
2 changes: 2 additions & 0 deletions handlers/style/style.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
})
Expand Down
13 changes: 13 additions & 0 deletions models/style.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"gorm.io/gorm"

"userstyles.world/modules/config"
"userstyles.world/modules/database"
"userstyles.world/modules/errors"
"userstyles.world/modules/util"
)
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion modules/storage/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
2 changes: 1 addition & 1 deletion web/views/email/regardsmod.text.tmpl
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
Regards,
The <a target="_blank" clicktracking="off" href="https://userstyles.world/">UserStyles.world</a> Moderation Team
The UserStyles.world Moderation Team
24 changes: 24 additions & 0 deletions web/views/email/style/bulkban.html.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{{ template "email/greeting.html" . }}

{{ template "email/noticeaction.html" . }}

<p>
Some of your styles have been removed from our platform for the following reason:<br>
{{ .Log.Reason }}
</p>

<p>Styles that were removed:</p>

<ul>
{{ range .Styles -}}<li>{{ .Name }}</li>{{ end }}
</ul>

{{ with .Log.Message -}}
<p>Additional message from the moderator:<br> {{ . }}</p>
{{ end }}

{{ template "email/actionrecorded.html" . }}

{{ template "email/getintouch.html" . }}

{{ template "email/regardsmod.html" . }}
23 changes: 23 additions & 0 deletions web/views/email/style/bulkban.text.tmpl
Original file line number Diff line number Diff line change
@@ -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" . }}
Loading

0 comments on commit a2e4a6f

Please sign in to comment.