Skip to content

connector: resend group messages as plain text when LINE says a member has Letter Sealing off - #311

Open
indent[bot] wants to merge 2 commits into
mainfrom
indent/line-plain-retry-member-settings-off
Open

indent[bot] wants to merge 2 commits into
mainfrom
indent/line-plain-retry-member-settings-off

Conversation

@indent

@indent indent Bot commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

Fixes PLAT-40295

Logs: In 24h, prod logged Failed to handle Matrix message with TalkException code 98 "member settings off" from SendMessage. That's 8 lines from 3 accounts across 4 groups. Before each failure the same three lines appear: No group key found, auto-registering, then Auto-register group key failed (code 98), then fetchAndUnwrapGroupKey before encrypt failed. Neither the plain-text fallback warning nor Sending plain text message (no E2EE) shows up.

Cause: Once a member turns Letter Sealing off, LINE stops serving a group key. The fetch error counts as "no usable key", but EncryptGroupMessageRaw still succeeds with the group key the runtime cached earlier. LINE then rejects the encrypted send with code 98. Nothing after the send handles that code, so the message fails. The plain-text fallback only runs when encryption itself fails. Groups with no cached key already take that fallback and send fine (156 a day across all clusters).

Change: If SendMessage rejects an encrypted group message with code 98 "member settings off", the bridge now marks the group as no-E2EE (the existing 1h cache), converts the message to plain text and resends once. The conversion is the same code the existing fallback uses, moved into one closure, and it keeps the sticon REPLACE metadata. A new line.IsMemberSettingsOff helper parses the TalkException.

Before / after (sweep tester, 2026-11-10; base ae61689 vs head 0923a43): go build ./..., go vet (all but pkg/ltsm) and go test ./... pass on both. Not a real account: the real HandleMatrixMessage, fetchAndUnwrapGroupKey and LTSM group encryption ran against a stubbed LINE HTTP transport. The group key is fetched and unwrapped once, so it's cached like a group that used to have Letter Sealing. After that LINE answers the way prod logs show: getLastE2EEGroupSharedKey gives code 5, auto-register gives code 98, and the encrypted sendMessage gives code 98 "member settings off". The harness is untracked and nothing was committed.

Case Base Head
Group text, cached key, code 98 on encrypted send Fails with the code 98 error after 1 encrypted send. Same 3 log lines as prod (No group key found, auto-registering, Auto-register group key failed, fetchAndUnwrapGroupKey before encrypt failed), no fallback Logs the warning ...member has Letter Sealing off, resending as plain text, then 1 plain resend with the same text. Delivered, group marked no-E2EE
Second group send within the hour Fails the same way Goes plain on the first try, 1 send
Same, with getLastE2EEGroupSharedKey returning code 98 directly Fails Plain resend, delivered
Group image (E2EE media flow) Fails Resends plain without OID/SID/ENC_KM/e2eeVersion. The original PNG bytes go to r/talk/m/<id> plus a preview
Group file rejected with "Extension does not support file upload", then the ZIP retry gets code 98 Fails after 2 encrypted sends Third send is plain with FILE_NAME=notes.xyz.zip and FILE_SIZE=166. The upload is the same 166-byte valid zip and contains the original file
Sticon text Fails. The encrypted send carries no REPLACE in metadata The plain resend has the original REPLACE and STICON_OWNERSHIP metadata
Other TalkException (code 35, or code 98 with another reason) Error, 1 send Unchanged: error, 1 send, group not marked
DM, code 98 on encrypted send Error, 1 send Unchanged, no retry
Plain resend also fails (code 35, or 98 again) n/a Returns that error after exactly 2 sends, no loop
line.IsMemberSettingsOff n/a True for raw, %w-wrapped and double-wrapped errors and for a reason in another case. False for code 99, code 98 with another reason, a non-TalkException, plain text, nil and ErrNoUsableE2EEGroupKey

Note, not caused by this PR: on the image fallback FILE_SIZE keeps the encrypted size (174 vs 142 plain bytes). The existing pre-send fallback does the same.

Real-account blocker: none of the sweep's accounts is a LINE account, and LINE login needs a phone-registered account (email/password plus PIN, or a QR scan from the LINE app). The run can't create one unattended. To get a real before/after, a person needs to sign a LINE test account into both builds. The account must be in a group whose key it already cached, then another member turns Letter Sealing off before a text and an image are sent.

View in Indent
Tag @indent to continue the conversation here.

@indent

indent Bot commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor Author

Issues

All clear! No issues remaining. 🎉

1 issue already resolved
  • If an encrypted group file send is retried as a ZIP and that ZIP send fails with code 98, fallBackToPlainText() sets plainMediaData = originalMediaData, the raw pre-zip bytes. FILE_NAME and FILE_SIZE still describe the zip, so recipients get raw bytes labeled as a zip with the wrong size. (fixed by commit 0923a43)
    Found by Indent Review Agent

CI Checks

All CI checks passed on 0923a43.

Review agents

Select any unchecked box below to run or rerun that agent.

Passed (1)
  • Indent Review Agent · No remaining issues found
Full results

Indent Review Agent

  • Summary: No remaining issues found
  • Last ran on commit: 0923a431
  • Latest result
    {
      "summary": "No remaining issues found",
      "findings": []
    }

@indent indent Bot added the to be tested label Nov 10, 2026
Comment thread pkg/connector/send_message.go Resolved
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Development

Successfully merging this pull request may close these issues.

1 participant