diff --git a/pkg/connector/client.go b/pkg/connector/client.go index d1f9d908..75978b56 100644 --- a/pkg/connector/client.go +++ b/pkg/connector/client.go @@ -551,6 +551,8 @@ func (t *TelegramClient) Connect(_ context.Context) { return } + t.userLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateConnecting}) + log.Info().Msg("Connecting client") // 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), } } + +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 +} diff --git a/pkg/connector/login.go b/pkg/connector/login.go index 32f8e4aa..5ddf1f73 100644 --- a/pkg/connector/login.go +++ b/pkg/connector/login.go @@ -18,21 +18,30 @@ package connector import ( "context" + "errors" "fmt" "net/http" "sync" "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/database" "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" ) const ( - LoginFlowIDPhone = "phone" - LoginFlowIDQR = "qr" + LoginFlowIDPhone = "phone" + LoginFlowIDQR = "qr" + LoginFlowIDBotToken = "bot_token" 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) { + bl := &baseLogin{ + user: user, + main: tg, + flowID: flowID, + } switch flowID { case LoginFlowIDPhone: - return &PhoneLogin{user: user, main: tg}, nil + return &PhoneLogin{baseLogin: bl}, nil case LoginFlowIDQR: - return &QRLogin{user: user, main: tg}, nil + return &QRLogin{baseLogin: bl}, nil default: 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()) - ul, err := user.NewLogin(ctx, &database.UserLogin{ - ID: userLoginID, - Metadata: &metadata, + ul, err := bl.user.NewLogin(ctx, &database.UserLogin{ + ID: userLoginID, + Metadata: metadata, + RemoteProfile: profile, + RemoteName: name, }, &bridgev2.NewLoginParams{ DeleteOnConflict: true, }) if err != nil { 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) - // 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() { - log := ul.Log.With().Str("component", "login_sync_chats").Logger() - if err := client.SyncChats(log.WithContext(client.clientCtx)); err != nil { + log := ul.Log.With().Str("action", "post-login sync").Logger() + 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") } }() go func() { - log := ul.Log.With().Str("component", "login_takeout").Logger() + log := ul.Log.With().Str("component", "post-login takeout").Logger() client.takeoutLock.Lock() defer client.takeoutLock.Unlock() - _, err = client.getTakeoutID(ctx) + err := client.clientInitialized.Wait(ctx) 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") - return - } - if client.stopTakeoutTimer == nil { + } else 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) })) } else { 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{ Type: bridgev2.LoginStepTypeComplete, 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{ UserLoginID: ul.ID, UserLogin: ul, diff --git a/pkg/connector/loginphone.go b/pkg/connector/loginphone.go index bad03b8a..054ac045 100644 --- a/pkg/connector/loginphone.go +++ b/pkg/connector/loginphone.go @@ -17,18 +17,14 @@ package connector import ( + "cmp" "context" "errors" "fmt" - "time" "github.com/rs/zerolog" - "go.mau.fi/util/exsync" - "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" "go.mau.fi/mautrix-telegram/pkg/gotd/tg" ) @@ -40,145 +36,115 @@ const ( ) type PhoneLogin struct { - user *bridgev2.User - main *TelegramConnector - authData UserLoginSession - authClient *telegram.Client - authClientCtx context.Context - authClientCancel context.CancelFunc - - phone string - hash string + *baseLogin + phone string + hash string + codeSubmitted bool } -var _ bridgev2.LoginProcessUserInput = (*PhoneLogin)(nil) +var ( + _ bridgev2.LoginProcessUserInput = (*PhoneLogin)(nil) + _ bridgev2.LoginProcessWithOverride = (*PhoneLogin)(nil) +) -func (p *PhoneLogin) Cancel() { - if p.authClientCancel != nil { - p.authClientCancel() - <-p.authClientCtx.Done() +func (pl *PhoneLogin) 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 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{ - Type: bridgev2.LoginStepTypeUserInput, - StepID: LoginStepIDPhoneNumber, - Instructions: "Please enter your phone number", + Type: bridgev2.LoginStepTypeUserInput, + StepID: LoginStepIDPhoneNumber, UserInputParams: &bridgev2.LoginUserInputParams{ - Fields: []bridgev2.LoginInputDataField{ - { - Type: bridgev2.LoginInputFieldTypePhoneNumber, - ID: LoginStepIDPhoneNumber, - Name: "Phone Number", - Description: "Include the country code with +", - }, - }, + Fields: []bridgev2.LoginInputDataField{{ + Type: bridgev2.LoginInputFieldTypePhoneNumber, + ID: LoginStepIDPhoneNumber, + Name: "Phone number", + Description: "Include the country code with +", + }}, }, }, nil } -func (p *PhoneLogin) SubmitUserInput(ctx context.Context, input map[string]string) (*bridgev2.LoginStep, error) { - log := zerolog.Ctx(ctx).With().Str("component", "telegram_phone_login").Logger() - if phone, ok := input[LoginStepIDPhoneNumber]; ok { - p.phone = phone - p.authClient = telegram.NewClient(p.main.Config.APIID, p.main.Config.APIHash, telegram.Options{ - CustomSessionStorage: &p.authData, - Logger: zap.New(zerozap.NewWithLevels(zerolog.Ctx(ctx).With().Str("component", "telegram_phone_login_client").Logger(), zapLevelMap)), - Device: p.main.deviceConfig(), - }) +func (pl *PhoneLogin) SubmitUserInput(ctx context.Context, input map[string]string) (*bridgev2.LoginStep, error) { + if pl.client == nil { + return pl.submitNumber(ctx, input[LoginStepIDPhoneNumber]) + } else if pl.codeSubmitted { + return pl.submitPassword(ctx, input[LoginStepIDPassword], pl.phone) + } else { + return pl.submitCode(ctx, input[LoginStepIDCode]) + } +} - p.authClientCtx, p.authClientCancel = context.WithTimeoutCause(log.WithContext(context.Background()), time.Hour, errors.New("phone login took over one hour")) - initialized := exsync.NewEvent() - done := NewFuture[error]() - runTelegramClient(p.authClientCtx, p.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 { - 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) +func (pl *PhoneLogin) submitNumber(ctx context.Context, phone string) (*bridgev2.LoginStep, error) { + if phone == "" { + return nil, fmt.Errorf("phone number is empty") + } + log := zerolog.Ctx(ctx).With().Str("component", "phone login").Logger() + ctx = log.WithContext(ctx) + pl.phone = phone + err := pl.makeClient(ctx, nil) + if err != nil { + return nil, err } - 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) { - defer p.authClientCancel() - return finalizeLogin(ctx, p.user, authorization, UserLoginMetadata{ - Phone: p.phone, - Session: p.authData, - }) +func (pl *PhoneLogin) submitCode(ctx context.Context, code string) (*bridgev2.LoginStep, error) { + if pl.client == nil { + return nil, fmt.Errorf("unexpected state: client is nil when submitting phone code") + } + 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}) } diff --git a/pkg/connector/loginqr.go b/pkg/connector/loginqr.go index a89db643..9266b7a2 100644 --- a/pkg/connector/loginqr.go +++ b/pkg/connector/loginqr.go @@ -23,15 +23,9 @@ import ( "time" "github.com/rs/zerolog" - "go.mau.fi/util/exsync" - "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" "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" ) @@ -43,14 +37,7 @@ type qrAuthResult struct { } type QRLogin struct { - user *bridgev2.User - main *TelegramConnector - authData UserLoginSession - authClient *telegram.Client - - authClientCtx context.Context - authClientCancel context.CancelFunc - + *baseLogin auth chan qrAuthResult 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.LoginProcessUserInput = (*QRLogin)(nil) // For asking for password -func (q *QRLogin) Cancel() { - if q.authClientCancel != nil { - q.authClientCancel() - <-q.authClientCtx.Done() - } +func waitContextDone(ctx context.Context) error { + <-ctx.Done() + return ctx.Err() } -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{}) +const LoginTimeout = 10 * time.Minute +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.OnLoginToken(func(ctx context.Context, e tg.Entities, update *tg.UpdateLoginToken) error { loggedIn <- struct{}{} return nil }) - zaplog := zap.New(zerozap.NewWithLevels(log, zapLevelMap)) - 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) + err := ql.makeClient(ctx, &dispatcher) if 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, + qr := qrlogin.NewQR(ql.client.API(), ql.main.Config.APIID, ql.main.Config.APIHash, qrlogin.Options{ + Migrate: ql.client.MigrateTo, }) - q.qrToken = make(chan qrlogin.Token) - q.auth = make(chan qrAuthResult) + ql.qrToken = make(chan qrlogin.Token) + ql.auth = make(chan qrAuthResult) go func() { - auth, err := qr.Auth(q.authClientCtx, loggedIn, func(ctx context.Context, token qrlogin.Token) error { - q.qrToken <- token + auth, err := qr.Auth(ctx, loggedIn, func(ctx context.Context, token qrlogin.Token) error { + ql.qrToken <- token 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.: select { - case token := <-q.qrToken: + case token := <-ql.qrToken: return &bridgev2.LoginStep{ Type: bridgev2.LoginStepTypeDisplayAndWait, StepID: LoginStepIDShowQR, @@ -130,20 +106,20 @@ func (q *QRLogin) Start(ctx context.Context) (*bridgev2.LoginStep, error) { }, }, nil case <-ctx.Done(): - q.Cancel() + ql.Cancel() return nil, ctx.Err() - case <-q.authClientCtx.Done(): - return nil, q.authClientCtx.Err() + case <-ql.ctx.Done(): + return nil, ql.ctx.Err() } } -func (q *QRLogin) Wait(ctx context.Context) (*bridgev2.LoginStep, error) { - if q.qrToken == nil { +func (ql *QRLogin) Wait(ctx context.Context) (*bridgev2.LoginStep, error) { + if ql.qrToken == nil { panic("qr token channel is nil") } select { - case token := <-q.qrToken: + case token := <-ql.qrToken: // There's a new token, show it to the user. return &bridgev2.LoginStep{ Type: bridgev2.LoginStepTypeDisplayAndWait, @@ -154,57 +130,23 @@ func (q *QRLogin) Wait(ctx context.Context) (*bridgev2.LoginStep, error) { Data: token.URL(), }, }, nil - case authResult := <-q.auth: + case authResult := <-ql.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 + return passwordLoginStep, nil } else if authResult.Error != nil { + ql.Cancel() 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, - }) + return ql.finalizeLogin(ctx, authResult.Authorization, nil) case <-ctx.Done(): - q.Cancel() + ql.Cancel() return nil, ctx.Err() - case <-q.authClientCtx.Done(): - return nil, q.authClientCtx.Err() + case <-ql.ctx.Done(): + return nil, ql.ctx.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 { - 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, - }) +func (ql *QRLogin) SubmitUserInput(ctx context.Context, input map[string]string) (*bridgev2.LoginStep, error) { + return ql.submitPassword(ctx, input[LoginStepIDPassword], "") } diff --git a/pkg/connector/metadata.go b/pkg/connector/metadata.go index 528c9057..debde235 100644 --- a/pkg/connector/metadata.go +++ b/pkg/connector/metadata.go @@ -74,9 +74,11 @@ type MessageMetadata struct { } type UserLoginMetadata struct { - Phone string `json:"phone"` - Session UserLoginSession `json:"session"` - TakeoutID int64 `json:"takeout_id,omitempty"` + LoginPhone string `json:"phone,omitempty"` + LoginMethod string `json:"login_method,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"` TakeoutDialogCrawlCursor networkid.PortalID `json:"takeout_portal_crawl_cursor,omitempty"`