diff --git a/frontend/sass/global.scss b/frontend/sass/global.scss
index 861b9c10..3c3a4d31 100644
--- a/frontend/sass/global.scss
+++ b/frontend/sass/global.scss
@@ -114,4 +114,21 @@ select.post-actions {
.ui-tabs-panel {
clear: both;
-}
\ No newline at end of file
+}
+
+.text-center {
+ text-align: center;
+}
+
+.text-bold {
+ font-weight: bold;
+}
+
+.text-italic {
+ font-style: italic;
+}
+
+.text-underline {
+ text-decoration: underline;
+}
+
diff --git a/html/css/global.css b/html/css/global.css
index 7ab5ec03..d569eaaa 100644
--- a/html/css/global.css
+++ b/html/css/global.css
@@ -758,3 +758,19 @@ select.post-actions {
.ui-tabs-panel {
clear: both;
}
+
+.text-center {
+ text-align: center;
+}
+
+.text-bold {
+ font-weight: bold;
+}
+
+.text-italic {
+ font-style: italic;
+}
+
+.text-underline {
+ text-decoration: underline;
+}
diff --git a/pkg/gcsql/reports.go b/pkg/gcsql/reports.go
index dc0c3808..d2c1d2d1 100644
--- a/pkg/gcsql/reports.go
+++ b/pkg/gcsql/reports.go
@@ -37,12 +37,11 @@ func CreateReport(postID int, ip string, reason string) (*Report, error) {
return nil, err
}
return &Report{
- ID: int(reportID),
- HandledByStaffID: -1,
- PostID: postID,
- IP: ip,
- Reason: reason,
- IsCleared: false,
+ ID: int(reportID),
+ PostID: postID,
+ IP: ip,
+ Reason: reason,
+ IsCleared: false,
}, nil
}
@@ -111,14 +110,10 @@ func GetReports(includeCleared bool) ([]Report, error) {
var reports []Report
for rows.Next() {
var report Report
- var staffID any
- err = rows.Scan(&report.ID, &staffID, &report.PostID, &report.IP, &report.Reason, &report.IsCleared)
+ err = rows.Scan(&report.ID, &report.HandledByStaffID, &report.PostID, &report.IP, &report.Reason, &report.IsCleared)
if err != nil {
return nil, err
}
-
- staffID64, _ := (staffID.(int64))
- report.HandledByStaffID = int(staffID64)
reports = append(reports, report)
}
return reports, rows.Close()
diff --git a/pkg/gcsql/tables.go b/pkg/gcsql/tables.go
index fb54a8d6..c4236035 100644
--- a/pkg/gcsql/tables.go
+++ b/pkg/gcsql/tables.go
@@ -227,12 +227,12 @@ type Post struct {
// table: DBPREFIXreports
type Report struct {
- ID int // sql: id
- HandledByStaffID int // sql: handled_by_staff_id
- PostID int // sql: post_id
- IP string // sql: ip
- Reason string // sql: reason
- IsCleared bool // sql: is_cleared
+ ID int `json:"id"` // sql: id
+ HandledByStaffID *int `json:"staff_id"` // sql: handled_by_staff_id
+ PostID int `json:"post_id"` // sql: post_id
+ IP string `json:"ip"` // sql: ip
+ Reason string `json:"reason"` // sql: reason
+ IsCleared bool `json:"is_cleared"` // sql: is_cleared
}
// table: DBPREFIXreports_audit
diff --git a/pkg/gcutil/logger.go b/pkg/gcutil/logger.go
index 86cae5f8..e537027e 100644
--- a/pkg/gcutil/logger.go
+++ b/pkg/gcutil/logger.go
@@ -23,6 +23,7 @@ var (
accessLogger zerolog.Logger
)
+// LogStr logs a string to the given zerolog events.
func LogStr(key, val string, events ...*zerolog.Event) {
for e := range events {
if events[e] != nil {
@@ -31,6 +32,7 @@ func LogStr(key, val string, events ...*zerolog.Event) {
}
}
+// LogInt logs an integer to the given zerolog events.
func LogInt(key string, i int, events ...*zerolog.Event) {
for e := range events {
if events[e] != nil {
@@ -39,6 +41,7 @@ func LogInt(key string, i int, events ...*zerolog.Event) {
}
}
+// LogBool logs a boolean value to the given zerolog events.
func LogBool(key string, b bool, events ...*zerolog.Event) {
for e := range events {
if events[e] != nil {
@@ -47,6 +50,7 @@ func LogBool(key string, b bool, events ...*zerolog.Event) {
}
}
+// LogTime logs a time value to the given zerolog events.
func LogTime(key string, t time.Time, events ...*zerolog.Event) {
for e := range events {
if events[e] != nil {
@@ -55,6 +59,18 @@ func LogTime(key string, t time.Time, events ...*zerolog.Event) {
}
}
+// LogArray logs a slice of any type as an array in the zerolog event.
+func LogArray[T any](key string, arr []T, events ...*zerolog.Event) {
+ zlArr := zerolog.Arr()
+ for _, v := range arr {
+ zlArr.Interface(v)
+ }
+
+ for e := range events {
+ events[e].Array(key, zlArr)
+ }
+}
+
func LogDiscard(events ...*zerolog.Event) {
for e := range events {
if events[e] == nil {
diff --git a/pkg/manage/actionsModPerm.go b/pkg/manage/actionsModPerm.go
index 966541ae..056ec079 100644
--- a/pkg/manage/actionsModPerm.go
+++ b/pkg/manage/actionsModPerm.go
@@ -2,7 +2,6 @@ package manage
import (
"bytes"
- "context"
"database/sql"
"encoding/json"
"errors"
@@ -13,7 +12,6 @@ import (
"net/url"
"strconv"
"strings"
- "time"
"github.com/gochan-org/gochan/pkg/building"
"github.com/gochan-org/gochan/pkg/config"
@@ -391,106 +389,6 @@ func ipSearchCallback(_ http.ResponseWriter, request *http.Request, staff *gcsql
return manageIpBuffer.String(), nil
}
-func reportsCallback(_ http.ResponseWriter, request *http.Request, staff *gcsql.Staff, wantsJSON bool, infoEv *zerolog.Event, errEv *zerolog.Event) (output any, err error) {
- dismissIDstr := request.FormValue("dismiss")
- if dismissIDstr != "" {
- // staff is dismissing a report
- dismissID := gcutil.HackyStringToInt(dismissIDstr)
- block := request.FormValue("block")
- if block != "" && staff.Rank != 3 {
- errEv.Caller().
- Int("postID", dismissID).
- Str("rejected", "not an admin").Send()
- return "", errors.New("only the administrator can block reports")
- }
- found, err := gcsql.ClearReport(dismissID, staff.ID, block != "" && staff.Rank == 3)
- if err != nil {
- errEv.Err(err).Caller().
- Int("postID", dismissID).Send()
- return nil, err
- }
- if !found {
- return nil, errors.New("no matching reports")
- }
- infoEv.
- Int("reportID", dismissID).
- Bool("blocked", block != "").
- Msg("Report cleared")
- }
-
- ctx, cancel := context.WithTimeout(context.Background(), config.DefaultSQLTimeout*time.Second)
- defer cancel()
-
- requestOptions := &gcsql.RequestOptions{
- Context: ctx,
- Cancel: cancel,
- }
-
- if err = gcsql.DeleteReportsOfDeletedPosts(requestOptions); err != nil {
- errEv.Err(err).Caller().Send()
- return nil, server.NewServerError("failed to clean up reports of deleted posts", http.StatusInternalServerError)
- }
-
- rows, err := gcsql.Query(requestOptions, `SELECT id, staff_id, staff_user, post_id, ip, reason, is_cleared FROM DBPREFIXv_post_reports`)
- if err != nil {
- errEv.Err(err).Caller().Send()
- return nil, err
- }
- defer rows.Close()
- reports := make([]map[string]any, 0)
- for rows.Next() {
- var id int
- var staffID any
- var staffUser []byte
- var postID int
- var ip string
- var reason string
- var isCleared int
- err = rows.Scan(&id, &staffID, &staffUser, &postID, &ip, &reason, &isCleared)
- if err != nil {
- errEv.Err(err).Caller().Send()
- return nil, server.NewServerError("failed to scan report row", http.StatusInternalServerError)
- }
-
- post, err := gcsql.GetPostFromID(postID, true, requestOptions)
- if err != nil {
- errEv.Err(err).Caller().Msg("failed to get post from ID")
- return nil, server.NewServerError("failed to get post from ID", http.StatusInternalServerError)
- }
-
- staffIDint, _ := staffID.(int64)
- reports = append(reports, map[string]any{
- "id": id,
- "staff_id": int(staffIDint),
- "staff_user": string(staffUser),
- "post_link": post.WebPath(),
- "ip": ip,
- "reason": reason,
- "is_cleared": isCleared,
- })
- }
- if err = rows.Close(); err != nil {
- errEv.Err(err).Caller().Send()
- return nil, err
- }
- if wantsJSON {
- return reports, nil
- }
-
- reportsBuffer := bytes.NewBufferString("")
- err = serverutil.MinifyTemplate(gctemplates.ManageReports,
- map[string]any{
- "reports": reports,
- "staff": staff,
- }, reportsBuffer, "text/html")
- if err != nil {
- errEv.Err(err).Caller().Send()
- return "", err
- }
- output = reportsBuffer.String()
- return
-}
-
func threadAttrsCallback(_ http.ResponseWriter, request *http.Request, _ *gcsql.Staff, wantsJSON bool, infoEv, errEv *zerolog.Event) (output any, err error) {
boardDir := request.FormValue("board")
attrBuffer := bytes.NewBufferString("")
diff --git a/pkg/manage/reports.go b/pkg/manage/reports.go
new file mode 100644
index 00000000..d0867078
--- /dev/null
+++ b/pkg/manage/reports.go
@@ -0,0 +1,165 @@
+package manage
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "net/http"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/gochan-org/gochan/pkg/config"
+ "github.com/gochan-org/gochan/pkg/gcsql"
+ "github.com/gochan-org/gochan/pkg/gctemplates"
+ "github.com/gochan-org/gochan/pkg/gcutil"
+ "github.com/gochan-org/gochan/pkg/server"
+ "github.com/gochan-org/gochan/pkg/server/serverutil"
+ "github.com/rs/zerolog"
+)
+
+type reportData struct {
+ gcsql.Report
+ StaffUser *string `json:"staff_user"`
+ PostLink string `json:"post_link"`
+}
+
+func doReportHandling(request *http.Request, staff *gcsql.Staff, infoEv, errEv *zerolog.Event) error {
+ doDismissAll := request.PostFormValue("dismiss-all")
+ doDismissSel := request.PostFormValue("dismiss-sel")
+ doBlockSel := request.PostFormValue("block-sel")
+
+ if doDismissAll != "" {
+ _, err := gcsql.Exec(nil, `UPDATE DBPREFIXreports SET is_cleared = 1`)
+ if err != nil {
+ errEv.Err(err).Caller().Send()
+ return err
+ }
+ infoEv.Msg("All reports dismissed")
+ return nil
+ }
+
+ if doDismissSel == "" && doBlockSel == "" {
+ return nil
+ }
+
+ if doBlockSel != "" && staff.Rank != 3 {
+ gcutil.LogWarning().Caller().
+ Str("IP", gcutil.GetRealIP(request)).
+ Str("staff", staff.Username).
+ Str("rejected", "not an admin").
+ Msg("only the administrator can block reports")
+ return server.NewServerError("only the administrator can block reports", http.StatusForbidden)
+ }
+
+ var checkedReports []int
+ for reportIDstr, val := range request.PostForm {
+ if len(val) == 0 {
+ continue
+ }
+
+ idStr, ok := strings.CutPrefix(reportIDstr, "report")
+ if !ok {
+ continue
+ }
+ id, err := strconv.Atoi(idStr)
+ if err != nil {
+ continue
+ }
+ checkedReports = append(checkedReports, id)
+ }
+
+ if len(checkedReports) == 0 {
+ return nil
+ }
+ gcutil.LogArray("reportIDs", checkedReports, infoEv)
+
+ for _, reportID := range checkedReports {
+ matched, err := gcsql.ClearReport(reportID, staff.ID, doBlockSel != "")
+ if !matched {
+ errEv.Err(err).Caller().
+ Int("reportID", reportID).
+ Msg("report not found")
+ return server.NewServerError(fmt.Sprintf("report with id %d does not exist or is cleared", reportID), http.StatusBadRequest)
+ }
+ if err != nil {
+ errEv.Err(err).Caller().
+ Int("reportID", reportID).
+ Msg("failed to clear report")
+ return server.NewServerError(fmt.Sprintf("failed to clear report with id %d", reportID), http.StatusInternalServerError)
+ }
+ }
+ infoEv.Msg("Reports dismissed")
+ return nil
+}
+
+func reportsCallback(_ http.ResponseWriter, request *http.Request, staff *gcsql.Staff, wantsJSON bool, infoEv *zerolog.Event, errEv *zerolog.Event) (output any, err error) {
+ if err = doReportHandling(request, staff, infoEv, errEv); err != nil {
+ errEv.Discard() // doReportHandling logs errors
+ return nil, err
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), config.DefaultSQLTimeout*time.Second)
+ defer cancel()
+
+ requestOptions := &gcsql.RequestOptions{
+ Context: ctx,
+ Cancel: cancel,
+ }
+
+ if err = gcsql.DeleteReportsOfDeletedPosts(requestOptions); err != nil {
+ errEv.Err(err).Caller().Send()
+ return nil, server.NewServerError("failed to clean up reports of deleted posts", http.StatusInternalServerError)
+ }
+
+ rows, err := gcsql.Query(requestOptions, `SELECT id, staff_id, staff_user, post_id, ip, reason, is_cleared FROM DBPREFIXv_post_reports`)
+ if err != nil {
+ errEv.Err(err).Caller().Send()
+ return nil, err
+ }
+ defer rows.Close()
+ // reports := make([]map[string]any, 0)
+ var reports []reportData
+ for rows.Next() {
+ var report reportData
+ err = rows.Scan(&report.ID, &report.HandledByStaffID, &report.StaffUser, &report.PostID, &report.IP, &report.Reason, &report.IsCleared)
+ if report.StaffUser == nil {
+ user := "unassigned"
+ report.StaffUser = &user
+ handledByStaffID := 0
+ report.HandledByStaffID = &handledByStaffID
+ }
+ if err != nil {
+ errEv.Err(err).Caller().Send()
+ return nil, server.NewServerError("failed to scan report row", http.StatusInternalServerError)
+ }
+
+ post, err := gcsql.GetPostFromID(report.PostID, true, requestOptions)
+ if err != nil {
+ errEv.Err(err).Caller().Msg("failed to get post from ID")
+ return nil, server.NewServerError("failed to get post from ID", http.StatusInternalServerError)
+ }
+ report.PostLink = post.WebPath()
+ reports = append(reports, report)
+ }
+ if err = rows.Close(); err != nil {
+ errEv.Err(err).Caller().Send()
+ return nil, err
+ }
+ if wantsJSON {
+ return reports, nil
+ }
+
+ reportsBuffer := bytes.NewBufferString("")
+ err = serverutil.MinifyTemplate(gctemplates.ManageReports,
+ map[string]any{
+ "reports": reports,
+ "staff": staff,
+ }, reportsBuffer, "text/html")
+ if err != nil {
+ errEv.Err(err).Caller().Send()
+ return "", err
+ }
+ output = reportsBuffer.String()
+ return
+}
diff --git a/sql/reset_views.sql b/sql/reset_views.sql
index 309af724..1c1101dd 100644
--- a/sql/reset_views.sql
+++ b/sql/reset_views.sql
@@ -104,4 +104,4 @@ LEFT JOIN DBPREFIXboards b ON b.id = t.board_id;
CREATE VIEW DBPREFIXv_post_reports AS
SELECT r.id, handled_by_staff_id AS staff_id, username AS staff_user, post_id, IP_NTOA as ip, reason, is_cleared
FROM DBPREFIXreports r LEFT JOIN DBPREFIXstaff s ON handled_by_staff_id = s.id
-WHERE is_cleared = FALSE;
+WHERE is_cleared = 0;
diff --git a/templates/manage_reports.html b/templates/manage_reports.html
index 852a7802..cbf15786 100644
--- a/templates/manage_reports.html
+++ b/templates/manage_reports.html
@@ -1,20 +1,29 @@
{{if eq 0 (len .reports)}}No reports{{else -}}
-
-Post | Reason | Reporter IP | Staff assigned | Actions |
-{{range $r,$report := .reports}}
-Link | {{$report.reason}} | {{$report.ip}} |
- {{- if (lt $report.staff_id 1) -}}
- unassigned
- {{- else -}}
- {{$report.staff_user}}
- {{- end -}}
- |
- Dismiss
+
+ |
-{{end}}
-
-{{end}}
\ No newline at end of file
+
+ {{- end -}}
+
+{{end}}
\ No newline at end of file