diff --git a/automod/action_dedupe_test.go b/automod/action_dedupe_test.go index 64462626c..efb28d79a 100644 --- a/automod/action_dedupe_test.go +++ b/automod/action_dedupe_test.go @@ -7,6 +7,7 @@ import ( appbsky "github.com/bluesky-social/indigo/api/bsky" "github.com/bluesky-social/indigo/atproto/identity" "github.com/bluesky-social/indigo/atproto/syntax" + "github.com/bluesky-social/indigo/automod/countstore" "github.com/stretchr/testify/assert" ) @@ -39,7 +40,7 @@ func TestAccountReportDedupe(t *testing.T) { assert.NoError(engine.ProcessRecord(ctx, id1.DID, path, cid1, &p1)) } - reports, err := engine.GetCount("automod-quota", "report", PeriodDay) + reports, err := engine.GetCount("automod-quota", "report", countstore.PeriodDay) assert.NoError(err) assert.Equal(1, reports) } diff --git a/automod/capture_test.go b/automod/capture_test.go index d5e6ee255..933888ba9 100644 --- a/automod/capture_test.go +++ b/automod/capture_test.go @@ -3,6 +3,7 @@ package automod import ( "testing" + "github.com/bluesky-social/indigo/automod/countstore" "github.com/stretchr/testify/assert" ) @@ -12,10 +13,10 @@ func TestNoOpCaptureReplyRule(t *testing.T) { engine := EngineTestFixture() capture := MustLoadCapture("testdata/capture_atprotocom.json") assert.NoError(ProcessCaptureRules(&engine, capture)) - c, err := engine.GetCount("automod-quota", "report", PeriodDay) + c, err := engine.GetCount("automod-quota", "report", countstore.PeriodDay) assert.NoError(err) assert.Equal(0, c) - c, err = engine.GetCount("automod-quota", "takedown", PeriodDay) + c, err = engine.GetCount("automod-quota", "takedown", countstore.PeriodDay) assert.NoError(err) assert.Equal(0, c) } diff --git a/automod/circuit_breaker_test.go b/automod/circuit_breaker_test.go index cd33dd770..3e55e7a73 100644 --- a/automod/circuit_breaker_test.go +++ b/automod/circuit_breaker_test.go @@ -8,6 +8,7 @@ import ( appbsky "github.com/bluesky-social/indigo/api/bsky" "github.com/bluesky-social/indigo/atproto/identity" "github.com/bluesky-social/indigo/atproto/syntax" + "github.com/bluesky-social/indigo/automod/countstore" "github.com/stretchr/testify/assert" ) @@ -49,11 +50,11 @@ func TestTakedownCircuitBreaker(t *testing.T) { assert.NoError(engine.ProcessRecord(ctx, ident.DID, path, cid1, &p1)) } - takedowns, err := engine.GetCount("automod-quota", "takedown", PeriodDay) + takedowns, err := engine.GetCount("automod-quota", "takedown", countstore.PeriodDay) assert.NoError(err) assert.Equal(QuotaModTakedownDay, takedowns) - reports, err := engine.GetCount("automod-quota", "report", PeriodDay) + reports, err := engine.GetCount("automod-quota", "report", countstore.PeriodDay) assert.NoError(err) assert.Equal(0, reports) } @@ -84,11 +85,11 @@ func TestReportCircuitBreaker(t *testing.T) { assert.NoError(engine.ProcessRecord(ctx, ident.DID, path, cid1, &p1)) } - takedowns, err := engine.GetCount("automod-quota", "takedown", PeriodDay) + takedowns, err := engine.GetCount("automod-quota", "takedown", countstore.PeriodDay) assert.NoError(err) assert.Equal(0, takedowns) - reports, err := engine.GetCount("automod-quota", "report", PeriodDay) + reports, err := engine.GetCount("automod-quota", "report", countstore.PeriodDay) assert.NoError(err) assert.Equal(QuotaModReportDay, reports) } diff --git a/automod/countstore.go b/automod/countstore.go deleted file mode 100644 index 4011eb062..000000000 --- a/automod/countstore.go +++ /dev/null @@ -1,99 +0,0 @@ -package automod - -import ( - "context" - "fmt" - "log/slog" - "time" -) - -const ( - PeriodTotal = "total" - PeriodDay = "day" - PeriodHour = "hour" -) - -type CountStore interface { - GetCount(ctx context.Context, name, val, period string) (int, error) - Increment(ctx context.Context, name, val string) error - IncrementPeriod(ctx context.Context, name, val, period string) error - // TODO: batch increment method - GetCountDistinct(ctx context.Context, name, bucket, period string) (int, error) - IncrementDistinct(ctx context.Context, name, bucket, val string) error -} - -// TODO: this implementation isn't race-safe (yet)! -type MemCountStore struct { - Counts map[string]int - DistinctCounts map[string]map[string]bool -} - -func NewMemCountStore() MemCountStore { - return MemCountStore{ - Counts: make(map[string]int), - DistinctCounts: make(map[string]map[string]bool), - } -} - -func PeriodBucket(name, val, period string) string { - switch period { - case PeriodTotal: - return fmt.Sprintf("%s/%s", name, val) - case PeriodDay: - t := time.Now().UTC().Format(time.DateOnly) - return fmt.Sprintf("%s/%s/%s", name, val, t) - case PeriodHour: - t := time.Now().UTC().Format(time.RFC3339)[0:13] - return fmt.Sprintf("%s/%s/%s", name, val, t) - default: - slog.Warn("unhandled counter period", "period", period) - return fmt.Sprintf("%s/%s", name, val) - } -} - -func (s MemCountStore) GetCount(ctx context.Context, name, val, period string) (int, error) { - v, ok := s.Counts[PeriodBucket(name, val, period)] - if !ok { - return 0, nil - } - return v, nil -} - -func (s MemCountStore) Increment(ctx context.Context, name, val string) error { - for _, p := range []string{PeriodTotal, PeriodDay, PeriodHour} { - s.IncrementPeriod(ctx, name, val, p) - } - return nil -} - -func (s MemCountStore) IncrementPeriod(ctx context.Context, name, val, period string) error { - k := PeriodBucket(name, val, period) - v, ok := s.Counts[k] - if !ok { - v = 0 - } - v = v + 1 - s.Counts[k] = v - return nil -} - -func (s MemCountStore) GetCountDistinct(ctx context.Context, name, bucket, period string) (int, error) { - v, ok := s.DistinctCounts[PeriodBucket(name, bucket, period)] - if !ok { - return 0, nil - } - return len(v), nil -} - -func (s MemCountStore) IncrementDistinct(ctx context.Context, name, bucket, val string) error { - for _, p := range []string{PeriodTotal, PeriodDay, PeriodHour} { - k := PeriodBucket(name, bucket, p) - m, ok := s.DistinctCounts[k] - if !ok { - m = make(map[string]bool) - } - m[val] = true - s.DistinctCounts[k] = m - } - return nil -} diff --git a/automod/event.go b/automod/event.go index b3f1eef92..6d32881ab 100644 --- a/automod/event.go +++ b/automod/event.go @@ -10,6 +10,7 @@ import ( comatproto "github.com/bluesky-social/indigo/api/atproto" appbsky "github.com/bluesky-social/indigo/api/bsky" "github.com/bluesky-social/indigo/atproto/syntax" + "github.com/bluesky-social/indigo/automod/countstore" "github.com/bluesky-social/indigo/xrpc" ) @@ -205,7 +206,7 @@ func dedupeReportActions(evt *RepoEvent, reports []ModReport) []ModReport { newReports := []ModReport{} for _, r := range reports { counterName := "automod-account-report-" + reasonShortName(r.ReasonType) - existing := evt.GetCount(counterName, evt.Account.Identity.DID.String(), PeriodDay) + existing := evt.GetCount(counterName, evt.Account.Identity.DID.String(), countstore.PeriodDay) if existing > 0 { evt.Logger.Debug("skipping account report due to counter", "existing", existing, "reason", reasonShortName(r.ReasonType)) } else { @@ -220,7 +221,7 @@ func circuitBreakReports(evt *RepoEvent, reports []ModReport) []ModReport { if len(reports) == 0 { return []ModReport{} } - if evt.GetCount("automod-quota", "report", PeriodDay) >= QuotaModReportDay { + if evt.GetCount("automod-quota", "report", countstore.PeriodDay) >= QuotaModReportDay { evt.Logger.Warn("CIRCUIT BREAKER: automod reports") return []ModReport{} } @@ -232,7 +233,7 @@ func circuitBreakTakedown(evt *RepoEvent, takedown bool) bool { if !takedown { return takedown } - if evt.GetCount("automod-quota", "takedown", PeriodDay) >= QuotaModTakedownDay { + if evt.GetCount("automod-quota", "takedown", countstore.PeriodDay) >= QuotaModTakedownDay { evt.Logger.Warn("CIRCUIT BREAKER: automod takedowns") return false } diff --git a/automod/rules/mentions.go b/automod/rules/mentions.go index 7ba745936..b877c4833 100644 --- a/automod/rules/mentions.go +++ b/automod/rules/mentions.go @@ -3,6 +3,7 @@ package rules import ( appbsky "github.com/bluesky-social/indigo/api/bsky" "github.com/bluesky-social/indigo/automod" + "github.com/bluesky-social/indigo/automod/countstore" ) var _ automod.PostRuleFunc = DistinctMentionsRule @@ -30,7 +31,7 @@ func DistinctMentionsRule(evt *automod.RecordEvent, post *appbsky.FeedPost) erro if !newMentions { return nil } - if mentionHourlyThreshold <= evt.GetCountDistinct("mentions", did, automod.PeriodHour) { + if mentionHourlyThreshold <= evt.GetCountDistinct("mentions", did, countstore.PeriodHour) { evt.AddAccountFlag("high-distinct-mentions") } diff --git a/automod/rules/replies.go b/automod/rules/replies.go index 5764bdef5..9c05687d1 100644 --- a/automod/rules/replies.go +++ b/automod/rules/replies.go @@ -44,7 +44,7 @@ func IdenticalReplyPostRule(evt *automod.RecordEvent, post *appbsky.FeedPost) er } // increment first. use a specific period (IncrementPeriod()) to reduce the number of counters (one per unique post text) - period := automod.PeriodDay + period := countstore.PeriodDay bucket := evt.Account.Identity.DID.String() + "/" + HashOfString(post.Text) evt.IncrementPeriod("reply-text", bucket, period) diff --git a/automod/testing.go b/automod/testing.go index aa0762c83..80f97c976 100644 --- a/automod/testing.go +++ b/automod/testing.go @@ -11,6 +11,7 @@ import ( appbsky "github.com/bluesky-social/indigo/api/bsky" "github.com/bluesky-social/indigo/atproto/identity" "github.com/bluesky-social/indigo/atproto/syntax" + "github.com/bluesky-social/indigo/automod/countstore" ) func simpleRule(evt *RecordEvent, post *appbsky.FeedPost) error { @@ -54,7 +55,7 @@ func EngineTestFixture() Engine { engine := Engine{ Logger: slog.Default(), Directory: &dir, - Counters: NewMemCountStore(), + Counters: countstore.NewMemCountStore(), Sets: sets, Flags: flags, Cache: cache,