login: implement QR login
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
This commit is contained in:
+29
-150
@@ -18,15 +18,9 @@ package connector
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/gotd/td/telegram"
|
||||
"github.com/gotd/td/telegram/auth"
|
||||
"github.com/gotd/td/tg"
|
||||
"github.com/rs/zerolog"
|
||||
"go.mau.fi/zerozap"
|
||||
"go.uber.org/zap"
|
||||
"maunium.net/go/mautrix/bridgev2"
|
||||
"maunium.net/go/mautrix/bridgev2/database"
|
||||
|
||||
@@ -34,159 +28,44 @@ import (
|
||||
"go.mau.fi/mautrix-telegram/pkg/connector/util"
|
||||
)
|
||||
|
||||
// TODO QR login support
|
||||
const (
|
||||
LoginFlowIDPhone = "phone"
|
||||
LoginFlowIDQR = "qr"
|
||||
|
||||
const LoginFlowIDPhone = "phone"
|
||||
LoginStepIDComplete = "fi.mau.telegram.login.complete"
|
||||
)
|
||||
|
||||
func (tg *TelegramConnector) GetLoginFlows() []bridgev2.LoginFlow {
|
||||
return []bridgev2.LoginFlow{{
|
||||
Name: "Phone Number",
|
||||
Description: "Login using your Telegram phone number",
|
||||
ID: LoginFlowIDPhone,
|
||||
}}
|
||||
return []bridgev2.LoginFlow{
|
||||
{
|
||||
Name: "Phone Number",
|
||||
Description: "Login using your Telegram phone number",
|
||||
ID: LoginFlowIDPhone,
|
||||
},
|
||||
{
|
||||
Name: "QR Code",
|
||||
Description: "Login by scanning a QR code from your phone",
|
||||
ID: LoginFlowIDQR,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (tg *TelegramConnector) CreateLogin(ctx context.Context, user *bridgev2.User, flowID string) (bridgev2.LoginProcess, error) {
|
||||
if flowID != LoginFlowIDPhone {
|
||||
switch flowID {
|
||||
case LoginFlowIDPhone:
|
||||
return &PhoneLogin{user: user, main: tg}, nil
|
||||
case LoginFlowIDQR:
|
||||
return &QRLogin{user: user, main: tg}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown flow ID %s", flowID)
|
||||
}
|
||||
return &PhoneLogin{user: user, main: tg}, nil
|
||||
}
|
||||
|
||||
const (
|
||||
LoginStepIDPhoneNumber = "fi.mau.telegram.phone_number"
|
||||
LoginStepIDCode = "fi.mau.telegram.code"
|
||||
LoginStepIDPassword = "fi.mau.telegram.password"
|
||||
LoginStepIDComplete = "fi.mau.telegram.complete"
|
||||
)
|
||||
|
||||
type PhoneLogin struct {
|
||||
user *bridgev2.User
|
||||
main *TelegramConnector
|
||||
authData UserLoginSession
|
||||
client *telegram.Client
|
||||
clientCancel context.CancelFunc
|
||||
|
||||
phone string
|
||||
hash string
|
||||
}
|
||||
|
||||
var _ bridgev2.LoginProcessUserInput = (*PhoneLogin)(nil)
|
||||
|
||||
func (p *PhoneLogin) Cancel() {
|
||||
p.clientCancel()
|
||||
}
|
||||
|
||||
func (p *PhoneLogin) Start(ctx context.Context) (*bridgev2.LoginStep, error) {
|
||||
return &bridgev2.LoginStep{
|
||||
Type: bridgev2.LoginStepTypeUserInput,
|
||||
StepID: LoginStepIDPhoneNumber,
|
||||
Instructions: "Please enter your phone number",
|
||||
UserInputParams: &bridgev2.LoginUserInputParams{
|
||||
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) {
|
||||
if phone, ok := input[LoginStepIDPhoneNumber]; ok {
|
||||
p.phone = phone
|
||||
p.client = telegram.NewClient(p.main.Config.APIID, p.main.Config.APIHash, telegram.Options{
|
||||
CustomSessionStorage: &p.authData,
|
||||
Logger: zap.New(zerozap.New(zerolog.Ctx(ctx).With().Str("component", "telegram_login_client").Logger())),
|
||||
})
|
||||
var err error
|
||||
p.clientCancel, err = connectTelegramClient(context.Background(), p.client)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sentCode, err := p.client.Auth().SendCode(ctx, 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 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.client.Auth().SignIn(ctx, 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.SignUpRequired{}) {
|
||||
return nil, fmt.Errorf("sign-up is not supported")
|
||||
} 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.client.Auth().Password(ctx, password)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to submit password: %w", err)
|
||||
}
|
||||
return p.handleAuthSuccess(ctx, authorization)
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unexpected state during phone login")
|
||||
}
|
||||
|
||||
func (p *PhoneLogin) handleAuthSuccess(ctx context.Context, authorization *tg.AuthAuthorization) (*bridgev2.LoginStep, error) {
|
||||
// Now that we have the Telegram user ID, store it in the database and
|
||||
// close the login client.
|
||||
p.clientCancel()
|
||||
|
||||
func finalizeLogin(ctx context.Context, user *bridgev2.User, authorization *tg.AuthAuthorization, metadata UserLoginMetadata) (*bridgev2.LoginStep, error) {
|
||||
userLoginID := ids.MakeUserLoginID(authorization.User.GetID())
|
||||
ul, err := p.user.NewLogin(ctx, &database.UserLogin{
|
||||
ID: userLoginID,
|
||||
Metadata: &UserLoginMetadata{
|
||||
Phone: p.phone,
|
||||
Session: p.authData,
|
||||
},
|
||||
ul, err := user.NewLogin(ctx, &database.UserLogin{
|
||||
ID: userLoginID,
|
||||
Metadata: &metadata,
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to save new login: %w", err)
|
||||
@@ -196,7 +75,7 @@ func (p *PhoneLogin) handleAuthSuccess(ctx context.Context, authorization *tg.Au
|
||||
return nil, fmt.Errorf("failed to connect after login: %w", err)
|
||||
}
|
||||
client := ul.Client.(*TelegramClient)
|
||||
user, err := client.client.Self(ctx)
|
||||
me, err := client.client.Self(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -209,7 +88,7 @@ func (p *PhoneLogin) handleAuthSuccess(ctx context.Context, authorization *tg.Au
|
||||
return &bridgev2.LoginStep{
|
||||
Type: bridgev2.LoginStepTypeComplete,
|
||||
StepID: LoginStepIDComplete,
|
||||
Instructions: fmt.Sprintf("Successfully logged in as %d / +%s (%s)", user.ID, user.Phone, util.FormatFullName(user.FirstName, user.LastName)),
|
||||
Instructions: fmt.Sprintf("Successfully logged in as %d / +%s (%s)", me.ID, me.Phone, util.FormatFullName(me.FirstName, me.LastName)),
|
||||
CompleteParams: &bridgev2.LoginCompleteParams{
|
||||
UserLoginID: ul.ID,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user