+49
automod/helpers/account.go
+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
+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
+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
+
}
-73
automod/helpers/helpers.go
automod/helpers/bsky.go
-73
automod/helpers/helpers.go
automod/helpers/bsky.go
···
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
-
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
11
28
12
func ExtractHashtagsPost(post *appbsky.FeedPost) []string {
29
13
var tags []string
···
152
136
return keyword.TokenizeText(s)
153
137
}
154
138
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
-
}
161
-
162
139
func ExtractTextURLsProfile(profile *appbsky.ActorProfile) []string {
163
140
s := ""
164
141
if profile.Description != nil {
···
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/helpers/helpers_test.go
-251
automod/helpers/helpers_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
-
"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
-
}
+35
automod/helpers/text.go
+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
+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
+
}