this repo has no description
0
fork

Configure Feed

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

beemo: split out files

+170 -154
-154
cmd/beemo/main.go
··· 4 4 package main 5 5 6 6 import ( 7 - "bytes" 8 - "context" 9 - "encoding/json" 10 - "fmt" 11 - "net/http" 12 7 "os" 13 - "strings" 14 - "time" 15 - 16 - comatproto "github.com/bluesky-social/indigo/api/atproto" 17 - toolsozone "github.com/bluesky-social/indigo/api/ozone" 18 - "github.com/bluesky-social/indigo/util" 19 - "github.com/bluesky-social/indigo/xrpc" 20 8 21 9 _ "github.com/joho/godotenv/autoload" 22 10 _ "go.uber.org/automaxprocs" ··· 97 85 return app.Run(args) 98 86 } 99 87 100 - func pollNewReports(cctx *cli.Context) error { 101 - // record last-seen report timestamp 102 - since := time.Now() 103 - // NOTE: uncomment this for testing 104 - //since = time.Now().Add(time.Duration(-12) * time.Hour) 105 - period := time.Duration(cctx.Int("poll-period")) * time.Second 106 - 107 - // create a new session 108 - xrpcc := &xrpc.Client{ 109 - Client: util.RobustHTTPClient(), 110 - Host: cctx.String("pds-host"), 111 - Auth: &xrpc.AuthInfo{Handle: cctx.String("handle")}, 112 - } 113 - 114 - auth, err := comatproto.ServerCreateSession(context.TODO(), xrpcc, &comatproto.ServerCreateSession_Input{ 115 - Identifier: xrpcc.Auth.Handle, 116 - Password: cctx.String("password"), 117 - }) 118 - if err != nil { 119 - return err 120 - } 121 - xrpcc.Auth.AccessJwt = auth.AccessJwt 122 - xrpcc.Auth.RefreshJwt = auth.RefreshJwt 123 - xrpcc.Auth.Did = auth.Did 124 - xrpcc.Auth.Handle = auth.Handle 125 - 126 - adminToken := cctx.String("admin-password") 127 - if len(adminToken) > 0 { 128 - xrpcc.AdminToken = &adminToken 129 - } 130 - log.Infof("report polling bot starting up...") 131 - // can flip this bool to false to prevent spamming slack channel on startup 132 - if true { 133 - err := sendSlackMsg(cctx, fmt.Sprintf("restarted bot, monitoring for reports since `%s`...", since.Format(time.RFC3339))) 134 - if err != nil { 135 - return err 136 - } 137 - } 138 - for { 139 - // refresh session 140 - xrpcc.Auth.AccessJwt = xrpcc.Auth.RefreshJwt 141 - refresh, err := comatproto.ServerRefreshSession(context.TODO(), xrpcc) 142 - if err != nil { 143 - return err 144 - } 145 - xrpcc.Auth.AccessJwt = refresh.AccessJwt 146 - xrpcc.Auth.RefreshJwt = refresh.RefreshJwt 147 - 148 - // query just new reports (regardless of resolution state) 149 - // ModerationQueryEvents(ctx context.Context, c *xrpc.Client, createdBy string, cursor string, includeAllUserRecords bool, limit int64, sortDirection string, subject string, types []string) (*ModerationQueryEvents_Output, error) 150 - var limit int64 = 50 151 - me, err := toolsozone.ModerationQueryEvents( 152 - cctx.Context, 153 - xrpcc, 154 - nil, 155 - nil, 156 - "", 157 - "", 158 - "", 159 - "", 160 - "", 161 - false, 162 - true, 163 - limit, 164 - nil, 165 - nil, 166 - nil, 167 - "", 168 - "", 169 - []string{"tools.ozone.moderation.defs#modEventReport"}, 170 - ) 171 - if err != nil { 172 - return err 173 - } 174 - // this works out to iterate from newest to oldest, which is the behavior we want (report only newest, then break) 175 - for _, evt := range me.Events { 176 - report := evt.Event.ModerationDefs_ModEventReport 177 - // TODO: filter out based on subject state? similar to old "report.ResolvedByActionIds" 178 - createdAt, err := time.Parse(time.RFC3339, evt.CreatedAt) 179 - if err != nil { 180 - return fmt.Errorf("invalid time format for 'createdAt': %w", err) 181 - } 182 - if createdAt.After(since) { 183 - shortType := "" 184 - if report.ReportType != nil && strings.Contains(*report.ReportType, "#") { 185 - shortType = strings.SplitN(*report.ReportType, "#", 2)[1] 186 - } 187 - // ok, we found a "new" report, need to notify 188 - msg := fmt.Sprintf("⚠️ New report at `%s` ⚠️\n", evt.CreatedAt) 189 - msg += fmt.Sprintf("report id: `%d`\t", evt.Id) 190 - msg += fmt.Sprintf("instance: `%s`\n", cctx.String("pds-host")) 191 - msg += fmt.Sprintf("reasonType: `%s`\t", shortType) 192 - msg += fmt.Sprintf("Admin: %s/reports/%d\n", cctx.String("admin-host"), evt.Id) 193 - //msg += fmt.Sprintf("reportedByDid: `%s`\n", report.ReportedByDid) 194 - log.Infof("found new report, notifying slack: %s", report) 195 - err := sendSlackMsg(cctx, msg) 196 - if err != nil { 197 - return fmt.Errorf("failed to send slack message: %w", err) 198 - } 199 - since = createdAt 200 - break 201 - } else { 202 - log.Debugf("skipping report: %s", report) 203 - } 204 - } 205 - log.Infof("... sleeping for %s", period) 206 - time.Sleep(period) 207 - } 208 - } 209 - 210 - type SlackWebhookBody struct { 211 - Text string `json:"text"` 212 - } 213 - 214 - // sends a simple slack message to a channel via "incoming webhook" 215 - // The slack incoming webhook must be already configured in the slack workplace. 216 - func sendSlackMsg(cctx *cli.Context, msg string) error { 217 - // loosely based on: https://golangcode.com/send-slack-messages-without-a-library/ 218 - 219 - webhookUrl := cctx.String("slack-webhook-url") 220 - body, _ := json.Marshal(SlackWebhookBody{Text: msg}) 221 - req, err := http.NewRequest(http.MethodPost, webhookUrl, bytes.NewBuffer(body)) 222 - if err != nil { 223 - return err 224 - } 225 - req.Header.Add("Content-Type", "application/json") 226 - client := util.RobustHTTPClient() 227 - resp, err := client.Do(req) 228 - if err != nil { 229 - return err 230 - } 231 - 232 - defer resp.Body.Close() 233 - 234 - buf := new(bytes.Buffer) 235 - buf.ReadFrom(resp.Body) 236 - if resp.StatusCode != 200 || buf.String() != "ok" { 237 - // TODO: in some cases print body? eg, if short and text 238 - return fmt.Errorf("failed slack webhook POST request. status=%d", resp.StatusCode) 239 - } 240 - return nil 241 - }
+125
cmd/beemo/notify_reports.go
··· 1 + package main 2 + 3 + import ( 4 + "context" 5 + "fmt" 6 + "strings" 7 + "time" 8 + 9 + comatproto "github.com/bluesky-social/indigo/api/atproto" 10 + toolsozone "github.com/bluesky-social/indigo/api/ozone" 11 + "github.com/bluesky-social/indigo/util" 12 + "github.com/bluesky-social/indigo/xrpc" 13 + 14 + "github.com/urfave/cli/v2" 15 + ) 16 + 17 + func pollNewReports(cctx *cli.Context) error { 18 + // record last-seen report timestamp 19 + since := time.Now() 20 + // NOTE: uncomment this for testing 21 + //since = time.Now().Add(time.Duration(-12) * time.Hour) 22 + period := time.Duration(cctx.Int("poll-period")) * time.Second 23 + 24 + // create a new session 25 + xrpcc := &xrpc.Client{ 26 + Client: util.RobustHTTPClient(), 27 + Host: cctx.String("pds-host"), 28 + Auth: &xrpc.AuthInfo{Handle: cctx.String("handle")}, 29 + } 30 + 31 + auth, err := comatproto.ServerCreateSession(context.TODO(), xrpcc, &comatproto.ServerCreateSession_Input{ 32 + Identifier: xrpcc.Auth.Handle, 33 + Password: cctx.String("password"), 34 + }) 35 + if err != nil { 36 + return err 37 + } 38 + xrpcc.Auth.AccessJwt = auth.AccessJwt 39 + xrpcc.Auth.RefreshJwt = auth.RefreshJwt 40 + xrpcc.Auth.Did = auth.Did 41 + xrpcc.Auth.Handle = auth.Handle 42 + 43 + adminToken := cctx.String("admin-password") 44 + if len(adminToken) > 0 { 45 + xrpcc.AdminToken = &adminToken 46 + } 47 + log.Infof("report polling bot starting up...") 48 + // can flip this bool to false to prevent spamming slack channel on startup 49 + if true { 50 + err := sendSlackMsg(cctx, fmt.Sprintf("restarted bot, monitoring for reports since `%s`...", since.Format(time.RFC3339))) 51 + if err != nil { 52 + return err 53 + } 54 + } 55 + for { 56 + // refresh session 57 + xrpcc.Auth.AccessJwt = xrpcc.Auth.RefreshJwt 58 + refresh, err := comatproto.ServerRefreshSession(context.TODO(), xrpcc) 59 + if err != nil { 60 + return err 61 + } 62 + xrpcc.Auth.AccessJwt = refresh.AccessJwt 63 + xrpcc.Auth.RefreshJwt = refresh.RefreshJwt 64 + 65 + // query just new reports (regardless of resolution state) 66 + // ModerationQueryEvents(ctx context.Context, c *xrpc.Client, createdBy string, cursor string, includeAllUserRecords bool, limit int64, sortDirection string, subject string, types []string) (*ModerationQueryEvents_Output, error) 67 + var limit int64 = 50 68 + me, err := toolsozone.ModerationQueryEvents( 69 + cctx.Context, 70 + xrpcc, 71 + nil, 72 + nil, 73 + "", 74 + "", 75 + "", 76 + "", 77 + "", 78 + false, 79 + true, 80 + limit, 81 + nil, 82 + nil, 83 + nil, 84 + "", 85 + "", 86 + []string{"tools.ozone.moderation.defs#modEventReport"}, 87 + ) 88 + if err != nil { 89 + return err 90 + } 91 + // this works out to iterate from newest to oldest, which is the behavior we want (report only newest, then break) 92 + for _, evt := range me.Events { 93 + report := evt.Event.ModerationDefs_ModEventReport 94 + // TODO: filter out based on subject state? similar to old "report.ResolvedByActionIds" 95 + createdAt, err := time.Parse(time.RFC3339, evt.CreatedAt) 96 + if err != nil { 97 + return fmt.Errorf("invalid time format for 'createdAt': %w", err) 98 + } 99 + if createdAt.After(since) { 100 + shortType := "" 101 + if report.ReportType != nil && strings.Contains(*report.ReportType, "#") { 102 + shortType = strings.SplitN(*report.ReportType, "#", 2)[1] 103 + } 104 + // ok, we found a "new" report, need to notify 105 + msg := fmt.Sprintf("⚠️ New report at `%s` ⚠️\n", evt.CreatedAt) 106 + msg += fmt.Sprintf("report id: `%d`\t", evt.Id) 107 + msg += fmt.Sprintf("instance: `%s`\n", cctx.String("pds-host")) 108 + msg += fmt.Sprintf("reasonType: `%s`\t", shortType) 109 + msg += fmt.Sprintf("Admin: %s/reports/%d\n", cctx.String("admin-host"), evt.Id) 110 + //msg += fmt.Sprintf("reportedByDid: `%s`\n", report.ReportedByDid) 111 + log.Infof("found new report, notifying slack: %s", report) 112 + err := sendSlackMsg(cctx, msg) 113 + if err != nil { 114 + return fmt.Errorf("failed to send slack message: %w", err) 115 + } 116 + since = createdAt 117 + break 118 + } else { 119 + log.Debugf("skipping report: %s", report) 120 + } 121 + } 122 + log.Infof("... sleeping for %s", period) 123 + time.Sleep(period) 124 + } 125 + }
+45
cmd/beemo/slack.go
··· 1 + package main 2 + 3 + import ( 4 + "bytes" 5 + "encoding/json" 6 + "fmt" 7 + "net/http" 8 + 9 + "github.com/bluesky-social/indigo/util" 10 + 11 + "github.com/urfave/cli/v2" 12 + ) 13 + 14 + type SlackWebhookBody struct { 15 + Text string `json:"text"` 16 + } 17 + 18 + // sends a simple slack message to a channel via "incoming webhook" 19 + // The slack incoming webhook must be already configured in the slack workplace. 20 + func sendSlackMsg(cctx *cli.Context, msg string) error { 21 + // loosely based on: https://golangcode.com/send-slack-messages-without-a-library/ 22 + 23 + webhookUrl := cctx.String("slack-webhook-url") 24 + body, _ := json.Marshal(SlackWebhookBody{Text: msg}) 25 + req, err := http.NewRequest(http.MethodPost, webhookUrl, bytes.NewBuffer(body)) 26 + if err != nil { 27 + return err 28 + } 29 + req.Header.Add("Content-Type", "application/json") 30 + client := util.RobustHTTPClient() 31 + resp, err := client.Do(req) 32 + if err != nil { 33 + return err 34 + } 35 + 36 + defer resp.Body.Close() 37 + 38 + buf := new(bytes.Buffer) 39 + buf.ReadFrom(resp.Body) 40 + if resp.StatusCode != 200 || buf.String() != "ok" { 41 + // TODO: in some cases print body? eg, if short and text 42 + return fmt.Errorf("failed slack webhook POST request. status=%d", resp.StatusCode) 43 + } 44 + return nil 45 + }