this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor rule helpers in to sub-module (#783)

authored by

bnewbold and committed by
GitHub
983ce4a4 ed5b6f29

+415 -377
+49
automod/helpers/account.go
··· 1 + package helpers 2 + 3 + import ( 4 + "time" 5 + 6 + "github.com/bluesky-social/indigo/automod" 7 + ) 8 + 9 + // no accounts exist before this time 10 + var atprotoAccountEpoch = time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC) 11 + 12 + // returns true if account creation timestamp is plausible: not-nil, not in distant past, not in the future 13 + func plausibleAccountCreation(when *time.Time) bool { 14 + if when == nil { 15 + return false 16 + } 17 + // this is mostly to check for misconfigurations or null values (eg, UNIX epoch zero means "unknown" not actually 1970) 18 + if !when.After(atprotoAccountEpoch) { 19 + return false 20 + } 21 + // a timestamp in the future would also indicate some misconfiguration 22 + if when.After(time.Now().Add(time.Hour)) { 23 + return false 24 + } 25 + return true 26 + } 27 + 28 + // checks if account was created recently, based on either public or private account metadata. if metadata isn't available at all, or seems bogus, returns 'false' 29 + func AccountIsYoungerThan(c *automod.AccountContext, age time.Duration) bool { 30 + // TODO: consider swapping priority order here (and below) 31 + if c.Account.CreatedAt != nil && plausibleAccountCreation(c.Account.CreatedAt) { 32 + return time.Since(*c.Account.CreatedAt) < age 33 + } 34 + if c.Account.Private != nil && plausibleAccountCreation(c.Account.Private.IndexedAt) { 35 + return time.Since(*c.Account.Private.IndexedAt) < age 36 + } 37 + return false 38 + } 39 + 40 + // checks if account was *not* created recently, based on either public or private account metadata. if metadata isn't available at all, or seems bogus, returns 'false' 41 + func AccountIsOlderThan(c *automod.AccountContext, age time.Duration) bool { 42 + if c.Account.CreatedAt != nil && plausibleAccountCreation(c.Account.CreatedAt) { 43 + return time.Since(*c.Account.CreatedAt) >= age 44 + } 45 + if c.Account.Private != nil && plausibleAccountCreation(c.Account.Private.IndexedAt) { 46 + return time.Since(*c.Account.Private.IndexedAt) >= age 47 + } 48 + return false 49 + }
+61
automod/helpers/account_test.go
··· 1 + package helpers 2 + 3 + import ( 4 + "testing" 5 + "time" 6 + 7 + "github.com/bluesky-social/indigo/atproto/identity" 8 + "github.com/bluesky-social/indigo/atproto/syntax" 9 + "github.com/bluesky-social/indigo/automod" 10 + "github.com/stretchr/testify/assert" 11 + ) 12 + 13 + func TestAccountIsYoungerThan(t *testing.T) { 14 + assert := assert.New(t) 15 + 16 + am := automod.AccountMeta{ 17 + Identity: &identity.Identity{ 18 + DID: syntax.DID("did:plc:abc111"), 19 + Handle: syntax.Handle("handle.example.com"), 20 + }, 21 + Profile: automod.ProfileSummary{}, 22 + Private: nil, 23 + } 24 + now := time.Now() 25 + ac := automod.AccountContext{ 26 + Account: am, 27 + } 28 + assert.False(AccountIsYoungerThan(&ac, time.Hour)) 29 + assert.False(AccountIsOlderThan(&ac, time.Hour)) 30 + 31 + ac.Account.CreatedAt = &now 32 + assert.True(AccountIsYoungerThan(&ac, time.Hour)) 33 + assert.False(AccountIsOlderThan(&ac, time.Hour)) 34 + 35 + yesterday := time.Now().Add(-1 * time.Hour * 24) 36 + ac.Account.CreatedAt = &yesterday 37 + assert.False(AccountIsYoungerThan(&ac, time.Hour)) 38 + assert.True(AccountIsOlderThan(&ac, time.Hour)) 39 + 40 + old := time.Date(1990, 1, 1, 0, 0, 0, 0, time.UTC) 41 + ac.Account.CreatedAt = &old 42 + assert.False(AccountIsYoungerThan(&ac, time.Hour)) 43 + assert.False(AccountIsYoungerThan(&ac, time.Hour*24*365*100)) 44 + assert.False(AccountIsOlderThan(&ac, time.Hour)) 45 + assert.False(AccountIsOlderThan(&ac, time.Hour*24*365*100)) 46 + 47 + future := time.Date(3000, 1, 1, 0, 0, 0, 0, time.UTC) 48 + ac.Account.CreatedAt = &future 49 + assert.False(AccountIsYoungerThan(&ac, time.Hour)) 50 + assert.False(AccountIsOlderThan(&ac, time.Hour)) 51 + 52 + ac.Account.CreatedAt = nil 53 + ac.Account.Private = &automod.AccountPrivate{ 54 + Email: "account@example.com", 55 + IndexedAt: &yesterday, 56 + } 57 + assert.True(AccountIsYoungerThan(&ac, 48*time.Hour)) 58 + assert.False(AccountIsYoungerThan(&ac, time.Hour)) 59 + assert.True(AccountIsOlderThan(&ac, time.Hour)) 60 + assert.False(AccountIsOlderThan(&ac, 48*time.Hour)) 61 + }
+141
automod/helpers/bsky_test.go
··· 1 + package helpers 2 + 3 + import ( 4 + comatproto "github.com/bluesky-social/indigo/api/atproto" 5 + appbsky "github.com/bluesky-social/indigo/api/bsky" 6 + "testing" 7 + 8 + "github.com/stretchr/testify/assert" 9 + ) 10 + 11 + func TestParentOrRootIsDid(t *testing.T) { 12 + assert := assert.New(t) 13 + 14 + post1 := &appbsky.FeedPost{ 15 + Text: "some random post that i dreamt up last night, idk", 16 + Reply: &appbsky.FeedPost_ReplyRef{ 17 + Root: &comatproto.RepoStrongRef{ 18 + Uri: "at://did:plc:abc123/app.bsky.feed.post/rkey123", 19 + }, 20 + Parent: &comatproto.RepoStrongRef{ 21 + Uri: "at://did:plc:abc123/app.bsky.feed.post/rkey123", 22 + }, 23 + }, 24 + } 25 + 26 + post2 := &appbsky.FeedPost{ 27 + Text: "some random post that i dreamt up last night, idk", 28 + Reply: &appbsky.FeedPost_ReplyRef{ 29 + Root: &comatproto.RepoStrongRef{ 30 + Uri: "at://did:plc:321abc/app.bsky.feed.post/rkey123", 31 + }, 32 + Parent: &comatproto.RepoStrongRef{ 33 + Uri: "at://did:plc:abc123/app.bsky.feed.post/rkey123", 34 + }, 35 + }, 36 + } 37 + 38 + post3 := &appbsky.FeedPost{ 39 + Text: "some random post that i dreamt up last night, idk", 40 + Reply: &appbsky.FeedPost_ReplyRef{ 41 + Root: &comatproto.RepoStrongRef{ 42 + Uri: "at://did:plc:abc123/app.bsky.feed.post/rkey123", 43 + }, 44 + Parent: &comatproto.RepoStrongRef{ 45 + Uri: "at://did:plc:321abc/app.bsky.feed.post/rkey123", 46 + }, 47 + }, 48 + } 49 + 50 + post4 := &appbsky.FeedPost{ 51 + Text: "some random post that i dreamt up last night, idk", 52 + Reply: &appbsky.FeedPost_ReplyRef{ 53 + Root: &comatproto.RepoStrongRef{ 54 + Uri: "at://did:plc:321abc/app.bsky.feed.post/rkey123", 55 + }, 56 + Parent: &comatproto.RepoStrongRef{ 57 + Uri: "at://did:plc:321abc/app.bsky.feed.post/rkey123", 58 + }, 59 + }, 60 + } 61 + 62 + assert.True(PostParentOrRootIsDid(post1, "did:plc:abc123")) 63 + assert.False(PostParentOrRootIsDid(post1, "did:plc:321abc")) 64 + 65 + assert.True(PostParentOrRootIsDid(post2, "did:plc:abc123")) 66 + assert.True(PostParentOrRootIsDid(post2, "did:plc:321abc")) 67 + 68 + assert.True(PostParentOrRootIsDid(post3, "did:plc:abc123")) 69 + assert.True(PostParentOrRootIsDid(post3, "did:plc:321abc")) 70 + 71 + assert.False(PostParentOrRootIsDid(post4, "did:plc:abc123")) 72 + assert.True(PostParentOrRootIsDid(post4, "did:plc:321abc")) 73 + 74 + didList1 := []string{ 75 + "did:plc:cba321", 76 + "did:web:bsky.app", 77 + "did:plc:abc123", 78 + } 79 + 80 + didList2 := []string{ 81 + "did:plc:321cba", 82 + "did:web:bsky.app", 83 + "did:plc:123abc", 84 + } 85 + 86 + assert.True(PostParentOrRootIsAnyDid(post1, didList1)) 87 + assert.False(PostParentOrRootIsAnyDid(post1, didList2)) 88 + } 89 + 90 + func TestPostMentionsDid(t *testing.T) { 91 + assert := assert.New(t) 92 + 93 + post := &appbsky.FeedPost{ 94 + Text: "@hailey.at what is upppp also hello to @darthbluesky.bsky.social", 95 + Facets: []*appbsky.RichtextFacet{ 96 + { 97 + Features: []*appbsky.RichtextFacet_Features_Elem{ 98 + { 99 + RichtextFacet_Mention: &appbsky.RichtextFacet_Mention{ 100 + Did: "did:plc:abc123", 101 + }, 102 + }, 103 + }, 104 + Index: &appbsky.RichtextFacet_ByteSlice{ 105 + ByteStart: 0, 106 + ByteEnd: 9, 107 + }, 108 + }, 109 + { 110 + Features: []*appbsky.RichtextFacet_Features_Elem{ 111 + { 112 + RichtextFacet_Mention: &appbsky.RichtextFacet_Mention{ 113 + Did: "did:plc:abc456", 114 + }, 115 + }, 116 + }, 117 + Index: &appbsky.RichtextFacet_ByteSlice{ 118 + ByteStart: 39, 119 + ByteEnd: 63, 120 + }, 121 + }, 122 + }, 123 + } 124 + assert.True(PostMentionsDid(post, "did:plc:abc123")) 125 + assert.False(PostMentionsDid(post, "did:plc:cba321")) 126 + 127 + didList1 := []string{ 128 + "did:plc:cba321", 129 + "did:web:bsky.app", 130 + "did:plc:abc456", 131 + } 132 + 133 + didList2 := []string{ 134 + "did:plc:321cba", 135 + "did:web:bsky.app", 136 + "did:plc:123abc", 137 + } 138 + 139 + assert.True(PostMentionsAnyDid(post, didList1)) 140 + assert.False(PostMentionsAnyDid(post, didList2)) 141 + }
+35
automod/helpers/text.go
··· 1 + package helpers 2 + 3 + import ( 4 + "fmt" 5 + "regexp" 6 + 7 + "github.com/spaolacci/murmur3" 8 + ) 9 + 10 + func DedupeStrings(in []string) []string { 11 + var out []string 12 + seen := make(map[string]bool) 13 + for _, v := range in { 14 + if !seen[v] { 15 + out = append(out, v) 16 + seen[v] = true 17 + } 18 + } 19 + return out 20 + } 21 + 22 + // returns a fast, compact hash of a string 23 + // 24 + // current implementation uses murmur3, default seed, and hex encoding 25 + func HashOfString(s string) string { 26 + val := murmur3.Sum64([]byte(s)) 27 + return fmt.Sprintf("%016x", val) 28 + } 29 + 30 + // based on: https://stackoverflow.com/a/48769624, with no trailing period allowed 31 + var urlRegex = regexp.MustCompile(`(?:(?:https?|ftp):\/\/)?[\w/\-?=%.]+\.[\w/\-&?=%.]*[\w/\-&?=%]+`) 32 + 33 + func ExtractTextURLs(raw string) []string { 34 + return urlRegex.FindAllString(raw, -1) 35 + }
+64
automod/helpers/text_test.go
··· 1 + package helpers 2 + 3 + import ( 4 + "testing" 5 + 6 + "github.com/bluesky-social/indigo/automod/keyword" 7 + 8 + "github.com/stretchr/testify/assert" 9 + ) 10 + 11 + func TestTokenizeText(t *testing.T) { 12 + assert := assert.New(t) 13 + 14 + fixtures := []struct { 15 + s string 16 + out []string 17 + }{ 18 + { 19 + s: "1 'Two' three!", 20 + out: []string{"1", "two", "three"}, 21 + }, 22 + { 23 + s: " foo1;bar2,baz3...", 24 + out: []string{"foo1", "bar2", "baz3"}, 25 + }, 26 + { 27 + s: "https://example.com/index.html", 28 + out: []string{"https", "example", "com", "index", "html"}, 29 + }, 30 + } 31 + 32 + for _, fix := range fixtures { 33 + assert.Equal(fix.out, keyword.TokenizeText(fix.s)) 34 + } 35 + } 36 + 37 + func TestExtractURL(t *testing.T) { 38 + assert := assert.New(t) 39 + 40 + fixtures := []struct { 41 + s string 42 + out []string 43 + }{ 44 + { 45 + s: "this is a description with example.com mentioned in the middle", 46 + out: []string{"example.com"}, 47 + }, 48 + { 49 + s: "this is another example with https://en.wikipedia.org/index.html: and archive.org, and https://eff.org/... and bsky.app.", 50 + out: []string{"https://en.wikipedia.org/index.html", "archive.org", "https://eff.org/", "bsky.app"}, 51 + }, 52 + } 53 + 54 + for _, fix := range fixtures { 55 + assert.Equal(fix.out, ExtractTextURLs(fix.s)) 56 + } 57 + } 58 + 59 + func TestHashOfString(t *testing.T) { 60 + assert := assert.New(t) 61 + 62 + // hashing function should be consistent over time 63 + assert.Equal("4e6f69c0e3d10992", HashOfString("dummy-value")) 64 + }
+6 -5
automod/rules/harassment.go
··· 8 8 "github.com/bluesky-social/indigo/atproto/syntax" 9 9 "github.com/bluesky-social/indigo/automod" 10 10 "github.com/bluesky-social/indigo/automod/countstore" 11 + "github.com/bluesky-social/indigo/automod/helpers" 11 12 ) 12 13 13 14 var _ automod.PostRuleFunc = HarassmentTargetInteractionPostRule 14 15 15 16 // looks for new accounts, which interact with frequently-harassed accounts, and report them for review 16 17 func HarassmentTargetInteractionPostRule(c *automod.RecordContext, post *appbsky.FeedPost) error { 17 - if c.Account.Identity == nil || !AccountIsYoungerThan(&c.AccountContext, 24*time.Hour) { 18 + if c.Account.Identity == nil || !helpers.AccountIsYoungerThan(&c.AccountContext, 24*time.Hour) { 18 19 return nil 19 20 } 20 21 21 22 var interactionDIDs []string 22 - facets, err := ExtractFacets(post) 23 + facets, err := helpers.ExtractFacets(post) 23 24 if err != nil { 24 25 return err 25 26 } ··· 28 29 interactionDIDs = append(interactionDIDs, *pf.DID) 29 30 } 30 31 } 31 - if post.Reply != nil && !IsSelfThread(c, post) { 32 + if post.Reply != nil && !helpers.IsSelfThread(c, post) { 32 33 parentURI, err := syntax.ParseATURI(post.Reply.Parent.Uri) 33 34 if err != nil { 34 35 return err ··· 57 58 return nil 58 59 } 59 60 60 - interactionDIDs = dedupeStrings(interactionDIDs) 61 + interactionDIDs = helpers.DedupeStrings(interactionDIDs) 61 62 for _, d := range interactionDIDs { 62 63 did, err := syntax.ParseDID(d) 63 64 if err != nil { ··· 114 115 115 116 // looks for new accounts, which frequently post the same type of content 116 117 func HarassmentTrivialPostRule(c *automod.RecordContext, post *appbsky.FeedPost) error { 117 - if c.Account.Identity == nil || !AccountIsYoungerThan(&c.AccountContext, 7*24*time.Hour) { 118 + if c.Account.Identity == nil || !helpers.AccountIsYoungerThan(&c.AccountContext, 7*24*time.Hour) { 118 119 return nil 119 120 } 120 121
+4 -3
automod/rules/hashtags.go
··· 5 5 6 6 appbsky "github.com/bluesky-social/indigo/api/bsky" 7 7 "github.com/bluesky-social/indigo/automod" 8 + "github.com/bluesky-social/indigo/automod/helpers" 8 9 "github.com/bluesky-social/indigo/automod/keyword" 9 10 ) 10 11 11 12 // looks for specific hashtags from known lists 12 13 func BadHashtagsPostRule(c *automod.RecordContext, post *appbsky.FeedPost) error { 13 - for _, tag := range ExtractHashtagsPost(post) { 14 - tag = NormalizeHashtag(tag) 14 + for _, tag := range helpers.ExtractHashtagsPost(post) { 15 + tag = helpers.NormalizeHashtag(tag) 15 16 // skip some bad-word hashtags which frequently false-positive 16 17 if tag == "nazi" || tag == "hitler" { 17 18 continue ··· 35 36 36 37 // if a post is "almost all" hashtags, it might be a form of search spam 37 38 func TooManyHashtagsPostRule(c *automod.RecordContext, post *appbsky.FeedPost) error { 38 - tags := ExtractHashtagsPost(post) 39 + tags := helpers.ExtractHashtagsPost(post) 39 40 tagChars := 0 40 41 for _, tag := range tags { 41 42 tagChars += len(tag)
+4 -77
automod/rules/helpers.go automod/helpers/bsky.go
··· 1 - package rules 1 + package helpers 2 2 3 3 import ( 4 4 "fmt" 5 - "regexp" 6 - "time" 7 5 8 6 appbsky "github.com/bluesky-social/indigo/api/bsky" 9 7 "github.com/bluesky-social/indigo/atproto/syntax" 10 8 "github.com/bluesky-social/indigo/automod" 11 9 "github.com/bluesky-social/indigo/automod/keyword" 12 - 13 - "github.com/spaolacci/murmur3" 14 10 ) 15 11 16 - func dedupeStrings(in []string) []string { 17 - var out []string 18 - seen := make(map[string]bool) 19 - for _, v := range in { 20 - if !seen[v] { 21 - out = append(out, v) 22 - seen[v] = true 23 - } 24 - } 25 - return out 26 - } 27 - 28 12 func ExtractHashtagsPost(post *appbsky.FeedPost) []string { 29 13 var tags []string 30 14 for _, tag := range post.Tags { ··· 37 21 } 38 22 } 39 23 } 40 - return dedupeStrings(tags) 24 + return DedupeStrings(tags) 41 25 } 42 26 43 27 func NormalizeHashtag(raw string) string { ··· 103 87 } 104 88 } 105 89 } 106 - return dedupeStrings(out) 90 + return DedupeStrings(out) 107 91 } 108 92 109 93 func ExtractBlobCIDsProfile(profile *appbsky.ActorProfile) []string { ··· 114 98 if profile.Banner != nil { 115 99 out = append(out, profile.Banner.Ref.String()) 116 100 } 117 - return dedupeStrings(out) 101 + return DedupeStrings(out) 118 102 } 119 103 120 104 func ExtractTextTokensPost(post *appbsky.FeedPost) []string { ··· 150 134 s += " " + *profile.DisplayName 151 135 } 152 136 return keyword.TokenizeText(s) 153 - } 154 - 155 - // based on: https://stackoverflow.com/a/48769624, with no trailing period allowed 156 - var urlRegex = regexp.MustCompile(`(?:(?:https?|ftp):\/\/)?[\w/\-?=%.]+\.[\w/\-&?=%.]*[\w/\-&?=%]+`) 157 - 158 - func ExtractTextURLs(raw string) []string { 159 - return urlRegex.FindAllString(raw, -1) 160 137 } 161 138 162 139 func ExtractTextURLsProfile(profile *appbsky.ActorProfile) []string { ··· 191 168 return false 192 169 } 193 170 194 - // returns a fast, compact hash of a string 195 - // 196 - // current implementation uses murmur3, default seed, and hex encoding 197 - func HashOfString(s string) string { 198 - val := murmur3.Sum64([]byte(s)) 199 - return fmt.Sprintf("%016x", val) 200 - } 201 - 202 171 func ParentOrRootIsFollower(c *automod.RecordContext, post *appbsky.FeedPost) bool { 203 172 if post.Reply == nil || IsSelfThread(c, post) { 204 173 return false ··· 238 207 rel = c.GetAccountRelationship(rootDID) 239 208 if rel.FollowedBy { 240 209 return true 241 - } 242 - return false 243 - } 244 - 245 - // no accounts exist before this time 246 - var atprotoAccountEpoch = time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC) 247 - 248 - // returns true if account creation timestamp is plausible: not-nil, not in distant past, not in the future 249 - func plausibleAccountCreation(when *time.Time) bool { 250 - if when == nil { 251 - return false 252 - } 253 - // this is mostly to check for misconfigurations or null values (eg, UNIX epoch zero means "unknown" not actually 1970) 254 - if !when.After(atprotoAccountEpoch) { 255 - return false 256 - } 257 - // a timestamp in the future would also indicate some misconfiguration 258 - if when.After(time.Now().Add(time.Hour)) { 259 - return false 260 - } 261 - return true 262 - } 263 - 264 - // checks if account was created recently, based on either public or private account metadata. if metadata isn't available at all, or seems bogus, returns 'false' 265 - func AccountIsYoungerThan(c *automod.AccountContext, age time.Duration) bool { 266 - // TODO: consider swapping priority order here (and below) 267 - if c.Account.CreatedAt != nil && plausibleAccountCreation(c.Account.CreatedAt) { 268 - return time.Since(*c.Account.CreatedAt) < age 269 - } 270 - if c.Account.Private != nil && plausibleAccountCreation(c.Account.Private.IndexedAt) { 271 - return time.Since(*c.Account.Private.IndexedAt) < age 272 - } 273 - return false 274 - } 275 - 276 - // checks if account was *not* created recently, based on either public or private account metadata. if metadata isn't available at all, or seems bogus, returns 'false' 277 - func AccountIsOlderThan(c *automod.AccountContext, age time.Duration) bool { 278 - if c.Account.CreatedAt != nil && plausibleAccountCreation(c.Account.CreatedAt) { 279 - return time.Since(*c.Account.CreatedAt) >= age 280 - } 281 - if c.Account.Private != nil && plausibleAccountCreation(c.Account.Private.IndexedAt) { 282 - return time.Since(*c.Account.Private.IndexedAt) >= age 283 210 } 284 211 return false 285 212 }
-251
automod/rules/helpers_test.go
··· 1 - package rules 2 - 3 - import ( 4 - comatproto "github.com/bluesky-social/indigo/api/atproto" 5 - appbsky "github.com/bluesky-social/indigo/api/bsky" 6 - "testing" 7 - "time" 8 - 9 - "github.com/bluesky-social/indigo/atproto/identity" 10 - "github.com/bluesky-social/indigo/atproto/syntax" 11 - "github.com/bluesky-social/indigo/automod" 12 - "github.com/bluesky-social/indigo/automod/keyword" 13 - "github.com/stretchr/testify/assert" 14 - ) 15 - 16 - func TestTokenizeText(t *testing.T) { 17 - assert := assert.New(t) 18 - 19 - fixtures := []struct { 20 - s string 21 - out []string 22 - }{ 23 - { 24 - s: "1 'Two' three!", 25 - out: []string{"1", "two", "three"}, 26 - }, 27 - { 28 - s: " foo1;bar2,baz3...", 29 - out: []string{"foo1", "bar2", "baz3"}, 30 - }, 31 - { 32 - s: "https://example.com/index.html", 33 - out: []string{"https", "example", "com", "index", "html"}, 34 - }, 35 - } 36 - 37 - for _, fix := range fixtures { 38 - assert.Equal(fix.out, keyword.TokenizeText(fix.s)) 39 - } 40 - } 41 - 42 - func TestExtractURL(t *testing.T) { 43 - assert := assert.New(t) 44 - 45 - fixtures := []struct { 46 - s string 47 - out []string 48 - }{ 49 - { 50 - s: "this is a description with example.com mentioned in the middle", 51 - out: []string{"example.com"}, 52 - }, 53 - { 54 - s: "this is another example with https://en.wikipedia.org/index.html: and archive.org, and https://eff.org/... and bsky.app.", 55 - out: []string{"https://en.wikipedia.org/index.html", "archive.org", "https://eff.org/", "bsky.app"}, 56 - }, 57 - } 58 - 59 - for _, fix := range fixtures { 60 - assert.Equal(fix.out, ExtractTextURLs(fix.s)) 61 - } 62 - } 63 - 64 - func TestHashOfString(t *testing.T) { 65 - assert := assert.New(t) 66 - 67 - // hashing function should be consistent over time 68 - assert.Equal("4e6f69c0e3d10992", HashOfString("dummy-value")) 69 - } 70 - 71 - func TestAccountIsYoungerThan(t *testing.T) { 72 - assert := assert.New(t) 73 - 74 - am := automod.AccountMeta{ 75 - Identity: &identity.Identity{ 76 - DID: syntax.DID("did:plc:abc111"), 77 - Handle: syntax.Handle("handle.example.com"), 78 - }, 79 - Profile: automod.ProfileSummary{}, 80 - Private: nil, 81 - } 82 - now := time.Now() 83 - ac := automod.AccountContext{ 84 - Account: am, 85 - } 86 - assert.False(AccountIsYoungerThan(&ac, time.Hour)) 87 - assert.False(AccountIsOlderThan(&ac, time.Hour)) 88 - 89 - ac.Account.CreatedAt = &now 90 - assert.True(AccountIsYoungerThan(&ac, time.Hour)) 91 - assert.False(AccountIsOlderThan(&ac, time.Hour)) 92 - 93 - yesterday := time.Now().Add(-1 * time.Hour * 24) 94 - ac.Account.CreatedAt = &yesterday 95 - assert.False(AccountIsYoungerThan(&ac, time.Hour)) 96 - assert.True(AccountIsOlderThan(&ac, time.Hour)) 97 - 98 - old := time.Date(1990, 1, 1, 0, 0, 0, 0, time.UTC) 99 - ac.Account.CreatedAt = &old 100 - assert.False(AccountIsYoungerThan(&ac, time.Hour)) 101 - assert.False(AccountIsYoungerThan(&ac, time.Hour*24*365*100)) 102 - assert.False(AccountIsOlderThan(&ac, time.Hour)) 103 - assert.False(AccountIsOlderThan(&ac, time.Hour*24*365*100)) 104 - 105 - future := time.Date(3000, 1, 1, 0, 0, 0, 0, time.UTC) 106 - ac.Account.CreatedAt = &future 107 - assert.False(AccountIsYoungerThan(&ac, time.Hour)) 108 - assert.False(AccountIsOlderThan(&ac, time.Hour)) 109 - 110 - ac.Account.CreatedAt = nil 111 - ac.Account.Private = &automod.AccountPrivate{ 112 - Email: "account@example.com", 113 - IndexedAt: &yesterday, 114 - } 115 - assert.True(AccountIsYoungerThan(&ac, 48*time.Hour)) 116 - assert.False(AccountIsYoungerThan(&ac, time.Hour)) 117 - assert.True(AccountIsOlderThan(&ac, time.Hour)) 118 - assert.False(AccountIsOlderThan(&ac, 48*time.Hour)) 119 - } 120 - 121 - func TestParentOrRootIsDid(t *testing.T) { 122 - assert := assert.New(t) 123 - 124 - post1 := &appbsky.FeedPost{ 125 - Text: "some random post that i dreamt up last night, idk", 126 - Reply: &appbsky.FeedPost_ReplyRef{ 127 - Root: &comatproto.RepoStrongRef{ 128 - Uri: "at://did:plc:abc123/app.bsky.feed.post/rkey123", 129 - }, 130 - Parent: &comatproto.RepoStrongRef{ 131 - Uri: "at://did:plc:abc123/app.bsky.feed.post/rkey123", 132 - }, 133 - }, 134 - } 135 - 136 - post2 := &appbsky.FeedPost{ 137 - Text: "some random post that i dreamt up last night, idk", 138 - Reply: &appbsky.FeedPost_ReplyRef{ 139 - Root: &comatproto.RepoStrongRef{ 140 - Uri: "at://did:plc:321abc/app.bsky.feed.post/rkey123", 141 - }, 142 - Parent: &comatproto.RepoStrongRef{ 143 - Uri: "at://did:plc:abc123/app.bsky.feed.post/rkey123", 144 - }, 145 - }, 146 - } 147 - 148 - post3 := &appbsky.FeedPost{ 149 - Text: "some random post that i dreamt up last night, idk", 150 - Reply: &appbsky.FeedPost_ReplyRef{ 151 - Root: &comatproto.RepoStrongRef{ 152 - Uri: "at://did:plc:abc123/app.bsky.feed.post/rkey123", 153 - }, 154 - Parent: &comatproto.RepoStrongRef{ 155 - Uri: "at://did:plc:321abc/app.bsky.feed.post/rkey123", 156 - }, 157 - }, 158 - } 159 - 160 - post4 := &appbsky.FeedPost{ 161 - Text: "some random post that i dreamt up last night, idk", 162 - Reply: &appbsky.FeedPost_ReplyRef{ 163 - Root: &comatproto.RepoStrongRef{ 164 - Uri: "at://did:plc:321abc/app.bsky.feed.post/rkey123", 165 - }, 166 - Parent: &comatproto.RepoStrongRef{ 167 - Uri: "at://did:plc:321abc/app.bsky.feed.post/rkey123", 168 - }, 169 - }, 170 - } 171 - 172 - assert.True(PostParentOrRootIsDid(post1, "did:plc:abc123")) 173 - assert.False(PostParentOrRootIsDid(post1, "did:plc:321abc")) 174 - 175 - assert.True(PostParentOrRootIsDid(post2, "did:plc:abc123")) 176 - assert.True(PostParentOrRootIsDid(post2, "did:plc:321abc")) 177 - 178 - assert.True(PostParentOrRootIsDid(post3, "did:plc:abc123")) 179 - assert.True(PostParentOrRootIsDid(post3, "did:plc:321abc")) 180 - 181 - assert.False(PostParentOrRootIsDid(post4, "did:plc:abc123")) 182 - assert.True(PostParentOrRootIsDid(post4, "did:plc:321abc")) 183 - 184 - didList1 := []string{ 185 - "did:plc:cba321", 186 - "did:web:bsky.app", 187 - "did:plc:abc123", 188 - } 189 - 190 - didList2 := []string{ 191 - "did:plc:321cba", 192 - "did:web:bsky.app", 193 - "did:plc:123abc", 194 - } 195 - 196 - assert.True(PostParentOrRootIsAnyDid(post1, didList1)) 197 - assert.False(PostParentOrRootIsAnyDid(post1, didList2)) 198 - } 199 - 200 - func TestPostMentionsDid(t *testing.T) { 201 - assert := assert.New(t) 202 - 203 - post := &appbsky.FeedPost{ 204 - Text: "@hailey.at what is upppp also hello to @darthbluesky.bsky.social", 205 - Facets: []*appbsky.RichtextFacet{ 206 - { 207 - Features: []*appbsky.RichtextFacet_Features_Elem{ 208 - { 209 - RichtextFacet_Mention: &appbsky.RichtextFacet_Mention{ 210 - Did: "did:plc:abc123", 211 - }, 212 - }, 213 - }, 214 - Index: &appbsky.RichtextFacet_ByteSlice{ 215 - ByteStart: 0, 216 - ByteEnd: 9, 217 - }, 218 - }, 219 - { 220 - Features: []*appbsky.RichtextFacet_Features_Elem{ 221 - { 222 - RichtextFacet_Mention: &appbsky.RichtextFacet_Mention{ 223 - Did: "did:plc:abc456", 224 - }, 225 - }, 226 - }, 227 - Index: &appbsky.RichtextFacet_ByteSlice{ 228 - ByteStart: 39, 229 - ByteEnd: 63, 230 - }, 231 - }, 232 - }, 233 - } 234 - assert.True(PostMentionsDid(post, "did:plc:abc123")) 235 - assert.False(PostMentionsDid(post, "did:plc:cba321")) 236 - 237 - didList1 := []string{ 238 - "did:plc:cba321", 239 - "did:web:bsky.app", 240 - "did:plc:abc456", 241 - } 242 - 243 - didList2 := []string{ 244 - "did:plc:321cba", 245 - "did:web:bsky.app", 246 - "did:plc:123abc", 247 - } 248 - 249 - assert.True(PostMentionsAnyDid(post, didList1)) 250 - assert.False(PostMentionsAnyDid(post, didList2)) 251 - }
+2 -1
automod/rules/identity.go
··· 7 7 8 8 "github.com/bluesky-social/indigo/automod" 9 9 "github.com/bluesky-social/indigo/automod/countstore" 10 + "github.com/bluesky-social/indigo/automod/helpers" 10 11 ) 11 12 12 13 // triggers on first identity event for an account (DID) 13 14 func NewAccountRule(c *automod.AccountContext) error { 14 - if c.Account.Identity == nil || !AccountIsYoungerThan(c, 4*time.Hour) { 15 + if c.Account.Identity == nil || !helpers.AccountIsYoungerThan(c, 4*time.Hour) { 15 16 return nil 16 17 } 17 18
+5 -4
automod/rules/keyword.go
··· 7 7 8 8 appbsky "github.com/bluesky-social/indigo/api/bsky" 9 9 "github.com/bluesky-social/indigo/automod" 10 + "github.com/bluesky-social/indigo/automod/helpers" 10 11 "github.com/bluesky-social/indigo/automod/keyword" 11 12 ) 12 13 ··· 17 18 isJapanese = true 18 19 } 19 20 } 20 - for _, tok := range ExtractTextTokensPost(post) { 21 + for _, tok := range helpers.ExtractTextTokensPost(post) { 21 22 word := keyword.SlugIsExplicitSlur(tok) 22 23 // used very frequently in a reclaimed context 23 24 if word != "" && word != "faggot" && word != "tranny" && word != "coon" && !(word == "kike" && isJapanese) { ··· 54 55 //c.Notify("slack") 55 56 } 56 57 } 57 - for _, tok := range ExtractTextTokensProfile(profile) { 58 + for _, tok := range helpers.ExtractTextTokensProfile(profile) { 58 59 // de-pluralize 59 60 tok = strings.TrimSuffix(tok, "s") 60 61 if c.InSet("worst-words", tok) { ··· 71 72 72 73 // looks for the specific harassment situation of a replay to another user with only a single word 73 74 func ReplySingleBadWordPostRule(c *automod.RecordContext, post *appbsky.FeedPost) error { 74 - if post.Reply != nil && !IsSelfThread(c, post) { 75 - tokens := ExtractTextTokensPost(post) 75 + if post.Reply != nil && !helpers.IsSelfThread(c, post) { 76 + tokens := helpers.ExtractTextTokensPost(post) 76 77 if len(tokens) != 1 { 77 78 return nil 78 79 }
+2 -1
automod/rules/mentions.go
··· 8 8 "github.com/bluesky-social/indigo/atproto/syntax" 9 9 "github.com/bluesky-social/indigo/automod" 10 10 "github.com/bluesky-social/indigo/automod/countstore" 11 + "github.com/bluesky-social/indigo/automod/helpers" 11 12 ) 12 13 13 14 var _ automod.PostRuleFunc = DistinctMentionsRule ··· 47 48 var _ automod.PostRuleFunc = YoungAccountDistinctMentionsRule 48 49 49 50 func YoungAccountDistinctMentionsRule(c *automod.RecordContext, post *appbsky.FeedPost) error { 50 - if c.Account.Identity == nil || !AccountIsYoungerThan(&c.AccountContext, 14*24*time.Hour) { 51 + if c.Account.Identity == nil || !helpers.AccountIsYoungerThan(&c.AccountContext, 14*24*time.Hour) { 51 52 return nil 52 53 } 53 54
+4 -3
automod/rules/misleading.go
··· 9 9 appbsky "github.com/bluesky-social/indigo/api/bsky" 10 10 "github.com/bluesky-social/indigo/atproto/syntax" 11 11 "github.com/bluesky-social/indigo/automod" 12 + "github.com/bluesky-social/indigo/automod/helpers" 12 13 ) 13 14 14 - func isMisleadingURLFacet(facet PostFacet, logger *slog.Logger) bool { 15 + func isMisleadingURLFacet(facet helpers.PostFacet, logger *slog.Logger) bool { 15 16 linkURL, err := url.Parse(*facet.URL) 16 17 if err != nil { 17 18 logger.Warn("invalid link metadata URL", "url", facet.URL) ··· 84 85 if c.Account.Identity.Handle == "nowbreezing.ntw.app" { 85 86 return nil 86 87 } 87 - facets, err := ExtractFacets(post) 88 + facets, err := helpers.ExtractFacets(post) 88 89 if err != nil { 89 90 c.Logger.Warn("invalid facets", "err", err) 90 91 // TODO: or some other "this record is corrupt" indicator? ··· 105 106 var _ automod.PostRuleFunc = MisleadingMentionPostRule 106 107 107 108 func MisleadingMentionPostRule(c *automod.RecordContext, post *appbsky.FeedPost) error { 108 - facets, err := ExtractFacets(post) 109 + facets, err := helpers.ExtractFacets(post) 109 110 if err != nil { 110 111 c.Logger.Warn("invalid facets", "err", err) 111 112 // TODO: or some other "this record is corrupt" indicator?
+11 -10
automod/rules/misleading_test.go
··· 11 11 "github.com/bluesky-social/indigo/atproto/syntax" 12 12 "github.com/bluesky-social/indigo/automod" 13 13 "github.com/bluesky-social/indigo/automod/engine" 14 + "github.com/bluesky-social/indigo/automod/helpers" 14 15 15 16 "github.com/stretchr/testify/assert" 16 17 ) ··· 118 119 logger := slog.Default() 119 120 120 121 fixtures := []struct { 121 - facet PostFacet 122 + facet helpers.PostFacet 122 123 out bool 123 124 }{ 124 125 { 125 - facet: PostFacet{ 126 + facet: helpers.PostFacet{ 126 127 Text: "https://atproto.com", 127 128 URL: pstr("https://atproto.com"), 128 129 }, 129 130 out: false, 130 131 }, 131 132 { 132 - facet: PostFacet{ 133 + facet: helpers.PostFacet{ 133 134 Text: "https://atproto.com", 134 135 URL: pstr("https://evil.com"), 135 136 }, 136 137 out: true, 137 138 }, 138 139 { 139 - facet: PostFacet{ 140 + facet: helpers.PostFacet{ 140 141 Text: "https://www.atproto.com", 141 142 URL: pstr("https://atproto.com"), 142 143 }, 143 144 out: false, 144 145 }, 145 146 { 146 - facet: PostFacet{ 147 + facet: helpers.PostFacet{ 147 148 Text: "https://atproto.com", 148 149 URL: pstr("https://www.atproto.com"), 149 150 }, 150 151 out: false, 151 152 }, 152 153 { 153 - facet: PostFacet{ 154 + facet: helpers.PostFacet{ 154 155 Text: "[example.com]", 155 156 URL: pstr("https://www.example.com"), 156 157 }, 157 158 out: false, 158 159 }, 159 160 { 160 - facet: PostFacet{ 161 + facet: helpers.PostFacet{ 161 162 Text: "example.com...", 162 163 URL: pstr("https://example.com.evil.com"), 163 164 }, 164 165 out: true, 165 166 }, 166 167 { 167 - facet: PostFacet{ 168 + facet: helpers.PostFacet{ 168 169 Text: "ATPROTO.com...", 169 170 URL: pstr("https://atproto.com"), 170 171 }, 171 172 out: false, 172 173 }, 173 174 { 174 - facet: PostFacet{ 175 + facet: helpers.PostFacet{ 175 176 Text: "1234.5678", 176 177 URL: pstr("https://arxiv.org/abs/1234.5678"), 177 178 }, 178 179 out: false, 179 180 }, 180 181 { 181 - facet: PostFacet{ 182 + facet: helpers.PostFacet{ 182 183 Text: "www.techdirt.com…", 183 184 URL: pstr("https://www.techdirt.com/"), 184 185 },
+2 -1
automod/rules/nostr.go
··· 7 7 8 8 appbsky "github.com/bluesky-social/indigo/api/bsky" 9 9 "github.com/bluesky-social/indigo/automod" 10 + "github.com/bluesky-social/indigo/automod/helpers" 10 11 ) 11 12 12 13 var _ automod.PostRuleFunc = NostrSpamPostRule 13 14 14 15 // looks for new accounts, which frequently post the same type of content 15 16 func NostrSpamPostRule(c *automod.RecordContext, post *appbsky.FeedPost) error { 16 - if c.Account.Identity == nil || !AccountIsYoungerThan(&c.AccountContext, 2*24*time.Hour) { 17 + if c.Account.Identity == nil || !helpers.AccountIsYoungerThan(&c.AccountContext, 2*24*time.Hour) { 17 18 return nil 18 19 } 19 20
+5 -4
automod/rules/promo.go
··· 9 9 appbsky "github.com/bluesky-social/indigo/api/bsky" 10 10 "github.com/bluesky-social/indigo/automod" 11 11 "github.com/bluesky-social/indigo/automod/countstore" 12 + "github.com/bluesky-social/indigo/automod/helpers" 12 13 ) 13 14 14 15 var _ automod.PostRuleFunc = AggressivePromotionRule ··· 17 18 // 18 19 // this rule depends on ReplyCountPostRule() to set counts 19 20 func AggressivePromotionRule(c *automod.RecordContext, post *appbsky.FeedPost) error { 20 - if c.Account.Identity == nil || !AccountIsYoungerThan(&c.AccountContext, 7*24*time.Hour) { 21 + if c.Account.Identity == nil || !helpers.AccountIsYoungerThan(&c.AccountContext, 7*24*time.Hour) { 21 22 return nil 22 23 } 23 - if post.Reply == nil || IsSelfThread(c, post) { 24 + if post.Reply == nil || helpers.IsSelfThread(c, post) { 24 25 return nil 25 26 } 26 27 27 - allURLs := ExtractTextURLs(post.Text) 28 + allURLs := helpers.ExtractTextURLs(post.Text) 28 29 if c.Account.Profile.Description != nil { 29 - profileURLs := ExtractTextURLs(*c.Account.Profile.Description) 30 + profileURLs := helpers.ExtractTextURLs(*c.Account.Profile.Description) 30 31 allURLs = append(allURLs, profileURLs...) 31 32 } 32 33 hasPromo := false
+3 -2
automod/rules/quick.go
··· 7 7 8 8 appbsky "github.com/bluesky-social/indigo/api/bsky" 9 9 "github.com/bluesky-social/indigo/automod" 10 + "github.com/bluesky-social/indigo/automod/helpers" 10 11 ) 11 12 12 13 var botLinkStrings = []string{"ainna13762491", "LINK押して", "→ https://tiny", "⇒ http://tiny"} ··· 54 55 var _ automod.IdentityRuleFunc = NewAccountBotEmailRule 55 56 56 57 func NewAccountBotEmailRule(c *automod.AccountContext) error { 57 - if c.Account.Identity == nil || !AccountIsYoungerThan(c, 1*time.Hour) { 58 + if c.Account.Identity == nil || !helpers.AccountIsYoungerThan(c, 1*time.Hour) { 58 59 return nil 59 60 } 60 61 ··· 73 74 74 75 // looks for new accounts, which frequently post the same type of content 75 76 func TrivialSpamPostRule(c *automod.RecordContext, post *appbsky.FeedPost) error { 76 - if c.Account.Identity == nil || !AccountIsYoungerThan(&c.AccountContext, 8*24*time.Hour) { 77 + if c.Account.Identity == nil || !helpers.AccountIsYoungerThan(&c.AccountContext, 8*24*time.Hour) { 77 78 return nil 78 79 } 79 80
+13 -12
automod/rules/replies.go
··· 9 9 "github.com/bluesky-social/indigo/atproto/syntax" 10 10 "github.com/bluesky-social/indigo/automod" 11 11 "github.com/bluesky-social/indigo/automod/countstore" 12 + "github.com/bluesky-social/indigo/automod/helpers" 12 13 ) 13 14 14 15 var _ automod.PostRuleFunc = ReplyCountPostRule 15 16 16 17 // does not count "self-replies" (direct to self, or in own post thread) 17 18 func ReplyCountPostRule(c *automod.RecordContext, post *appbsky.FeedPost) error { 18 - if post.Reply == nil || IsSelfThread(c, post) { 19 + if post.Reply == nil || helpers.IsSelfThread(c, post) { 19 20 return nil 20 21 } 21 22 ··· 47 48 // 48 49 // There can be legitimate situations that trigger this rule, so in most situations should be a "report" not "label" action. 49 50 func IdenticalReplyPostRule(c *automod.RecordContext, post *appbsky.FeedPost) error { 50 - if post.Reply == nil || IsSelfThread(c, post) { 51 + if post.Reply == nil || helpers.IsSelfThread(c, post) { 51 52 return nil 52 53 } 53 54 ··· 55 56 if utf8.RuneCountInString(post.Text) <= 10 { 56 57 return nil 57 58 } 58 - if AccountIsOlderThan(&c.AccountContext, 14*24*time.Hour) { 59 + if helpers.AccountIsOlderThan(&c.AccountContext, 14*24*time.Hour) { 59 60 return nil 60 61 } 61 62 62 63 // don't count if there is a follow-back relationship 63 - if ParentOrRootIsFollower(c, post) { 64 + if helpers.ParentOrRootIsFollower(c, post) { 64 65 return nil 65 66 } 66 67 67 68 // increment before read. use a specific period (IncrementPeriod()) to reduce the number of counters (one per unique post text) 68 69 period := countstore.PeriodDay 69 - bucket := c.Account.Identity.DID.String() + "/" + HashOfString(post.Text) 70 + bucket := c.Account.Identity.DID.String() + "/" + helpers.HashOfString(post.Text) 70 71 c.IncrementPeriod("reply-text", bucket, period) 71 72 72 73 count := c.GetCount("reply-text", bucket, period) ··· 91 92 var _ automod.PostRuleFunc = IdenticalReplyPostSameParentRule 92 93 93 94 func IdenticalReplyPostSameParentRule(c *automod.RecordContext, post *appbsky.FeedPost) error { 94 - if post.Reply == nil || IsSelfThread(c, post) { 95 + if post.Reply == nil || helpers.IsSelfThread(c, post) { 95 96 return nil 96 97 } 97 98 98 - if ParentOrRootIsFollower(c, post) { 99 + if helpers.ParentOrRootIsFollower(c, post) { 99 100 return nil 100 101 } 101 102 102 103 postCount := c.Account.PostsCount 103 - if AccountIsOlderThan(&c.AccountContext, identicalReplySameParentMaxAge) || postCount >= identicalReplySameParentMaxPosts { 104 + if helpers.AccountIsOlderThan(&c.AccountContext, identicalReplySameParentMaxAge) || postCount >= identicalReplySameParentMaxPosts { 104 105 return nil 105 106 } 106 107 107 108 period := countstore.PeriodHour 108 - bucket := c.Account.Identity.DID.String() + "/" + post.Reply.Parent.Uri + "/" + HashOfString(post.Text) 109 + bucket := c.Account.Identity.DID.String() + "/" + post.Reply.Parent.Uri + "/" + helpers.HashOfString(post.Text) 109 110 c.IncrementPeriod("reply-text-same-post", bucket, period) 110 111 111 112 count := c.GetCount("reply-text-same-post", bucket, period) ··· 126 127 127 128 func YoungAccountDistinctRepliesRule(c *automod.RecordContext, post *appbsky.FeedPost) error { 128 129 // only replies, and skip self-replies (eg, threads) 129 - if post.Reply == nil || IsSelfThread(c, post) { 130 + if post.Reply == nil || helpers.IsSelfThread(c, post) { 130 131 return nil 131 132 } 132 133 ··· 134 135 if utf8.RuneCountInString(post.Text) <= 10 { 135 136 return nil 136 137 } 137 - if AccountIsOlderThan(&c.AccountContext, 14*24*time.Hour) { 138 + if helpers.AccountIsOlderThan(&c.AccountContext, 14*24*time.Hour) { 138 139 return nil 139 140 } 140 141 141 142 // don't count if there is a follow-back relationship 142 - if ParentOrRootIsFollower(c, post) { 143 + if helpers.ParentOrRootIsFollower(c, post) { 143 144 return nil 144 145 } 145 146
+2 -1
automod/rules/reposts.go
··· 7 7 8 8 "github.com/bluesky-social/indigo/automod" 9 9 "github.com/bluesky-social/indigo/automod/countstore" 10 + "github.com/bluesky-social/indigo/automod/helpers" 10 11 ) 11 12 12 13 var dailyRepostThresholdWithoutPost = 30 ··· 18 19 // looks for accounts which do frequent reposts 19 20 func TooManyRepostRule(c *automod.RecordContext) error { 20 21 // Don't bother checking reposts from accounts older than 30 days 21 - if c.Account.Identity == nil || !AccountIsYoungerThan(&c.AccountContext, 30*24*time.Hour) { 22 + if c.Account.Identity == nil || !helpers.AccountIsYoungerThan(&c.AccountContext, 30*24*time.Hour) { 22 23 return nil 23 24 } 24 25
+2 -2
automod/visual/hiveai_rule.go
··· 5 5 "time" 6 6 7 7 "github.com/bluesky-social/indigo/automod" 8 - "github.com/bluesky-social/indigo/automod/rules" 8 + "github.com/bluesky-social/indigo/automod/helpers" 9 9 lexutil "github.com/bluesky-social/indigo/lex/util" 10 10 ) 11 11 ··· 43 43 44 44 for _, l := range labels { 45 45 // NOTE: experimenting with profile reporting for new accounts 46 - if l == "sexual" && c.RecordOp.Collection.String() == "app.bsky.actor.profile" && rules.AccountIsYoungerThan(&c.AccountContext, 2*24*time.Hour) { 46 + if l == "sexual" && c.RecordOp.Collection.String() == "app.bsky.actor.profile" && helpers.AccountIsYoungerThan(&c.AccountContext, 2*24*time.Hour) { 47 47 c.ReportRecord(automod.ReportReasonSexual, "possible sexual profile (not labeled yet)") 48 48 c.Logger.Info("skipping record label", "label", l, "reason", "sexual-profile-experiment") 49 49 } else {