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:
parent
4aab676c67
commit
a47e03def4
9 changed files with 255 additions and 139 deletions
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
165
pkg/manage/reports.go
Normal 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
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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}}
|
Loading…
Add table
Add a link
Reference in a new issue