From b9bd1d4400ca5f077f139b5ed82995cc9f108d48 Mon Sep 17 00:00:00 2001 From: highesttt Date: Fri, 16 Jan 2026 01:29:47 -0600 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20=F0=9F=90=9B=20Update=20login=20flow?= =?UTF-8?q?=20to=20use=20display=20and=20wait=20for=20PIN=20verification?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 13 +++++- pkg/connector/connector.go | 87 +++++++++++++++++++++----------------- 2 files changed, 60 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index fd99b45..220f80c 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ Based on the [mautrix-twilio](https://github.com/mautrix/twilio) bridge mv config.yaml data/ ``` -5. Change `public_address` in `data/config.yaml` to your desired public address +5. Change `public_address` in `data/config.yaml` to your desired public address (default: `https://localhost:4000`). 6. Build and run the bridge using Docker (use -d for detached mode): @@ -72,5 +72,16 @@ Based on the [mautrix-twilio](https://github.com/mautrix/twilio) bridge ## Login +### Using the LINE SelfHosted Bridge Bot + 1. Open the Matrix client of your choice and start a chat with `@sh-linebot:your.matrix.homeserver.domain`. (For local beeper bridges, use `@sh-linebot:beeper.local`) 2. Send the command `login` and follow the instructions to log in to your LINE account. + +or + +### Via Beeper Desktop Settings + +1. Open Beeper Desktop Settings +2. Navigate to `Bridges` +3. Click on the three dots next to your LINE Bridge and select `Experimental: Add an account` +4. Follow the instructions to log in to your LINE account. diff --git a/pkg/connector/connector.go b/pkg/connector/connector.go index a18e573..5291ae2 100644 --- a/pkg/connector/connector.go +++ b/pkg/connector/connector.go @@ -122,10 +122,11 @@ func (lc *LineConnector) CreateLogin(ctx context.Context, user *bridgev2.User, f } type LineEmailLogin struct { - User *bridgev2.User - Email string - Password string - Verifier string + User *bridgev2.User + Email string + Password string + Verifier string + AwaitingPIN bool pollResult chan *line.LoginResult pollErr chan error @@ -134,6 +135,7 @@ type LineEmailLogin struct { } var _ bridgev2.LoginProcessUserInput = (*LineEmailLogin)(nil) +var _ bridgev2.LoginProcessDisplayAndWait = (*LineEmailLogin)(nil) func (ll *LineEmailLogin) Start(ctx context.Context) (*bridgev2.LoginStep, error) { return &bridgev2.LoginStep{ @@ -159,18 +161,7 @@ func (ll *LineEmailLogin) Start(ctx context.Context) (*bridgev2.LoginStep, error func (ll *LineEmailLogin) SubmitUserInput(ctx context.Context, input map[string]string) (*bridgev2.LoginStep, error) { if ll.Verifier != "" { - // User clicked "Continue" on the PIN screen. Wait for the background polling to finish. - select { - case res := <-ll.pollResult: - if res.AuthToken != "" { - return ll.finishLogin(ctx, res) - } - case err := <-ll.pollErr: - return nil, fmt.Errorf("verification failed: %w", err) - case <-ctx.Done(): - return nil, ctx.Err() - } - return nil, fmt.Errorf("unexpected end of polling") + return ll.Wait(ctx) } if input["email"] != "" { @@ -188,13 +179,45 @@ func (ll *LineEmailLogin) SubmitUserInput(ctx context.Context, input map[string] return nil, fmt.Errorf("login failed: %w", err) } + return ll.handleLoginResponse(ctx, res) +} + +func (ll *LineEmailLogin) Wait(ctx context.Context) (*bridgev2.LoginStep, error) { + if ll.Verifier != "" { + select { + case res := <-ll.pollResult: + if res.AuthToken != "" { + return ll.finishLogin(ctx, res) + } + case err := <-ll.pollErr: + return nil, fmt.Errorf("verification failed: %w", err) + case <-ctx.Done(): + return nil, ctx.Err() + } + return nil, fmt.Errorf("unexpected end of polling") + } + + if ll.AwaitingPIN { + client := line.NewClient("") + res, err := client.Login(ll.Email, ll.Password) + if err != nil { + return nil, fmt.Errorf("login failed: %w", err) + } + return ll.handleLoginResponse(ctx, res) + } + + return nil, fmt.Errorf("no pending login continuation") +} + +func (ll *LineEmailLogin) handleLoginResponse(ctx context.Context, res *line.LoginResult) (*bridgev2.LoginStep, error) { if res.AuthToken != "" { return ll.finishLogin(ctx, res) } if (res.Type == 3 || res.Type == 0) && res.Verifier != "" { ll.Verifier = res.Verifier - instructions := "A verification request has been sent to your LINE device." + ll.AwaitingPIN = false + instructions := "" pin := res.Pin if res.PinCode != "" { pin = res.PinCode @@ -202,7 +225,6 @@ func (ll *LineEmailLogin) SubmitUserInput(ctx context.Context, input map[string] if pin != "" { instructions += fmt.Sprintf("\n\n**Please enter this PIN code on your mobile device: %s**", pin) } - instructions += "\n\nAfter entering the code (or approving), click 'Continue' to finish login." // Start polling in background immediately so it's running while the user enters the PIN ll.mu.Lock() @@ -221,36 +243,23 @@ func (ll *LineEmailLogin) SubmitUserInput(ctx context.Context, input map[string] ll.mu.Unlock() return &bridgev2.LoginStep{ - Type: bridgev2.LoginStepTypeUserInput, + Type: bridgev2.LoginStepTypeDisplayAndWait, StepID: "dev.highest.matrix.line.wait_verification", Instructions: instructions, - UserInputParams: &bridgev2.LoginUserInputParams{ - Fields: []bridgev2.LoginInputDataField{ - { - Type: bridgev2.LoginInputFieldTypePassword, // hidden field just to have a submit button - ID: "dummy", - Name: "Action", - Description: "Press Enter/Continue when done", - }, - }, + DisplayAndWaitParams: &bridgev2.LoginDisplayAndWaitParams{ + Type: bridgev2.LoginDisplayTypeNothing, }, }, nil } if res.Certificate != "" { + ll.AwaitingPIN = true return &bridgev2.LoginStep{ - Type: bridgev2.LoginStepTypeUserInput, + Type: bridgev2.LoginStepTypeDisplayAndWait, StepID: "dev.highest.matrix.line.enter_pin", - Instructions: fmt.Sprintf("Please open the LINE app on your mobile device and enter this PIN code: **%s**\n\nAfter entering the code, click 'Continue' below.", res.Certificate), - UserInputParams: &bridgev2.LoginUserInputParams{ - Fields: []bridgev2.LoginInputDataField{ - { - Type: bridgev2.LoginInputFieldTypePassword, // hidden dummy field - ID: "dummy", - Name: "Hidden", - Description: "Press Enter to continue", - }, - }, + Instructions: fmt.Sprintf("Please open the LINE app on your mobile device and enter this PIN code: **%s**\n\nAfter entering the code, click Continue below.", res.Certificate), + DisplayAndWaitParams: &bridgev2.LoginDisplayAndWaitParams{ + Type: bridgev2.LoginDisplayTypeNothing, }, }, nil } From 163b9a7359edaf6d75ad73fbbfb3202e3169e81e Mon Sep 17 00:00:00 2001 From: highesttt Date: Fri, 16 Jan 2026 01:38:07 -0600 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20=F0=9F=90=9B=20Improve=20error=20han?= =?UTF-8?q?dling=20and=20user=20instructions=20for=20PIN=20verification?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/connector/connector.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/connector/connector.go b/pkg/connector/connector.go index 5291ae2..8171b58 100644 --- a/pkg/connector/connector.go +++ b/pkg/connector/connector.go @@ -189,12 +189,12 @@ func (ll *LineEmailLogin) Wait(ctx context.Context) (*bridgev2.LoginStep, error) if res.AuthToken != "" { return ll.finishLogin(ctx, res) } + return nil, fmt.Errorf("verification failed: no auth token received") case err := <-ll.pollErr: return nil, fmt.Errorf("verification failed: %w", err) case <-ctx.Done(): return nil, ctx.Err() } - return nil, fmt.Errorf("unexpected end of polling") } if ll.AwaitingPIN { @@ -217,13 +217,13 @@ func (ll *LineEmailLogin) handleLoginResponse(ctx context.Context, res *line.Log if (res.Type == 3 || res.Type == 0) && res.Verifier != "" { ll.Verifier = res.Verifier ll.AwaitingPIN = false - instructions := "" + instructions := "Please open the LINE app on your mobile device to complete the login." pin := res.Pin if res.PinCode != "" { pin = res.PinCode } if pin != "" { - instructions += fmt.Sprintf("\n\n**Please enter this PIN code on your mobile device: %s**", pin) + instructions = fmt.Sprintf("Open LINE and enter this PIN on your mobile device: %s", pin) } // Start polling in background immediately so it's running while the user enters the PIN