An ask reads at a glance on the phone: a headline, a summary and its points, the rest in a note (issues 383 and 639)
The operator would not read the Telegram ask for ADR 0343: thirty plain lines, every clause on one, no bold, no summary. A settings proposal was the same wall of text, its values masked so the approval was blind. - messenger: an ask's message is the asker's headline and explanation alone; who asked and when, what each answer does, the expiry, where else it is answered and the asker's Details travel as the message's note (asks.Message.Note). Answered, the message is one line, "You chose Approve at 16:06 on Telegram · <headline>". An explanation that does not read at a glance (asks.Glance) is shown and logged. On a kind that proves who answers, an ask's values are shown whole (asks.Ask.Whole), held to the secret rule alone (from the earlier work on issue 383, #178). - telegram: every message in HTML, never reworded: the title bold, a key line monospace, the note apart in a collapsed blockquote; markup escaped. A tap is compared by its words, whitespace aside, and a message shown before is still compared by the digest it was kept under. - desk-channel: the note follows the body. - mesh-knowledge: a record that needs the operator carries a brief (headline, summary, at most three points), the 30th per-record rule; its ask shows the brief and never the clauses. An ask made before is composed again as it was asked (form), so its warrant still holds. - claude-code: an instructions proposal's explanation is one summary line (the proposer's, or the module's), its text in the Details; a question's context is at most 300 characters and its recommendation one point. Older proposals and questions keep their form. The SDK is pinned at mesh-sdk 9588871 (fix/639-an-ask-reads-at-a-glance), which adds Message.Note and asks.Glance.
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
package main
|
||||
|
||||
// How a message looks in the chat (novox/hq issues 383 and 639, the operator's words: "no bold text, no
|
||||
// formatting", "too much text, no formatting, no summary"). The router's words are shown as given —
|
||||
// `exact-render` — and formatted, never reworded:
|
||||
//
|
||||
// - the title in bold: the headline, or how an answered ask ended;
|
||||
// - the body as it is, a line that is a key with its value (as a settings proposal lists them, "+ key: value")
|
||||
// in monospace;
|
||||
// - the note — who asked, what each answer does, the expiry, the asker's details — apart, after a blank line,
|
||||
// in a blockquote Telegram shows collapsed to its first lines, expanded on a tap.
|
||||
//
|
||||
// Every character Telegram's HTML would read as markup is escaped first, so a message carrying '<' neither
|
||||
// breaks nor injects. What the operator taps on is compared by its plain words (wordsDigest): Telegram gives a
|
||||
// message's text back without its formatting, so the digest is over the words before they are rendered.
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/asks"
|
||||
)
|
||||
|
||||
// ParseMode is the formatting the Bot API is told the text is in.
|
||||
const ParseMode = "HTML"
|
||||
|
||||
var keyLine = regexp.MustCompile(`^[+~=-] `)
|
||||
|
||||
// escape is a text with the characters Telegram's HTML reads as markup written as entities.
|
||||
func escape(s string) string {
|
||||
return strings.NewReplacer("&", "&", "<", "<", ">", ">").Replace(s)
|
||||
}
|
||||
|
||||
// shown is a message's three parts as the chat carries them.
|
||||
type shown struct{ title, body, note string }
|
||||
|
||||
// partsOf is a message's parts, cut so that its plain words fit MaxLength and the cut said (D1): the note is cut
|
||||
// first, then the body. The bound is on the words, as Telegram's is on the text once its markup is read.
|
||||
func partsOf(m asks.Message) shown {
|
||||
s := shown{title: strings.TrimSpace(m.Title), body: strings.TrimSpace(m.Body), note: strings.TrimSpace(m.Note)}
|
||||
over := utf8.RuneCountInString(s.plain()) - MaxLength
|
||||
if over <= 0 {
|
||||
return s
|
||||
}
|
||||
mark := utf8.RuneCountInString(cutNote)
|
||||
cut := func(text string, by int) string {
|
||||
r := []rune(text)
|
||||
keep := max(len(r)-by-mark, 0)
|
||||
return string(r[:keep]) + cutNote
|
||||
}
|
||||
if utf8.RuneCountInString(s.note) > over+mark {
|
||||
s.note = cut(s.note, over)
|
||||
return s
|
||||
}
|
||||
s.note = ""
|
||||
keep := max(MaxLength-utf8.RuneCountInString(s.title)-1-mark, 0)
|
||||
if r := []rune(s.body); keep < len(r) {
|
||||
s.body = string(r[:keep])
|
||||
}
|
||||
s.body += cutNote
|
||||
return s
|
||||
}
|
||||
|
||||
// plain is the words as Telegram gives them back: the title, the body on the lines after it, the note after a
|
||||
// blank line.
|
||||
func (s shown) plain() string {
|
||||
t := s.title
|
||||
if s.body != "" {
|
||||
t += "\n" + s.body
|
||||
}
|
||||
if s.note != "" {
|
||||
t += "\n\n" + s.note
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
// html is the same words as the chat shows them.
|
||||
func (s shown) html() string {
|
||||
var b strings.Builder
|
||||
b.WriteString("<b>" + escape(s.title) + "</b>")
|
||||
if s.body != "" {
|
||||
for _, line := range strings.Split(s.body, "\n") {
|
||||
b.WriteString("\n")
|
||||
if keyLine.MatchString(line) {
|
||||
b.WriteString("<code>" + escape(line) + "</code>")
|
||||
} else {
|
||||
b.WriteString(escape(line))
|
||||
}
|
||||
}
|
||||
}
|
||||
if s.note != "" {
|
||||
b.WriteString("\n\n<blockquote expandable>" + escape(s.note) + "</blockquote>")
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
Reference in New Issue
Block a user