Files
remembos/internal/telegram/format.go
T
av 1905e7ab16
release / docker-image (push) Successful in 1m9s
release / goreleaser (push) Successful in 10m13s
fix long text in telegram
2026-02-12 20:36:37 +03:00

165 lines
3.8 KiB
Go

package telegram
import (
"fmt"
"strings"
"time"
"git.vakhrushev.me/av/remembos/internal/memos"
"git.vakhrushev.me/av/remembos/internal/search"
)
const (
maxMessageLen = 4096
maxCaptionLen = 1024
)
// formatMemory returns the full message text formatted as HTML for Telegram.
// The text is NOT truncated — the caller is responsible for splitting if needed.
func formatMemory(mem *search.Memory, publicURL string) string {
var b strings.Builder
// Header: date and "ago" text
b.WriteString(fmt.Sprintf("<b>%s</b>", formatDate(mem.Date)))
if ago := agoText(mem.Date); ago != "" {
b.WriteString(fmt.Sprintf(" <i>(%s)</i>", ago))
}
b.WriteString("\n\n")
// Content
b.WriteString(escapeHTML(mem.Memo.Content))
// Tags
if len(mem.Memo.Tags) > 0 {
b.WriteString("\n\n")
tags := make([]string, len(mem.Memo.Tags))
for i, t := range mem.Memo.Tags {
tags[i] = "#" + t
}
b.WriteString(strings.Join(tags, " "))
}
// Link to original
memoURL := fmt.Sprintf("%s/%s", publicURL, mem.Memo.Name)
b.WriteString("\n\n<a href=\"" + memoURL + "\">Оригинал</a>")
return b.String()
}
// imageAttachments returns image attachments from the memo.
func imageAttachments(memo *memos.Memo) []memos.Attachment {
var images []memos.Attachment
for _, att := range memo.Attachments {
if att.IsImage() {
images = append(images, att)
}
}
return images
}
// splitText splits text into chunks of at most maxLen bytes,
// breaking at paragraph (\n\n), line (\n), or word boundaries.
func splitText(text string, maxLen int) []string {
if len(text) <= maxLen {
return []string{text}
}
var parts []string
remaining := text
for len(remaining) > maxLen {
chunk := remaining[:maxLen]
// Try to break at a paragraph boundary
idx := strings.LastIndex(chunk, "\n\n")
if idx <= 0 {
// Try a line boundary
idx = strings.LastIndex(chunk, "\n")
}
if idx <= 0 {
// Try a word boundary
idx = strings.LastIndexByte(chunk, ' ')
}
if idx <= 0 {
// Hard cut as last resort
idx = maxLen
}
parts = append(parts, remaining[:idx])
remaining = strings.TrimLeft(remaining[idx:], "\n ")
}
if remaining != "" {
parts = append(parts, remaining)
}
return parts
}
// escapeHTML escapes special HTML characters for Telegram HTML parse mode.
func escapeHTML(s string) string {
s = strings.ReplaceAll(s, "&", "&amp;")
s = strings.ReplaceAll(s, "<", "&lt;")
s = strings.ReplaceAll(s, ">", "&gt;")
return s
}
var months = [...]string{
1: "января", 2: "февраля", 3: "марта",
4: "апреля", 5: "мая", 6: "июня",
7: "июля", 8: "августа", 9: "сентября",
10: "октября", 11: "ноября", 12: "декабря",
}
func formatDate(t time.Time) string {
return fmt.Sprintf("%d %s %d", t.Day(), months[t.Month()], t.Year())
}
func agoText(t time.Time) string {
now := time.Now()
years := now.Year() - t.Year()
monthsDiff := int(now.Month()) - int(t.Month())
if monthsDiff < 0 {
years--
monthsDiff += 12
}
if years > 0 {
return fmt.Sprintf("%s назад", pluralYears(years))
}
if monthsDiff > 0 {
return fmt.Sprintf("%s назад", pluralMonths(monthsDiff))
}
return ""
}
func pluralYears(n int) string {
mod10 := n % 10
mod100 := n % 100
switch {
case mod100 >= 11 && mod100 <= 14:
return fmt.Sprintf("%d лет", n)
case mod10 == 1:
return fmt.Sprintf("%d год", n)
case mod10 >= 2 && mod10 <= 4:
return fmt.Sprintf("%d года", n)
default:
return fmt.Sprintf("%d лет", n)
}
}
func pluralMonths(n int) string {
mod10 := n % 10
mod100 := n % 100
switch {
case mod100 >= 11 && mod100 <= 14:
return fmt.Sprintf("%d месяцев", n)
case mod10 == 1:
return fmt.Sprintf("%d месяц", n)
case mod10 >= 2 && mod10 <= 4:
return fmt.Sprintf("%d месяца", n)
default:
return fmt.Sprintf("%d месяцев", n)
}
}