7a04f298d2
- update to latest telegram layer - remove some references to fields in tg.Entities that don't exist in the schema - originally added here: https://github.com/beeper/td/commit/820929062a2ba0104397bc01235ab58a9cff780e - referenced here - https://github.com/mautrix/telegramgo/commit/124f0967ed195b5a380c9bd02e170ada9710dde3 - https://github.com/mautrix/telegramgo/commit/4205047aab2e0639217148b5d125bfaab668bd8e
199 lines
5.9 KiB
Go
199 lines
5.9 KiB
Go
// mautrix-telegram - A Matrix-Telegram puppeting bridge.
|
|
// Copyright (C) 2025 Sumner Evans
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
package connector
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/rs/zerolog"
|
|
"go.mau.fi/zerozap"
|
|
"go.uber.org/zap"
|
|
"maunium.net/go/mautrix/bridgev2"
|
|
|
|
"go.mau.fi/mautrix-telegram/pkg/gotd/telegram"
|
|
"go.mau.fi/mautrix-telegram/pkg/gotd/telegram/auth/qrlogin"
|
|
"go.mau.fi/mautrix-telegram/pkg/gotd/telegram/updates"
|
|
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
|
|
"go.mau.fi/mautrix-telegram/pkg/gotd/tgerr"
|
|
)
|
|
|
|
type qrAuthResult struct {
|
|
PasswordNeeded bool
|
|
Authorization *tg.AuthAuthorization
|
|
Error error
|
|
}
|
|
|
|
type QRLogin struct {
|
|
user *bridgev2.User
|
|
main *TelegramConnector
|
|
authData UserLoginSession
|
|
authClient *telegram.Client
|
|
|
|
authClientCtx context.Context
|
|
authClientCancel context.CancelFunc
|
|
authClientCloseC <-chan struct{}
|
|
|
|
auth chan qrAuthResult
|
|
qrToken chan qrlogin.Token
|
|
}
|
|
|
|
const LoginStepIDShowQR = "fi.mau.telegram.login.show_qr"
|
|
|
|
var _ bridgev2.LoginProcessDisplayAndWait = (*QRLogin)(nil) // For showing QR code
|
|
var _ bridgev2.LoginProcessUserInput = (*QRLogin)(nil) // For asking for password
|
|
|
|
func (q *QRLogin) Cancel() {
|
|
if q.authClientCancel != nil {
|
|
q.authClientCancel()
|
|
}
|
|
if q.authClientCloseC != nil {
|
|
<-q.authClientCloseC
|
|
}
|
|
}
|
|
|
|
func (q *QRLogin) Start(ctx context.Context) (*bridgev2.LoginStep, error) {
|
|
log := zerolog.Ctx(ctx).With().Str("component", "telegram_qr_login").Logger()
|
|
loggedIn := make(chan struct{})
|
|
|
|
dispatcher := tg.NewUpdateDispatcher()
|
|
dispatcher.OnLoginToken(func(ctx context.Context, e tg.Entities, update *tg.UpdateLoginToken) error {
|
|
loggedIn <- struct{}{}
|
|
return nil
|
|
})
|
|
zaplog := zap.New(zerozap.New(log))
|
|
updateManager := updates.New(updates.Config{
|
|
Handler: dispatcher,
|
|
Logger: zaplog.Named("login_update_manager"),
|
|
})
|
|
q.authClient = telegram.NewClient(q.main.Config.APIID, q.main.Config.APIHash, telegram.Options{
|
|
CustomSessionStorage: &q.authData,
|
|
UpdateHandler: updateManager,
|
|
Logger: zaplog,
|
|
})
|
|
|
|
var err error
|
|
q.authClientCtx, q.authClientCancel = context.WithTimeoutCause(log.WithContext(context.Background()), time.Hour, errors.New("phone login took over one hour"))
|
|
if q.authClientCloseC, err = connectTelegramClient(q.authClientCtx, q.authClientCancel, q.authClient); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
qr := qrlogin.NewQR(q.authClient.API(), q.main.Config.APIID, q.main.Config.APIHash, qrlogin.Options{
|
|
Migrate: q.authClient.MigrateTo,
|
|
})
|
|
q.qrToken = make(chan qrlogin.Token)
|
|
q.auth = make(chan qrAuthResult)
|
|
go func() {
|
|
auth, err := qr.Auth(q.authClientCtx, loggedIn, func(ctx context.Context, token qrlogin.Token) error {
|
|
q.qrToken <- token
|
|
return nil
|
|
})
|
|
|
|
q.auth <- qrAuthResult{false, auth, err}
|
|
}()
|
|
|
|
// Wait for the first QR token and show it to the user.:
|
|
select {
|
|
case token := <-q.qrToken:
|
|
return &bridgev2.LoginStep{
|
|
Type: bridgev2.LoginStepTypeDisplayAndWait,
|
|
StepID: LoginStepIDShowQR,
|
|
Instructions: "Scan the QR code on your phone to log in",
|
|
DisplayAndWaitParams: &bridgev2.LoginDisplayAndWaitParams{
|
|
Type: bridgev2.LoginDisplayTypeQR,
|
|
Data: token.URL(),
|
|
},
|
|
}, nil
|
|
case <-ctx.Done():
|
|
q.Cancel()
|
|
return nil, ctx.Err()
|
|
case <-q.authClientCtx.Done():
|
|
return nil, q.authClientCtx.Err()
|
|
}
|
|
}
|
|
|
|
func (q *QRLogin) Wait(ctx context.Context) (*bridgev2.LoginStep, error) {
|
|
if q.qrToken == nil {
|
|
panic("qr token channel is nil")
|
|
}
|
|
|
|
select {
|
|
case token := <-q.qrToken:
|
|
// There's a new token, show it to the user.
|
|
return &bridgev2.LoginStep{
|
|
Type: bridgev2.LoginStepTypeDisplayAndWait,
|
|
StepID: LoginStepIDShowQR,
|
|
Instructions: "Scan the QR code on your phone to log in",
|
|
DisplayAndWaitParams: &bridgev2.LoginDisplayAndWaitParams{
|
|
Type: bridgev2.LoginDisplayTypeQR,
|
|
Data: token.URL(),
|
|
},
|
|
}, nil
|
|
case authResult := <-q.auth:
|
|
if tgerr.Is(authResult.Error, "SESSION_PASSWORD_NEEDED") {
|
|
return &bridgev2.LoginStep{
|
|
Type: bridgev2.LoginStepTypeUserInput,
|
|
StepID: LoginStepIDPassword,
|
|
Instructions: "Please enter your password",
|
|
UserInputParams: &bridgev2.LoginUserInputParams{
|
|
Fields: []bridgev2.LoginInputDataField{
|
|
{
|
|
Type: bridgev2.LoginInputFieldTypePassword,
|
|
ID: LoginStepIDPassword,
|
|
Name: "Password",
|
|
},
|
|
},
|
|
},
|
|
}, nil
|
|
} else if authResult.Error != nil {
|
|
return nil, fmt.Errorf("failed to authenticate: %w", authResult.Error)
|
|
}
|
|
|
|
// Stop the login client
|
|
q.authClientCancel()
|
|
|
|
return finalizeLogin(ctx, q.user, authResult.Authorization, UserLoginMetadata{
|
|
Session: q.authData,
|
|
})
|
|
case <-ctx.Done():
|
|
q.Cancel()
|
|
return nil, ctx.Err()
|
|
case <-q.authClientCtx.Done():
|
|
return nil, q.authClientCtx.Err()
|
|
}
|
|
}
|
|
|
|
func (q *QRLogin) SubmitUserInput(ctx context.Context, input map[string]string) (*bridgev2.LoginStep, error) {
|
|
password, ok := input[LoginStepIDPassword]
|
|
if !ok {
|
|
return nil, fmt.Errorf("unexpected state during phone login")
|
|
}
|
|
authorization, err := q.authClient.Auth().Password(q.authClientCtx, password)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to submit password: %w", err)
|
|
}
|
|
|
|
// Stop the login client
|
|
q.authClientCancel()
|
|
|
|
return finalizeLogin(ctx, q.user, authorization, UserLoginMetadata{
|
|
Session: q.authData,
|
|
})
|
|
}
|