fix long text in telegram
This commit is contained in:
+26
-20
@@ -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
@@ -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.
|
||||
|
||||
@@ -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})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user