mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2025-08-31 22:46:45 +00:00
- Implementation of milestone 5. 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): `5. Forgejo admins can see a list of reports` There is a lot of room for improvements, but it was decided to start with a basic version so that feedback can be collected from real-life usages (based on which the UI might change a lot). - Also covers milestone 2. from same **Task F. Moderation features: Reporting**: `2. Reports from multiple users are combined in the database and don't create additional reports.` But instead of combining the reports when stored, they are grouped when retrieved (it was concluded _that it might be preferable to take care of the deduplication while implementing the admin interface_; see https://codeberg.org/forgejo/forgejo/pulls/7939#issuecomment-4841754 for more details). --- Follow-up of !6977 ### See also: - forgejo/design#30 --- This adds a new _Moderation reports_ section (/admin/moderation/reports) within the _Site administration_ page, where administrators can see an overview with the submitted abuse reports that are still open (not yet handled in any way). When multiple reports exist for the same content (submitted by distinct users) only the first one will be shown in the list and a counter can be seen on the right side (indicating the number of open reports for the same content type and ID). Clicking on the counter or the icon from the right side will open the details page where a list with all the reports (when multiple) linked to the reported content is available, as well as any shadow copy saved for the current report(s). The new section is available only when moderation in enabled ([moderation] ENABLED config is set as true within app.ini). Discussions regarding the UI/UX started with https://codeberg.org/forgejo/design/issues/30#issuecomment-2908849 Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/7905 Reviewed-by: Otto <otto@codeberg.org> Reviewed-by: jerger <jerger@noreply.codeberg.org> Co-authored-by: floss4good <floss4good@disroot.org> Co-committed-by: floss4good <floss4good@disroot.org>
184 lines
7.4 KiB
Go
184 lines
7.4 KiB
Go
// 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
|
|
)
|
|
|
|
var AbuseCategoriesTranslationKeys = map[AbuseCategoryType]string{
|
|
AbuseCategoryTypeSpam: "moderation.abuse_category.spam",
|
|
AbuseCategoryTypeMalware: "moderation.abuse_category.malware",
|
|
AbuseCategoryTypeIllegalContent: "moderation.abuse_category.illegal_content",
|
|
AbuseCategoryTypeOther: "moderation.abuse_category.other_violations",
|
|
}
|
|
|
|
// GetAbuseCategoriesList returns a list of pairs with the available abuse category types
|
|
// and their corresponding translation keys
|
|
func GetAbuseCategoriesList() []AbuseCategoryItem {
|
|
return []AbuseCategoryItem{
|
|
{AbuseCategoryTypeSpam, AbuseCategoriesTranslationKeys[AbuseCategoryTypeSpam]},
|
|
{AbuseCategoryTypeMalware, AbuseCategoriesTranslationKeys[AbuseCategoryTypeMalware]},
|
|
{AbuseCategoryTypeIllegalContent, AbuseCategoriesTranslationKeys[AbuseCategoryTypeIllegalContent]},
|
|
{AbuseCategoryTypeOther, AbuseCategoriesTranslationKeys[AbuseCategoryTypeOther]},
|
|
}
|
|
}
|
|
|
|
// 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 `xorm:"VARCHAR(500)"`
|
|
// 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
|
|
}
|
|
*/
|