Skip to content

Commit 4a259e4

Browse files
committed
fix: misidentified the connected user as a removed group member
1 parent 79f8227 commit 4a259e4

2 files changed

Lines changed: 185 additions & 6 deletions

File tree

pkg/connector/sync.go

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1154,8 +1154,13 @@ func (lc *LineClient) cacheGroupMembersFromSystemMessage(msg *line.Message) {
11541154
lc.addGroupMembersToCache(chatMid, append(midsFromSystemLocArgs(msg.ContentMetadata["LOC_ARGS"]), msg.From)...)
11551155
case "C_MJ", "A_MJ":
11561156
lc.addGroupMembersToCache(chatMid, msg.From)
1157-
case "C_ML", "A_ML", "C_MR", "A_MR":
1157+
case "C_ML", "A_ML":
11581158
lc.removeGroupMemberFromCache(chatMid, msg.From)
1159+
case "C_MR", "A_MR":
1160+
_, removedMID, ok := memberRemovalMIDs(msg)
1161+
if ok {
1162+
lc.removeGroupMemberFromCache(chatMid, removedMID)
1163+
}
11591164
case "C_IC":
11601165
parts := strings.SplitN(msg.ContentMetadata["LOC_ARGS"], "\x1e", 2)
11611166
if len(parts) == 2 {
@@ -1207,6 +1212,23 @@ func midsFromSystemLocArgs(locArgs string) []string {
12071212
return mids
12081213
}
12091214

1215+
// memberRemovalMIDs returns the actor and target encoded by LINE's C_MR/A_MR
1216+
// system records. msg.From is the member who performed the removal, while
1217+
// LOC_ARGS contains the removed member MID (and may also repeat the actor MID).
1218+
func memberRemovalMIDs(msg *line.Message) (removerMID, removedMID string, ok bool) {
1219+
if msg == nil || !isUserMID(msg.From) || msg.ContentMetadata == nil {
1220+
return "", "", false
1221+
}
1222+
removerMID = msg.From
1223+
mids := midsFromSystemLocArgs(msg.ContentMetadata["LOC_ARGS"])
1224+
for i := len(mids) - 1; i >= 0; i-- {
1225+
if mids[i] != removerMID {
1226+
return removerMID, mids[i], true
1227+
}
1228+
}
1229+
return "", "", false
1230+
}
1231+
12101232
func isUserMID(mid string) bool {
12111233
return len(mid) > 1 && (mid[0] == 'U' || mid[0] == 'u')
12121234
}
@@ -2374,8 +2396,11 @@ func isHandledSystemMessage(msg *line.Message) bool {
23742396
case "C_PN":
23752397
parts := strings.SplitN(msg.ContentMetadata["LOC_ARGS"], "\x1e", 2)
23762398
return len(parts) == 2 && parts[1] != ""
2377-
case "C_MJ", "A_MJ", "C_ML", "A_ML", "C_MR", "A_MR", "A_MC":
2399+
case "C_MJ", "A_MJ", "C_ML", "A_ML", "A_MC":
23782400
return msg.From != ""
2401+
case "C_MR", "A_MR":
2402+
_, _, ok := memberRemovalMIDs(msg)
2403+
return ok
23792404
case "C_GI", "C_MI", "A_MI", "C_IC":
23802405
parts := strings.SplitN(msg.ContentMetadata["LOC_ARGS"], "\x1e", 2)
23812406
return len(parts) == 2 && parts[1] != ""
@@ -2431,12 +2456,18 @@ func (lc *LineClient) makeSystemMessageEvent(op line.Operation) (*simplevent.Cha
24312456
leaver := lc.eventSenderForMID(msg.From)
24322457
return makeMemberChangeEvent(portalKey, leaver, leaver, event.MembershipLeave, ts, false), true
24332458
case "C_MR", "A_MR":
2434-
lc.UserLogin.Bridge.Log.Debug().Str("loc_key", locKey).Str("chat_mid", msg.To).Str("removed_mid", msg.From).Msg("System message: member removed")
2435-
lc.removeGroupMemberFromCache(msg.To, msg.From)
2459+
removerMID, removedMID, _ := memberRemovalMIDs(msg)
2460+
lc.UserLogin.Bridge.Log.Debug().
2461+
Str("loc_key", locKey).
2462+
Str("chat_mid", msg.To).
2463+
Str("remover_mid", removerMID).
2464+
Str("removed_mid", removedMID).
2465+
Msg("System message: member removed")
2466+
lc.removeGroupMemberFromCache(msg.To, removedMID)
24362467
return makeMemberChangeEvent(
24372468
portalKey,
2438-
lc.eventSenderForMID(msg.From),
2439-
bridgev2.EventSender{},
2469+
lc.eventSenderForMID(removedMID),
2470+
lc.eventSenderForMID(removerMID),
24402471
event.MembershipLeave,
24412472
ts,
24422473
false,

pkg/connector/sync_test.go

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"encoding/json"
77
"errors"
88
"io"
9+
"slices"
910
"strings"
1011
"testing"
1112
"time"
@@ -122,6 +123,153 @@ func TestMakeSystemMessageEventForHistoricalMembership(t *testing.T) {
122123
}
123124
}
124125

126+
func TestMemberRemovalSystemMessageAttribution(t *testing.T) {
127+
tests := []struct {
128+
name string
129+
locKey string
130+
fromMID string
131+
locArgs string
132+
removedMID string
133+
senderIsFromMe bool
134+
}{
135+
{
136+
name: "connected user removes another member",
137+
locKey: "C_MR",
138+
fromMID: "Uself",
139+
locArgs: "Uself\x1eUremoved",
140+
removedMID: "Uremoved",
141+
senderIsFromMe: true,
142+
},
143+
{
144+
name: "another member removes connected user",
145+
locKey: "A_MR",
146+
fromMID: "Uremover",
147+
locArgs: "Uself",
148+
removedMID: "Uself",
149+
},
150+
}
151+
152+
for _, test := range tests {
153+
t.Run(test.name, func(t *testing.T) {
154+
lc := &LineClient{
155+
Mid: "Uself",
156+
UserLogin: &bridgev2.UserLogin{
157+
UserLogin: &database.UserLogin{ID: "Uself"},
158+
Bridge: &bridgev2.Bridge{Log: zerolog.Nop()},
159+
},
160+
groupMemberCache: map[string][]string{
161+
"Cgroup": {"Uself", "Uremover", "Uremoved"},
162+
},
163+
}
164+
165+
removal, handled := lc.makeSystemMessageEvent(line.Operation{Message: &line.Message{
166+
ID: "remove-message",
167+
From: test.fromMID,
168+
To: "Cgroup",
169+
ContentType: int(ContentSystem),
170+
CreatedTime: json.Number("1784930400123"),
171+
ContentMetadata: map[string]string{
172+
"LOC_KEY": test.locKey,
173+
"LOC_ARGS": test.locArgs,
174+
},
175+
}})
176+
if !handled || removal == nil {
177+
t.Fatalf("removal handled/event = %v/%#v, want true/non-nil", handled, removal)
178+
}
179+
if removal.EventMeta.Sender.Sender != networkid.UserID(test.fromMID) || removal.EventMeta.Sender.IsFromMe != test.senderIsFromMe {
180+
t.Fatalf("removal sender = %#v, want MID %q with IsFromMe=%v", removal.EventMeta.Sender, test.fromMID, test.senderIsFromMe)
181+
}
182+
if changes := removal.ChatInfoChange.MemberChanges.MemberMap; len(changes) != 1 {
183+
t.Fatalf("member changes = %#v, want exactly one", changes)
184+
} else if change, ok := changes[networkid.UserID(test.removedMID)]; !ok {
185+
t.Fatalf("removed member %q missing from %#v", test.removedMID, changes)
186+
} else if change.Membership != event.MembershipLeave || change.EventSender.Sender != networkid.UserID(test.removedMID) {
187+
t.Fatalf("removed member change = %#v", change)
188+
}
189+
190+
members := lc.getCachedGroupMembers("Cgroup")
191+
if slices.Contains(members, test.removedMID) {
192+
t.Fatalf("removed member %q remained cached: %v", test.removedMID, members)
193+
}
194+
if test.fromMID != test.removedMID && !slices.Contains(members, test.fromMID) {
195+
t.Fatalf("remover %q was incorrectly evicted: %v", test.fromMID, members)
196+
}
197+
})
198+
}
199+
}
200+
201+
func TestMemberRemovalSystemMessageCacheTarget(t *testing.T) {
202+
for _, locKey := range []string{"C_MR", "A_MR"} {
203+
t.Run(locKey, func(t *testing.T) {
204+
lc := &LineClient{
205+
Mid: "Uself",
206+
groupMemberCache: map[string][]string{
207+
"Cgroup": {"Uself", "Uremoved"},
208+
},
209+
}
210+
lc.cacheGroupMembersFromSystemMessage(&line.Message{
211+
From: "Uself",
212+
To: "Cgroup",
213+
ContentType: int(ContentSystem),
214+
ContentMetadata: map[string]string{
215+
"LOC_KEY": locKey,
216+
"LOC_ARGS": "Uself\x1eUremoved",
217+
},
218+
})
219+
220+
members := lc.getCachedGroupMembers("Cgroup")
221+
if slices.Contains(members, "Uremoved") {
222+
t.Fatalf("removed member remained cached: %v", members)
223+
}
224+
if !slices.Contains(members, "Uself") {
225+
t.Fatalf("connected remover was incorrectly evicted: %v", members)
226+
}
227+
})
228+
}
229+
}
230+
231+
func TestMemberRemovalSystemMessageValidation(t *testing.T) {
232+
tests := []struct {
233+
name string
234+
fromMID string
235+
locArgs string
236+
}{
237+
{name: "missing target", fromMID: "Uremover"},
238+
{name: "only repeats remover", fromMID: "Uremover", locArgs: "Uremover"},
239+
{name: "target is not a user MID", fromMID: "Uremover", locArgs: "not-a-mid"},
240+
{name: "remover is not a user MID", fromMID: "Cgroup", locArgs: "Uremoved"},
241+
}
242+
for _, test := range tests {
243+
t.Run(test.name, func(t *testing.T) {
244+
msg := &line.Message{
245+
From: test.fromMID,
246+
To: "Cgroup",
247+
ContentType: int(ContentSystem),
248+
ContentMetadata: map[string]string{
249+
"LOC_KEY": "C_MR",
250+
"LOC_ARGS": test.locArgs,
251+
},
252+
}
253+
if isHandledSystemMessage(msg) {
254+
t.Fatal("malformed removal was unexpectedly accepted")
255+
}
256+
})
257+
}
258+
259+
valid := &line.Message{
260+
From: "Uremover",
261+
To: "Cgroup",
262+
ContentType: int(ContentSystem),
263+
ContentMetadata: map[string]string{
264+
"LOC_KEY": "A_MR",
265+
"LOC_ARGS": "Uremoved",
266+
},
267+
}
268+
if !isHandledSystemMessage(valid) {
269+
t.Fatal("valid removal with a distinct target was rejected")
270+
}
271+
}
272+
125273
func TestHandledSystemMessageRejectsUnsupportedRecords(t *testing.T) {
126274
tests := []struct {
127275
name string

0 commit comments

Comments
 (0)