Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions pkg/connector/capabilities_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package connector

import (
"context"
"testing"

"maunium.net/go/mautrix/event"
)

func TestCapabilitiesAdvertiseFileSizeLimit(t *testing.T) {
const wantMaxFileSize int64 = 100 * 1024 * 1024
caps := (&LineClient{}).GetCapabilities(context.Background(), nil)
for _, messageType := range []event.CapabilityMsgType{
event.MsgImage,
event.MsgFile,
event.MsgVideo,
event.MsgAudio,
event.CapMsgVoice,
} {
features := caps.File[messageType]
if features == nil {
t.Fatalf("%s file features are missing", messageType)
}
if features.MaxSize != wantMaxFileSize {
t.Errorf("%s MaxSize = %d, want %d", messageType, features.MaxSize, wantMaxFileSize)
}
}
}
2 changes: 1 addition & 1 deletion pkg/connector/connector.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func (lc *LineConnector) Start(ctx context.Context) error {
}

func (lc *LineConnector) GetBridgeInfoVersion() (info, capabilities int) {
return 1, 2
return 1, 3
}

func (lc *LineConnector) GetCapabilities() *bridgev2.NetworkGeneralCapabilities {
Expand Down
8 changes: 8 additions & 0 deletions pkg/connector/handlers/audio.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ import (

// ConvertAudio converts a LINE audio message to a Matrix audio message.
func (h *Handler) ConvertAudio(ctx context.Context, portal *bridgev2.Portal, intent bridgev2.MatrixAPI, data line.Message, decryptedBody string, relatesTo *event.RelatesTo) (*bridgev2.ConvertedMessage, error) {
if oversized := h.oversizedMediaNoticeFromMetadata(data.ContentMetadata, relatesTo); oversized != nil {
return oversized, nil
}

client := h.NewClient()
oid := data.ContentMetadata["OID"]
isPlainMedia := oid == ""
Expand Down Expand Up @@ -95,6 +99,10 @@ func (h *Handler) ConvertAudio(ctx context.Context, portal *bridgev2.Portal, int
}
}

if oversized := h.oversizedMediaNotice(int64(len(audioData)), "downloaded", relatesTo); oversized != nil {
return oversized, nil
}

var duration int
if durationStr := data.ContentMetadata["DURATION"]; durationStr != "" {
if d, err := strconv.Atoi(durationStr); err == nil {
Expand Down
8 changes: 8 additions & 0 deletions pkg/connector/handlers/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ import (

// ConvertFile converts a LINE file message to a Matrix file message.
func (h *Handler) ConvertFile(ctx context.Context, portal *bridgev2.Portal, intent bridgev2.MatrixAPI, data line.Message, decryptedBody string, relatesTo *event.RelatesTo) (*bridgev2.ConvertedMessage, error) {
if oversized := h.oversizedMediaNoticeFromMetadata(data.ContentMetadata, relatesTo); oversized != nil {
return oversized, nil
}

client := h.NewClient()
oid := data.ContentMetadata["OID"]
isPlainMedia := oid == ""
Expand Down Expand Up @@ -89,6 +93,10 @@ func (h *Handler) ConvertFile(ctx context.Context, portal *bridgev2.Portal, inte
}
}

if oversized := h.oversizedMediaNotice(int64(len(fileData)), "downloaded", relatesTo); oversized != nil {
return oversized, nil
}

if fileName == "" {
fileName = data.ContentMetadata["FILE_NAME"]
}
Expand Down
8 changes: 8 additions & 0 deletions pkg/connector/handlers/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ import (

// ConvertImage converts a LINE image message to a Matrix image message.
func (h *Handler) ConvertImage(ctx context.Context, portal *bridgev2.Portal, intent bridgev2.MatrixAPI, data line.Message, decryptedBody string, relatesTo *event.RelatesTo) (*bridgev2.ConvertedMessage, error) {
if oversized := h.oversizedMediaNoticeFromMetadata(data.ContentMetadata, relatesTo); oversized != nil {
return oversized, nil
}

client := h.NewClient()
oid := data.ContentMetadata["OID"]
isPlainMedia := oid == ""
Expand Down Expand Up @@ -95,6 +99,10 @@ func (h *Handler) ConvertImage(ctx context.Context, portal *bridgev2.Portal, int
}
}

if oversized := h.oversizedMediaNotice(int64(len(imgData)), "downloaded", relatesTo); oversized != nil {
return oversized, nil
}

// Upload to Matrix
uploadStart := time.Now()
mxc, file, err := intent.UploadMedia(ctx, portal.MXID, imgData, "image.jpg", "image/jpeg")
Expand Down
48 changes: 48 additions & 0 deletions pkg/connector/handlers/media_size.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package handlers

import (
"strconv"
"strings"

"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/event"
)

const (
BeeperMaxFileSize = 100 * 1024 * 1024
encryptedMediaSizeOverhead = 32
oversizedFileBody = "This file exceeds Beeper's 100MB file size limit. Open LINE to view it."
)

func (h *Handler) oversizedMediaNoticeFromMetadata(metadata map[string]string, relatesTo *event.RelatesTo) *bridgev2.ConvertedMessage {
size, err := strconv.ParseInt(strings.TrimSpace(metadata["FILE_SIZE"]), 10, 64)
// E2EE media stored in OBS may include a 32-byte HMAC in FILE_SIZE. Keep
// borderline values on the authoritative post-decryption size-check path.
if err != nil || size <= BeeperMaxFileSize+encryptedMediaSizeOverhead {
return nil
}
return h.oversizedMediaNotice(size, "metadata", relatesTo)
}

func (h *Handler) oversizedMediaNotice(size int64, sizeSource string, relatesTo *event.RelatesTo) *bridgev2.ConvertedMessage {
if size <= BeeperMaxFileSize {
return nil
}
h.Log.Warn().
Int64("size_bytes", size).
Int("limit_bytes", BeeperMaxFileSize).
Str("size_source", sizeSource).
Msg("Skipping oversized LINE media upload")
return &bridgev2.ConvertedMessage{
Parts: []*bridgev2.ConvertedMessagePart{
{
Type: event.EventMessage,
Content: &event.MessageEventContent{
MsgType: event.MsgNotice,
Body: oversizedFileBody,
RelatesTo: relatesTo,
},
},
},
}
}
108 changes: 108 additions & 0 deletions pkg/connector/handlers/media_size_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package handlers

import (
"context"
"testing"

"github.com/rs/zerolog"
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/event"

"github.com/highesttt/matrix-line-messenger/pkg/line"
)

func TestOversizedMediaNotice(t *testing.T) {
h := &Handler{Log: zerolog.Nop()}
relatesTo := &event.RelatesTo{}
converted := h.oversizedMediaNotice(BeeperMaxFileSize+1, "downloaded", relatesTo)
if converted == nil || len(converted.Parts) != 1 {
t.Fatalf("converted = %#v, want one notice part", converted)
}

part := converted.Parts[0]
if part.Type != event.EventMessage {
t.Fatalf("event type = %v, want %v", part.Type, event.EventMessage)
}
if part.Content == nil {
t.Fatal("notice content is nil")
}
if part.Content.MsgType != event.MsgNotice {
t.Fatalf("message type = %v, want %v", part.Content.MsgType, event.MsgNotice)
}
const expectedBody = "This file exceeds Beeper's 100MB file size limit. Open LINE to view it."
if part.Content.Body != expectedBody {
t.Fatalf("body = %q, want %q", part.Content.Body, expectedBody)
}
if part.Content.RelatesTo != relatesTo {
t.Fatalf("relates_to = %#v, want original pointer %#v", part.Content.RelatesTo, relatesTo)
}
}

func TestOversizedMediaNoticeAllowsLimitAndBelow(t *testing.T) {
h := &Handler{Log: zerolog.Nop()}
for _, size := range []int64{0, BeeperMaxFileSize - 1, BeeperMaxFileSize} {
if converted := h.oversizedMediaNotice(size, "downloaded", nil); converted != nil {
t.Fatalf("size %d converted = %#v, want nil", size, converted)
}
}
}

func TestOversizedMediaMetadataShortCircuitsAllMediaHandlers(t *testing.T) {
h := &Handler{
Log: zerolog.Nop(),
NewClient: func() *line.Client {
t.Fatal("NewClient was called for media declared over the size limit")
return nil
},
}
message := line.Message{
ID: "message-id",
ContentMetadata: map[string]string{
"FILE_SIZE": "104857633",
},
}
relatesTo := &event.RelatesTo{}

tests := map[string]func() (*bridgev2.ConvertedMessage, error){
"image": func() (*bridgev2.ConvertedMessage, error) {
return h.ConvertImage(context.Background(), nil, nil, message, "", relatesTo)
},
"video": func() (*bridgev2.ConvertedMessage, error) {
return h.ConvertVideo(context.Background(), nil, nil, message, "", relatesTo)
},
"audio and voice": func() (*bridgev2.ConvertedMessage, error) {
return h.ConvertAudio(context.Background(), nil, nil, message, "", relatesTo)
},
"file": func() (*bridgev2.ConvertedMessage, error) {
return h.ConvertFile(context.Background(), nil, nil, message, "", relatesTo)
},
}
for name, convert := range tests {
t.Run(name, func(t *testing.T) {
converted, err := convert()
if err != nil {
t.Fatal(err)
}
if converted == nil || len(converted.Parts) != 1 || converted.Parts[0].Content.Body != oversizedFileBody {
t.Fatalf("converted = %#v, want oversized notice", converted)
}
})
}
}

func TestOversizedMediaMetadataFallsBackForMissingInvalidAndAllowedSizes(t *testing.T) {
h := &Handler{Log: zerolog.Nop()}
for name, metadata := range map[string]map[string]string{
"missing": nil,
"invalid": {"FILE_SIZE": "not-a-number"},
"negative": {"FILE_SIZE": "-1"},
"at limit": {"FILE_SIZE": "104857600"},
"possible encrypted overhead": {"FILE_SIZE": "104857632"},
} {
t.Run(name, func(t *testing.T) {
if converted := h.oversizedMediaNoticeFromMetadata(metadata, nil); converted != nil {
t.Fatalf("converted = %#v, want normal download fallback", converted)
}
})
}
}
8 changes: 8 additions & 0 deletions pkg/connector/handlers/video.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ import (

// ConvertVideo converts a LINE video message to a Matrix video message.
func (h *Handler) ConvertVideo(ctx context.Context, portal *bridgev2.Portal, intent bridgev2.MatrixAPI, data line.Message, decryptedBody string, relatesTo *event.RelatesTo) (*bridgev2.ConvertedMessage, error) {
if oversized := h.oversizedMediaNoticeFromMetadata(data.ContentMetadata, relatesTo); oversized != nil {
return oversized, nil
}

client := h.NewClient()
oid := data.ContentMetadata["OID"]
isPlainMedia := oid == ""
Expand Down Expand Up @@ -109,6 +113,10 @@ func (h *Handler) ConvertVideo(ctx context.Context, portal *bridgev2.Portal, int
}
}

if oversized := h.oversizedMediaNotice(int64(len(videoData)), "downloaded", relatesTo); oversized != nil {
return oversized, nil
}

fileName := data.ContentMetadata["FILE_NAME"]

if fileName == "" && decryptedBody != "" && strings.Contains(decryptedBody, "fileName") {
Expand Down
4 changes: 2 additions & 2 deletions pkg/connector/reaction_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ import (

func TestCapabilitiesAdvertiseSupportedReactions(t *testing.T) {
infoVersion, capabilityVersion := (&LineConnector{}).GetBridgeInfoVersion()
if infoVersion != 1 || capabilityVersion != 2 {
t.Fatalf("bridge info/capability versions = %d/%d, want 1/2", infoVersion, capabilityVersion)
if infoVersion != 1 || capabilityVersion != 3 {
t.Fatalf("bridge info/capability versions = %d/%d, want 1/3", infoVersion, capabilityVersion)
}

caps := (&LineClient{}).GetCapabilities(context.Background(), nil)
Expand Down
6 changes: 6 additions & 0 deletions pkg/connector/userinfo.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"maunium.net/go/mautrix/bridgev2/networkid"
"maunium.net/go/mautrix/event"

"github.com/highesttt/matrix-line-messenger/pkg/connector/handlers"
"github.com/highesttt/matrix-line-messenger/pkg/line"
)

Expand Down Expand Up @@ -64,6 +65,7 @@ func (lc *LineClient) GetCapabilities(ctx context.Context, portal *bridgev2.Port
File: event.FileFeatureMap{
event.MsgImage: {
Caption: event.CapLevelRejected,
MaxSize: handlers.BeeperMaxFileSize,
MimeTypes: map[string]event.CapabilitySupportLevel{
"image/jpeg": event.CapLevelFullySupported,
"image/png": event.CapLevelFullySupported,
Expand All @@ -80,13 +82,15 @@ func (lc *LineClient) GetCapabilities(ctx context.Context, portal *bridgev2.Port
},
event.MsgFile: {
Caption: event.CapLevelRejected,
MaxSize: handlers.BeeperMaxFileSize,
MimeTypes: map[string]event.CapabilitySupportLevel{
"image/gif": event.CapLevelFullySupported,
"*/*": event.CapLevelFullySupported,
},
},
event.MsgVideo: {
Caption: event.CapLevelRejected,
MaxSize: handlers.BeeperMaxFileSize,
MimeTypes: map[string]event.CapabilitySupportLevel{
"video/mp4": event.CapLevelFullySupported,
"video/webm": event.CapLevelFullySupported,
Expand All @@ -100,6 +104,7 @@ func (lc *LineClient) GetCapabilities(ctx context.Context, portal *bridgev2.Port
},
event.MsgAudio: {
Caption: event.CapLevelRejected,
MaxSize: handlers.BeeperMaxFileSize,
MimeTypes: map[string]event.CapabilitySupportLevel{
"audio/mpeg": event.CapLevelFullySupported,
"audio/ogg": event.CapLevelFullySupported,
Expand All @@ -117,6 +122,7 @@ func (lc *LineClient) GetCapabilities(ctx context.Context, portal *bridgev2.Port
},
event.CapMsgVoice: {
Caption: event.CapLevelRejected,
MaxSize: handlers.BeeperMaxFileSize,
MimeTypes: map[string]event.CapabilitySupportLevel{
"audio/ogg": event.CapLevelFullySupported,
"audio/mp4": event.CapLevelFullySupported,
Expand Down
Loading