Skip to content

Commit

Permalink
automod: plumb context through rules.
Browse files Browse the repository at this point in the history
Context objects are now available in all rules.

This is mostly not yet used, but prepares for future needs.
It does also remove one TODO -- in a rule that uses a directory lookup.

I took the liberty of adding type assertion "variables" to the header
of each rule file, while at it.
  • Loading branch information
warpfork committed Dec 18, 2023
1 parent 8d5de74 commit e6a6274
Show file tree
Hide file tree
Showing 19 changed files with 104 additions and 46 deletions.
2 changes: 1 addition & 1 deletion automod/action_dedupe_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
"github.com/stretchr/testify/assert"
)

func alwaysReportAccountRule(evt *RecordEvent) error {
func alwaysReportAccountRule(ctx context.Context, evt *RecordEvent) error {
evt.ReportAccount(ReportReasonOther, "test report")
return nil
}
Expand Down
4 changes: 2 additions & 2 deletions automod/circuit_breaker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ import (
"github.com/stretchr/testify/assert"
)

func alwaysTakedownRecordRule(evt *RecordEvent) error {
func alwaysTakedownRecordRule(ctx context.Context, evt *RecordEvent) error {
evt.TakedownRecord()
return nil
}

func alwaysReportRecordRule(evt *RecordEvent) error {
func alwaysReportRecordRule(ctx context.Context, evt *RecordEvent) error {
evt.ReportRecord(ReportReasonOther, "test report")
return nil
}
Expand Down
6 changes: 3 additions & 3 deletions automod/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ func (e *Engine) ProcessIdentityEvent(ctx context.Context, t string, did syntax.
Account: *am,
},
}
if err := e.Rules.CallIdentityRules(&evt); err != nil {
if err := e.Rules.CallIdentityRules(ctx, &evt); err != nil {
return err
}
if evt.Err != nil {
Expand Down Expand Up @@ -100,7 +100,7 @@ func (e *Engine) ProcessRecord(ctx context.Context, did syntax.DID, path, recCID
}
evt := e.NewRecordEvent(*am, path, recCID, rec)
e.Logger.Debug("processing record", "did", ident.DID, "path", path)
if err := e.Rules.CallRecordRules(&evt); err != nil {
if err := e.Rules.CallRecordRules(ctx, &evt); err != nil {
return err
}
if evt.Err != nil {
Expand Down Expand Up @@ -146,7 +146,7 @@ func (e *Engine) ProcessRecordDelete(ctx context.Context, did syntax.DID, path s
}
evt := e.NewRecordDeleteEvent(*am, path)
e.Logger.Debug("processing record deletion", "did", ident.DID, "path", path)
if err := e.Rules.CallRecordDeleteRules(&evt); err != nil {
if err := e.Rules.CallRecordDeleteRules(ctx, &evt); err != nil {
return err
}
if evt.Err != nil {
Expand Down
10 changes: 5 additions & 5 deletions automod/event.go
Original file line number Diff line number Diff line change
Expand Up @@ -592,8 +592,8 @@ type RecordDeleteEvent struct {
RecordKey string
}

type IdentityRuleFunc = func(evt *IdentityEvent) error
type RecordRuleFunc = func(evt *RecordEvent) error
type PostRuleFunc = func(evt *RecordEvent, post *appbsky.FeedPost) error
type ProfileRuleFunc = func(evt *RecordEvent, profile *appbsky.ActorProfile) error
type RecordDeleteRuleFunc = func(evt *RecordDeleteEvent) error
type IdentityRuleFunc = func(ctx context.Context, evt *IdentityEvent) error
type RecordRuleFunc = func(ctx context.Context, evt *RecordEvent) error
type PostRuleFunc = func(ctx context.Context, evt *RecordEvent, post *appbsky.FeedPost) error
type ProfileRuleFunc = func(ctx context.Context, evt *RecordEvent, profile *appbsky.ActorProfile) error
type RecordDeleteRuleFunc = func(ctx context.Context, evt *RecordDeleteEvent) error
10 changes: 8 additions & 2 deletions automod/rules/gtube.go
Original file line number Diff line number Diff line change
@@ -1,23 +1,29 @@
package rules

import (
"context"
"strings"

appbsky "github.com/bluesky-social/indigo/api/bsky"
"github.com/bluesky-social/indigo/automod"
)

var (
_ automod.PostRuleFunc = GtubePostRule
_ automod.ProfileRuleFunc = GtubeProfileRule
)

// https://en.wikipedia.org/wiki/GTUBE
var gtubeString = "XJS*C4JDBQADN1.NSBN3*2IDNEN*GTUBE-STANDARD-ANTI-UBE-TEST-EMAIL*C.34X"

func GtubePostRule(evt *automod.RecordEvent, post *appbsky.FeedPost) error {
func GtubePostRule(ctx context.Context, evt *automod.RecordEvent, post *appbsky.FeedPost) error {
if strings.Contains(post.Text, gtubeString) {
evt.AddRecordLabel("spam")
}
return nil
}

func GtubeProfileRule(evt *automod.RecordEvent, profile *appbsky.ActorProfile) error {
func GtubeProfileRule(ctx context.Context, evt *automod.RecordEvent, profile *appbsky.ActorProfile) error {
if profile.Description != nil && strings.Contains(*profile.Description, gtubeString) {
evt.AddRecordLabel("spam")
}
Expand Down
11 changes: 9 additions & 2 deletions automod/rules/hashtags.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
package rules

import (
"context"

appbsky "github.com/bluesky-social/indigo/api/bsky"
"github.com/bluesky-social/indigo/automod"
)

var (
_ automod.PostRuleFunc = BadHashtagsPostRule
_ automod.PostRuleFunc = TooManyHashtagsPostRule
)

// looks for specific hashtags from known lists
func BadHashtagsPostRule(evt *automod.RecordEvent, post *appbsky.FeedPost) error {
func BadHashtagsPostRule(ctx context.Context, evt *automod.RecordEvent, post *appbsky.FeedPost) error {
for _, tag := range ExtractHashtags(post) {
tag = NormalizeHashtag(tag)
if evt.InSet("bad-hashtags", tag) {
Expand All @@ -18,7 +25,7 @@ func BadHashtagsPostRule(evt *automod.RecordEvent, post *appbsky.FeedPost) error
}

// if a post is "almost all" hashtags, it might be a form of search spam
func TooManyHashtagsPostRule(evt *automod.RecordEvent, post *appbsky.FeedPost) error {
func TooManyHashtagsPostRule(ctx context.Context, evt *automod.RecordEvent, post *appbsky.FeedPost) error {
tags := ExtractHashtags(post)
tagChars := 0
for _, tag := range tags {
Expand Down
6 changes: 4 additions & 2 deletions automod/rules/hashtags_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package rules

import (
"context"
"testing"

appbsky "github.com/bluesky-social/indigo/api/bsky"
Expand All @@ -12,6 +13,7 @@ import (
)

func TestBadHashtagPostRule(t *testing.T) {
ctx := context.Background()
assert := assert.New(t)

engine := automod.EngineTestFixture()
Expand All @@ -27,14 +29,14 @@ func TestBadHashtagPostRule(t *testing.T) {
Text: "some post blah",
}
evt1 := engine.NewRecordEvent(am1, path, cid1, &p1)
assert.NoError(BadHashtagsPostRule(&evt1, &p1))
assert.NoError(BadHashtagsPostRule(ctx, &evt1, &p1))
assert.Empty(evt1.RecordFlags)

p2 := appbsky.FeedPost{
Text: "some post blah",
Tags: []string{"one", "slur"},
}
evt2 := engine.NewRecordEvent(am1, path, cid1, &p2)
assert.NoError(BadHashtagsPostRule(&evt2, &p2))
assert.NoError(BadHashtagsPostRule(ctx, &evt2, &p2))
assert.NotEmpty(evt2.RecordFlags)
}
5 changes: 4 additions & 1 deletion automod/rules/identity.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package rules

import (
"context"
"net/url"
"strings"
"time"
Expand All @@ -9,8 +10,10 @@ import (
"github.com/bluesky-social/indigo/automod/countstore"
)

var _ automod.IdentityRuleFunc = NewAccountRule

// triggers on first identity event for an account (DID)
func NewAccountRule(evt *automod.IdentityEvent) error {
func NewAccountRule(ctx context.Context, evt *automod.IdentityEvent) error {
// need access to IndexedAt for this rule
if evt.Account.Private == nil || evt.Account.Identity == nil {
return nil
Expand Down
10 changes: 8 additions & 2 deletions automod/rules/interaction.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
package rules

import (
"context"
"fmt"

"github.com/bluesky-social/indigo/automod"
"github.com/bluesky-social/indigo/automod/countstore"
)

var (
_ automod.RecordRuleFunc = InteractionChurnRule
_ automod.RecordDeleteRuleFunc = DeleteInteractionRule
)

var interactionDailyThreshold = 800

// looks for accounts which do frequent interaction churn, such as follow-unfollow.
func InteractionChurnRule(evt *automod.RecordEvent) error {
func InteractionChurnRule(ctx context.Context, evt *automod.RecordEvent) error {
did := evt.Account.Identity.DID.String()
switch evt.Collection {
case "app.bsky.feed.like":
Expand All @@ -37,7 +43,7 @@ func InteractionChurnRule(evt *automod.RecordEvent) error {
return nil
}

func DeleteInteractionRule(evt *automod.RecordDeleteEvent) error {
func DeleteInteractionRule(ctx context.Context, evt *automod.RecordDeleteEvent) error {
did := evt.Account.Identity.DID.String()
switch evt.Collection {
case "app.bsky.feed.like":
Expand Down
14 changes: 11 additions & 3 deletions automod/rules/keyword.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
package rules

import (
"context"

appbsky "github.com/bluesky-social/indigo/api/bsky"
"github.com/bluesky-social/indigo/automod"
)

func KeywordPostRule(evt *automod.RecordEvent, post *appbsky.FeedPost) error {
var (
_ automod.PostRuleFunc = KeywordPostRule
_ automod.ProfileRuleFunc = KeywordProfileRule
_ automod.PostRuleFunc = ReplySingleKeywordPostRule
)

func KeywordPostRule(ctx context.Context, evt *automod.RecordEvent, post *appbsky.FeedPost) error {
for _, tok := range ExtractTextTokensPost(post) {
if evt.InSet("bad-words", tok) {
evt.AddRecordFlag("bad-word")
Expand All @@ -15,7 +23,7 @@ func KeywordPostRule(evt *automod.RecordEvent, post *appbsky.FeedPost) error {
return nil
}

func KeywordProfileRule(evt *automod.RecordEvent, profile *appbsky.ActorProfile) error {
func KeywordProfileRule(ctx context.Context, evt *automod.RecordEvent, profile *appbsky.ActorProfile) error {
for _, tok := range ExtractTextTokensProfile(profile) {
if evt.InSet("bad-words", tok) {
evt.AddRecordFlag("bad-word")
Expand All @@ -25,7 +33,7 @@ func KeywordProfileRule(evt *automod.RecordEvent, profile *appbsky.ActorProfile)
return nil
}

func ReplySingleKeywordPostRule(evt *automod.RecordEvent, post *appbsky.FeedPost) error {
func ReplySingleKeywordPostRule(ctx context.Context, evt *automod.RecordEvent, post *appbsky.FeedPost) error {
if post.Reply != nil && !IsSelfThread(evt, post) {
tokens := ExtractTextTokensPost(post)
if len(tokens) == 1 && evt.InSet("bad-words", tokens[0]) {
Expand Down
4 changes: 3 additions & 1 deletion automod/rules/mentions.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package rules

import (
"context"

appbsky "github.com/bluesky-social/indigo/api/bsky"
"github.com/bluesky-social/indigo/automod"
)
Expand All @@ -10,7 +12,7 @@ var _ automod.PostRuleFunc = DistinctMentionsRule
var mentionHourlyThreshold = 20

// DistinctMentionsRule looks for accounts which mention an unusually large number of distinct accounts per period.
func DistinctMentionsRule(evt *automod.RecordEvent, post *appbsky.FeedPost) error {
func DistinctMentionsRule(ctx context.Context, evt *automod.RecordEvent, post *appbsky.FeedPost) error {
did := evt.Account.Identity.DID.String()

// Increment counters for all new mentions in this post.
Expand Down
11 changes: 7 additions & 4 deletions automod/rules/misleading.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ import (
"github.com/bluesky-social/indigo/automod"
)

var (
_ automod.PostRuleFunc = MisleadingURLPostRule
_ automod.PostRuleFunc = MisleadingMentionPostRule
)

func isMisleadingURLFacet(facet PostFacet, logger *slog.Logger) bool {
linkURL, err := url.Parse(*facet.URL)
if err != nil {
Expand Down Expand Up @@ -78,7 +83,7 @@ func isMisleadingURLFacet(facet PostFacet, logger *slog.Logger) bool {
return false
}

func MisleadingURLPostRule(evt *automod.RecordEvent, post *appbsky.FeedPost) error {
func MisleadingURLPostRule(ctx context.Context, evt *automod.RecordEvent, post *appbsky.FeedPost) error {
// TODO: make this an InSet() config?
if evt.Account.Identity.Handle == "nowbreezing.ntw.app" {
return nil
Expand All @@ -100,9 +105,7 @@ func MisleadingURLPostRule(evt *automod.RecordEvent, post *appbsky.FeedPost) err
return nil
}

func MisleadingMentionPostRule(evt *automod.RecordEvent, post *appbsky.FeedPost) error {
// TODO: do we really need to route context around? probably
ctx := context.TODO()
func MisleadingMentionPostRule(ctx context.Context, evt *automod.RecordEvent, post *appbsky.FeedPost) error {
facets, err := ExtractFacets(post)
if err != nil {
evt.Logger.Warn("invalid facets", "err", err)
Expand Down
7 changes: 5 additions & 2 deletions automod/rules/misleading_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package rules

import (
"context"
"log/slog"
"testing"

Expand All @@ -13,6 +14,7 @@ import (
)

func TestMisleadingURLPostRule(t *testing.T) {
ctx := context.Background()
assert := assert.New(t)

engine := automod.EngineTestFixture()
Expand Down Expand Up @@ -43,11 +45,12 @@ func TestMisleadingURLPostRule(t *testing.T) {
},
}
evt1 := engine.NewRecordEvent(am1, path, cid1, &p1)
assert.NoError(MisleadingURLPostRule(&evt1, &p1))
assert.NoError(MisleadingURLPostRule(ctx, &evt1, &p1))
assert.NotEmpty(evt1.RecordFlags)
}

func TestMisleadingMentionPostRule(t *testing.T) {
ctx := context.Background()
assert := assert.New(t)

engine := automod.EngineTestFixture()
Expand Down Expand Up @@ -78,7 +81,7 @@ func TestMisleadingMentionPostRule(t *testing.T) {
},
}
evt1 := engine.NewRecordEvent(am1, path, cid1, &p1)
assert.NoError(MisleadingMentionPostRule(&evt1, &p1))
assert.NoError(MisleadingMentionPostRule(ctx, &evt1, &p1))
assert.NotEmpty(evt1.RecordFlags)
}

Expand Down
5 changes: 4 additions & 1 deletion automod/rules/private.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
package rules

import (
"context"
"strings"

appbsky "github.com/bluesky-social/indigo/api/bsky"
"github.com/bluesky-social/indigo/automod"
)

var _ automod.PostRuleFunc = AccountPrivateDemoPostRule

// dummy rule. this leaks PII (account email) in logs and should never be used in real life
func AccountPrivateDemoPostRule(evt *automod.RecordEvent, post *appbsky.FeedPost) error {
func AccountPrivateDemoPostRule(ctx context.Context, evt *automod.RecordEvent, post *appbsky.FeedPost) error {
if evt.Account.Private != nil {
if strings.HasSuffix(evt.Account.Private.Email, "@blueskyweb.xyz") {
evt.Logger.Info("hello dev!", "email", evt.Account.Private.Email)
Expand Down
6 changes: 5 additions & 1 deletion automod/rules/profile.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package rules

import (
"context"

appbsky "github.com/bluesky-social/indigo/api/bsky"
"github.com/bluesky-social/indigo/automod"
)

var _ automod.PostRuleFunc = AccountDemoPostRule

// this is a dummy rule to demonstrate accessing account metadata (eg, profile) from within post handler
func AccountDemoPostRule(evt *automod.RecordEvent, post *appbsky.FeedPost) error {
func AccountDemoPostRule(ctx context.Context, evt *automod.RecordEvent, post *appbsky.FeedPost) error {
if evt.Account.Profile.Description != nil && len(post.Text) > 5 && *evt.Account.Profile.Description == post.Text {
evt.AddRecordFlag("own-profile-description")
}
Expand Down
5 changes: 4 additions & 1 deletion automod/rules/promo.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package rules

import (
"context"
"net/url"
"strings"
"time"
Expand All @@ -10,10 +11,12 @@ import (
"github.com/bluesky-social/indigo/automod/countstore"
)

var _ automod.PostRuleFunc = AggressivePromotionRule

// looks for new accounts, with a commercial or donation link in profile, which directly reply to several accounts
//
// this rule depends on ReplyCountPostRule() to set counts
func AggressivePromotionRule(evt *automod.RecordEvent, post *appbsky.FeedPost) error {
func AggressivePromotionRule(ctx context.Context, evt *automod.RecordEvent, post *appbsky.FeedPost) error {
if evt.Account.Private == nil || evt.Account.Identity == nil {
return nil
}
Expand Down
Loading

0 comments on commit e6a6274

Please sign in to comment.