From 4c0ee055701b9527ac5ef00ae7d39edb81a995b1 Mon Sep 17 00:00:00 2001 From: Gerardo Rodriguez Date: Wed, 28 Oct 2026 11:05:23 +0000 Subject: [PATCH 1/2] handlers: upload link preview images instead of sending LINE CDN URLs --- pkg/connector/handle_message.go | 2 +- pkg/connector/handlers/emoji.go | 4 ++-- pkg/connector/handlers/text.go | 41 ++++++++++++++++++++++++++++++--- 3 files changed, 41 insertions(+), 6 deletions(-) diff --git a/pkg/connector/handle_message.go b/pkg/connector/handle_message.go index 10e1c03c..812354b6 100644 --- a/pkg/connector/handle_message.go +++ b/pkg/connector/handle_message.go @@ -476,7 +476,7 @@ func (lc *LineClient) convertLineMessage(ctx context.Context, portal *bridgev2.P } // Default to text - converted, err = h.ConvertText(unwrappedText, replyRelatesTo) + converted, err = h.ConvertText(ctx, portal, intent, unwrappedText, replyRelatesTo) } if err != nil { return nil, err diff --git a/pkg/connector/handlers/emoji.go b/pkg/connector/handlers/emoji.go index 01f2c89e..064a7e20 100644 --- a/pkg/connector/handlers/emoji.go +++ b/pkg/connector/handlers/emoji.go @@ -185,7 +185,7 @@ func (h *Handler) ConvertInlineEmoji(ctx context.Context, portal *bridgev2.Porta if msg := h.tryDownloadSticon(ctx, portal, intent, stkPkgID, stkID, relatesTo); msg != nil { return msg, nil } - return h.ConvertText(cleanInlineSticonPlaceholders(stkTxt), relatesTo) + return h.ConvertText(ctx, portal, intent, cleanInlineSticonPlaceholders(stkTxt), relatesTo) } // Try parsing the text body for inline sticon references ($STK:productId:emojiId$). @@ -233,7 +233,7 @@ func (h *Handler) ConvertInlineEmoji(ctx context.Context, portal *bridgev2.Porta Int("raw_body_length", len(bodyText)). Int("metadata_count", len(data.ContentMetadata)). Msg("Falling back to text for inline emoji") - return h.ConvertText(cleanInlineSticonPlaceholders(stkTxt), relatesTo) + return h.ConvertText(ctx, portal, intent, cleanInlineSticonPlaceholders(stkTxt), relatesTo) } type sticonReplacement struct { diff --git a/pkg/connector/handlers/text.go b/pkg/connector/handlers/text.go index a31fc66c..ce1deded 100644 --- a/pkg/connector/handlers/text.go +++ b/pkg/connector/handlers/text.go @@ -1,16 +1,19 @@ package handlers import ( + "context" + "fmt" + "io" + "net/http" "regexp" "strings" "maunium.net/go/mautrix/bridgev2" "maunium.net/go/mautrix/event" - "maunium.net/go/mautrix/id" ) // ConvertText converts a LINE text message to a Matrix text message, including link previews. -func (h *Handler) ConvertText(unwrappedText string, relatesTo *event.RelatesTo) (*bridgev2.ConvertedMessage, error) { +func (h *Handler) ConvertText(ctx context.Context, portal *bridgev2.Portal, intent bridgev2.MatrixAPI, unwrappedText string, relatesTo *event.RelatesTo) (*bridgev2.ConvertedMessage, error) { client := h.NewClient() content := &event.MessageEventContent{ @@ -36,7 +39,9 @@ func (h *Handler) ConvertText(unwrappedText string, relatesTo *event.RelatesTo) }, } if info.Image != "" && info.Obs.CDN != "" { - preview.ImageURL = id.ContentURIString(info.Obs.CDN + info.Image) + if err := h.uploadPreviewImage(ctx, portal, intent, info.Obs.CDN+info.Image, preview); err != nil { + h.Log.Warn().Err(err).Str("url", requestURL).Msg("Failed to bridge link preview image") + } } content.BeeperLinkPreviews = []*event.BeeperLinkPreview{preview} } @@ -51,3 +56,33 @@ func (h *Handler) ConvertText(unwrappedText string, relatesTo *event.RelatesTo) }, }, nil } + +func (h *Handler) uploadPreviewImage(ctx context.Context, portal *bridgev2.Portal, intent bridgev2.MatrixAPI, imageURL string, preview *event.BeeperLinkPreview) error { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, imageURL, nil) + if err != nil { + return err + } + resp, err := h.HTTPClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("unexpected status %d", resp.StatusCode) + } + data, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + mimeType := resp.Header.Get("Content-Type") + if mimeType == "" { + mimeType = http.DetectContentType(data) + } + preview.ImageURL, preview.ImageEncryption, err = intent.UploadMedia(ctx, portal.MXID, data, "", mimeType) + if err != nil { + return err + } + preview.ImageSize = event.IntOrString(len(data)) + preview.ImageType = mimeType + return nil +} From 2a2b807e25ecc67831bc30dd8a10fbc13c6a4005 Mon Sep 17 00:00:00 2001 From: Gerardo Rodriguez Date: Wed, 28 Oct 2026 11:08:38 +0000 Subject: [PATCH 2/2] handlers: fetch preview images through the public media client and require an image type --- pkg/connector/handlers/text.go | 27 ++++++++------------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/pkg/connector/handlers/text.go b/pkg/connector/handlers/text.go index ce1deded..31afb1ba 100644 --- a/pkg/connector/handlers/text.go +++ b/pkg/connector/handlers/text.go @@ -3,13 +3,14 @@ package handlers import ( "context" "fmt" - "io" "net/http" "regexp" "strings" "maunium.net/go/mautrix/bridgev2" "maunium.net/go/mautrix/event" + + "github.com/beeper/line/pkg/line" ) // ConvertText converts a LINE text message to a Matrix text message, including link previews. @@ -39,7 +40,7 @@ func (h *Handler) ConvertText(ctx context.Context, portal *bridgev2.Portal, inte }, } if info.Image != "" && info.Obs.CDN != "" { - if err := h.uploadPreviewImage(ctx, portal, intent, info.Obs.CDN+info.Image, preview); err != nil { + if err := h.uploadPreviewImage(ctx, client, portal, intent, info.Obs.CDN+info.Image, preview); err != nil { h.Log.Warn().Err(err).Str("url", requestURL).Msg("Failed to bridge link preview image") } } @@ -57,26 +58,14 @@ func (h *Handler) ConvertText(ctx context.Context, portal *bridgev2.Portal, inte }, nil } -func (h *Handler) uploadPreviewImage(ctx context.Context, portal *bridgev2.Portal, intent bridgev2.MatrixAPI, imageURL string, preview *event.BeeperLinkPreview) error { - req, err := http.NewRequestWithContext(ctx, http.MethodGet, imageURL, nil) - if err != nil { - return err - } - resp, err := h.HTTPClient.Do(req) - if err != nil { - return err - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("unexpected status %d", resp.StatusCode) - } - data, err := io.ReadAll(resp.Body) +func (h *Handler) uploadPreviewImage(ctx context.Context, client *line.Client, portal *bridgev2.Portal, intent bridgev2.MatrixAPI, imageURL string, preview *event.BeeperLinkPreview) error { + data, err := client.DownloadOBSPublicResource(ctx, imageURL) if err != nil { return err } - mimeType := resp.Header.Get("Content-Type") - if mimeType == "" { - mimeType = http.DetectContentType(data) + mimeType := http.DetectContentType(data) + if !strings.HasPrefix(mimeType, "image/") { + return fmt.Errorf("unexpected content type %q", mimeType) } preview.ImageURL, preview.ImageEncryption, err = intent.UploadMedia(ctx, portal.MXID, data, "", mimeType) if err != nil {