From 058b57a53d78431befb037140103e4c3c31161c8 Mon Sep 17 00:00:00 2001 From: Rustam Gilyazov <16064414+rusq@users.noreply.github.com> Date: Sun, 3 Nov 2024 13:08:22 +1000 Subject: [PATCH] backport --- auth/auth_ui/huh.go | 93 ++++++++++++++++++++++++++++----------------- auth/rod.go | 33 ++++++++-------- go.mod | 1 + go.sum | 2 + 4 files changed, 77 insertions(+), 52 deletions(-) diff --git a/auth/auth_ui/huh.go b/auth/auth_ui/huh.go index 5e4d03f7..086de9e8 100644 --- a/auth/auth_ui/huh.go +++ b/auth/auth_ui/huh.go @@ -20,13 +20,13 @@ func (h *Huh) RequestWorkspace(w io.Writer) (string, error) { huh.NewInput(). Title("Enter Slack workspace name"). Value(&workspace). - Validate(valRequired). + Validate(valWorkspace). Description("The workspace name is the part of the URL that comes before `.slack.com' in\nhttps://.slack.com/. Both workspace name or URL are acceptable."), )).Run() if err != nil { return "", err } - return Sanitize(workspace) + return workspace, nil } func (*Huh) Stop() {} @@ -61,7 +61,7 @@ func (m methodMenuItem) String() string { var methods = []methodMenuItem{ { - "Manual", + "Interactive", "Works with most authentication schemes, except Google.", LInteractive, }, @@ -71,18 +71,33 @@ var methods = []methodMenuItem{ LHeadless, }, { - "User's Browser", + "User Browser", "Loads your user profile, works with Google Auth", LUserBrowser, }, } type LoginOpts struct { + Workspace string Type LoginType BrowserPath string } -func (*Huh) RequestLoginType(w io.Writer) (LoginOpts, error) { +func valWorkspace(s string) error { + if err := valRequired(s); err != nil { + return err + } + _, err := Sanitize(s) + return err +} + +func (*Huh) RequestLoginType(w io.Writer, workspace string) (LoginOpts, error) { + var ret = LoginOpts{ + Workspace: workspace, + Type: LInteractive, + BrowserPath: "", + } + var opts = make([]huh.Option[LoginType], 0, len(methods)) for _, m := range methods { opts = append(opts, huh.NewOption(m.String(), m.Type)) @@ -91,41 +106,49 @@ func (*Huh) RequestLoginType(w io.Writer) (LoginOpts, error) { huh.NewOption("------", LoginType(-1)), huh.NewOption("Cancel", LCancel), ) - var loginType LoginType - err := huh.NewForm(huh.NewGroup( - huh.NewSelect[LoginType]().Title("Select login type"). - Options(opts...). - Value(&loginType). - Validate(valSepEaster()). - DescriptionFunc(func() string { - switch loginType { - case LInteractive: - return "Clean browser will open on a Slack Login page." - case LHeadless: - return "You will be prompted to enter your email and password, login is automated." - case LUserBrowser: - return "System browser will open on a Slack Login page." - case LCancel: - return "Cancel the login process." - default: - return "" - } - }, &loginType), - )).Run() - if err != nil { - return LoginOpts{Type: LCancel}, err + var fields []huh.Field + if workspace == "" { + fields = append(fields, huh.NewInput(). + Title("Enter Slack workspace name"). + Value(&ret.Workspace). + Validate(valWorkspace). + Description("The workspace name is the part of the URL that comes before `.slack.com' in\nhttps://.slack.com/. Both workspace name or URL are acceptable."), + ) + } + + fields = append(fields, huh.NewSelect[LoginType](). + TitleFunc(func() string { + return fmt.Sprintf("Select login type for [%s]", ret.Workspace) + }, &ret.Workspace). + Options(opts...). + Value(&ret.Type). + Validate(valSepEaster()). + DescriptionFunc(func() string { + switch ret.Type { + case LInteractive: + return "Clean browser will open on a Slack Login page." + case LHeadless: + return "You will be prompted to enter your email and password, login is automated." + case LUserBrowser: + return "System browser will open on a Slack Login page." + case LCancel: + return "Cancel the login process." + default: + return "" + } + }, &ret.Type)) + if err := huh.NewForm(huh.NewGroup(fields...)).Run(); err != nil { + return ret, err } - if loginType == LUserBrowser { + if ret.Type == LUserBrowser { path, err := chooseBrowser() if err != nil { - return LoginOpts{Type: LCancel}, err + return ret, err } - return LoginOpts{ - Type: LUserBrowser, - BrowserPath: path, - }, err + ret.BrowserPath = path + return ret, err } - return LoginOpts{Type: loginType}, nil + return ret, nil } func chooseBrowser() (string, error) { diff --git a/auth/rod.go b/auth/rod.go index a01f5a79..f27792e2 100644 --- a/auth/rod.go +++ b/auth/rod.go @@ -7,6 +7,7 @@ import ( "os" "time" + "github.com/charmbracelet/huh/spinner" "github.com/rusq/slackauth" "github.com/rusq/slackdump/v2/auth/auth_ui" @@ -60,11 +61,10 @@ func (ro rodOpts) slackauthOpts() []slackauth.Option { } type browserAuthUIExt interface { - BrowserAuthUI // RequestLoginType should request the login type from the user and return // one of the [auth_ui.LoginType] constants. The implementation should // provide a way to cancel the login flow, returning [auth_ui.LoginCancel]. - RequestLoginType(w io.Writer) (auth_ui.LoginOpts, error) + RequestLoginType(w io.Writer, workspace string) (auth_ui.LoginOpts, error) // RequestCreds should request the user's email and password and return // them. RequestCreds(w io.Writer, workspace string) (email string, passwd string, err error) @@ -89,23 +89,13 @@ func NewRODAuth(ctx context.Context, opts ...Option) (RodAuth, error) { for _, opt := range opts { opt(&r.opts) } - if r.opts.workspace == "" { - var err error - r.opts.workspace, err = r.opts.ui.RequestWorkspace(os.Stdout) - if err != nil { - return r, err - } - if r.opts.workspace == "" { - return r, fmt.Errorf("workspace cannot be empty") - } - } if wsp, err := auth_ui.Sanitize(r.opts.workspace); err != nil { return r, err } else { r.opts.workspace = wsp } - resp, err := r.opts.ui.RequestLoginType(os.Stdout) + resp, err := r.opts.ui.RequestLoginType(os.Stdout, r.opts.workspace) if err != nil { return r, err } @@ -117,7 +107,7 @@ func NewRODAuth(ctx context.Context, opts ...Option) (RodAuth, error) { } cl, err := slackauth.New( - r.opts.workspace, + resp.Workspace, sopts..., ) if err != nil { @@ -137,14 +127,13 @@ func NewRODAuth(ctx context.Context, opts ...Option) (RodAuth, error) { return r, err } case auth_ui.LHeadless: - sp, err = headlessFlow(ctx, cl, r.opts.workspace, r.opts.ui) + sp, err = headlessFlow(ctx, cl, resp.Workspace, r.opts.ui) if err != nil { return r, err } case auth_ui.LCancel: return r, ErrCancelled } - lg.Printf("✅ authenticated (time taken: %s)", time.Since(t)) return RodAuth{ @@ -163,12 +152,22 @@ func headlessFlow(ctx context.Context, cl *slackauth.Client, workspace string, u if password == "" { return sp, fmt.Errorf("password cannot be empty") } - logger.FromContext(ctx).Println("⏳ Logging in to Slack, depending on your connection speed, it will take 25-40 seconds...") + + sctx, cancel := context.WithCancel(ctx) + defer cancel() + go func() { + _ = spinner.New(). + Type(spinner.Dots). + Title("Logging in to Slack, it will take 25-40 seconds"). + Context(sctx). + Run() + }() var loginErr error sp.Token, sp.Cookie, loginErr = cl.Headless(ctx, username, password) if loginErr != nil { return sp, loginErr } + return } diff --git a/go.mod b/go.mod index fba99572..f00e073e 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.23 require ( github.com/MercuryEngineering/CookieMonster v0.0.0-20180304172713-1584578b3403 github.com/charmbracelet/huh v0.6.0 + github.com/charmbracelet/huh/spinner v0.0.0-20241028115900-20a4d21717a8 github.com/denisbrodbeck/machineid v1.0.1 github.com/fatih/color v1.17.0 github.com/joho/godotenv v1.5.1 diff --git a/go.sum b/go.sum index 6cf9df32..e43479a0 100644 --- a/go.sum +++ b/go.sum @@ -14,6 +14,8 @@ github.com/charmbracelet/bubbletea v1.1.1 h1:KJ2/DnmpfqFtDNVTvYZ6zpPFL9iRCRr0qqK github.com/charmbracelet/bubbletea v1.1.1/go.mod h1:9Ogk0HrdbHolIKHdjfFpyXJmiCzGwy+FesYkZr7hYU4= github.com/charmbracelet/huh v0.6.0 h1:mZM8VvZGuE0hoDXq6XLxRtgfWyTI3b2jZNKh0xWmax8= github.com/charmbracelet/huh v0.6.0/go.mod h1:GGNKeWCeNzKpEOh/OJD8WBwTQjV3prFAtQPpLv+AVwU= +github.com/charmbracelet/huh/spinner v0.0.0-20241028115900-20a4d21717a8 h1:g+Bz64hsMLTf3lAgUqI6Rj1YEAlm/HN39IuhyneCokc= +github.com/charmbracelet/huh/spinner v0.0.0-20241028115900-20a4d21717a8/go.mod h1:Cxhgl8N0sX9A+EQxedzzGZAalaF8fUVL+JP/pSOW8cI= github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA/SsA3cjw= github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY= github.com/charmbracelet/x/ansi v0.3.2 h1:wsEwgAN+C9U06l9dCVMX0/L3x7ptvY1qmjMwyfE6USY=