// Copyright 2025 The Forgejo Authors. All rights reserved. // SPDX-License-Identifier: GPL-3.0-or-later package moderation import ( "context" "database/sql" "errors" "slices" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/timeutil" "xorm.io/builder" ) // ReportStatusType defines the statuses a report (of abusive content) can have. type ReportStatusType int //revive:disable-line:exported 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 //revive:disable-line:exported // AbuseCategoryItem defines a pair of value and it's corresponding translation key // (used when new reports are submitted). AbuseCategoryItem struct { Value AbuseCategoryType TranslationKey string } ) const ( AbuseCategoryTypeSpam AbuseCategoryType = iota + 1 // 1 AbuseCategoryTypeMalware // 2 AbuseCategoryTypeIllegalContent // 3 AbuseCategoryTypeOtherViolations // 4 (Other violations of platform rules) ) // 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"}, {AbuseCategoryTypeOtherViolations, "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 //revive:disable-line:exported 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:"NOT NULL DEFAULT 1"` // The ID of the user who submitted the report. ReporterID int64 `xorm:"NOT NULL"` // index ?! // 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 // TODO: ReporterReparks or Reason // 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)) } // IsReported reports whether one or more reports were already submitted for contentType and contentID // (regardless the status of the reports). func IsReported(ctx context.Context, contentType ReportedContentType, contentID int64) bool { // TODO: only consider the reports with 'New' status (and adjust the function name)?! reported, _ := db.GetEngine(ctx).Exist(&AbuseReport{ContentType: contentType, ContentID: contentID}) return reported } // 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 } 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 }