You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
164 lines
3.8 KiB
164 lines
3.8 KiB
// Package qrlogin provides QR login flow implementation. |
|
// |
|
// See https://core.telegram.org/api/qr-login. |
|
package qrlogin |
|
|
|
import ( |
|
"context" |
|
"time" |
|
|
|
"github.com/go-faster/errors" |
|
|
|
"github.com/gotd/td/clock" |
|
"github.com/gotd/td/tg" |
|
) |
|
|
|
// QR implements Telegram QR login flow. |
|
type QR struct { |
|
api *tg.Client |
|
appID int |
|
appHash string |
|
migrate func(ctx context.Context, dcID int) error |
|
clock clock.Clock |
|
} |
|
|
|
// NewQR creates new QR |
|
func NewQR(api *tg.Client, appID int, appHash string, opts Options) QR { |
|
opts.setDefaults() |
|
return QR{ |
|
api: api, |
|
appID: appID, |
|
appHash: appHash, |
|
clock: opts.Clock, |
|
migrate: opts.Migrate, |
|
} |
|
} |
|
|
|
// Export exports new login token. |
|
// |
|
// See https://core.telegram.org/api/qr-login#exporting-a-login-token. |
|
func (q QR) Export(ctx context.Context, exceptIDs ...int64) (Token, error) { |
|
result, err := q.api.AuthExportLoginToken(ctx, &tg.AuthExportLoginTokenRequest{ |
|
APIID: q.appID, |
|
APIHash: q.appHash, |
|
ExceptIDs: exceptIDs, |
|
}) |
|
if err != nil { |
|
return Token{}, errors.Wrap(err, "export") |
|
} |
|
|
|
t, ok := result.(*tg.AuthLoginToken) |
|
if !ok { |
|
return Token{}, errors.Errorf("unexpected type %T", result) |
|
} |
|
return NewToken(t.Token, t.Expires), nil |
|
} |
|
|
|
// Accept accepts given token. |
|
// |
|
// See https://core.telegram.org/api/qr-login#accepting-a-login-token. |
|
func (q QR) Accept(ctx context.Context, t Token) (*tg.Authorization, error) { |
|
return AcceptQR(ctx, q.api, t) |
|
} |
|
|
|
// Import imports accepted token. |
|
// |
|
// See https://core.telegram.org/api/qr-login#confirming-importing-the-login-token. |
|
func (q QR) Import(ctx context.Context) (*tg.AuthAuthorization, error) { |
|
migrated := false |
|
|
|
retry: |
|
result, err := q.api.AuthExportLoginToken(ctx, &tg.AuthExportLoginTokenRequest{ |
|
APIID: q.appID, |
|
APIHash: q.appHash, |
|
}) |
|
if err != nil { |
|
return nil, errors.Wrap(err, "import") |
|
} |
|
|
|
switch t := result.(type) { |
|
case *tg.AuthLoginTokenMigrateTo: |
|
if migrated || q.migrate == nil { |
|
return nil, &MigrationNeededError{ |
|
MigrateTo: t, |
|
Tried: migrated, |
|
} |
|
} |
|
if err := q.migrate(ctx, t.DCID); err != nil { |
|
return nil, errors.Wrap(err, "migrate") |
|
} |
|
migrated = true |
|
goto retry |
|
case *tg.AuthLoginTokenSuccess: |
|
auth, ok := t.Authorization.(*tg.AuthAuthorization) |
|
if !ok { |
|
return nil, errors.Errorf("unexpected type %T", t.Authorization) |
|
} |
|
return auth, nil |
|
default: |
|
return nil, errors.Errorf("unexpected type %T", result) |
|
} |
|
} |
|
|
|
// LoggedIn is signal channel to notify about tg.UpdateLoginToken. |
|
type LoggedIn <-chan struct{} |
|
|
|
// OnLoginToken sets handler for given dispatcher and returns signal channel. |
|
func OnLoginToken(d interface { |
|
OnLoginToken(tg.LoginTokenHandler) |
|
}) LoggedIn { |
|
loggedIn := make(chan struct{}) |
|
d.OnLoginToken(func(ctx context.Context, e tg.Entities, update *tg.UpdateLoginToken) error { |
|
select { |
|
case loggedIn <- struct{}{}: |
|
return nil |
|
default: |
|
} |
|
return nil |
|
}) |
|
return loggedIn |
|
} |
|
|
|
// Auth generates new QR login token, shows it and awaits acceptation. |
|
// |
|
// NB: Show callback may be called more than once if QR expires. |
|
func (q QR) Auth( |
|
ctx context.Context, |
|
loggedIn LoggedIn, |
|
show func(ctx context.Context, token Token) error, |
|
exceptIDs ...int64, |
|
) (*tg.AuthAuthorization, error) { |
|
until := func(token Token) time.Duration { |
|
return token.Expires().Sub(q.clock.Now()).Truncate(time.Second) |
|
} |
|
|
|
token, err := q.Export(ctx, exceptIDs...) |
|
if err != nil { |
|
return nil, err |
|
} |
|
timer := q.clock.Timer(until(token)) |
|
defer clock.StopTimer(timer) |
|
|
|
for { |
|
if err := show(ctx, token); err != nil { |
|
return nil, errors.Wrap(err, "show") |
|
} |
|
|
|
select { |
|
case <-ctx.Done(): |
|
return nil, ctx.Err() |
|
case <-timer.C(): |
|
t, err := q.Export(ctx, exceptIDs...) |
|
if err != nil { |
|
return nil, err |
|
} |
|
token = t |
|
timer.Reset(until(token)) |
|
|
|
continue |
|
case <-loggedIn: |
|
} |
|
|
|
return q.Import(ctx) |
|
} |
|
}
|
|
|