login: refactor to share more code

This commit is contained in:
Tulir Asokan
2025-12-06 21:08:32 +02:00
parent abb4671a16
commit 48fed1c026
5 changed files with 285 additions and 267 deletions
+12
View File
@@ -551,6 +551,8 @@ func (t *TelegramClient) Connect(_ context.Context) {
return return
} }
t.userLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateConnecting})
log.Info().Msg("Connecting client") log.Info().Msg("Connecting client")
// Add a cancellation layer we can use for explicit Disconnect // Add a cancellation layer we can use for explicit Disconnect
@@ -704,3 +706,13 @@ func (t *TelegramClient) senderForUserID(userID int64) bridgev2.EventSender {
Sender: ids.MakeUserID(userID), Sender: ids.MakeUserID(userID),
} }
} }
func (t *TelegramClient) FillBridgeState(state status.BridgeState) status.BridgeState {
if state.Info == nil {
state.Info = make(map[string]any)
}
meta := t.userLogin.Metadata.(*UserLoginMetadata)
state.Info["is_bot"] = meta.IsBot
state.Info["login_method"] = meta.LoginMethod
return state
}
+129 -33
View File
@@ -18,21 +18,30 @@ package connector
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"net/http" "net/http"
"sync" "sync"
"time" "time"
"github.com/rs/zerolog"
"go.mau.fi/util/exsync"
"go.mau.fi/zerozap"
"go.uber.org/zap"
"maunium.net/go/mautrix/bridgev2" "maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/database" "maunium.net/go/mautrix/bridgev2/database"
"go.mau.fi/mautrix-telegram/pkg/connector/ids" "go.mau.fi/mautrix-telegram/pkg/connector/ids"
"go.mau.fi/mautrix-telegram/pkg/gotd/telegram"
"go.mau.fi/mautrix-telegram/pkg/gotd/telegram/auth"
"go.mau.fi/mautrix-telegram/pkg/gotd/telegram/updates"
"go.mau.fi/mautrix-telegram/pkg/gotd/tg" "go.mau.fi/mautrix-telegram/pkg/gotd/tg"
) )
const ( const (
LoginFlowIDPhone = "phone" LoginFlowIDPhone = "phone"
LoginFlowIDQR = "qr" LoginFlowIDQR = "qr"
LoginFlowIDBotToken = "bot_token"
LoginStepIDComplete = "fi.mau.telegram.login.complete" LoginStepIDComplete = "fi.mau.telegram.login.complete"
) )
@@ -71,73 +80,160 @@ func (tg *TelegramConnector) GetLoginFlows() []bridgev2.LoginFlow {
} }
func (tg *TelegramConnector) CreateLogin(ctx context.Context, user *bridgev2.User, flowID string) (bridgev2.LoginProcess, error) { func (tg *TelegramConnector) CreateLogin(ctx context.Context, user *bridgev2.User, flowID string) (bridgev2.LoginProcess, error) {
bl := &baseLogin{
user: user,
main: tg,
flowID: flowID,
}
switch flowID { switch flowID {
case LoginFlowIDPhone: case LoginFlowIDPhone:
return &PhoneLogin{user: user, main: tg}, nil return &PhoneLogin{baseLogin: bl}, nil
case LoginFlowIDQR: case LoginFlowIDQR:
return &QRLogin{user: user, main: tg}, nil return &QRLogin{baseLogin: bl}, nil
default: default:
return nil, fmt.Errorf("unknown flow ID %s", flowID) return nil, fmt.Errorf("unknown flow ID %s", flowID)
} }
} }
func finalizeLogin(ctx context.Context, user *bridgev2.User, authorization *tg.AuthAuthorization, metadata UserLoginMetadata) (*bridgev2.LoginStep, error) { type baseLogin struct {
user *bridgev2.User
main *TelegramConnector
session UserLoginSession
client *telegram.Client
ctx context.Context
cancel context.CancelFunc
flowID string
}
func (bl *baseLogin) Cancel() {
if bl.cancel != nil {
bl.cancel()
}
}
func (bl *baseLogin) makeClient(ctx context.Context, dispatcher *tg.UpdateDispatcher) error {
log := zerolog.Ctx(ctx)
zaplog := zap.New(zerozap.NewWithLevels(*log, zapLevelMap))
var updateManager *updates.Manager
if dispatcher != nil {
updateManager = updates.New(updates.Config{
Handler: dispatcher,
Logger: zaplog.Named("login_update_manager"),
})
}
bl.client = telegram.NewClient(bl.main.Config.APIID, bl.main.Config.APIHash, telegram.Options{
CustomSessionStorage: &bl.session,
Logger: zaplog,
Device: bl.main.deviceConfig(),
UpdateHandler: updateManager,
})
bl.ctx, bl.cancel = context.WithTimeoutCause(log.WithContext(bl.main.Bridge.BackgroundCtx), LoginTimeout, ErrLoginTimeout)
initialized := exsync.NewEvent()
done := NewFuture[error]()
runTelegramClient(bl.ctx, bl.client, initialized, done, waitContextDone)
log.Debug().Msg("Waiting for client to connect")
err := initialized.Wait(ctx)
if err != nil {
bl.Cancel()
return err
}
return nil
}
var passwordLoginStep = &bridgev2.LoginStep{
Type: bridgev2.LoginStepTypeUserInput,
StepID: LoginStepIDPassword,
UserInputParams: &bridgev2.LoginUserInputParams{
Fields: []bridgev2.LoginInputDataField{{
Type: bridgev2.LoginInputFieldTypePassword,
ID: LoginStepIDPassword,
Name: "Password",
}},
},
}
func (bl *baseLogin) submitPassword(ctx context.Context, password, loginPhone string) (*bridgev2.LoginStep, error) {
if bl.client == nil {
return nil, fmt.Errorf("unexpected state: client is nil when submitting password")
} else if password == "" {
return nil, fmt.Errorf("password not provided")
}
authorization, err := bl.client.Auth().Password(ctx, password)
if err != nil {
if errors.Is(err, auth.ErrPasswordInvalid) {
// TODO re-prompt password instead of cancelling
bl.Cancel()
return nil, ErrInvalidPassword
}
bl.Cancel()
return nil, fmt.Errorf("failed to submit password: %w", err)
}
return bl.finalizeLogin(ctx, authorization, &UserLoginMetadata{LoginPhone: loginPhone})
}
func (bl *baseLogin) finalizeLogin(
ctx context.Context,
authorization *tg.AuthAuthorization,
metadata *UserLoginMetadata,
) (*bridgev2.LoginStep, error) {
self, err := bl.client.Self(ctx)
bl.Cancel()
if err != nil {
return nil, fmt.Errorf("failed to get self: %w", err)
}
if metadata == nil {
metadata = &UserLoginMetadata{}
}
metadata.Session = bl.session
metadata.LoginMethod = bl.flowID
profile, name := userToRemoteProfile(self, nil, nil)
userLoginID := ids.MakeUserLoginID(authorization.User.GetID()) userLoginID := ids.MakeUserLoginID(authorization.User.GetID())
ul, err := user.NewLogin(ctx, &database.UserLogin{ ul, err := bl.user.NewLogin(ctx, &database.UserLogin{
ID: userLoginID, ID: userLoginID,
Metadata: &metadata, Metadata: metadata,
RemoteProfile: profile,
RemoteName: name,
}, &bridgev2.NewLoginParams{ }, &bridgev2.NewLoginParams{
DeleteOnConflict: true, DeleteOnConflict: true,
}) })
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to save new login: %w", err) return nil, fmt.Errorf("failed to save new login: %w", err)
} }
ul.Client.Connect(ul.Log.WithContext(ctx)) ul.Client.Connect(ul.Log.WithContext(bl.main.Bridge.BackgroundCtx))
client := ul.Client.(*TelegramClient) client := ul.Client.(*TelegramClient)
// Connecting is non-blocking so wait for gotd to initialize before doing anythign to avoid deadlocking
err = client.clientInitialized.Wait(ctx)
if err != nil {
return nil, err
}
me, err := client.client.Self(ctx)
if err != nil {
return nil, err
}
go func() { go func() {
log := ul.Log.With().Str("component", "login_sync_chats").Logger() log := ul.Log.With().Str("action", "post-login sync").Logger()
if err := client.SyncChats(log.WithContext(client.clientCtx)); err != nil { err := client.clientInitialized.Wait(ctx)
if err != nil {
log.Err(err).Msg("Failed to wait for client init to sync chats after login")
} else if err = client.SyncChats(log.WithContext(client.clientCtx)); err != nil {
log.Err(err).Msg("Failed to sync chats") log.Err(err).Msg("Failed to sync chats")
} }
}() }()
go func() { go func() {
log := ul.Log.With().Str("component", "login_takeout").Logger() log := ul.Log.With().Str("component", "post-login takeout").Logger()
client.takeoutLock.Lock() client.takeoutLock.Lock()
defer client.takeoutLock.Unlock() defer client.takeoutLock.Unlock()
_, err = client.getTakeoutID(ctx) err := client.clientInitialized.Wait(ctx)
if err != nil { if err != nil {
log.Err(err).Msg("Failed to wait for client init to start takeout")
} else if _, err = client.getTakeoutID(ctx); err != nil {
log.Err(err).Msg("Failed to get takeout") log.Err(err).Msg("Failed to get takeout")
return } else if client.stopTakeoutTimer == nil {
}
if client.stopTakeoutTimer == nil {
client.stopTakeoutTimer = time.AfterFunc(max(time.Hour, time.Duration(client.main.Bridge.Config.Backfill.Queue.BatchDelay*2)), sync.OnceFunc(func() { client.stopTakeout(ctx) })) client.stopTakeoutTimer = time.AfterFunc(max(time.Hour, time.Duration(client.main.Bridge.Config.Backfill.Queue.BatchDelay*2)), sync.OnceFunc(func() { client.stopTakeout(ctx) }))
} else { } else {
client.stopTakeoutTimer.Reset(max(time.Hour, time.Duration(client.main.Bridge.Config.Backfill.Queue.BatchDelay*2))) client.stopTakeoutTimer.Reset(max(time.Hour, time.Duration(client.main.Bridge.Config.Backfill.Queue.BatchDelay*2)))
} }
}() }()
ul.RemoteProfile, ul.RemoteName = userToRemoteProfile(me, nil, nil)
err = ul.Save(ctx)
if err != nil {
return nil, fmt.Errorf("failed to save login: %w", err)
}
return &bridgev2.LoginStep{ return &bridgev2.LoginStep{
Type: bridgev2.LoginStepTypeComplete, Type: bridgev2.LoginStepTypeComplete,
StepID: LoginStepIDComplete, StepID: LoginStepIDComplete,
Instructions: fmt.Sprintf("Successfully logged in as %s (`%d`)", ul.RemoteName, me.ID), Instructions: fmt.Sprintf("Successfully logged in as %s (`%d`)", ul.RemoteName, self.ID),
CompleteParams: &bridgev2.LoginCompleteParams{ CompleteParams: &bridgev2.LoginCompleteParams{
UserLoginID: ul.ID, UserLoginID: ul.ID,
UserLogin: ul, UserLogin: ul,
+95 -129
View File
@@ -17,18 +17,14 @@
package connector package connector
import ( import (
"cmp"
"context" "context"
"errors" "errors"
"fmt" "fmt"
"time"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"go.mau.fi/util/exsync"
"go.mau.fi/zerozap"
"go.uber.org/zap"
"maunium.net/go/mautrix/bridgev2" "maunium.net/go/mautrix/bridgev2"
"go.mau.fi/mautrix-telegram/pkg/gotd/telegram"
"go.mau.fi/mautrix-telegram/pkg/gotd/telegram/auth" "go.mau.fi/mautrix-telegram/pkg/gotd/telegram/auth"
"go.mau.fi/mautrix-telegram/pkg/gotd/tg" "go.mau.fi/mautrix-telegram/pkg/gotd/tg"
) )
@@ -40,145 +36,115 @@ const (
) )
type PhoneLogin struct { type PhoneLogin struct {
user *bridgev2.User *baseLogin
main *TelegramConnector phone string
authData UserLoginSession hash string
authClient *telegram.Client codeSubmitted bool
authClientCtx context.Context
authClientCancel context.CancelFunc
phone string
hash string
} }
var _ bridgev2.LoginProcessUserInput = (*PhoneLogin)(nil) var (
_ bridgev2.LoginProcessUserInput = (*PhoneLogin)(nil)
_ bridgev2.LoginProcessWithOverride = (*PhoneLogin)(nil)
)
func (p *PhoneLogin) Cancel() { func (pl *PhoneLogin) StartWithOverride(ctx context.Context, override *bridgev2.UserLogin) (*bridgev2.LoginStep, error) {
if p.authClientCancel != nil { meta := override.Metadata.(*UserLoginMetadata)
p.authClientCancel() if meta.IsBot {
<-p.authClientCtx.Done() return nil, fmt.Errorf("can't re-login to a bot account with phone login")
} }
phone := cmp.Or(meta.LoginPhone, override.RemoteProfile.Phone)
if phone != "" {
zerolog.Ctx(ctx).Debug().Str("phone_number", phone).Msg("Using existing phone number for relogin")
return pl.submitNumber(ctx, phone)
}
zerolog.Ctx(ctx).Debug().Msg("No existing phone number for relogin, re-prompting")
return pl.Start(ctx)
} }
func (p *PhoneLogin) Start(ctx context.Context) (*bridgev2.LoginStep, error) { func (pl *PhoneLogin) Start(ctx context.Context) (*bridgev2.LoginStep, error) {
return &bridgev2.LoginStep{ return &bridgev2.LoginStep{
Type: bridgev2.LoginStepTypeUserInput, Type: bridgev2.LoginStepTypeUserInput,
StepID: LoginStepIDPhoneNumber, StepID: LoginStepIDPhoneNumber,
Instructions: "Please enter your phone number",
UserInputParams: &bridgev2.LoginUserInputParams{ UserInputParams: &bridgev2.LoginUserInputParams{
Fields: []bridgev2.LoginInputDataField{ Fields: []bridgev2.LoginInputDataField{{
{ Type: bridgev2.LoginInputFieldTypePhoneNumber,
Type: bridgev2.LoginInputFieldTypePhoneNumber, ID: LoginStepIDPhoneNumber,
ID: LoginStepIDPhoneNumber, Name: "Phone number",
Name: "Phone Number", Description: "Include the country code with +",
Description: "Include the country code with +", }},
},
},
}, },
}, nil }, nil
} }
func (p *PhoneLogin) SubmitUserInput(ctx context.Context, input map[string]string) (*bridgev2.LoginStep, error) { func (pl *PhoneLogin) SubmitUserInput(ctx context.Context, input map[string]string) (*bridgev2.LoginStep, error) {
log := zerolog.Ctx(ctx).With().Str("component", "telegram_phone_login").Logger() if pl.client == nil {
if phone, ok := input[LoginStepIDPhoneNumber]; ok { return pl.submitNumber(ctx, input[LoginStepIDPhoneNumber])
p.phone = phone } else if pl.codeSubmitted {
p.authClient = telegram.NewClient(p.main.Config.APIID, p.main.Config.APIHash, telegram.Options{ return pl.submitPassword(ctx, input[LoginStepIDPassword], pl.phone)
CustomSessionStorage: &p.authData, } else {
Logger: zap.New(zerozap.NewWithLevels(zerolog.Ctx(ctx).With().Str("component", "telegram_phone_login_client").Logger(), zapLevelMap)), return pl.submitCode(ctx, input[LoginStepIDCode])
Device: p.main.deviceConfig(), }
}) }
p.authClientCtx, p.authClientCancel = context.WithTimeoutCause(log.WithContext(context.Background()), time.Hour, errors.New("phone login took over one hour")) func (pl *PhoneLogin) submitNumber(ctx context.Context, phone string) (*bridgev2.LoginStep, error) {
initialized := exsync.NewEvent() if phone == "" {
done := NewFuture[error]() return nil, fmt.Errorf("phone number is empty")
runTelegramClient(p.authClientCtx, p.authClient, initialized, done, func(ctx context.Context) error { }
<-ctx.Done() log := zerolog.Ctx(ctx).With().Str("component", "phone login").Logger()
return ctx.Err() ctx = log.WithContext(ctx)
}) pl.phone = phone
err := pl.makeClient(ctx, nil)
log.Info().Msg("Waiting for client to connect.") if err != nil {
err := initialized.Wait(ctx) return nil, err
if err != nil {
return nil, err
}
sentCode, err := p.authClient.Auth().SendCode(p.authClientCtx, p.phone, auth.SendCodeOptions{})
if err != nil {
return nil, err
}
switch s := sentCode.(type) {
case *tg.AuthSentCode:
p.hash = s.PhoneCodeHash
return &bridgev2.LoginStep{
Type: bridgev2.LoginStepTypeUserInput,
StepID: LoginStepIDCode,
Instructions: "Please enter the code sent to the Telegram app on your phone",
UserInputParams: &bridgev2.LoginUserInputParams{
Fields: []bridgev2.LoginInputDataField{
{
Type: bridgev2.LoginInputFieldType2FACode,
ID: LoginStepIDCode,
Name: "Code",
},
},
},
}, nil
case *tg.AuthSentCodeSuccess:
switch a := s.Authorization.(type) {
case *tg.AuthAuthorization:
// Looks that we are already authorized.
return p.handleAuthSuccess(ctx, a)
case *tg.AuthAuthorizationSignUpRequired:
return nil, fmt.Errorf("phone number does not correspond with an existing Telegram account and sign-up is not supported")
default:
return nil, fmt.Errorf("unexpected authorization type: %T", sentCode)
}
default:
return nil, fmt.Errorf("unexpected sent code type: %T", sentCode)
}
} else if code, ok := input[LoginStepIDCode]; ok {
authorization, err := p.authClient.Auth().SignIn(p.authClientCtx, p.phone, code, p.hash)
if errors.Is(err, auth.ErrPasswordAuthNeeded) {
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 errors.Is(err, auth.ErrPhoneCodeInvalid) {
return nil, ErrPhoneCodeInvalid
} else if errors.Is(err, &auth.SignUpRequired{}) {
return nil, ErrSignUpNotSupported
} else if err != nil {
return nil, fmt.Errorf("failed to submit code: %w", err)
}
return p.handleAuthSuccess(ctx, authorization)
} else if password, ok := input[LoginStepIDPassword]; ok {
authorization, err := p.authClient.Auth().Password(p.authClientCtx, password)
if err != nil {
if errors.Is(err, auth.ErrPasswordInvalid) {
return nil, ErrInvalidPassword
}
return nil, fmt.Errorf("failed to submit password: %w", err)
}
return p.handleAuthSuccess(ctx, authorization)
} }
return nil, fmt.Errorf("unexpected state during phone login") sentCode, err := pl.client.Auth().SendCode(ctx, pl.phone, auth.SendCodeOptions{})
if err != nil {
return nil, err
}
switch s := sentCode.(type) {
case *tg.AuthSentCode:
pl.hash = s.PhoneCodeHash
return &bridgev2.LoginStep{
Type: bridgev2.LoginStepTypeUserInput,
StepID: LoginStepIDCode,
UserInputParams: &bridgev2.LoginUserInputParams{
Fields: []bridgev2.LoginInputDataField{{
Type: bridgev2.LoginInputFieldType2FACode,
ID: LoginStepIDCode,
Name: "Code",
Description: "The code was sent to the Telegram app on your phone",
}},
},
}, nil
case *tg.AuthSentCodeSuccess:
switch authorization := s.Authorization.(type) {
case *tg.AuthAuthorization:
return pl.finalizeLogin(ctx, authorization, &UserLoginMetadata{LoginPhone: pl.phone})
case *tg.AuthAuthorizationSignUpRequired:
return nil, ErrSignUpNotSupported
default:
return nil, fmt.Errorf("unexpected authorization type: %T", sentCode)
}
default:
return nil, fmt.Errorf("unexpected sent code type: %T", sentCode)
}
} }
func (p *PhoneLogin) handleAuthSuccess(ctx context.Context, authorization *tg.AuthAuthorization) (*bridgev2.LoginStep, error) { func (pl *PhoneLogin) submitCode(ctx context.Context, code string) (*bridgev2.LoginStep, error) {
defer p.authClientCancel() if pl.client == nil {
return finalizeLogin(ctx, p.user, authorization, UserLoginMetadata{ return nil, fmt.Errorf("unexpected state: client is nil when submitting phone code")
Phone: p.phone, }
Session: p.authData, authorization, err := pl.client.Auth().SignIn(ctx, pl.phone, code, pl.hash)
}) if errors.Is(err, auth.ErrPasswordAuthNeeded) {
pl.codeSubmitted = true
return passwordLoginStep, nil
} else if errors.Is(err, auth.ErrPhoneCodeInvalid) {
return nil, ErrPhoneCodeInvalid
} else if errors.Is(err, &auth.SignUpRequired{}) {
return nil, ErrSignUpNotSupported
} else if err != nil {
return nil, fmt.Errorf("failed to submit code: %w", err)
}
return pl.finalizeLogin(ctx, authorization, &UserLoginMetadata{LoginPhone: pl.phone})
} }
+44 -102
View File
@@ -23,15 +23,9 @@ import (
"time" "time"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"go.mau.fi/util/exsync"
"go.mau.fi/zerozap"
"go.uber.org/zap"
"maunium.net/go/mautrix/bridgev2" "maunium.net/go/mautrix/bridgev2"
"go.mau.fi/mautrix-telegram/pkg/gotd/telegram"
"go.mau.fi/mautrix-telegram/pkg/gotd/telegram/auth"
"go.mau.fi/mautrix-telegram/pkg/gotd/telegram/auth/qrlogin" "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/tg"
"go.mau.fi/mautrix-telegram/pkg/gotd/tgerr" "go.mau.fi/mautrix-telegram/pkg/gotd/tgerr"
) )
@@ -43,14 +37,7 @@ type qrAuthResult struct {
} }
type QRLogin struct { type QRLogin struct {
user *bridgev2.User *baseLogin
main *TelegramConnector
authData UserLoginSession
authClient *telegram.Client
authClientCtx context.Context
authClientCancel context.CancelFunc
auth chan qrAuthResult auth chan qrAuthResult
qrToken chan qrlogin.Token qrToken chan qrlogin.Token
} }
@@ -60,66 +47,55 @@ const LoginStepIDShowQR = "fi.mau.telegram.login.show_qr"
var _ bridgev2.LoginProcessDisplayAndWait = (*QRLogin)(nil) // For showing QR code var _ bridgev2.LoginProcessDisplayAndWait = (*QRLogin)(nil) // For showing QR code
var _ bridgev2.LoginProcessUserInput = (*QRLogin)(nil) // For asking for password var _ bridgev2.LoginProcessUserInput = (*QRLogin)(nil) // For asking for password
func (q *QRLogin) Cancel() { func waitContextDone(ctx context.Context) error {
if q.authClientCancel != nil { <-ctx.Done()
q.authClientCancel() return ctx.Err()
<-q.authClientCtx.Done()
}
} }
func (q *QRLogin) Start(ctx context.Context) (*bridgev2.LoginStep, error) { const LoginTimeout = 10 * time.Minute
log := zerolog.Ctx(ctx).With().Str("component", "telegram_qr_login").Logger()
loggedIn := make(chan struct{})
var ErrLoginTimeout = errors.New("login process timed out")
func (ql *QRLogin) StartWithOverride(ctx context.Context, override *bridgev2.UserLogin) (*bridgev2.LoginStep, error) {
meta := override.Metadata.(*UserLoginMetadata)
if meta.IsBot {
return nil, fmt.Errorf("can't re-login to a bot account with QR login")
}
return ql.Start(ctx)
}
func (ql *QRLogin) Start(ctx context.Context) (*bridgev2.LoginStep, error) {
log := zerolog.Ctx(ctx).With().Str("component", "qr login").Logger()
ctx = log.WithContext(ctx)
loggedIn := make(chan struct{})
dispatcher := tg.NewUpdateDispatcher() dispatcher := tg.NewUpdateDispatcher()
dispatcher.OnLoginToken(func(ctx context.Context, e tg.Entities, update *tg.UpdateLoginToken) error { dispatcher.OnLoginToken(func(ctx context.Context, e tg.Entities, update *tg.UpdateLoginToken) error {
loggedIn <- struct{}{} loggedIn <- struct{}{}
return nil return nil
}) })
zaplog := zap.New(zerozap.NewWithLevels(log, zapLevelMap)) err := ql.makeClient(ctx, &dispatcher)
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,
Device: q.main.deviceConfig(),
})
q.authClientCtx, q.authClientCancel = context.WithTimeoutCause(log.WithContext(context.Background()), time.Hour, errors.New("phone login took over one hour"))
initialized := exsync.NewEvent()
done := NewFuture[error]()
runTelegramClient(q.authClientCtx, q.authClient, initialized, done, func(ctx context.Context) error {
<-ctx.Done()
return ctx.Err()
})
log.Info().Msg("Waiting for client to connect.")
err := initialized.Wait(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
qr := qrlogin.NewQR(q.authClient.API(), q.main.Config.APIID, q.main.Config.APIHash, qrlogin.Options{ qr := qrlogin.NewQR(ql.client.API(), ql.main.Config.APIID, ql.main.Config.APIHash, qrlogin.Options{
Migrate: q.authClient.MigrateTo, Migrate: ql.client.MigrateTo,
}) })
q.qrToken = make(chan qrlogin.Token) ql.qrToken = make(chan qrlogin.Token)
q.auth = make(chan qrAuthResult) ql.auth = make(chan qrAuthResult)
go func() { go func() {
auth, err := qr.Auth(q.authClientCtx, loggedIn, func(ctx context.Context, token qrlogin.Token) error { auth, err := qr.Auth(ctx, loggedIn, func(ctx context.Context, token qrlogin.Token) error {
q.qrToken <- token ql.qrToken <- token
return nil return nil
}) })
q.auth <- qrAuthResult{false, auth, err} ql.auth <- qrAuthResult{false, auth, err}
}() }()
// Wait for the first QR token and show it to the user.: // Wait for the first QR token and show it to the user.:
select { select {
case token := <-q.qrToken: case token := <-ql.qrToken:
return &bridgev2.LoginStep{ return &bridgev2.LoginStep{
Type: bridgev2.LoginStepTypeDisplayAndWait, Type: bridgev2.LoginStepTypeDisplayAndWait,
StepID: LoginStepIDShowQR, StepID: LoginStepIDShowQR,
@@ -130,20 +106,20 @@ func (q *QRLogin) Start(ctx context.Context) (*bridgev2.LoginStep, error) {
}, },
}, nil }, nil
case <-ctx.Done(): case <-ctx.Done():
q.Cancel() ql.Cancel()
return nil, ctx.Err() return nil, ctx.Err()
case <-q.authClientCtx.Done(): case <-ql.ctx.Done():
return nil, q.authClientCtx.Err() return nil, ql.ctx.Err()
} }
} }
func (q *QRLogin) Wait(ctx context.Context) (*bridgev2.LoginStep, error) { func (ql *QRLogin) Wait(ctx context.Context) (*bridgev2.LoginStep, error) {
if q.qrToken == nil { if ql.qrToken == nil {
panic("qr token channel is nil") panic("qr token channel is nil")
} }
select { select {
case token := <-q.qrToken: case token := <-ql.qrToken:
// There's a new token, show it to the user. // There's a new token, show it to the user.
return &bridgev2.LoginStep{ return &bridgev2.LoginStep{
Type: bridgev2.LoginStepTypeDisplayAndWait, Type: bridgev2.LoginStepTypeDisplayAndWait,
@@ -154,57 +130,23 @@ func (q *QRLogin) Wait(ctx context.Context) (*bridgev2.LoginStep, error) {
Data: token.URL(), Data: token.URL(),
}, },
}, nil }, nil
case authResult := <-q.auth: case authResult := <-ql.auth:
if tgerr.Is(authResult.Error, "SESSION_PASSWORD_NEEDED") { if tgerr.Is(authResult.Error, "SESSION_PASSWORD_NEEDED") {
return &bridgev2.LoginStep{ return passwordLoginStep, nil
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 { } else if authResult.Error != nil {
ql.Cancel()
return nil, fmt.Errorf("failed to authenticate: %w", authResult.Error) return nil, fmt.Errorf("failed to authenticate: %w", authResult.Error)
} }
// Stop the login client return ql.finalizeLogin(ctx, authResult.Authorization, nil)
q.authClientCancel()
return finalizeLogin(ctx, q.user, authResult.Authorization, UserLoginMetadata{
Session: q.authData,
})
case <-ctx.Done(): case <-ctx.Done():
q.Cancel() ql.Cancel()
return nil, ctx.Err() return nil, ctx.Err()
case <-q.authClientCtx.Done(): case <-ql.ctx.Done():
return nil, q.authClientCtx.Err() return nil, ql.ctx.Err()
} }
} }
func (q *QRLogin) SubmitUserInput(ctx context.Context, input map[string]string) (*bridgev2.LoginStep, error) { func (ql *QRLogin) SubmitUserInput(ctx context.Context, input map[string]string) (*bridgev2.LoginStep, error) {
password, ok := input[LoginStepIDPassword] return ql.submitPassword(ctx, 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 {
if errors.Is(err, auth.ErrPasswordInvalid) {
return nil, ErrInvalidPassword
}
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,
})
} }
+5 -3
View File
@@ -74,9 +74,11 @@ type MessageMetadata struct {
} }
type UserLoginMetadata struct { type UserLoginMetadata struct {
Phone string `json:"phone"` LoginPhone string `json:"phone,omitempty"`
Session UserLoginSession `json:"session"` LoginMethod string `json:"login_method,omitempty"`
TakeoutID int64 `json:"takeout_id,omitempty"` IsBot bool `json:"is_bot,omitempty"`
Session UserLoginSession `json:"session"`
TakeoutID int64 `json:"takeout_id,omitempty"`
TakeoutDialogCrawlDone bool `json:"takeout_portal_crawl_done,omitempty"` TakeoutDialogCrawlDone bool `json:"takeout_portal_crawl_done,omitempty"`
TakeoutDialogCrawlCursor networkid.PortalID `json:"takeout_portal_crawl_cursor,omitempty"` TakeoutDialogCrawlCursor networkid.PortalID `json:"takeout_portal_crawl_cursor,omitempty"`