Files
mesh-catalog/modules/messenger/cmd/messenger/words.go
T
jochen 8dfc19c9f7
mesh/merge-gate pass: builds desk-channel, messenger, telegram → novox; no bus step; every machine composes with the change as it did without (4 of 4 compo…
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery superseded: its walk was superseded and names no walk that took it over: a walk that took it over answers its merge, and it follows that wa…
mesh/delivery-group group fix/373-one-message-per-condition stopped: superseded, not carried by a later delivery: novox/mesh-catalog@8dfc19c9f7c8
A silence is said on the shown message, which then times out (issue 373)
Seen at 14:40: machine.shanks.units was silenced for 7 days with a reason,
and its critical popup stood on the desk as "Still open" while the
router logged nothing. A silence now edits the message where it is shown
to "Silenced until <day>: <headline>" with the reason, no answers and no
urgency, so the desk's popup times out; nothing more is said while it
lasts. When the silence ends and the condition is still open, the message
is edited back to its open words. Resolved while silenced, it is said only
where an edit notifies nobody. The silence is taken from the controller's
event and from the 10-min reading of the open set alike.
2026-10-10 14:32:35 +02:00

238 lines
7.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 "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
}
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")
}
// 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:]
}