diff --git a/pkg/line/client.go b/pkg/line/client.go index 80c9004..9d1925c 100644 --- a/pkg/line/client.go +++ b/pkg/line/client.go @@ -331,16 +331,28 @@ func (c *Client) waitForLoginLF1(verifier string) (*LoginResult, error) { } defer resp.Body.Close() - body, _ := io.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read LF1 polling response: %w", err) + } + if resp.StatusCode != http.StatusOK { + return nil, loginPollingFailure(resp.StatusCode, body) + } var wrapper struct { - Code int `json:"code"` + Code *int `json:"code"` Message string `json:"message"` Data LoginPollingResult `json:"data"` } if err := json.Unmarshal(body, &wrapper); err != nil { return nil, fmt.Errorf("failed to parse LF1 polling response: %w", err) } + if wrapper.Code == nil { + return nil, errors.New("LF1 polling returned an invalid response without a success code") + } + if *wrapper.Code != 0 { + return nil, loginPollingFailure(resp.StatusCode, body) + } meta := wrapper.Data.Result.Metadata diff --git a/pkg/line/errors.go b/pkg/line/errors.go index 3960efc..ff30ce4 100644 --- a/pkg/line/errors.go +++ b/pkg/line/errors.go @@ -6,6 +6,7 @@ import ( "encoding/json" "errors" "fmt" + "net/http" "strconv" "strings" ) @@ -256,6 +257,50 @@ type talkExceptionData struct { Reason string `json:"reason"` } +func loginPollingFailure(httpStatus int, body []byte) error { + var response struct { + Code *int `json:"code"` + Message string `json:"message"` + Data talkExceptionData `json:"data"` + } + decodeErr := json.Unmarshal(body, &response) + details := "LINE response without a valid code" + if decodeErr == nil && response.Code != nil { + details = fmt.Sprintf("LINE response code %d", *response.Code) + } + if decodeErr == nil && response.Code != nil && *response.Code == 10051 && strings.EqualFold(response.Message, "RESPONSE_ERROR") && strings.EqualFold(response.Data.Name, "TalkException") { + response.Message = "RESPONSE_ERROR" + response.Data.Name = "TalkException" + response.Data.Message = safeLoginPollingReason(response.Data.Message) + response.Data.Reason = safeLoginPollingReason(response.Data.Reason) + if sanitized, err := json.Marshal(response); err == nil { + details = string(sanitized) + } + } + if httpStatus != http.StatusOK { + return fmt.Errorf("LF1 polling failed: API error %d: %s", httpStatus, details) + } + return fmt.Errorf("LF1 polling failed: %s", details) +} + +func safeLoginPollingReason(reason string) string { + // Arbitrary provider text can echo secrets, so retain only fixed rejection messages. + switch strings.ToLower(strings.TrimSpace(reason)) { + case "authentication failed": + return "authentication failed" + case "failed to issue v3 token": + return "Failed to issue V3 token" + case "blocked user": + return "blocked user" + case "account id or password is invalid": + return "Account ID or password is invalid" + case "too many login attempts": + return "Too many login attempts" + default: + return "" + } +} + // IsE2EEGroupMemberMismatch identifies a rejected registration that needs a // fresh server member/key snapshot, not a plaintext fallback. func IsE2EEGroupMemberMismatch(err error) bool {