1
0
Fork 0
mirror of https://github.com/Eggbertx/gochan.git synced 2025-08-26 10:36:23 -07:00

Add text formatting classes and update report handling logic to dismissing in bulk

This commit is contained in:
Eggbertx 2025-03-28 21:55:15 -07:00
parent 4aab676c67
commit a47e03def4
9 changed files with 255 additions and 139 deletions

View file

@ -114,4 +114,21 @@ 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;
}

View file

@ -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;
}

View file

@ -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()

View file

@ -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

View file

@ -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 {

View file

@ -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("")

165
pkg/manage/reports.go Normal file
View file

@ -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
}

View file

@ -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;

View file

@ -1,20 +1,29 @@
{{if eq 0 (len .reports)}}<i>No reports</i>{{else -}}
<table id="reportstable" class="mgmt-table">
<tr><th>Post</th><th>Reason</th><th>Reporter IP</th><th>Staff assigned</th><th>Actions</th></tr>
{{range $r,$report := .reports}}
<tr><td><a href="{{$report.post_link}}">Link</a></td><td>{{$report.reason}}</td><td>{{$report.ip}}</td><td>
{{- if (lt $report.staff_id 1) -}}
<i>unassigned</i>
{{- else -}}
{{$report.staff_user}}
{{- end -}}
</td><td class="table-actions">
<a href="{{webPath "manage/reports?dismiss="}}{{$report.id}}">Dismiss</a>
<form action="{{webPath `/manage/reports`}}" method="POST" onsubmit="return confirm('Are you sure you want to continue?');">
<input type="submit" name="dismiss-all" value="Dismiss All">
<input type="submit" name="dismiss-sel" value="Dismiss Selected">
{{if eq $.staff.Rank 3 -}}
|
<a href="{{webPath "manage/reports?dismiss="}}{{$report.id}}&block=1" title="Prevent future reports of this post, regardless of report reason">Make post unreportable</a>
{{- end}}
</td></tr>
{{end}}
</table>
{{end}}
<input type="submit" name="block-sel" value="Make Selected Unreportable">
{{- end -}}
<table id="reportstable" class="mgmt-table">
<colgroup>
<col style="width: 5%;">
<col style="width: 5%;">
<col style="width: 55%;">
<col style="width: 20%;">
<col style="width: 15%;">
</colgroup>
<tr><th></th><th>Post</th><th>Reason</th><th>Reporter IP</th><th>Staff assigned</th></tr>
{{range $r,$report := .reports}}
<tr>
<td class="text-center"><input type="checkbox" name="report{{$report.ID}}"></td>
<td class="text-center"><a href="{{$report.PostLink}}">Link</a></td>
<td>{{$report.Reason}}</td>
<td class="text-center">{{$report.IP}}</td>
<td class="text-center {{if eq $report.HandledByStaffID nil}}text-italic{{end}}">
{{$report.StaffUser}}
</td>
</tr>{{end}}
</table>
</form>{{end}}