Skip to content
Open
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
1 change: 1 addition & 0 deletions pkg/connector/consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ type OperationType int
const (
OpSendMessage OperationType = 25
OpReceiveMessage OperationType = 26
OpSendChatChecked OperationType = 40
OpSendChatRemoved OperationType = 41
OpContactUpdate OperationType = 49
OpReadReceipt OperationType = 55
Expand Down
74 changes: 74 additions & 0 deletions pkg/connector/read_receipt_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package connector

import (
"encoding/json"
"testing"
"time"

"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/database"
"maunium.net/go/mautrix/bridgev2/networkid"

"github.com/beeper/line/pkg/line"
)

func newOwnReadReceiptTestClient() *LineClient {
return &LineClient{
Mid: "Uown",
UserLogin: &bridgev2.UserLogin{
UserLogin: &database.UserLogin{ID: networkid.UserLoginID("Uown")},
},
}
}

func TestMakeOwnReadReceiptEvent(t *testing.T) {
lc := newOwnReadReceiptTestClient()

receipt, ok := lc.makeOwnReadReceiptEvent(line.Operation{
Type: int(OpSendChatChecked),
Param1: "Cgroup",
Param2: "12345",
CreatedTime: json.Number("1700000000000"),
})
if !ok {
t.Fatal("receipt was rejected")
}

if receipt.GetType() != bridgev2.RemoteEventReadReceipt {
t.Fatalf("type = %v", receipt.GetType())
}
if want := (networkid.PortalKey{ID: "Cgroup", Receiver: "Uown"}); receipt.GetPortalKey() != want {
t.Fatalf("portal key = %+v, want %+v", receipt.GetPortalKey(), want)
}
if sender := receipt.GetSender(); !sender.IsFromMe || sender.Sender != "Uown" {
t.Fatalf("sender = %+v, want own login", sender)
}
if receipt.GetLastReceiptTarget() != "12345" {
t.Fatalf("last target = %q", receipt.GetLastReceiptTarget())
}
if !receipt.GetReadUpTo().IsZero() {
t.Fatalf("read up to = %v, want zero so an unknown target is not guessed", receipt.GetReadUpTo())
}
if want := time.UnixMilli(1700000000000); !receipt.GetTimestamp().Equal(want) {
t.Fatalf("timestamp = %v, want %v", receipt.GetTimestamp(), want)
}
}

func TestMakeOwnReadReceiptEventRejectsMalformedOperations(t *testing.T) {
lc := newOwnReadReceiptTestClient()

for _, tc := range []struct {
name string
op line.Operation
}{
{name: "missing chat", op: line.Operation{Type: int(OpSendChatChecked), Param2: "12345"}},
{name: "missing message", op: line.Operation{Type: int(OpSendChatChecked), Param1: "Upeer"}},
{name: "another operation", op: line.Operation{Type: int(OpReadReceipt), Param1: "Upeer", Param2: "12345"}},
} {
t.Run(tc.name, func(t *testing.T) {
if receipt, ok := lc.makeOwnReadReceiptEvent(tc.op); ok || receipt != nil {
t.Fatalf("got receipt %+v, ok %v", receipt, ok)
}
})
}
}
32 changes: 32 additions & 0 deletions pkg/connector/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -1997,6 +1997,16 @@ func (lc *LineClient) handleOperation(ctx context.Context, op line.Operation) {
lc.syncSingleChat(context.Background(), op)
}()

case OpSendChatChecked:
receipt, ok := lc.makeOwnReadReceiptEvent(op)
if !ok {
lc.UserLogin.Bridge.Log.Warn().
Int("op_type", op.Type).
Msg("Ignoring malformed own read receipt")
return
}
lc.UserLogin.Bridge.QueueRemoteEvent(lc.UserLogin, receipt)

case OpReadReceipt:
portalID := makePortalID(op.Param1)
senderID := makeUserID(op.Param2)
Expand Down Expand Up @@ -2387,6 +2397,28 @@ func (lc *LineClient) handleMemberLeft(chatMid, leaverMid string) {
lc.handleMemberLeaveWithSender(chatMid, leaverMid, lc.eventSenderForMID(leaverMid))
}

// makeOwnReadReceiptEvent converts SEND_CHAT_CHECKED, where param1 is the chat
// MID and param2 is the last message this account read.
func (lc *LineClient) makeOwnReadReceiptEvent(op line.Operation) (*simplevent.Receipt, bool) {
if OperationType(op.Type) != OpSendChatChecked || op.Param1 == "" || op.Param2 == "" {
return nil, false
}

ts := time.Now()
if timestamp, err := op.CreatedTime.Int64(); err == nil && timestamp > 0 {
ts = time.UnixMilli(timestamp)
}
return &simplevent.Receipt{
EventMeta: simplevent.EventMeta{
Type: bridgev2.RemoteEventReadReceipt,
PortalKey: networkid.PortalKey{ID: makePortalID(op.Param1), Receiver: lc.UserLogin.ID},
Timestamp: ts,
Sender: bridgev2.EventSender{IsFromMe: true, Sender: networkid.UserID(lc.UserLogin.ID)},
},
LastTarget: networkid.MessageID(op.Param2),
}, true
}

func (lc *LineClient) makeNotifiedMemberRemovalEvent(op line.Operation) (*simplevent.ChatInfoChange, bool) {
if OperationType(op.Type) != OpNotifiedDeleteOtherFromChat ||
!isChatMID(op.Param1) ||
Expand Down