blob: 06ce0c6f2b25c6a74897b6d999627b5ce32bf85a [file] [log] [blame]
// 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"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
"github.com/sendgrid/sendgrid-go"
"github.com/sendgrid/sendgrid-go/helpers/mail"
)
const repoURL = "https://github.com/golang/go.wiki.git"
var tempRepoDir = filepath.Join(os.TempDir(), strings.NewReplacer("://", "-", "/", "-").Replace(repoURL))
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)
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
}
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://go.dev/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("go.dev/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 go.dev/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.Env = append(os.Environ(), "PWD="+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
cmd.Env = append(os.Environ(), "PWD="+r.dir)
return cmd
}