Skip to content

fix: backfilling system events and reactions#224

Open
highesttt wants to merge 3 commits into
mainfrom
highest/plat-38142
Open

fix: backfilling system events and reactions#224
highesttt wants to merge 3 commits into
mainfrom
highest/plat-38142

Conversation

@highesttt

Copy link
Copy Markdown
Collaborator

No description provided.

@linear-code

linear-code Bot commented Jul 24, 2026

Copy link
Copy Markdown

PLAT-38142

@indent-zero

indent-zero Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor
PR Summary

Fixes startup backfill to replay historical system events (renames, joins, leaves, invites) and reactions from getRecentMessagesV2, which were previously dropped. Historical system messages now insert a DontBridge: true marker row for DB dedup while synchronously applying the corresponding ChatInfoChange, and after backfill a hidden syncChats re-fetches LINE's authoritative state to correct any drift.

  • Adds Message.Reactions []MessageReaction decoding (with json.Number for atMillis) and a new queueMessageReactionSync that emits a simplevent.ReactionSync for both freshly-queued and already-existing messages during backfill; skips ContentSystem markers to avoid targeting fake-MXID rows.
  • Factors icon download+upload out of handlePaidReaction/handlePredefinedReaction into shared getPaidReactionMXC/getPredefinedReactionMXC helpers, backed by a new per-URL paidReactionIconMXC cache; paid-reaction uploads now go through the bot intent instead of the sender ghost. A shared reactionUploadMXC helper explicitly rejects encrypted media so reaction icons never cache an undecryptable MXC.
  • Adds ContentSystem (contentType=18) constant and refactors handleSystemMessage into a pure makeSystemMessageEvent + isHandledSystemMessage pair shared by live SSE and the new queueHistoricalSystemMessage; the live path now debug-logs unhandled LOC_KEYs so new system-message types remain observable. queueHistoricalSystemMessage applies the historical state via portal.ProcessChatInfoChange directly to avoid the deprecated Internal().HandleRemoteChatInfoChange wrapper.
  • Threads a bool return through queueIncomingMessage and backfillRecentMessages so prefetchMessages can trigger a post-backfill syncChats only when at least one historical system event was actually queued. syncChats is split into a wg-tracked wrapper and a syncChatsNow core so synchronous callers (Connect, prefetchMessages) invoke the core directly without the fragile inline wg.Add(1)/deferred-Done pairing.
  • Adds test coverage for reaction unmarshaling (missing vs empty), reaction conversion, system-message event construction, unknown-LOC_KEY logging, encrypted-upload rejection, and system-marker reaction-sync suppression.

Issues

All clear! No issues remaining. 🎉

4 issues already resolved
  • Encrypted-upload fallback caches an undecryptable MXC for reaction icons: when Bot.UploadMedia returns encrypted media (empty MXC + non-nil uploadedFile), getPredefinedReactionMXC/getPaidReactionMXC store uploadedFile.URL as the reaction key, which Matrix clients cannot decrypt. (fixed by commit 6c1e025)
  • Redundant wg.Add(1) around a synchronous syncChats(ctx) call in prefetchMessages: syncChats defers wg.Done, so the add/done pair cancels out inline; the counter oscillation is fragile because removing the Add(1) would panic on the deferred Done. (fixed by commit 6c1e025)
  • queueMessageReactionSync runs against DontBridge system-message markers: existing-message backfill dedup calls the reaction sync for every message including historical system-message rows inserted with a fake MXID, so any reactions LINE ever attaches to a contentType=18 record will be routed to a Matrix event that doesn't exist. (fixed by commit 6c1e025)
  • Live-path observability regression: the default: Debug().Msg("Unhandled system message LOC_KEY") log was dropped in the refactor, so new/unknown LOC_KEYs seen via SSE now silently return handled=false without any warning. (fixed by commit 6c1e025)

CI Checks

