Files
mesh-catalog/modules/messenger/cmd/messenger/words.go
T
jochen d83711f11e
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
Take only acknowledgements from a desk click, and make each answer a one-time ticket
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).
2026-10-08 14:38:24 +02:00

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