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("%s", formatDate(mem.Date))) if ago := agoText(mem.Date); ago != "" { b.WriteString(fmt.Sprintf(" (%s)", ago)) } b.WriteString("\n\n") // Content b.WriteString(escapeHTML(mem.Memo.Content)) // Link to original memoURL := fmt.Sprintf("%s/%s", publicURL, mem.Memo.Name) b.WriteString("\n\nОригинал") return b.String() } // mediaAttachments splits memo attachments into images, videos, and audios. func mediaAttachments(memo *memos.Memo) (images, videos, audios []memos.Attachment) { for _, att := range memo.Attachments { switch { case att.IsImage(): images = append(images, att) case att.IsVideo(): videos = append(videos, att) case att.IsAudio(): audios = append(audios, att) } } return } // 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, "&", "&") s = strings.ReplaceAll(s, "<", "<") s = strings.ReplaceAll(s, ">", ">") 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) } }