Skip to content

feat: ✨ 100+ Matrix -> LINE reactions#172

Merged
highesttt merged 4 commits into
mainfrom
167-feat-translate-emoji-react-to-line-reactions
Jun 4, 2026
Merged

feat: ✨ 100+ Matrix -> LINE reactions#172
highesttt merged 4 commits into
mainfrom
167-feat-translate-emoji-react-to-line-reactions

Conversation

@highesttt

Copy link
Copy Markdown
Collaborator

No description provided.

@highesttt highesttt linked an issue Jun 4, 2026 that may be closed by this pull request
@indent

indent Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor
PR Summary

Enables Matrix → LINE reactions by replacing the previous ErrReactionsNotSupported stubs with handlers that translate ~110 Matrix emoji to LINE sticon "paid" reactions, wire two new TalkService RPCs (react, cancelReaction), and suppress the LINE SSE echo of self-sent reactions via the existing sentReqSeqs dedup. Flips RoomFeatures.Reaction from CapLevelRejected to CapLevelPartialSupport. Follow-up commits harden reqSeq generation with collision-safe monotonic counter + TTL cleanup, widen the reaction error matchers to the new react failed: code N … shape, and route the HandleMatrixMessage/HandleMatrixMessageRemove/HandleMatrixLeaveRoom reqSeq writes through the shared trackReqSeq helper so all RPC paths benefit from the cleanup and lastReqSeq bookkeeping.

  • Adds pkg/connector/reaction.go with the emoji→sticon map, PreHandleMatrixReaction / HandleMatrixReaction / HandleMatrixReactionRemove implementations, key normalization (strips VS15/VS16, collapses single-digit keycaps), and new nextReqSeq / trackReqSeq / consumeSentReqSeq helpers on LineClient.
  • Removes the no-op reaction stubs from pkg/connector/client.go and adds a lastReqSeq field to back the new collision-safe sequence generator.
  • Adds TalkService.react and TalkService.cancelReaction clients plus ReactionType / ReactRequest / CancelReactionRequest payload structs (pkg/line/methods.go, pkg/line/reaction.go); makes PaidReactionType.ResourceType / Version omitempty. RPC error wrappers now include the structured code N message … data … so downstream matchers can dispatch on the LINE error code.
  • Adds IsInvalidPaidReactionType and relaxes IsNotAMemberError matchers, both now go through a shared hasResponseErrorCode helper that accepts either "code":10051 or code 10051 (pkg/line/errors.go).
  • Inserts an early-return in handleOperation (pkg/connector/sync.go) that consumes a tracked reqSeq for OpPredefinedReaction / OpReaction echoes before any other reaction processing.
  • Hardens nextReqSeq with a monotonic lastReqSeq counter that wraps mod 1_000_000_000, skips 0, and scans forward over occupied slots; nextReqSeq / trackReqSeq / consumeSentReqSeq now invoke cleanupSentReqSeqsLocked to evict entries older than 5 minutes.
  • Refactors pkg/connector/send_message.go so all five inline sentReqSeqs writes (HandleMatrixMessage send + two retry paths, HandleMatrixMessageRemove, HandleMatrixLeaveRoom) go through lc.trackReqSeq, sharing the TTL cleanup and keeping lastReqSeq in sync across reaction and message reqSeq generation.
  • Switches userinfo.go reaction capability from CapLevelRejected to CapLevelPartialSupport.

Issues

4 potential issues found:

  • Nit: parseLineSticonURL silently downgrades version to 0 when the ?v= query is present but non-numeric — version, _ = strconv.Atoi(rawVersion) discards the error after the version := 1 default has been set. Not currently reachable because every URL in lineEmojiReactionURLs is hard-coded without a v query, but if a ?v=foo ever slips into the map the default falls back to 0 rather than 1. → Autofix
  • Latent: OpReaction (type 140, "other-user" event) is included in the new reqSeq-based dedup at sync.go:765, but our own reactions are only echoed as OpPredefinedReaction (139); a 140 carries another participant's reqSeq, so a rare collision with one of our recent tracked reqSeqs (10^9-mod millisecond) can silently drop a legitimate reaction. Trigger: another user reacts while we have an un-echoed react/cancel pending whose nextReqSeq() happens to equal theirs. → Autofix
  • The six core reactions (👍 ❤ 😆 😮 😢 😡) are routed through the paid sticon path even though LINE exposes a predefinedReactionType enum that is universally available; if a user's account lacks the 670e0cce… / 5ac1bfd5… sticon packs, the most common reactions will always fail with MessageStatusUnsupported. → Autofix
  • Nit: ✌ (\u270C) is mapped to original-pack sticon 146, but in lineEmojiReactionURLs the surrounding original-pack run uses 145 for 🤞 (\U0001F91E) — worth double-checking that 670e0cce840a8236ddd4ee4c/android/146.png actually renders a peace-sign rather than being a duplicate of 145 or a different glyph. → Autofix
