1package rules
2
3import (
4 "fmt"
5 "time"
6 "unicode/utf8"
7
8 appbsky "github.com/bluesky-social/indigo/api/bsky"
9 "github.com/bluesky-social/indigo/atproto/syntax"
10 "github.com/bluesky-social/indigo/automod"
11 "github.com/bluesky-social/indigo/automod/countstore"
12 "github.com/bluesky-social/indigo/automod/helpers"
13)
14
15var _ automod.PostRuleFunc = ReplyCountPostRule
16
17// does not count "self-replies" (direct to self, or in own post thread)
18func ReplyCountPostRule(c *automod.RecordContext, post *appbsky.FeedPost) error {
19 if post.Reply == nil || helpers.IsSelfThread(c, post) {
20 return nil
21 }
22
23 did := c.Account.Identity.DID.String()
24 if c.GetCount("reply", did, countstore.PeriodDay) > 3 {
25 // TODO: disabled, too noisy for prod
26 //c.AddAccountFlag("frequent-replier")
27 }
28 c.Increment("reply", did)
29
30 parentURI, err := syntax.ParseATURI(post.Reply.Parent.Uri)
31 if err != nil {
32 c.Logger.Warn("failed to parse reply AT-URI", "uri", post.Reply.Parent.Uri)
33 return nil
34 }
35 c.IncrementDistinct("reply-to", did, parentURI.Authority().String())
36 return nil
37}
38
39// triggers on the N+1 post
40// var identicalReplyLimit = 6
41// TODO: bumping temporarily
42var identicalReplyLimit = 20
43var identicalReplyActionLimit = 75
44
45var _ automod.PostRuleFunc = IdenticalReplyPostRule
46
47// Looks for accounts posting the exact same text multiple times. Does not currently count the number of distinct accounts replied to, just counts replies at all.
48//
49// There can be legitimate situations that trigger this rule, so in most situations should be a "report" not "label" action.
50func IdenticalReplyPostRule(c *automod.RecordContext, post *appbsky.FeedPost) error {
51 if post.Reply == nil || helpers.IsSelfThread(c, post) {
52 return nil
53 }
54
55 // don't action short replies, or accounts more than two weeks old
56 if utf8.RuneCountInString(post.Text) <= 10 {
57 return nil
58 }
59 if helpers.AccountIsOlderThan(&c.AccountContext, 14*24*time.Hour) {
60 return nil
61 }
62
63 // don't count if there is a follow-back relationship
64 if helpers.ParentOrRootIsFollower(c, post) {
65 return nil
66 }
67
68 // increment before read. use a specific period (IncrementPeriod()) to reduce the number of counters (one per unique post text)
69 period := countstore.PeriodDay
70 bucket := c.Account.Identity.DID.String() + "/" + helpers.HashOfString(post.Text)
71 c.IncrementPeriod("reply-text", bucket, period)
72
73 count := c.GetCount("reply-text", bucket, period)
74 if count >= identicalReplyLimit {
75 c.AddAccountFlag("multi-identical-reply")
76 c.ReportAccount(automod.ReportReasonSpam, fmt.Sprintf("possible spam (new account, %d identical reply-posts today)", count))
77 c.Notify("slack")
78 }
79 if count >= identicalReplyActionLimit && utf8.RuneCountInString(post.Text) > 100 {
80 c.ReportAccount(automod.ReportReasonRude, fmt.Sprintf("likely spam/harassment (new account, %d identical reply-posts today), actioned (remove label urgently if account is ok)", count))
81 c.AddAccountLabel("!warn")
82 c.Notify("slack")
83 }
84
85 return nil
86}
87
88// Similar to above rule but only counts replies to the same post. More aggressively applies a spam label to new accounts that are less than a day old.
89var identicalReplySameParentLimit = 3
90var identicalReplySameParentMaxAge = 24 * time.Hour
91var identicalReplySameParentMaxPosts int64 = 50
92var _ automod.PostRuleFunc = IdenticalReplyPostSameParentRule
93
94func IdenticalReplyPostSameParentRule(c *automod.RecordContext, post *appbsky.FeedPost) error {
95 if post.Reply == nil || helpers.IsSelfThread(c, post) {
96 return nil
97 }
98
99 if helpers.ParentOrRootIsFollower(c, post) {
100 return nil
101 }
102
103 postCount := c.Account.PostsCount
104 if helpers.AccountIsOlderThan(&c.AccountContext, identicalReplySameParentMaxAge) || postCount >= identicalReplySameParentMaxPosts {
105 return nil
106 }
107
108 period := countstore.PeriodHour
109 bucket := c.Account.Identity.DID.String() + "/" + post.Reply.Parent.Uri + "/" + helpers.HashOfString(post.Text)
110 c.IncrementPeriod("reply-text-same-post", bucket, period)
111
112 count := c.GetCount("reply-text-same-post", bucket, period)
113 if count >= identicalReplySameParentLimit {
114 c.AddAccountFlag("multi-identical-reply-same-post")
115 c.ReportAccount(automod.ReportReasonSpam, fmt.Sprintf("possible spam (%d identical reply-posts to same post today)", count))
116 c.AddAccountLabel("spam")
117 c.Notify("slack")
118 }
119
120 return nil
121}
122
123// TODO: bumping temporarily
124// var youngReplyAccountLimit = 12
125var youngReplyAccountLimit = 200
126var _ automod.PostRuleFunc = YoungAccountDistinctRepliesRule
127
128func YoungAccountDistinctRepliesRule(c *automod.RecordContext, post *appbsky.FeedPost) error {
129 // only replies, and skip self-replies (eg, threads)
130 if post.Reply == nil || helpers.IsSelfThread(c, post) {
131 return nil
132 }
133
134 // don't action short replies, or accounts more than two weeks old
135 if utf8.RuneCountInString(post.Text) <= 10 {
136 return nil
137 }
138 if helpers.AccountIsOlderThan(&c.AccountContext, 14*24*time.Hour) {
139 return nil
140 }
141
142 // don't count if there is a follow-back relationship
143 if helpers.ParentOrRootIsFollower(c, post) {
144 return nil
145 }
146
147 parentURI, err := syntax.ParseATURI(post.Reply.Parent.Uri)
148 if err != nil {
149 c.Logger.Warn("failed to parse reply AT-URI", "uri", post.Reply.Parent.Uri)
150 return nil
151 }
152 parentDID, err := parentURI.Authority().AsDID()
153 if err != nil {
154 c.Logger.Warn("reply AT-URI authority not a DID", "uri", post.Reply.Parent.Uri)
155 return nil
156 }
157
158 did := c.Account.Identity.DID.String()
159
160 c.IncrementDistinct("young-reply-to", did, parentDID.String())
161 // NOTE: won't include the increment from this event
162 count := c.GetCountDistinct("young-reply-to", did, countstore.PeriodHour)
163 if count >= youngReplyAccountLimit {
164 c.AddAccountFlag("new-account-distinct-account-reply")
165 c.ReportAccount(automod.ReportReasonRude, fmt.Sprintf("possible spam (new account, reply-posts to %d distinct accounts in past hour)", count))
166 c.Notify("slack")
167 }
168
169 return nil
170}