Skip to content

Commit 0fc10ea

Browse files
authored
fix: advertise 100MB limit, files that exceed that limit show a message (#229)
* fix: advertise 100MB limit, files that exceed that limit show a message * fix: advertise 100MB limit to all media/file types, not just files
1 parent 50a04f8 commit 0fc10ea

10 files changed

Lines changed: 225 additions & 3 deletions

File tree

pkg/connector/capabilities_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package connector
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"maunium.net/go/mautrix/event"
8+
)
9+
10+
func TestCapabilitiesAdvertiseFileSizeLimit(t *testing.T) {
11+
const wantMaxFileSize int64 = 100 * 1024 * 1024
12+
caps := (&LineClient{}).GetCapabilities(context.Background(), nil)
13+
for _, messageType := range []event.CapabilityMsgType{
14+
event.MsgImage,
15+
event.MsgFile,
16+
event.MsgVideo,
17+
event.MsgAudio,
18+
event.CapMsgVoice,
19+
} {
20+
features := caps.File[messageType]
21+
if features == nil {
22+
t.Fatalf("%s file features are missing", messageType)
23+
}
24+
if features.MaxSize != wantMaxFileSize {
25+
t.Errorf("%s MaxSize = %d, want %d", messageType, features.MaxSize, wantMaxFileSize)
26+
}
27+
}
28+
}

pkg/connector/connector.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ func (lc *LineConnector) Start(ctx context.Context) error {
5050
}
5151

5252
func (lc *LineConnector) GetBridgeInfoVersion() (info, capabilities int) {
53-
return 1, 2
53+
return 1, 3
5454
}
5555

