Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Style (spam) bulk removal for moderators #272

Merged
merged 8 commits into from
Oct 18, 2023
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
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