mesh/merge-gate head bb5a342863ee merged into trunk 9c166398f204 — pass: builds claude-code, desk-channel, mesh-knowledge, messenger, telegram → ace, g14, …
mesh/repo-check head bb5a342863ee merged into trunk 9c166398f204 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery-group group fix/383-an-ask-reads-at-a-glance checking: 1 of 2 member(s) ready
mesh/delivery superseded: a newer head of the same pull request
The review of mesh-controller #304: a proposal of more than three keys showed only the names of the rest, and its Details could carry no value, so Approve bound values the operator never saw. The router now shows an ask's WholeDetails in the note in place of its Details on a kind that proves who answers, held to the secret rule alone, and refuses an ask whose whole details carry a secret's shape. Telegram keeps the note whole when a message is too long, cutting the body first and saying so, and escapes the note as every other part. SDK at mesh-sdk f474544.
94 lines
3.4 KiB
Go
94 lines
3.4 KiB
Go
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 body is cut
|
|
// first: the note carries what each answer does and every value an approval sets, so it is kept whole as long as
|
|
// the title and the note fit; only then is the note cut, and that said too. 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)
|
|
if r := []rune(s.body); len(r) >= over+mark {
|
|
s.body = string(r[:len(r)-over-mark]) + cutNote
|
|
return s
|
|
}
|
|
// The body cannot make room alone: what is left of it is the cut's word, and the note is cut too.
|
|
s.body = strings.TrimPrefix(cutNote, "\n")
|
|
if over = utf8.RuneCountInString(s.plain()) - MaxLength; over > 0 {
|
|
r := []rune(s.note)
|
|
s.note = string(r[:max(len(r)-over-mark, 0)]) + 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()
|
|
}
|