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() }