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
|
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
@@ -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.
|
||||||
|
|||||||
@@ -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})
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user