login: refactor to share more code
This commit is contained in:
@@ -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
@@ -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
@@ -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
@@ -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,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"`
|
||||||
|
|||||||
Reference in New Issue
Block a user