diff --git a/pkg/connector/auth_recovery.go b/pkg/connector/auth_recovery.go index c818614..853d764 100644 --- a/pkg/connector/auth_recovery.go +++ b/pkg/connector/auth_recovery.go @@ -29,7 +29,7 @@ func callLineWithRecovery[T any](ctx context.Context, client *line.Client, deps if errRecover := deps.recover(ctx); errRecover != nil { var zero T - return client, zero, fmt.Errorf("failed to recover token after LINE auth error: %w", errRecover) + return client, zero, fmt.Errorf("failed to recover token after LINE auth error (%w): %w", err, errRecover) } client = deps.newClient() diff --git a/pkg/connector/auth_recovery_test.go b/pkg/connector/auth_recovery_test.go index 07e8b01..e6f2453 100644 --- a/pkg/connector/auth_recovery_test.go +++ b/pkg/connector/auth_recovery_test.go @@ -29,6 +29,7 @@ func TestCallLineWithRecovery(t *testing.T) { wantRecover int wantErr error wantErrPrefix string + wantAuthError bool }{ { name: "success without recovery", @@ -62,6 +63,7 @@ func TestCallLineWithRecovery(t *testing.T) { wantCalls: 1, wantRecover: 1, wantErrPrefix: "failed to recover token after LINE auth error", + wantAuthError: true, }, { name: "retry auth error is not retried again", @@ -114,6 +116,9 @@ func TestCallLineWithRecovery(t *testing.T) { t.Fatalf("err = %v, want containing %q", err, tt.wantErrPrefix) } } + if tt.wantAuthError && (!errors.Is(err, errAuthRequired) || !line.IsAuthError(err)) { + t.Fatalf("err = %v, want original auth error to remain detectable", err) + } if tt.wantErr == nil && tt.wantErrPrefix == "" && err != nil { t.Fatalf("unexpected err: %v", err) } diff --git a/pkg/connector/creategroup.go b/pkg/connector/creategroup.go index d839335..bd87fcc 100644 --- a/pkg/connector/creategroup.go +++ b/pkg/connector/creategroup.go @@ -13,7 +13,13 @@ import ( "github.com/highesttt/matrix-line-messenger/pkg/line" ) -var _ bridgev2.GroupCreatingNetworkAPI = (*LineClient)(nil) +var ( + _ bridgev2.GroupCreatingNetworkAPI = (*LineClient)(nil) + + getLastE2EEPublicKeysWithClient = func(client *line.Client, req line.GetLastE2EEPublicKeysRequest) (map[string]line.E2EEPeerPublicKey, error) { + return client.GetLastE2EEPublicKeys(req) + } +) func (lc *LineClient) CreateGroup(ctx context.Context, params *bridgev2.GroupCreateParams) (*bridgev2.CreateChatResponse, error) { participantMids := make([]string, len(params.Participants)) @@ -133,73 +139,92 @@ func (lc *LineClient) CreateGroup(ctx context.Context, params *bridgev2.GroupCre }, nil } -// registerGroupKey generates a random 32-byte group key, wraps it for each member -// using ECDH + AES-256-CBC, and registers it with the LINE server so all members -// can decrypt group messages. The bridge user's own MID is excluded since the -// creator already has the group key. -func (lc *LineClient) registerGroupKey(ctx context.Context, chatMid string, members []string) error { - // Exclude the bridge user's own MID — the creator already has the group key. +func (lc *LineClient) groupKeyMemberMIDs(chatMid string, members []string) []string { otherMembers := make([]string, 0, len(members)) + seen := make(map[string]struct{}, len(members)) for _, mid := range members { - if mid != lc.Mid { - otherMembers = append(otherMembers, mid) + if !isUserMID(mid) || lc.isOwnMID(mid) || mid == chatMid { + continue } + if _, ok := seen[mid]; ok { + continue + } + seen[mid] = struct{}{} + otherMembers = append(otherMembers, mid) } - if len(otherMembers) == 0 { - return fmt.Errorf("no other members to register group key for") - } - members = otherMembers - - client := lc.newClient() + return otherMembers +} - // Fetch current E2EE public keys for all other members as a batch. If the batch - // call fails (e.g. server 500 for a specific member), fall back to fetching - // each member's key individually via NegotiateE2EEPublicKey. - pubKeysReq := line.GetLastE2EEPublicKeysRequest{ - ChatMid: chatMid, - Members: members, - } - pubKeys, err := client.GetLastE2EEPublicKeys(pubKeysReq) - if err != nil { - if lc.shouldAttemptTokenRecovery(ctx, err) { - if errRecover := lc.recoverToken(ctx); errRecover == nil { - client = lc.newClient() - pubKeys, err = client.GetLastE2EEPublicKeys(pubKeysReq) - } +func (lc *LineClient) resolveGroupMemberPublicKeys(ctx context.Context, client *line.Client, chatMid string, members []string) (*line.Client, map[string]line.E2EEPeerPublicKey, error) { + req := line.GetLastE2EEPublicKeysRequest{ChatMid: chatMid, Members: members} + client, pubKeys, batchErr := callLineResultUsing(lc, ctx, client, func(client *line.Client) (map[string]line.E2EEPeerPublicKey, error) { + return getLastE2EEPublicKeysWithClient(client, req) + }) + if batchErr != nil { + if ctx.Err() != nil || lc.isSessionInvalidated() || line.IsAuthError(batchErr) { + return client, nil, batchErr } - } - if err != nil { - // Batch call failed — try individual key negotiation per member - lc.UserLogin.Bridge.Log.Warn().Err(err). + lc.UserLogin.Bridge.Log.Warn().Err(batchErr). Str("chat_mid", chatMid). Int("members", len(members)). - Msg("Batch GetLastE2EEPublicKeys failed, falling back to per-member NegotiateE2EEPublicKey") - pubKeys = make(map[string]line.E2EEPeerPublicKey) - for _, mid := range members { - res, nErr := client.NegotiateE2EEPublicKey(mid) - if nErr != nil { - if line.IsNoUsableE2EEPublicKey(nErr) { - lc.UserLogin.Bridge.Log.Debug().Str("member", mid).Msg("Member has Letter Sealing disabled, skipping") - continue - } - if lc.shouldAttemptTokenRecovery(ctx, nErr) { - if errRecover := lc.recoverToken(ctx); errRecover == nil { - client = lc.newClient() - res, nErr = client.NegotiateE2EEPublicKey(mid) - } - } - if nErr != nil { - lc.UserLogin.Bridge.Log.Warn().Err(nErr).Str("member", mid).Msg("Failed to negotiate key for member, skipping") - continue - } + Msg("Batch GetLastE2EEPublicKeys failed, resolving member keys individually") + } + if pubKeys == nil { + pubKeys = make(map[string]line.E2EEPeerPublicKey, len(members)) + } + + for _, mid := range members { + if pk, ok := pubKeys[mid]; ok && pk.KeyID > 0 && pk.KeyData != "" { + continue + } + + var res *line.E2EEPublicKey + var err error + client, res, err = callLineResultUsing(lc, ctx, client, func(client *line.Client) (*line.E2EEPublicKey, error) { + return negotiateE2EEPublicKeyWithClient(client, mid) + }) + if err != nil { + if ctx.Err() != nil || lc.isSessionInvalidated() || line.IsAuthError(err) { + return client, nil, fmt.Errorf("negotiate E2EE key for member %s: %w", mid, err) } - keyID, nErr := res.KeyID.Int64() - if nErr != nil { - lc.UserLogin.Bridge.Log.Warn().Err(nErr).Str("member", mid).Msg("Failed to parse key ID, skipping") - continue + if line.IsNoUsableE2EEPublicKey(err) { + return client, nil, fmt.Errorf("%w: member %s has Letter Sealing disabled", line.ErrNoUsableE2EEGroupKey, mid) } - pubKeys[mid] = line.E2EEPeerPublicKey{KeyID: int(keyID), KeyData: res.PublicKey} + return client, nil, fmt.Errorf("%w: negotiate E2EE key for member %s: %w", line.ErrNoUsableE2EEGroupKey, mid, err) + } + if res == nil || res.PublicKey == "" { + return client, nil, fmt.Errorf("%w: member %s returned no public key", line.ErrNoUsableE2EEGroupKey, mid) } + keyID, err := res.KeyID.Int64() + if err != nil { + return client, nil, fmt.Errorf("%w: parse E2EE key ID for member %s: %w", line.ErrNoUsableE2EEGroupKey, mid, err) + } + if keyID <= 0 { + return client, nil, fmt.Errorf("%w: member %s returned invalid key ID %d", line.ErrNoUsableE2EEGroupKey, mid, keyID) + } + pubKeys[mid] = line.E2EEPeerPublicKey{KeyID: int(keyID), KeyData: res.PublicKey} + } + + return client, pubKeys, nil +} + +// registerGroupKey generates a random 32-byte group key, wraps it for each member +// using ECDH + AES-256-CBC, and registers it with the LINE server so all members +// can decrypt group messages. +func (lc *LineClient) registerGroupKey(ctx context.Context, chatMid string, members []string) error { + members = lc.groupKeyMemberMIDs(chatMid, members) + if len(members) == 0 { + return fmt.Errorf("no other members to register group key for") + } + + client := lc.newClient() + + // Batch responses can be partial without returning an error. Resolve every + // missing member individually so registration arrays retain the server's + // expected member count. + client, pubKeys, err := lc.resolveGroupMemberPublicKeys(ctx, client, chatMid, members) + if err != nil { + return err } // Generate group key in WASM (same approach as LINE Chrome Extension). @@ -216,19 +241,13 @@ func (lc *LineClient) registerGroupKey(ctx context.Context, chatMid string, memb for _, mid := range members { pk, ok := pubKeys[mid] - if !ok { - lc.UserLogin.Bridge.Log.Debug().Str("member", mid).Msg("No E2EE public key for member, skipping") - continue - } - if pk.KeyData == "" { - lc.UserLogin.Bridge.Log.Debug().Str("member", mid).Int("key_id", pk.KeyID).Msg("Empty public key data for member, skipping") - continue + if !ok || pk.KeyID <= 0 || pk.KeyData == "" { + return fmt.Errorf("%w: incomplete E2EE public key for member %s", line.ErrNoUsableE2EEGroupKey, mid) } encryptedKey, err := lc.E2EE.WrapGroupKeyForMember(pk.KeyData, groupKeyID) if err != nil { - lc.UserLogin.Bridge.Log.Warn().Err(err).Str("member", mid).Msg("Failed to wrap group key for member, skipping") - continue + return fmt.Errorf("wrap group key for member %s: %w", mid, err) } apiMembers = append(apiMembers, mid) diff --git a/pkg/connector/e2ee_keys.go b/pkg/connector/e2ee_keys.go index 2d178f9..0cbc31c 100644 --- a/pkg/connector/e2ee_keys.go +++ b/pkg/connector/e2ee_keys.go @@ -46,8 +46,8 @@ func (lc *LineClient) fetchAndUnwrapGroupKey(ctx context.Context, chatMid string Msg("No group key found, auto-registering") if registerErr := lc.autoRegisterGroupKey(ctx, chatMid); registerErr != nil { lc.UserLogin.Bridge.Log.Warn().Err(registerErr).Str("chat_mid", chatMid). - Msg("Auto-register group key failed, returning original error") - return err + Msg("Auto-register group key failed") + return fmt.Errorf("auto-register group key: %w", registerErr) } sharedKey, err = fetch() } @@ -193,6 +193,20 @@ func (lc *LineClient) clearGroupNoE2EE(chatMid string) { delete(lc.noE2EEGroups, chatMid) } +func (lc *LineClient) cacheGroupMemberMIDs(chatMid string, mids []string) { + // A self-only result is ambiguous: LINE sometimes returns an empty member + // map for active groups. Keep a previously complete list for fallback. + if len(mids) <= 1 { + return + } + lc.cacheMu.Lock() + defer lc.cacheMu.Unlock() + if lc.groupMemberCache == nil { + lc.groupMemberCache = make(map[string][]string) + } + lc.groupMemberCache[chatMid] = append([]string(nil), mids...) +} + // getChatMemberMIDs fetches the member and invitee MIDs for a group chat via GetChats. // Invitees are included because group key registration must happen before they accept, // otherwise the key won't be available when they start sending messages. @@ -219,13 +233,17 @@ func (lc *LineClient) getChatMemberMIDs(ctx context.Context, chatMid string) ([] } seen := make(map[string]struct{}) for mid := range chat.Extra.GroupExtra.MemberMids { - seen[mid] = struct{}{} + if isUserMID(mid) { + seen[mid] = struct{}{} + } } for mid := range chat.Extra.GroupExtra.InviteeMids { - seen[mid] = struct{}{} + if isUserMID(mid) { + seen[mid] = struct{}{} + } } - // Always include the bridge user's own MID since we're definitely a member - if _, ok := seen[lc.Mid]; !ok { + // Include the bridge user's MID when it has a valid user prefix. + if isUserMID(lc.Mid) { seen[lc.Mid] = struct{}{} } mids := make([]string, 0, len(seen)) @@ -236,13 +254,9 @@ func (lc *LineClient) getChatMemberMIDs(ctx context.Context, chatMid string) ([] return nil, fmt.Errorf("chat %s has no members or invitees", chatMid) } - // Cache the successful result for fallback use. - lc.cacheMu.Lock() - if lc.groupMemberCache == nil { - lc.groupMemberCache = make(map[string][]string) - } - lc.groupMemberCache[chatMid] = mids - lc.cacheMu.Unlock() + // Cache complete results for fallback use. Do not replace a richer cached + // list when LINE returns only the caller. + lc.cacheGroupMemberMIDs(chatMid, mids) return mids, nil } @@ -261,6 +275,7 @@ func (lc *LineClient) autoRegisterGroupKey(ctx context.Context, chatMid string) if len(members) == 1 && members[0] == lc.Mid { lc.cacheMu.Lock() cached, ok := lc.groupMemberCache[chatMid] + cached = append([]string(nil), cached...) lc.cacheMu.Unlock() if ok && len(cached) > 1 { lc.UserLogin.Bridge.Log.Warn().Str("chat_mid", chatMid). @@ -308,6 +323,7 @@ func (lc *LineClient) getGroupMemberMIDsViaMatrix(ctx context.Context, chatMid s } mids := make([]string, 0, len(matrixMembers)) + seen := make(map[string]struct{}, len(matrixMembers)+1) for mxid := range matrixMembers { // Skip the bridge user's own Matrix account if present. if mxid == lc.UserLogin.UserMXID { @@ -316,7 +332,11 @@ func (lc *LineClient) getGroupMemberMIDsViaMatrix(ctx context.Context, chatMid s // Parse the ghost MXID back to a network ID (LINE MID). if netID, ok := lc.UserLogin.Bridge.Matrix.ParseGhostMXID(mxid); ok { mid := string(netID) - if mid != lc.Mid && mid != string(lc.UserLogin.ID) { + if isUserMID(mid) && !lc.isOwnMID(mid) { + if _, exists := seen[mid]; exists { + continue + } + seen[mid] = struct{}{} mids = append(mids, mid) } } @@ -326,8 +346,10 @@ func (lc *LineClient) getGroupMemberMIDsViaMatrix(ctx context.Context, chatMid s return nil, fmt.Errorf("no LINE members found via Matrix") } - // Always include the bridge user's own MID. - mids = append(mids, lc.Mid) + // Include the bridge user's MID when it has a valid user prefix. + if isUserMID(lc.Mid) { + mids = append(mids, lc.Mid) + } lc.UserLogin.Bridge.Log.Debug().Str("chat_mid", chatMid). Int("matrix_members", len(matrixMembers)). diff --git a/pkg/connector/e2ee_keys_test.go b/pkg/connector/e2ee_keys_test.go index cb9d8a5..b3b0ad3 100644 --- a/pkg/connector/e2ee_keys_test.go +++ b/pkg/connector/e2ee_keys_test.go @@ -160,3 +160,219 @@ func TestEnsurePeerKeyCachesNoUsablePublicKeyWithoutRecovery(t *testing.T) { t.Fatalf("negotiate calls = %d, want cached negative lookup", negotiateCalls) } } + +func TestGroupKeyMemberMIDsKeepsOnlyUniqueUsers(t *testing.T) { + lc := newPeerKeyTestClient() + lc.Mid = "U-self" + + got := lc.groupKeyMemberMIDs("C-group", []string{ + "U-self", + "C-group", + "R-room", + "U-peer", + "U-peer", + "u-lowercase-peer", + "", + "not-a-mid", + }) + + if len(got) != 2 || got[0] != "U-peer" || got[1] != "u-lowercase-peer" { + t.Fatalf("groupKeyMemberMIDs = %v, want [U-peer u-lowercase-peer]", got) + } +} + +func TestIsUserMIDAcceptsWirePrefixCasing(t *testing.T) { + tests := map[string]bool{ + "U-peer": true, + "u-peer": true, + "C-chat": false, + "R-room": false, + "": false, + "U": false, + } + for mid, want := range tests { + if got := isUserMID(mid); got != want { + t.Errorf("isUserMID(%q) = %t, want %t", mid, got, want) + } + } +} + +func TestCacheGroupMemberMIDsPreservesRicherList(t *testing.T) { + lc := newPeerKeyTestClient() + lc.groupMemberCache = map[string][]string{ + "C-group": {"U-self", "U-peer"}, + } + + lc.cacheGroupMemberMIDs("C-group", []string{"U-self"}) + + got := lc.groupMemberCache["C-group"] + if len(got) != 2 || got[0] != "U-self" || got[1] != "U-peer" { + t.Fatalf("cached members = %v, want richer prior list", got) + } +} + +func TestResolveGroupMemberPublicKeysFillsPartialBatchResponse(t *testing.T) { + oldGetLast := getLastE2EEPublicKeysWithClient + oldNegotiate := negotiateE2EEPublicKeyWithClient + t.Cleanup(func() { + getLastE2EEPublicKeysWithClient = oldGetLast + negotiateE2EEPublicKeyWithClient = oldNegotiate + }) + + getLastE2EEPublicKeysWithClient = func(*line.Client, line.GetLastE2EEPublicKeysRequest) (map[string]line.E2EEPeerPublicKey, error) { + return map[string]line.E2EEPeerPublicKey{ + "U-batch": {KeyID: 10, KeyData: "batch-public-key"}, + }, nil + } + var negotiated []string + negotiateE2EEPublicKeyWithClient = func(_ *line.Client, mid string) (*line.E2EEPublicKey, error) { + negotiated = append(negotiated, mid) + return &line.E2EEPublicKey{KeyID: json.Number("20"), PublicKey: "negotiated-public-key"}, nil + } + + lc := newPeerKeyTestClient() + _, got, err := lc.resolveGroupMemberPublicKeys( + context.Background(), + line.NewClient("access"), + "C-group", + []string{"U-batch", "U-missing"}, + ) + if err != nil { + t.Fatalf("resolveGroupMemberPublicKeys returned error: %v", err) + } + if len(negotiated) != 1 || negotiated[0] != "U-missing" { + t.Fatalf("negotiated members = %v, want [U-missing]", negotiated) + } + if got["U-batch"].KeyID != 10 || got["U-missing"].KeyID != 20 { + t.Fatalf("resolved keys = %#v, want batch and negotiated entries", got) + } +} + +func TestResolveGroupMemberPublicKeysFallsBackAfterBatchError(t *testing.T) { + oldGetLast := getLastE2EEPublicKeysWithClient + oldNegotiate := negotiateE2EEPublicKeyWithClient + t.Cleanup(func() { + getLastE2EEPublicKeysWithClient = oldGetLast + negotiateE2EEPublicKeyWithClient = oldNegotiate + }) + + getLastE2EEPublicKeysWithClient = func(*line.Client, line.GetLastE2EEPublicKeysRequest) (map[string]line.E2EEPeerPublicKey, error) { + return nil, errors.New("batch unavailable") + } + var negotiated []string + negotiateE2EEPublicKeyWithClient = func(_ *line.Client, mid string) (*line.E2EEPublicKey, error) { + negotiated = append(negotiated, mid) + return &line.E2EEPublicKey{KeyID: json.Number("30"), PublicKey: "fallback-public-key"}, nil + } + + lc := newPeerKeyTestClient() + _, got, err := lc.resolveGroupMemberPublicKeys( + context.Background(), + line.NewClient("access"), + "C-group", + []string{"U-peer"}, + ) + if err != nil { + t.Fatalf("resolveGroupMemberPublicKeys returned error: %v", err) + } + if len(negotiated) != 1 || negotiated[0] != "U-peer" { + t.Fatalf("negotiated members = %v, want [U-peer]", negotiated) + } + if got["U-peer"].KeyID != 30 { + t.Fatalf("resolved key = %#v, want fallback key", got["U-peer"]) + } +} + +func TestResolveGroupMemberPublicKeysReturnsNoUsableGroupKey(t *testing.T) { + oldGetLast := getLastE2EEPublicKeysWithClient + oldNegotiate := negotiateE2EEPublicKeyWithClient + t.Cleanup(func() { + getLastE2EEPublicKeysWithClient = oldGetLast + negotiateE2EEPublicKeyWithClient = oldNegotiate + }) + + getLastE2EEPublicKeysWithClient = func(*line.Client, line.GetLastE2EEPublicKeysRequest) (map[string]line.E2EEPeerPublicKey, error) { + return nil, nil + } + negotiateE2EEPublicKeyWithClient = func(*line.Client, string) (*line.E2EEPublicKey, error) { + return nil, line.ErrNoUsableE2EEPublicKey + } + + lc := newPeerKeyTestClient() + _, _, err := lc.resolveGroupMemberPublicKeys( + context.Background(), + line.NewClient("access"), + "C-group", + []string{"U-peer"}, + ) + if !errors.Is(err, line.ErrNoUsableE2EEGroupKey) { + t.Fatalf("error = %v, want ErrNoUsableE2EEGroupKey", err) + } +} + +func TestResolveGroupMemberPublicKeysAllowsFallbackAfterMemberError(t *testing.T) { + oldGetLast := getLastE2EEPublicKeysWithClient + oldNegotiate := negotiateE2EEPublicKeyWithClient + t.Cleanup(func() { + getLastE2EEPublicKeysWithClient = oldGetLast + negotiateE2EEPublicKeyWithClient = oldNegotiate + }) + + getLastE2EEPublicKeysWithClient = func(*line.Client, line.GetLastE2EEPublicKeysRequest) (map[string]line.E2EEPeerPublicKey, error) { + return nil, nil + } + memberErr := errors.New("temporary member key failure") + negotiateE2EEPublicKeyWithClient = func(*line.Client, string) (*line.E2EEPublicKey, error) { + return nil, memberErr + } + + lc := newPeerKeyTestClient() + _, _, err := lc.resolveGroupMemberPublicKeys( + context.Background(), + line.NewClient("access"), + "C-group", + []string{"U-peer"}, + ) + if !errors.Is(err, line.ErrNoUsableE2EEGroupKey) { + t.Fatalf("error = %v, want ErrNoUsableE2EEGroupKey", err) + } + if !errors.Is(err, memberErr) { + t.Fatalf("error = %v, want member error to remain wrapped", err) + } +} + +func TestResolveGroupMemberPublicKeysPreservesAuthRecoveryFailure(t *testing.T) { + oldGetLast := getLastE2EEPublicKeysWithClient + oldNegotiate := negotiateE2EEPublicKeyWithClient + oldRecover := recoverLineToken + t.Cleanup(func() { + getLastE2EEPublicKeysWithClient = oldGetLast + negotiateE2EEPublicKeyWithClient = oldNegotiate + recoverLineToken = oldRecover + }) + + getLastE2EEPublicKeysWithClient = func(*line.Client, line.GetLastE2EEPublicKeysRequest) (map[string]line.E2EEPeerPublicKey, error) { + return nil, nil + } + negotiateE2EEPublicKeyWithClient = func(*line.Client, string) (*line.E2EEPublicKey, error) { + return nil, errAuthRequired + } + recoveryErr := errors.New("token recovery failed") + recoverLineToken = func(*LineClient, context.Context) error { + return recoveryErr + } + + lc := newPeerKeyTestClient() + _, _, err := lc.resolveGroupMemberPublicKeys( + context.Background(), + line.NewClient("access"), + "C-group", + []string{"U-peer"}, + ) + if !errors.Is(err, errAuthRequired) || !errors.Is(err, recoveryErr) { + t.Fatalf("error = %v, want auth and recovery failures", err) + } + if errors.Is(err, line.ErrNoUsableE2EEGroupKey) { + t.Fatalf("error = %v, auth failure must not allow plaintext fallback", err) + } +} diff --git a/pkg/connector/send_message_test.go b/pkg/connector/send_message_test.go index 3bb308e..add22a8 100644 --- a/pkg/connector/send_message_test.go +++ b/pkg/connector/send_message_test.go @@ -51,7 +51,7 @@ func TestLineGroupE2EEFetchFailureErrorReturnsAuthErrors(t *testing.T) { } func TestLineGroupE2EEFetchFailureErrorAllowsNoUsableGroupKeyFallback(t *testing.T) { - err := lineGroupE2EEFetchFailureError(line.ErrNoUsableE2EEGroupKey) + err := lineGroupE2EEFetchFailureError(fmt.Errorf("auto-register group key: %w", line.ErrNoUsableE2EEGroupKey)) if err != nil { t.Fatalf("err = %v, want nil", err) } diff --git a/pkg/connector/sync.go b/pkg/connector/sync.go index 5fad3b5..bd0f183 100644 --- a/pkg/connector/sync.go +++ b/pkg/connector/sync.go @@ -684,7 +684,7 @@ func (lc *LineClient) chatToChatInfo(ctx context.Context, chat *line.Chat, exclu // returns empty MemberMids (known LINE API issue). allMemberMids := make([]string, 0, len(chat.Extra.GroupExtra.MemberMids)) for m := range chat.Extra.GroupExtra.MemberMids { - if m == lc.Mid || m == string(lc.UserLogin.ID) || strings.HasPrefix(m, "c") || strings.HasPrefix(m, "r") { + if !isUserMID(m) || lc.isOwnMID(m) { continue } allMemberMids = append(allMemberMids, m) @@ -697,7 +697,7 @@ func (lc *LineClient) chatToChatInfo(ctx context.Context, chat *line.Chat, exclu }) } for m := range chat.Extra.GroupExtra.InviteeMids { - if m == lc.Mid || m == string(lc.UserLogin.ID) || strings.HasPrefix(m, "c") || strings.HasPrefix(m, "r") { + if !isUserMID(m) || lc.isOwnMID(m) { continue } allMemberMids = append(allMemberMids, m) @@ -716,7 +716,7 @@ func (lc *LineClient) chatToChatInfo(ctx context.Context, chat *line.Chat, exclu if len(allMemberMids) == 0 { lc.cacheGroupMembersFromRecentMessages(ctx, chat.ChatMid) for _, m := range lc.getCachedGroupMembers(chat.ChatMid) { - if m == lc.Mid || m == string(lc.UserLogin.ID) || strings.HasPrefix(m, "c") || strings.HasPrefix(m, "r") { + if !isUserMID(m) || lc.isOwnMID(m) { continue } allMemberMids = append(allMemberMids, m) @@ -733,14 +733,11 @@ func (lc *LineClient) chatToChatInfo(ctx context.Context, chat *line.Chat, exclu groupMemberMids = make([]string, 0, len(allMemberMids)+1) groupMemberMids = append(groupMemberMids, lc.Mid) groupMemberMids = append(groupMemberMids, allMemberMids...) + lc.cacheGroupMemberMIDs(chat.ChatMid, groupMemberMids) lc.cacheMu.Lock() - if lc.groupMemberCache == nil { - lc.groupMemberCache = make(map[string][]string) - } if lc.generatedGroupNameCache == nil { lc.generatedGroupNameCache = make(map[string]bool) } - lc.groupMemberCache[chat.ChatMid] = groupMemberMids lc.cacheMu.Unlock() } @@ -917,7 +914,7 @@ func midsFromSystemLocArgs(locArgs string) []string { } func isUserMID(mid string) bool { - return len(mid) > 1 && strings.HasPrefix(mid, "U") + return len(mid) > 1 && (mid[0] == 'U' || mid[0] == 'u') } func isChatMID(mid string) bool { @@ -935,7 +932,7 @@ func (lc *LineClient) isOwnMID(mid string) bool { if mid == lc.Mid { return true } - if lc.UserLogin != nil && mid == string(lc.UserLogin.ID) { + if lc.UserLogin != nil && lc.UserLogin.UserLogin != nil && mid == string(lc.UserLogin.ID) { return true } return false