mirror of
https://github.com/harness/drone.git
synced 2025-05-03 20:21:25 +08:00
feat: process notification and send emails (#803)
This commit is contained in:
parent
6915034816
commit
02aac02993
96
app/services/notification/branch_updated.go
Normal file
96
app/services/notification/branch_updated.go
Normal file
@ -0,0 +1,96 @@
|
||||
// Copyright 2023 Harness, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package notification
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
pullreqevents "github.com/harness/gitness/app/events/pullreq"
|
||||
"github.com/harness/gitness/events"
|
||||
"github.com/harness/gitness/types"
|
||||
)
|
||||
|
||||
type PullReqBranchUpdatedPayload struct {
|
||||
Base *BasePullReqPayload
|
||||
Committer *types.PrincipalInfo
|
||||
NewSHA string
|
||||
}
|
||||
|
||||
func (s *Service) notifyPullReqBranchUpdated(
|
||||
ctx context.Context,
|
||||
event *events.Event[*pullreqevents.BranchUpdatedPayload],
|
||||
) error {
|
||||
payload, reviewers, err := s.processPullReqBranchUpdatedEvent(ctx, event)
|
||||
if err != nil {
|
||||
return fmt.Errorf(
|
||||
"failed to process %s event for pullReqID %d: %w",
|
||||
pullreqevents.BranchUpdatedEvent,
|
||||
event.Payload.PullReqID,
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
if len(reviewers) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
err = s.notificationClient.SendPullReqBranchUpdated(ctx, reviewers, payload)
|
||||
if err != nil {
|
||||
return fmt.Errorf(
|
||||
"failed to send notification for event %s for pullReqID %d: %w",
|
||||
pullreqevents.BranchUpdatedEvent,
|
||||
event.Payload.PullReqID,
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) processPullReqBranchUpdatedEvent(
|
||||
ctx context.Context,
|
||||
event *events.Event[*pullreqevents.BranchUpdatedPayload],
|
||||
) (*PullReqBranchUpdatedPayload, []*types.PrincipalInfo, error) {
|
||||
base, err := s.getBasePayload(ctx, event.Payload.Base)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to get base payload: %w", err)
|
||||
}
|
||||
|
||||
committer, err := s.principalInfoCache.Get(ctx, event.Payload.PrincipalID)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to get principal info for %d: %w", event.Payload.PrincipalID, err)
|
||||
}
|
||||
|
||||
reviewers, err := s.pullReqReviewersStore.List(ctx, event.Payload.PullReqID)
|
||||
if err != nil {
|
||||
return nil, nil,
|
||||
fmt.Errorf("failed to get reviewers for pull request %d: %w", event.Payload.PullReqID, err)
|
||||
}
|
||||
|
||||
reviewerPrincipals := make([]*types.PrincipalInfo, len(reviewers))
|
||||
for i, reviewer := range reviewers {
|
||||
reviewerPrincipals[i], err = s.principalInfoCache.Get(ctx, reviewer.PrincipalID)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to get principal info for %d: %w", reviewer.PrincipalID, err)
|
||||
}
|
||||
}
|
||||
|
||||
return &PullReqBranchUpdatedPayload{
|
||||
Base: base,
|
||||
NewSHA: event.Payload.NewSHA,
|
||||
Committer: committer,
|
||||
}, reviewerPrincipals, nil
|
||||
}
|
33
app/services/notification/client_interface.go
Normal file
33
app/services/notification/client_interface.go
Normal file
@ -0,0 +1,33 @@
|
||||
// Copyright 2023 Harness, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package notification
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/harness/gitness/types"
|
||||
)
|
||||
|
||||
// Client is an interface for sending notifications, such as emails, Slack messages etc.
|
||||
// It is implemented by MailClient and in future we can have other implementations for other channels like Slack etc.
|
||||
type Client interface {
|
||||
SendCommentCreated(ctx context.Context, recipients []*types.PrincipalInfo, payload *CommentCreatedPayload) error
|
||||
SendReviewerAdded(ctx context.Context, recipients []*types.PrincipalInfo, payload *ReviewerAddedPayload) error
|
||||
SendPullReqBranchUpdated(
|
||||
ctx context.Context,
|
||||
recipients []*types.PrincipalInfo,
|
||||
payload *PullReqBranchUpdatedPayload,
|
||||
) error
|
||||
}
|
85
app/services/notification/comment_created.go
Normal file
85
app/services/notification/comment_created.go
Normal file
@ -0,0 +1,85 @@
|
||||
// Copyright 2023 Harness, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package notification
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
pullreqevents "github.com/harness/gitness/app/events/pullreq"
|
||||
"github.com/harness/gitness/events"
|
||||
"github.com/harness/gitness/types"
|
||||
)
|
||||
|
||||
type CommentCreatedPayload struct {
|
||||
Base *BasePullReqPayload
|
||||
Commenter *types.PrincipalInfo
|
||||
Text string
|
||||
}
|
||||
|
||||
func (s *Service) notifyCommentCreated(
|
||||
ctx context.Context,
|
||||
event *events.Event[*pullreqevents.CommentCreatedPayload],
|
||||
) error {
|
||||
payload, recipients, err := s.processCommentCreatedEvent(ctx, event)
|
||||
if err != nil {
|
||||
return fmt.Errorf(
|
||||
"failed to process %s event for pullReqID %d: %w",
|
||||
pullreqevents.CommentCreatedEvent,
|
||||
event.Payload.PullReqID,
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
err = s.notificationClient.SendCommentCreated(ctx, recipients, payload)
|
||||
if err != nil {
|
||||
return fmt.Errorf(
|
||||
"failed to send notification for event %s for pullReqID %d: %w",
|
||||
pullreqevents.CommentCreatedEvent,
|
||||
event.Payload.PullReqID,
|
||||
err,
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) processCommentCreatedEvent(
|
||||
ctx context.Context,
|
||||
event *events.Event[*pullreqevents.CommentCreatedPayload],
|
||||
) (*CommentCreatedPayload, []*types.PrincipalInfo, error) {
|
||||
base, err := s.getBasePayload(ctx, event.Payload.Base)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to get base payload: %w", err)
|
||||
}
|
||||
|
||||
activity, err := s.pullReqActivityStore.Find(ctx, event.Payload.ActivityID)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to fetch activity from pullReqActivityStore: %w", err)
|
||||
}
|
||||
|
||||
commenter, err := s.principalInfoView.Find(ctx, activity.CreatedBy)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to fetch commenter from principalInfoView: %w", err)
|
||||
}
|
||||
recipients := []*types.PrincipalInfo{
|
||||
base.Author,
|
||||
}
|
||||
|
||||
return &CommentCreatedPayload{
|
||||
Base: base,
|
||||
Commenter: commenter,
|
||||
Text: activity.Text,
|
||||
}, recipients, nil
|
||||
}
|
149
app/services/notification/mail_client.go
Normal file
149
app/services/notification/mail_client.go
Normal file
@ -0,0 +1,149 @@
|
||||
// Copyright 2023 Harness, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package notification
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
pullreqevents "github.com/harness/gitness/app/events/pullreq"
|
||||
"github.com/harness/gitness/app/services/notification/mailer"
|
||||
"github.com/harness/gitness/types"
|
||||
)
|
||||
|
||||
const (
|
||||
TemplateReviewerAdded = "reviewer_added.html"
|
||||
TemplateCommentCreated = "comment_created.html"
|
||||
TemplatePullReqBranchUpdated = "pullreq_branch_updated.html"
|
||||
)
|
||||
|
||||
type MailClient struct {
|
||||
mailer.Mailer
|
||||
}
|
||||
|
||||
func NewMailClient(mailer mailer.Mailer) MailClient {
|
||||
return MailClient{
|
||||
Mailer: mailer,
|
||||
}
|
||||
}
|
||||
|
||||
func (m MailClient) SendCommentCreated(
|
||||
ctx context.Context,
|
||||
recipients []*types.PrincipalInfo,
|
||||
payload *CommentCreatedPayload,
|
||||
) error {
|
||||
mailPayload, err := GenerateEmailFromPayload(
|
||||
TemplateCommentCreated,
|
||||
recipients,
|
||||
payload.Base,
|
||||
payload,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate mail requests after processing %s event: %w",
|
||||
pullreqevents.CommentCreatedEvent, err)
|
||||
}
|
||||
|
||||
return m.Mailer.Send(ctx, *mailPayload)
|
||||
}
|
||||
|
||||
func (m MailClient) SendReviewerAdded(
|
||||
ctx context.Context,
|
||||
recipients []*types.PrincipalInfo,
|
||||
payload *ReviewerAddedPayload,
|
||||
) error {
|
||||
reviewerAddedMail, err := GenerateEmailFromPayload(
|
||||
TemplateReviewerAdded,
|
||||
recipients,
|
||||
payload.Base,
|
||||
payload,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate mail requests after processing %s event: %w",
|
||||
pullreqevents.ReviewerAddedEvent, err)
|
||||
}
|
||||
|
||||
return m.Mailer.Send(ctx, *reviewerAddedMail)
|
||||
}
|
||||
|
||||
func (m MailClient) SendPullReqBranchUpdated(
|
||||
ctx context.Context,
|
||||
recipients []*types.PrincipalInfo,
|
||||
payload *PullReqBranchUpdatedPayload,
|
||||
) error {
|
||||
mailPayload, err := GenerateEmailFromPayload(
|
||||
TemplatePullReqBranchUpdated,
|
||||
recipients,
|
||||
payload.Base,
|
||||
payload,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate mail requests after processing %s event: %w",
|
||||
pullreqevents.BranchUpdatedEvent, err)
|
||||
}
|
||||
|
||||
return m.Mailer.Send(ctx, *mailPayload)
|
||||
}
|
||||
|
||||
func GetSubjectPullRequest(
|
||||
repoUID string,
|
||||
prNum int64,
|
||||
prTitle string,
|
||||
) string {
|
||||
return fmt.Sprintf(subjectPullReqEvent, repoUID, prTitle, prNum)
|
||||
}
|
||||
|
||||
func GetHTMLBody(templateName string, data interface{}) ([]byte, error) {
|
||||
tmpl := htmlTemplates[templateName]
|
||||
tmplOutput := bytes.Buffer{}
|
||||
err := tmpl.Execute(&tmplOutput, data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to execute template %s", templateName)
|
||||
}
|
||||
|
||||
return tmplOutput.Bytes(), nil
|
||||
}
|
||||
|
||||
func GenerateEmailFromPayload(
|
||||
templateName string,
|
||||
recipients []*types.PrincipalInfo,
|
||||
base *BasePullReqPayload,
|
||||
payload interface{},
|
||||
) (*mailer.Payload, error) {
|
||||
subject := GetSubjectPullRequest(base.Repo.UID, base.PullReq.Number,
|
||||
base.PullReq.Title)
|
||||
|
||||
body, err := GetHTMLBody(templateName, payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var mail mailer.Payload
|
||||
mail.Body = string(body)
|
||||
mail.Subject = subject
|
||||
mail.RepoRef = base.Repo.Path
|
||||
|
||||
recipientEmails := RetrieveEmailsFromPrincipals(recipients)
|
||||
mail.ToRecipients = recipientEmails
|
||||
return &mail, nil
|
||||
}
|
||||
|
||||
func RetrieveEmailsFromPrincipals(principals []*types.PrincipalInfo) []string {
|
||||
emails := make([]string, len(principals))
|
||||
for i, principal := range principals {
|
||||
emails[i] = principal.Email
|
||||
}
|
||||
return emails
|
||||
}
|
@ -15,12 +15,13 @@
|
||||
package mailer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
|
||||
gomail "gopkg.in/mail.v2"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
type GoMailClient struct {
|
||||
dialer *gomail.Dialer
|
||||
fromMail string
|
||||
}
|
||||
@ -32,18 +33,18 @@ func NewMailClient(
|
||||
fromMail string,
|
||||
password string,
|
||||
insecure bool,
|
||||
) *Service {
|
||||
) GoMailClient {
|
||||
d := gomail.NewDialer(host, port, username, password)
|
||||
d.TLSConfig = &tls.Config{InsecureSkipVerify: insecure} // #nosec G402 (insecure TLS configuration)
|
||||
|
||||
return &Service{
|
||||
return GoMailClient{
|
||||
dialer: d,
|
||||
fromMail: fromMail,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Service) SendMail(mailRequest *MailRequest) error {
|
||||
mail := mailRequest.ToGoMail()
|
||||
func (c GoMailClient) Send(_ context.Context, mailPayload Payload) error {
|
||||
mail := ToGoMail(mailPayload)
|
||||
mail.SetHeader("From", c.fromMail)
|
||||
return c.dialer.DialAndSend(mail)
|
||||
}
|
@ -14,21 +14,34 @@
|
||||
|
||||
package mailer
|
||||
|
||||
import gomail "gopkg.in/mail.v2"
|
||||
import (
|
||||
"context"
|
||||
|
||||
type MailRequest struct {
|
||||
CCRecipients []string `json:"cc_recipients"`
|
||||
ToRecipients []string `json:"to_recipients"`
|
||||
Subject string `json:"subject"`
|
||||
Body string `json:"body"`
|
||||
ContentType string `json:"content_type"`
|
||||
gomail "gopkg.in/mail.v2"
|
||||
)
|
||||
|
||||
const (
|
||||
mailContentType = "text/html"
|
||||
)
|
||||
|
||||
type Mailer interface {
|
||||
Send(ctx context.Context, mailPayload Payload) error
|
||||
}
|
||||
|
||||
func (dto *MailRequest) ToGoMail() *gomail.Message {
|
||||
type Payload struct {
|
||||
CCRecipients []string
|
||||
ToRecipients []string
|
||||
Subject string
|
||||
Body string
|
||||
ContentType string
|
||||
RepoRef string
|
||||
}
|
||||
|
||||
func ToGoMail(dto Payload) *gomail.Message {
|
||||
mail := gomail.NewMessage()
|
||||
mail.SetHeader("To", dto.ToRecipients...)
|
||||
mail.SetHeader("Cc", dto.CCRecipients...)
|
||||
mail.SetHeader("Subject", dto.Subject)
|
||||
mail.SetBody(dto.ContentType, dto.Body)
|
||||
mail.SetBody(mailContentType, dto.Body)
|
||||
return mail
|
||||
}
|
@ -21,11 +21,11 @@ import (
|
||||
)
|
||||
|
||||
var WireSet = wire.NewSet(
|
||||
ProvideMailService,
|
||||
ProvideMailClient,
|
||||
)
|
||||
|
||||
func ProvideMailService(config *types.Config) *Service {
|
||||
mailSvc := NewMailClient(
|
||||
func ProvideMailClient(config *types.Config) Mailer {
|
||||
return NewMailClient(
|
||||
config.SMTP.Host,
|
||||
config.SMTP.Port,
|
||||
config.SMTP.Username,
|
||||
@ -33,5 +33,4 @@ func ProvideMailService(config *types.Config) *Service {
|
||||
config.SMTP.Password,
|
||||
config.SMTP.Insecure, // #nosec G402 (insecure skipVerify configuration)
|
||||
)
|
||||
return mailSvc
|
||||
}
|
81
app/services/notification/reviewer_added.go
Normal file
81
app/services/notification/reviewer_added.go
Normal file
@ -0,0 +1,81 @@
|
||||
// Copyright 2023 Harness, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package notification
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
pullreqevents "github.com/harness/gitness/app/events/pullreq"
|
||||
"github.com/harness/gitness/events"
|
||||
"github.com/harness/gitness/types"
|
||||
)
|
||||
|
||||
type ReviewerAddedPayload struct {
|
||||
Base *BasePullReqPayload
|
||||
Reviewer *types.PrincipalInfo
|
||||
}
|
||||
|
||||
func (s *Service) notifyReviewerAdded(
|
||||
ctx context.Context,
|
||||
event *events.Event[*pullreqevents.ReviewerAddedPayload],
|
||||
) error {
|
||||
payload, err := s.processReviewerAddedEvent(ctx, event)
|
||||
if err != nil {
|
||||
return fmt.Errorf(
|
||||
"failed to process %s event for pullReqID %d: %w",
|
||||
pullreqevents.ReviewerAddedEvent,
|
||||
event.Payload.PullReqID,
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
// Send notification to author and reviewer
|
||||
recipients := []*types.PrincipalInfo{
|
||||
payload.Base.Author,
|
||||
payload.Reviewer,
|
||||
}
|
||||
err = s.notificationClient.SendReviewerAdded(ctx, recipients, payload)
|
||||
if err != nil {
|
||||
return fmt.Errorf(
|
||||
"failed to send notification for event %s for pullReqID %d: %w",
|
||||
pullreqevents.ReviewerAddedEvent,
|
||||
event.Payload.PullReqID,
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) processReviewerAddedEvent(
|
||||
ctx context.Context,
|
||||
event *events.Event[*pullreqevents.ReviewerAddedPayload],
|
||||
) (*ReviewerAddedPayload, error) {
|
||||
base, err := s.getBasePayload(ctx, event.Payload.Base)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get base payload: %w", err)
|
||||
}
|
||||
|
||||
reviewerPrincipal, err := s.principalInfoCache.Get(ctx, event.Payload.ReviewerID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get reviewer from principalInfoCache: %w", err)
|
||||
}
|
||||
|
||||
return &ReviewerAddedPayload{
|
||||
Base: base,
|
||||
Reviewer: reviewerPrincipal,
|
||||
}, nil
|
||||
}
|
179
app/services/notification/service.go
Normal file
179
app/services/notification/service.go
Normal file
@ -0,0 +1,179 @@
|
||||
// Copyright 2023 Harness, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package notification
|
||||
|
||||
import (
|
||||
"context"
|
||||
"embed"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io/fs"
|
||||
"path"
|
||||
|
||||
pullreqevents "github.com/harness/gitness/app/events/pullreq"
|
||||
"github.com/harness/gitness/app/store"
|
||||
"github.com/harness/gitness/app/url"
|
||||
"github.com/harness/gitness/events"
|
||||
"github.com/harness/gitness/stream"
|
||||
"github.com/harness/gitness/types"
|
||||
)
|
||||
|
||||
const (
|
||||
eventReaderGroupName = "gitness:notification"
|
||||
templatesDir = "templates"
|
||||
subjectPullReqEvent = "[%s] %s (PR #%d)"
|
||||
)
|
||||
|
||||
var (
|
||||
//go:embed templates/*
|
||||
files embed.FS
|
||||
htmlTemplates map[string]*template.Template
|
||||
)
|
||||
|
||||
func init() {
|
||||
err := LoadTemplates()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func LoadTemplates() error {
|
||||
htmlTemplates = make(map[string]*template.Template)
|
||||
tmplFiles, err := fs.ReadDir(files, templatesDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, tmpl := range tmplFiles {
|
||||
if tmpl.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
pt, err := template.ParseFS(files, path.Join(templatesDir, tmpl.Name()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
htmlTemplates[tmpl.Name()] = pt
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type BasePullReqPayload struct {
|
||||
Repo *types.Repository
|
||||
PullReq *types.PullReq
|
||||
Author *types.PrincipalInfo
|
||||
PullReqURL string
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
EventReaderName string
|
||||
Concurrency int
|
||||
MaxRetries int
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
config Config
|
||||
notificationClient Client
|
||||
prReaderFactory *events.ReaderFactory[*pullreqevents.Reader]
|
||||
pullReqStore store.PullReqStore
|
||||
repoStore store.RepoStore
|
||||
principalInfoView store.PrincipalInfoView
|
||||
principalInfoCache store.PrincipalInfoCache
|
||||
pullReqReviewersStore store.PullReqReviewerStore
|
||||
pullReqActivityStore store.PullReqActivityStore
|
||||
spacePathStore store.SpacePathStore
|
||||
urlProvider url.Provider
|
||||
}
|
||||
|
||||
func NewService(
|
||||
ctx context.Context,
|
||||
config Config,
|
||||
notificationClient Client,
|
||||
prReaderFactory *events.ReaderFactory[*pullreqevents.Reader],
|
||||
pullReqStore store.PullReqStore,
|
||||
repoStore store.RepoStore,
|
||||
principalInfoView store.PrincipalInfoView,
|
||||
principalInfoCache store.PrincipalInfoCache,
|
||||
pullReqReviewersStore store.PullReqReviewerStore,
|
||||
pullReqActivityStore store.PullReqActivityStore,
|
||||
spacePathStore store.SpacePathStore,
|
||||
urlProvider url.Provider,
|
||||
) (*Service, error) {
|
||||
service := &Service{
|
||||
config: config,
|
||||
notificationClient: notificationClient,
|
||||
prReaderFactory: prReaderFactory,
|
||||
pullReqStore: pullReqStore,
|
||||
repoStore: repoStore,
|
||||
principalInfoView: principalInfoView,
|
||||
principalInfoCache: principalInfoCache,
|
||||
pullReqReviewersStore: pullReqReviewersStore,
|
||||
pullReqActivityStore: pullReqActivityStore,
|
||||
spacePathStore: spacePathStore,
|
||||
urlProvider: urlProvider,
|
||||
}
|
||||
|
||||
_, err := service.prReaderFactory.Launch(
|
||||
ctx,
|
||||
eventReaderGroupName,
|
||||
config.EventReaderName,
|
||||
func(r *pullreqevents.Reader,
|
||||
) error {
|
||||
r.Configure(
|
||||
stream.WithConcurrency(config.Concurrency),
|
||||
stream.WithHandlerOptions(
|
||||
stream.WithMaxRetries(config.MaxRetries),
|
||||
))
|
||||
|
||||
_ = r.RegisterReviewerAdded(service.notifyReviewerAdded)
|
||||
_ = r.RegisterCommentCreated(service.notifyCommentCreated)
|
||||
_ = r.RegisterBranchUpdated(service.notifyPullReqBranchUpdated)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to launch event reader for %s: %w", eventReaderGroupName, err)
|
||||
}
|
||||
|
||||
return service, nil
|
||||
}
|
||||
|
||||
func (s *Service) getBasePayload(
|
||||
ctx context.Context,
|
||||
base pullreqevents.Base,
|
||||
) (*BasePullReqPayload, error) {
|
||||
repo, err := s.repoStore.Find(ctx, base.TargetRepoID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch repo from repoStore: %w", err)
|
||||
}
|
||||
pullReq, err := s.pullReqStore.Find(ctx, base.PullReqID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch pullreq from pullReqStore: %w", err)
|
||||
}
|
||||
|
||||
author, err := s.principalInfoCache.Get(ctx, pullReq.CreatedBy)
|
||||
if err != nil {
|
||||
return nil,
|
||||
fmt.Errorf("failed to fetch author %d from principalInfoCache while building base notification: %w",
|
||||
pullReq.CreatedBy, err)
|
||||
}
|
||||
|
||||
return &BasePullReqPayload{
|
||||
Repo: repo,
|
||||
PullReq: pullReq,
|
||||
Author: author,
|
||||
PullReqURL: s.urlProvider.GenerateUIPRURL(repo.Path, pullReq.Number),
|
||||
}, nil
|
||||
}
|
17
app/services/notification/templates/comment_created.html
Normal file
17
app/services/notification/templates/comment_created.html
Normal file
@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
</head>
|
||||
<body>
|
||||
<p>
|
||||
<b>@{{.Commenter.DisplayName}}</b> commented on pull request <b>#{{.Base.PullReq.Number}}:{{.Base.PullReq.Title}}</b>
|
||||
</p>
|
||||
<p>
|
||||
{{.Text}}
|
||||
</p>
|
||||
<p>
|
||||
<a href="{{.Base.PullReqURL}}">View pull request #{{.Base.PullReq.Number}}</a>
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
</head>
|
||||
<body>
|
||||
<p>
|
||||
<b>@{{.Committer.DisplayName}}</b> pushed new commits to pull request <b>#{{.Base.PullReq.Number}}:{{.Base.PullReq.Title}}</b>
|
||||
</p>
|
||||
<p>
|
||||
Latest commit is {{.NewSHA}}
|
||||
</p>
|
||||
<p>
|
||||
<a href="{{.Base.PullReqURL}}">View pull request #{{.Base.PullReq.Number}}</a>
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
14
app/services/notification/templates/reviewer_added.html
Normal file
14
app/services/notification/templates/reviewer_added.html
Normal file
@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
</head>
|
||||
<body>
|
||||
<p>
|
||||
<b>@{{.Reviewer.DisplayName}}</b> was added as a reviewer for the Pull request: <b>#{{.Base.PullReq.Number}}:{{.Base.PullReq.Title}}</b>
|
||||
</p>
|
||||
<p>
|
||||
<a href="{{.Base.PullReqURL}}">View pull request #{{.Base.PullReq.Number}}</a>
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
66
app/services/notification/wire.go
Normal file
66
app/services/notification/wire.go
Normal file
@ -0,0 +1,66 @@
|
||||
// Copyright 2023 Harness, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package notification
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
pullreqevents "github.com/harness/gitness/app/events/pullreq"
|
||||
"github.com/harness/gitness/app/services/notification/mailer"
|
||||
"github.com/harness/gitness/app/store"
|
||||
"github.com/harness/gitness/app/url"
|
||||
"github.com/harness/gitness/events"
|
||||
|
||||
"github.com/google/wire"
|
||||
)
|
||||
|
||||
var WireSet = wire.NewSet(
|
||||
ProvideMailClient,
|
||||
ProvideNotificationService,
|
||||
)
|
||||
|
||||
func ProvideNotificationService(
|
||||
ctx context.Context,
|
||||
notificationClient Client,
|
||||
pullReqConfig Config,
|
||||
prReaderFactory *events.ReaderFactory[*pullreqevents.Reader],
|
||||
pullReqStore store.PullReqStore,
|
||||
repoStore store.RepoStore,
|
||||
principalInfoView store.PrincipalInfoView,
|
||||
principalInfoCache store.PrincipalInfoCache,
|
||||
pullReqReviewersStore store.PullReqReviewerStore,
|
||||
pullReqActivityStore store.PullReqActivityStore,
|
||||
spacePathStore store.SpacePathStore,
|
||||
urlProvider url.Provider,
|
||||
) (*Service, error) {
|
||||
return NewService(
|
||||
ctx,
|
||||
pullReqConfig,
|
||||
notificationClient,
|
||||
prReaderFactory,
|
||||
pullReqStore,
|
||||
repoStore,
|
||||
principalInfoView,
|
||||
principalInfoCache,
|
||||
pullReqReviewersStore,
|
||||
pullReqActivityStore,
|
||||
spacePathStore,
|
||||
urlProvider,
|
||||
)
|
||||
}
|
||||
|
||||
func ProvideMailClient(mailer mailer.Mailer) Client {
|
||||
return NewMailClient(mailer)
|
||||
}
|
@ -19,6 +19,7 @@ import (
|
||||
"github.com/harness/gitness/app/services/job"
|
||||
"github.com/harness/gitness/app/services/keywordsearch"
|
||||
"github.com/harness/gitness/app/services/metric"
|
||||
"github.com/harness/gitness/app/services/notification"
|
||||
"github.com/harness/gitness/app/services/pullreq"
|
||||
"github.com/harness/gitness/app/services/trigger"
|
||||
"github.com/harness/gitness/app/services/webhook"
|
||||
@ -37,6 +38,7 @@ type Services struct {
|
||||
JobScheduler *job.Scheduler
|
||||
MetricCollector *metric.Collector
|
||||
Cleanup *cleanup.Service
|
||||
Notification *notification.Service
|
||||
Keywordsearch *keywordsearch.Service
|
||||
}
|
||||
|
||||
@ -47,6 +49,7 @@ func ProvideServices(
|
||||
jobScheduler *job.Scheduler,
|
||||
metricCollector *metric.Collector,
|
||||
cleanupSvc *cleanup.Service,
|
||||
notificationSvc *notification.Service,
|
||||
keywordsearchSvc *keywordsearch.Service,
|
||||
) Services {
|
||||
return Services{
|
||||
@ -56,6 +59,7 @@ func ProvideServices(
|
||||
JobScheduler: jobScheduler,
|
||||
MetricCollector: metricCollector,
|
||||
Cleanup: cleanupSvc,
|
||||
Notification: notificationSvc,
|
||||
Keywordsearch: keywordsearchSvc,
|
||||
}
|
||||
}
|
||||
|
@ -25,6 +25,7 @@ import (
|
||||
"github.com/harness/gitness/app/services/cleanup"
|
||||
"github.com/harness/gitness/app/services/codeowners"
|
||||
"github.com/harness/gitness/app/services/keywordsearch"
|
||||
"github.com/harness/gitness/app/services/notification"
|
||||
"github.com/harness/gitness/app/services/trigger"
|
||||
"github.com/harness/gitness/app/services/webhook"
|
||||
"github.com/harness/gitness/blob"
|
||||
@ -279,6 +280,14 @@ func ProvideWebhookConfig(config *types.Config) webhook.Config {
|
||||
}
|
||||
}
|
||||
|
||||
func ProvideNotificationConfig(config *types.Config) notification.Config {
|
||||
return notification.Config{
|
||||
EventReaderName: config.InstanceID,
|
||||
Concurrency: config.Notification.Concurrency,
|
||||
MaxRetries: config.Notification.MaxRetries,
|
||||
}
|
||||
}
|
||||
|
||||
// ProvideTriggerConfig loads the trigger service config from the main config.
|
||||
func ProvideTriggerConfig(config *types.Config) trigger.Config {
|
||||
return trigger.Config{
|
||||
|
@ -9,7 +9,6 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
checkcontroller "github.com/harness/gitness/app/api/controller/check"
|
||||
"github.com/harness/gitness/app/api/controller/connector"
|
||||
"github.com/harness/gitness/app/api/controller/execution"
|
||||
@ -56,6 +55,8 @@ import (
|
||||
"github.com/harness/gitness/app/services/job"
|
||||
"github.com/harness/gitness/app/services/keywordsearch"
|
||||
"github.com/harness/gitness/app/services/metric"
|
||||
"github.com/harness/gitness/app/services/notification"
|
||||
"github.com/harness/gitness/app/services/notification/mailer"
|
||||
"github.com/harness/gitness/app/services/protection"
|
||||
pullreqservice "github.com/harness/gitness/app/services/pullreq"
|
||||
"github.com/harness/gitness/app/services/trigger"
|
||||
@ -92,6 +93,8 @@ func initSystem(ctx context.Context, config *types.Config) (*cliserver.System, e
|
||||
cliserver.ProvideDatabaseConfig,
|
||||
database.WireSet,
|
||||
cliserver.ProvideBlobStoreConfig,
|
||||
mailer.WireSet,
|
||||
notification.WireSet,
|
||||
blob.WireSet,
|
||||
dbtx.WireSet,
|
||||
cache.WireSet,
|
||||
@ -124,6 +127,7 @@ func initSystem(ctx context.Context, config *types.Config) (*cliserver.System, e
|
||||
cliserver.ProvideEventsConfig,
|
||||
events.WireSet,
|
||||
cliserver.ProvideWebhookConfig,
|
||||
cliserver.ProvideNotificationConfig,
|
||||
webhook.WireSet,
|
||||
cliserver.ProvideTriggerConfig,
|
||||
trigger.WireSet,
|
||||
|
@ -55,6 +55,8 @@ import (
|
||||
"github.com/harness/gitness/app/services/job"
|
||||
"github.com/harness/gitness/app/services/keywordsearch"
|
||||
"github.com/harness/gitness/app/services/metric"
|
||||
"github.com/harness/gitness/app/services/notification"
|
||||
"github.com/harness/gitness/app/services/notification/mailer"
|
||||
"github.com/harness/gitness/app/services/protection"
|
||||
"github.com/harness/gitness/app/services/pullreq"
|
||||
trigger2 "github.com/harness/gitness/app/services/trigger"
|
||||
@ -286,12 +288,19 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mailerMailer := mailer.ProvideMailClient(config)
|
||||
notificationClient := notification.ProvideMailClient(mailerMailer)
|
||||
notificationConfig := server.ProvideNotificationConfig(config)
|
||||
notificationService, err := notification.ProvideNotificationService(ctx, notificationClient, notificationConfig, eventsReaderFactory, pullReqStore, repoStore, principalInfoView, principalInfoCache, pullReqReviewerStore, pullReqActivityStore, spacePathStore, provider)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keywordsearchConfig := server.ProvideKeywordSearchConfig(config)
|
||||
keywordsearchService, err := keywordsearch.ProvideService(ctx, keywordsearchConfig, readerFactory, repoStore, indexer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
servicesServices := services.ProvideServices(webhookService, pullreqService, triggerService, jobScheduler, collector, cleanupService, keywordsearchService)
|
||||
servicesServices := services.ProvideServices(webhookService, pullreqService, triggerService, jobScheduler, collector, cleanupService, notificationService, keywordsearchService)
|
||||
serverSystem := server.NewSystem(bootstrapBootstrap, serverServer, poller, pluginManager, servicesServices)
|
||||
return serverSystem, nil
|
||||
}
|
||||
|
@ -324,6 +324,11 @@ type Config struct {
|
||||
Insecure bool `envconfig:"GITNESS_SMTP_INSECURE"`
|
||||
}
|
||||
|
||||
Notification struct {
|
||||
MaxRetries int `envconfig:"GITNESS_NOTIFICATION_MAX_RETRIES" default:"3"`
|
||||
Concurrency int `envconfig:"GITNESS_NOTIFICATION_CONCURRENCY" default:"4"`
|
||||
}
|
||||
|
||||
KeywordSearch struct {
|
||||
Concurrency int `envconfig:"GITNESS_KEYWORD_SEARCH_CONCURRENCY" default:"4"`
|
||||
MaxRetries int `envconfig:"GITNESS_KEYWORD_SEARCH_MAX_RETRIES" default:"3"`
|
||||
|
Loading…
Reference in New Issue
Block a user