Skip to content
quarry Artifact [f43238215c]
Login

Artifact [f43238215c]

Artifact f43238215c9cb9b6e3f97f2c3dd70ac259455707f91a235e09bd3a35ddcdea0a:


package api

import (
	"html/template"
	"net/url"
	"strings"
	"time"

	"craftdesign.group/quarry/internal/markup"
)

// mailCard is what one workflow's email says around its payload: the
// verb in the context line, what the recipient did to deserve it, and
// whether the workflow takes replies (it must agree with the reply flag
// on the workflow's email step, internal/notify/workflows.go).
type mailCard struct {
	context func(from, repo, title string) template.HTML
	why     func(repo string) string
	reply   bool
	body    bool // the payload's text is rendered as the body
}

func strong(s string) string { return "<strong>" + template.HTMLEscapeString(s) + "</strong>" }

var mailCards = map[string]mailCard{
	"quarry-mention": {
		context: func(from, repo, _ string) template.HTML {
			return template.HTML(strong(from) + " mentioned you in " + strong(repo))
		},
		why:   func(repo string) string { return "You're getting this because you were mentioned in " + repo + "." },
		reply: true, body: true,
	},
	"quarry-forum-reply": {
		context: func(from, _, title string) template.HTML {
			return template.HTML(strong(from) + " replied in " + strong(title))
		},
		why:   func(repo string) string { return "You're getting this because you follow this thread in " + repo + "." },
		reply: true, body: true,
	},
	"quarry-landing": {
		context: func(from, repo, title string) template.HTML {
			return template.HTML(strong(title))
		},
		why:   func(repo string) string { return "You're getting this because it concerns you in " + repo + "." },
		reply: true,
	},
}

// mailOpen is the button's words, by the surface the link opens.
var mailOpen = map[string]string{
	"chat": "Open the message", "team chat": "Open the chat", "forum": "Open the post",
	"landing": "Open the landing", "ci": "Open the run",
}

func payloadStr(p map[string]any, k string) string {
	s, _ := p[k].(string)
	return s
}

// withMailHTML adds payload.html to an email workflow's event: the whole
// card (markup.EmailDocument) around the same rendering and sanitizing
// the pages use, made mail-safe. The text body stays as the workflow
// wrote it. It adds nothing without an origin (links could not be
// absolute) or when the document would be too large, and notify then
// sends text only. The payload is copied, like absoluteLink's.
func (d Deps) withMailHTML(e NotifyEvent, now time.Time) NotifyEvent {
	if e.Workflow == "quarry-activity" && d.Origin != "" {
		// The digest's card is written in notify (notify.Workflows), once
		// for the batch; it names the forge through the payload.
		p := make(map[string]any, len(e.Payload)+2)
		for k, v := range e.Payload {
			p[k] = v
		}
		p["origin"], p["host"] = d.link(""), d.originHost()
		e.Payload = p
		return e
	}
	c, ok := mailCards[e.Workflow]
	link := payloadStr(e.Payload, "link")
	if !ok || d.Origin == "" || !strings.HasPrefix(link, "http") {
		return e
	}
	var body template.HTML
	if c.body {
		text := payloadStr(e.Payload, "text")
		if text == "" {
			return e
		}
		if body, ok = markup.EmailHTML([]byte(text), resolver{}, d.link); !ok {
			return e
		}
	}
	repo, surface := payloadStr(e.Payload, "repo"), payloadStr(e.Payload, "surface")
	title := payloadStr(e.Payload, "title")
	if e.Workflow == "quarry-landing" {
		title = payloadStr(e.Payload, "summary")
	}
	meta := now.UTC().Format("Mon 2 Jan, 15:04 UTC")
	if ref := payloadStr(e.Payload, "ref"); ref != "" {
		meta += " ยท " + ref
	}
	doc := markup.EmailDoc{
		Title:   title,
		Repo:    repo,
		Surface: surface,
		Context: c.context(payloadStr(e.Payload, "from"), repo, title),
		Meta:    meta,
		Body:    body,
		Open:    mailOpen[surface], OpenURL: link,
		Why:         c.why(repo),
		SettingsURL: d.link("/me#notifications"),
		HomeURL:     d.link(""), HomeName: d.originHost(),
	}
	if c.reply {
		doc.ReplyIn = surface
	}
	h, ok := markup.EmailDocument(doc)
	if !ok {
		return e
	}
	p := make(map[string]any, len(e.Payload)+1)
	for k, v := range e.Payload {
		p[k] = v
	}
	p["html"] = h
	e.Payload = p
	return e
}

// originHost is the forge's host for display, falling back to the origin.
func (d Deps) originHost() string {
	if u, err := url.Parse(d.Origin); err == nil && u.Host != "" {
		return u.Host
	}
	return d.Origin
}

// accountCard wraps the body of a mail about the account itself: no repo,
// no preference to point at, and a button when the mail has one job.
func (d Deps) accountCard(subject string, body template.HTML, open, openURL string) string {
	doc := markup.EmailDoc{
		Title: subject, Surface: "account",
		Context: template.HTML(strong(subject)),
		Body:    body, Open: open, OpenURL: openURL,
		Why: "You're getting this because it is about your quarry account.",
	}
	if d.Origin != "" {
		doc.HomeURL, doc.HomeName = d.link(""), d.originHost()
	}
	h, ok := markup.EmailDocument(doc)
	if !ok {
		return ""
	}
	return h
}