From 6e68a26abea6b814dab5544af192d3950e7f61cf Mon Sep 17 00:00:00 2001 From: Foysal Ahamed Date: Fri, 20 Sep 2024 16:32:22 +0200 Subject: [PATCH] :sparkles: Emit record delete events on post/profile with label and appeal --- automod/countstore/countstore.go | 1 + automod/countstore/countstore_mem.go | 6 +++++ automod/countstore/countstore_redis.go | 12 +++++++++ automod/engine/context.go | 3 +++ automod/engine/effects.go | 11 ++++++++ automod/rules/all.go | 1 + automod/rules/ozone_persist.go | 18 +++++++++---- automod/rules/resolve_appeal_on_delete.go | 31 +++++++++++++++++++++++ 8 files changed, 78 insertions(+), 5 deletions(-) create mode 100644 automod/rules/resolve_appeal_on_delete.go 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..15e7be342 100644 --- a/automod/countstore/countstore_redis.go +++ b/automod/countstore/countstore_redis.go @@ -66,6 +66,18 @@ func (s *RedisCountStore) Increment(ctx context.Context, name, val string) error return err } +// Deletes a counter key +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 d925dab5a..108fde7cf 100644 --- a/automod/engine/context.go +++ b/automod/engine/context.go @@ -244,6 +244,9 @@ 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 806909229..c45519e35 100644 --- a/automod/engine/effects.go +++ b/automod/engine/effects.go @@ -34,6 +34,7 @@ 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 + 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. @@ -79,6 +80,16 @@ func (e *Effects) IncrementPeriod(name, val string, period string) { e.CounterIncrements = append(e.CounterIncrements, CounterRef{Name: name, Val: val, Period: &period}) } +// 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 "distinct value" counter based on the supplied string value ("val") to be incremented at the end of all rule processing. Will automatically increment for all time periods. func (e *Effects) IncrementDistinct(name, bucket, val string) { e.mu.Lock() diff --git a/automod/rules/all.go b/automod/rules/all.go index ca056e46f..713f6722d 100644 --- a/automod/rules/all.go +++ b/automod/rules/all.go @@ -62,6 +62,7 @@ func DefaultRules() automod.RuleSet { }, OzoneEventRules: []automod.OzoneEventRuleFunc{ HarassmentProtectionOzoneEventRule, + MarkAppealOzoneEventRule, }, } return rules diff --git a/automod/rules/ozone_persist.go b/automod/rules/ozone_persist.go index c13c0a84c..756f6aa79 100644 --- a/automod/rules/ozone_persist.go +++ b/automod/rules/ozone_persist.go @@ -2,22 +2,30 @@ package rules import ( "github.com/bluesky-social/indigo/automod" + "github.com/bluesky-social/indigo/automod/countstore" ) var _ automod.RecordRuleFunc = OzoneRecordHistoryPersistRule func OzoneRecordHistoryPersistRule(c *automod.RecordContext) error { - // TODO: flesh out this logic - // based on record type switch c.RecordOp.Collection { case "app.bsky.labeler.service": c.PersistRecordOzoneEvent() case "app.bsky.feed.post": - if c.RecordOp.Action == "update" { + // @TODO: we should probably persist if a deleted post has reports on it but right now, we are not keeping track of reports on records + if c.RecordOp.Action == "delete" { + // If a post being deleted has an active appeal, persist the event + if c.GetCount("appeal", c.RecordOp.ATURI().String(), countstore.PeriodTotal) > 0 { + c.PersistRecordOzoneEvent() + } + } case "app.bsky.actor.profile": - // TODO: fix this logic - if len(c.Account.AccountLabels) > 2 { + hasLabels := len(c.Account.AccountLabels) > 0 + // If there is an appeal on the account or the profile record + // Appeal counts are reset when appeals are resolved so this should only be true when there is an unresolved appeal + hasAppeals := c.GetCount("appeal", c.Account.Identity.DID.String(), countstore.PeriodTotal) > 0 || c.GetCount("appeal", c.RecordOp.ATURI().String(), countstore.PeriodTotal) > 0 + if hasLabels || hasAppeals { c.PersistRecordOzoneEvent() } } diff --git a/automod/rules/resolve_appeal_on_delete.go b/automod/rules/resolve_appeal_on_delete.go new file mode 100644 index 000000000..dc228dbd2 --- /dev/null +++ b/automod/rules/resolve_appeal_on_delete.go @@ -0,0 +1,31 @@ +package rules + +import ( + "github.com/bluesky-social/indigo/automod" +) + +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("appeal", counterKey) + } else { + c.ResetCounter("appeal", counterKey) + } + + return nil +}