From b424617c3c6beebbadb07b0b1a434ea91e4fcd60 Mon Sep 17 00:00:00 2001 From: highesttt Date: Wed, 1 Jul 2026 09:47:15 -0400 Subject: [PATCH 1/2] fix: refresh auth issue --- pkg/connector/handle_message.go | 10 +++++-- pkg/connector/handle_message_test.go | 42 ++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 pkg/connector/handle_message_test.go diff --git a/pkg/connector/handle_message.go b/pkg/connector/handle_message.go index 902acd0..8c4e599 100644 --- a/pkg/connector/handle_message.go +++ b/pkg/connector/handle_message.go @@ -23,6 +23,8 @@ import ( "github.com/highesttt/matrix-line-messenger/pkg/line" ) +const lineDecryptFallbackText = "[Unable to decrypt message. Open an issue on GitHub.]" + func (lc *LineClient) newMessageHandler() *handlers.Handler { return &handlers.Handler{ Log: lc.UserLogin.Bridge.Log, @@ -208,8 +210,8 @@ func (lc *LineClient) parseMessageTimestamp(msg *line.Message) time.Time { func (lc *LineClient) decryptMessageBody(msg *line.Message, portalIDStr string, opType int) (bodyText, unwrappedText string) { // Handle Content bodyText = msg.Text - if bodyText == "" && len(msg.Chunks) > 0 { - bodyText = "[Unable to decrypt message. Open an issue on GitHub.]" + if len(msg.Chunks) > 0 && (bodyText == "" || isLineDecryptFallbackText(bodyText)) { + bodyText = "" if lc.E2EE != nil { // Ensure peer keys are available before attempting decryption lc.ensurePeerKeyForMessage(context.Background(), msg) @@ -304,6 +306,10 @@ func (lc *LineClient) decryptMessageBody(msg *line.Message, portalIDStr string, return bodyText, unwrappedText } +func isLineDecryptFallbackText(text string) bool { + return strings.TrimSpace(text) == lineDecryptFallbackText +} + // convertLineMessage converts an inbound LINE message into a Matrix // ConvertedMessage. bodyText/unwrappedText are the (decrypted) message text as // returned by decryptMessageBody. Shared by the live message path and backfill. diff --git a/pkg/connector/handle_message_test.go b/pkg/connector/handle_message_test.go new file mode 100644 index 0000000..87faa73 --- /dev/null +++ b/pkg/connector/handle_message_test.go @@ -0,0 +1,42 @@ +package connector + +import ( + "testing" + + "github.com/highesttt/matrix-line-messenger/pkg/line" +) + +func TestDecryptMessageBodySkipsGeneratedFallbackWhenDecryptUnavailable(t *testing.T) { + msg := &line.Message{ + Text: "", + Chunks: []string{"encrypted"}, + } + + bodyText, unwrappedText := (&LineClient{}).decryptMessageBody(msg, "chat-mid", int(OpReceiveMessage)) + if bodyText != "" || unwrappedText != "" { + t.Fatalf("decryptMessageBody returned body=%q unwrapped=%q, want empty strings", bodyText, unwrappedText) + } +} + +func TestDecryptMessageBodyTreatsLineFallbackAsEncryptedFailure(t *testing.T) { + msg := &line.Message{ + Text: lineDecryptFallbackText, + Chunks: []string{"encrypted"}, + } + + bodyText, unwrappedText := (&LineClient{}).decryptMessageBody(msg, "chat-mid", int(OpReceiveMessage)) + if bodyText != "" || unwrappedText != "" { + t.Fatalf("decryptMessageBody returned body=%q unwrapped=%q, want empty strings", bodyText, unwrappedText) + } +} + +func TestDecryptMessageBodyKeepsFallbackTextWithoutEncryptedChunks(t *testing.T) { + msg := &line.Message{ + Text: lineDecryptFallbackText, + } + + bodyText, unwrappedText := (&LineClient{}).decryptMessageBody(msg, "chat-mid", int(OpReceiveMessage)) + if bodyText != lineDecryptFallbackText || unwrappedText != lineDecryptFallbackText { + t.Fatalf("decryptMessageBody returned body=%q unwrapped=%q, want fallback text", bodyText, unwrappedText) + } +} From 49856414ea2f591e05cd31db94445db79508dbc3 Mon Sep 17 00:00:00 2001 From: highesttt Date: Wed, 1 Jul 2026 10:15:41 -0400 Subject: [PATCH 2/2] fix: properly assert recovery --- pkg/connector/handle_message.go | 40 +++++++++++++++++++---- pkg/connector/handle_message_test.go | 48 ++++++++++++++++++++++------ pkg/connector/sync.go | 4 +-- 3 files changed, 74 insertions(+), 18 deletions(-) diff --git a/pkg/connector/handle_message.go b/pkg/connector/handle_message.go index 8c4e599..13abba9 100644 --- a/pkg/connector/handle_message.go +++ b/pkg/connector/handle_message.go @@ -23,7 +23,10 @@ import ( "github.com/highesttt/matrix-line-messenger/pkg/line" ) -const lineDecryptFallbackText = "[Unable to decrypt message. Open an issue on GitHub.]" +const ( + lineDecryptFallbackText = "[Unable to decrypt message. Open an issue on GitHub.]" + lineDecryptFailureNoticeText = "[Unable to decrypt LINE message.]" +) func (lc *LineClient) newMessageHandler() *handlers.Handler { return &handlers.Handler{ @@ -123,7 +126,7 @@ func (lc *LineClient) queueIncomingMessage(msg *line.Message, opType int) { lc.ensureGroupMessageSenderKnown(portalIDStr, msg.From, ts) senderID := makeUserID(msg.From) - bodyText, unwrappedText := lc.decryptMessageBody(msg, portalIDStr, opType) + bodyText, unwrappedText, decryptionFailed := lc.decryptMessageBody(msg, portalIDStr, opType) messageEvent := &simplevent.Message[line.Message]{ EventMeta: simplevent.EventMeta{ @@ -140,7 +143,7 @@ func (lc *LineClient) queueIncomingMessage(msg *line.Message, opType int) { Data: *msg, ID: networkid.MessageID(msg.ID), ConvertMessageFunc: func(ctx context.Context, portal *bridgev2.Portal, intent bridgev2.MatrixAPI, data line.Message) (*bridgev2.ConvertedMessage, error) { - return lc.convertLineMessage(ctx, portal, intent, data, bodyText, unwrappedText) + return lc.convertLineMessage(ctx, portal, intent, data, bodyText, unwrappedText, decryptionFailed) }, } @@ -207,11 +210,12 @@ func (lc *LineClient) parseMessageTimestamp(msg *line.Message) time.Time { // decryptMessageBody runs E2EE decryption (when needed) for an inbound message // and returns the plaintext body plus the JSON-unwrapped text. Shared by the // live message path (queueIncomingMessage) and the backfill path (FetchMessages). -func (lc *LineClient) decryptMessageBody(msg *line.Message, portalIDStr string, opType int) (bodyText, unwrappedText string) { +func (lc *LineClient) decryptMessageBody(msg *line.Message, portalIDStr string, opType int) (bodyText, unwrappedText string, decryptionFailed bool) { // Handle Content bodyText = msg.Text if len(msg.Chunks) > 0 && (bodyText == "" || isLineDecryptFallbackText(bodyText)) { bodyText = "" + decryptionFailed = true if lc.E2EE != nil { // Ensure peer keys are available before attempting decryption lc.ensurePeerKeyForMessage(context.Background(), msg) @@ -237,6 +241,7 @@ func (lc *LineClient) decryptMessageBody(msg *line.Message, portalIDStr string, pt, keyID, err := lc.E2EE.DecryptGroupMessage(msg, portalIDStr) if err == nil { bodyText = pt + decryptionFailed = false } else { groupDecryptLogContext(lc.UserLogin.Bridge.Log.Debug().Err(err), msg, portalIDStr, opType). Msg("DecryptGroupMessage failed, trying to fetch key") @@ -246,6 +251,7 @@ func (lc *LineClient) decryptMessageBody(msg *line.Message, portalIDStr string, Msg("Failed to fetch/unwrap group key") } else if ptRetry, _, errRetry := lc.E2EE.DecryptGroupMessage(msg, portalIDStr); errRetry == nil { bodyText = ptRetry + decryptionFailed = false } else { groupDecryptLogContext(lc.UserLogin.Bridge.Log.Warn().Err(errRetry), msg, portalIDStr, opType). Msg("DecryptGroupMessage failed after group key refresh") @@ -256,6 +262,7 @@ func (lc *LineClient) decryptMessageBody(msg *line.Message, portalIDStr string, // 1-1 Decryption if pt, err := lc.E2EE.DecryptMessageV2(msg); err == nil { bodyText = pt + decryptionFailed = false } else { lc.UserLogin.Bridge.Log.Debug().Err(err).Msg("DecryptMessageV2 failed on first attempt") if _, _, errKey := lc.E2EE.MyKeyIDs(); errKey != nil { @@ -284,6 +291,7 @@ func (lc *LineClient) decryptMessageBody(msg *line.Message, portalIDStr string, } if ptRetry, errRetry := lc.E2EE.DecryptMessageV2(msg); errRetry == nil { bodyText = ptRetry + decryptionFailed = false } else { lc.UserLogin.Bridge.Log.Warn().Err(errRetry).Msg("DecryptMessageV2 failed on retry") } @@ -303,21 +311,39 @@ func (lc *LineClient) decryptMessageBody(msg *line.Message, portalIDStr string, } } } - return bodyText, unwrappedText + return bodyText, unwrappedText, decryptionFailed } func isLineDecryptFallbackText(text string) bool { + // LINE may include this historical fallback in Text while still sending + // encrypted chunks. Treat it as a decrypt marker, not user-authored text. return strings.TrimSpace(text) == lineDecryptFallbackText } // convertLineMessage converts an inbound LINE message into a Matrix // ConvertedMessage. bodyText/unwrappedText are the (decrypted) message text as // returned by decryptMessageBody. Shared by the live message path and backfill. -func (lc *LineClient) convertLineMessage(ctx context.Context, portal *bridgev2.Portal, intent bridgev2.MatrixAPI, data line.Message, bodyText, unwrappedText string) (*bridgev2.ConvertedMessage, error) { +func (lc *LineClient) convertLineMessage(ctx context.Context, portal *bridgev2.Portal, intent bridgev2.MatrixAPI, data line.Message, bodyText, unwrappedText string, decryptionFailed bool) (*bridgev2.ConvertedMessage, error) { decryptedBody := bodyText - h := lc.newMessageHandler() replyRelatesTo := lc.resolveReplyRelatesTo(ctx, &data) + if decryptionFailed && strings.TrimSpace(unwrappedText) == "" && ContentType(data.ContentType) == ContentText { + return &bridgev2.ConvertedMessage{ + Parts: []*bridgev2.ConvertedMessagePart{ + { + Type: event.EventMessage, + Content: &event.MessageEventContent{ + MsgType: event.MsgNotice, + Body: lineDecryptFailureNoticeText, + RelatesTo: replyRelatesTo, + }, + }, + }, + }, nil + } + + h := lc.newMessageHandler() + // Handle call events (ORGCONTP == "CALL") if data.ContentMetadata["ORGCONTP"] == "CALL" { return h.ConvertCall(data, replyRelatesTo) diff --git a/pkg/connector/handle_message_test.go b/pkg/connector/handle_message_test.go index 87faa73..7469901 100644 --- a/pkg/connector/handle_message_test.go +++ b/pkg/connector/handle_message_test.go @@ -3,6 +3,8 @@ package connector import ( "testing" + "maunium.net/go/mautrix/event" + "github.com/highesttt/matrix-line-messenger/pkg/line" ) @@ -12,9 +14,9 @@ func TestDecryptMessageBodySkipsGeneratedFallbackWhenDecryptUnavailable(t *testi Chunks: []string{"encrypted"}, } - bodyText, unwrappedText := (&LineClient{}).decryptMessageBody(msg, "chat-mid", int(OpReceiveMessage)) - if bodyText != "" || unwrappedText != "" { - t.Fatalf("decryptMessageBody returned body=%q unwrapped=%q, want empty strings", bodyText, unwrappedText) + bodyText, unwrappedText, decryptionFailed := (&LineClient{}).decryptMessageBody(msg, "chat-mid", int(OpReceiveMessage)) + if bodyText != "" || unwrappedText != "" || !decryptionFailed { + t.Fatalf("decryptMessageBody returned body=%q unwrapped=%q failed=%v, want empty strings and failed=true", bodyText, unwrappedText, decryptionFailed) } } @@ -24,9 +26,9 @@ func TestDecryptMessageBodyTreatsLineFallbackAsEncryptedFailure(t *testing.T) { Chunks: []string{"encrypted"}, } - bodyText, unwrappedText := (&LineClient{}).decryptMessageBody(msg, "chat-mid", int(OpReceiveMessage)) - if bodyText != "" || unwrappedText != "" { - t.Fatalf("decryptMessageBody returned body=%q unwrapped=%q, want empty strings", bodyText, unwrappedText) + bodyText, unwrappedText, decryptionFailed := (&LineClient{}).decryptMessageBody(msg, "chat-mid", int(OpReceiveMessage)) + if bodyText != "" || unwrappedText != "" || !decryptionFailed { + t.Fatalf("decryptMessageBody returned body=%q unwrapped=%q failed=%v, want empty strings and failed=true", bodyText, unwrappedText, decryptionFailed) } } @@ -35,8 +37,36 @@ func TestDecryptMessageBodyKeepsFallbackTextWithoutEncryptedChunks(t *testing.T) Text: lineDecryptFallbackText, } - bodyText, unwrappedText := (&LineClient{}).decryptMessageBody(msg, "chat-mid", int(OpReceiveMessage)) - if bodyText != lineDecryptFallbackText || unwrappedText != lineDecryptFallbackText { - t.Fatalf("decryptMessageBody returned body=%q unwrapped=%q, want fallback text", bodyText, unwrappedText) + bodyText, unwrappedText, decryptionFailed := (&LineClient{}).decryptMessageBody(msg, "chat-mid", int(OpReceiveMessage)) + if bodyText != lineDecryptFallbackText || unwrappedText != lineDecryptFallbackText || decryptionFailed { + t.Fatalf("decryptMessageBody returned body=%q unwrapped=%q failed=%v, want fallback text and failed=false", bodyText, unwrappedText, decryptionFailed) + } +} + +func TestConvertLineMessageReturnsNoticeForDecryptFailure(t *testing.T) { + converted, err := (&LineClient{}).convertLineMessage( + t.Context(), + nil, + nil, + line.Message{ContentType: int(ContentText)}, + "", + "", + true, + ) + if err != nil { + t.Fatalf("convertLineMessage returned error: %v", err) + } + if converted == nil || len(converted.Parts) != 1 { + t.Fatalf("convertLineMessage returned %#v, want one notice part", converted) + } + content := converted.Parts[0].Content + if content.MsgType != event.MsgNotice { + t.Fatalf("MsgType = %s, want %s", content.MsgType, event.MsgNotice) + } + if content.Body != lineDecryptFailureNoticeText { + t.Fatalf("Body = %q, want %q", content.Body, lineDecryptFailureNoticeText) + } + if content.Body == lineDecryptFallbackText { + t.Fatal("notice body must not reuse LINE's historical fallback text") } } diff --git a/pkg/connector/sync.go b/pkg/connector/sync.go index 34bca86..69cb03c 100644 --- a/pkg/connector/sync.go +++ b/pkg/connector/sync.go @@ -336,8 +336,8 @@ func (lc *LineClient) FetchMessages(ctx context.Context, params bridgev2.FetchMe if msg.From == lc.Mid { opType = OpSendMessage } - bodyText, unwrappedText := lc.decryptMessageBody(msg, chatMID, int(opType)) - converted, err := lc.convertLineMessage(ctx, params.Portal, intent, *msg, bodyText, unwrappedText) + bodyText, unwrappedText, decryptionFailed := lc.decryptMessageBody(msg, chatMID, int(opType)) + converted, err := lc.convertLineMessage(ctx, params.Portal, intent, *msg, bodyText, unwrappedText, decryptionFailed) if err != nil { lc.UserLogin.Bridge.Log.Warn().Err(err).Str("msg_id", msg.ID).Str("chat_mid", chatMID).Msg("Failed to convert message for backfill") continue