All CI checks passed for 6c1e025.

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Added reaction synchronization and backfill for predefined and paid reactions, including icon-to-emoji handling and conversion from embedded reaction history.
    • Reactions from historical messages now include sender and timestamp details.
    • Enhanced historical system-message processing to replay chat info changes more reliably (including membership updates).
    • Added parsing support for reactions embedded in message payloads.
  • Bug Fixes
    • Improved handling of unsupported message/reaction types and ensured system markers don’t trigger reaction sync.
  • Tests
    • Added unit tests covering reaction conversion, upload validation, and historical system-message event handling.

Walkthrough

LINE reaction payloads are parsed and converted into cached MXC-backed reactions for backfill and synchronization. Historical system messages are classified, converted into chat-info events, and applied during backfill with updated queue and startup synchronization handling.

Changes

Reaction and historical synchronization

Layer / File(s) Summary
Reaction payload contract
pkg/line/reaction.go, pkg/line/structs.go, pkg/line/*_test.go
LINE messages now parse embedded reactions with sender MIDs, millisecond timestamps, and predefined or paid reaction types, including missing-versus-empty array behavior.
Reaction icon conversion and sync
pkg/connector/client.go, pkg/connector/reaction.go, pkg/connector/sync.go, pkg/connector/reaction_test.go
Predefined and paid reaction icons are cached as MXC URIs, converted into backfill reactions, and queued through reaction synchronization events; live reaction handlers reuse the same icon resolution helpers.
Historical system-message processing
pkg/connector/consts.go, pkg/connector/sync.go, pkg/connector/sync_test.go
System messages use ContentSystem, supported historical records become ChatInfoChange events, and historical membership changes are applied through portal internals.
Backfill and queue orchestration
pkg/connector/handle_message.go, pkg/connector/sync.go
Backfill includes reactions, routes system messages separately, reports queue success, tracks system-event processing, and conditionally triggers startup chat synchronization.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant LINE
  participant LineClient
  participant BridgeWorker
  participant Portal
  LINE->>LineClient: return recent messages with reactions
  LineClient->>LineClient: convert reactions and resolve MXC icons
  LineClient->>BridgeWorker: queue backfill and ReactionSync events
  BridgeWorker->>Portal: apply messages and reactions
  LINE->>LineClient: return historical system messages
  LineClient->>Portal: apply ChatInfoChange events
Loading

Possibly related PRs

  • beeper/line#217: Updates overlapping reaction event sender and actor resolution paths in pkg/connector/sync.go.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Description check ❓ Inconclusive No description was provided, so there is no meaningful summary to evaluate. Add a brief description of the sync/backfill changes and affected message types.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: backfilling system events and reactions.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch highest/plat-38142

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
pkg/connector/reaction_test.go (1)

522-574: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider covering the complete == false and empty/nil Reactions cases.

The current test only exercises the cache-hit happy path. The behavioral hinges that protect against data loss are elsewhere: complete == false must suppress the sync (otherwise HasAllUsers: true redacts reactions LINE still reports), and Reactions == [] must produce an intentional clear-all. A couple of small table cases would lock those in.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/connector/reaction_test.go` around lines 522 - 574, Extend
TestConvertMessageReactionsUsesEmbeddedHistory with table-driven cases for
incomplete conversion and empty or nil msg.Reactions. Verify complete == false
suppresses synchronization, while both empty and nil reactions intentionally
produce a clear-all result. Preserve the existing cache-hit assertions for
populated reactions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pkg/connector/reaction.go`:
- Around line 340-343: Update the per-reaction timestamp initialization near
reaction.AtMillis.Int64 so missing, malformed, or non-positive atMillis values
fall back to the enclosing message timestamp instead of remaining zero. Preserve
the parsed time.UnixMilli value for valid positive atMillis values, ensuring
queueMessageReactionSync and FetchMessages receive a usable reaction timestamp.
- Around line 260-271: Update the paid reaction icon download flow around
lc.HTTPClient.Get to create and use a context with the operation’s ctx and
ensure cancellation propagates during shutdown. Replace unbounded io.ReadAll of
resp.Body with a bounded read using the established maximum icon size, while
preserving the existing HTTP-status and error-wrapping behavior.

---

Nitpick comments:
In `@pkg/connector/reaction_test.go`:
- Around line 522-574: Extend TestConvertMessageReactionsUsesEmbeddedHistory
with table-driven cases for incomplete conversion and empty or nil
msg.Reactions. Verify complete == false suppresses synchronization, while both
empty and nil reactions intentionally produce a clear-all result. Preserve the
existing cache-hit assertions for populated reactions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bc351f2f-0cb0-424b-90e3-c3550f79b556

📥 Commits

Reviewing files that changed from the base of the PR and between 6329262 and 9dd3e2b.

📒 Files selected for processing (10)
  • pkg/connector/client.go
  • pkg/connector/consts.go
  • pkg/connector/handle_message.go
  • pkg/connector/reaction.go
  • pkg/connector/reaction_test.go
  • pkg/connector/sync.go
  • pkg/connector/sync_test.go
  • pkg/line/reaction.go
  • pkg/line/structs.go
  • pkg/line/structs_test.go
📜 Review details
⏰ Context from checks skipped due to timeout. (4)
  • GitHub Check: build-docker
  • GitHub Check: Lint with 1.25
  • GitHub Check: build-docker
  • GitHub Check: Lint with 1.25
🧰 Additional context used
📓 Path-based instructions (2)
**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

**/*.go: Use go fmt for code formatting across all Go files
Use goimports with -local "github.com/highesttt/matrix-line-messenger" flag to group project-local imports correctly
Use zerolog for logging throughout the codebase
Do not use Msgf in logging; use Msg with structured fields instead
Use Stringer interface where applicable in Go code

Files:

  • pkg/connector/consts.go
  • pkg/line/structs.go
  • pkg/line/reaction.go
  • pkg/connector/client.go
  • pkg/line/structs_test.go
  • pkg/connector/handle_message.go
  • pkg/connector/reaction_test.go
  • pkg/connector/sync_test.go
  • pkg/connector/reaction.go
  • pkg/connector/sync.go
**/!(ltsm)/**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

**/!(ltsm)/**/*.go: Run staticcheck on all Go files excluding pkg/ltsm package (transpiled WASM code)
Run go vet on all Go files excluding pkg/ltsm package (transpiled WASM code)

Files:

  • pkg/connector/consts.go
  • pkg/line/structs.go
  • pkg/line/reaction.go
  • pkg/connector/client.go
  • pkg/line/structs_test.go
  • pkg/connector/handle_message.go
  • pkg/connector/reaction_test.go
  • pkg/connector/sync_test.go
  • pkg/connector/reaction.go
  • pkg/connector/sync.go
🔇 Additional comments (16)
pkg/line/reaction.go (1)

69-75: LGTM!

pkg/line/structs.go (1)

112-113: LGTM!

pkg/line/structs_test.go (1)

48-91: LGTM!

Also applies to: 93-109

pkg/connector/client.go (1)

82-82: LGTM!

pkg/connector/reaction.go (2)

206-245: LGTM!


353-395: LGTM!

pkg/connector/sync.go (7)

650-657: LGTM!


773-840: LGTM!


1197-1197: LGTM!

Also applies to: 2039-2039


2063-2083: LGTM!

Also applies to: 2093-2119


2468-2607: LGTM!


739-742: 🩺 Stability & Availability

No deadlock here — syncChats already owns the matching wg.Done(). The wg.Add(1) at this call site is balanced by defer lc.wg.Done() at the top of syncChats, so lc.wg.Wait() won’t hang on this path.

			> Likely an incorrect or invalid review comment.

2635-2658: 🗄️ Data Integrity & Integration

DontBridge rows still get inserted with a fake MXID, and chat-info updates already run through the normal portal event path.

			> Likely an incorrect or invalid review comment.
pkg/connector/consts.go (1)

41-41: LGTM!

pkg/connector/sync_test.go (1)

69-121: LGTM!

Also applies to: 123-161

pkg/connector/handle_message.go (1)

116-116: LGTM!

Also applies to: 126-126, 167-168

Comment thread pkg/connector/reaction.go
Comment on lines +260 to +271
resp, err := lc.HTTPClient.Get(iconURL)
if err != nil {
return "", fmt.Errorf("download paid reaction icon: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return "", fmt.Errorf("download paid reaction icon: HTTP %d", resp.StatusCode)
}
data, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("read paid reaction icon: %w", err)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Locate HTTPClient construction for LineClient and check for Timeout settings.
rg -nP -C4 'HTTPClient\s*[:=]' --type=go
rg -nP -C3 '&http\.Client\{' --type=go

Repository: beeper/line

Length of output: 149


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find all HTTP client construction/use sites related to the connector/client.
rg -n -C3 --type=go '\bHTTPClient\b|&http\.Client\{|http\.Client\{' .

# Inspect the file under review and nearby context.
sed -n '220,310p' pkg/connector/reaction.go

# Find the LineClient type and constructor.
rg -n -C4 --type=go 'type LineClient|func NewLineClient|NewLineClient\(' pkg

# Search for any request-timeout or context-aware download helpers around this path.
rg -n -C3 --type=go 'NewRequestWithContext|LimitReader|io\.ReadAll|StatusOK|Timeout:' pkg

Repository: beeper/line

Length of output: 34768


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the startup backfill / wait path mentioned in the comment.
rg -n -C4 --type=go 'queueMessageReactionSync|backfillRecentMessages|wg\.Wait\(\)|Wait\(\)' pkg/connector

# Inspect the surrounding implementation in the connector.
sed -n '520,690p' pkg/connector/connector.go

# Inspect the paid reaction download call site in full context.
sed -n '240,290p' pkg/connector/reaction.go

Repository: beeper/line

Length of output: 12191


Propagate ctx into the icon download and bound the body size. This runs on the startup backfill path, so a slow fetch can still hold up wg.Wait() on logout/shutdown even though lc.HTTPClient has a 10s timeout, and io.ReadAll is unbounded on remote content.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/connector/reaction.go` around lines 260 - 271, Update the paid reaction
icon download flow around lc.HTTPClient.Get to create and use a context with the
operation’s ctx and ensure cancellation propagates during shutdown. Replace
unbounded io.ReadAll of resp.Body with a bounded read using the established
maximum icon size, while preserving the existing HTTP-status and error-wrapping
behavior.

Comment thread pkg/connector/reaction.go
Comment on lines +340 to +343
var timestamp time.Time
if timestampMillis, err := reaction.AtMillis.Int64(); err == nil && timestampMillis > 0 {
timestamp = time.UnixMilli(timestampMillis)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Fall back to the message timestamp when atMillis is unusable.

A missing/malformed atMillis leaves Timestamp as the zero time.Time. queueMessageReactionSync only falls back for the aggregate event timestamp; the per-reaction value stays zero, and FetchMessages (pkg/connector/sync.go Line 650) puts these straight into BackfillMessage.Reactions, producing epoch-1970 reactions in the batch.

🔧 Proposed fix
-		var timestamp time.Time
-		if timestampMillis, err := reaction.AtMillis.Int64(); err == nil && timestampMillis > 0 {
-			timestamp = time.UnixMilli(timestampMillis)
-		}
+		timestamp := lc.parseMessageTimestamp(msg)
+		if timestampMillis, err := reaction.AtMillis.Int64(); err == nil && timestampMillis > 0 {
+			timestamp = time.UnixMilli(timestampMillis)
+		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var timestamp time.Time
if timestampMillis, err := reaction.AtMillis.Int64(); err == nil && timestampMillis > 0 {
timestamp = time.UnixMilli(timestampMillis)
}
timestamp := lc.parseMessageTimestamp(msg)
if timestampMillis, err := reaction.AtMillis.Int64(); err == nil && timestampMillis > 0 {
timestamp = time.UnixMilli(timestampMillis)
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/connector/reaction.go` around lines 340 - 343, Update the per-reaction
timestamp initialization near reaction.AtMillis.Int64 so missing, malformed, or
non-positive atMillis values fall back to the enclosing message timestamp
instead of remaining zero. Preserve the parsed time.UnixMilli value for valid
positive atMillis values, ensuring queueMessageReactionSync and FetchMessages
receive a usable reaction timestamp.

Comment thread pkg/connector/reaction.go Outdated
Comment thread pkg/connector/sync.go
Comment thread pkg/connector/sync.go
Comment thread pkg/connector/sync.go Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
pkg/connector/client.go (1)

82-82: 📐 Maintainability & Code Quality | 🔵 Trivial

Run the repo Go checks before merge. gofmt, goimports -local "github.com/highesttt/matrix-line-messenger", staticcheck, and go vet should pass for all Go packages except pkg/ltsm.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/connector/client.go` at line 82, Run gofmt, goimports with the
repository’s local import prefix, staticcheck, and go vet across all Go packages
except pkg/ltsm; resolve any reported issues and formatting changes before
merging, without modifying pkg/ltsm checks.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@pkg/connector/client.go`:
- Line 82: Run gofmt, goimports with the repository’s local import prefix,
staticcheck, and go vet across all Go packages except pkg/ltsm; resolve any
reported issues and formatting changes before merging, without modifying
pkg/ltsm checks.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 65d77776-cc29-4684-be83-b264ca5f50ce

📥 Commits

Reviewing files that changed from the base of the PR and between b418dc9 and 6c1e025.

📒 Files selected for processing (5)
  • pkg/connector/client.go
  • pkg/connector/reaction.go
  • pkg/connector/reaction_test.go
  • pkg/connector/sync.go
  • pkg/connector/sync_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • pkg/connector/reaction.go
  • pkg/connector/sync.go
📜 Review details
⏰ Context from checks skipped due to timeout. (4)
  • GitHub Check: build-docker
  • GitHub Check: Lint with 1.25
  • GitHub Check: build-docker
  • GitHub Check: Lint with 1.25
🧰 Additional context used
📓 Path-based instructions (2)
**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

**/*.go: Use go fmt for code formatting across all Go files
Use goimports with -local "github.com/highesttt/matrix-line-messenger" flag to group project-local imports correctly
Use zerolog for logging throughout the codebase
Do not use Msgf in logging; use Msg with structured fields instead
Use Stringer interface where applicable in Go code

Files:

  • pkg/connector/client.go
  • pkg/connector/sync_test.go
  • pkg/connector/reaction_test.go
**/!(ltsm)/**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

**/!(ltsm)/**/*.go: Run staticcheck on all Go files excluding pkg/ltsm package (transpiled WASM code)
Run go vet on all Go files excluding pkg/ltsm package (transpiled WASM code)

Files:

  • pkg/connector/client.go
  • pkg/connector/sync_test.go
  • pkg/connector/reaction_test.go
🔇 Additional comments (9)
pkg/connector/client.go (2)

655-655: LGTM!


82-82: 🩺 Stability & Availability

No change needed for paidReactionIconMXC It is allocated lazily in getPaidReactionMXC before the first write, so leaving it nil on construction is safe.

			> Likely an incorrect or invalid review comment.
pkg/connector/reaction_test.go (4)

5-12: LGTM!


576-586: LGTM!


588-604: LGTM!


522-574: 🎯 Functional Correctness

Incorrect concern: the embedded-reaction test body is present here and already covers predefined/paid reactions, sender checks, and millisecond timestamps.

			> Likely an incorrect or invalid review comment.
pkg/connector/sync_test.go (3)

4-9: LGTM!


165-190: LGTM!


71-123: 🎯 Functional Correctness

No action needed The historical membership, malformed-record, and unsupported-LOC_KEY cases are already present in pkg/connector/sync_test.go, so this concern doesn’t apply.

			> Likely an incorrect or invalid review comment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant