Skip to content
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

:saprkles: Auto escalate accounts for high follow/like churn #740

Merged
merged 14 commits into from
Jan 17, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
4 changes: 4 additions & 0 deletions automod/engine/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,10 @@ func (c *AccountContext) ReportAccount(reason, comment string) {
c.effects.ReportAccount(reason, comment)
}

func (c *AccountContext) EscalateAccount() {
c.effects.EscalateAccount()
}

func (c *AccountContext) TakedownAccount() {
c.effects.TakedownAccount()
}
Expand Down
9 changes: 9 additions & 0 deletions automod/engine/effects.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ var (
QuotaModReportDay = 2000
// number of takedowns automod can action per day, for all subjects combined (circuit breaker)
QuotaModTakedownDay = 200
// number of escalations automod can action per day, for all subjects combined (circuit breaker)
QuotaModEscalationDay = 1000
)

type CounterRef struct {
Expand Down Expand Up @@ -44,6 +46,8 @@ type Effects struct {
AccountReports []ModReport
// If "true", indicates that a rule indicates that the entire account should have a takedown.
AccountTakedown bool
// If "true", indicates that a rule indicates that the reported account should be escalated.
AccountEscalate bool
// Same as "AccountLabels", but at record-level
RecordLabels []string
// Same as "AccountFlags", but at record-level
Expand Down Expand Up @@ -128,6 +132,11 @@ func (e *Effects) TakedownAccount() {
e.AccountTakedown = true
}

// Enqueues the entire account to be taken down at the end of rule processing.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment out of date (copypasta)

func (e *Effects) EscalateAccount() {
e.AccountEscalate = true
}

// Enqueues the provided label (string value) to be added to the record at the end of rule processing.
func (e *Effects) AddRecordLabel(val string) {
e.mu.Lock()
Expand Down
5 changes: 5 additions & 0 deletions automod/engine/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ var actionNewTakedownCount = promauto.NewCounterVec(prometheus.CounterOpts{
Help: "Number of new flags persisted",
}, []string{"type"})

var actionNewEscalationCount = promauto.NewCounterVec(prometheus.CounterOpts{
Name: "automod_new_action_escalations",
Help: "Number of new flags persisted",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

out of date

}, []string{"type"})

var accountMetaFetches = promauto.NewCounter(prometheus.CounterOpts{
Name: "automod_account_meta_fetches",
Help: "Number of account metadata reads (API calls)",
Expand Down
28 changes: 27 additions & 1 deletion automod/engine/persist.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,13 @@ func (eng *Engine) persistAccountModActions(c *AccountContext) error {
if err != nil {
return fmt.Errorf("circuit-breaking takedowns: %w", err)
}
// @TODO: do we want to check if the account is already escalated?
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, we should check here. we should also maybe popular review start as part of account meta? and clear account meta when it changes.

newEscalation, err := eng.circuitBreakEscalation(ctx, c.effects.AccountEscalate)
if err != nil {
return fmt.Errorf("circuit-breaking escalation: %w", err)
}

anyModActions := newTakedown || len(newLabels) > 0 || len(newFlags) > 0 || len(newReports) > 0
anyModActions := newTakedown || len(newLabels) > 0 || len(newFlags) > 0 || len(newReports) > 0 || newEscalation
if anyModActions && eng.Notifier != nil {
for _, srv := range dedupeStrings(c.effects.NotifyServices) {
if err := eng.Notifier.SendAccount(ctx, srv, c); err != nil {
Expand Down Expand Up @@ -145,6 +150,27 @@ func (eng *Engine) persistAccountModActions(c *AccountContext) error {
if err != nil {
c.Logger.Error("failed to execute account takedown", "err", err)
}
} else if newEscalation {
// we don't want to escalate if there is a takedown
c.Logger.Warn("account-escalate")
actionNewEscalationCount.WithLabelValues("account").Inc()
comment := "[automod]: auto account-escalation"
_, err := toolsozone.ModerationEmitEvent(ctx, xrpcc, &toolsozone.ModerationEmitEvent_Input{
CreatedBy: xrpcc.Auth.Did,
Event: &toolsozone.ModerationEmitEvent_Input_Event{
ModerationDefs_ModEventEscalate: &toolsozone.ModerationDefs_ModEventEscalate{
Comment: &comment,
},
},
Subject: &toolsozone.ModerationEmitEvent_Input_Subject{
AdminDefs_RepoRef: &comatproto.AdminDefs_RepoRef{
Did: c.Account.Identity.DID.String(),
},
},
})
if err != nil {
c.Logger.Error("failed to execute account escalation", "err", err)
}
}

needCachePurge := newTakedown || len(newLabels) > 0 || len(newFlags) > 0 || createdReports
Expand Down
19 changes: 19 additions & 0 deletions automod/engine/persisthelpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,25 @@ func (eng *Engine) circuitBreakReports(ctx context.Context, reports []ModReport)
return reports, nil
}

func (eng *Engine) circuitBreakEscalation(ctx context.Context, escalate bool) (bool, error) {
if !escalate {
return false, nil
}
c, err := eng.Counters.GetCount(ctx, "automod-quota", "escalate", countstore.PeriodDay)
if err != nil {
return false, fmt.Errorf("checking escalate action quota: %w", err)
}
if c >= QuotaModEscalationDay {
eng.Logger.Warn("CIRCUIT BREAKER: automod escalation")
return false, nil
}
err = eng.Counters.Increment(ctx, "automod-quota", "escalate")
if err != nil {
return false, fmt.Errorf("incrementing escalate action quota: %w", err)
}
return escalate, nil
}

func (eng *Engine) circuitBreakTakedown(ctx context.Context, takedown bool) (bool, error) {
if !takedown {
return false, nil
Expand Down
2 changes: 2 additions & 0 deletions automod/rules/interaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ func InteractionChurnRule(c *automod.RecordContext) error {
c.Logger.Info("high-like-churn", "created-today", created, "deleted-today", deleted)
c.AddAccountFlag("high-like-churn")
c.ReportAccount(automod.ReportReasonSpam, fmt.Sprintf("interaction churn: %d likes, %d unlikes today (so far)", created, deleted))
c.EscalateAccount()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd be in favor of commenting this out for now, so we can review and get the engine change merged, then do the policy/behavior change separately.

c.Notify("slack")
}
case "app.bsky.graph.follow":
Expand All @@ -36,6 +37,7 @@ func InteractionChurnRule(c *automod.RecordContext) error {
c.Logger.Info("high-follow-churn", "created-today", created, "deleted-today", deleted)
c.AddAccountFlag("high-follow-churn")
c.ReportAccount(automod.ReportReasonSpam, fmt.Sprintf("interaction churn: %d follows, %d unfollows today (so far)", created, deleted))
c.EscalateAccount()
c.Notify("slack")
}
// just generic bulk following
Expand Down
Loading