Skip to content

WIP: Additional tools for Spam Management #13

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 17 commits into
base: apply-patches-v1.24.0
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions BLENDER_README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Blender Merges

Currently the process for merging upstream changes is to rebase, and keep
Blender modifications on top. This keeps a clear overview of the modifications
that were made.

When merging a major new release, cherry-pick all the Blender commits on
top of it. A simple `git rebase` will not work because the release and main
branches diverge.

First do changes in `blender-merged-develop`, and deploy on uatest. Then apply
the changes in `blender-merged` and deploy in production.
5 changes: 5 additions & 0 deletions Dockerfile.rootless
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ RUN apk --no-cache add \
gnupg \
&& rm -rf /var/cache/apk/*

# External renderers
RUN apk --no-cache add \
python3-dev \
&& rm -rf /var/cache/apk/*

RUN addgroup \
-S -g 1000 \
git && \
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,7 @@ require (
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mozillazg/go-unidecode v0.2.0 // indirect
github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450 // indirect
github.com/mschoch/smat v0.2.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
Expand Down
3 changes: 3 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,9 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/mozillazg/go-unidecode v0.2.0 h1:vFGEzAH9KSwyWmXCOblazEWDh7fOkpmy/Z4ArmamSUc=
github.com/mozillazg/go-unidecode v0.2.0/go.mod h1:zB48+/Z5toiRolOZy9ksLryJ976VIwmDmpQ2quyt1aA=
github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450 h1:j2kD3MT1z4PXCiUllUJF9mWUESr9TWKS7iEKsQ/IipM=
github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450/go.mod h1:skjdDftzkFALcuGzYSklqYd8gvat6F1gZJ4YPVbkZpM=
github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg=
Expand Down
2 changes: 1 addition & 1 deletion models/git/lfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ func CountLFSMetaObjects(ctx context.Context, repoID int64) (int64, error) {

// LFSObjectAccessible checks if a provided Oid is accessible to the user
func LFSObjectAccessible(ctx context.Context, user *user_model.User, oid string) (bool, error) {
if user.IsAdmin {
if user != nil && user.IsAdmin {
count, err := db.GetEngine(ctx).Count(&LFSMetaObject{Pointer: lfs.Pointer{Oid: oid}})
return count > 0, err
}
Expand Down
18 changes: 18 additions & 0 deletions models/issues/comment.go
Original file line number Diff line number Diff line change
Expand Up @@ -1313,3 +1313,21 @@ func InsertIssueComments(ctx context.Context, comments []*Comment) error {
}
return committer.Commit()
}

// GetRecentComments returns the most recent issue comments
func GetRecentComments(ctx context.Context, opts *db.ListOptions) ([]*Comment, error) {
sess := db.GetEngine(ctx).
Where("type = ?", CommentTypeComment).
OrderBy("created_unix DESC")

if opts != nil {
sess = db.SetSessionPagination(sess, opts)
}

cap := 0
if opts != nil {
cap = opts.PageSize
}
comments := make([]*Comment, 0, cap)
return comments, sess.Find(&comments)
}
18 changes: 18 additions & 0 deletions models/issues/issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -824,3 +824,21 @@ func ChangeIssueTimeEstimate(ctx context.Context, issue *Issue, doer *user_model
return nil
})
}

// GetRecentIssues returns the most recently created issues
func GetRecentIssues(ctx context.Context, opts *db.ListOptions) ([]*Issue, error) {
sess := db.GetEngine(ctx).
Where("is_pull = ?", false).
OrderBy("created_unix DESC")

if opts != nil {
sess = db.SetSessionPagination(sess, opts)
}

cap := 0
if opts != nil {
cap = opts.PageSize
}
issues := make([]*Issue, 0, cap)
return issues, sess.Find(&issues)
}
15 changes: 10 additions & 5 deletions models/issues/label.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,18 +184,23 @@ func (l *Label) BelongsToRepo() bool {
return l.RepoID > 0
}

// ExclusiveScope returns scope substring of label name, or empty string if none exists
func (l *Label) ExclusiveScope() string {
if !l.Exclusive {
return ""
}
// Return scope substring of label name, or empty string if none exists
func (l *Label) Scope() string {
lastIndex := strings.LastIndex(l.Name, "/")
if lastIndex == -1 || lastIndex == 0 || lastIndex == len(l.Name)-1 {
return ""
}
return l.Name[:lastIndex]
}

// ExclusiveScope returns scope substring of label name, or empty string if none exists
func (l *Label) ExclusiveScope() string {
if !l.Exclusive {
return ""
}
return l.Scope()
}

// NewLabel creates a new label
func NewLabel(ctx context.Context, l *Label) error {
color, err := label.NormalizeColor(l.Color)
Expand Down
16 changes: 16 additions & 0 deletions models/organization/team.go
Original file line number Diff line number Diff line change
Expand Up @@ -247,3 +247,19 @@ func IncrTeamRepoNum(ctx context.Context, teamID int64) error {
_, err := db.GetEngine(ctx).Incr("num_repos").ID(teamID).Update(new(Team))
return err
}

// Avoid notifying large teams accidentally
func FilterLargeTeams(teams []*Team, err error) ([]*Team, error) {
if err != nil {
return nil, err
}

var smallTeams []*Team
for _, team := range teams {
if team.NumMembers <= 10 {
smallTeams = append(smallTeams, team)
}
}

return smallTeams, nil
}
22 changes: 16 additions & 6 deletions models/user/badge.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,13 +105,23 @@ func RemoveUserBadge(ctx context.Context, u *User, badge *Badge) error {
// RemoveUserBadges removes badges from a user.
func RemoveUserBadges(ctx context.Context, u *User, badges []*Badge) error {
return db.WithTx(ctx, func(ctx context.Context) error {
badgeSlugs := make([]string, 0, len(badges))
for _, badge := range badges {
if _, err := db.GetEngine(ctx).
Join("INNER", "badge", "badge.id = `user_badge`.badge_id").
Where("`user_badge`.user_id=? AND `badge`.slug=?", u.ID, badge.Slug).
Delete(&UserBadge{}); err != nil {
return err
}
badgeSlugs = append(badgeSlugs, badge.Slug)
}
var userBadges []UserBadge
if err := db.GetEngine(ctx).Table("user_badge").
Join("INNER", "badge", "badge.id = `user_badge`.badge_id").
Where("`user_badge`.user_id = ?", u.ID).In("`badge`.slug", badgeSlugs).
Find(&userBadges); err != nil {
return err
}
userBadgeIDs := make([]int64, 0, len(userBadges))
for _, ub := range userBadges {
userBadgeIDs = append(userBadgeIDs, ub.ID)
}
if _, err := db.GetEngine(ctx).Table("user_badge").In("id", userBadgeIDs).Delete(); err != nil {
return err
}
return nil
})
Expand Down
136 changes: 136 additions & 0 deletions models/user/spamreport.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

// BLENDER: spam reporting

package user

import (
"context"
"fmt"

"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/timeutil"
)

// SpamReportStatusType is used to support a spam report lifecycle:
//
// pending -> locked
// locked -> processed | dismissed
//
// "locked" status works as a lock for a record that is being processed.
type SpamReportStatusType int

const (
SpamReportStatusTypePending = iota // 0
SpamReportStatusTypeLocked // 1
SpamReportStatusTypeProcessed // 2
SpamReportStatusTypeDismissed // 3
)

func (t SpamReportStatusType) String() string {
switch t {
case SpamReportStatusTypePending:
return "pending"
case SpamReportStatusTypeLocked:
return "locked"
case SpamReportStatusTypeProcessed:
return "processed"
case SpamReportStatusTypeDismissed:
return "dismissed"
}
return "unknown"
}

type SpamReport struct {
ID int64 `xorm:"pk autoincr"`
UserID int64 `xorm:"UNIQUE"`
ReporterID int64 `xorm:"NOT NULL"`
Status SpamReportStatusType `xorm:"INDEX NOT NULL DEFAULT 0"`
CreatedUnix timeutil.TimeStamp `xorm:"created"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
}

func (*SpamReport) TableName() string {
return "user_spamreport"
}

func init() {
// This table doesn't exist in the upstream code.
// We don't introduce migrations for it to avoid migration id clashes.
// Gitea will create the table in the database during startup,
// so no manual action is required until we start modifying the table.
db.RegisterModel(new(SpamReport))
}

type ListSpamReportsOptions struct {
db.ListOptions
Status SpamReportStatusType
}

type ListSpamReportsResults struct {
ID int64
CreatedUnix timeutil.TimeStamp
UpdatedUnix timeutil.TimeStamp
Status SpamReportStatusType
UserName string
UserCreatedUnix timeutil.TimeStamp
ReporterName string
}

func ListSpamReports(ctx context.Context, opts *ListSpamReportsOptions) ([]*ListSpamReportsResults, int64, error) {
opts.SetDefaultValues()
count, err := db.GetEngine(ctx).Count(new(SpamReport))
if err != nil {
return nil, 0, fmt.Errorf("Count: %w", err)
}
spamReports := make([]*ListSpamReportsResults, 0, opts.PageSize)
err = db.GetEngine(ctx).Table("user_spamreport").Select(
"user_spamreport.id, "+
"user_spamreport.created_unix, "+
"user_spamreport.updated_unix, "+
"user_spamreport.status, "+
"`user`.name as user_name, "+
"`user`.created_unix as user_created_unix, "+
"reporter.name as reporter_name",
).
Join("LEFT", "`user`", "`user`.id = user_spamreport.user_id").
Join("LEFT", "`user` as reporter", "`reporter`.id = user_spamreport.reporter_id").
Where("status = ?", opts.Status).
OrderBy("user_spamreport.id").
Limit(opts.PageSize, (opts.Page-1)*opts.PageSize).
Find(&spamReports)

return spamReports, count, err
}

func GetPendingSpamReportIDs(ctx context.Context) ([]int64, error) {
var ids []int64
err := db.GetEngine(ctx).Table("user_spamreport").
Select("id").Where("status = ?", SpamReportStatusTypePending).Find(&ids)
return ids, err
}

type SpamReportStatusCounts struct {
Count int64
Status SpamReportStatusType
}

func GetSpamReportStatusCounts(ctx context.Context) ([]*SpamReportStatusCounts, error) {
statusCounts := make([]*SpamReportStatusCounts, 0, 4) // 4 status types
err := db.GetEngine(ctx).Table("user_spamreport").
Select("count(*) as count, status").
GroupBy("status").
Find(&statusCounts)

return statusCounts, err
}

func GetSpamReportForUser(ctx context.Context, user *User) (*SpamReport, error) {
spamReport := &SpamReport{}
has, err := db.GetEngine(ctx).Where("user_id = ?", user.ID).Get(spamReport)
if has {
return spamReport, err
}
return nil, err
}
5 changes: 4 additions & 1 deletion modules/git/repo_compare.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,12 @@ func (repo *Repository) GetMergeBase(tmpRemote, base, head string) (string, stri
if tmpRemote != "origin" {
tmpBaseName := RemotePrefix + tmpRemote + "/tmp_" + base
// Fetch commit into a temporary branch in order to be able to handle commits and tags
_, _, err := NewCommand("fetch", "--no-tags").AddDynamicArguments(tmpRemote).AddDashesAndList(base+":"+tmpBaseName).RunStdString(repo.Ctx, &RunOpts{Dir: repo.Path})
// --no-write-commit-graph works around issue with commit-graph-chain.lock files that should not be there.
_, _, err := NewCommand("fetch", "--no-write-commit-graph", "--no-tags").AddDynamicArguments(tmpRemote).AddDashesAndList(base+":"+tmpBaseName).RunStdString(repo.Ctx, &RunOpts{Dir: repo.Path})
if err == nil {
base = tmpBaseName
} else {
logger.Trace("GetMergeBase failed to git fetch. Error: %v", err)
}
}

Expand Down
2 changes: 1 addition & 1 deletion modules/templates/util_render.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ func (ut *RenderUtils) RenderLabel(label *issues_model.Label) template.HTML {
locale := ut.ctx.Value(translation.ContextKey).(translation.Locale)
var extraCSSClasses string
textColor := util.ContrastColor(label.Color)
labelScope := label.ExclusiveScope()
labelScope := label.Scope()
descriptionText := emoji.ReplaceAliases(label.Description)

if label.IsArchived() {
Expand Down
Loading