Add email support (#35)

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
This commit is contained in:
Mathieu Velten 2020-09-16 11:25:56 +02:00 committed by GitHub
parent 1042a93da1
commit fab3f9b37d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 148 additions and 53 deletions

View file

@ -91,3 +91,11 @@ The response (if successful) will be a JSON object with the following fields:
* `report_url`: A URL where the user can track their bug report. Omitted if * `report_url`: A URL where the user can track their bug report. Omitted if
issue submission was disabled. issue submission was disabled.
## Notifications
You can get notifications when a new rageshake arrives on the server.
Currently this tool supports pushing notifications as GitHub issues in a repo,
through a Slack webhook or by email, cf sample config file for how to
configure them.

1
changelog.d/35.feature Normal file
View file

@ -0,0 +1 @@
Add email support.

1
go.mod
View file

@ -7,6 +7,7 @@ require (
github.com/google/go-github v0.0.0-20170401000335-12363ffc1001 github.com/google/go-github v0.0.0-20170401000335-12363ffc1001
github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135 github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135
github.com/googleapis/gax-go v0.0.0-20170321005343-9af46dd5a171 github.com/googleapis/gax-go v0.0.0-20170321005343-9af46dd5a171
github.com/jordan-wright/email v4.0.1-0.20200824153738-3f5bafa1cd84+incompatible
github.com/pkg/errors v0.0.0-20171018195549-f15c970de5b7 github.com/pkg/errors v0.0.0-20171018195549-f15c970de5b7
golang.org/x/net v0.0.0-20170329170435-ffcf1bedda3b golang.org/x/net v0.0.0-20170329170435-ffcf1bedda3b
golang.org/x/oauth2 v0.0.0-20170321013421-7fdf09982454 golang.org/x/oauth2 v0.0.0-20170321013421-7fdf09982454

3
go.sum
View file

@ -3,9 +3,12 @@ github.com/golang/protobuf v0.0.0-20170331031902-2bba0603135d/go.mod h1:6lQm79b+
github.com/google/go-genproto v0.0.0-20170404132009-411e09b969b1/go.mod h1:3Rcd9jSoLVkV/osPrt5CogLvLiarfI8U9/x78NwhuDU= github.com/google/go-genproto v0.0.0-20170404132009-411e09b969b1/go.mod h1:3Rcd9jSoLVkV/osPrt5CogLvLiarfI8U9/x78NwhuDU=
github.com/google/go-github v0.0.0-20170401000335-12363ffc1001 h1:OK4gfzCBCtPg14E4sYsczwFhjVu1jQJZI+OEOpiTigw= github.com/google/go-github v0.0.0-20170401000335-12363ffc1001 h1:OK4gfzCBCtPg14E4sYsczwFhjVu1jQJZI+OEOpiTigw=
github.com/google/go-github v0.0.0-20170401000335-12363ffc1001/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-github v0.0.0-20170401000335-12363ffc1001/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY=
github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135 h1:zLTLjkaOFEFIOxY5BWLFLwh+cL8vOBW4XJ2aqLE/Tf0= github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135 h1:zLTLjkaOFEFIOxY5BWLFLwh+cL8vOBW4XJ2aqLE/Tf0=
github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/googleapis/gax-go v0.0.0-20170321005343-9af46dd5a171/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= github.com/googleapis/gax-go v0.0.0-20170321005343-9af46dd5a171/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
github.com/jordan-wright/email v4.0.1-0.20200824153738-3f5bafa1cd84+incompatible h1:d60x4RsAHk/UX/0OT8Gc6D7scVvhBbEANpTAWrDhA/I=
github.com/jordan-wright/email v4.0.1-0.20200824153738-3f5bafa1cd84+incompatible/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A=
github.com/pkg/errors v0.0.0-20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.0.0-20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
golang.org/x/net v0.0.0-20170329170435-ffcf1bedda3b h1:Co3zyosPfwWowmu8+roHGC+aDgizpCPH3ukhubZ0Ttg= golang.org/x/net v0.0.0-20170329170435-ffcf1bedda3b h1:Co3zyosPfwWowmu8+roHGC+aDgizpCPH3ukhubZ0Ttg=
golang.org/x/net v0.0.0-20170329170435-ffcf1bedda3b/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20170329170435-ffcf1bedda3b/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=

21
main.go
View file

@ -21,8 +21,6 @@ import (
"crypto/subtle" "crypto/subtle"
"flag" "flag"
"fmt" "fmt"
"github.com/google/go-github/github"
"golang.org/x/oauth2"
"io/ioutil" "io/ioutil"
"log" "log"
"net" "net"
@ -31,6 +29,9 @@ import (
"strings" "strings"
"time" "time"
"github.com/google/go-github/github"
"golang.org/x/oauth2"
yaml "gopkg.in/yaml.v2" yaml "gopkg.in/yaml.v2"
) )
@ -51,6 +52,16 @@ type config struct {
GithubProjectMappings map[string]string `yaml:"github_project_mappings"` GithubProjectMappings map[string]string `yaml:"github_project_mappings"`
SlackWebhookURL string `yaml:"slack_webhook_url"` SlackWebhookURL string `yaml:"slack_webhook_url"`
EmailAddresses []string `yaml:"email_addresses"`
EmailFrom string `yaml:"email_from"`
SMTPServer string `yaml:"smtp_server"`
SMTPUsername string `yaml:"smtp_username"`
SMTPPassword string `yaml:"smtp_password"`
} }
func basicAuth(handler http.Handler, username, password, realm string) http.Handler { func basicAuth(handler http.Handler, username, password, realm string) http.Handler {
@ -99,6 +110,10 @@ func main() {
slack = newSlackClient(cfg.SlackWebhookURL) slack = newSlackClient(cfg.SlackWebhookURL)
} }
if len(cfg.EmailAddresses) > 0 && cfg.SMTPServer == "" {
log.Fatal("Email address(es) specified but no smtp_server configured. Wrong configuration, aborting...")
}
apiPrefix := cfg.APIPrefix apiPrefix := cfg.APIPrefix
if apiPrefix == "" { if apiPrefix == "" {
_, port, err := net.SplitHostPort(*bindAddr) _, port, err := net.SplitHostPort(*bindAddr)
@ -112,7 +127,7 @@ func main() {
} }
log.Printf("Using %s/listing as public URI", apiPrefix) log.Printf("Using %s/listing as public URI", apiPrefix)
http.Handle("/api/submit", &submitServer{ghClient, apiPrefix, cfg.GithubProjectMappings, slack}) http.Handle("/api/submit", &submitServer{ghClient, apiPrefix, slack, cfg})
// Make sure bugs directory exists // Make sure bugs directory exists
_ = os.Mkdir("bugs", os.ModePerm) _ = os.Mkdir("bugs", os.ModePerm)

View file

@ -20,3 +20,16 @@ github_project_mappings:
# a Slack personal webhook URL (https://api.slack.com/incoming-webhooks), which # a Slack personal webhook URL (https://api.slack.com/incoming-webhooks), which
# will be used to post a notification on Slack for each report. # will be used to post a notification on Slack for each report.
slack_webhook_url: https://hooks.slack.com/services/TTTTTTT/XXXXXXXXXX/YYYYYYYYYYY slack_webhook_url: https://hooks.slack.com/services/TTTTTTT/XXXXXXXXXX/YYYYYYYYYYY
# notification can also be pushed by email.
# this param controls the target emails
email_addresses:
- support@matrix.org
# this is the from field that will be used in the email notifications
email_from: Rageshake <rageshake@matrix.org>
# SMTP server configuration
smtp_server: localhost:25
smtp_username: myemailuser
smtp_password: myemailpass

154
submit.go
View file

@ -28,6 +28,7 @@ import (
"mime" "mime"
"mime/multipart" "mime/multipart"
"net/http" "net/http"
"net/smtp"
"os" "os"
"path/filepath" "path/filepath"
"regexp" "regexp"
@ -37,6 +38,7 @@ import (
"time" "time"
"github.com/google/go-github/github" "github.com/google/go-github/github"
"github.com/jordan-wright/email"
) )
var maxPayloadSize = 1024 * 1024 * 55 // 55 MB var maxPayloadSize = 1024 * 1024 * 55 // 55 MB
@ -49,10 +51,9 @@ type submitServer struct {
// External URI to /api // External URI to /api
apiPrefix string apiPrefix string
// mappings from application to github owner/project
githubProjectMappings map[string]string
slack *slackClient slack *slackClient
cfg *config
} }
// the type of payload which can be uploaded as JSON to the submit endpoint // the type of payload which can be uploaded as JSON to the submit endpoint
@ -470,70 +471,77 @@ func (s *submitServer) saveReport(ctx context.Context, p parsedPayload, reportDi
return nil, err return nil, err
} }
if err := s.sendEmail(p, reportDir); err != nil {
return nil, err
}
return &resp, nil return &resp, nil
} }
func (s *submitServer) submitGithubIssue(ctx context.Context, p parsedPayload, listingURL string, resp *submitResponse) error { func (s *submitServer) submitGithubIssue(ctx context.Context, p parsedPayload, listingURL string, resp *submitResponse) error {
if s.ghClient == nil { if s.ghClient == nil {
log.Println("GH issue submission disabled") return nil
} else {
// submit a github issue
ghProj := s.githubProjectMappings[p.AppName]
if ghProj == "" {
log.Println("Not creating GH issue for unknown app", p.AppName)
return nil
}
splits := strings.SplitN(ghProj, "/", 2)
if len(splits) < 2 {
log.Println("Can't create GH issue for invalid repo", ghProj)
}
owner, repo := splits[0], splits[1]
issueReq := buildGithubIssueRequest(p, listingURL)
issue, _, err := s.ghClient.Issues.Create(ctx, owner, repo, &issueReq)
if err != nil {
return err
}
log.Println("Created issue:", *issue.HTMLURL)
resp.ReportURL = *issue.HTMLURL
} }
// submit a github issue
ghProj := s.cfg.GithubProjectMappings[p.AppName]
if ghProj == "" {
log.Println("Not creating GH issue for unknown app", p.AppName)
return nil
}
splits := strings.SplitN(ghProj, "/", 2)
if len(splits) < 2 {
log.Println("Can't create GH issue for invalid repo", ghProj)
}
owner, repo := splits[0], splits[1]
issueReq := buildGithubIssueRequest(p, listingURL)
issue, _, err := s.ghClient.Issues.Create(ctx, owner, repo, &issueReq)
if err != nil {
return err
}
log.Println("Created issue:", *issue.HTMLURL)
resp.ReportURL = *issue.HTMLURL
return nil return nil
} }
func (s *submitServer) submitSlackNotification(p parsedPayload, listingURL string) error { func (s *submitServer) submitSlackNotification(p parsedPayload, listingURL string) error {
if s.slack == nil { if s.slack == nil {
log.Println("Slack notifications disabled") return nil
} else {
slackBuf := fmt.Sprintf(
"%s\nApplication: %s\nReport: %s",
p.UserText, p.AppName, listingURL,
)
err := s.slack.Notify(slackBuf)
if err != nil {
return err
}
} }
slackBuf := fmt.Sprintf(
"%s\nApplication: %s\nReport: %s",
p.UserText, p.AppName, listingURL,
)
err := s.slack.Notify(slackBuf)
if err != nil {
return err
}
return nil return nil
} }
func buildGithubIssueRequest(p parsedPayload, listingURL string) github.IssueRequest { func buildReportTitle(p parsedPayload) string {
// set the title to the first (non-empty) line of the user's report, if any // set the title to the first (non-empty) line of the user's report, if any
var title string
trimmedUserText := strings.TrimSpace(p.UserText) trimmedUserText := strings.TrimSpace(p.UserText)
if trimmedUserText == "" { if trimmedUserText == "" {
title = "Untitled report" return "Untitled report"
} else {
if i := strings.IndexAny(trimmedUserText, "\r\n"); i < 0 {
title = trimmedUserText
} else {
title = trimmedUserText[0:i]
}
} }
if i := strings.IndexAny(trimmedUserText, "\r\n"); i >= 0 {
return trimmedUserText[0:i]
}
return trimmedUserText
}
func buildReportBody(p parsedPayload, quoteChar string) *bytes.Buffer {
var bodyBuf bytes.Buffer var bodyBuf bytes.Buffer
fmt.Fprintf(&bodyBuf, "User message:\n\n%s\n\n", p.UserText) fmt.Fprintf(&bodyBuf, "User message:\n\n%s\n\n", p.UserText)
var dataKeys []string var dataKeys []string
@ -543,19 +551,29 @@ func buildGithubIssueRequest(p parsedPayload, listingURL string) github.IssueReq
sort.Strings(dataKeys) sort.Strings(dataKeys)
for _, k := range dataKeys { for _, k := range dataKeys {
v := p.Data[k] v := p.Data[k]
fmt.Fprintf(&bodyBuf, "%s: `%s`\n", k, v) fmt.Fprintf(&bodyBuf, "%s: %s%s%s\n", k, quoteChar, v, quoteChar)
} }
fmt.Fprintf(&bodyBuf, "[Logs](%s)", listingURL)
return &bodyBuf
}
func buildGithubIssueRequest(p parsedPayload, listingURL string) github.IssueRequest {
bodyBuf := buildReportBody(p, "`")
// Add log links to the body
fmt.Fprintf(bodyBuf, "[Logs](%s)", listingURL)
for _, file := range p.Files { for _, file := range p.Files {
fmt.Fprintf( fmt.Fprintf(
&bodyBuf, bodyBuf,
" / [%s](%s)", " / [%s](%s)",
file, file,
listingURL+"/"+file, listingURL+"/"+file,
) )
} }
title := buildReportTitle(p)
body := bodyBuf.String() body := bodyBuf.String()
labels := p.Labels labels := p.Labels
@ -570,6 +588,42 @@ func buildGithubIssueRequest(p parsedPayload, listingURL string) github.IssueReq
} }
} }
func (s *submitServer) sendEmail(p parsedPayload, reportDir string) error {
if len(s.cfg.EmailAddresses) == 0 {
return nil
}
e := email.NewEmail()
e.From = "Rageshake <rageshake@matrix.org>"
if s.cfg.EmailFrom != "" {
e.From = s.cfg.EmailFrom
}
e.To = s.cfg.EmailAddresses
e.Subject = fmt.Sprintf("[%s] %s", p.AppName, buildReportTitle(p))
e.Text = buildReportBody(p, "\"").Bytes()
allFiles := append(p.Files, p.Logs...)
for _, file := range allFiles {
fullPath := filepath.Join(reportDir, file)
e.AttachFile(fullPath)
}
var auth smtp.Auth = nil
if s.cfg.SMTPPassword != "" || s.cfg.SMTPUsername != "" {
auth = smtp.PlainAuth("", s.cfg.SMTPUsername, s.cfg.SMTPPassword, s.cfg.SMTPServer)
}
err := e.Send(s.cfg.SMTPServer, auth)
if err != nil {
return err
}
return nil
}
func respond(code int, w http.ResponseWriter) { func respond(code int, w http.ResponseWriter) {
w.WriteHeader(code) w.WriteHeader(code)
w.Write([]byte("{}")) w.Write([]byte("{}"))