mesh/merge-gate pass: builds dunst, messenger → g14, novox, shanks; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery-group group fix/notifications-after-review ready: every member ready, and composed together they pass
mesh/delivery superseded: a newer head of the same pull request
Review found that any process of the operator's account can choose a dunst action, so a click could release or stop a delivery in the operator's name; that tokens lived for ever; that a warning's answers died in ten seconds; and that a restarted notifier left dead buttons (hq ADR 0258).
214 lines
6.3 KiB
Go
214 lines
6.3 KiB
Go
package main
|
|
|
|
// What a message says (novox/hq ADR 0253): **it reads in one glance.** A title in plain words, one or two
|
|
// plain sentences on what happened, what it means and whether to act, and when it began in the
|
|
// operator's own time. No key, id, commit, verb or markup: the condition's key and summary stay in the
|
|
// controller (`conditions`) and in this holder's history, for whoever looks closer.
|
|
//
|
|
// - The words are the condition's own: its headline, explanation and resolved line, made by the
|
|
// controller where the condition is made, so every channel says the same thing. A condition from a
|
|
// controller older than them, or a module's notice without a headline, is said from its summary,
|
|
// with what the operator cannot read taken out (plainSummary).
|
|
// - A clearance is one short line: "Resolved: openrazer works again on g14, after 26 min".
|
|
// - A reminder, an escalation and a reopening say so in the title, before the same headline.
|
|
// - Times are the operator's (the setting time-zone; this machine's zone when not given), and said as a
|
|
// clock time today, a weekday this week, a date before that.
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
func (h *Holder) zone() *time.Location {
|
|
if h.Zone != nil {
|
|
if loc := h.Zone(); loc != nil {
|
|
return loc
|
|
}
|
|
}
|
|
return time.Local
|
|
}
|
|
|
|
// compose is the message for a record. withhold > 0 when the content rule refused what it said: 1 says
|
|
// the headline alone, more says only that something was withheld.
|
|
func compose(r *Record, what string, now time.Time, loc *time.Location, withhold int) Message {
|
|
headline, explanation := wordsOf(r)
|
|
urgent := r.Severity == Urgent
|
|
if withhold >= 2 {
|
|
return withheld(r.Refused, urgent)
|
|
}
|
|
if withhold == 1 {
|
|
explanation = "Part of this message was withheld, because it carried " + r.Refused + "."
|
|
}
|
|
var title string
|
|
switch what {
|
|
case "cleared":
|
|
return Message{Title: resolvedLine(r) + ", after " + roughly(r.Cleared.Sub(r.Raised)), Quiet: true}
|
|
case "reminder":
|
|
title = "Still open: " + headline
|
|
case "quiet-reminder":
|
|
title = "Still open after a day: " + headline
|
|
case "escalated":
|
|
title = "Now urgent: " + headline
|
|
case "reopened":
|
|
title = headline + " (again" + times(r.Count) + ")"
|
|
if urgent {
|
|
title = "Urgent: " + title
|
|
}
|
|
default:
|
|
title = headline
|
|
if urgent {
|
|
title = "Urgent: " + headline
|
|
}
|
|
}
|
|
since := "Since " + when(r.Raised, now, loc)
|
|
if what == "reminder" || what == "quiet-reminder" {
|
|
since = fmt.Sprintf("Open since %s, for %s.", when(r.Raised, now, loc), roughly(now.Sub(r.Raised)))
|
|
} else {
|
|
since += "."
|
|
}
|
|
body := since
|
|
if explanation != "" {
|
|
body = explanation + "\n" + since
|
|
}
|
|
return Message{Title: title, Body: body, Urgent: urgent}
|
|
}
|
|
|
|
// withheld is what is sent in place of a message the content rule refused.
|
|
func withheld(carried string, urgent bool) Message {
|
|
return Message{Title: "A mesh message was withheld",
|
|
Body: "It carried " + carried + ", which may not leave the mesh. An agent can read it among the mesh's " +
|
|
"open conditions.", Urgent: urgent}
|
|
}
|
|
|
|
// times is " 2nd time" for a count of 2, "" for 1.
|
|
func times(n int) string {
|
|
if n < 2 {
|
|
return ""
|
|
}
|
|
suffix := "th"
|
|
switch {
|
|
case n%100 >= 11 && n%100 <= 13:
|
|
case n%10 == 1:
|
|
suffix = "st"
|
|
case n%10 == 2:
|
|
suffix = "nd"
|
|
case n%10 == 3:
|
|
suffix = "rd"
|
|
}
|
|
return fmt.Sprintf(", %d%s time", n, suffix)
|
|
}
|
|
|
|
// wordsOf is a record's headline and explanation: its own, or made from its summary.
|
|
func wordsOf(r *Record) (string, string) {
|
|
if r.Headline != "" {
|
|
return r.Headline, r.Explanation
|
|
}
|
|
summary := plainSummary(r.Summary)
|
|
if r.Origin == "notify" && len(summary) <= 80 {
|
|
return capital(summary), ""
|
|
}
|
|
return fallbackHeadline(r), capital(summary)
|
|
}
|
|
|
|
// resolvedLine is the line a clearance says: the condition's own, or made from its headline.
|
|
func resolvedLine(r *Record) string {
|
|
line := r.Resolved
|
|
if line == "" {
|
|
headline, _ := wordsOf(r)
|
|
line = "Resolved: " + lowerFirst(headline)
|
|
}
|
|
if !strings.HasPrefix(line, "Resolved") {
|
|
line = "Resolved: " + lowerFirst(line)
|
|
}
|
|
return line
|
|
}
|
|
|
|
// fallbackHeadline is a title for something with no headline: what it is about and its kind, in words.
|
|
func fallbackHeadline(r *Record) string {
|
|
what := strings.ReplaceAll(r.Kind, "-", " ")
|
|
if what == "" || what == "notice" {
|
|
what = "a notice"
|
|
}
|
|
about := ""
|
|
if r.Origin == "notify" {
|
|
about = r.Subject
|
|
} else if machine := machineOf(r.Subject); machine != "" {
|
|
about = machine
|
|
} else {
|
|
// A seat, a provider or a module is named by a name; a plan, a delivery or a call by an id, never said.
|
|
for _, scope := range []string{"seat ", "provider ", "module "} {
|
|
if name, ok := strings.CutPrefix(r.Subject, scope); ok {
|
|
about = name
|
|
}
|
|
}
|
|
}
|
|
if about == "" {
|
|
return capital(what)
|
|
}
|
|
return capital(what) + " on " + about
|
|
}
|
|
|
|
// machineOf is the machine a record's subject words name: "machine ace" or "x on ace".
|
|
func machineOf(subject string) string {
|
|
if _, on, ok := strings.Cut(subject, " on "); ok {
|
|
return on
|
|
}
|
|
if m, ok := strings.CutPrefix(subject, "machine "); ok {
|
|
return m
|
|
}
|
|
return ""
|
|
}
|
|
|
|
var (
|
|
codeSpan = regexp.MustCompile("`([^`]*)`")
|
|
noise = regexp.MustCompile(`\s+`)
|
|
)
|
|
|
|
// plainSummary is a summary with what the operator cannot read taken out: markup and the verbs in code
|
|
// spans. Used only where the condition says no plain words of its own.
|
|
func plainSummary(s string) string {
|
|
s = codeSpan.ReplaceAllStringFunc(s, func(m string) string {
|
|
inner := strings.Trim(m, "`")
|
|
if strings.ContainsAny(inner, ". ") {
|
|
return "a command"
|
|
}
|
|
return inner
|
|
})
|
|
s = strings.NewReplacer("**", "", "`", "").Replace(s)
|
|
return strings.TrimSpace(noise.ReplaceAllString(s, " "))
|
|
}
|
|
|
|
// when is a moment as the operator reads it, in their zone: "10:40" today, "Tue 10:40" this week,
|
|
// "1 Oct 10:40" before.
|
|
func when(t, now time.Time, loc *time.Location) string {
|
|
if t.IsZero() {
|
|
return "now"
|
|
}
|
|
lt, ln := t.In(loc), now.In(loc)
|
|
y1, m1, d1 := lt.Date()
|
|
y2, m2, d2 := ln.Date()
|
|
switch {
|
|
case y1 == y2 && m1 == m2 && d1 == d2:
|
|
return lt.Format("15:04")
|
|
case ln.Sub(lt) < 6*24*time.Hour:
|
|
return lt.Format("Mon 15:04")
|
|
}
|
|
return lt.Format("2 Jan 15:04")
|
|
}
|
|
|
|
func capital(s string) string {
|
|
if s == "" {
|
|
return s
|
|
}
|
|
return strings.ToUpper(s[:1]) + s[1:]
|
|
}
|
|
|
|
func lowerFirst(s string) string {
|
|
if s == "" || (len(s) > 1 && strings.ToUpper(s[:2]) == s[:2]) {
|
|
return s
|
|
}
|
|
return strings.ToLower(s[:1]) + s[1:]
|
|
}
|