1// Bluesky MOderation bot (BMO), a chatops helper for slack
2// For now, polls a PDS for new moderation reports and publishes notifications to slack
3
4package main
5
6import (
7 "io"
8 "log/slog"
9 "os"
10 "strings"
11
12 _ "github.com/joho/godotenv/autoload"
13 _ "go.uber.org/automaxprocs"
14
15 "github.com/carlmjohnson/versioninfo"
16 "github.com/urfave/cli/v2"
17)
18
19func main() {
20 if err := run(os.Args); err != nil {
21 slog.Error("exiting", "err", err)
22 os.Exit(-1)
23 }
24}
25
26func run(args []string) error {
27
28 app := cli.App{
29 Name: "beemo",
30 Usage: "bluesky moderation reporting bot",
31 Version: versioninfo.Short(),
32 }
33
34 app.Flags = []cli.Flag{
35 &cli.StringFlag{
36 Name: "log-level",
37 Usage: "log verbosity level (eg: warn, info, debug)",
38 EnvVars: []string{"BEEMO_LOG_LEVEL", "GO_LOG_LEVEL", "LOG_LEVEL"},
39 },
40 &cli.StringFlag{
41 Name: "slack-webhook-url",
42 // eg: https://hooks.slack.com/services/X1234
43 Usage: "full URL of slack webhook",
44 Required: true,
45 EnvVars: []string{"SLACK_WEBHOOK_URL"},
46 },
47 }
48 app.Commands = []*cli.Command{
49 &cli.Command{
50 Name: "notify-reports",
51 Usage: "watch for new moderation reports, notify in slack",
52 Action: pollNewReports,
53 Flags: []cli.Flag{
54 &cli.StringFlag{
55 Name: "pds-host",
56 Usage: "method, hostname, and port of PDS instance",
57 Value: "http://localhost:4849",
58 EnvVars: []string{"ATP_PDS_HOST"},
59 },
60 &cli.StringFlag{
61 Name: "admin-host",
62 Usage: "method, hostname, and port of admin interface (eg, Ozone), for direct links",
63 Value: "http://localhost:3000",
64 EnvVars: []string{"ATP_ADMIN_HOST"},
65 },
66 &cli.IntFlag{
67 Name: "poll-period",
68 Usage: "API poll period in seconds",
69 Value: 30,
70 EnvVars: []string{"POLL_PERIOD"},
71 },
72 &cli.StringFlag{
73 Name: "handle",
74 Usage: "for PDS login",
75 Required: true,
76 EnvVars: []string{"ATP_AUTH_HANDLE"},
77 },
78 &cli.StringFlag{
79 Name: "password",
80 Usage: "for PDS login",
81 Required: true,
82 EnvVars: []string{"ATP_AUTH_PASSWORD"},
83 },
84 &cli.StringFlag{
85 Name: "admin-password",
86 Usage: "admin authentication password for PDS",
87 Required: true,
88 EnvVars: []string{"ATP_AUTH_ADMIN_PASSWORD"},
89 },
90 },
91 },
92 &cli.Command{
93 Name: "notify-mentions",
94 Usage: "watch firehose for posts mentioning specific accounts",
95 Action: notifyMentions,
96 Flags: []cli.Flag{
97 &cli.StringFlag{
98 Name: "relay-host",
99 Usage: "method, hostname, and port of Relay instance (websocket)",
100 Value: "wss://bsky.network",
101 EnvVars: []string{"ATP_RELAY_HOST"},
102 },
103 &cli.StringFlag{
104 Name: "mention-dids",
105 Usage: "DIDs to look for in mentions (comma-separated)",
106 Required: true,
107 EnvVars: []string{"BEEMO_MENTION_DIDS"},
108 },
109 &cli.IntFlag{
110 Name: "minimum-words",
111 Usage: "minimum length of post text (word count; zero for no minimum)",
112 Value: 0,
113 EnvVars: []string{"BEEMO_MINIMUM_WORDS"},
114 },
115 },
116 },
117 }
118 return app.Run(args)
119}
120
121func configLogger(cctx *cli.Context, writer io.Writer) *slog.Logger {
122 var level slog.Level
123 switch strings.ToLower(cctx.String("log-level")) {
124 case "error":
125 level = slog.LevelError
126 case "warn":
127 level = slog.LevelWarn
128 case "info":
129 level = slog.LevelInfo
130 case "debug":
131 level = slog.LevelDebug
132 default:
133 level = slog.LevelInfo
134 }
135 logger := slog.New(slog.NewJSONHandler(writer, &slog.HandlerOptions{
136 Level: level,
137 }))
138 slog.SetDefault(logger)
139 return logger
140}