feat(telegram): add outbound media support for sending images and files
Add Media field to OutboundMessage and update Telegram channel Send() to handle image/file attachments via SendPhoto and SendDocument. - Stream local files directly without buffering in memory - URL media delegated to Telegram server-side download - Image detection via file extension (.jpg/.png/.gif/.webp) - Fallback to text-only message if all media sends fail - Delete placeholder instead of editing for media messages
This commit is contained in:
parent
0870815af2
commit
869acc2932
2 changed files with 111 additions and 3 deletions
|
|
@ -11,9 +11,10 @@ type InboundMessage struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type OutboundMessage struct {
|
type OutboundMessage struct {
|
||||||
Channel string `json:"channel"`
|
Channel string `json:"channel"`
|
||||||
ChatID string `json:"chat_id"`
|
ChatID string `json:"chat_id"`
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
|
Media []string `json:"media,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type MessageHandler func(InboundMessage) error
|
type MessageHandler func(InboundMessage) error
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
@ -164,6 +165,19 @@ func (c *TelegramChannel) Send(ctx context.Context, msg bus.OutboundMessage) err
|
||||||
c.stopThinking.Delete(msg.ChatID)
|
c.stopThinking.Delete(msg.ChatID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If media is present, send media (skip placeholder edit — can't edit text into photo)
|
||||||
|
if len(msg.Media) > 0 {
|
||||||
|
// Delete placeholder instead of editing it
|
||||||
|
if pID, ok := c.placeholders.Load(msg.ChatID); ok {
|
||||||
|
c.placeholders.Delete(msg.ChatID)
|
||||||
|
_ = c.bot.DeleteMessage(ctx, &telego.DeleteMessageParams{
|
||||||
|
ChatID: tu.ID(chatID),
|
||||||
|
MessageID: pID.(int),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return c.sendMedia(ctx, chatID, msg)
|
||||||
|
}
|
||||||
|
|
||||||
htmlContent := markdownToTelegramHTML(msg.Content)
|
htmlContent := markdownToTelegramHTML(msg.Content)
|
||||||
|
|
||||||
// Try to edit placeholder
|
// Try to edit placeholder
|
||||||
|
|
@ -193,6 +207,99 @@ func (c *TelegramChannel) Send(ctx context.Context, msg bus.OutboundMessage) err
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sendMedia sends media files (images/documents) to a Telegram chat.
|
||||||
|
// For images (.jpg/.jpeg/.png/.gif/.webp), uses SendPhoto.
|
||||||
|
// For all other file types, uses SendDocument.
|
||||||
|
// Supports both local file paths and URLs. Files are streamed, not buffered.
|
||||||
|
func (c *TelegramChannel) sendMedia(ctx context.Context, chatID int64, msg bus.OutboundMessage) error {
|
||||||
|
caption := msg.Content
|
||||||
|
var firstErr error
|
||||||
|
|
||||||
|
for i, mediaPath := range msg.Media {
|
||||||
|
// Only first media gets the caption
|
||||||
|
itemCaption := ""
|
||||||
|
if i == 0 {
|
||||||
|
itemCaption = caption
|
||||||
|
}
|
||||||
|
|
||||||
|
err := c.sendSingleMedia(ctx, chatID, mediaPath, itemCaption)
|
||||||
|
if err != nil {
|
||||||
|
logger.ErrorCF("telegram", "Failed to send media", map[string]any{
|
||||||
|
"path": mediaPath,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
if firstErr == nil {
|
||||||
|
firstErr = err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If all media failed and we have text content, fall back to text
|
||||||
|
if firstErr != nil && caption != "" {
|
||||||
|
tgMsg := tu.Message(tu.ID(chatID), markdownToTelegramHTML(caption))
|
||||||
|
tgMsg.ParseMode = telego.ModeHTML
|
||||||
|
if _, err := c.bot.SendMessage(ctx, tgMsg); err != nil {
|
||||||
|
tgMsg.ParseMode = ""
|
||||||
|
_, _ = c.bot.SendMessage(ctx, tgMsg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return firstErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendSingleMedia sends one media file. It determines the input source (URL vs local file)
|
||||||
|
// and the send method (photo vs document) based on the path and file extension.
|
||||||
|
func (c *TelegramChannel) sendSingleMedia(ctx context.Context, chatID int64, mediaPath, caption string) error {
|
||||||
|
isURL := strings.HasPrefix(mediaPath, "http://") || strings.HasPrefix(mediaPath, "https://")
|
||||||
|
isImage := isImageExtension(mediaPath)
|
||||||
|
|
||||||
|
if isURL {
|
||||||
|
input := tu.FileFromURL(mediaPath)
|
||||||
|
if isImage {
|
||||||
|
params := tu.Photo(tu.ID(chatID), input)
|
||||||
|
params.Caption = caption
|
||||||
|
_, err := c.bot.SendPhoto(ctx, params)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
params := tu.Document(tu.ID(chatID), input)
|
||||||
|
params.Caption = caption
|
||||||
|
_, err := c.bot.SendDocument(ctx, params)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Local file — stream directly, don't buffer in memory
|
||||||
|
file, err := os.Open(mediaPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("open media file: %w", err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
fileName := filepath.Base(mediaPath)
|
||||||
|
input := tu.File(tu.NameReader(file, fileName))
|
||||||
|
|
||||||
|
if isImage {
|
||||||
|
params := tu.Photo(tu.ID(chatID), input)
|
||||||
|
params.Caption = caption
|
||||||
|
_, err = c.bot.SendPhoto(ctx, params)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
params := tu.Document(tu.ID(chatID), input)
|
||||||
|
params.Caption = caption
|
||||||
|
_, err = c.bot.SendDocument(ctx, params)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// isImageExtension returns true for common image file extensions.
|
||||||
|
func isImageExtension(path string) bool {
|
||||||
|
ext := strings.ToLower(filepath.Ext(path))
|
||||||
|
switch ext {
|
||||||
|
case ".jpg", ".jpeg", ".png", ".gif", ".webp":
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func (c *TelegramChannel) handleMessage(ctx context.Context, message *telego.Message) error {
|
func (c *TelegramChannel) handleMessage(ctx context.Context, message *telego.Message) error {
|
||||||
if message == nil {
|
if message == nil {
|
||||||
return fmt.Errorf("message is nil")
|
return fmt.Errorf("message is nil")
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue