mesh/merge-gate head b7d9590fdc95 merged into trunk e1c8965ddbf1 — pass: builds messenger, telegram → novox; no bus step; every machine composes with the c…
mesh/repo-check head b7d9590fdc95 merged into trunk e1c8965ddbf1 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
Issue 372's fix with issue 639's one line edited an answered ask down to 'You chose X at HH:MM · headline', so the operator could not read back what they approved. The headline stays the title, the summary, points, link and Details stay the body, and how it ended and who acts on it become the note. Telegram cuts the body from its end past 4096 characters, never the title or the note.
95 lines
3.4 KiB
Go
95 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, an answered ask's too (issue 714);
|
|
// - 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; once answered, how it ended
|
|
// and who acts on it — 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()
|
|
}
|