|  | // Copyright 2019 The Go Authors. All rights reserved. | 
|  | // Use of this source code is governed by a BSD-style | 
|  | // license that can be found in the LICENSE file. | 
|  |  | 
|  | // Package sendwikidiff implements a Google Cloud background function that | 
|  | // reacts to a pubsub message containing a GitHub webhook change payload. | 
|  | // It assumes the payload is in reaction to a change to the Go wiki, then | 
|  | // sends the full diff to the golang-wikichanges mailing list. | 
|  | package sendwikidiff | 
|  |  | 
|  | import ( | 
|  | "bytes" | 
|  | "context" | 
|  | "encoding/json" | 
|  | "fmt" | 
|  | "html/template" | 
|  | "net/url" | 
|  | "os" | 
|  | "os/exec" | 
|  | "path" | 
|  | "sync" | 
|  |  | 
|  | "github.com/sendgrid/sendgrid-go" | 
|  | "github.com/sendgrid/sendgrid-go/helpers/mail" | 
|  | ) | 
|  |  | 
|  | const repoURL = "https://github.com/golang/go.wiki.git" | 
|  |  | 
|  | var sendgridAPIKey = os.Getenv("SENDGRID_API_KEY") | 
|  |  | 
|  | type pubsubMessage struct { | 
|  | Data []byte `json:"data"` | 
|  | } | 
|  |  | 
|  | func HandleWikiChangePubSub(ctx context.Context, m pubsubMessage) error { | 
|  | if sendgridAPIKey == "" { | 
|  | return fmt.Errorf("Environment variable SENDGRID_API_KEY is empty") | 
|  | } | 
|  |  | 
|  | var payload struct { | 
|  | Pages []struct { | 
|  | PageName string `json:"page_name"` | 
|  | SHA      string `json:"sha"` | 
|  | } `json:"pages"` | 
|  | } | 
|  | if err := json.Unmarshal(m.Data, &payload); err != nil { | 
|  | fmt.Fprintf(os.Stderr, "Unable to decode payload: %v", err) | 
|  | return err | 
|  | } | 
|  |  | 
|  | repo := newGitRepo(repoURL, tempRepoDir(repoURL)) | 
|  | if err := repo.update(); err != nil { | 
|  | fmt.Fprintf(os.Stderr, "Unable to update repo: %v", err) | 
|  | return err | 
|  | } | 
|  | for _, page := range payload.Pages { | 
|  | out, err := repo.cmdShow(page.SHA).Output() | 
|  | if err != nil { | 
|  | fmt.Fprintf(os.Stderr, "Could not show SHA %q: %v", page.SHA, err) | 
|  | return err | 
|  | } | 
|  | if err := sendEmail(page.PageName, string(out)); err != nil { | 
|  | fmt.Fprintf(os.Stderr, "Could not send email: %v", err) | 
|  | return err | 
|  | } | 
|  | } | 
|  | return nil | 
|  | } | 
|  |  | 
|  | func tempRepoDir(repoURL string) string { | 
|  | return path.Join(os.TempDir(), url.PathEscape(repoURL)) | 
|  | } | 
|  |  | 
|  | var htmlTmpl = template.Must(template.New("email").Parse(`<p><a href="{{.PageURL}}">View page</a></p> | 
|  | <pre style="font-family: monospace,monospace; white-space: pre-wrap;">{{.Diff}}</pre> | 
|  | `)) | 
|  |  | 
|  | func emailBody(page, diff string) (string, error) { | 
|  | var buf bytes.Buffer | 
|  | if err := htmlTmpl.Execute(&buf, struct { | 
|  | PageURL, Diff string | 
|  | }{ | 
|  | Diff:    diff, | 
|  | PageURL: fmt.Sprintf("https://golang.org/wiki/%s", page), | 
|  | }); err != nil { | 
|  | return "", fmt.Errorf("template.Execute: %v", err) | 
|  | } | 
|  | return buf.String(), nil | 
|  | } | 
|  |  | 
|  | func sendEmailSendGrid(page, diff string) error { | 
|  | from := mail.NewEmail("WikiDiffBot", "nobody@golang.org") | 
|  | subject := fmt.Sprintf("golang.org/wiki/%s was updated", page) | 
|  | to := mail.NewEmail("", "golang-wikichanges@googlegroups.com") | 
|  |  | 
|  | body, err := emailBody(page, diff) | 
|  | if err != nil { | 
|  | return fmt.Errorf("emailBody: %v", err) | 
|  | } | 
|  | message := mail.NewSingleEmail(from, subject, to, diff, body) | 
|  | client := sendgrid.NewSendClient(sendgridAPIKey) | 
|  | _, err = client.Send(message) | 
|  | return err | 
|  | } | 
|  |  | 
|  | // sendEmail sends an email that the golang.org/wiki/$page was updated | 
|  | // with the provided diff. | 
|  | // Var for testing. | 
|  | var sendEmail func(page, diff string) error = sendEmailSendGrid | 
|  |  | 
|  | type gitRepo struct { | 
|  | sync.RWMutex | 
|  |  | 
|  | repo string // remote address of repo | 
|  | dir  string // location of the repo | 
|  | } | 
|  |  | 
|  | func newGitRepo(repo, dir string) *gitRepo { | 
|  | return &gitRepo{ | 
|  | repo: repo, | 
|  | dir:  dir, | 
|  | } | 
|  | } | 
|  |  | 
|  | func (r *gitRepo) clone() error { | 
|  | r.Lock() | 
|  | defer r.Unlock() | 
|  | cmd := exec.Command("git", "clone", r.repo, r.dir) | 
|  | cmd.Stderr = os.Stderr | 
|  | cmd.Stdout = os.Stdout | 
|  | if err := cmd.Run(); err != nil { | 
|  | return err | 
|  | } | 
|  | return nil | 
|  | } | 
|  |  | 
|  | func (r *gitRepo) pull() error { | 
|  | r.Lock() | 
|  | defer r.Unlock() | 
|  | cmd := exec.Command("git", "pull") | 
|  | cmd.Dir = r.dir | 
|  | cmd.Stderr = os.Stderr | 
|  | cmd.Stdout = os.Stdout | 
|  | if err := cmd.Run(); err != nil { | 
|  | return err | 
|  | } | 
|  | return nil | 
|  | } | 
|  |  | 
|  | func (r *gitRepo) update() error { | 
|  | r.RLock() | 
|  | _, err := os.Stat(r.dir) | 
|  | r.RUnlock() | 
|  | if os.IsNotExist(err) { | 
|  | if err := r.clone(); err != nil { | 
|  | return fmt.Errorf("could not clone %q into %q: %v", r.repo, r.dir, err) | 
|  | } | 
|  | return nil | 
|  | } | 
|  |  | 
|  | if err := r.pull(); err != nil { | 
|  | return fmt.Errorf("could not pull %q: %v", r.repo, err) | 
|  | } | 
|  | return nil | 
|  | } | 
|  |  | 
|  | func (r *gitRepo) cmdShow(ref string) *exec.Cmd { | 
|  | r.RLock() | 
|  | defer r.RUnlock() | 
|  | cmd := exec.Command("git", "show", ref) | 
|  | cmd.Dir = r.dir | 
|  | return cmd | 
|  | } |