Skip to content
quarry Artifact [06d8a06e44]
Login

Artifact [06d8a06e44]

Artifact 06d8a06e444a6d7da4e95165e1917955768ea08fdabef7d6650cbeef5131f1cd:


package notify

// Workflows returns the definitions quarry provisions at startup (RFC
// 0011 §7). These are the same six workflows docs/running.md used to
// ask an operator to `curl` by hand — this is now the one source,
// created by EnsureWorkflow rather than typed.
//
// RFC 0011 §6's routing table is spelled here and nowhere else:
//
//	quarry-mention      in_app -> push (immediate)
//	quarry-forum-reply  in_app -> push (immediate)
//	quarry-landing      in_app -> push (immediate)
//	quarry-activity     digest 15 min -> in_app
//	quarry-error/slow   in_app (admins; unchanged)
//
// IMPORTANT for an EXISTING forge: EnsureWorkflow upserts with
// notify's `onConflict=keep`, which never rewrites a workflow that is
// already there (an operator may have edited its copy). So a forge
// provisioned before this phase keeps its single-in_app definitions:
// the push and digest steps below reach it only after the operator
// deletes those workflows in notify's console once and lets the next
// startup recreate them.
func Workflows() []WorkflowDef {
	// Control values are notify's own per-step-type shapes, read off
	// internal/steptype (channels.go for in_app/push, sleep.go for
	// digest). They are typed map[string]any rather than map[string]string
	// because a digest's `amount` is a NUMBER on notify's side: a
	// quoted "15" fails its json.Unmarshal into int64 and the step
	// never compiles.
	// The redirect is what makes an inbox row a link. notify stores it
	// on the message as extras.redirect.url, which is exactly where
	// Message.Link reads from, and the inbox renders a row with no link
	// as dead text — eight rows of "something happened" leading
	// nowhere. Every trigger quarry sends carries payload.link for this
	// reason; the push step has always used it.
	inApp := func(id, subject, body string) WorkflowStep {
		return WorkflowStep{
			StepID: id,
			Type:   "in_app",
			ControlValues: map[string]any{
				"subject":  subject,
				"body":     body,
				"redirect": map[string]any{"url": "{{payload.link}}"},
			},
		}
	}
	// push carries the link in data_json, which notify's push step
	// validates as a JSON object of STRING values (steptype.push) and
	// passes to the browser as the notification's data — where sw.js
	// reads data.url to decide what a click opens.
	push := func(id, title, body string) WorkflowStep {
		return WorkflowStep{
			StepID: id,
			Type:   "push",
			ControlValues: map[string]any{
				// BOTH spellings, deliberately. notify's push editor
				// authors "title", but the envelope its delivery
				// renders from (runenv.StepControls) carries only
				// "subject" — so a push authored the documented way
				// arrives with an empty title and the service worker
				// falls back to the word "quarry" for every
				// notification (notify#334). "subject" is what renders
				// today; "title" is what the editor will show an
				// operator, and what will keep working when the two
				// spellings meet.
				"title":     title,
				"subject":   title,
				"body":      body,
				"data_json": `{"url":"{{payload.link}}"}`,
			},
		}
	}
	email := func(id, subject, body string) WorkflowStep {
		return WorkflowStep{
			StepID: id,
			Type:   "email",
			ControlValues: map[string]any{
				"subject": subject,
				"body":    body,
			},
		}
	}
	return []WorkflowDef{
		{
			// An invite goes to an ADDRESS: the person has no account, so
			// no subscriber, and no in-app inbox to land in.
			Name:              "quarry-invite",
			TriggerIdentifier: "quarry-invite",
			Steps: []WorkflowStep{
				email("S1", "{{payload.from}} invited you to quarry",
					"You have been invited to quarry as {{payload.name}}. Set your password here: {{payload.link}}"),
			},
		},
		{
			// What changed on the forge: one inbox item and a push per
			// announced changelog entry, to everyone with an account.
			Name:              "quarry-changelog",
			TriggerIdentifier: "quarry-changelog",
			Steps: []WorkflowStep{
				inApp("S1", "What changed in quarry: {{payload.title}}",
					"{{payload.from}} published a changelog entry — {{payload.link}}"),
				push("S2", "What changed in quarry", "{{payload.title}}"),
			},
		},
		{
			Name:              "quarry-mention",
			TriggerIdentifier: "quarry-mention",
			Steps: []WorkflowStep{
				inApp("S1", "{{payload.from}} mentioned you in {{payload.repo}}",
					"{{payload.repo}} — {{payload.text}}"),
				push("S2", "{{payload.from}} mentioned you",
					"{{payload.repo}} — {{payload.text}}"),
			},
		},
		{
			Name:              "quarry-forum-reply",
			TriggerIdentifier: "quarry-forum-reply",
			Steps: []WorkflowStep{
				inApp("S1", "{{payload.from}} replied in {{payload.title}}",
					"{{payload.repo}} — {{payload.text}}"),
				push("S2", "{{payload.from}} replied in {{payload.title}}",
					"{{payload.repo}} — {{payload.text}}"),
			},
		},
		{
			// The one workflow with a digest: activity is a stream of
			// small things, and a phone that buzzes per check-in is a
			// phone somebody turns off. Fifteen minutes is RFC 0011 §6's
			// window; the digest runs BEFORE the in_app step, so what
			// lands in the inbox is the batch rather than each event.
			Name:              "quarry-activity",
			TriggerIdentifier: "quarry-activity",
			Steps: []WorkflowStep{
				{
					StepID: "S1",
					Type:   "digest",
					ControlValues: map[string]any{
						"type":   "regular",
						"amount": 15,
						"unit":   "minutes",
					},
				},
				inApp("S2", "{{payload.summary}}", "{{payload.repo}} — by {{payload.actor}}"),
			},
		},
		{
			Name:              "quarry-landing",
			TriggerIdentifier: "quarry-landing",
			Steps: []WorkflowStep{
				inApp("S1", "{{payload.summary}}", "{{payload.repo}} — {{payload.link}}"),
				push("S2", "{{payload.summary}}", "{{payload.repo}}"),
			},
		},
		{
			Name:              "quarry-error",
			TriggerIdentifier: "quarry-error",
			Steps: []WorkflowStep{inApp("S1",
				"quarry error on {{payload.surface}}",
				"{{payload.path}} — {{payload.summary}}",
			)},
		},
		{
			// A push a person asks for, to find out whether push
			// reaches this device at all. Push ONLY: a test that also
			// wrote to the inbox would make the inbox worse every time
			// somebody checked their phone.
			Name:              "quarry-test",
			TriggerIdentifier: "quarry-test",
			Steps: []WorkflowStep{push("S1", "quarry",
				"Test notification — push reaches this device.")},
		},
		{
			Name:              "quarry-slow",
			TriggerIdentifier: "quarry-slow",
			Steps: []WorkflowStep{inApp("S1",
				"slow render: {{payload.path}}",
				"{{payload.summary}}",
			)},
		},
	}
}