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

automod: account ack and escalation #782

Merged
merged 5 commits into from
Oct 30, 2024
Merged
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
10 changes: 10 additions & 0 deletions automod/engine/account_meta.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ import (
"github.com/bluesky-social/indigo/atproto/identity"
)

var (
ReviewStateEscalated = "escalated"
ReviewStateOpen = "open"
ReviewStateClosed = "closed"
ReviewStateNone = "none"
)

// information about a repo/account/identity, always pre-populated and relevant to many rules
type AccountMeta struct {
Identity *identity.Identity
Expand Down Expand Up @@ -34,4 +41,7 @@ type AccountPrivate struct {
EmailConfirmed bool
IndexedAt *time.Time
AccountTags []string
// ReviewState will be one of ReviewStateEscalated, ReviewStateOpen, ReviewStateClosed, ReviewStateNone, or "" (unknown)
ReviewState string
Appealed bool
}
8 changes: 8 additions & 0 deletions automod/engine/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,14 @@ func (c *AccountContext) TakedownAccount() {
c.effects.TakedownAccount()
}

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

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

func (c *RecordContext) AddRecordFlag(val string) {
c.effects.AddRecordFlag(val)
}
Expand Down
18 changes: 17 additions & 1 deletion 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 misc actions automod can do per day, for all subjects combined (circuit breaker)
QuotaModActionDay = 1000
)