3 issues already resolved
  • Latent: sentReqSeqs entries created by nextReqSeq() are never deleted when client.React / client.CancelReaction returns an error or when LINE never sends a 139 echo (e.g. SSE reconnect), so the map grows unbounded over the life of a LineClient. The stored time.Time value is set but never read, so a simple TTL janitor would suffice. (fixed by commit 434f3bc)
  • Latent: IsInvalidPaidReactionType requires "code":10051 in the error string, but Client.React / Client.CancelReaction wrap wrapper.Code != 0 failures as react failed: <wrapper.Message> which does not include the JSON code. The matcher only succeeds via the HTTP-400 path inside callRPC; any 200-with-non-zero-code rejection will surface as a raw error instead of MessageStatusUnsupported. (fixed by commit 434f3bc)
  • Latent: nextReqSeq() uses time.Now().UnixMilli() % 1_000_000_000, so two reactions sent within the same millisecond share a reqSeq and only one of two 139 echoes is suppressed; the second echo flows through handlePaidReaction which stores EmojiID="", producing a duplicate reaction in Matrix (the outbound flow stored EmojiID="paid:…"). Same bug also fires deterministically once per ~11.6 days when the modulo lands on 0, since both trackReqSeq and consumeSentReqSeq short-circuit on reqSeq == 0. (fixed by commit 434f3bc)

CI Checks

All CI checks pass on commit 6d2860e. The previous Lint with 1.25 failure was the staticcheck U1000 on (*LineClient).trackReqSeq; the refactor routes the five inline sentReqSeqs write sites in pkg/connector/send_message.go through lc.trackReqSeq, so the helper now has callers and lint passes.


⚡ Autofix All Issues

@coderabbitai

coderabbitai Bot commented Jun 4, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 478b84ec-f2a0-4072-b511-81aafc4ade0e

📥 Commits

Reviewing files that changed from the base of the PR and between 434f3bc and 6d2860e.

📒 Files selected for processing (1)
  • pkg/connector/send_message.go
