+4
automod/engine/context.go
+4
automod/engine/context.go
+14
automod/engine/effects.go
+14
automod/engine/effects.go
···
56
56
BlobTakedowns []string
57
57
// If "true", indicates that a rule indicates that the action causing the event should be blocked or prevented
58
58
RejectEvent bool
59
+
// Services, if any, which should blast out a notification about this even (eg, Slack)
60
+
NotifyServices []string
59
61
}
60
62
61
63
// Enqueues the named counter to be incremented at the end of all rule processing. Will automatically increment for all time periods.
···
183
185
}
184
186
}
185
187
e.BlobTakedowns = append(e.BlobTakedowns, cid)
188
+
}
189
+
190
+
// Records that the given service should be notified about this event
191
+
func (e *Effects) Notify(srv string) {
192
+
e.mu.Lock()
193
+
defer e.mu.Unlock()
194
+
for _, v := range e.NotifyServices {
195
+
if v == srv {
196
+
return
197
+
}
198
+
}
199
+
e.NotifyServices = append(e.NotifyServices, srv)
186
200
}
187
201
188
202
func (e *Effects) Reject() {
+13
-10
automod/engine/engine.go
+13
-10
automod/engine/engine.go
···
26
26
//
27
27
// NOTE: careful when initializing: several fields must not be nil or zero, even though they are pointer type.
28
28
type Engine struct {
29
-
Logger *slog.Logger
30
-
Directory identity.Directory
31
-
Rules RuleSet
32
-
Counters countstore.CountStore
33
-
Sets setstore.SetStore
34
-
Cache cachestore.CacheStore
35
-
Flags flagstore.FlagStore
29
+
Logger *slog.Logger
30
+
Directory identity.Directory
31
+
Rules RuleSet
32
+
Counters countstore.CountStore
33
+
Sets setstore.SetStore
34
+
Cache cachestore.CacheStore
35
+
Flags flagstore.FlagStore
36
+
// unlike the other sub-modules, this field (Notifier) may be nil
37
+
Notifier Notifier
38
+
// TODO: unused; remove?
36
39
RelayClient *xrpc.Client
37
-
BskyClient *xrpc.Client
40
+
// use to fetch public account metadata from AppView
41
+
BskyClient *xrpc.Client
38
42
// used to persist moderation actions in mod service (optional)
39
-
AdminClient *xrpc.Client
40
-
SlackWebhookURL string
43
+
AdminClient *xrpc.Client
41
44
}
42
45
43
46
// Entrypoint for external code pushing arbitrary identity events in to the engine.
+11
automod/engine/notifier.go
+11
automod/engine/notifier.go
···
1
+
package engine
2
+
3
+
import (
4
+
"context"
5
+
)
6
+
7
+
// Interface for a type that can handle sending notifications
8
+
type Notifier interface {
9
+
SendAccount(ctx context.Context, service string, c *AccountContext) error
10
+
SendRecord(ctx context.Context, service string, c *RecordContext) error
11
+
}
+10
-9
automod/engine/persist.go
+10
-9
automod/engine/persist.go
···
58
58
}
59
59
60
60
anyModActions := newTakedown || len(newLabels) > 0 || len(newFlags) > 0 || len(newReports) > 0
61
-
if anyModActions && eng.SlackWebhookURL != "" {
62
-
msg := slackBody("⚠️ Automod Account Action ⚠️\n", c.Account, newLabels, newFlags, newReports, newTakedown)
63
-
if err := eng.SendSlackMsg(ctx, msg); err != nil {
64
-
c.Logger.Error("sending slack webhook", "err", err)
61
+
if anyModActions && eng.Notifier != nil {
62
+
for _, srv := range dedupeStrings(c.effects.NotifyServices) {
63
+
if err := eng.Notifier.SendAccount(ctx, srv, c); err != nil {
64
+
c.Logger.Error("failed to deliver notification", "service", srv, "err", err)
65
+
}
65
66
}
66
67
}
67
68
···
167
168
atURI := fmt.Sprintf("at://%s/%s/%s", c.Account.Identity.DID, c.RecordOp.Collection, c.RecordOp.RecordKey)
168
169
169
170
if newTakedown || len(newLabels) > 0 || len(newFlags) > 0 || len(newReports) > 0 {
170
-
if eng.SlackWebhookURL != "" {
171
-
msg := slackBody("⚠️ Automod Record Action ⚠️\n", c.Account, newLabels, newFlags, newReports, newTakedown)
172
-
msg += fmt.Sprintf("`%s`\n", atURI)
173
-
if err := eng.SendSlackMsg(ctx, msg); err != nil {
174
-
c.Logger.Error("sending slack webhook", "err", err)
171
+
if eng.Notifier != nil {
172
+
for _, srv := range dedupeStrings(c.effects.NotifyServices) {
173
+
if err := eng.Notifier.SendRecord(ctx, srv, c); err != nil {
174
+
c.Logger.Error("failed to deliver notification", "service", srv, "err", err)
175
+
}
175
176
}
176
177
}
177
178
}
-24
automod/engine/persisthelpers.go
-24
automod/engine/persisthelpers.go
···
3
3
import (
4
4
"context"
5
5
"fmt"
6
-
"strings"
7
6
"time"
8
7
9
8
comatproto "github.com/bluesky-social/indigo/api/atproto"
···
158
157
}
159
158
return true, nil
160
159
}
161
-
162
-
func slackBody(header string, acct AccountMeta, newLabels, newFlags []string, newReports []ModReport, newTakedown bool) string {
163
-
msg := header
164
-
msg += fmt.Sprintf("`%s` / `%s` / <https://bsky.app/profile/%s|bsky> / <https://admin.prod.bsky.dev/repositories/%s|ozone>\n",
165
-
acct.Identity.DID,
166
-
acct.Identity.Handle,
167
-
acct.Identity.DID,
168
-
acct.Identity.DID,
169
-
)
170
-
if len(newLabels) > 0 {
171
-
msg += fmt.Sprintf("New Labels: `%s`\n", strings.Join(newLabels, ", "))
172
-
}
173
-
if len(newFlags) > 0 {
174
-
msg += fmt.Sprintf("New Flags: `%s`\n", strings.Join(newFlags, ", "))
175
-
}
176
-
for _, rep := range newReports {
177
-
msg += fmt.Sprintf("Report `%s`: %s\n", rep.ReasonType, rep.Comment)
178
-
}
179
-
if newTakedown {
180
-
msg += fmt.Sprintf("Takedown!\n")
181
-
}
182
-
return msg
183
-
}
+50
-2
automod/engine/slack.go
+50
-2
automod/engine/slack.go
···
6
6
"encoding/json"
7
7
"fmt"
8
8
"net/http"
9
+
"strings"
9
10
)
10
11
12
+
type SlackNotifier struct {
13
+
SlackWebhookURL string
14
+
}
15
+
16
+
func (n *SlackNotifier) SendAccount(ctx context.Context, service string, c *AccountContext) error {
17
+
if service != "slack" {
18
+
return nil
19
+
}
20
+
msg := slackBody("⚠️ Automod Account Action ⚠️\n", c.Account, c.effects.AccountLabels, c.effects.AccountFlags, c.effects.AccountReports, c.effects.AccountTakedown)
21
+
c.Logger.Debug("sending slack notification")
22
+
return n.sendSlackMsg(ctx, msg)
23
+
}
24
+
25
+
func (n *SlackNotifier) SendRecord(ctx context.Context, service string, c *RecordContext) error {
26
+
if service != "slack" {
27
+
return nil
28
+
}
29
+
atURI := fmt.Sprintf("at://%s/%s/%s", c.Account.Identity.DID, c.RecordOp.Collection, c.RecordOp.RecordKey)
30
+
msg := slackBody("⚠️ Automod Record Action ⚠️\n", c.Account, c.effects.RecordLabels, c.effects.RecordFlags, c.effects.RecordReports, c.effects.RecordTakedown)
31
+
msg += fmt.Sprintf("`%s`\n", atURI)
32
+
c.Logger.Debug("sending slack notification")
33
+
return n.sendSlackMsg(ctx, msg)
34
+
}
35
+
11
36
type SlackWebhookBody struct {
12
37
Text string `json:"text"`
13
38
}
···
15
40
// Sends a simple slack message to a channel via "incoming webhook".
16
41
//
17
42
// The slack incoming webhook must be already configured in the slack workplace.
18
-
func (e *Engine) SendSlackMsg(ctx context.Context, msg string) error {
43
+
func (n *SlackNotifier) sendSlackMsg(ctx context.Context, msg string) error {
19
44
// loosely based on: https://golangcode.com/send-slack-messages-without-a-library/
20
45
21
46
body, err := json.Marshal(SlackWebhookBody{Text: msg})
22
47
if err != nil {
23
48
return err
24
49
}
25
-
req, err := http.NewRequestWithContext(ctx, http.MethodPost, e.SlackWebhookURL, bytes.NewBuffer(body))
50
+
req, err := http.NewRequestWithContext(ctx, http.MethodPost, n.SlackWebhookURL, bytes.NewBuffer(body))
26
51
if err != nil {
27
52
return err
28
53
}
···
42
67
}
43
68
return nil
44
69
}
70
+
71
+
func slackBody(header string, acct AccountMeta, newLabels, newFlags []string, newReports []ModReport, newTakedown bool) string {
72
+
msg := header
73
+
msg += fmt.Sprintf("`%s` / `%s` / <https://bsky.app/profile/%s|bsky> / <https://admin.prod.bsky.dev/repositories/%s|ozone>\n",
74
+
acct.Identity.DID,
75
+
acct.Identity.Handle,
76
+
acct.Identity.DID,
77
+
acct.Identity.DID,
78
+
)
79
+
if len(newLabels) > 0 {
80
+
msg += fmt.Sprintf("Labels: `%s`\n", strings.Join(newLabels, ", "))
81
+
}
82
+
if len(newFlags) > 0 {
83
+
msg += fmt.Sprintf("Flags: `%s`\n", strings.Join(newFlags, ", "))
84
+
}
85
+
for _, rep := range newReports {
86
+
msg += fmt.Sprintf("Report `%s`: %s\n", rep.ReasonType, rep.Comment)
87
+
}
88
+
if newTakedown {
89
+
msg += fmt.Sprintf("Takedown!\n")
90
+
}
91
+
return msg
92
+
}