From 16da171ce62143c7e4757e72fbd254470c692971 Mon Sep 17 00:00:00 2001 From: highesttt Date: Mon, 19 Jan 2026 19:47:30 -0600 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20Add=20support=20for?= =?UTF-8?q?=20receiving=20reactions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/connector/client.go | 86 ++++++++++++++++++++++++++++++++++++++++- pkg/line/reaction.go | 27 +++++++++++++ 2 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 pkg/line/reaction.go diff --git a/pkg/connector/client.go b/pkg/connector/client.go index d41db57..ac60b84 100644 --- a/pkg/connector/client.go +++ b/pkg/connector/client.go @@ -34,6 +34,7 @@ import ( "maunium.net/go/mautrix/bridgev2/simplevent" "maunium.net/go/mautrix/bridgev2/status" "maunium.net/go/mautrix/event" + "maunium.net/go/mautrix/id" "github.com/rs/zerolog" @@ -698,7 +699,7 @@ func (lc *LineClient) pollLoop(ctx context.Context) { } } -func (lc *LineClient) handleOperation(_ context.Context, op line.Operation) { +func (lc *LineClient) handleOperation(ctx context.Context, op line.Operation) { // Type 25 = SEND_MESSAGE (Message sent by you from another device) // Type 26 = RECEIVE_MESSAGE (Message received from another user) @@ -728,6 +729,89 @@ func (lc *LineClient) handleOperation(_ context.Context, op line.Operation) { }) } + if op.Type == 140 { + go func() { + bgCtx := context.Background() + param2, err := line.ParseReactionParam2(op.Param2) + if err != nil { + lc.UserLogin.Bridge.Log.Error().Err(err).Msg("Failed to parse reaction param2") + return + } + if param2.Curr == nil || param2.Curr.PaidReactionType == nil { + return + } + + prt := param2.Curr.PaidReactionType + url := fmt.Sprintf("https://stickershop.line-scdn.net/sticonshop/v1/sticon/%s/android/%s.png", prt.ProductID, prt.EmojiID) + + resp, err := lc.HTTPClient.Get(url) + if err != nil { + lc.UserLogin.Bridge.Log.Error().Err(err).Str("url", url).Msg("Failed to download reaction image") + return + } + defer resp.Body.Close() + if resp.StatusCode != 200 { + lc.UserLogin.Bridge.Log.Error().Int("status_code", resp.StatusCode).Str("url", url).Msg("Failed to download reaction image: bad status code") + return + } + + data, err := io.ReadAll(resp.Body) + if err != nil { + lc.UserLogin.Bridge.Log.Error().Err(err).Msg("Failed to read reaction image body") + return + } + + mimeType := resp.Header.Get("Content-Type") + if mimeType == "" { + mimeType = "image/png" + } + + senderID := makeUserID(op.Param3) + ghost, err := lc.UserLogin.Bridge.GetGhostByID(bgCtx, senderID) + if err != nil { + lc.UserLogin.Bridge.Log.Error().Err(err).Msg("Failed to get ghost for reaction sender") + return + } + + portalKey := networkid.PortalKey{ID: makePortalID(param2.ChatMid), Receiver: lc.UserLogin.ID} + portal, err := lc.UserLogin.Bridge.GetPortalByKey(bgCtx, portalKey) + if err != nil || portal == nil { + lc.UserLogin.Bridge.Log.Error().Err(err).Str("chat_mid", param2.ChatMid).Msg("Failed to get portal for reaction") + return + } + + if portal.MXID == "" { + lc.UserLogin.Bridge.Log.Error().Msg("Portal MXID is empty, cannot upload media") + return + } + + mxc, uploadedFile, err := ghost.Intent.UploadMedia(bgCtx, "", data, "reaction.png", mimeType) + if err != nil { + lc.UserLogin.Bridge.Log.Error().Err(err).Int("data_len", len(data)).Msg("Failed to upload reaction image to Matrix") + return + } + if mxc == "" && uploadedFile != nil && uploadedFile.URL != "" { + mxc = id.ContentURIString(uploadedFile.URL) + } + if mxc == "" { + lc.UserLogin.Bridge.Log.Error().Interface("uploaded_file", uploadedFile).Msg("UploadMedia returned empty MXC URI") + return + } + + ts, _ := op.CreatedTime.Int64() + lc.UserLogin.Bridge.QueueRemoteEvent(lc.UserLogin, &simplevent.Reaction{ + EventMeta: simplevent.EventMeta{ + Type: bridgev2.RemoteEventReaction, + PortalKey: portalKey, + Timestamp: time.UnixMilli(ts), + Sender: bridgev2.EventSender{Sender: senderID}, + }, + TargetMessage: networkid.MessageID(op.Param1), + Emoji: string(mxc), + }) + }() + } + if op.Type == 25 { lc.reqSeqMu.Lock() _, ok := lc.sentReqSeqs[op.ReqSeq] diff --git a/pkg/line/reaction.go b/pkg/line/reaction.go new file mode 100644 index 0000000..8c05e9b --- /dev/null +++ b/pkg/line/reaction.go @@ -0,0 +1,27 @@ +package line + +import "encoding/json" + +type ReactionParam2 struct { + ChatMid string `json:"chatMid"` + Curr *ReactionDetail `json:"curr,omitempty"` +} + +type ReactionDetail struct { + PaidReactionType *PaidReactionType `json:"paidReactionType,omitempty"` +} + +type PaidReactionType struct { + ProductID string `json:"productId"` + EmojiID string `json:"emojiId"` + ResourceType int `json:"resourceType"` + Version int `json:"version"` +} + +func ParseReactionParam2(data string) (*ReactionParam2, error) { + var p ReactionParam2 + if err := json.Unmarshal([]byte(data), &p); err != nil { + return nil, err + } + return &p, nil +} \ No newline at end of file From 1f3cf34d00dc99526fea63491b12435ba6b1df4b Mon Sep 17 00:00:00 2001 From: highesttt Date: Mon, 19 Jan 2026 19:48:05 -0600 Subject: [PATCH 2/4] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20Update=20README=20to?= =?UTF-8?q?=20reflect=20reaction=20support=20as=20complete=20(Receive=20ON?= =?UTF-8?q?LY)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 41395ef..2e5d180 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Based on the [mautrix-twilio](https://github.com/mautrix/twilio) bridge - [x] Decrypt usernames - [x] Sending messages - [x] Read receipts -- [ ] Reaction support +- [x] Reaction support (Receive ONLY) - [x] Reply support - [ ] Prefetch chats - [x] Group chats From 6bedda4cc0756700b5725c319c6cc2371b408692 Mon Sep 17 00:00:00 2001 From: highesttt Date: Mon, 19 Jan 2026 19:49:03 -0600 Subject: [PATCH 3/4] =?UTF-8?q?chore:=20=F0=9F=A4=96=20linting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/connector/client.go | 2 +- pkg/line/reaction.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/connector/client.go b/pkg/connector/client.go index ac60b84..19126c2 100644 --- a/pkg/connector/client.go +++ b/pkg/connector/client.go @@ -807,7 +807,7 @@ func (lc *LineClient) handleOperation(ctx context.Context, op line.Operation) { Sender: bridgev2.EventSender{Sender: senderID}, }, TargetMessage: networkid.MessageID(op.Param1), - Emoji: string(mxc), + Emoji: string(mxc), }) }() } diff --git a/pkg/line/reaction.go b/pkg/line/reaction.go index 8c05e9b..8659531 100644 --- a/pkg/line/reaction.go +++ b/pkg/line/reaction.go @@ -24,4 +24,4 @@ func ParseReactionParam2(data string) (*ReactionParam2, error) { return nil, err } return &p, nil -} \ No newline at end of file +} From caa5f5c5f474ef9ab20fcc751cdc6b4b2e07f8ea Mon Sep 17 00:00:00 2001 From: highesttt Date: Mon, 19 Jan 2026 20:06:45 -0600 Subject: [PATCH 4/4] =?UTF-8?q?chore:=20=F0=9F=A4=96=20Added=20extra=20log?= =?UTF-8?q?s=20Removed=20extra=20spawned=20in=20context=20Renamed=20param2?= =?UTF-8?q?=20to=20payload=20in=20reaction?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/connector/client.go | 8 ++++---- pkg/line/reaction.go | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pkg/connector/client.go b/pkg/connector/client.go index 19126c2..60a9727 100644 --- a/pkg/connector/client.go +++ b/pkg/connector/client.go @@ -731,13 +731,13 @@ func (lc *LineClient) handleOperation(ctx context.Context, op line.Operation) { if op.Type == 140 { go func() { - bgCtx := context.Background() param2, err := line.ParseReactionParam2(op.Param2) if err != nil { lc.UserLogin.Bridge.Log.Error().Err(err).Msg("Failed to parse reaction param2") return } if param2.Curr == nil || param2.Curr.PaidReactionType == nil { + lc.UserLogin.Bridge.Log.Error().Msg("No current reaction or paid reaction type found") return } @@ -767,14 +767,14 @@ func (lc *LineClient) handleOperation(ctx context.Context, op line.Operation) { } senderID := makeUserID(op.Param3) - ghost, err := lc.UserLogin.Bridge.GetGhostByID(bgCtx, senderID) + ghost, err := lc.UserLogin.Bridge.GetGhostByID(ctx, senderID) if err != nil { lc.UserLogin.Bridge.Log.Error().Err(err).Msg("Failed to get ghost for reaction sender") return } portalKey := networkid.PortalKey{ID: makePortalID(param2.ChatMid), Receiver: lc.UserLogin.ID} - portal, err := lc.UserLogin.Bridge.GetPortalByKey(bgCtx, portalKey) + portal, err := lc.UserLogin.Bridge.GetPortalByKey(ctx, portalKey) if err != nil || portal == nil { lc.UserLogin.Bridge.Log.Error().Err(err).Str("chat_mid", param2.ChatMid).Msg("Failed to get portal for reaction") return @@ -785,7 +785,7 @@ func (lc *LineClient) handleOperation(ctx context.Context, op line.Operation) { return } - mxc, uploadedFile, err := ghost.Intent.UploadMedia(bgCtx, "", data, "reaction.png", mimeType) + mxc, uploadedFile, err := ghost.Intent.UploadMedia(ctx, "", data, "reaction.png", mimeType) if err != nil { lc.UserLogin.Bridge.Log.Error().Err(err).Int("data_len", len(data)).Msg("Failed to upload reaction image to Matrix") return diff --git a/pkg/line/reaction.go b/pkg/line/reaction.go index 8659531..8134364 100644 --- a/pkg/line/reaction.go +++ b/pkg/line/reaction.go @@ -2,7 +2,7 @@ package line import "encoding/json" -type ReactionParam2 struct { +type ReactionPayload struct { ChatMid string `json:"chatMid"` Curr *ReactionDetail `json:"curr,omitempty"` } @@ -18,8 +18,8 @@ type PaidReactionType struct { Version int `json:"version"` } -func ParseReactionParam2(data string) (*ReactionParam2, error) { - var p ReactionParam2 +func ParseReactionParam2(data string) (*ReactionPayload, error) { + var p ReactionPayload if err := json.Unmarshal([]byte(data), &p); err != nil { return nil, err }