📜 Recent review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (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/send_message.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/send_message.go
🔇 Additional comments (1)
pkg/connector/send_message.go (1)

623-623: LGTM!

Also applies to: 683-683, 711-711, 799-799, 825-825


📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Full reaction bridging between Matrix and LINE, including LINE paid (sticon) reactions and reaction removals.
  • Bug Fixes
    • Improved deduplication and handling to prevent duplicate reaction processing and surface clear error statuses for invalid/unauthorized reactions.
  • Chores
    • Connector now advertises partial reaction support for rooms.

Walkthrough

Replaces stub handlers with full Matrix→LINE paid reaction bridging: adds LINE reaction types and RPCs, emoji→sticon mapping and parsing, request-sequence generation/tracking, PreHandle/Handle/Remove implementations, error mapping, sync dedup, and capability reporting.

Changes

LINE Reaction Support

Layer / File(s) Summary
LINE reaction types, RPC methods, and error helpers
pkg/line/reaction.go, pkg/line/methods.go, pkg/line/errors.go
Adds ReactionType, ReactRequest, CancelReactionRequest; marks PaidReactionType fields omitempty; implements Client.React and Client.CancelReaction RPC wrappers; adds IsInvalidPaidReactionType and refines IsNotAMemberError.
Connector emoji mapping, handlers, and reqseq tracking
pkg/connector/reaction.go
Adds Matrix emoji → LINE sticon mapping, sticon URL parser, reaction key normalization, linePaidReactionForMatrixEmoji, parseReactionTargetMessageID, PreHandleMatrixReaction, HandleMatrixReaction, HandleMatrixReactionRemove, and LineClient request-sequence helpers (next/track/consume with TTL).
Connector wiring, sync integration, capability update
pkg/connector/client.go, pkg/connector/sync.go, pkg/connector/userinfo.go, pkg/connector/send_message.go
Removes previous stub reaction methods and unused bridgev2/database import; handleOperation consumes sent reqSeq for reaction ops to dedupe; GetCapabilities reports reactions as CapLevelPartialSupport; centralizes reqSeq tracking via lc.trackReqSeq calls across send/remove/leave paths.

Sequence Diagram

sequenceDiagram
  participant MatrixUser
  participant Connector
  participant LineClient
  participant TalkService
  participant Database
  MatrixUser->>Connector: PreHandleMatrixReaction(event)
  Connector->>Connector: normalizeMatrixReactionKey & map to sticon URL
  MatrixUser->>Connector: HandleMatrixReaction(confirm)
  Connector->>LineClient: React(reqSeq, messageID, reactionType)
  LineClient->>TalkService: react(ReactRequest)
  TalkService-->>LineClient: {code,message,data}
  LineClient-->>Connector: success / mapped error
  Connector->>Database: create Reaction record (on success)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive No pull request description was provided by the author, making it impossible to assess relevance to the changeset. Add a pull request description explaining the implementation details of Matrix-to-LINE reaction bridging, the scope of supported reactions, and any relevant usage notes.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly references Matrix to LINE reactions, which is the main focus of the changeset across multiple files (reaction.go, methods.go, sync.go, etc.).
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 167-feat-translate-emoji-react-to-line-reactions

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

Comment thread pkg/connector/reaction.go
ref, ok := linePaidReactionForMatrixEmoji(key)
if !ok {
return bridgev2.MatrixReactionPreResponse{}, unsupportedMatrixReactionError(key)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Predefined reactions are silently routed through the paid path. PreHandleMatrixReaction / HandleMatrixReaction always call linePaidReactionForMatrixEmoji and build a PaidReactionType request, but pkg/line/reaction.go:81-88 declares the six LINE predefined reactions (👍 LIKE=2, ❤ LOVE=3, 😆 LAUGH=4, 😮 SURPRISE=5, 😢 SAD=6, 😡 ANGRY=7) which are also present in lineEmojiReactionURLs. The ReactionType.PredefinedReactionType int field exists in the new request struct but is never set anywhere in the PR.

This matters because paid sticon reactions require the account to have access to the 670e0cce840a8236ddd4ee4c / 5ac1bfd5040ab15980c9b435 packs. If LINE returns the invalid paidreactiontype in reactiontype error you already handle, the bridge falls back to unsupportedMatrixReactionError(key) and the Matrix user sees their thumbs-up/heart fail — even though sending those six as predefinedReactionType would always succeed.

Suggested fix: try predefined first, fall back to paid. Something like:

if n, ok := linePredefinedReactionForMatrixEmoji(key); ok {
    return line.ReactionType{PredefinedReactionType: n}, networkid.EmojiID("predef:"+strconv.Itoa(n)), true
}
ref, ok := linePaidReactionForMatrixEmoji(key)
...

built off an inverse of PredefinedReactionEmoji (running normalizeMatrixReactionKey over its keys so \u2764\uFE0F resolves).

Comment thread pkg/connector/sync.go
func (lc *LineClient) handleOperation(ctx context.Context, op line.Operation) {
opType := OperationType(op.Type)

if opType == OpPredefinedReaction || opType == OpReaction {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Latent: OpReaction (140) shouldn't be part of the reqSeq dedup. The existing handler at lines 996-1000 already documents that "Type 140 is the 'other' event — param3 is the observer, not the actor." Our self-sent reactions are echoed as OpPredefinedReaction (139); the OpReaction branch only fires for other users' reactions, whose op.ReqSeq is generated by their client.

Since nextReqSeq() is time.Now().UnixMilli() % 1_000_000_000, the value space is small and the comparison is purely numeric. If another participant reacts while we have a recently-tracked but not-yet-echoed reqSeq (e.g. an in-flight client.React/client.CancelReaction, or a client.SendMessage that shares the same map) whose value happens to equal theirs, consumeSentReqSeq(op.ReqSeq) will return true and silently swallow their reaction.

Suggested fix:

if opType == OpPredefinedReaction {
    if lc.consumeSentReqSeq(op.ReqSeq) {
        return
    }
}
Suggested change
if opType == OpPredefinedReaction || opType == OpReaction {
if opType == OpPredefinedReaction {
if lc.consumeSentReqSeq(op.ReqSeq) {
return
}
}

Comment thread pkg/connector/reaction.go

@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

🤖 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 196-197: The current parsing assigns version = 0 when strconv.Atoi
fails; change it so the default version (1) is preserved unless parsing
succeeds: after getting rawVersion from parsed.Query().Get("v"), call
strconv.Atoi(rawVersion) and only set the variable version if err == nil (e.g.,
v, err := strconv.Atoi(rawVersion); if err == nil { version = v }), leaving
version unchanged on parse errors or empty input.
- Around line 273-277: The current nextReqSeq() creates collisions by using only
UnixMilli; add a lastReqSeq int field to LineClient and make nextReqSeq generate
a unique seq by taking (time.Now().UnixMilli() % 1_000_000_000) but if it equals
lastReqSeq then increment (wrapping mod 1_000_000_000) until unique, update
lastReqSeq with the chosen value and call trackReqSeq(reqSeq); protect
reads/writes to lastReqSeq with the same mutex used for LineClient state (or add
a mutex) so nextReqSeq() is concurrency-safe and preserves consumeSentReqSeq
dedup behavior.
🪄 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: db8a9c92-627d-4e99-ac05-1d57205e0c05

📥 Commits

Reviewing files that changed from the base of the PR and between 7175614 and 826752c.

📒 Files selected for processing (7)
  • pkg/connector/client.go
  • pkg/connector/reaction.go
  • pkg/connector/sync.go
  • pkg/connector/userinfo.go
  • pkg/line/errors.go
  • pkg/line/methods.go
  • pkg/line/reaction.go
💤 Files with no reviewable changes (1)
  • pkg/connector/client.go
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: build-docker
  • GitHub Check: Lint with 1.25
  • GitHub Check: Lint with 1.25
  • GitHub Check: build-docker
🧰 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/userinfo.go
  • pkg/line/methods.go
  • pkg/connector/sync.go
  • pkg/line/errors.go
  • pkg/line/reaction.go
  • pkg/connector/reaction.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/userinfo.go
  • pkg/line/methods.go
  • pkg/connector/sync.go
  • pkg/line/errors.go
  • pkg/line/reaction.go
  • pkg/connector/reaction.go
🧠 Learnings (1)
📚 Learning: 2026-05-29T10:13:37.093Z
Learnt from: CR
Repo: beeper/matrix-line-messenger PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-05-29T10:13:37.093Z
Learning: Applies to pkg/connector/connector.go : Implement `bridgev2.NetworkConnector` and `bridgev2.NetworkAPI` interfaces in the connector package for bridge logic

Applied to files:

  • pkg/connector/reaction.go
🔇 Additional comments (5)
pkg/line/reaction.go (1)

60-78: LGTM!

pkg/line/methods.go (1)

392-435: LGTM!

pkg/line/errors.go (1)

107-117: LGTM!

pkg/connector/sync.go (1)

765-769: LGTM!

pkg/connector/userinfo.go (1)

48-48: LGTM!

Comment thread pkg/connector/reaction.go
Comment on lines +196 to +197
if rawVersion := parsed.Query().Get("v"); rawVersion != "" {
version, _ = strconv.Atoi(rawVersion)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Keep default version=1 when query parsing fails.

Line 197 overwrites version with 0 on invalid v query input. Preserve the default unless parsing succeeds.

💡 Proposed fix
 		version := 1
 		if rawVersion := parsed.Query().Get("v"); rawVersion != "" {
-			version, _ = strconv.Atoi(rawVersion)
+			if parsedVersion, err := strconv.Atoi(rawVersion); err == nil && parsedVersion > 0 {
+				version = parsedVersion
+			}
 		}
📝 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
if rawVersion := parsed.Query().Get("v"); rawVersion != "" {
version, _ = strconv.Atoi(rawVersion)
if rawVersion := parsed.Query().Get("v"); rawVersion != "" {
if parsedVersion, err := strconv.Atoi(rawVersion); err == nil && parsedVersion > 0 {
version = parsedVersion
}
}
🤖 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 196 - 197, The current parsing
assigns version = 0 when strconv.Atoi fails; change it so the default version
(1) is preserved unless parsing succeeds: after getting rawVersion from
parsed.Query().Get("v"), call strconv.Atoi(rawVersion) and only set the variable
version if err == nil (e.g., v, err := strconv.Atoi(rawVersion); if err == nil {
version = v }), leaving version unchanged on parse errors or empty input.

Comment thread pkg/connector/reaction.go
Comment thread pkg/connector/reaction.go
break
}
version := 1
if rawVersion := parsed.Query().Get("v"); rawVersion != "" {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Default version is silently overwritten with 0 on parse failure.

version, _ = strconv.Atoi(rawVersion) discards the error, so a non-empty but non-numeric ?v= query (e.g. ?v=latest) drops version from the intended default of 1 to 0. The map entries in lineEmojiReactionURLs are all hard-coded without a v query today, so this isn't reachable in current code — but the existing branch is dead-defensive logic for URLs that might be added later, and it currently has the opposite effect of the comment-implied intent.

Suggested change
if rawVersion := parsed.Query().Get("v"); rawVersion != "" {
version := 1
if rawVersion := parsed.Query().Get("v"); rawVersion != "" {
if v, err := strconv.Atoi(rawVersion); err == nil {
version = v
}
}

@highesttt highesttt merged commit ba7f799 into main Jun 4, 2026
10 checks passed
@highesttt highesttt deleted the 167-feat-translate-emoji-react-to-line-reactions branch June 4, 2026 08:09
@clins1994

Copy link
Copy Markdown
Contributor

@highesttt amazing! thanks ❤️

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.

feat: ✨ Translate emoji react to LINE reactions?

2 participants