add image conversion for telegram

This commit is contained in:
2026-02-13 09:37:54 +03:00
parent 1905e7ab16
commit e1ebb83641
5 changed files with 203 additions and 35 deletions
+105 -28
View File
@@ -11,6 +11,7 @@ import (
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
"git.vakhrushev.me/av/remembos/internal/config"
"git.vakhrushev.me/av/remembos/internal/media"
"git.vakhrushev.me/av/remembos/internal/memory"
"git.vakhrushev.me/av/remembos/internal/memos"
"git.vakhrushev.me/av/remembos/internal/search"
@@ -130,14 +131,22 @@ func (b *Bot) sendMemory(ctx context.Context, mem *search.Memory) {
}
text := formatMemory(mem, b.publicURL)
images := imageAttachments(mem.Memo)
imageAtts, videoAtts, audioAtts := mediaAttachments(mem.Memo)
var downloaded []imageFile
if len(images) > 0 {
downloaded = b.downloadImages(ctx, images)
var images []mediaFile
var skipped bool
if len(imageAtts) > 0 {
images, skipped = b.downloadAndCompressImages(ctx, imageAtts)
}
if err := b.sendWithRetry(ctx, text, downloaded); err != nil {
videos := b.downloadFiles(ctx, videoAtts)
audios := b.downloadFiles(ctx, audioAtts)
if skipped {
text += "\n\nПоказаны не все вложения"
}
if err := b.sendWithRetry(ctx, text, images, videos, audios); err != nil {
b.logger.Error("failed to send telegram message after retries", "error", err)
}
}
@@ -183,35 +192,57 @@ func (b *Bot) handleMore(ctx context.Context) {
b.sendMemory(ctx, mem)
}
type imageFile struct {
type mediaFile struct {
filename string
data []byte
}
// downloadImages downloads image attachments, skipping failures.
func (b *Bot) downloadImages(ctx context.Context, attachments []memos.Attachment) []imageFile {
files := make([]imageFile, 0, len(attachments))
// downloadAndCompressImages downloads image attachments and compresses them if needed.
// Returns downloaded files and whether any were skipped due to errors.
func (b *Bot) downloadAndCompressImages(ctx context.Context, attachments []memos.Attachment) ([]mediaFile, bool) {
files := make([]mediaFile, 0, len(attachments))
var skipped bool
for _, att := range attachments {
data, err := b.client.DownloadAttachment(ctx, att)
if err != nil {
b.logger.Warn("failed to download attachment, skipping", "name", att.Name, "error", err)
skipped = true
continue
}
data, filename, err := media.CompressImage(ctx, data, att.Filename)
if err != nil {
b.logger.Warn("failed to compress image, skipping", "name", att.Name, "error", err)
skipped = true
continue
}
files = append(files, mediaFile{filename: filename, data: data})
}
return files, skipped
}
// downloadFiles downloads attachments, skipping failures.
func (b *Bot) downloadFiles(ctx context.Context, attachments []memos.Attachment) []mediaFile {
files := make([]mediaFile, 0, len(attachments))
for _, att := range attachments {
data, err := b.client.DownloadAttachment(ctx, att)
if err != nil {
b.logger.Warn("failed to download attachment, skipping", "name", att.Name, "error", err)
continue
}
files = append(files, imageFile{
filename: att.Filename,
data: data,
})
files = append(files, mediaFile{filename: att.Filename, data: data})
}
return files
}
// sendWithRetry attempts to send the message with up to 3 retries.
func (b *Bot) sendWithRetry(ctx context.Context, text string, images []imageFile) error {
func (b *Bot) sendWithRetry(ctx context.Context, text string, images, videos, audios []mediaFile) error {
backoffs := []time.Duration{30 * time.Second, 60 * time.Second, 120 * time.Second}
var lastErr error
for attempt := range 3 {
lastErr = b.send(text, images)
lastErr = b.sendWithMedia(text, images, videos, audios)
if lastErr == nil {
return nil
}
@@ -228,31 +259,59 @@ func (b *Bot) sendWithRetry(ctx context.Context, text string, images []imageFile
return lastErr
}
// send executes the actual Telegram API calls based on the sending strategy.
// 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 {
// sendWithMedia executes the actual Telegram API calls for text, images, videos, and audios.
func (b *Bot) sendWithMedia(text string, images, videos, audios []mediaFile) error {
hasMedia := len(images) > 0 || len(videos) > 0 || len(audios) > 0
// Send text
switch {
case len(images) == 0:
case !hasMedia:
return b.sendTextParts(splitText(text, maxMessageLen))
case len(text) <= maxCaptionLen:
case len(text) <= maxCaptionLen && len(images) > 0:
// Short text — use as caption on image(s)
if len(images) == 1 {
return b.sendPhoto(images[0], text)
if err := b.sendPhoto(images[0], text); err != nil {
return err
}
} else {
if err := b.sendMediaGroup(images, text); err != nil {
return err
}
}
return b.sendMediaGroup(images, text)
default:
// Long text — send text messages first, then images without caption
// Long text or no images — send text first, then all media
if err := b.sendTextParts(splitText(text, maxMessageLen)); err != nil {
return err
}
// Send images without caption
if len(images) == 1 {
return b.sendPhoto(images[0], "")
if err := b.sendPhoto(images[0], ""); err != nil {
return err
}
} else if len(images) > 1 {
if err := b.sendMediaGroup(images, ""); err != nil {
return err
}
}
return b.sendMediaGroup(images, "")
}
// Send videos one by one
for _, v := range videos {
if err := b.sendVideo(v); err != nil {
return err
}
}
// Send audios one by one
for _, a := range audios {
if err := b.sendAudio(a); err != nil {
return err
}
}
return nil
}
func (b *Bot) sendTextParts(parts []string) error {
@@ -272,7 +331,7 @@ func (b *Bot) sendText(text string) error {
return err
}
func (b *Bot) sendPhoto(img imageFile, caption string) error {
func (b *Bot) sendPhoto(img mediaFile, caption string) error {
photo := tgbotapi.NewPhoto(b.chatID, tgbotapi.FileBytes{
Name: img.filename,
Bytes: img.data,
@@ -285,7 +344,25 @@ func (b *Bot) sendPhoto(img imageFile, caption string) error {
return err
}
func (b *Bot) sendMediaGroup(images []imageFile, caption string) error {
func (b *Bot) sendVideo(v mediaFile) error {
video := tgbotapi.NewVideo(b.chatID, tgbotapi.FileBytes{
Name: v.filename,
Bytes: v.data,
})
_, err := b.api.Send(video)
return err
}
func (b *Bot) sendAudio(a mediaFile) error {
audio := tgbotapi.NewAudio(b.chatID, tgbotapi.FileBytes{
Name: a.filename,
Bytes: a.data,
})
_, err := b.api.Send(audio)
return err
}
func (b *Bot) sendMediaGroup(images []mediaFile, caption string) error {
media := make([]interface{}, len(images))
for i, img := range images {
photo := tgbotapi.NewInputMediaPhoto(tgbotapi.FileBytes{