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 (mainText, captionText) formatted as HTML for Telegram. // mainText is for sendMessage (up to 4096 chars), captionText for photo captions (up to 1024 chars). func formatMemory(mem *search.Memory, publicURL string) (mainText, captionText 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)) // 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Оригинал") full := b.String() mainText = truncateHTML(full, maxMessageLen) captionText = truncateHTML(full, maxCaptionLen) return mainText, captionText } // 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 } // truncateHTML truncates text to maxLen, cutting at a word/line boundary and adding "...". func truncateHTML(text string, maxLen int) string { if len(text) <= maxLen { return text } // Reserve space for "..." cut := maxLen - 3 // Find last newline or space before cut point idx := strings.LastIndexAny(text[:cut], "\n ") if idx <= 0 { idx = cut } return text[:idx] + "..." } // 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) } }