type CounterRef struct {
Expand Down Expand Up @@ -42,8 +44,12 @@ type Effects struct {
AccountFlags []string
// Reports which should be filed against this account, as a result of rule execution.
AccountReports []ModReport
// If "true", indicates that a rule indicates that the entire account should have a takedown.
// If "true", a rule decided that the entire account should have a takedown.
AccountTakedown bool
// If "true", a rule decided that the reported account should be escalated.
AccountEscalate bool
// If "true", a rule decided that the reports on account should be resolved as acknowledged.
AccountAcknowledge bool
// Same as "AccountLabels", but at record-level
RecordLabels []string
// Same as "AccountFlags", but at record-level
Expand Down Expand Up @@ -128,6 +134,16 @@ func (e *Effects) TakedownAccount() {
e.AccountTakedown = true
}

// Enqueues the account to be "escalated" for mod review at the end of rule processing.
func (e *Effects) EscalateAccount() {
e.AccountEscalate = true
}

// Enqueues reports on account to be "acknowledged" (closed) at the end of rule processing.
func (e *Effects) AcknowledgeAccount() {
e.AccountAcknowledge = 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
15 changes: 15 additions & 0 deletions automod/engine/fetch_account_meta.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,22 @@ func (e *Engine) GetAccountMeta(ctx context.Context, ident *identity.Identity) (
if rd.Moderation.SubjectStatus.Takendown != nil && *rd.Moderation.SubjectStatus.Takendown == true {
am.Takendown = true
}
if rd.Moderation.SubjectStatus.Appealed != nil && *rd.Moderation.SubjectStatus.Appealed == true {
ap.Appealed = true
}
ap.AccountTags = dedupeStrings(rd.Moderation.SubjectStatus.Tags)
if rd.Moderation.SubjectStatus.ReviewState != nil {
switch *rd.Moderation.SubjectStatus.ReviewState {
case "#reviewOpen":
ap.ReviewState = ReviewStateOpen
case "#reviewEscalated":
ap.ReviewState = ReviewStateEscalated
case "#reviewClosed":
ap.ReviewState = ReviewStateClosed
case "#reviewNonde":
ap.ReviewState = ReviewStateNone
}
}
}
am.Private = &ap
}
Expand Down
12 changes: 11 additions & 1 deletion automod/engine/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,17 @@ var actionNewReportCount = promauto.NewCounterVec(prometheus.CounterOpts{

var actionNewTakedownCount = promauto.NewCounterVec(prometheus.CounterOpts{
Name: "automod_new_action_takedowns",
Help: "Number of new flags persisted",
Help: "Number of new takedowns",
}, []string{"type"})

var actionNewEscalationCount = promauto.NewCounterVec(prometheus.CounterOpts{
Name: "automod_new_action_escalations",
Help: "Number of new subject escalations",
}, []string{"type"})

var actionNewAcknowledgeCount = promauto.NewCounterVec(prometheus.CounterOpts{
Name: "automod_new_action_acknowledges",
Help: "Number of new subjects acknowledged",
}, []string{"type"})

var accountMetaFetches = promauto.NewCounter(prometheus.CounterOpts{
Expand Down
72 changes: 70 additions & 2 deletions automod/engine/persist.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,28 @@ func (eng *Engine) persistAccountModActions(c *AccountContext) error {
if err != nil {
return fmt.Errorf("circuit-breaking takedowns: %w", err)
}
newEscalation := c.effects.AccountEscalate
if c.Account.Private != nil && c.Account.Private.ReviewState == ReviewStateEscalated {
// de-dupe account escalation
newEscalation = false
} else {
newEscalation, err = eng.circuitBreakModAction(ctx, newEscalation)
if err != nil {
return fmt.Errorf("circuit-breaking escalation: %w", err)
}
}
newAcknowledge := c.effects.AccountAcknowledge
if c.Account.Private != nil && (c.Account.Private.ReviewState == "closed" || c.Account.Private.ReviewState == "none") {
// de-dupe account escalation
newAcknowledge = false
} else {
newAcknowledge, err = eng.circuitBreakModAction(ctx, newAcknowledge)
if err != nil {
return fmt.Errorf("circuit-breaking acknowledge: %w", err)
}
}

anyModActions := newTakedown || len(newLabels) > 0 || len(newFlags) > 0 || len(newReports) > 0
anyModActions := newTakedown || newEscalation || newAcknowledge || len(newLabels) > 0 || len(newFlags) > 0 || len(newReports) > 0
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,9 +165,56 @@ func (eng *Engine) persistAccountModActions(c *AccountContext) error {
if err != nil {
c.Logger.Error("failed to execute account takedown", "err", err)
}

// we don't want to escalate if there is a takedown
newEscalation = false
}

if newEscalation {
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)
}
}

if newAcknowledge {
c.Logger.Warn("account-acknowledge")
actionNewAcknowledgeCount.WithLabelValues("account").Inc()
comment := "[automod]: auto account-acknowledge"
_, err := toolsozone.ModerationEmitEvent(ctx, xrpcc, &toolsozone.ModerationEmitEvent_Input{
CreatedBy: xrpcc.Auth.Did,
Event: &toolsozone.ModerationEmitEvent_Input_Event{
ModerationDefs_ModEventAcknowledge: &toolsozone.ModerationDefs_ModEventAcknowledge{
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 acknowledge", "err", err)
}
}

needCachePurge := newTakedown || len(newLabels) > 0 || len(newFlags) > 0 || createdReports
needCachePurge := newTakedown || newEscalation || newAcknowledge || len(newLabels) > 0 || len(newFlags) > 0 || createdReports
if needCachePurge {
return eng.PurgeAccountCaches(ctx, c.Account.Identity.DID)
}
Expand Down Expand Up @@ -303,5 +370,6 @@ func (eng *Engine) persistRecordModActions(c *RecordContext) error {
c.Logger.Error("failed to execute record takedown", "err", err)
}
}

return nil
}
20 changes: 20 additions & 0 deletions automod/engine/persisthelpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,26 @@ func (eng *Engine) circuitBreakTakedown(ctx context.Context, takedown bool) (boo
return takedown, nil
}

// Combined circuit breaker for miscellaneous mod actions like: escalate, acknowledge
func (eng *Engine) circuitBreakModAction(ctx context.Context, action bool) (bool, error) {
if !action {
return false, nil
}
c, err := eng.Counters.GetCount(ctx, "automod-quota", "mod-action", countstore.PeriodDay)
if err != nil {
return false, fmt.Errorf("checking mod action quota: %w", err)
}
if c >= QuotaModActionDay {
eng.Logger.Warn("CIRCUIT BREAKER: automod action")
return false, nil
}
err = eng.Counters.Increment(ctx, "automod-quota", "mod-action")
if err != nil {
return false, fmt.Errorf("incrementing mod action quota: %w", err)
}
return action, nil
}

// Creates a moderation report, but checks first if there was a similar recent one, and skips if so.
//
// Returns a bool indicating if a new report was created.
Expand Down
Loading