From 3c3f13528d83417c8819d69548dc58e5bb031a49 Mon Sep 17 00:00:00 2001 From: topi314 Date: Wed, 5 Jun 2024 23:12:26 +0200 Subject: [PATCH] add github release discord pinging --- config.example.yml | 9 +++ go.mod | 12 ++-- go.sum | 24 ++++---- lavalinkbot/bot.go | 2 + lavalinkbot/config.go | 33 ++++++++++ main.go | 13 ++++ routes/github_webhook.go | 129 +++++++++++++++++++++++++++++++++++++++ 7 files changed, 204 insertions(+), 18 deletions(-) create mode 100644 routes/github_webhook.go diff --git a/config.example.yml b/config.example.yml index bacf064..0590fe0 100644 --- a/config.example.yml +++ b/config.example.yml @@ -6,6 +6,15 @@ bot: token: ... guild_ids: [ ... ] +github: + server_addr: ":8080" + webhook_secret: "" + releases: + user/repo: + webhook_id: 1248018508823138376 + webhook_token: "..." + ping_role: 123456789012345678 + nodes: - name: node1 address: localhost:2333 diff --git a/go.mod b/go.mod index cf3f151..8d8ca0b 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/lavalink-devs/lavalink-bot go 1.22 require ( - github.com/disgoorg/disgo v0.18.5 + github.com/disgoorg/disgo v0.18.7 github.com/disgoorg/disgolink/v3 v3.0.1-0.20240311001109-56f250c13235 github.com/disgoorg/json v1.1.0 github.com/disgoorg/lavalyrics-plugin v0.0.0-20240428194130-71a50d68e826 @@ -27,9 +27,9 @@ require ( github.com/magefile/mage v1.15.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/sasha-s/go-csync v0.0.0-20240107134140-fcbab37b09ad // indirect - golang.org/x/crypto v0.23.0 // indirect - golang.org/x/net v0.25.0 // indirect - golang.org/x/oauth2 v0.20.0 // indirect - golang.org/x/sys v0.20.0 // indirect - golang.org/x/text v0.15.0 // indirect + golang.org/x/crypto v0.24.0 // indirect + golang.org/x/net v0.26.0 // indirect + golang.org/x/oauth2 v0.21.0 // indirect + golang.org/x/sys v0.21.0 // indirect + golang.org/x/text v0.16.0 // indirect ) diff --git a/go.sum b/go.sum index eee95a3..fab496f 100644 --- a/go.sum +++ b/go.sum @@ -7,8 +7,8 @@ github.com/cloudflare/circl v1.3.8/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/disgoorg/disgo v0.18.5 h1:T4X9ARKJFwCon4xkw4Dg+SjGpFo7usQ7QCCX2+snGXQ= -github.com/disgoorg/disgo v0.18.5/go.mod h1:gkl6DBdbKUvmOOJayWPSvS52KPN/8uJGJ2f13gCEB1o= +github.com/disgoorg/disgo v0.18.7 h1:Xg5eiOdSo+wR3CDMIPh9Vmykdkwk/rdcs00vhr2U6m0= +github.com/disgoorg/disgo v0.18.7/go.mod h1:gkl6DBdbKUvmOOJayWPSvS52KPN/8uJGJ2f13gCEB1o= github.com/disgoorg/disgolink/v3 v3.0.1-0.20240311001109-56f250c13235 h1:Mtqh7yLVBW6uKWAbrqKZv7DgXpoWBfPctok8sABYIiY= github.com/disgoorg/disgolink/v3 v3.0.1-0.20240311001109-56f250c13235/go.mod h1:YIwjIteZcjfI7HYZWH241iRI7RjTLoN51HLDOUHVSFI= github.com/disgoorg/json v1.1.0 h1:7xigHvomlVA9PQw9bMGO02PHGJJPqvX5AnwlYg/Tnys= @@ -59,8 +59,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= -golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -69,10 +69,10 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= -golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo= -golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= +golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -86,8 +86,8 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= @@ -100,8 +100,8 @@ golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= diff --git a/lavalinkbot/bot.go b/lavalinkbot/bot.go index 2c2a469..ee07135 100644 --- a/lavalinkbot/bot.go +++ b/lavalinkbot/bot.go @@ -8,6 +8,7 @@ import ( "time" "github.com/disgoorg/disgo/bot" + "github.com/disgoorg/disgo/webhook" "github.com/disgoorg/disgolink/v3/disgolink" "github.com/google/go-github/v52/github" "github.com/topi314/tint" @@ -22,6 +23,7 @@ type Bot struct { Lavalink disgolink.Client GitHub *github.Client MusicQueue *PlayerManager + Webhooks map[string]webhook.Client } func (b *Bot) Start() error { diff --git a/lavalinkbot/config.go b/lavalinkbot/config.go index 4cebb6f..766bf97 100644 --- a/lavalinkbot/config.go +++ b/lavalinkbot/config.go @@ -35,6 +35,7 @@ func ReadConfig(path string) (Config, error) { type Config struct { Log LogConfig `yaml:"log"` Bot BotConfig `yaml:"bot"` + GitHub GitHubConfig `yaml:"github"` Nodes NodeConfigs `yaml:"nodes"` Plugins PluginConfigs `yaml:"plugins"` } @@ -76,6 +77,38 @@ func (c BotConfig) String() string { ) } +type GitHubConfig struct { + ServerAddr string `yaml:"server_addr"` + WebhookSecret string `yaml:"webhook_secret"` + Releases map[string]GithubReleaseConfig `yaml:"github_releases"` +} + +func (c GitHubConfig) String() string { + var s string + for repo, cfg := range c.Releases { + s += fmt.Sprintf("\n %s: %s", repo, cfg) + } + return fmt.Sprintf("\n ServerAddr: %s\n WebhookSecret: %s\n Releases: %s", + c.ServerAddr, + c.WebhookSecret, + s, + ) +} + +type GithubReleaseConfig struct { + WebhookID snowflake.ID `yaml:"webhook_id"` + WebhookToken string `yaml:"webhook_token"` + PingRole snowflake.ID `yaml:"ping_role"` +} + +func (c GithubReleaseConfig) String() string { + return fmt.Sprintf("\n WebhookID: %s\n WebhookToken: %s\n PingRole: %s", + c.WebhookID, + c.WebhookToken, + c.PingRole, + ) +} + type NodeConfig struct { Name string `yaml:"name"` Address string `yaml:"address"` diff --git a/main.go b/main.go index 61d4d5f..33f3da8 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,7 @@ package main import ( + "errors" "flag" "log/slog" "net/http" @@ -15,6 +16,7 @@ import ( "github.com/disgoorg/disgo/gateway" "github.com/disgoorg/disgo/handler" "github.com/disgoorg/disgo/handler/middleware" + "github.com/disgoorg/disgo/webhook" "github.com/disgoorg/disgolink/v3/disgolink" "github.com/disgoorg/sponsorblock-plugin" "github.com/google/go-github/v52/github" @@ -22,6 +24,7 @@ import ( "github.com/lavalink-devs/lavalink-bot/handlers" "github.com/lavalink-devs/lavalink-bot/internal/maven" "github.com/lavalink-devs/lavalink-bot/lavalinkbot" + "github.com/lavalink-devs/lavalink-bot/routes" "github.com/mattn/go-colorable" "github.com/topi314/tint" ) @@ -46,6 +49,7 @@ func main() { Timeout: 10 * time.Second, }), MusicQueue: lavalinkbot.NewPlayerManager(), + Webhooks: map[string]webhook.Client{}, } cmds := &commands.Commands{Bot: b} @@ -92,6 +96,15 @@ func main() { hdlr := &handlers.Handlers{Bot: b} + mux := http.NewServeMux() + mux.Handle("POST /github/webhook", routes.HandleGithubWebhook(b)) + + go func() { + if err := http.ListenAndServe(cfg.GitHub.ServerAddr, mux); err != nil && !errors.Is(err, http.ErrServerClosed) { + slog.Error("failed to start github webhook server", tint.Err(err)) + } + }() + if b.Client, err = disgo.New(cfg.Bot.Token, bot.WithGatewayConfigOpts( gateway.WithIntents(gateway.IntentGuilds, gateway.IntentGuildVoiceStates), diff --git a/routes/github_webhook.go b/routes/github_webhook.go new file mode 100644 index 0000000..98a6e9c --- /dev/null +++ b/routes/github_webhook.go @@ -0,0 +1,129 @@ +package routes + +import ( + "errors" + "fmt" + "log/slog" + "net/http" + "regexp" + "strings" + + "github.com/disgoorg/disgo/discord" + "github.com/disgoorg/disgo/webhook" + "github.com/google/go-github/v52/github" + "github.com/lavalink-devs/lavalink-bot/lavalinkbot" + "github.com/topi314/tint" +) + +var ( + markdownHeaderRegex = regexp.MustCompile(`[ \t]*#+[ \t]+([^\r\n]+)`) + markdownBulletRegex = regexp.MustCompile(`([ \t]*)[*|-][ \t]+([^\r\n]+)`) + markdownCheckBoxCheckedRegex = regexp.MustCompile(`([ \t]*)[*|-][ \t]{0,4}\[x][ \t]+([^\r\n]+)`) + markdownCheckBoxUncheckedRegex = regexp.MustCompile(`([ \t]*)[*|-][ \t]{0,4}\[ ][ \t]+([^\r\n]+)`) + prURLRegex = regexp.MustCompile(`https?://github\.com/(\w+/\w+)/pull/(\d+)`) + commitURLRegex = regexp.MustCompile(`https?://github\.com/\w+/\w+/commit/([a-f\d]{7})[a-f\d]+`) + mentionRegex = regexp.MustCompile(`@(\w+)`) +) + +func HandleGithubWebhook(b *lavalinkbot.Bot) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + payload, err := github.ValidatePayload(r, []byte(b.Cfg.GitHub.WebhookSecret)) + if err != nil { + slog.Error("Failed to validate payload", tint.Err(err)) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + event, err := github.ParseWebHook(github.WebHookType(r), payload) + if err != nil { + slog.Error("Failed to parse webhook", tint.Err(err)) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + switch e := event.(type) { + case *github.ReleaseEvent: + err = processReleaseEvent(b, e) + } + if err != nil { + slog.Error("Failed to process webhook", tint.Err(err)) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } +} + +func processReleaseEvent(b *lavalinkbot.Bot, e *github.ReleaseEvent) error { + if e.GetAction() != "published" { + return nil + } + + repo := e.GetRepo().GetName() + fullName := e.GetRepo().GetFullName() + + cfg, ok := b.Cfg.GitHub.Releases[fullName] + if !ok { + return errors.New("no config found for this repo") + } + + webhookClient, ok := b.Webhooks[fullName] + if !ok { + webhookClient = webhook.New(cfg.WebhookID, cfg.WebhookToken) + b.Webhooks[fullName] = webhookClient + } + + message := parseMarkdown(e.GetRelease().GetBody()) + if len(message) > 1024 { + message = substr(message, 0, 1024) + if index := strings.LastIndex(message, "\n"); index != -1 { + message = message[:index] + } + message += "\n…" + } + + msg, err := webhookClient.CreateMessage(discord.NewWebhookMessageCreateBuilder(). + SetContent(discord.RoleMention(cfg.PingRole)). + SetEmbeds(discord.NewEmbedBuilder(). + SetAuthor( + fmt.Sprintf("%s version %s has been released", repo, e.Release.GetTagName()), + e.GetRelease().GetHTMLURL(), + e.GetRepo().GetOwner().GetAvatarURL(), + ). + SetDescription(message). + SetColor(0xff624a). + SetFooter("Release by "+e.GetRelease().GetAuthor().GetLogin(), e.GetRelease().GetAuthor().GetAvatarURL()). + SetTimestamp(e.GetRelease().GetCreatedAt().Time). + Build(), + ). + SetAvatarURL(e.GetRepo().GetOwner().GetAvatarURL()). + Build(), + ) + if err != nil { + return err + } + _, err = b.Client.Rest().CrosspostMessage(msg.ChannelID, msg.ID) + return err +} + +func substr(input string, start int, length int) string { + asRunes := []rune(input) + + if start >= len(asRunes) { + return "" + } + + if start+length > len(asRunes) { + length = len(asRunes) - start + } + + return string(asRunes[start : start+length]) +} + +func parseMarkdown(text string) string { + text = markdownCheckBoxCheckedRegex.ReplaceAllString(text, "$1:ballot_box_with_check: $2") + text = markdownCheckBoxUncheckedRegex.ReplaceAllString(text, "$1:white_square_button: $2") + text = markdownHeaderRegex.ReplaceAllString(text, "**$1**") + text = markdownBulletRegex.ReplaceAllString(text, "$1• $2") + text = prURLRegex.ReplaceAllString(text, "[$1#$2]($0)") + text = commitURLRegex.ReplaceAllString(text, "[`$1`]($0)") + text = mentionRegex.ReplaceAllString(text, "[@$1](https://github.com/$1)") + return text +}