Skip to content

Commit c6f1c77

Browse files
committed
fix: authenticate LINE album preview downloads
1 parent 4314232 commit c6f1c77

6 files changed

Lines changed: 767 additions & 63 deletions

File tree

pkg/connector/handlers/handler.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,18 @@ type Handler struct {
3232
// DownloadOBSResource overrides non-talk OBS downloads in tests.
3333
DownloadOBSResource func(ctx context.Context, client *line.Client, service, sid, oid string) ([]byte, error)
3434

35+
// DownloadAlbumPreview overrides album thumbnail downloads in tests.
36+
DownloadAlbumPreview func(ctx context.Context, client *line.Client, oid, chatID, albumID string) ([]byte, error)
37+
3538
// DecryptMedia decrypts E2EE encrypted media data using the given key material.
3639
DecryptMedia func(data []byte, keyMaterial string) ([]byte, error)
3740
}
3841

39-
func (h *Handler) downloadOBSResource(ctx context.Context, client *line.Client, service, sid, oid string) ([]byte, error) {
40-
if h.DownloadOBSResource != nil {
41-
return h.DownloadOBSResource(ctx, client, service, sid, oid)
42+
func (h *Handler) downloadAlbumPreview(ctx context.Context, client *line.Client, oid, chatID, albumID string) ([]byte, error) {
43+
if h.DownloadAlbumPreview != nil {
44+
return h.DownloadAlbumPreview(ctx, client, oid, chatID, albumID)
4245
}
43-
return client.DownloadOBSResource(ctx, service, sid, oid, "")
46+
return client.DownloadAlbumPreview(ctx, oid, chatID, albumID)
4447
}
4548

4649
func obsTalkMetaMessageID(messageID string, isPlainMedia bool) string {

pkg/connector/handlers/post_notification.go

Lines changed: 187 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ import (
66
"errors"
77
"fmt"
88
"net/http"
9+
"net/url"
910
"strings"
11+
"sync"
1012

1113
"maunium.net/go/mautrix/bridgev2"
1214
"maunium.net/go/mautrix/bridgev2/networkid"
@@ -15,13 +17,20 @@ import (
1517
"github.com/highesttt/matrix-line-messenger/pkg/line"
1618
)
1719

20+
const albumPreviewWorkerLimit = 4
21+
1822
type postPreviewMedia struct {
1923
Service string `json:"svc"`
2024
SID string `json:"sid"`
2125
OID string `json:"mediaOid"`
2226
MediaType string `json:"mediaType"`
2327
}
2428

29+
type albumPreviewContext struct {
30+
ChatID string
31+
AlbumID string
32+
}
33+
2534
// ConvertPostNotification converts a LINE note, album, or unknown post
2635
// notification into a readable Matrix notice, including album preview images.
2736
func (h *Handler) ConvertPostNotification(
@@ -86,63 +95,194 @@ func (h *Handler) ConvertPostNotification(
8695
if len(previewMedias) == 0 {
8796
return converted, nil
8897
}
98+
previewContext := parseAlbumPreviewContext(data.ContentMetadata)
99+
if previewContext.ChatID == "" {
100+
h.Log.Warn().
101+
Str("msg_id", data.ID).
102+
Msg("LINE album preview metadata is missing chatId")
103+
return converted, nil
104+
}
89105
if h.NewClient == nil || intent == nil || portal == nil {
90106
return nil, errors.New("album preview conversion requires LINE and Matrix media clients")
91107
}
92108

93109
client := h.NewClient()
94-
for index, media := range previewMedias {
95-
imageData, err := h.downloadOBSResource(ctx, client, media.Service, media.SID, media.OID)
96-
if newClient, ok := h.tryRecoverClient(ctx, err); ok {
97-
client = newClient
98-
imageData, err = h.downloadOBSResource(ctx, client, media.Service, media.SID, media.OID)
99-
}
100-
if errors.Is(err, line.ErrOBSObjectNotFound) {
101-
h.Log.Warn().
102-
Str("msg_id", data.ID).
103-
Str("media_oid", media.OID).
104-
Msg("LINE album preview image expired before it could be bridged")
105-
continue
106-
} else if err != nil {
107-
return nil, fmt.Errorf(
108-
"%w: failed to download LINE album preview %q: %w",
109-
bridgev2.ErrIgnoringRemoteEvent,
110-
media.OID,
111-
err,
112-
)
110+
parts, err := h.convertAlbumPreviews(
111+
ctx,
112+
portal,
113+
intent,
114+
client,
115+
data.ID,
116+
previewContext,
117+
previewMedias,
118+
relatesTo,
119+
)
120+
if err != nil {
121+
return nil, err
122+
}
123+
for _, part := range parts {
124+
if part != nil {
125+
converted.Parts = append(converted.Parts, part)
113126
}
127+
}
128+
return converted, nil
129+
}
114130

115-
mimeType, extension := albumPreviewImageType(imageData)
116-
if mimeType == "" {
117-
h.Log.Warn().
118-
Str("msg_id", data.ID).
119-
Str("media_oid", media.OID).
120-
Msg("Ignoring LINE album preview with unsupported image data")
121-
continue
122-
}
123-
fileName := fmt.Sprintf("album-image-%d.%s", index+1, extension)
124-
mxc, file, err := intent.UploadMedia(ctx, portal.MXID, imageData, fileName, mimeType)
125-
if err != nil {
126-
return nil, fmt.Errorf("failed to upload LINE album preview to Matrix: %w", err)
127-
}
131+
func (h *Handler) convertAlbumPreviews(
132+
ctx context.Context,
133+
portal *bridgev2.Portal,
134+
intent bridgev2.MatrixAPI,
135+
client *line.Client,
136+
messageID string,
137+
previewContext albumPreviewContext,
138+
previewMedias []postPreviewMedia,
139+
relatesTo *event.RelatesTo,
140+
) ([]*bridgev2.ConvertedMessagePart, error) {
141+
workCtx, cancel := context.WithCancel(ctx)
142+
defer cancel()
143+
144+
jobs := make(chan int, len(previewMedias))
145+
for index := range previewMedias {
146+
jobs <- index
147+
}
148+
close(jobs)
128149

129-
converted.Parts = append(converted.Parts, &bridgev2.ConvertedMessagePart{
130-
ID: networkid.PartID(fmt.Sprintf("album-image-%d", index+1)),
131-
Type: event.EventMessage,
132-
Content: &event.MessageEventContent{
133-
MsgType: event.MsgImage,
134-
Body: fileName,
135-
URL: mxc,
136-
File: file,
137-
Info: &event.FileInfo{
138-
MimeType: mimeType,
139-
Size: len(imageData),
140-
},
141-
RelatesTo: relatesTo,
150+
parts := make([]*bridgev2.ConvertedMessagePart, len(previewMedias))
151+
var workers sync.WaitGroup
152+
var errOnce sync.Once
153+
var firstErr error
154+
workerCount := min(albumPreviewWorkerLimit, len(previewMedias))
155+
workers.Add(workerCount)
156+
for range workerCount {
157+
go func() {
158+
defer workers.Done()
159+
for index := range jobs {
160+
if workCtx.Err() != nil {
161+
continue
162+
}
163+
part, err := h.convertAlbumPreview(
164+
workCtx,
165+
portal,
166+
intent,
167+
client,
168+
messageID,
169+
previewContext,
170+
previewMedias[index],
171+
index,
172+
relatesTo,
173+
)
174+
if err != nil {
175+
errOnce.Do(func() {
176+
firstErr = err
177+
cancel()
178+
})
179+
continue
180+
}
181+
parts[index] = part
182+
}
183+
}()
184+
}
185+
workers.Wait()
186+
187+
if firstErr != nil {
188+
return nil, firstErr
189+
}
190+
if err := ctx.Err(); err != nil {
191+
return nil, err
192+
}
193+
return parts, nil
194+
}
195+
196+
func (h *Handler) convertAlbumPreview(
197+
ctx context.Context,
198+
portal *bridgev2.Portal,
199+
intent bridgev2.MatrixAPI,
200+
client *line.Client,
201+
messageID string,
202+
previewContext albumPreviewContext,
203+
media postPreviewMedia,
204+
index int,
205+
relatesTo *event.RelatesTo,
206+
) (*bridgev2.ConvertedMessagePart, error) {
207+
imageData, err := h.downloadAlbumPreview(
208+
ctx,
209+
client,
210+
media.OID,
211+
previewContext.ChatID,
212+
previewContext.AlbumID,
213+
)
214+
if newClient, ok := h.tryRecoverClient(ctx, err); ok {
215+
imageData, err = h.downloadAlbumPreview(
216+
ctx,
217+
newClient,
218+
media.OID,
219+
previewContext.ChatID,
220+
previewContext.AlbumID,
221+
)
222+
}
223+
if errors.Is(err, line.ErrOBSObjectNotFound) {
224+
h.Log.Warn().
225+
Str("msg_id", messageID).
226+
Str("media_oid", media.OID).
227+
Msg("LINE album preview image expired before it could be bridged")
228+
return nil, nil
229+
} else if err != nil {
230+
return nil, fmt.Errorf(
231+
"%w: failed to download LINE album preview %q: %w",
232+
bridgev2.ErrIgnoringRemoteEvent,
233+
media.OID,
234+
err,
235+
)
236+
}
237+
238+
mimeType, extension := albumPreviewImageType(imageData)
239+
if mimeType == "" {
240+
h.Log.Warn().
241+
Str("msg_id", messageID).
242+
Str("media_oid", media.OID).
243+
Msg("Ignoring LINE album preview with unsupported image data")
244+
return nil, nil
245+
}
246+
fileName := fmt.Sprintf("album-image-%d.%s", index+1, extension)
247+
mxc, file, err := intent.UploadMedia(ctx, portal.MXID, imageData, fileName, mimeType)
248+
if err != nil {
249+
return nil, fmt.Errorf("failed to upload LINE album preview to Matrix: %w", err)
250+
}
251+
252+
return &bridgev2.ConvertedMessagePart{
253+
ID: networkid.PartID(fmt.Sprintf("album-image-%d", index+1)),
254+
Type: event.EventMessage,
255+
Content: &event.MessageEventContent{
256+
MsgType: event.MsgImage,
257+
Body: fileName,
258+
URL: mxc,
259+
File: file,
260+
Info: &event.FileInfo{
261+
MimeType: mimeType,
262+
Size: len(imageData),
142263
},
143-
})
264+
RelatesTo: relatesTo,
265+
},
266+
}, nil
267+
}
268+
269+
func parseAlbumPreviewContext(metadata map[string]string) albumPreviewContext {
270+
previewContext := albumPreviewContext{
271+
ChatID: strings.TrimSpace(metadata["chatId"]),
144272
}
145-
return converted, nil
273+
postEndURL := strings.TrimSpace(metadata["postEndUrl"])
274+
if postEndURL == "" {
275+
return previewContext
276+
}
277+
parsedURL, err := url.Parse(postEndURL)
278+
if err != nil {
279+
return previewContext
280+
}
281+
previewContext.AlbumID = strings.TrimSpace(parsedURL.Query().Get("albumIdV2"))
282+
if previewContext.AlbumID == "" {
283+
previewContext.AlbumID = strings.TrimSpace(parsedURL.Query().Get("albumId"))
284+
}
285+
return previewContext
146286
}
147287

148288
func parseAlbumPreviewMedias(metadata map[string]string) ([]postPreviewMedia, error) {

0 commit comments

Comments
 (0)