Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
86 changes: 85 additions & 1 deletion pkg/connector/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -728,6 +729,89 @@ func (lc *LineClient) handleOperation(_ context.Context, op line.Operation) {
})
}

if op.Type == 140 {
go func() {
Comment thread
highesttt marked this conversation as resolved.
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 {
Comment thread
highesttt marked this conversation as resolved.
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)
Comment thread
highesttt marked this conversation as resolved.
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
}
Comment thread
highesttt marked this conversation as resolved.

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]
Expand Down
27 changes: 27 additions & 0 deletions pkg/line/reaction.go
Original file line number Diff line number Diff line change
@@ -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
}