Skip to content

Commit 30bb1bb

Browse files
committed
fix: Post notifications now bypass decrypt-failure fallback.
1 parent bcfbc2a commit 30bb1bb

4 files changed

Lines changed: 99 additions & 47 deletions

File tree

pkg/connector/handle_message.go

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,14 @@ func (lc *LineClient) convertLineMessage(ctx context.Context, portal *bridgev2.P
372372
decryptedBody := bodyText
373373
replyRelatesTo := lc.resolveReplyRelatesTo(ctx, &data)
374374

375+
// Handle LINE notes/albums before decryption failures and ordinary text
376+
// conversion. Post metadata is unencrypted, so it remains useful even when
377+
// a shared post's text fallback was marked as encrypted but could not be
378+
// decrypted.
379+
if isPostNotification(&data) {
380+
return lc.newMessageHandler().ConvertPostNotification(data, replyRelatesTo)
381+
}
382+
375383
if decryptionFailed && strings.TrimSpace(unwrappedText) == "" && ContentType(data.ContentType) == ContentText {
376384
return &bridgev2.ConvertedMessage{
377385
Parts: []*bridgev2.ConvertedMessagePart{
@@ -389,13 +397,6 @@ func (lc *LineClient) convertLineMessage(ctx context.Context, portal *bridgev2.P
389397

390398
h := lc.newMessageHandler()
391399

392-
// Handle LINE notes/albums before ordinary text conversion. Shared posts
393-
// arrive as contentType 0 with LINE's unsupported-client fallback in Text,
394-
// while the useful preview and link are in ContentMetadata.
395-
if isPostNotification(&data) {
396-
return h.ConvertPostNotification(data, replyRelatesTo)
397-
}
398-
399400
// Handle call events (ORGCONTP == "CALL")
400401
if data.ContentMetadata["ORGCONTP"] == "CALL" {
401402
return h.ConvertCall(data, replyRelatesTo)

pkg/connector/handle_message_test.go

Lines changed: 46 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -122,32 +122,54 @@ func TestConvertLineMessageDispatchesSharedPostBeforeTextFallback(t *testing.T)
122122
},
123123
}
124124

125-
converted, err := lc.convertLineMessage(
126-
t.Context(),
127-
nil,
128-
nil,
129-
data,
130-
fallbackText,
131-
fallbackText,
132-
false,
133-
)
134-
if err != nil {
135-
t.Fatalf("convertLineMessage returned error: %v", err)
136-
}
137-
if converted == nil || len(converted.Parts) != 1 || converted.Parts[0].Content == nil {
138-
t.Fatalf("convertLineMessage returned %#v, want one message part", converted)
139-
}
140-
content := converted.Parts[0].Content
141-
if content.MsgType != event.MsgNotice {
142-
t.Fatalf("MsgType = %s, want %s", content.MsgType, event.MsgNotice)
143-
}
144-
if strings.Contains(content.Body, fallbackText) {
145-
t.Fatalf("Body = %q, must not contain LINE's unsupported-client fallback", content.Body)
146-
}
147125
expectedBody := "You received a LINE note.\n\nPreview:\nShared note preview\n\n" +
148126
"Open in LINE: https://line.me/R/group/home/posts/post?example=shared"
149-
if content.Body != expectedBody {
150-
t.Fatalf("Body = %q, want %q", content.Body, expectedBody)
127+
128+
tests := []struct {
129+
name string
130+
bodyText string
131+
unwrappedText string
132+
decryptionFailed bool
133+
}{
134+
{
135+
name: "unsupported text fallback",
136+
bodyText: fallbackText,
137+
unwrappedText: fallbackText,
138+
},
139+
{
140+
name: "decryption failure",
141+
decryptionFailed: true,
142+
},
143+
}
144+
145+
for _, test := range tests {
146+
t.Run(test.name, func(t *testing.T) {
147+
converted, err := lc.convertLineMessage(
148+
t.Context(),
149+
nil,
150+
nil,
151+
data,
152+
test.bodyText,
153+
test.unwrappedText,
154+
test.decryptionFailed,
155+
)
156+
if err != nil {
157+
t.Fatalf("convertLineMessage returned error: %v", err)
158+
}
159+
if converted == nil || len(converted.Parts) != 1 || converted.Parts[0].Content == nil {
160+
t.Fatalf("convertLineMessage returned %#v, want one message part", converted)
161+
}
162+
content := converted.Parts[0].Content
163+
if content.MsgType != event.MsgNotice {
164+
t.Fatalf("MsgType = %s, want %s", content.MsgType, event.MsgNotice)
165+
}
166+
if strings.Contains(content.Body, fallbackText) {
167+
t.Fatalf("Body = %q, must not contain LINE's unsupported-client fallback", content.Body)
168+
}
169+
if content.Body != expectedBody {
170+
t.Fatalf("Body = %q, want %q", content.Body, expectedBody)
171+
}
172+
})
151173
}
152174
}
153175

pkg/connector/handlers/post_notification.go

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package handlers
22

33
import (
4+
"html"
45
"strings"
56

67
"maunium.net/go/mautrix/bridgev2"
@@ -46,15 +47,24 @@ func (*Handler) ConvertPostNotification(data line.Message, relatesTo *event.Rela
4647
body.WriteString("\n\nOpen LINE for full details.")
4748
}
4849

50+
content := &event.MessageEventContent{
51+
MsgType: event.MsgNotice,
52+
Body: body.String(),
53+
RelatesTo: relatesTo,
54+
}
55+
if postURL != "" {
56+
plainPrefix := strings.TrimSuffix(content.Body, postURL)
57+
escapedURL := html.EscapeString(postURL)
58+
content.Format = event.FormatHTML
59+
content.FormattedBody = strings.ReplaceAll(html.EscapeString(plainPrefix), "\n", "<br>") +
60+
`<a href="` + escapedURL + `">` + escapedURL + `</a>`
61+
}
62+
4963
return &bridgev2.ConvertedMessage{
5064
Parts: []*bridgev2.ConvertedMessagePart{
5165
{
52-
Type: event.EventMessage,
53-
Content: &event.MessageEventContent{
54-
MsgType: event.MsgNotice,
55-
Body: body.String(),
56-
RelatesTo: relatesTo,
57-
},
66+
Type: event.EventMessage,
67+
Content: content,
5868
},
5969
},
6070
}, nil

pkg/connector/handlers/post_notification_test.go

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@ import (
1212
func TestConvertPostNotification(t *testing.T) {
1313
relatesTo := &event.RelatesTo{}
1414
tests := []struct {
15-
name string
16-
metadata map[string]string
17-
expected string
15+
name string
16+
metadata map[string]string
17+
expected string
18+
expectedHTML string
1819
}{
1920
{
2021
name: "note with multiline preview and link",
@@ -25,16 +26,22 @@ func TestConvertPostNotification(t *testing.T) {
2526
},
2627
expected: "You received a LINE note.\n\nPreview:\nFirst line\nSecond line\n\n" +
2728
"Open in LINE: https://line.me/R/group/home/posts/post?example=1",
29+
expectedHTML: "You received a LINE note.<br><br>Preview:<br>First line<br>Second line<br><br>" +
30+
`Open in LINE: <a href="https://line.me/R/group/home/posts/post?example=1">` +
31+
"https://line.me/R/group/home/posts/post?example=1</a>",
2832
},
2933
{
30-
name: "album with name and deep link",
34+
name: "album with escaped name and deep link",
3135
metadata: map[string]string{
3236
"serviceType": "AB",
33-
"albumName": "Summer photos",
34-
"postEndUrl": "line://group/home/albums/album?example=1",
37+
"albumName": "Summer <photos>",
38+
"postEndUrl": "line://group/home/albums/album?example=1&source=chat",
3539
},
36-
expected: "LINE album update: Summer photos\n\n" +
37-
"Open in LINE: line://group/home/albums/album?example=1",
40+
expected: "LINE album update: Summer <photos>\n\n" +
41+
"Open in LINE: line://group/home/albums/album?example=1&source=chat",
42+
expectedHTML: "LINE album update: Summer &lt;photos&gt;<br><br>" +
43+
`Open in LINE: <a href="line://group/home/albums/album?example=1&amp;source=chat">` +
44+
"line://group/home/albums/album?example=1&amp;source=chat</a>",
3845
},
3946
{
4047
name: "missing metadata",
@@ -60,12 +67,12 @@ func TestConvertPostNotification(t *testing.T) {
6067
if err != nil {
6168
t.Fatalf("ConvertPostNotification returned error: %v", err)
6269
}
63-
assertPostNotificationContent(t, converted, test.expected, relatesTo)
70+
assertPostNotificationContent(t, converted, test.expected, test.expectedHTML, relatesTo)
6471
})
6572
}
6673
}
6774

68-
func assertPostNotificationContent(t *testing.T, converted *bridgev2.ConvertedMessage, expectedBody string, relatesTo *event.RelatesTo) {
75+
func assertPostNotificationContent(t *testing.T, converted *bridgev2.ConvertedMessage, expectedBody, expectedHTML string, relatesTo *event.RelatesTo) {
6976
t.Helper()
7077
if converted == nil || len(converted.Parts) != 1 || converted.Parts[0].Content == nil {
7178
t.Fatalf("converted = %#v, want one message part", converted)
@@ -80,6 +87,18 @@ func assertPostNotificationContent(t *testing.T, converted *bridgev2.ConvertedMe
8087
if part.Content.Body != expectedBody {
8188
t.Fatalf("body = %q, want %q", part.Content.Body, expectedBody)
8289
}
90+
if expectedHTML == "" {
91+
if part.Content.Format != "" || part.Content.FormattedBody != "" {
92+
t.Fatalf("formatted message = %q / %q, want plain text only", part.Content.Format, part.Content.FormattedBody)
93+
}
94+
} else {
95+
if part.Content.Format != event.FormatHTML {
96+
t.Fatalf("format = %q, want %q", part.Content.Format, event.FormatHTML)
97+
}
98+
if part.Content.FormattedBody != expectedHTML {
99+
t.Fatalf("formatted body = %q, want %q", part.Content.FormattedBody, expectedHTML)
100+
}
101+
}
83102
if part.Content.RelatesTo != relatesTo {
84103
t.Fatalf("relates_to = %#v, want original pointer %#v", part.Content.RelatesTo, relatesTo)
85104
}

0 commit comments

Comments
 (0)