mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2025-08-05 09:55:20 +02:00
feat!: Abusive content reporting (#6977)
This implements milestones 1. and 4. from **Task F. Moderation features: Reporting** (part of [amendment of the workplan](https://codeberg.org/forgejo/sustainability/src/branch/main/2022-12-01-nlnet/2025-02-07-extended-workplan.md#task-f-moderation-features-reporting) for NLnet 2022-12-035): > 1. A reporting feature is implemented in the database. It ensures that content remains available for review, even if a user deletes it after a report was sent. > 4. Users can report the most relevant content types (at least: issue comments, repositories, users) ### See also: - forgejo/discussions#291 - forgejo/discussions#304 - forgejo/design#30 Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/6977 Reviewed-by: Gusted <gusted@noreply.codeberg.org> Reviewed-by: Otto <otto@codeberg.org> Co-authored-by: floss4good <floss4good@disroot.org> Co-committed-by: floss4good <floss4good@disroot.org>
This commit is contained in:
parent
c1fad04473
commit
dc56486b1f
34 changed files with 1040 additions and 6 deletions
177
models/moderation/abuse_report.go
Normal file
177
models/moderation/abuse_report.go
Normal file
|
@ -0,0 +1,177 @@
|
|||
// Copyright 2025 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package moderation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"slices"
|
||||
|
||||
"forgejo.org/models/db"
|
||||
"forgejo.org/modules/log"
|
||||
"forgejo.org/modules/timeutil"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// ReportStatusType defines the statuses a report (of abusive content) can have.
|
||||
type ReportStatusType int
|
||||
|
||||
const (
|
||||
// ReportStatusTypeOpen represents the status of open reports that were not yet handled in any way.
|
||||
ReportStatusTypeOpen ReportStatusType = iota + 1 // 1
|
||||
// ReportStatusTypeHandled represents the status of valid reports, that have been acted upon.
|
||||
ReportStatusTypeHandled // 2
|
||||
// ReportStatusTypeIgnored represents the status of ignored reports, that were closed without any action.
|
||||
ReportStatusTypeIgnored // 3
|
||||
)
|
||||
|
||||
type (
|
||||
// AbuseCategoryType defines the categories in which a user can include the reported content.
|
||||
AbuseCategoryType int
|
||||
|
||||
// AbuseCategoryItem defines a pair of value and it's corresponding translation key
|
||||
// (used to add options within the dropdown shown when new reports are submitted).
|
||||
AbuseCategoryItem struct {
|
||||
Value AbuseCategoryType
|
||||
TranslationKey string
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
AbuseCategoryTypeOther AbuseCategoryType = iota + 1 // 1 (Other violations of platform rules)
|
||||
AbuseCategoryTypeSpam // 2
|
||||
AbuseCategoryTypeMalware // 3
|
||||
AbuseCategoryTypeIllegalContent // 4
|
||||
)
|
||||
|
||||
// GetAbuseCategoriesList returns a list of pairs with the available abuse category types
|
||||
// and their corresponding translation keys
|
||||
func GetAbuseCategoriesList() []AbuseCategoryItem {
|
||||
return []AbuseCategoryItem{
|
||||
{AbuseCategoryTypeSpam, "moderation.abuse_category.spam"},
|
||||
{AbuseCategoryTypeMalware, "moderation.abuse_category.malware"},
|
||||
{AbuseCategoryTypeIllegalContent, "moderation.abuse_category.illegal_content"},
|
||||
{AbuseCategoryTypeOther, "moderation.abuse_category.other_violations"},
|
||||
}
|
||||
}
|
||||
|
||||
// ReportedContentType defines the types of content that can be reported
|
||||
// (i.e. user/organization profile, repository, issue/pull, comment).
|
||||
type ReportedContentType int
|
||||
|
||||
const (
|
||||
// ReportedContentTypeUser should be used when reporting abusive users or organizations.
|
||||
ReportedContentTypeUser ReportedContentType = iota + 1 // 1
|
||||
|
||||
// ReportedContentTypeRepository should be used when reporting a repository with abusive content.
|
||||
ReportedContentTypeRepository // 2
|
||||
|
||||
// ReportedContentTypeIssue should be used when reporting an issue or pull request with abusive content.
|
||||
ReportedContentTypeIssue // 3
|
||||
|
||||
// ReportedContentTypeComment should be used when reporting a comment with abusive content.
|
||||
ReportedContentTypeComment // 4
|
||||
)
|
||||
|
||||
var allReportedContentTypes = []ReportedContentType{
|
||||
ReportedContentTypeUser,
|
||||
ReportedContentTypeRepository,
|
||||
ReportedContentTypeIssue,
|
||||
ReportedContentTypeComment,
|
||||
}
|
||||
|
||||
func (t ReportedContentType) IsValid() bool {
|
||||
return slices.Contains(allReportedContentTypes, t)
|
||||
}
|
||||
|
||||
// AbuseReport represents a report of abusive content.
|
||||
type AbuseReport struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
Status ReportStatusType `xorm:"INDEX NOT NULL DEFAULT 1"`
|
||||
// The ID of the user who submitted the report.
|
||||
ReporterID int64 `xorm:"NOT NULL"`
|
||||
// Reported content type: user/organization profile, repository, issue/pull or comment.
|
||||
ContentType ReportedContentType `xorm:"INDEX NOT NULL"`
|
||||
// The ID of the reported item (based on ContentType: user, repository, issue or comment).
|
||||
ContentID int64 `xorm:"NOT NULL"`
|
||||
// The abuse category selected by the reporter.
|
||||
Category AbuseCategoryType `xorm:"INDEX NOT NULL"`
|
||||
// Remarks provided by the reporter.
|
||||
Remarks string
|
||||
// The ID of the corresponding shadow-copied content when exists; otherwise null.
|
||||
ShadowCopyID sql.NullInt64 `xorm:"DEFAULT NULL"`
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"created NOT NULL"`
|
||||
}
|
||||
|
||||
var ErrSelfReporting = errors.New("reporting yourself is not allowed")
|
||||
|
||||
func init() {
|
||||
// RegisterModel will create the table if does not already exist
|
||||
// or any missing columns if the table was previously created.
|
||||
// It will not drop or rename existing columns (when struct has changed).
|
||||
db.RegisterModel(new(AbuseReport))
|
||||
}
|
||||
|
||||
// IsShadowCopyNeeded reports whether one or more reports were already submitted
|
||||
// for contentType and contentID and not yet linked to a shadow copy (regardless their status).
|
||||
func IsShadowCopyNeeded(ctx context.Context, contentType ReportedContentType, contentID int64) (bool, error) {
|
||||
return db.GetEngine(ctx).Cols("id").Where(builder.IsNull{"shadow_copy_id"}).Exist(
|
||||
&AbuseReport{ContentType: contentType, ContentID: contentID},
|
||||
)
|
||||
}
|
||||
|
||||
// AlreadyReportedByAndOpen returns if doerID has already submitted a report for contentType and contentID that is still Open.
|
||||
func AlreadyReportedByAndOpen(ctx context.Context, doerID int64, contentType ReportedContentType, contentID int64) bool {
|
||||
reported, _ := db.GetEngine(ctx).Exist(&AbuseReport{
|
||||
Status: ReportStatusTypeOpen,
|
||||
ReporterID: doerID,
|
||||
ContentType: contentType,
|
||||
ContentID: contentID,
|
||||
})
|
||||
return reported
|
||||
}
|
||||
|
||||
// ReportAbuse creates a new abuse report in the DB with 'Open' status.
|
||||
// If the reported content is the user profile of the reporter ErrSelfReporting is returned.
|
||||
// If there is already an open report submitted by the same user for the same content,
|
||||
// the request will be ignored without returning an error (and a warning will be logged).
|
||||
func ReportAbuse(ctx context.Context, report *AbuseReport) error {
|
||||
if report.ContentType == ReportedContentTypeUser && report.ReporterID == report.ContentID {
|
||||
return ErrSelfReporting
|
||||
}
|
||||
|
||||
if AlreadyReportedByAndOpen(ctx, report.ReporterID, report.ContentType, report.ContentID) {
|
||||
log.Warn("Seems that user %d wanted to report again the content with type %d and ID %d; this request will be ignored.", report.ReporterID, report.ContentType, report.ContentID)
|
||||
return nil
|
||||
}
|
||||
|
||||
report.Status = ReportStatusTypeOpen
|
||||
_, err := db.GetEngine(ctx).Insert(report)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
/*
|
||||
// MarkAsHandled will change the status to 'Handled' for all reports linked to the same item (user, repository, issue or comment).
|
||||
func MarkAsHandled(ctx context.Context, contentType ReportedContentType, contentID int64) error {
|
||||
return updateStatus(ctx, contentType, contentID, ReportStatusTypeHandled)
|
||||
}
|
||||
|
||||
// MarkAsIgnored will change the status to 'Ignored' for all reports linked to the same item (user, repository, issue or comment).
|
||||
func MarkAsIgnored(ctx context.Context, contentType ReportedContentType, contentID int64) error {
|
||||
return updateStatus(ctx, contentType, contentID, ReportStatusTypeIgnored)
|
||||
}
|
||||
|
||||
// updateStatus will set the provided status for any reports linked to the item with the given type and ID.
|
||||
func updateStatus(ctx context.Context, contentType ReportedContentType, contentID int64, status ReportStatusType) error {
|
||||
_, err := db.GetEngine(ctx).Where(builder.Eq{
|
||||
"content_type": contentType,
|
||||
"content_id": contentID,
|
||||
}).Cols("status").Update(&AbuseReport{Status: status})
|
||||
|
||||
return err
|
||||
}
|
||||
*/
|
Loading…
Add table
Add a link
Reference in a new issue