5656
func (lc *LineConnector) GetCapabilities() *bridgev2.NetworkGeneralCapabilities {

pkg/connector/handlers/audio.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ import (
1515

1616
// ConvertAudio converts a LINE audio message to a Matrix audio message.
1717
func (h *Handler) ConvertAudio(ctx context.Context, portal *bridgev2.Portal, intent bridgev2.MatrixAPI, data line.Message, decryptedBody string, relatesTo *event.RelatesTo) (*bridgev2.ConvertedMessage, error) {
18+
if oversized := h.oversizedMediaNoticeFromMetadata(data.ContentMetadata, relatesTo); oversized != nil {
19+
return oversized, nil
20+
}
21+
1822
client := h.NewClient()
1923
oid := data.ContentMetadata["OID"]
2024
isPlainMedia := oid == ""
@@ -95,6 +99,10 @@ func (h *Handler) ConvertAudio(ctx context.Context, portal *bridgev2.Portal, int
9599
}
96100
}
97101

102+
if oversized := h.oversizedMediaNotice(int64(len(audioData)), "downloaded", relatesTo); oversized != nil {
103+
return oversized, nil
104+
}
105+
98106
var duration int
99107
if durationStr := data.ContentMetadata["DURATION"]; durationStr != "" {
100108
if d, err := strconv.Atoi(durationStr); err == nil {

pkg/connector/handlers/file.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ import (
1414

1515
// ConvertFile converts a LINE file message to a Matrix file message.
1616
func (h *Handler) ConvertFile(ctx context.Context, portal *bridgev2.Portal, intent bridgev2.MatrixAPI, data line.Message, decryptedBody string, relatesTo *event.RelatesTo) (*bridgev2.ConvertedMessage, error) {
17+
if oversized := h.oversizedMediaNoticeFromMetadata(data.ContentMetadata, relatesTo); oversized != nil {
18+
return oversized, nil
19+
}
20+
1721
client := h.NewClient()
1822
oid := data.ContentMetadata["OID"]
1923
isPlainMedia := oid == ""
@@ -89,6 +93,10 @@ func (h *Handler) ConvertFile(ctx context.Context, portal *bridgev2.Portal, inte
8993
}
9094
}
9195

96+
if oversized := h.oversizedMediaNotice(int64(len(fileData)), "downloaded", relatesTo); oversized != nil {
97+
return oversized, nil
98+
}
99+
92100
if fileName == "" {
93101
fileName = data.ContentMetadata["FILE_NAME"]
94102
}

pkg/connector/handlers/image.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ import (
1515

1616
// ConvertImage converts a LINE image message to a Matrix image message.
1717
func (h *Handler) ConvertImage(ctx context.Context, portal *bridgev2.Portal, intent bridgev2.MatrixAPI, data line.Message, decryptedBody string, relatesTo *event.RelatesTo) (*bridgev2.ConvertedMessage, error) {
18+
if oversized := h.oversizedMediaNoticeFromMetadata(data.ContentMetadata, relatesTo); oversized != nil {
19+
return oversized, nil
20+
}
21+
1822
client := h.NewClient()
1923
oid := data.ContentMetadata["OID"]
2024
isPlainMedia := oid == ""
@@ -95,6 +99,10 @@ func (h *Handler) ConvertImage(ctx context.Context, portal *bridgev2.Portal, int
9599
}
96100
}
97101

102+
if oversized := h.oversizedMediaNotice(int64(len(imgData)), "downloaded", relatesTo); oversized != nil {
103+
return oversized, nil
104+
}
105+
98106
// Upload to Matrix
99107
uploadStart := time.Now()
100108
mxc, file, err := intent.UploadMedia(ctx, portal.MXID, imgData, "image.jpg", "image/jpeg")
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package handlers
2+
3+
import (
4+
"strconv"
5+
"strings"
6+
7+
"maunium.net/go/mautrix/bridgev2"
8+
"maunium.net/go/mautrix/event"
9+
)
10+
11+
const (
12+
BeeperMaxFileSize = 100 * 1024 * 1024
13+
encryptedMediaSizeOverhead = 32
14+
oversizedFileBody = "This file exceeds Beeper's 100MB file size limit. Open LINE to view it."
15+
)
16+
17+
func (h *Handler) oversizedMediaNoticeFromMetadata(metadata map[string]string, relatesTo *event.RelatesTo) *bridgev2.ConvertedMessage {
18+
size, err := strconv.ParseInt(strings.TrimSpace(metadata["FILE_SIZE"]), 10, 64)
19+
// E2EE media stored in OBS may include a 32-byte HMAC in FILE_SIZE. Keep
20+
// borderline values on the authoritative post-decryption size-check path.
21+
if err != nil || size <= BeeperMaxFileSize+encryptedMediaSizeOverhead {
22+
return nil
23+
}
24+
return h.oversizedMediaNotice(size, "metadata", relatesTo)
25+
}
26+
27+
func (h *Handler) oversizedMediaNotice(size int64, sizeSource string, relatesTo *event.RelatesTo) *bridgev2.ConvertedMessage {
28+
if size <= BeeperMaxFileSize {
29+
return nil
30+
}
31+
h.Log.Warn().
32+
Int64("size_bytes", size).
33+
Int("limit_bytes", BeeperMaxFileSize).
34+
Str("size_source", sizeSource).
35+
Msg("Skipping oversized LINE media upload")
36+
return &bridgev2.ConvertedMessage{
37+
Parts: []*bridgev2.ConvertedMessagePart{
38+
{
39+
Type: event.EventMessage,
40+
Content: &event.MessageEventContent{
41+
MsgType: event.MsgNotice,
42+
Body: oversizedFileBody,
43+
RelatesTo: relatesTo,
44+
},
45+
},
46+
},
47+
}
48+
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package handlers
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/rs/zerolog"
8+
"maunium.net/go/mautrix/bridgev2"
9+
"maunium.net/go/mautrix/event"
10+
11+
"github.com/highesttt/matrix-line-messenger/pkg/line"
12+
)
13+
14+
func TestOversizedMediaNotice(t *testing.T) {
15+
h := &Handler{Log: zerolog.Nop()}
16+
relatesTo := &event.RelatesTo{}
17+
converted := h.oversizedMediaNotice(BeeperMaxFileSize+1, "downloaded", relatesTo)
18+
if converted == nil || len(converted.Parts) != 1 {
19+
t.Fatalf("converted = %#v, want one notice part", converted)
20+
}
21+
22+
part := converted.Parts[0]
23+
if part.Type != event.EventMessage {
24+
t.Fatalf("event type = %v, want %v", part.Type, event.EventMessage)
25+
}
26+
if part.Content == nil {
27+
t.Fatal("notice content is nil")
28+
}
29+
if part.Content.MsgType != event.MsgNotice {
30+
t.Fatalf("message type = %v, want %v", part.Content.MsgType, event.MsgNotice)
31+
}
32+
const expectedBody = "This file exceeds Beeper's 100MB file size limit. Open LINE to view it."
33+
if part.Content.Body != expectedBody {
34+
t.Fatalf("body = %q, want %q", part.Content.Body, expectedBody)
35+
}
36+
if part.Content.RelatesTo != relatesTo {
37+
t.Fatalf("relates_to = %#v, want original pointer %#v", part.Content.RelatesTo, relatesTo)
38+
}
39+
}
40+
41+
func TestOversizedMediaNoticeAllowsLimitAndBelow(t *testing.T) {
42+
h := &Handler{Log: zerolog.Nop()}
43+
for _, size := range []int64{0, BeeperMaxFileSize - 1, BeeperMaxFileSize} {
44+
if converted := h.oversizedMediaNotice(size, "downloaded", nil); converted != nil {
45+
t.Fatalf("size %d converted = %#v, want nil", size, converted)
46+
}
47+
}
48+
}
49+
50+
func TestOversizedMediaMetadataShortCircuitsAllMediaHandlers(t *testing.T) {
51+
h := &Handler{
52+
Log: zerolog.Nop(),
53+
NewClient: func() *line.Client {
54+
t.Fatal("NewClient was called for media declared over the size limit")
55+
return nil
56+
},
57+
}
58+
message := line.Message{
59+
ID: "message-id",
60+
ContentMetadata: map[string]string{
61+
"FILE_SIZE": "104857633",
62+
},
63+
}
64+
relatesTo := &event.RelatesTo{}
65+
66+
tests := map[string]func() (*bridgev2.ConvertedMessage, error){
67+
"image": func() (*bridgev2.ConvertedMessage, error) {
68+
return h.ConvertImage(context.Background(), nil, nil, message, "", relatesTo)
69+
},
70+
"video": func() (*bridgev2.ConvertedMessage, error) {
71+
return h.ConvertVideo(context.Background(), nil, nil, message, "", relatesTo)
72+
},
73+
"audio and voice": func() (*bridgev2.ConvertedMessage, error) {
74+
return h.ConvertAudio(context.Background(), nil, nil, message, "", relatesTo)
75+
},
76+
"file": func() (*bridgev2.ConvertedMessage, error) {
77+
return h.ConvertFile(context.Background(), nil, nil, message, "", relatesTo)
78+
},
79+
}
80+
for name, convert := range tests {
81+
t.Run(name, func(t *testing.T) {
82+
converted, err := convert()
83+
if err != nil {
84+
t.Fatal(err)
85+
}
86+
if converted == nil || len(converted.Parts) != 1 || converted.Parts[0].Content.Body != oversizedFileBody {
87+
t.Fatalf("converted = %#v, want oversized notice", converted)
88+
}
89+
})
90+
}
91+
}
92+
93+
func TestOversizedMediaMetadataFallsBackForMissingInvalidAndAllowedSizes(t *testing.T) {
94+
h := &Handler{Log: zerolog.Nop()}
95+
for name, metadata := range map[string]map[string]string{
96+
"missing": nil,
97+
"invalid": {"FILE_SIZE": "not-a-number"},
98+
"negative": {"FILE_SIZE": "-1"},
99+
"at limit": {"FILE_SIZE": "104857600"},
100+
"possible encrypted overhead": {"FILE_SIZE": "104857632"},
101+
} {
102+
t.Run(name, func(t *testing.T) {
103+
if converted := h.oversizedMediaNoticeFromMetadata(metadata, nil); converted != nil {
104+
t.Fatalf("converted = %#v, want normal download fallback", converted)
105+
}
106+
})
107+
}
108+
}

pkg/connector/handlers/video.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ import (
1616

1717
// ConvertVideo converts a LINE video message to a Matrix video message.
1818
func (h *Handler) ConvertVideo(ctx context.Context, portal *bridgev2.Portal, intent bridgev2.MatrixAPI, data line.Message, decryptedBody string, relatesTo *event.RelatesTo) (*bridgev2.ConvertedMessage, error) {
19+
if oversized := h.oversizedMediaNoticeFromMetadata(data.ContentMetadata, relatesTo); oversized != nil {
20+
return oversized, nil
21+
}
22+
1923
client := h.NewClient()
2024
oid := data.ContentMetadata["OID"]
2125
isPlainMedia := oid == ""
@@ -109,6 +113,10 @@ func (h *Handler) ConvertVideo(ctx context.Context, portal *bridgev2.Portal, int
109113
}
110114
}
111115

116+
if oversized := h.oversizedMediaNotice(int64(len(videoData)), "downloaded", relatesTo); oversized != nil {
117+
return oversized, nil
118+
}
119+
112120
fileName := data.ContentMetadata["FILE_NAME"]
113121

114122
if fileName == "" && decryptedBody != "" && strings.Contains(decryptedBody, "fileName") {

pkg/connector/reaction_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ import (
2020

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

2727
caps := (&LineClient{}).GetCapabilities(context.Background(), nil)

pkg/connector/userinfo.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"maunium.net/go/mautrix/bridgev2/networkid"
1616
"maunium.net/go/mautrix/event"
1717

18+
"github.com/highesttt/matrix-line-messenger/pkg/connector/handlers"
1819
"github.com/highesttt/matrix-line-messenger/pkg/line"
1920
)
2021

@@ -64,6 +65,7 @@ func (lc *LineClient) GetCapabilities(ctx context.Context, portal *bridgev2.Port
6465
File: event.FileFeatureMap{
6566
event.MsgImage: {
6667
Caption: event.CapLevelRejected,
68+
MaxSize: handlers.BeeperMaxFileSize,
6769
MimeTypes: map[string]event.CapabilitySupportLevel{
6870
"image/jpeg": event.CapLevelFullySupported,
6971
"image/png": event.CapLevelFullySupported,
@@ -80,13 +82,15 @@ func (lc *LineClient) GetCapabilities(ctx context.Context, portal *bridgev2.Port
8082
},
8183
event.MsgFile: {
8284
Caption: event.CapLevelRejected,
85+
MaxSize: handlers.BeeperMaxFileSize,
8386
MimeTypes: map[string]event.CapabilitySupportLevel{
8487
"image/gif": event.CapLevelFullySupported,
8588
"*/*": event.CapLevelFullySupported,
8689
},
8790
},
8891
event.MsgVideo: {
8992
Caption: event.CapLevelRejected,
93+
MaxSize: handlers.BeeperMaxFileSize,
9094
MimeTypes: map[string]event.CapabilitySupportLevel{
9195
"video/mp4": event.CapLevelFullySupported,
9296
"video/webm": event.CapLevelFullySupported,
@@ -100,6 +104,7 @@ func (lc *LineClient) GetCapabilities(ctx context.Context, portal *bridgev2.Port
100104
},
101105
event.MsgAudio: {
102106
Caption: event.CapLevelRejected,
107+
MaxSize: handlers.BeeperMaxFileSize,
103108
MimeTypes: map[string]event.CapabilitySupportLevel{
104109
"audio/mpeg": event.CapLevelFullySupported,
105110
"audio/ogg": event.CapLevelFullySupported,
@@ -117,6 +122,7 @@ func (lc *LineClient) GetCapabilities(ctx context.Context, portal *bridgev2.Port
117122
},
118123
event.CapMsgVoice: {
119124
Caption: event.CapLevelRejected,
125+
MaxSize: handlers.BeeperMaxFileSize,
120126
MimeTypes: map[string]event.CapabilitySupportLevel{
121127
"audio/ogg": event.CapLevelFullySupported,
122128
"audio/mp4": event.CapLevelFullySupported,

0 commit comments

Comments
 (0)