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 diff --git a/pkg/connector/client.go b/pkg/connector/client.go index d41db57..60a9727 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() { + 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 + } + + 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(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(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 + } + + if portal.MXID == "" { + lc.UserLogin.Bridge.Log.Error().Msg("Portal MXID is empty, cannot upload media") + return + } + + 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 + } + 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..8134364 --- /dev/null +++ b/pkg/line/reaction.go @@ -0,0 +1,27 @@ +package line + +import "encoding/json" + +type ReactionPayload 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) (*ReactionPayload, error) { + var p ReactionPayload + if err := json.Unmarshal([]byte(data), &p); err != nil { + return nil, err + } + return &p, nil +}