From 489c828eb8329ae5ee78fc9e8d9a106625cc212d Mon Sep 17 00:00:00 2001 From: Ethan M Lewis Date: Tue, 27 Aug 2024 21:15:20 -0700 Subject: [PATCH] GIthub Discussions and Discussion Comments Webhooks (#808) * feat: added github discussions and discussion comment webhooks * fix: remove additional logging * "fix: remove white space" Co-authored-by: Felipe Martin <812088+fmartingr@users.noreply.github.com> * "fix: correct spelling of discussion" Co-authored-by: Felipe Martin <812088+fmartingr@users.noreply.github.com> * fix: spelling of discussions Co-authored-by: Felipe Martin <812088+fmartingr@users.noreply.github.com> * Update server/plugin/webhook.go Co-authored-by: Felipe Martin <812088+fmartingr@users.noreply.github.com> * Update server/plugin/webhook.go Co-authored-by: Felipe Martin <812088+fmartingr@users.noreply.github.com> * Update server/plugin/webhook.go Co-authored-by: Felipe Martin <812088+fmartingr@users.noreply.github.com> * fix: correct spacing in template for discussion additons * fix: update webhook consts for lint error * update error for discussion Co-authored-by: Raghav Aggarwal * update error for webhook discussion Co-authored-by: Raghav Aggarwal * fix: update templates, remove duplicate check in webhook handler, and use const feature key * fix: typo in const name for featureDiscussion * fix typo Co-authored-by: Doug Lauder * fix: update spelling of const for feature check * fix: const spelling for feature discussions * fix: revert package-lock.json for e2e tests --------- Co-authored-by: Felipe Martin <812088+fmartingr@users.noreply.github.com> Co-authored-by: Raghav Aggarwal Co-authored-by: Doug Lauder --- Makefile | 2 +- server/plugin/command.go | 54 ++++++++++--------- server/plugin/subscriptions.go | 8 +++ server/plugin/template.go | 12 +++++ server/plugin/webhook.go | 98 ++++++++++++++++++++++++++++++++-- 5 files changed, 144 insertions(+), 30 deletions(-) diff --git a/Makefile b/Makefile index dfcd96a66..130d2433e 100644 --- a/Makefile +++ b/Makefile @@ -158,7 +158,7 @@ apply: ## Install go tools install-go-tools: @echo Installing go tools - $(GO) install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.51.1 + $(GO) install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.59.1 $(GO) install gotest.tools/gotestsum@v1.7.0 ## Runs eslint and golangci-lint diff --git a/server/plugin/command.go b/server/plugin/command.go index 14071d9db..6f632583d 100644 --- a/server/plugin/command.go +++ b/server/plugin/command.go @@ -15,18 +15,20 @@ import ( ) const ( - featureIssueCreation = "issue_creations" - featureIssues = "issues" - featurePulls = "pulls" - featurePullsMerged = "pulls_merged" - featurePullsCreated = "pulls_created" - featurePushes = "pushes" - featureCreates = "creates" - featureDeletes = "deletes" - featureIssueComments = "issue_comments" - featurePullReviews = "pull_reviews" - featureStars = "stars" - featureReleases = "releases" + featureIssueCreation = "issue_creations" + featureIssues = "issues" + featurePulls = "pulls" + featurePullsMerged = "pulls_merged" + featurePullsCreated = "pulls_created" + featurePushes = "pushes" + featureCreates = "creates" + featureDeletes = "deletes" + featureIssueComments = "issue_comments" + featurePullReviews = "pull_reviews" + featureStars = "stars" + featureReleases = "releases" + featureDiscussions = "discussions" + featureDiscussionComments = "discussion_comments" ) const ( @@ -34,18 +36,20 @@ const ( ) var validFeatures = map[string]bool{ - featureIssueCreation: true, - featureIssues: true, - featurePulls: true, - featurePullsMerged: true, - featurePullsCreated: true, - featurePushes: true, - featureCreates: true, - featureDeletes: true, - featureIssueComments: true, - featurePullReviews: true, - featureStars: true, - featureReleases: true, + featureIssueCreation: true, + featureIssues: true, + featurePulls: true, + featurePullsMerged: true, + featurePullsCreated: true, + featurePushes: true, + featureCreates: true, + featureDeletes: true, + featureIssueComments: true, + featurePullReviews: true, + featureStars: true, + featureReleases: true, + featureDiscussions: true, + featureDiscussionComments: true, } type Features string @@ -897,7 +901,7 @@ func getAutocompleteData(config *Configuration) *model.AutocompleteData { subscriptionsAdd := model.NewAutocompleteData("add", "[owner/repo] [features] [flags]", "Subscribe the current channel to receive notifications about opened pull requests and issues for an organization or repository. [features] and [flags] are optional arguments") subscriptionsAdd.AddTextArgument("Owner/repo to subscribe to", "[owner/repo]", "") - subscriptionsAdd.AddNamedTextArgument("features", "Comma-delimited list of one or more of: issues, pulls, pulls_merged, pulls_created, pushes, creates, deletes, issue_creations, issue_comments, pull_reviews, releases, label:\"\". Defaults to pulls,issues,creates,deletes", "", `/[^,-\s]+(,[^,-\s]+)*/`, false) + subscriptionsAdd.AddNamedTextArgument("features", "Comma-delimited list of one or more of: issues, pulls, pulls_merged, pulls_created, pushes, creates, deletes, issue_creations, issue_comments, pull_reviews, releases, discussions, discussion_comments, label:\"\". Defaults to pulls,issues,creates,deletes", "", `/[^,-\s]+(,[^,-\s]+)*/`, false) if config.GitHubOrg != "" { subscriptionsAdd.AddNamedStaticListArgument("exclude-org-member", "Events triggered by organization members will not be delivered (the organization config should be set, otherwise this flag has not effect)", false, []model.AutocompleteListItem{ diff --git a/server/plugin/subscriptions.go b/server/plugin/subscriptions.go index 3d13986ea..65b305484 100644 --- a/server/plugin/subscriptions.go +++ b/server/plugin/subscriptions.go @@ -127,6 +127,14 @@ func (s *Subscription) Release() bool { return strings.Contains(s.Features.String(), featureReleases) } +func (s *Subscription) Discussions() bool { + return strings.Contains(s.Features.String(), featureDiscussions) +} + +func (s *Subscription) DiscussionComments() bool { + return strings.Contains(s.Features.String(), featureDiscussionComments) +} + func (s *Subscription) Label() string { if !strings.Contains(s.Features.String(), "label:") { return "" diff --git a/server/plugin/template.go b/server/plugin/template.go index 753abc183..ae041dc94 100644 --- a/server/plugin/template.go +++ b/server/plugin/template.go @@ -414,6 +414,8 @@ Assignees: {{range $i, $el := .Assignees -}} {{- if $i}}, {{end}}{{template "use " * `pull_reviews` - includes pull request reviews\n" + " * `releases` - includes release created and deleted\n" + " * `label:` - limit pull request and issue events to only this label. Must include `pulls` or `issues` in feature list when using a label.\n" + + " * `discussions` - includes new discussions\n" + + " * `discussion_comments` - includes new discussion comments\n" + " * Defaults to `pulls,issues,creates,deletes`\n\n" + " * `--exclude-org-member` - events triggered by organization members will not be delivered (the GitHub organization config should be set, otherwise this flag has not effect)\n" + " * `--render-style` - notifications will be delivered in the specified style (for example, the body of a pull request will not be displayed). Supported values are `collapsed`, `skip-body` or `default` (same as omitting the flag).\n" + @@ -440,6 +442,16 @@ It now has **{{.GetRepo.GetStargazersCount}}** stars.`)) {{- if eq .GetAction "created" }} created a release {{template "release" .GetRelease}} {{- else if eq .GetAction "deleted" }} deleted a release {{template "release" .GetRelease}} {{- end -}}`)) + + template.Must(masterTemplate.New("newDiscussion").Funcs(funcMap).Parse(` +{{template "user" .GetSender}} started a new discussion [#{{.GetDiscussion.GetNumber}} {{.GetDiscussion.GetTitle}}]({{.GetDiscussion.GetHTMLURL}}) on {{template "repo" .GetRepo}} +`)) + + template.Must(masterTemplate.New("newDiscussionComment").Funcs(funcMap).Parse(` +{{template "repo" .GetRepo}} New comment by {{template "user" .GetSender}} on discussion [#{{.GetDiscussion.GetNumber}} {{.GetDiscussion.GetTitle}}]({{.GetDiscussion.GetHTMLURL}}): + +{{.GetComment.GetBody | trimBody | replaceAllGitHubUsernames}} +`)) } func registerGitHubToUsernameMappingCallback(callback func(string) string) { diff --git a/server/plugin/webhook.go b/server/plugin/webhook.go index 27366199d..f91962f45 100644 --- a/server/plugin/webhook.go +++ b/server/plugin/webhook.go @@ -36,9 +36,10 @@ const ( postPropGithubObjectID = "gh_object_id" postPropGithubObjectType = "gh_object_type" - githubObjectTypeIssue = "issue" - githubObjectTypeIssueComment = "issue_comment" - githubObjectTypePRReviewComment = "pr_review_comment" + githubObjectTypeIssue = "issue" + githubObjectTypeIssueComment = "issue_comment" + githubObjectTypePRReviewComment = "pr_review_comment" + githubObjectTypeDiscussionComment = "discussion_comment" ) // RenderConfig holds various configuration options to be used in a template @@ -185,7 +186,6 @@ func (wb *WebhookBroker) Close() { func (p *Plugin) handleWebhook(w http.ResponseWriter, r *http.Request) { config := p.getConfiguration() - body, err := io.ReadAll(r.Body) if err != nil { http.Error(w, "Bad request body", http.StatusBadRequest) @@ -287,6 +287,16 @@ func (p *Plugin) handleWebhook(w http.ResponseWriter, r *http.Request) { handler = func() { p.postReleaseEvent(event) } + case *github.DiscussionEvent: + repo = event.GetRepo() + handler = func() { + p.postDiscussionEvent(event) + } + case *github.DiscussionCommentEvent: + repo = event.GetRepo() + handler = func() { + p.postDiscussionCommentEvent(event) + } } if handler == nil { @@ -1383,3 +1393,83 @@ func (p *Plugin) postReleaseEvent(event *github.ReleaseEvent) { } } } + +func (p *Plugin) postDiscussionEvent(event *github.DiscussionEvent) { + repo := event.GetRepo() + + subs := p.GetSubscribedChannelsForRepository(repo) + if len(subs) == 0 { + return + } + + newDiscussionMessage, err := renderTemplate("newDiscussion", event) + if err != nil { + p.client.Log.Warn("Failed to render template", "error", err.Error()) + return + } + + for _, sub := range subs { + if !sub.Discussions() { + continue + } + + if p.excludeConfigOrgMember(event.GetSender(), sub) { + continue + } + + post := p.makeBotPost(newDiscussionMessage, "custom_git_discussion") + + repoName := strings.ToLower(repo.GetFullName()) + discussionNumber := event.GetDiscussion().GetNumber() + + post.AddProp(postPropGithubRepo, repoName) + post.AddProp(postPropGithubObjectID, discussionNumber) + post.AddProp(postPropGithubObjectType, "discussion") + post.ChannelId = sub.ChannelID + if err = p.client.Post.CreatePost(post); err != nil { + p.client.Log.Warn("Error creating discussion notification post", "Post", post, "Error", err.Error()) + } + } +} + +func (p *Plugin) postDiscussionCommentEvent(event *github.DiscussionCommentEvent) { + repo := event.GetRepo() + + subs := p.GetSubscribedChannelsForRepository(repo) + if len(subs) == 0 { + return + } + + if event.GetAction() != actionCreated { + return + } + + newDiscussionCommentMessage, err := renderTemplate("newDiscussionComment", event) + if err != nil { + p.client.Log.Warn("Failed to render template", "error", err.Error()) + return + } + for _, sub := range subs { + if !sub.DiscussionComments() { + continue + } + + if p.excludeConfigOrgMember(event.GetSender(), sub) { + continue + } + + post := p.makeBotPost(newDiscussionCommentMessage, "custom_git_dis_comment") + + repoName := strings.ToLower(repo.GetFullName()) + commentID := event.GetComment().GetID() + + post.AddProp(postPropGithubRepo, repoName) + post.AddProp(postPropGithubObjectID, commentID) + post.AddProp(postPropGithubObjectType, githubObjectTypeDiscussionComment) + + post.ChannelId = sub.ChannelID + if err = p.client.Post.CreatePost(post); err != nil { + p.client.Log.Warn("Error creating discussion comment post", "Post", post, "Error", err.Error()) + } + } +}