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 -}} - - -{{range $r,$report := .reports}} - -{{end}} -
PostReasonReporter IPStaff assignedActions
Link{{$report.reason}}{{$report.ip}} - {{- if (lt $report.staff_id 1) -}} - unassigned - {{- else -}} - {{$report.staff_user}} - {{- end -}} - - Dismiss + +
+ + {{if eq $.staff.Rank 3 -}} - | - Make post unreportable - {{- end}} -
-{{end}} \ No newline at end of file + + {{- end -}} + + + + + + + + + + {{range $r,$report := .reports}} + + + + + + + {{end}} +
PostReasonReporter IPStaff assigned
Link{{$report.Reason}}{{$report.IP}} + {{$report.StaffUser}} +
+{{end}} \ No newline at end of file