Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pkg/connector/block_user.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func (lc *LineClient) HandleMatrixBlockUser(ctx context.Context, msg *bridgev2.M
_, err := lc.callLine(ctx, func(client *line.Client) error {
return client.SetContactBlocked(ctx, int64(reqSeq), mid, msg.Content.Block)
})
return err
return wrapSendError(err)
}

func (lc *LineClient) queueUserBlocked(ctx context.Context, mid string, blocked bool) bool {
Expand Down
44 changes: 42 additions & 2 deletions pkg/connector/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ var (
errLineQRLoginRequired = errors.New("LINE requires a new QR scan. Reconnect in Beeper to continue")
)

func init() {
status.BridgeStateHumanErrors.Update(status.BridgeStateErrorMap{
"line-login-failed": "LINE could not sign you in. Sign in again.",
"line-token-expired": "Your LINE session expired and could not be restored. Sign in again.",
"line-sse-disconnected": "Disconnected from LINE, reconnecting",
})
}

const lineMissingE2EEKeyMessage = "LINE encryption keys are unavailable. Reconnect LINE in Beeper to restore message decryption."

var newLineAPIClient = line.NewClient
Expand Down Expand Up @@ -409,6 +417,10 @@ func (lc *LineClient) sendConnectedStateIfCurrent(ctx context.Context) bool {
// Whichever transition gets this lock last determines the visible state.
lc.recoverMu.Lock()
defer lc.recoverMu.Unlock()
return lc.sendConnectedStateLocked(ctx)
}

func (lc *LineClient) sendConnectedStateLocked(ctx context.Context) bool {
if ctx.Err() != nil || lc.superseded.Load() || lc.isSessionInvalidated() || !lc.hasAccessToken() {
return false
}
Expand All @@ -419,6 +431,34 @@ func (lc *LineClient) sendConnectedStateIfCurrent(ctx context.Context) bool {
return true
}

// SSE reconnects must not replace the relogin prompt sent by markMissingE2EEKey.
func (lc *LineClient) sendSSEConnectedIfCurrent(ctx context.Context) bool {
lc.recoverMu.Lock()
defer lc.recoverMu.Unlock()
lc.missingE2EEKeyMu.Lock()
defer lc.missingE2EEKeyMu.Unlock()
if lc.missingE2EEKeyMarked {
return false
}
return lc.sendConnectedStateLocked(ctx)
}

func (lc *LineClient) sendSSEDisconnectedIfCurrent(ctx context.Context, err error) bool {
Comment thread
indent[bot] marked this conversation as resolved.
lc.recoverMu.Lock()
defer lc.recoverMu.Unlock()
lc.missingE2EEKeyMu.Lock()
defer lc.missingE2EEKeyMu.Unlock()
if ctx.Err() != nil || lc.superseded.Load() || lc.isSessionInvalidated() || lc.missingE2EEKeyMarked || lc.UserLogin.BridgeState == nil {
return false
}
lc.UserLogin.BridgeState.Send(status.BridgeState{
StateEvent: status.StateTransientDisconnect,
Error: "line-sse-disconnected",
Info: map[string]any{"go_error": err.Error()},
})
return true
}

func (lc *LineClient) saveSessionInvalidated(ctx context.Context) {
if lc.UserLogin == nil || lc.UserLogin.UserLogin == nil {
return
Expand Down Expand Up @@ -603,7 +643,7 @@ func (lc *LineClient) Connect(ctx context.Context) {
lc.UserLogin.BridgeState.Send(status.BridgeState{
StateEvent: status.StateBadCredentials,
Error: "line-login-failed",
Message: err.Error(),
Info: map[string]any{"go_error": err.Error()},
UserAction: status.UserActionRelogin,
})
return
Expand All @@ -622,7 +662,7 @@ func (lc *LineClient) Connect(ctx context.Context) {
lc.UserLogin.BridgeState.Send(status.BridgeState{
StateEvent: status.StateBadCredentials,
Error: "line-token-expired",
Message: fmt.Sprintf("session expired and could not be restored: %v", err),
Info: map[string]any{"go_error": err.Error()},
UserAction: status.UserActionRelogin,
})
return
Expand Down
2 changes: 1 addition & 1 deletion pkg/connector/connector.go
Original file line number Diff line number Diff line change
Expand Up @@ -724,7 +724,7 @@ func (ll *LineEmailLogin) finishLogin(ctx context.Context, res *line.LoginResult
client.LoginAttempt = res.Attempt
profile, err := client.GetProfileContext(ctx)
if err != nil {
return nil, fmt.Errorf("failed to verify token: %w", err)
return nil, wrapLineLoginError(fmt.Errorf("failed to verify token: %w", err))
}

displayName := profile.DisplayName
Expand Down
2 changes: 1 addition & 1 deletion pkg/connector/creategroup.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func (lc *LineClient) CreateGroup(ctx context.Context, params *bridgev2.GroupCre
return client.CreateChat(participantMids, lineName, chatType)
})
if err != nil {
return nil, fmt.Errorf("failed to create LINE chat: %w", err)
return nil, wrapLineRespError(fmt.Errorf("failed to create LINE chat: %w", err))
}

lc.UserLogin.Bridge.Log.Info().
Expand Down
6 changes: 3 additions & 3 deletions pkg/connector/delete_chat.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@ func (lc *LineClient) HandleMatrixDeleteChat(ctx context.Context, msg *bridgev2.
_, err := lc.callLine(ctx, func(client *line.Client) error {
return client.RejectChatInvitationContext(ctx, int64(reqSeq), chatMid)
})
return err
return wrapSendError(err)
}
_, boxes, err := callLineResult(lc, ctx, func(client *line.Client) (map[string]line.MessageBox, error) {
return client.GetMessageBoxesByIDsContext(ctx, []string{chatMid})
})
if err != nil {
return err
return wrapSendError(err)
}
lastDelivered := boxes[chatMid].LastDeliveredMessageID
if lastDelivered == nil || lastDelivered.MessageID == "" || lastDelivered.MessageID == "-1" {
Expand All @@ -43,7 +43,7 @@ func (lc *LineClient) HandleMatrixDeleteChat(ctx context.Context, msg *bridgev2.
return client.SendChatRemovedContext(ctx, int64(reqSeq), chatMid, lastDelivered.MessageID, 0)
})
if err != nil {
return err
return wrapSendError(err)
}
return lc.saveChatDeletion(context.WithoutCancel(ctx), chatMid, lastDelivered.MessageID)
}
Expand Down
7 changes: 4 additions & 3 deletions pkg/connector/login_qr.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ func (lq *LineQRLogin) Start(ctx context.Context) (*bridgev2.LoginStep, error) {
defer stop()
client := newLineAPIClient("")
session, qr, err := client.StartQRLogin(processCtx)
err = wrapLineLoginError(err)
ll.mu.Lock()
ll.attempt = client.LoginAttempt
canceled := ll.canceled || ctx.Err() != nil || processCtx.Err() != nil
Expand Down Expand Up @@ -123,15 +124,15 @@ func (lq *LineQRLogin) Wait(ctx context.Context) (*bridgev2.LoginStep, error) {
verified, err := lq.client.VerifyQRCertificate(ctx, lq.session, lq.login.Certificate)
if err != nil {
lq.Cancel()
return nil, err
return nil, wrapLineLoginError(err)
}
if verified {
return lq.finish(ctx)
}
pin, err := lq.client.CreatePinCode(ctx, lq.session)
if err != nil {
lq.Cancel()
return nil, err
return nil, wrapLineLoginError(err)
}
lq.pin = true
lq.startPoll(lq.client.CheckPinCodeVerifiedContext, false)
Expand All @@ -148,7 +149,7 @@ func (lq *LineQRLogin) finish(ctx context.Context) (*bridgev2.LoginStep, error)
defer lq.Cancel()
res, err := lq.client.QRCodeLoginV2(ctx, lq.session)
if err != nil {
return nil, err
return nil, wrapLineLoginError(err)
}
return lq.login.finishLogin(ctx, res)
}
Expand Down
40 changes: 34 additions & 6 deletions pkg/connector/loginerrors.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package connector

import (
"context"
"errors"
"fmt"
"net/http"

"maunium.net/go/mautrix"
"maunium.net/go/mautrix/bridgev2"

"github.com/beeper/line/pkg/line"
Expand All @@ -29,18 +31,14 @@ var (
ErrLoginTooManyAttempts = bridgev2.RespError{
ErrCode: "DEV.HIGHEST.LINE.TOO_MANY_ATTEMPTS",
Err: loginTooManyAttemptsReason,
StatusCode: http.StatusTooManyRequests,
StatusCode: http.StatusBadRequest,
}
ErrLoginRejected = bridgev2.RespError{
ErrCode: "DEV.HIGHEST.LINE.LOGIN_REJECTED",
Err: genericLoginFailureReason,
StatusCode: http.StatusUnauthorized,
}
ErrLoginUnknown = bridgev2.RespError{
ErrCode: "M_UNKNOWN",
Err: "Internal error logging in to LINE",
StatusCode: http.StatusInternalServerError,
}
ErrLoginUnknown = bridgev2.RespError(mautrix.MUnknown.WithMessage("Internal error logging in to LINE"))
)

// wrapLineLoginError translates a LINE error into one the client can act on, keeping the
Expand Down Expand Up @@ -70,3 +68,33 @@ func wrapLineLoginError(err error) error {
}
return fmt.Errorf("%w: %w", mapped, err)
}

// wrapLineRespError translates an error from a LINE call made for a provisioning
// request into an HTTP error, keeping the original error in the chain.
func wrapLineRespError(err error) error {
if err == nil {
return nil
}
var respErr bridgev2.RespError
if errors.As(err, &respErr) {
return err
}
var mapped bridgev2.RespError
switch statusCode := line.HTTPStatusCode(err); {
case errors.Is(err, errLineSessionInvalidated), errors.Is(err, errLineClientSuperseded), line.IsAuthError(err):
mapped = bridgev2.RespError(mautrix.MForbidden.WithMessage("Not logged in to LINE. Reconnect and try again."))
case line.IsNotAMemberError(err):
mapped = bridgev2.RespError(mautrix.MForbidden.WithMessage("You are not a member of this LINE chat"))
case line.IsTalkExceptionNotFound(err):
mapped = bridgev2.RespError(mautrix.MNotFound.WithMessage("Not found on LINE"))
case statusCode == http.StatusTooManyRequests:
mapped = bridgev2.RespError(mautrix.MLimitExceeded.WithMessage("LINE is rate limiting requests. Please wait a moment and try again.").WithStatus(http.StatusBadRequest))
case errors.Is(err, context.DeadlineExceeded):
mapped = bridgev2.RespError(mautrix.MUnknown.WithMessage("Timed out waiting for LINE"))
case line.IsTalkException(err), statusCode >= 400 && statusCode < 500:
mapped = bridgev2.RespError(mautrix.MForbidden.WithMessage("LINE rejected the request"))
default:
mapped = bridgev2.RespError(mautrix.MUnknown.WithMessage("Request to LINE failed"))
}
return fmt.Errorf("%w: %w", mapped, err)
}
18 changes: 2 additions & 16 deletions pkg/connector/reaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -607,14 +607,6 @@ func invalidReactionTargetError(messageID string) error {
WithErrorReason(event.MessageStatusUnsupported)
}

func reactionNotAMemberError() error {
return bridgev2.WrapErrorInStatus(fmt.Errorf("LINE says this account is not a member of the chat")).
WithStatus(event.MessageStatusFail).
WithIsCertain(true).
WithErrorAsMessage().
WithErrorReason(event.MessageStatusNoPermission)
}

func parseReactionTargetMessageID(messageID networkid.MessageID) (string, error) {
raw := string(messageID)
if raw == "" || strings.HasPrefix(raw, "local-") || strings.HasPrefix(raw, "$") {
Expand Down Expand Up @@ -737,10 +729,7 @@ func (lc *LineClient) HandleMatrixReaction(ctx context.Context, msg *bridgev2.Ma
if line.IsInvalidPaidReactionType(err) {
return nil, unsupportedMatrixReactionError(key)
}
if line.IsNotAMemberError(err) {
return nil, reactionNotAMemberError()
}
return nil, err
return nil, wrapSendError(err)
}

return &database.Reaction{
Expand All @@ -762,8 +751,5 @@ func (lc *LineClient) HandleMatrixReactionRemove(ctx context.Context, msg *bridg
_, err = lc.callLine(ctx, func(client *line.Client) error {
return client.CancelReaction(int64(reqSeq), targetID)
})
if line.IsNotAMemberError(err) {
return reactionNotAMemberError()
}
return err
return wrapSendError(err)
}
6 changes: 3 additions & 3 deletions pkg/connector/reaction_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -632,7 +632,7 @@ func TestUnsupportedMatrixReactionError(t *testing.T) {
}

func TestReactionNotAMemberError(t *testing.T) {
err := reactionNotAMemberError()
err := wrapSendError(errors.New(`API error 400: {"code":10051,"message":"RESPONSE_ERROR","data":{"name":"TalkException","code":10,"reason":"not a member"}}`))
var status bridgev2.MessageStatus
if !errors.As(err, &status) {
t.Fatalf("error %T does not wrap bridgev2.MessageStatus", err)
Expand All @@ -643,8 +643,8 @@ func TestReactionNotAMemberError(t *testing.T) {
if status.ErrorReason != event.MessageStatusNoPermission {
t.Fatalf("ErrorReason = %q, want %q", status.ErrorReason, event.MessageStatusNoPermission)
}
if !status.IsCertain || !status.ErrorAsMessage {
t.Fatalf("status certainty/message flags = %v/%v, want true/true", status.IsCertain, status.ErrorAsMessage)
if !status.IsCertain || status.Message == "" {
t.Fatalf("status certainty/message = %v/%q, want true and a message", status.IsCertain, status.Message)
}
}

Expand Down
Loading
Loading