Files
mesh-catalog/modules/messenger/cmd/messenger/words.go
T

249 lines
7.9 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 "silenced":
// Said in place of the open message: no urgency, so the desk's popup times out, and no answers.
body := "Nothing more is said of it until then."
if r.SilencedWhy != "" {
body = "Because: " + strings.TrimSuffix(strings.TrimSpace(r.SilencedWhy), ".") + ". " + body
}
return Message{Title: "Silenced until " + by(r.SilencedTill, now, loc) + ": " + headline, Body: body, Quiet: true}
case "reopened":
title = headline + " (again" + times(r.Count) + ")"
if urgent {
title = "Urgent: " + title
}
case "tell":
// A plain notice (ADR 0380 §1, §8): the headline and its one or two lines, nothing else — no
// "since", no severity, no reminder: it waits for no answer and is never said again.
return Message{Title: headline, Body: explanation}
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 r.Origin == "ask" && what != "reminder" && what != "quiet-reminder" {
// An ask's own message (issue 639): its body is the asker's summary and points; when it was asked is in
// its note, with who asked.
body = ""
}
if explanation != "" && body == "" {
body = explanation
} else 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")
}
// by is a moment ahead as the operator reads it, in their zone: "23:33" today, "tomorrow 23:33", "Fri 16 Oct
// 23:33" later. An ask's expiry was said as a clock time alone, so an ask lasting a week read as one ending
// tonight (issue 373).
func by(t, now time.Time, loc *time.Location) string {
lt, ln := t.In(loc), now.In(loc)
y1, m1, d1 := lt.Date()
y2, m2, d2 := ln.Date()
if y1 == y2 && m1 == m2 && d1 == d2 {
return lt.Format("15:04")
}
ty, tm, td := ln.AddDate(0, 0, 1).Date()
if y1 == ty && m1 == tm && d1 == td {
return "tomorrow " + lt.Format("15:04")
}
return lt.Format("Mon 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:]
}