1 Commits

Author SHA1 Message Date
av 1905e7ab16 fix long text in telegram
release / docker-image (push) Successful in 1m9s
release / goreleaser (push) Successful in 10m13s
2026-02-12 20:36:37 +03:00
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
}
mainText, captionText := formatMemory(mem, b.publicURL)
text := formatMemory(mem, b.publicURL)
images := imageAttachments(mem.Memo)
var downloaded []imageFile
@@ -137,7 +137,7 @@ func (b *Bot) sendMemory(ctx context.Context, mem *search.Memory) {
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)
}
}
@@ -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.
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}
var lastErr error
for attempt := range 3 {
lastErr = b.send(mainText, captionText, images)
lastErr = b.send(text, images)
if lastErr == 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.
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 {
case len(images) == 0:
// No images — just text
return b.sendText(mainText)
return b.sendTextParts(splitText(text, maxMessageLen))
case len(images) == 1 && len(captionText) <= maxCaptionLen:
// Single image with short caption
return b.sendPhoto(images[0], captionText)
case len(text) <= maxCaptionLen:
// Short text — use as caption on image(s)
if len(images) == 1 {
return b.sendPhoto(images[0], text)
}
return b.sendMediaGroup(images, text)
case len(images) > 1 && len(captionText) <= maxCaptionLen:
// Multiple images with short caption
return b.sendMediaGroup(images, captionText)
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 {
default:
// Long text — send text messages first, then images without caption
if err := b.sendTextParts(splitText(text, maxMessageLen)); err != nil {
return err
}
if len(images) == 1 {
return b.sendPhoto(images[0], "")
}
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 {
msg := tgbotapi.NewMessage(b.chatID, text)
msg.ParseMode = tgbotapi.ModeHTML
+35 -17
View File
@@ -14,9 +14,9 @@ const (
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) {
// 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
@@ -43,10 +43,7 @@ func formatMemory(mem *search.Memory, publicURL string) (mainText, captionText s
memoURL := fmt.Sprintf("%s/%s", publicURL, mem.Memo.Name)
b.WriteString("\n\n<a href=\"" + memoURL + "\">Оригинал</a>")
full := b.String()
mainText = truncateHTML(full, maxMessageLen)
captionText = truncateHTML(full, maxCaptionLen)
return mainText, captionText
return b.String()
}
// imageAttachments returns image attachments from the memo.
@@ -60,22 +57,43 @@ func imageAttachments(memo *memos.Memo) []memos.Attachment {
return images
}
// truncateHTML truncates text to maxLen, cutting at a word/line boundary and adding "...".
func truncateHTML(text string, maxLen int) string {
// 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 text
return []string{text}
}
// Reserve space for "..."
cut := maxLen - 3
var parts []string
remaining := text
// Find last newline or space before cut point
idx := strings.LastIndexAny(text[:cut], "\n ")
if idx <= 0 {
idx = cut
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 ")
}
return text[:idx] + "..."
if remaining != "" {
parts = append(parts, remaining)
}
return parts
}
// escapeHTML escapes special HTML characters for Telegram HTML parse mode.
+2 -4
View File
@@ -42,8 +42,7 @@ type Handler struct {
service *memory.Service
logger *slog.Logger
mux *http.ServeMux
memosURL string // internal Memos URL (for attachment files)
publicURL string // public Memos URL (for memo links)
publicURL string // public Memos URL (for images and memo links)
allowLoadMore bool
}
@@ -54,7 +53,6 @@ func NewHandler(service *memory.Service, memosURL, publicURL string, allowLoadMo
}
h := &Handler{
service: service,
memosURL: strings.TrimRight(memosURL, "/"),
publicURL: strings.TrimRight(pub, "/"),
allowLoadMore: allowLoadMore,
logger: logger,
@@ -107,7 +105,7 @@ func (h *Handler) handleMemory(w http.ResponseWriter, r *http.Request) {
if att.ExternalLink != "" {
imgURL = att.ExternalLink
} 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})
}