package telegram import ( "encoding/base64" "fmt" "html" "strconv" "strings" ) const TelegramMessageLimit = 4096 const TelegramHTMLMessageLimit = 3900 func EscapeHTML(text string) string { return html.EscapeString(text) } func SummaryDetailsHTML(summary, details string) string { summary = strings.TrimSpace(summary) details = strings.TrimSpace(details) if details == "" { return EscapeHTML(summary) } if summary == "" { return ExpandableQuoteHTML(details) } return EscapeHTML(summary) + "\n" + ExpandableQuoteHTML(details) } func ExpandableQuoteHTML(text string) string { text = strings.TrimSpace(text) if text == "" { return "" } return "
" + EscapeHTML(text) + "" } func SummaryDetailsHTMLLimited(summary, details string, limit int) string { if limit <= 0 { limit = TelegramHTMLMessageLimit } summary = strings.TrimSpace(summary) details = strings.TrimSpace(details) out := SummaryDetailsHTML(summary, details) if len([]rune(out)) <= limit || details == "" { return out } suffix := "\n...[truncated]" runes := []rune(details) for len(runes) > 0 { candidateLen := len(runes) - max(1, (len([]rune(out))-limit)/2) if candidateLen < 0 { candidateLen = 0 } if candidateLen > len(runes) { candidateLen = len(runes) } candidate := strings.TrimSpace(string(runes[:candidateLen])) + suffix out = SummaryDetailsHTML(summary, candidate) if len([]rune(out)) <= limit || candidateLen == 0 { return out } runes = runes[:candidateLen] } return SummaryDetailsHTML(summary, suffix) } func ChunkText(text string, max int) []string { if max <= 0 { max = TelegramMessageLimit } runes := []rune(text) if len(runes) == 0 { return nil } var chunks []string for len(runes) > max { cut := max for i := max; i > max/2; i-- { if runes[i-1] == '\n' { cut = i break } } chunks = append(chunks, string(runes[:cut])) runes = runes[cut:] } if len(runes) > 0 { chunks = append(chunks, string(runes)) } return chunks } func ApprovalCallbackData(id int64, decision string) string { return fmt.Sprintf("approval:%d:%s", id, decision) } func ParseApprovalCallbackData(data string) (int64, string, bool) { parts := strings.Split(data, ":") if len(parts) != 3 || parts[0] != "approval" { return 0, "", false } id, err := strconv.ParseInt(parts[1], 10, 64) if err != nil || id <= 0 { return 0, "", false } switch parts[2] { case "accept", "acceptForSession", "decline", "cancel", "details": return id, parts[2], true default: return 0, "", false } } func WorkspaceCallbackData(id int64) string { return fmt.Sprintf("workspace:%d", id) } func ParseWorkspaceCallbackData(data string) (int64, bool) { if !strings.HasPrefix(data, "workspace:") { return 0, false } id, err := strconv.ParseInt(strings.TrimPrefix(data, "workspace:"), 10, 64) return id, err == nil && id > 0 } func ResumeThreadCallbackData(id int64) string { return fmt.Sprintf("resume:thread:%d", id) } func ParseResumeThreadCallbackData(data string) (int64, bool) { if !strings.HasPrefix(data, "resume:thread:") { return 0, false } id, err := strconv.ParseInt(strings.TrimPrefix(data, "resume:thread:"), 10, 64) return id, err == nil && id > 0 } func ResumePageCallbackData(page int) string { return fmt.Sprintf("resume:page:%d", page) } func ParseResumePageCallbackData(data string) (int, bool) { if !strings.HasPrefix(data, "resume:page:") { return 0, false } page, err := strconv.Atoi(strings.TrimPrefix(data, "resume:page:")) return page, err == nil && page >= 0 } func ModelCallbackData(modelID string) (string, bool) { encoded := base64.RawURLEncoding.EncodeToString([]byte(modelID)) data := "model:" + encoded return data, len([]rune(data)) <= 64 } func ParseModelCallbackData(data string) (string, bool) { if !strings.HasPrefix(data, "model:") { return "", false } decoded, err := base64.RawURLEncoding.DecodeString(strings.TrimPrefix(data, "model:")) if err != nil || len(decoded) == 0 { return "", false } return string(decoded), true } func EffortCallbackData(effort string) string { encoded := base64.RawURLEncoding.EncodeToString([]byte(effort)) return "effort:" + encoded } func ParseEffortCallbackData(data string) (string, bool) { if !strings.HasPrefix(data, "effort:") { return "", false } decoded, err := base64.RawURLEncoding.DecodeString(strings.TrimPrefix(data, "effort:")) if err != nil || len(decoded) == 0 { return "", false } return string(decoded), true }