From e97168ca469abda3e50d65fb04c2f0f75541754e Mon Sep 17 00:00:00 2001 From: highesttt Date: Thu, 4 Jun 2026 00:34:55 -0500 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=E2=9C=A8=20improve=20image=20fetch?= =?UTF-8?q?ing=20speeds?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/connector/handlers/image.go | 69 ++++++++++++++++++++++++++++++--- pkg/line/client.go | 65 ++++++++++++++++++++++++------- 2 files changed, 114 insertions(+), 20 deletions(-) diff --git a/pkg/connector/handlers/image.go b/pkg/connector/handlers/image.go index 2b292e0..1c362f3 100644 --- a/pkg/connector/handlers/image.go +++ b/pkg/connector/handlers/image.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "strings" + "time" "maunium.net/go/mautrix/bridgev2" "maunium.net/go/mautrix/event" @@ -27,23 +28,40 @@ func (h *Handler) ConvertImage(ctx context.Context, portal *bridgev2.Portal, int return nil, nil } + downloadOptions := line.OBSDownloadOptions{ + OBSPop: data.ContentMetadata["OBS_POP"], + } + if isPlainMedia && lineMediaCategory(data.ContentMetadata) == "original" { + downloadOptions.TID = "original" + } + var imgData []byte var err error + dlStart := time.Now() + h.Log.Debug(). + Str("oid", oid). + Str("msg_id", data.ID). + Str("tid", downloadOptions.TID). + Bool("has_obs_pop", downloadOptions.OBSPop != ""). + Bool("plain_media", isPlainMedia). + Interface("content_metadata", data.ContentMetadata). + Msg("Downloading image from LINE OBS") if isPlainMedia { - imgData, err = client.DownloadOBSWithSID(ctx, oid, data.ID, "m") + imgData, err = client.DownloadOBSWithSIDOptions(ctx, oid, data.ID, "m", downloadOptions) } else { - imgData, err = client.DownloadOBS(ctx, oid, data.ID) + imgData, err = client.DownloadOBSWithOptions(ctx, oid, data.ID, downloadOptions) } // Refresh token if we get a 401 if newClient, ok := h.tryRecoverClient(ctx, err); ok { client = newClient if isPlainMedia { - imgData, err = client.DownloadOBSWithSID(ctx, oid, data.ID, "m") + imgData, err = client.DownloadOBSWithSIDOptions(ctx, oid, data.ID, "m", downloadOptions) } else { - imgData, err = client.DownloadOBS(ctx, oid, data.ID) + imgData, err = client.DownloadOBSWithOptions(ctx, oid, data.ID, downloadOptions) } } + downloadDuration := time.Since(dlStart) if err != nil { h.Log.Warn(). @@ -51,6 +69,7 @@ func (h *Handler) ConvertImage(ctx context.Context, portal *bridgev2.Portal, int Str("oid", oid). Str("msg_id", data.ID). Bool("plain_media", isPlainMedia). + Dur("download_duration", downloadDuration). Msg("Failed to download image from OBS, sending placeholder") return &bridgev2.ConvertedMessage{ Parts: []*bridgev2.ConvertedMessagePart{ @@ -67,15 +86,22 @@ func (h *Handler) ConvertImage(ctx context.Context, portal *bridgev2.Portal, int } // Decrypt image if it has keyMaterial (E2EE) + var decryptDuration time.Duration if decryptedBody != "" && strings.Contains(decryptedBody, "keyMaterial") { var decryptInfo struct { KeyMaterial string `json:"keyMaterial"` FileName string `json:"fileName"` } if err := json.Unmarshal([]byte(decryptedBody), &decryptInfo); err == nil && decryptInfo.KeyMaterial != "" { + decryptStart := time.Now() decryptedImg, err := h.DecryptMedia(imgData, decryptInfo.KeyMaterial) + decryptDuration = time.Since(decryptStart) if err != nil { - h.Log.Error().Err(err).Msg("Failed to decrypt image data") + h.Log.Error(). + Err(err). + Dur("download_duration", downloadDuration). + Dur("decrypt_duration", decryptDuration). + Msg("Failed to decrypt image data") return nil, fmt.Errorf("failed to decrypt image data: %w", err) } imgData = decryptedImg @@ -83,12 +109,28 @@ func (h *Handler) ConvertImage(ctx context.Context, portal *bridgev2.Portal, int } // Upload to Matrix + uploadStart := time.Now() mxc, file, err := intent.UploadMedia(ctx, portal.MXID, imgData, "image.jpg", "image/jpeg") + uploadDuration := time.Since(uploadStart) if err != nil { - h.Log.Error().Err(err).Int("size_bytes", len(imgData)).Msg("Failed to upload image to Matrix") + h.Log.Error(). + Err(err). + Int("size_bytes", len(imgData)). + Dur("download_duration", downloadDuration). + Dur("decrypt_duration", decryptDuration). + Dur("upload_duration", uploadDuration). + Msg("Failed to upload image to Matrix") return nil, fmt.Errorf("failed to upload image to matrix: %w", err) } + h.Log.Info(). + Str("mxc", mxc.ParseOrIgnore().String()). + Int("size", len(imgData)). + Dur("download_duration", downloadDuration). + Dur("decrypt_duration", decryptDuration). + Dur("upload_duration", uploadDuration). + Msg("Successfully uploaded image to Matrix") + return &bridgev2.ConvertedMessage{ Parts: []*bridgev2.ConvertedMessagePart{ { @@ -104,3 +146,18 @@ func (h *Handler) ConvertImage(ctx context.Context, portal *bridgev2.Portal, int }, }, nil } + +func lineMediaCategory(metadata map[string]string) string { + if metadata == nil || metadata["MEDIA_CONTENT_INFO"] == "" { + return "" + } + + var info struct { + Category string `json:"category"` + } + if err := json.Unmarshal([]byte(metadata["MEDIA_CONTENT_INFO"]), &info); err != nil { + return "" + } + + return info.Category +} diff --git a/pkg/line/client.go b/pkg/line/client.go index 79c92a7..0eaeb81 100644 --- a/pkg/line/client.go +++ b/pkg/line/client.go @@ -11,6 +11,7 @@ import ( "io" "log" "net/http" + "net/url" "strings" "time" @@ -24,6 +25,10 @@ const ( ShopBaseURL = "https://line-chrome-gw.line-apps.com/api/shop/thrift/ShopService" ExtensionVersion = "3.7.2" UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36" + rpcClientTimeout = 30 * time.Second + obsClientTimeout = 5 * time.Minute + obsRetryDelay = 2 * time.Second + obsMaxRetries = 5 ) type Client struct { @@ -32,14 +37,29 @@ type Client struct { AccessToken string } +type OBSDownloadOptions struct { + TID string + OBSPop string +} + func NewClient(token string) *Client { return &Client{ - HTTPClient: &http.Client{Timeout: 30 * time.Second}, - OBSClient: &http.Client{}, + HTTPClient: &http.Client{Timeout: rpcClientTimeout}, + OBSClient: &http.Client{Timeout: obsClientTimeout}, AccessToken: token, } } +func (c *Client) obsHTTPClient() *http.Client { + if c.OBSClient != nil { + return c.OBSClient + } + if c.HTTPClient != nil { + return c.HTTPClient + } + return &http.Client{Timeout: obsClientTimeout} +} + func (c *Client) Login(email, pass, certificate string) (*LoginResult, error) { // 1. Get RSA Key Info rsaKey, err := c.GetRSAKeyInfo() @@ -517,7 +537,7 @@ func (c *Client) UploadOBSPlain(data []byte, oid string, obsType string) error { req.Header.Set("X-Obs-Params", obsParamsB64) req.Header.Set("x-line-access", obsToken) - resp, err := c.HTTPClient.Do(req) + resp, err := c.obsHTTPClient().Do(req) if err != nil { return fmt.Errorf("OBS upload request failed: %w", err) } @@ -579,7 +599,7 @@ func (c *Client) UploadOBSWithSID(data []byte, sid string) (string, error) { // Use the OBS-specific access token req.Header.Set("x-line-access", obsToken) - resp, err := c.HTTPClient.Do(req) + resp, err := c.obsHTTPClient().Do(req) if err != nil { return "", fmt.Errorf("OBS upload request failed: %w", err) } @@ -637,7 +657,7 @@ func (c *Client) UploadOBSWithOIDAndSID(data []byte, oid string, sid string) err req.Header.Set("X-Obs-Params", obsParamsB64) req.Header.Set("x-line-access", obsToken) - resp, err := c.HTTPClient.Do(req) + resp, err := c.obsHTTPClient().Do(req) if err != nil { return fmt.Errorf("OBS upload request failed: %w", err) } @@ -657,20 +677,33 @@ func (c *Client) DownloadOBS(ctx context.Context, oid string, messageID string) return c.DownloadOBSWithSID(ctx, oid, messageID, "emi") } +func (c *Client) DownloadOBSWithOptions(ctx context.Context, oid string, messageID string, opts OBSDownloadOptions) ([]byte, error) { + return c.DownloadOBSWithSIDOptions(ctx, oid, messageID, "emi", opts) +} + func (c *Client) DownloadOBSWithSID(ctx context.Context, oid string, messageID string, sid string) ([]byte, error) { + return c.DownloadOBSWithSIDOptions(ctx, oid, messageID, sid, OBSDownloadOptions{}) +} + +func (c *Client) DownloadOBSWithSIDOptions(ctx context.Context, oid string, messageID string, sid string, opts OBSDownloadOptions) ([]byte, error) { // URL structure: https://obs.line-apps.com/r/talk/{SID}/{OID} // SID: emi (images), emv (videos), ema (audio), emf (files) - url := fmt.Sprintf("%s/r/talk/%s/%s", OBSBaseURL, sid, oid) + obsURL := fmt.Sprintf("%s/r/talk/%s/%s", OBSBaseURL, sid, oid) + if opts.TID != "" { + obsURL += "/" + url.PathEscape(opts.TID) + } + if opts.OBSPop != "" { + obsURL += "?p=" + url.QueryEscape(opts.OBSPop) + } obsToken, err := c.AcquireEncryptedAccessToken() if err != nil { return nil, fmt.Errorf("failed to acquire encrypted access token: %w", err) } - // Retry loop for 202 (media still processing, e.g. video transcoding) - maxRetries := 5 - for attempt := 0; attempt <= maxRetries; attempt++ { - req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + // Retry loop for 202/404 (media still processing, e.g. video transcoding). + for attempt := 0; attempt <= obsMaxRetries; attempt++ { + req, err := http.NewRequestWithContext(ctx, "GET", obsURL, nil) if err != nil { return nil, fmt.Errorf("failed to create OBS download request: %w", err) } @@ -686,14 +719,18 @@ func (c *Client) DownloadOBSWithSID(ctx context.Context, oid string, messageID s req.Header.Set("x-talk-meta", talkMeta) } - resp, err := c.OBSClient.Do(req) + resp, err := c.obsHTTPClient().Do(req) if err != nil { return nil, fmt.Errorf("OBS download request failed: %w", err) } - if (resp.StatusCode == 202 || resp.StatusCode == 404) && attempt < maxRetries { + if (resp.StatusCode == 202 || resp.StatusCode == 404) && attempt < obsMaxRetries { resp.Body.Close() - time.Sleep(2 * time.Second) + select { + case <-time.After(obsRetryDelay): + case <-ctx.Done(): + return nil, ctx.Err() + } continue } @@ -712,7 +749,7 @@ func (c *Client) DownloadOBSWithSID(ctx context.Context, oid string, messageID s return data, nil } - return nil, fmt.Errorf("OBS download failed: media still processing after %d retries", maxRetries) + return nil, fmt.Errorf("OBS download failed: media still processing after %d retries", obsMaxRetries) } // this builds x-talk-meta From 9ebf55efaa2d3c961021465b05bb47f12eba4ef7 Mon Sep 17 00:00:00 2001 From: highesttt Date: Thu, 4 Jun 2026 01:00:17 -0500 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20=E2=9C=A8=20also=20improved=20audio?= =?UTF-8?q?,=20file=20and=20video=20receiving=20speeds=20Fixed=20some=20is?= =?UTF-8?q?sues=20from=20indent?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/connector/handlers/audio.go | 5 +++-- pkg/connector/handlers/file.go | 3 ++- pkg/connector/handlers/image.go | 27 +++++++++++++++++++-------- pkg/connector/handlers/video.go | 5 +++-- pkg/line/client.go | 10 ++++++---- 5 files changed, 33 insertions(+), 17 deletions(-) diff --git a/pkg/connector/handlers/audio.go b/pkg/connector/handlers/audio.go index ef1b2b2..7fe1822 100644 --- a/pkg/connector/handlers/audio.go +++ b/pkg/connector/handlers/audio.go @@ -44,11 +44,12 @@ func (h *Handler) ConvertAudio(ctx context.Context, portal *bridgev2.Portal, int if isPlainMedia { sid = "m" } - audioData, err := client.DownloadOBSWithSID(ctx, oid, data.ID, sid) + downloadOptions := lineOBSDownloadOptions(data.ContentMetadata, isPlainMedia) + audioData, err := client.DownloadOBSWithSIDOptions(ctx, oid, data.ID, sid, downloadOptions) if newClient, ok := h.tryRecoverClient(ctx, err); ok { client = newClient - audioData, err = client.DownloadOBSWithSID(ctx, oid, data.ID, sid) + audioData, err = client.DownloadOBSWithSIDOptions(ctx, oid, data.ID, sid, downloadOptions) } _ = client diff --git a/pkg/connector/handlers/file.go b/pkg/connector/handlers/file.go index 46309c3..2788c53 100644 --- a/pkg/connector/handlers/file.go +++ b/pkg/connector/handlers/file.go @@ -35,7 +35,8 @@ func (h *Handler) ConvertFile(ctx context.Context, portal *bridgev2.Portal, inte if isPlainMedia { sid = "m" } - fileData, err := client.DownloadOBSWithSID(ctx, oid, data.ID, sid) + downloadOptions := lineOBSDownloadOptions(data.ContentMetadata, isPlainMedia) + fileData, err := client.DownloadOBSWithSIDOptions(ctx, oid, data.ID, sid, downloadOptions) if err != nil { h.Log.Warn(). Err(err). diff --git a/pkg/connector/handlers/image.go b/pkg/connector/handlers/image.go index 1c362f3..173041d 100644 --- a/pkg/connector/handlers/image.go +++ b/pkg/connector/handlers/image.go @@ -28,12 +28,8 @@ func (h *Handler) ConvertImage(ctx context.Context, portal *bridgev2.Portal, int return nil, nil } - downloadOptions := line.OBSDownloadOptions{ - OBSPop: data.ContentMetadata["OBS_POP"], - } - if isPlainMedia && lineMediaCategory(data.ContentMetadata) == "original" { - downloadOptions.TID = "original" - } + mediaCategory := lineMediaCategory(data.ContentMetadata) + downloadOptions := lineOBSDownloadOptions(data.ContentMetadata, isPlainMedia) var imgData []byte var err error @@ -42,9 +38,9 @@ func (h *Handler) ConvertImage(ctx context.Context, portal *bridgev2.Portal, int Str("oid", oid). Str("msg_id", data.ID). Str("tid", downloadOptions.TID). + Str("media_category", mediaCategory). Bool("has_obs_pop", downloadOptions.OBSPop != ""). Bool("plain_media", isPlainMedia). - Interface("content_metadata", data.ContentMetadata). Msg("Downloading image from LINE OBS") if isPlainMedia { imgData, err = client.DownloadOBSWithSIDOptions(ctx, oid, data.ID, "m", downloadOptions) @@ -123,8 +119,13 @@ func (h *Handler) ConvertImage(ctx context.Context, portal *bridgev2.Portal, int return nil, fmt.Errorf("failed to upload image to matrix: %w", err) } + matrixMediaURL := string(mxc) + if file != nil && file.URL != "" { + matrixMediaURL = string(file.URL) + } + h.Log.Info(). - Str("mxc", mxc.ParseOrIgnore().String()). + Str("matrix_media_url", matrixMediaURL). Int("size", len(imgData)). Dur("download_duration", downloadDuration). Dur("decrypt_duration", decryptDuration). @@ -161,3 +162,13 @@ func lineMediaCategory(metadata map[string]string) string { return info.Category } + +func lineOBSDownloadOptions(metadata map[string]string, isPlainMedia bool) line.OBSDownloadOptions { + opts := line.OBSDownloadOptions{ + OBSPop: metadata["OBS_POP"], + } + if isPlainMedia && lineMediaCategory(metadata) == "original" { + opts.TID = "original" + } + return opts +} diff --git a/pkg/connector/handlers/video.go b/pkg/connector/handlers/video.go index 7e5d64c..8dbf05c 100644 --- a/pkg/connector/handlers/video.go +++ b/pkg/connector/handlers/video.go @@ -45,12 +45,13 @@ func (h *Handler) ConvertVideo(ctx context.Context, portal *bridgev2.Portal, int if isPlainMedia { sid = "m" } + downloadOptions := lineOBSDownloadOptions(data.ContentMetadata, isPlainMedia) dlStart := time.Now() - videoData, err := client.DownloadOBSWithSID(ctx, oid, data.ID, sid) + videoData, err := client.DownloadOBSWithSIDOptions(ctx, oid, data.ID, sid, downloadOptions) if newClient, ok := h.tryRecoverClient(ctx, err); ok { client = newClient - videoData, err = client.DownloadOBSWithSID(ctx, oid, data.ID, sid) + videoData, err = client.DownloadOBSWithSIDOptions(ctx, oid, data.ID, sid, downloadOptions) } _ = client diff --git a/pkg/line/client.go b/pkg/line/client.go index 0eaeb81..d65f06e 100644 --- a/pkg/line/client.go +++ b/pkg/line/client.go @@ -26,7 +26,6 @@ const ( ExtensionVersion = "3.7.2" UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36" rpcClientTimeout = 30 * time.Second - obsClientTimeout = 5 * time.Minute obsRetryDelay = 2 * time.Second obsMaxRetries = 5 ) @@ -45,7 +44,7 @@ type OBSDownloadOptions struct { func NewClient(token string) *Client { return &Client{ HTTPClient: &http.Client{Timeout: rpcClientTimeout}, - OBSClient: &http.Client{Timeout: obsClientTimeout}, + OBSClient: &http.Client{}, AccessToken: token, } } @@ -57,7 +56,7 @@ func (c *Client) obsHTTPClient() *http.Client { if c.HTTPClient != nil { return c.HTTPClient } - return &http.Client{Timeout: obsClientTimeout} + return &http.Client{} } func (c *Client) Login(email, pass, certificate string) (*LoginResult, error) { @@ -724,8 +723,11 @@ func (c *Client) DownloadOBSWithSIDOptions(ctx context.Context, oid string, mess return nil, fmt.Errorf("OBS download request failed: %w", err) } - if (resp.StatusCode == 202 || resp.StatusCode == 404) && attempt < obsMaxRetries { + if resp.StatusCode == 202 || resp.StatusCode == 404 { resp.Body.Close() + if attempt >= obsMaxRetries { + return nil, fmt.Errorf("OBS download failed: media still processing after %d retries", obsMaxRetries) + } select { case <-time.After(obsRetryDelay): case <-ctx.Done():