mesh-sdk holds neither asks nor asker, nor the vault's answer types, and mesh-delivery's sdk/views is mesh-sdk's go/views (a1-progress.md, the rewrite map for the join): thirteen modules switch to the operatorchannel contract and client, mesh-sdk's channel package and meshvault, with go.mod requiring each at the group pin (nats: meshbroker too). The gate holds the checks mesh-sdk dropped (an ask's id is its key's asking, at least one option), with a row that fails without them; claude-code refuses a router record whose times do not parse, as its JSON decode did, with a row. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
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/channel"
|
|
)
|
|
|
|
// 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 channel.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()
|
|
}
|