From 97110ad9a0083f3ab10e74dbd9fc0e148142bba5 Mon Sep 17 00:00:00 2001 From: Foysal Ahamed Date: Mon, 2 Sep 2024 21:35:15 +0200 Subject: [PATCH 1/8] :sparkles: Add combo hashtag rule --- automod/rules/hashtags.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/automod/rules/hashtags.go b/automod/rules/hashtags.go index c6d734807..62b663179 100644 --- a/automod/rules/hashtags.go +++ b/automod/rules/hashtags.go @@ -10,6 +10,11 @@ import ( // looks for specific hashtags from known lists func BadHashtagsPostRule(c *automod.RecordContext, post *appbsky.FeedPost) error { + // In cases where we have a soft signaling hashtag that are problematic in combination with other hashtags + // we'd want to check the record contains the combination before taking any action + potentiallyBadHashtags := []string{} + potentiallyBadHashtagCombo := []string{} + for _, tag := range ExtractHashtagsPost(post) { tag = NormalizeHashtag(tag) // skip some bad-word hashtags which frequently false-positive @@ -19,6 +24,16 @@ func BadHashtagsPostRule(c *automod.RecordContext, post *appbsky.FeedPost) error if c.InSet("bad-hashtags", tag) || c.InSet("bad-words", tag) { c.AddRecordFlag("bad-hashtag") c.ReportRecord(automod.ReportReasonRude, fmt.Sprintf("possible bad word in hashtags: %s", tag)) + + if c.InSet("potentially-bad-hashtags", tag) { + potentiallyBadHashtags = append(potentiallyBadHashtags, tag) + break + } + if c.InSet("potentially-bad-hashtag-combos", tag) { + potentiallyBadHashtagCombo = append(potentiallyBadHashtagCombo, tag) + break + } + break } word := keyword.SlugContainsExplicitSlur(keyword.Slugify(tag)) @@ -28,6 +43,13 @@ func BadHashtagsPostRule(c *automod.RecordContext, post *appbsky.FeedPost) error break } } + + if len(potentiallyBadHashtagCombo) > 0 && len(potentiallyBadHashtags) > 0 { + c.AddRecordFlag("bad-hashtag-combo") + c.ReportRecord(automod.ReportReasonRude, fmt.Sprintf("bad hashtag combo: %s %s. account will be taken down", potentiallyBadHashtagCombo, potentiallyBadHashtags)) + c.TakedownAccount() + } + return nil } From a5a103c0dcf646f6bc68162e39ac130312708a29 Mon Sep 17 00:00:00 2001 From: Foysal Ahamed Date: Tue, 3 Sep 2024 20:40:01 +0200 Subject: [PATCH 2/8] :sparkles: Add rule to resolve appeal on removed record --- api/ozone/moderationemitEvent.go | 8 +++++++ automod/engine/context.go | 4 ++++ automod/engine/effects.go | 9 +++++++ automod/engine/engine_ozone.go | 1 + automod/engine/persist.go | 29 +++++++++++++++++++++-- automod/engine/persisthelpers.go | 19 +++++++++++++++ automod/rules/all.go | 1 + automod/rules/resolve_appeal_on_delete.go | 15 ++++++++++++ 8 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 automod/rules/resolve_appeal_on_delete.go diff --git a/api/ozone/moderationemitEvent.go b/api/ozone/moderationemitEvent.go index ebd8662c8..064eef3a6 100644 --- a/api/ozone/moderationemitEvent.go +++ b/api/ozone/moderationemitEvent.go @@ -34,6 +34,7 @@ type ModerationEmitEvent_Input_Event struct { ModerationDefs_ModEventMuteReporter *ModerationDefs_ModEventMuteReporter ModerationDefs_ModEventUnmuteReporter *ModerationDefs_ModEventUnmuteReporter ModerationDefs_ModEventReverseTakedown *ModerationDefs_ModEventReverseTakedown + ModerationDefs_ModEventResolveAppeal *ModerationDefs_ModEventResolveAppeal ModerationDefs_ModEventEmail *ModerationDefs_ModEventEmail ModerationDefs_ModEventTag *ModerationDefs_ModEventTag } @@ -83,6 +84,10 @@ func (t *ModerationEmitEvent_Input_Event) MarshalJSON() ([]byte, error) { t.ModerationDefs_ModEventReverseTakedown.LexiconTypeID = "tools.ozone.moderation.defs#modEventReverseTakedown" return json.Marshal(t.ModerationDefs_ModEventReverseTakedown) } + if t.ModerationDefs_ModEventResolveAppeal != nil { + t.ModerationDefs_ModEventResolveAppeal.LexiconTypeID = "tools.ozone.moderation.defs#modEventResolveAppeal" + return json.Marshal(t.ModerationDefs_ModEventResolveAppeal) + } if t.ModerationDefs_ModEventEmail != nil { t.ModerationDefs_ModEventEmail.LexiconTypeID = "tools.ozone.moderation.defs#modEventEmail" return json.Marshal(t.ModerationDefs_ModEventEmail) @@ -133,6 +138,9 @@ func (t *ModerationEmitEvent_Input_Event) UnmarshalJSON(b []byte) error { case "tools.ozone.moderation.defs#modEventReverseTakedown": t.ModerationDefs_ModEventReverseTakedown = new(ModerationDefs_ModEventReverseTakedown) return json.Unmarshal(b, t.ModerationDefs_ModEventReverseTakedown) + case "tools.ozone.moderation.defs#modEventResolveAppeal": + t.ModerationDefs_ModEventResolveAppeal = new(ModerationDefs_ModEventResolveAppeal) + return json.Unmarshal(b, t.ModerationDefs_ModEventResolveAppeal) case "tools.ozone.moderation.defs#modEventEmail": t.ModerationDefs_ModEventEmail = new(ModerationDefs_ModEventEmail) return json.Unmarshal(b, t.ModerationDefs_ModEventEmail) diff --git a/automod/engine/context.go b/automod/engine/context.go index cc5b6a5e8..c313f7941 100644 --- a/automod/engine/context.go +++ b/automod/engine/context.go @@ -288,6 +288,10 @@ func (c *RecordContext) TakedownRecord() { c.effects.TakedownRecord() } +func (c *RecordContext) ResolveRecordAppeal() { + c.effects.ResolveRecordAppeal() +} + func (c *RecordContext) TakedownBlob(cid string) { c.effects.TakedownBlob(cid) } diff --git a/automod/engine/effects.go b/automod/engine/effects.go index ed5ac2998..1f8d555e4 100644 --- a/automod/engine/effects.go +++ b/automod/engine/effects.go @@ -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 appeal resolutions automod can action per day, for all subjects combined (circuit breaker) + QuotaModResolveAppealDay = 200 ) type CounterRef struct { @@ -58,6 +60,8 @@ type Effects struct { RejectEvent bool // Services, if any, which should blast out a notification about this even (eg, Slack) NotifyServices []string + // If "true", indicates that a rule indicates that any appeal on the record should be resolved + RecordAppealResolve bool } // Enqueues the named counter to be incremented at the end of all rule processing. Will automatically increment for all time periods. @@ -172,6 +176,11 @@ func (e *Effects) TakedownRecord() { e.RecordTakedown = true } +// Enqueues the record's appeals to be resolved at the end of rule processing. +func (e *Effects) ResolveRecordAppeal() { + e.RecordAppealResolve = true +} + // Enqueues the blob CID to be taken down (aka, CDN purge) as part of any record takedown func (e *Effects) TakedownBlob(cid string) { e.mu.Lock() diff --git a/automod/engine/engine_ozone.go b/automod/engine/engine_ozone.go index bbe354225..a21b31ca6 100644 --- a/automod/engine/engine_ozone.go +++ b/automod/engine/engine_ozone.go @@ -212,6 +212,7 @@ func (e *Engine) CanonicalLogLineOzoneEvent(c *OzoneEventContext) { "accountReports", len(c.effects.AccountReports), "recordLabels", c.effects.RecordLabels, "recordFlags", c.effects.RecordFlags, + "RecordAppealResolve", c.effects.RecordAppealResolve, "recordTakedown", c.effects.RecordTakedown, "recordReports", len(c.effects.RecordReports), ) diff --git a/automod/engine/persist.go b/automod/engine/persist.go index 7d6675bbf..05b0d850f 100644 --- a/automod/engine/persist.go +++ b/automod/engine/persist.go @@ -211,7 +211,12 @@ func (eng *Engine) persistRecordModActions(c *RecordContext) error { return fmt.Errorf("failed to circuit break takedowns: %w", err) } - if newTakedown || len(newLabels) > 0 || len(newFlags) > 0 || len(newReports) > 0 { + resolveAppeal, err := eng.circuitBreakResolveAppeal(ctx, c.effects.RecordAppealResolve) + if err != nil { + return fmt.Errorf("failed to circuit break resolve appeal: %w", err) + } + + if newTakedown || len(newLabels) > 0 || len(newFlags) > 0 || len(newReports) > 0 || resolveAppeal { if eng.Notifier != nil { for _, srv := range dedupeStrings(c.effects.NotifyServices) { if err := eng.Notifier.SendRecord(ctx, srv, c); err != nil { @@ -231,7 +236,7 @@ func (eng *Engine) persistRecordModActions(c *RecordContext) error { } // exit early - if !newTakedown && len(newLabels) == 0 && len(newReports) == 0 { + if !newTakedown && len(newLabels) == 0 && len(newReports) == 0 && !resolveAppeal { return nil } @@ -303,5 +308,25 @@ func (eng *Engine) persistRecordModActions(c *RecordContext) error { c.Logger.Error("failed to execute record takedown", "err", err) } } + + if resolveAppeal { + c.Logger.Warn("record-resolve-appeal") + actionNewTakedownCount.WithLabelValues("record").Inc() + comment := "[automod]: automated appeal resolution due to content deletion" + _, err := toolsozone.ModerationEmitEvent(ctx, xrpcc, &toolsozone.ModerationEmitEvent_Input{ + CreatedBy: xrpcc.Auth.Did, + Event: &toolsozone.ModerationEmitEvent_Input_Event{ + ModerationDefs_ModEventResolveAppeal: &toolsozone.ModerationDefs_ModEventResolveAppeal{ + Comment: &comment, + }, + }, + Subject: &toolsozone.ModerationEmitEvent_Input_Subject{ + RepoStrongRef: &strongRef, + }, + }) + if err != nil { + c.Logger.Error("failed to execute appeal resolve", "err", err) + } + } return nil } diff --git a/automod/engine/persisthelpers.go b/automod/engine/persisthelpers.go index c224cc4ab..daecb4b0a 100644 --- a/automod/engine/persisthelpers.go +++ b/automod/engine/persisthelpers.go @@ -111,6 +111,25 @@ func (eng *Engine) circuitBreakTakedown(ctx context.Context, takedown bool) (boo return takedown, nil } +func (eng *Engine) circuitBreakResolveAppeal(ctx context.Context, resolveAppeal bool) (bool, error) { + if !resolveAppeal { + return false, nil + } + c, err := eng.Counters.GetCount(ctx, "automod-quota", "resolveAppeal", countstore.PeriodDay) + if err != nil { + return false, fmt.Errorf("checking resolve appeal action quota: %w", err) + } + if c >= QuotaModResolveAppealDay { + eng.Logger.Warn("CIRCUIT BREAKER: automod appeal resolution") + return false, nil + } + err = eng.Counters.Increment(ctx, "automod-quota", "resolveAppeal") + if err != nil { + return false, fmt.Errorf("incrementing resolve appeal action quota: %w", err) + } + return resolveAppeal, 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. diff --git a/automod/rules/all.go b/automod/rules/all.go index 33f9c16a6..739f6efe0 100644 --- a/automod/rules/all.go +++ b/automod/rules/all.go @@ -42,6 +42,7 @@ func DefaultRules() automod.RuleSet { }, RecordDeleteRules: []automod.RecordRuleFunc{ DeleteInteractionRule, + ResolveAppealOnDeleteRule, }, IdentityRules: []automod.IdentityRuleFunc{ NewAccountRule, diff --git a/automod/rules/resolve_appeal_on_delete.go b/automod/rules/resolve_appeal_on_delete.go new file mode 100644 index 000000000..4c3dc378b --- /dev/null +++ b/automod/rules/resolve_appeal_on_delete.go @@ -0,0 +1,15 @@ +package rules + +import ( + "github.com/bluesky-social/indigo/automod" +) + +var _ automod.RecordRuleFunc = ResolveAppealOnDeleteRule + +func ResolveAppealOnDeleteRule(c *automod.RecordContext) error { + switch c.RecordOp.Collection { + case "app.bsky.feed.post": + c.ResolveRecordAppeal() + } + return nil +} From 2d34f6e1a81c9edb852bda3d81af85e88f6125a6 Mon Sep 17 00:00:00 2001 From: Foysal Ahamed Date: Tue, 10 Sep 2024 00:10:05 +0200 Subject: [PATCH 3/8] :sparkles: Flag appeals and check flag before resolving appeals on deleted account/record --- automod/engine/context.go | 4 ++ automod/engine/effects.go | 18 ++++++++ automod/rules/all.go | 4 +- automod/rules/resolve_appeal_on_delete.go | 51 +++++++++++++++++++++-- 4 files changed, 73 insertions(+), 4 deletions(-) diff --git a/automod/engine/context.go b/automod/engine/context.go index c313f7941..2da160cfa 100644 --- a/automod/engine/context.go +++ b/automod/engine/context.go @@ -272,6 +272,10 @@ func (c *AccountContext) TakedownAccount() { c.effects.TakedownAccount() } +func (c *AccountContext) ResolveAccountAppeal() { + c.effects.ResolveAccountAppeal() +} + func (c *RecordContext) AddRecordFlag(val string) { c.effects.AddRecordFlag(val) } diff --git a/automod/engine/effects.go b/automod/engine/effects.go index 1f8d555e4..de38e26b7 100644 --- a/automod/engine/effects.go +++ b/automod/engine/effects.go @@ -46,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 appeals on the account should be resolved. + AccountAppealResolve bool // Same as "AccountLabels", but at record-level RecordLabels []string // Same as "AccountFlags", but at record-level @@ -132,6 +134,11 @@ func (e *Effects) TakedownAccount() { e.AccountTakedown = true } +// Enqueues the accounts's appeals to be resolved at the end of rule processing. +func (e *Effects) ResolveAccountAppeal() { + e.AccountAppealResolve = 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() @@ -156,6 +163,17 @@ func (e *Effects) AddRecordFlag(val string) { e.RecordFlags = append(e.RecordFlags, val) } +// Enqueues the provided flag (string value) to be removed (in the Engine's flagstore) at the end of rule processing. +func (e *Effects) RemoveRecordFlag(val string) { + e.mu.Lock() + defer e.mu.Unlock() + for i, v := range e.RecordFlags { + if v == val { + e.RecordFlags = append(e.RecordFlags[:i], e.RecordFlags[i+1:]...) + } + } +} + // Enqueues a moderation report to be filed against the record at the end of rule processing. func (e *Effects) ReportRecord(reason, comment string) { e.mu.Lock() diff --git a/automod/rules/all.go b/automod/rules/all.go index 27faea404..8cc0a768e 100644 --- a/automod/rules/all.go +++ b/automod/rules/all.go @@ -45,7 +45,7 @@ func DefaultRules() automod.RuleSet { }, RecordDeleteRules: []automod.RecordRuleFunc{ DeleteInteractionRule, - ResolveAppealOnDeleteRule, + ResolveAppealOnRecordDeleteRule, }, IdentityRules: []automod.IdentityRuleFunc{ NewAccountRule, @@ -53,6 +53,7 @@ func DefaultRules() automod.RuleSet { BadWordDIDRule, NewAccountBotEmailRule, CelebSpamIdentityRule, + ResolveAppealOnAccountDeleteRule, }, BlobRules: []automod.BlobRuleFunc{ //BlobVerifyRule, @@ -62,6 +63,7 @@ func DefaultRules() automod.RuleSet { }, OzoneEventRules: []automod.OzoneEventRuleFunc{ HarassmentProtectionOzoneEventRule, + MarkAppealOzoneEventRule, }, } return rules diff --git a/automod/rules/resolve_appeal_on_delete.go b/automod/rules/resolve_appeal_on_delete.go index 4c3dc378b..c042ef9ce 100644 --- a/automod/rules/resolve_appeal_on_delete.go +++ b/automod/rules/resolve_appeal_on_delete.go @@ -2,14 +2,59 @@ package rules import ( "github.com/bluesky-social/indigo/automod" + "github.com/bluesky-social/indigo/automod/countstore" ) -var _ automod.RecordRuleFunc = ResolveAppealOnDeleteRule +var _ automod.RecordRuleFunc = ResolveAppealOnRecordDeleteRule -func ResolveAppealOnDeleteRule(c *automod.RecordContext) error { +func ResolveAppealOnRecordDeleteRule(c *automod.RecordContext) error { switch c.RecordOp.Collection { case "app.bsky.feed.post": - c.ResolveRecordAppeal() + hasAppeal := c.GetCount("appeal", c.RecordOp.ATURI().String(), countstore.PeriodTotal) + + if hasAppeal > 0 { + c.ResolveRecordAppeal() + } + } + return nil +} + +var _ automod.IdentityRuleFunc = ResolveAppealOnAccountDeleteRule + +func ResolveAppealOnAccountDeleteRule(c *automod.AccountContext) error { + hasAppeal := c.GetCount("appeal", c.Account.Identity.DID.String(), countstore.PeriodTotal) + + // @TODO: Check here that we check if the account has been deleted or not before resolving + // This is not currently available on the context + if hasAppeal > 0 && (c.Account.Deactivated) { + c.ResolveAccountAppeal() + } + return nil +} + +var _ automod.OzoneEventRuleFunc = MarkAppealOzoneEventRule + +// looks for appeals on records/accounts and flags subjects +func MarkAppealOzoneEventRule(c *automod.OzoneEventContext) error { + isResolveAppealEvent := c.Event.Event.ModerationDefs_ModEventResolveAppeal != nil + // appeals are just report events emitted by the author of the reported content with a special report type + isAppealEvent := c.Event.Event.ModerationDefs_ModEventReport != nil && *c.Event.Event.ModerationDefs_ModEventReport.ReportType == "com.atproto.moderation.defs#reasonAppeal" + + if !isAppealEvent && !isResolveAppealEvent { + return nil } + + counterKey := c.Event.SubjectDID.String() + if c.Event.SubjectURI != nil { + counterKey = c.Event.SubjectURI.String() + } + + if isAppealEvent { + c.Increment("appealed", counterKey) + } else { + // @TODO: We should reset the appeal counter here but there doesn't seem to be an api for it + // c.Reset("appealed", counterKey) + } + return nil } From 268065e87d2ddb03f55addbdda19aa7745764f00 Mon Sep 17 00:00:00 2001 From: Foysal Ahamed Date: Fri, 20 Sep 2024 11:24:21 +0200 Subject: [PATCH 4/8] :sparkles: Add appeal count reset --- automod/countstore/countstore.go | 1 + automod/countstore/countstore_mem.go | 6 ++++++ automod/countstore/countstore_redis.go | 11 +++++++++++ automod/engine/context.go | 3 +++ automod/engine/effects.go | 12 ++++++++++++ automod/engine/persist.go | 6 ++++++ automod/rules/resolve_appeal_on_delete.go | 5 ++--- 7 files changed, 41 insertions(+), 3 deletions(-) diff --git a/automod/countstore/countstore.go b/automod/countstore/countstore.go index a3ae2b9d1..5531541fd 100644 --- a/automod/countstore/countstore.go +++ b/automod/countstore/countstore.go @@ -48,6 +48,7 @@ type CountStore interface { // TODO: batch increment method GetCountDistinct(ctx context.Context, name, bucket, period string) (int, error) IncrementDistinct(ctx context.Context, name, bucket, val string) error + Reset(ctx context.Context, name, val string) error } func periodBucket(name, val, period string) string { diff --git a/automod/countstore/countstore_mem.go b/automod/countstore/countstore_mem.go index f33c076ee..679ed9e9c 100644 --- a/automod/countstore/countstore_mem.go +++ b/automod/countstore/countstore_mem.go @@ -40,6 +40,12 @@ func (s MemCountStore) Increment(ctx context.Context, name, val string) error { return nil } +func (s *MemCountStore) Reset(ctx context.Context, name, val string) error { + key := periodBucket(name, val, PeriodTotal) + s.Counts.Delete(key) + return nil +} + func (s MemCountStore) IncrementPeriod(ctx context.Context, name, val, period string) error { k := periodBucket(name, val, period) s.Counts.Compute(k, func(oldVal int, _ bool) (int, bool) { diff --git a/automod/countstore/countstore_redis.go b/automod/countstore/countstore_redis.go index 2e42c96f8..89bb8bc30 100644 --- a/automod/countstore/countstore_redis.go +++ b/automod/countstore/countstore_redis.go @@ -66,6 +66,17 @@ func (s *RedisCountStore) Increment(ctx context.Context, name, val string) error return err } +func (s *RedisCountStore) Reset(ctx context.Context, name, val string) error { + var key string + + // increment multiple counters in a single redis round-trip + multi := s.Client.Pipeline() + key = redisCountPrefix + periodBucket(name, val, PeriodHour) + multi.Del(ctx, key) + _, err := multi.Exec(ctx) + return err +} + // Variant of Increment() which only acts on a single specified time period. The intended us of this variant is to control the total number of counters persisted, by using a relatively short time period, for which the counters will expire. func (s *RedisCountStore) IncrementPeriod(ctx context.Context, name, val, period string) error { diff --git a/automod/engine/context.go b/automod/engine/context.go index 2da160cfa..293d7e65e 100644 --- a/automod/engine/context.go +++ b/automod/engine/context.go @@ -243,6 +243,9 @@ func (c *BaseContext) GetAccountMeta(did syntax.DID) *AccountMeta { func (c *BaseContext) Increment(name, val string) { c.effects.Increment(name, val) } +func (c *BaseContext) ResetCounter(name, val string) { + c.effects.ResetCounter(name, val) +} func (c *BaseContext) IncrementDistinct(name, bucket, val string) { c.effects.IncrementDistinct(name, bucket, val) diff --git a/automod/engine/effects.go b/automod/engine/effects.go index de38e26b7..b140ccad2 100644 --- a/automod/engine/effects.go +++ b/automod/engine/effects.go @@ -36,6 +36,8 @@ type Effects struct { mu sync.Mutex // List of counters which should be incremented as part of processing this event. These are collected during rule execution and persisted in bulk at the end. CounterIncrements []CounterRef + // List of counters which should be reset as part of processing this event. These are collected during rule execution and persisted in bulk at the end. + CounterResets []CounterRef // Similar to "CounterIncrements", but for "distinct" style counters CounterDistinctIncrements []CounterDistinctRef // TODO: better variable names // Label values which should be applied to the overall account, as a result of rule execution. @@ -76,6 +78,16 @@ func (e *Effects) Increment(name, val string) { e.CounterIncrements = append(e.CounterIncrements, CounterRef{Name: name, Val: val}) } +// Enqueues the named counter to be reset at the end of all rule processing. +// +// "name" is the counter namespace. +// "val" is the specific counter with that namespace. +func (e *Effects) ResetCounter(name, val string) { + e.mu.Lock() + defer e.mu.Unlock() + e.CounterResets = append(e.CounterResets, CounterRef{Name: name, Val: val}) +} + // Enqueues the named counter to be incremented at the end of all rule processing. Will only increment the indicated time period bucket. func (e *Effects) IncrementPeriod(name, val string, period string) { e.mu.Lock() diff --git a/automod/engine/persist.go b/automod/engine/persist.go index 05b0d850f..2f876823d 100644 --- a/automod/engine/persist.go +++ b/automod/engine/persist.go @@ -29,6 +29,12 @@ func (eng *Engine) persistCounters(ctx context.Context, eff *Effects) error { return err } } + for _, ref := range eff.CounterResets { + err := eng.Counters.Reset(ctx, ref.Name, ref.Val) + if err != nil { + return err + } + } return nil } diff --git a/automod/rules/resolve_appeal_on_delete.go b/automod/rules/resolve_appeal_on_delete.go index c042ef9ce..3b756e80e 100644 --- a/automod/rules/resolve_appeal_on_delete.go +++ b/automod/rules/resolve_appeal_on_delete.go @@ -50,10 +50,9 @@ func MarkAppealOzoneEventRule(c *automod.OzoneEventContext) error { } if isAppealEvent { - c.Increment("appealed", counterKey) + c.Increment("appeal", counterKey) } else { - // @TODO: We should reset the appeal counter here but there doesn't seem to be an api for it - // c.Reset("appealed", counterKey) + c.ResetCounter("appeal", counterKey) } return nil From 05607b4d17c5562fa66eba1d4aabeec5a725026d Mon Sep 17 00:00:00 2001 From: Foysal Ahamed Date: Tue, 15 Oct 2024 12:28:05 +0200 Subject: [PATCH 5/8] :pencil2: Fix typo --- automod/engine/engine_ozone.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/automod/engine/engine_ozone.go b/automod/engine/engine_ozone.go index a21b31ca6..18ee58a1e 100644 --- a/automod/engine/engine_ozone.go +++ b/automod/engine/engine_ozone.go @@ -212,7 +212,7 @@ func (e *Engine) CanonicalLogLineOzoneEvent(c *OzoneEventContext) { "accountReports", len(c.effects.AccountReports), "recordLabels", c.effects.RecordLabels, "recordFlags", c.effects.RecordFlags, - "RecordAppealResolve", c.effects.RecordAppealResolve, + "recordAppealResolve", c.effects.RecordAppealResolve, "recordTakedown", c.effects.RecordTakedown, "recordReports", len(c.effects.RecordReports), ) From 4bd2cf460febe901f2142435b53064ca84b65769 Mon Sep 17 00:00:00 2001 From: Foysal Ahamed Date: Tue, 15 Oct 2024 15:23:31 +0200 Subject: [PATCH 6/8] :bug: Fix memory and redis counterstore reset implementation --- automod/countstore/countstore_mem.go | 13 +++++++++++-- automod/countstore/countstore_redis.go | 10 +++++++++- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/automod/countstore/countstore_mem.go b/automod/countstore/countstore_mem.go index 679ed9e9c..43bfa06e0 100644 --- a/automod/countstore/countstore_mem.go +++ b/automod/countstore/countstore_mem.go @@ -40,8 +40,17 @@ func (s MemCountStore) Increment(ctx context.Context, name, val string) error { return nil } -func (s *MemCountStore) Reset(ctx context.Context, name, val string) error { - key := periodBucket(name, val, PeriodTotal) +func (s MemCountStore) Reset(ctx context.Context, name, val string) error { + for _, p := range []string{PeriodTotal, PeriodDay, PeriodHour} { + if err := s.ResetPeriod(ctx, name, val, p); err != nil { + return err + } + } + return nil +} + +func (s MemCountStore) ResetPeriod(ctx context.Context, name, val, period string) error { + key := periodBucket(name, val, period) s.Counts.Delete(key) return nil } diff --git a/automod/countstore/countstore_redis.go b/automod/countstore/countstore_redis.go index 89bb8bc30..cc5c38846 100644 --- a/automod/countstore/countstore_redis.go +++ b/automod/countstore/countstore_redis.go @@ -69,10 +69,18 @@ func (s *RedisCountStore) Increment(ctx context.Context, name, val string) error func (s *RedisCountStore) Reset(ctx context.Context, name, val string) error { var key string - // increment multiple counters in a single redis round-trip + // delete multiple counters in a single redis round-trip multi := s.Client.Pipeline() + key = redisCountPrefix + periodBucket(name, val, PeriodHour) multi.Del(ctx, key) + + key = redisCountPrefix + periodBucket(name, val, PeriodDay) + multi.Del(ctx, key) + + key = redisCountPrefix + periodBucket(name, val, PeriodTotal) + multi.Del(ctx, key) + _, err := multi.Exec(ctx) return err } From 40079e32d24375373de91de61ef7be67a698cdba Mon Sep 17 00:00:00 2001 From: Foysal Ahamed Date: Tue, 15 Oct 2024 15:28:20 +0200 Subject: [PATCH 7/8] :broom: Cleanup unnecessary hashtag rule --- automod/rules/hashtags.go | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/automod/rules/hashtags.go b/automod/rules/hashtags.go index 62b663179..612197b16 100644 --- a/automod/rules/hashtags.go +++ b/automod/rules/hashtags.go @@ -10,11 +10,6 @@ import ( // looks for specific hashtags from known lists func BadHashtagsPostRule(c *automod.RecordContext, post *appbsky.FeedPost) error { - // In cases where we have a soft signaling hashtag that are problematic in combination with other hashtags - // we'd want to check the record contains the combination before taking any action - potentiallyBadHashtags := []string{} - potentiallyBadHashtagCombo := []string{} - for _, tag := range ExtractHashtagsPost(post) { tag = NormalizeHashtag(tag) // skip some bad-word hashtags which frequently false-positive @@ -24,16 +19,6 @@ func BadHashtagsPostRule(c *automod.RecordContext, post *appbsky.FeedPost) error if c.InSet("bad-hashtags", tag) || c.InSet("bad-words", tag) { c.AddRecordFlag("bad-hashtag") c.ReportRecord(automod.ReportReasonRude, fmt.Sprintf("possible bad word in hashtags: %s", tag)) - - if c.InSet("potentially-bad-hashtags", tag) { - potentiallyBadHashtags = append(potentiallyBadHashtags, tag) - break - } - if c.InSet("potentially-bad-hashtag-combos", tag) { - potentiallyBadHashtagCombo = append(potentiallyBadHashtagCombo, tag) - break - } - break } word := keyword.SlugContainsExplicitSlur(keyword.Slugify(tag)) @@ -44,12 +29,6 @@ func BadHashtagsPostRule(c *automod.RecordContext, post *appbsky.FeedPost) error } } - if len(potentiallyBadHashtagCombo) > 0 && len(potentiallyBadHashtags) > 0 { - c.AddRecordFlag("bad-hashtag-combo") - c.ReportRecord(automod.ReportReasonRude, fmt.Sprintf("bad hashtag combo: %s %s. account will be taken down", potentiallyBadHashtagCombo, potentiallyBadHashtags)) - c.TakedownAccount() - } - return nil } From 43b2356189e86a1701191c80e48d3d18499d1e56 Mon Sep 17 00:00:00 2001 From: Foysal Ahamed Date: Sat, 16 Nov 2024 21:16:50 +0100 Subject: [PATCH 8/8] :sparkles: Adjust resolving appeal with refactored code --- automod/engine/persist.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/automod/engine/persist.go b/automod/engine/persist.go index 170ac0452..8bac82df3 100644 --- a/automod/engine/persist.go +++ b/automod/engine/persist.go @@ -272,7 +272,8 @@ func (eng *Engine) persistRecordModActions(c *RecordContext) error { atURI := c.RecordOp.ATURI().String() newLabels := dedupeStrings(c.effects.RecordLabels) newTags := dedupeStrings(c.effects.RecordTags) - if (len(newLabels) > 0 || len(newTags) > 0) && eng.OzoneClient != nil { + resolveAppeal := c.effects.RecordAppealResolve + if (len(newLabels) > 0 || len(newTags) > 0 || resolveAppeal) && eng.OzoneClient != nil { // fetch existing record labels, tags, etc rv, err := toolsozone.ModerationGetRecord(ctx, eng.OzoneClient, c.RecordOp.CID.String(), c.RecordOp.ATURI().String()) if err != nil { @@ -296,6 +297,7 @@ func (eng *Engine) persistRecordModActions(c *RecordContext) error { existingTags = rv.Moderation.SubjectStatus.Tags } newTags = dedupeTagActions(newTags, existingTags) + resolveAppeal = resolveAppeal && *rv.Moderation.SubjectStatus.Appealed } } @@ -323,7 +325,7 @@ func (eng *Engine) persistRecordModActions(c *RecordContext) error { return fmt.Errorf("failed to circuit break takedowns: %w", err) } - resolveAppeal, err := eng.circuitBreakModAction(ctx, c.effects.RecordAppealResolve) + resolveAppeal, err = eng.circuitBreakModAction(ctx, resolveAppeal) if err != nil { return fmt.Errorf("failed to circuit break resolve appeal: %w", err) } @@ -444,6 +446,7 @@ func (eng *Engine) persistRecordModActions(c *RecordContext) error { if err != nil { c.Logger.Error("failed to execute record takedown", "err", err) } + resolveAppeal = false } if resolveAppeal {