Skip to content

Commit 412eda7

Browse files
authored
fix: backfilling not working for some accounts (#221)
* fix: backfilling not working for some accounts * fix: indent comments
1 parent 2ddfe44 commit 412eda7

5 files changed

Lines changed: 695 additions & 44 deletions

File tree

pkg/connector/client.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ type LineClient struct {
8080
knownMemberChatMIDs map[string]struct{} // chatMid -> current member chats returned by getAllChatMids
8181
reactionIconMXC map[int]string // predefinedReactionType -> cached MXC URI
8282
recentReactions sync.Map // "msgID\x00emoji" -> struct{} to dedup concurrent 139/140 events
83+
unblockBackfills sync.Map // chat MID -> *unblockBackfillState while unblock history restoration is active
8384

8485
wg sync.WaitGroup
8586
}

pkg/connector/connector.go

Lines changed: 127 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package connector
33
import (
44
"context"
55
"encoding/json"
6+
"errors"
67
"fmt"
78
"net/http"
89
"strings"
@@ -227,6 +228,7 @@ func (ll *LineEmailLogin) StartWithOverride(ctx context.Context, override *bridg
227228

228229
res, err := loginWithCredentials(ll.Email, ll.Password, ll.Certificate)
229230
if err != nil {
231+
ll.logLoginFailure(err, "reconnect")
230232
reason := loginErrorReason(err)
231233
if reason == "" {
232234
reason = fmt.Sprintf("Login failed: %v", err)
@@ -253,6 +255,7 @@ func (ll *LineEmailLogin) SubmitUserInput(ctx context.Context, input map[string]
253255

254256
res, err := loginWithCredentials(ll.Email, ll.Password, "")
255257
if err != nil {
258+
ll.logLoginFailure(err, "credentials")
256259
reason := loginErrorReason(err)
257260
if reason == "" {
258261
reason = fmt.Sprintf("Login failed: %v", err)
@@ -299,28 +302,140 @@ func loginErrorInstructions(message string) string {
299302
return fmt.Sprintf("Could not log in to LINE: %s", message)
300303
}
301304

302-
func loginErrorReason(err error) string {
305+
type loginErrorDetails struct {
306+
HTTPStatus int
307+
ResponseCode int
308+
ResponseMessage string
309+
ErrorName string
310+
ErrorCode int
311+
ErrorMessage string
312+
ErrorReason string
313+
HasHTTPStatus bool
314+
HasResponseCode bool
315+
HasErrorCode bool
316+
HasResponseFields bool
317+
}
318+
319+
func parseLoginErrorDetails(err error) loginErrorDetails {
320+
var details loginErrorDetails
303321
if err == nil {
304-
return ""
322+
return details
305323
}
324+
306325
msg := err.Error()
326+
if apiErrorIndex := strings.Index(msg, "API error "); apiErrorIndex >= 0 {
327+
if parsed, scanErr := fmt.Sscanf(msg[apiErrorIndex:], "API error %d:", &details.HTTPStatus); parsed == 1 && scanErr == nil {
328+
details.HasHTTPStatus = true
329+
}
330+
}
331+
307332
start := strings.Index(msg, "{")
308333
end := strings.LastIndex(msg, "}")
309334
if start == -1 || end == -1 || end <= start {
310-
return ""
335+
return details
311336
}
337+
312338
var payload struct {
313-
Data struct {
314-
Message string `json:"message"`
315-
Reason string `json:"reason"`
316-
} `json:"data"`
339+
Code *int `json:"code"`
340+
Message string `json:"message"`
341+
Data json.RawMessage `json:"data"`
342+
}
343+
if json.Unmarshal([]byte(msg[start:end+1]), &payload) != nil {
344+
return details
345+
}
346+
347+
details.HasResponseFields = true
348+
details.ResponseMessage = payload.Message
349+
if payload.Code != nil {
350+
details.ResponseCode = *payload.Code
351+
details.HasResponseCode = true
352+
}
353+
354+
var responseError struct {
355+
Name string `json:"name"`
356+
Code *int `json:"code"`
357+
Message string `json:"message"`
358+
Reason string `json:"reason"`
359+
}
360+
if len(payload.Data) > 0 && json.Unmarshal(payload.Data, &responseError) == nil {
361+
details.ErrorName = responseError.Name
362+
details.ErrorMessage = responseError.Message
363+
details.ErrorReason = responseError.Reason
364+
if responseError.Code != nil {
365+
details.ErrorCode = *responseError.Code
366+
details.HasErrorCode = true
367+
}
317368
}
318-
if err := json.Unmarshal([]byte(msg[start:end+1]), &payload); err != nil {
369+
return details
370+
}
371+
372+
func loginErrorSummary(err error, details loginErrorDetails) string {
373+
if err == nil {
319374
return ""
320375
}
321-
reason := payload.Data.Reason
376+
if details.HasHTTPStatus {
377+
return fmt.Sprintf("API error %d", details.HTTPStatus)
378+
}
379+
if errors.Is(err, context.DeadlineExceeded) {
380+
return "request timed out"
381+
}
382+
if errors.Is(err, context.Canceled) {
383+
return "request canceled"
384+
}
385+
return ""
386+
}
387+
388+
func loginLogField(value string) string {
389+
value = strings.TrimSpace(value)
390+
const maxRunes = 512
391+
runes := []rune(value)
392+
if len(runes) > maxRunes {
393+
return string(runes[:maxRunes]) + "…"
394+
}
395+
return value
396+
}
397+
398+
func (ll *LineEmailLogin) logLoginFailure(err error, flow string) {
399+
if err == nil || ll.User == nil {
400+
return
401+
}
402+
403+
details := parseLoginErrorDetails(err)
404+
event := ll.User.Log.Warn().
405+
Str("login_flow", flow).
406+
Bool("has_certificate", ll.Certificate != "")
407+
if summary := loginErrorSummary(err, details); summary != "" {
408+
event.Str("error_summary", summary)
409+
}
410+
if details.HasHTTPStatus {
411+
event.Int("http_status", details.HTTPStatus)
412+
}
413+
if details.HasResponseCode {
414+
event.Int("line_response_code", details.ResponseCode)
415+
}
416+
if details.ResponseMessage != "" {
417+
event.Str("line_response_message", loginLogField(details.ResponseMessage))
418+
}
419+
if details.ErrorName != "" {
420+
event.Str("line_error_name", loginLogField(details.ErrorName))
421+
}
422+
if details.HasErrorCode {
423+
event.Int("line_error_code", details.ErrorCode)
424+
}
425+
if details.ErrorMessage != "" {
426+
event.Str("line_error_message", loginLogField(details.ErrorMessage))
427+
}
428+
if details.ErrorReason != "" {
429+
event.Str("line_error_reason", loginLogField(details.ErrorReason))
430+
}
431+
event.Msg("LINE login attempt failed")
432+
}
433+
434+
func loginErrorReason(err error) string {
435+
details := parseLoginErrorDetails(err)
436+
reason := details.ErrorReason
322437
if reason == "" {
323-
reason = payload.Data.Message
438+
reason = details.ErrorMessage
324439
}
325440
if isBlockedUserLoginError(reason) {
326441
return loginTooManyAttemptsReason
@@ -341,6 +456,7 @@ func (ll *LineEmailLogin) Wait(ctx context.Context) (*bridgev2.LoginStep, error)
341456
}
342457
return nil, fmt.Errorf("verification failed: no auth token received")
343458
case err := <-ll.pollErr:
459+
ll.logLoginFailure(err, "verification_poll")
344460
return nil, fmt.Errorf("verification failed: %w", err)
345461
case <-ctx.Done():
346462
return nil, ctx.Err()
@@ -350,6 +466,7 @@ func (ll *LineEmailLogin) Wait(ctx context.Context) (*bridgev2.LoginStep, error)
350466
if ll.AwaitingPIN {
351467
res, err := loginWithCredentials(ll.Email, ll.Password, ll.Certificate)
352468
if err != nil {
469+
ll.logLoginFailure(err, "pin_continuation")
353470
return nil, fmt.Errorf("login failed: %w", err)
354471
}
355472
return ll.handleLoginResponse(ctx, res)
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package connector
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"encoding/json"
7+
"errors"
8+
"fmt"
9+
"strings"
10+
"testing"
11+
12+
"github.com/rs/zerolog"
13+
14+
"maunium.net/go/mautrix/bridgev2"
15+
16+
"github.com/highesttt/matrix-line-messenger/pkg/line"
17+
)
18+
19+
func TestSubmitUserInputLogsStructuredLoginErrorWithoutCredentials(t *testing.T) {
20+
oldLogin := loginWithCredentials
21+
t.Cleanup(func() {
22+
loginWithCredentials = oldLogin
23+
})
24+
25+
loginWithCredentials = func(_, _, _ string) (*line.LoginResult, error) {
26+
return nil, errors.New(`login failed: API error 400: {"code":10051,"message":"RESPONSE_ERROR","data":{"name":"TalkException","message":"Blocked user","code":35,"reason":"blocked user","token":"response-token"},"password":"response-password"}`)
27+
}
28+
29+
var output bytes.Buffer
30+
logger := zerolog.New(&output)
31+
login := &LineEmailLogin{
32+
User: &bridgev2.User{
33+
Bridge: &bridgev2.Bridge{Log: logger},
34+
Log: logger,
35+
},
36+
}
37+
step, err := login.SubmitUserInput(context.Background(), map[string]string{
38+
"email": "ana@example.com",
39+
"password": "input-password",
40+
})
41+
if err != nil {
42+
t.Fatalf("SubmitUserInput returned error: %v", err)
43+
}
44+
if step == nil || step.Instructions != loginTooManyAttemptsInstructions {
45+
t.Fatalf("step instructions = %q, want %q", step.Instructions, loginTooManyAttemptsInstructions)
46+
}
47+
48+
var event map[string]any
49+
if err := json.Unmarshal(bytes.TrimSpace(output.Bytes()), &event); err != nil {
50+
t.Fatalf("failed to parse log event: %v\n%s", err, output.String())
51+
}
52+
wantFields := map[string]any{
53+
"level": "warn",
54+
"message": "LINE login attempt failed",
55+
"login_flow": "credentials",
56+
"has_certificate": false,
57+
"error_summary": "API error 400",
58+
"http_status": float64(400),
59+
"line_response_code": float64(10051),
60+
"line_response_message": "RESPONSE_ERROR",
61+
"line_error_name": "TalkException",
62+
"line_error_code": float64(35),
63+
"line_error_message": "Blocked user",
64+
"line_error_reason": "blocked user",
65+
}
66+
for key, want := range wantFields {
67+
if got := event[key]; got != want {
68+
t.Errorf("log field %s = %#v, want %#v", key, got, want)
69+
}
70+
}
71+
72+
logged := output.String()
73+
for _, secret := range []string{"ana@example.com", "input-password", "response-token", "response-password"} {
74+
if strings.Contains(logged, secret) {
75+
t.Errorf("log contains secret %q: %s", secret, logged)
76+
}
77+
}
78+
}
79+
80+
func TestParseLoginErrorDetailsWithoutJSON(t *testing.T) {
81+
err := errors.New("login failed: request failed: context deadline exceeded")
82+
details := parseLoginErrorDetails(err)
83+
if details.HasHTTPStatus || details.HasResponseFields {
84+
t.Fatalf("unexpected parsed response details: %#v", details)
85+
}
86+
if got := loginErrorSummary(err, details); got != "" {
87+
t.Fatalf("loginErrorSummary = %q, want empty", got)
88+
}
89+
}
90+
91+
func TestLoginErrorSummaryDoesNotIncludeNonJSONResponseBody(t *testing.T) {
92+
err := errors.New("login failed: API error 502: upstream secret response")
93+
details := parseLoginErrorDetails(err)
94+
if got := loginErrorSummary(err, details); got != "API error 502" {
95+
t.Fatalf("loginErrorSummary = %q, want %q", got, "API error 502")
96+
}
97+
}
98+
99+
func TestLoginErrorSummaryAllowsKnownContextErrors(t *testing.T) {
100+
err := fmt.Errorf("login failed: %w", context.DeadlineExceeded)
101+
details := parseLoginErrorDetails(err)
102+
if got := loginErrorSummary(err, details); got != "request timed out" {
103+
t.Fatalf("loginErrorSummary = %q, want %q", got, "request timed out")
104+
}
105+
}

0 commit comments

Comments
 (0)