fix long text in telegram
release / docker-image (push) Successful in 1m9s
release / goreleaser (push) Successful in 10m13s

This commit is contained in:
2026-02-12 20:36:37 +03:00
parent 868c90c896
commit 1905e7ab16
3 changed files with 63 additions and 41 deletions
+26 -20
View File
@@ -129,7 +129,7 @@ func (b *Bot) sendMemory(ctx context.Context, mem *search.Memory) {
return return
} }
mainText, captionText := formatMemory(mem, b.publicURL) text := formatMemory(mem, b.publicURL)
images := imageAttachments(mem.Memo) images := imageAttachments(mem.Memo)
var downloaded []imageFile var downloaded []imageFile
@@ -137,7 +137,7 @@ func (b *Bot) sendMemory(ctx context.Context, mem *search.Memory) {
downloaded = b.downloadImages(ctx, images) downloaded = b.downloadImages(ctx, images)
} }
if err := b.sendWithRetry(ctx, mainText, captionText, downloaded); err != nil { if err := b.sendWithRetry(ctx, text, downloaded); err != nil {
b.logger.Error("failed to send telegram message after retries", "error", err) b.logger.Error("failed to send telegram message after retries", "error", err)
} }
} }
@@ -206,12 +206,12 @@ func (b *Bot) downloadImages(ctx context.Context, attachments []memos.Attachment
} }
// sendWithRetry attempts to send the message with up to 3 retries. // sendWithRetry attempts to send the message with up to 3 retries.
func (b *Bot) sendWithRetry(ctx context.Context, mainText, captionText string, images []imageFile) error { func (b *Bot) sendWithRetry(ctx context.Context, text string, images []imageFile) error {
backoffs := []time.Duration{30 * time.Second, 60 * time.Second, 120 * time.Second} backoffs := []time.Duration{30 * time.Second, 60 * time.Second, 120 * time.Second}
var lastErr error var lastErr error
for attempt := range 3 { for attempt := range 3 {
lastErr = b.send(mainText, captionText, images) lastErr = b.send(text, images)
if lastErr == nil { if lastErr == nil {
return nil return nil
} }
@@ -229,35 +229,41 @@ func (b *Bot) sendWithRetry(ctx context.Context, mainText, captionText string, i
} }
// send executes the actual Telegram API calls based on the sending strategy. // send executes the actual Telegram API calls based on the sending strategy.
func (b *Bot) send(mainText, captionText string, images []imageFile) error { // Long text is split into multiple messages. Images are sent with a caption
// only if the full text fits within the caption limit.
func (b *Bot) send(text string, images []imageFile) error {
switch { switch {
case len(images) == 0: case len(images) == 0:
// No images — just text return b.sendTextParts(splitText(text, maxMessageLen))
return b.sendText(mainText)
case len(images) == 1 && len(captionText) <= maxCaptionLen: case len(text) <= maxCaptionLen:
// Single image with short caption // Short text — use as caption on image(s)
return b.sendPhoto(images[0], captionText) if len(images) == 1 {
return b.sendPhoto(images[0], text)
}
return b.sendMediaGroup(images, text)
case len(images) > 1 && len(captionText) <= maxCaptionLen: default:
// Multiple images with short caption // Long text — send text messages first, then images without caption
return b.sendMediaGroup(images, captionText) if err := b.sendTextParts(splitText(text, maxMessageLen)); err != nil {
case len(images) >= 1 && len(captionText) > maxCaptionLen:
// Images with long text — send text first, then images without caption
if err := b.sendText(mainText); err != nil {
return err return err
} }
if len(images) == 1 { if len(images) == 1 {
return b.sendPhoto(images[0], "") return b.sendPhoto(images[0], "")
} }
return b.sendMediaGroup(images, "") return b.sendMediaGroup(images, "")
default:
return b.sendText(mainText)
} }
} }
func (b *Bot) sendTextParts(parts []string) error {
for _, part := range parts {
if err := b.sendText(part); err != nil {
return err
}
}
return nil
}
func (b *Bot) sendText(text string) error { func (b *Bot) sendText(text string) error {
msg := tgbotapi.NewMessage(b.chatID, text) msg := tgbotapi.NewMessage(b.chatID, text)
msg.ParseMode = tgbotapi.ModeHTML msg.ParseMode = tgbotapi.ModeHTML
+35 -17
View File
@@ -14,9 +14,9 @@ const (
maxCaptionLen = 1024 maxCaptionLen = 1024
) )
// formatMemory returns (mainText, captionText) formatted as HTML for Telegram. // formatMemory returns the full message text formatted as HTML for Telegram.
// mainText is for sendMessage (up to 4096 chars), captionText for photo captions (up to 1024 chars). // The text is NOT truncated — the caller is responsible for splitting if needed.
func formatMemory(mem *search.Memory, publicURL string) (mainText, captionText string) { func formatMemory(mem *search.Memory, publicURL string) string {
var b strings.Builder var b strings.Builder
// Header: date and "ago" text // Header: date and "ago" text
@@ -43,10 +43,7 @@ func formatMemory(mem *search.Memory, publicURL string) (mainText, captionText s
memoURL := fmt.Sprintf("%s/%s", publicURL, mem.Memo.Name) memoURL := fmt.Sprintf("%s/%s", publicURL, mem.Memo.Name)
b.WriteString("\n\n<a href=\"" + memoURL + "\">Оригинал</a>") b.WriteString("\n\n<a href=\"" + memoURL + "\">Оригинал</a>")
full := b.String() return b.String()
mainText = truncateHTML(full, maxMessageLen)
captionText = truncateHTML(full, maxCaptionLen)
return mainText, captionText
} }
// imageAttachments returns image attachments from the memo. // imageAttachments returns image attachments from the memo.
@@ -60,22 +57,43 @@ func imageAttachments(memo *memos.Memo) []memos.Attachment {
return images return images
} }
// truncateHTML truncates text to maxLen, cutting at a word/line boundary and adding "...". // splitText splits text into chunks of at most maxLen bytes,
func truncateHTML(text string, maxLen int) string { // breaking at paragraph (\n\n), line (\n), or word boundaries.
func splitText(text string, maxLen int) []string {
if len(text) <= maxLen { if len(text) <= maxLen {
return text return []string{text}
} }
// Reserve space for "..." var parts []string
cut := maxLen - 3 remaining := text
// Find last newline or space before cut point for len(remaining) > maxLen {
idx := strings.LastIndexAny(text[:cut], "\n ") chunk := remaining[:maxLen]
if idx <= 0 {
idx = cut // 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 ")
} }
return text[:idx] + "..." if remaining != "" {
parts = append(parts, remaining)
}
return parts
} }
// escapeHTML escapes special HTML characters for Telegram HTML parse mode. // escapeHTML escapes special HTML characters for Telegram HTML parse mode.
+2 -4
View File
@@ -42,8 +42,7 @@ type Handler struct {
service *memory.Service service *memory.Service
logger *slog.Logger logger *slog.Logger
mux *http.ServeMux mux *http.ServeMux
memosURL string // internal Memos URL (for attachment files) publicURL string // public Memos URL (for images and memo links)
publicURL string // public Memos URL (for memo links)
allowLoadMore bool allowLoadMore bool
} }
@@ -54,7 +53,6 @@ func NewHandler(service *memory.Service, memosURL, publicURL string, allowLoadMo
} }
h := &Handler{ h := &Handler{
service: service, service: service,
memosURL: strings.TrimRight(memosURL, "/"),
publicURL: strings.TrimRight(pub, "/"), publicURL: strings.TrimRight(pub, "/"),
allowLoadMore: allowLoadMore, allowLoadMore: allowLoadMore,
logger: logger, logger: logger,
@@ -107,7 +105,7 @@ func (h *Handler) handleMemory(w http.ResponseWriter, r *http.Request) {
if att.ExternalLink != "" { if att.ExternalLink != "" {
imgURL = att.ExternalLink imgURL = att.ExternalLink
} else { } else {
imgURL = fmt.Sprintf("%s/file/%s/%s", h.memosURL, att.Name, att.Filename) imgURL = fmt.Sprintf("%s/file/%s/%s", h.publicURL, att.Name, att.Filename)
} }
images = append(images, imageData{URL: imgURL, Alt: att.Filename}) images = append(images, imageData{URL: imgURL, Alt: att.Filename})
} }