Skip to content

Commit 16cc82d

Browse files
Add tweet webhook (#24)
* Add tweet webhook * Move
1 parent 709f306 commit 16cc82d

File tree

8 files changed

+171
-20
lines changed

8 files changed

+171
-20
lines changed

common/jobs/riverclient/river.go

+7-4
Original file line numberDiff line numberDiff line change
@@ -4,28 +4,31 @@ import (
44
"context"
55
"github.com/jackc/pgx/v5"
66
"github.com/jackc/pgx/v5/pgxpool"
7+
"github.com/metoro-io/statusphere/common/db"
78
"github.com/metoro-io/statusphere/common/jobs/slack_webhook"
9+
"github.com/metoro-io/statusphere/common/jobs/twitter_post"
810
"github.com/riverqueue/river"
911
"github.com/riverqueue/river/riverdriver/riverpgxv5"
1012
"github.com/riverqueue/river/rivermigrate"
1113
"go.uber.org/zap"
1214
"net/http"
1315
)
1416

15-
func spawnWorkers(logger *zap.Logger, client *http.Client) *river.Workers {
17+
func spawnWorkers(logger *zap.Logger, client *http.Client, dbClient *db.DbClient) *river.Workers {
1618
workers := river.NewWorkers()
1719
river.AddWorker(workers, slack_webhook.NewSlackWebhookWorker(logger, client))
20+
river.AddWorker(workers, twitter_post.NewTwitterPostWorker(logger, client, dbClient))
1821
return workers
1922
}
2023

21-
func NewRiverClient(pool *pgxpool.Pool, logger *zap.Logger, client *http.Client, numWorkers int) (*river.Client[pgx.Tx], error) {
22-
return river.NewClient(riverpgxv5.New(pool), &river.Config{
24+
func NewRiverClient(dbClient *db.DbClient, logger *zap.Logger, client *http.Client, numWorkers int) (*river.Client[pgx.Tx], error) {
25+
return river.NewClient(riverpgxv5.New(dbClient.PgxPool), &river.Config{
2326
Queues: map[string]river.QueueConfig{
2427
river.QueueDefault: {
2528
MaxWorkers: numWorkers,
2629
},
2730
},
28-
Workers: spawnWorkers(logger, client),
31+
Workers: spawnWorkers(logger, client, dbClient),
2932
})
3033
}
3134

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package twitter_post
2+
3+
import (
4+
"github.com/metoro-io/statusphere/common/api"
5+
"github.com/riverqueue/river"
6+
)
7+
8+
type TwitterPostArgs struct {
9+
Incident api.Incident `json:"incident"`
10+
WebhookUrl string `json:"webhook_url"`
11+
}
12+
13+
func (TwitterPostArgs) Kind() string {
14+
return "twitter_post"
15+
}
16+
17+
func (TwitterPostArgs) InsertOpts() river.InsertOpts {
18+
return river.InsertOpts{
19+
MaxAttempts: 10,
20+
}
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package twitter_post
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"fmt"
7+
"github.com/metoro-io/statusphere/common/api"
8+
"github.com/metoro-io/statusphere/common/db"
9+
"github.com/pkg/errors"
10+
"github.com/riverqueue/river"
11+
"go.uber.org/zap"
12+
"math"
13+
"net/http"
14+
"time"
15+
)
16+
17+
type TwitterPostWorker struct {
18+
// An embedded WorkerDefaults sets up default methods to fulfill the rest of
19+
// the Worker interface:
20+
river.WorkerDefaults[TwitterPostArgs]
21+
logger *zap.Logger
22+
httpClient *http.Client
23+
db *db.DbClient
24+
}
25+
26+
func NewTwitterPostWorker(logger *zap.Logger, httpClient *http.Client, dbClient *db.DbClient) *TwitterPostWorker {
27+
return &TwitterPostWorker{
28+
logger: logger,
29+
httpClient: httpClient,
30+
db: dbClient,
31+
}
32+
}
33+
34+
func (w *TwitterPostWorker) Work(ctx context.Context, job *river.Job[TwitterPostArgs]) error {
35+
w.logger.Info("Sending slack webhook", zap.Any("incident", job.Args.Incident))
36+
if job.Args.WebhookUrl == "" {
37+
w.logger.Error("webhook url is empty")
38+
return nil
39+
}
40+
tweet, err := generateTweet(w.db, job.Args.Incident)
41+
if err != nil {
42+
return errors.Wrap(err, "failed to generate tweet")
43+
}
44+
postBody := fmt.Sprintf(`{'tweet': '%s'}`, string(tweet))
45+
req, err := http.NewRequest("POST", job.Args.WebhookUrl, bytes.NewBuffer([]byte(postBody)))
46+
if err != nil {
47+
return errors.Wrap(err, "failed to create request")
48+
}
49+
req.Header.Set("Content-Type", "application/json")
50+
resp, err := w.httpClient.Do(req)
51+
if err != nil {
52+
return errors.Wrap(err, "failed to send request")
53+
}
54+
if resp.StatusCode != http.StatusOK {
55+
return errors.Errorf("expected status code 200, got %d", resp.StatusCode)
56+
}
57+
return nil
58+
}
59+
60+
func generateTweet(db *db.DbClient, incident api.Incident) (string, error) {
61+
// Get the status page of the incident
62+
statusPage, err := db.GetStatusPage(context.Background(), incident.StatusPageUrl)
63+
if err != nil {
64+
return "", errors.Wrap(err, "failed to get status page")
65+
}
66+
// Tweet format
67+
// {Status page Name} Incident
68+
// {Incident Title}
69+
// {Incident Description}
70+
// {Incident Deep Link}
71+
// https://metoro.io/statusphere/status/{statusPageName}
72+
73+
tweet := fmt.Sprintf("%s Incident\n%s\n%s\n%s\nhttps://metoro.io/statusphere/status/%s", statusPage.Name, incident.Title, incident.Description, incident.DeepLink, statusPage.Name)
74+
75+
return tweet, nil
76+
}
77+
78+
func (w *TwitterPostWorker) Timeout(job *river.Job[TwitterPostArgs]) time.Duration {
79+
return time.Minute * 5
80+
}
81+
82+
// NextRetry performs exponential backoff with a maximum of 120 seconds.
83+
func (w *TwitterPostWorker) NextRetry(job *river.Job[TwitterPostArgs]) time.Time {
84+
return time.Now().Add(time.Duration(math.Min(math.Pow(2.0, float64(job.Attempt)), 120)) * time.Second)
85+
}

go.mod

+6
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,13 @@ require (
66
github.com/PuerkitoBio/goquery v1.9.1 // indirect
77
github.com/andybalholm/cascadia v1.3.2 // indirect
88
github.com/bytedance/sonic v1.11.3 // indirect
9+
github.com/cenkalti/backoff/v4 v4.1.3 // indirect
910
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
1011
github.com/chenzhuoyu/iasm v0.9.1 // indirect
12+
github.com/dghubble/go-twitter v0.0.0-20221104224141-912508c3888b // indirect
13+
github.com/dghubble/oauth1 v0.7.3 // indirect
14+
github.com/dghubble/sling v1.4.0 // indirect
15+
github.com/g8rswimmer/go-twitter/v2 v2.1.5 // indirect
1116
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
1217
github.com/gin-contrib/cors v1.7.1 // indirect
1318
github.com/gin-contrib/gzip v1.0.0 // indirect
@@ -17,6 +22,7 @@ require (
1722
github.com/go-playground/universal-translator v0.18.1 // indirect
1823
github.com/go-playground/validator/v10 v10.19.0 // indirect
1924
github.com/goccy/go-json v0.10.2 // indirect
25+
github.com/google/go-querystring v1.1.0 // indirect
2026
github.com/jackc/pgpassfile v1.0.0 // indirect
2127
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
2228
github.com/jackc/pgx/v5 v5.5.5 // indirect

go.sum

+13
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZX
88
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
99
github.com/bytedance/sonic v1.11.3 h1:jRN+yEjakWh8aK5FzrciUHG8OFXK+4/KrAX/ysEtHAA=
1010
github.com/bytedance/sonic v1.11.3/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
11+
github.com/cenkalti/backoff/v4 v4.1.3 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8UtC4=
12+
github.com/cenkalti/backoff/v4 v4.1.3/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
1113
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
1214
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
1315
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
@@ -19,6 +21,14 @@ github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLI
1921
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
2022
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
2123
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
24+
github.com/dghubble/go-twitter v0.0.0-20221104224141-912508c3888b h1:XQu6o3AwJx/jsg9LZ41uIeUdXK5be099XFfFn6H9ikk=
25+
github.com/dghubble/go-twitter v0.0.0-20221104224141-912508c3888b/go.mod h1:B0/qdW5XUupJvcsx40hnVbfjzz9He5YpYXx6eVVdiSY=
26+
github.com/dghubble/oauth1 v0.7.3 h1:EkEM/zMDMp3zOsX2DC/ZQ2vnEX3ELK0/l9kb+vs4ptE=
27+
github.com/dghubble/oauth1 v0.7.3/go.mod h1:oxTe+az9NSMIucDPDCCtzJGsPhciJV33xocHfcR2sVY=
28+
github.com/dghubble/sling v1.4.0 h1:/n8MRosVTthvMbwlNZgLx579OGVjUOy3GNEv5BIqAWY=
29+
github.com/dghubble/sling v1.4.0/go.mod h1:0r40aNsU9EdDUVBNhfCstAtFgutjgJGYbO1oNzkMoM8=
30+
github.com/g8rswimmer/go-twitter/v2 v2.1.5 h1:Uj9Yuof2UducrP4Xva7irnUJfB9354/VyUXKmc2D5gg=
31+
github.com/g8rswimmer/go-twitter/v2 v2.1.5/go.mod h1:/55xWb313KQs25X7oZrNSEwLQNkYHhPsDwFstc45vhc=
2232
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
2333
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
2434
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
@@ -42,7 +52,10 @@ github.com/go-playground/validator/v10 v10.19.0/go.mod h1:dbuPbCMFw/DrkbEynArYaC
4252
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
4353
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
4454
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
55+
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
4556
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
57+
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
58+
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
4659
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
4760
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
4861
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=

jobrunner/internal/config/config.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ package config
33
import "github.com/kelseyhightower/envconfig"
44

55
type Config struct {
6-
SlackWebhookUrl string `envconfig:"SLACK_WEBHOOK_URL"`
6+
SlackWebhookUrl string `envconfig:"SLACK_WEBHOOK_URL"`
7+
TwitterWebhookUrl string `envconfig:"TWITTER_WEBHOOK_URL"`
78
}
89

910
func GetConfigFromEnvironment() (Config, error) {

jobrunner/internal/incidentpoller/incidentpoller.go

+35-13
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ package incidentpoller
33
import (
44
"context"
55
"github.com/jackc/pgx/v5"
6+
"github.com/metoro-io/statusphere/common/api"
67
"github.com/metoro-io/statusphere/common/db"
78
"github.com/metoro-io/statusphere/common/jobs/slack_webhook"
9+
"github.com/metoro-io/statusphere/common/jobs/twitter_post"
810
"github.com/pkg/errors"
911
"github.com/riverqueue/river"
1012
"go.uber.org/zap"
@@ -14,18 +16,20 @@ import (
1416
// IncidentPoller is a poller that polls incidents from the database, if the jobs have not been started for them, then it starts them
1517

1618
type IncidentPoller struct {
17-
db *db.DbClient
18-
logger *zap.Logger
19-
riverClient *river.Client[pgx.Tx]
20-
slackWebhookUrl string
19+
db *db.DbClient
20+
logger *zap.Logger
21+
riverClient *river.Client[pgx.Tx]
22+
slackWebhookUrl string
23+
twitterWebhookUrl string
2124
}
2225

23-
func NewIncidentPoller(db *db.DbClient, logger *zap.Logger, client *river.Client[pgx.Tx], webhookUrl string) *IncidentPoller {
26+
func NewIncidentPoller(db *db.DbClient, logger *zap.Logger, client *river.Client[pgx.Tx], slackWebhookUrl string, twitterWebhookUrl string) *IncidentPoller {
2427
return &IncidentPoller{
25-
db: db,
26-
logger: logger,
27-
riverClient: client,
28-
slackWebhookUrl: webhookUrl,
28+
db: db,
29+
logger: logger,
30+
riverClient: client,
31+
slackWebhookUrl: slackWebhookUrl,
32+
twitterWebhookUrl: twitterWebhookUrl,
2933
}
3034
}
3135

@@ -64,11 +68,9 @@ func (p *IncidentPoller) pollInner() error {
6468
p.logger.Info("found incidents without jobs started", zap.Int("count", len(incidents)))
6569

6670
jobArgs := make([]river.InsertManyParams, 0)
67-
for _, incident := range incidents {
68-
if p.slackWebhookUrl == "" {
69-
continue
70-
}
7171

72+
var incidentsToProcess = make([]api.Incident, 0)
73+
for _, incident := range incidents {
7274
// We only want to notify about incidents that have started in the last hour
7375
// Otherwise, we will be sending notifications for incidents that have already been resolved
7476
if incident.StartTime.Before(time.Now().Add(-1 * time.Hour)) {
@@ -78,12 +80,32 @@ func (p *IncidentPoller) pollInner() error {
7880
if incident.Impact == "maintenance" {
7981
continue
8082
}
83+
84+
incidentsToProcess = append(incidentsToProcess, incident)
85+
}
86+
87+
// Slack webhook notifications
88+
for _, incident := range incidentsToProcess {\
89+
if p.slackWebhookUrl == "" {
90+
continue
91+
}
8192
jobArgs = append(jobArgs, river.InsertManyParams{Args: slack_webhook.SlackWebhookArgs{
8293
Incident: incident,
8394
WebhookUrl: p.slackWebhookUrl,
8495
}})
8596
}
8697

98+
// Twitter post notifications
99+
for _, incident := range incidentsToProcess {
100+
if p.twitterWebhookUrl == "" {
101+
continue
102+
}
103+
jobArgs = append(jobArgs, river.InsertManyParams{Args: twitter_post.TwitterPostArgs{
104+
WebhookUrl: p.twitterWebhookUrl,
105+
Incident: incident,
106+
}})
107+
}
108+
87109
p.logger.Info("starting to insert jobs", zap.Int("count", len(jobArgs)))
88110
// Start the jobs for each incident and update the database
89111
if len(jobArgs) != 0 {

jobrunner/main.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ func main() {
2828

2929
err = riverclient.RunMigration(db.PgxPool)
3030

31-
client, err := riverclient.NewRiverClient(db.PgxPool, logger, http.DefaultClient, 100)
31+
client, err := riverclient.NewRiverClient(db, logger, http.DefaultClient, 100)
3232
if err != nil {
3333
panic(err)
3434
}
@@ -45,7 +45,7 @@ func main() {
4545
panic(err)
4646
}
4747

48-
incidentPoller := incidentpoller.NewIncidentPoller(db, logger, client, config.SlackWebhookUrl)
48+
incidentPoller := incidentpoller.NewIncidentPoller(db, logger, client, config.SlackWebhookUrl, config.TwitterWebhookUrl)
4949
incidentPoller.Start()
5050

5151
// Work forever

0 commit comments

Comments
 (0)