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:] }