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("" + escape(s.title) + "")
if s.body != "" {
for _, line := range strings.Split(s.body, "\n") {
b.WriteString("\n")
if keyLine.MatchString(line) {
b.WriteString("" + escape(line) + "")
} else {
b.WriteString(escape(line))
}
}
}
if s.note != "" {
b.WriteString("\n\n
" + escape(s.note) + "") } return b.String() }