diff --git a/pkg/connector/send_message_test.go b/pkg/connector/send_message_test.go index 2b6511d..071d3cd 100644 --- a/pkg/connector/send_message_test.go +++ b/pkg/connector/send_message_test.go @@ -123,6 +123,14 @@ func TestLineGroupE2EEFetchFailureErrorAllowsNoUsableGroupKeyFallback(t *testing } } +func TestLineGroupE2EEFetchFailureErrorAllowsOversizedGroupFallback(t *testing.T) { + registerErr := errors.New(`registerE2EEGroupKey failed: API error 400: {"code":10051,"message":"RESPONSE_ERROR","data":{"name":"TalkException","message":"TalkException","code":100,"reason":"exceed max member","parameterMap":{}}}`) + err := lineGroupE2EEFetchFailureError(fmt.Errorf("auto-register group key: %w", registerErr)) + if err != nil { + t.Fatalf("err = %v, want nil to allow plaintext fallback", err) + } +} + func TestLineGroupE2EEFetchFailureErrorWrapsMissingPrivateKeyStatus(t *testing.T) { err := lineGroupE2EEFetchFailureError(fmt.Errorf("failed to unwrap group key: %w", e2ee.ErrMissingOwnPrivateKey)) var status bridgev2.MessageStatus diff --git a/pkg/line/errors.go b/pkg/line/errors.go index 8956d37..17c50b3 100644 --- a/pkg/line/errors.go +++ b/pkg/line/errors.go @@ -129,11 +129,14 @@ func IsNoUsableE2EEGroupKey(err error) bool { return true } // Detect TalkException codes in raw API error strings (HTTP 400 with code 10051). - // Code 98 = member has LS off; Code 1 = auth failed. + // Code 98 = member has LS off; Code 1 = auth failed; + // Code 100 "exceed max member" = the group is too large for key registration. // NOTE: Code 5 "not found" is handled by IsGroupKeyNotFound (auto-register), NOT here. - if strings.Contains(msg, "\"code\":10051") && strings.Contains(msg, "talkexception") { + if hasResponseErrorCode(msg) && strings.Contains(msg, "talkexception") { if strings.Contains(msg, "\"code\":98,") || strings.Contains(msg, "\"code\":98}") || - strings.Contains(msg, "\"code\":1,") || strings.Contains(msg, "\"code\":1}") { + strings.Contains(msg, "\"code\":1,") || strings.Contains(msg, "\"code\":1}") || + (hasJSONCode(msg, 100) && (strings.Contains(msg, `"reason":"exceed max member"`) || + strings.Contains(msg, `"reason": "exceed max member"`))) { return true } } @@ -222,8 +225,10 @@ func isNoUsableE2EEGroupKeyTalkException(message string, data talkExceptionData) } // Error 5 "not found" = no group shared key exists // Error 98 "member settings off" = at least one member has LS disabled + // Error 100 "exceed max member" = the group is too large for key registration return (data.Code == 5 && strings.EqualFold(data.Reason, "not found")) || - (data.Code == 98 && strings.Contains(strings.ToLower(data.Reason), "member settings off")) + (data.Code == 98 && strings.Contains(strings.ToLower(data.Reason), "member settings off")) || + (data.Code == 100 && strings.EqualFold(strings.TrimSpace(data.Reason), "exceed max member")) } func parseTalkExceptionData(raw json.RawMessage) talkExceptionData { diff --git a/pkg/line/errors_test.go b/pkg/line/errors_test.go index 345326d..a268a57 100644 --- a/pkg/line/errors_test.go +++ b/pkg/line/errors_test.go @@ -121,3 +121,30 @@ func TestIsTalkExceptionNotFound(t *testing.T) { t.Fatal("nil should not be classified as not-found") } } + +func TestIsNoUsableE2EEGroupKeyExceedMaxMember(t *testing.T) { + err := errors.New(`API error 400: {"code":10051,"message":"RESPONSE_ERROR","data":{"name":"TalkException","message":"TalkException","code":100,"reason":"exceed max member","parameterMap":{}}}`) + if !IsNoUsableE2EEGroupKey(err) { + t.Fatal("expected exceed max member to disable group E2EE") + } + + otherCode100 := errors.New(`API error 400: {"code":10051,"message":"RESPONSE_ERROR","data":{"name":"TalkException","message":"TalkException","code":100,"reason":"different reason","parameterMap":{}}}`) + if IsNoUsableE2EEGroupKey(otherCode100) { + t.Fatal("unrelated TalkException code 100 must not disable group E2EE") + } + + phraseOutsideReason := errors.New(`API error 400: {"code":10051,"message":"exceed max member","data":{"name":"TalkException","code":100,"reason":"different reason"}}`) + if IsNoUsableE2EEGroupKey(phraseOutsideReason) { + t.Fatal("exceed max member outside the reason field must not disable group E2EE") + } +} + +func TestNoUsableE2EEGroupKeyTalkExceptionExceedMaxMember(t *testing.T) { + if !isNoUsableE2EEGroupKeyTalkException("RESPONSE_ERROR", talkExceptionData{ + Name: "TalkException", + Code: 100, + Reason: "exceed max member", + }) { + t.Fatal("expected structured exceed max member response to disable group E2EE") + } +}