From 900991d8a75f9639b4f07acd6308736946ba77db Mon Sep 17 00:00:00 2001 From: Rustam Gilyazov <16064414+rusq@users.noreply.github.com> Date: Mon, 11 Nov 2024 21:59:51 +1000 Subject: [PATCH] token/cookie new auth flow --- cmd/slackdump/internal/diag/eztest.go | 70 +++++++--- cmd/slackdump/internal/export/wizard.go | 17 +-- .../internal/ui/bubbles/menu/model.go | 44 ++++-- cmd/slackdump/internal/ui/theme.go | 3 +- .../internal/workspace/workspaceui/api.go | 34 +++++ .../internal/workspace/workspaceui/dialogs.go | 34 +++++ .../workspace/workspaceui/tokencookie.go | 132 ++++++++++++++++++ .../workspace/workspaceui/workspaceui.go | 95 +++++++++++++ .../internal/workspace/workspaceui/wsp_new.go | 61 -------- cmd/slackdump/main.go | 7 +- internal/cache/manager.go | 7 +- internal/structures/structures.go | 18 +++ .../structures/structures_test.go | 7 +- 13 files changed, 411 insertions(+), 118 deletions(-) create mode 100644 cmd/slackdump/internal/workspace/workspaceui/api.go create mode 100644 cmd/slackdump/internal/workspace/workspaceui/dialogs.go create mode 100644 cmd/slackdump/internal/workspace/workspaceui/tokencookie.go create mode 100644 cmd/slackdump/internal/workspace/workspaceui/workspaceui.go delete mode 100644 cmd/slackdump/internal/workspace/workspaceui/wsp_new.go rename cmd/slackdump/internal/export/wizard_test.go => internal/structures/structures_test.go (89%) diff --git a/cmd/slackdump/internal/diag/eztest.go b/cmd/slackdump/internal/diag/eztest.go index f3aaef4f..02d1567e 100644 --- a/cmd/slackdump/internal/diag/eztest.go +++ b/cmd/slackdump/internal/diag/eztest.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "net/http" "os" "github.com/playwright-community/playwright-go" @@ -29,34 +30,49 @@ You will see "OK" in the end if there were no issues, otherwise an error will be printed and the test will be terminated. `, CustomFlags: true, + PrintFlags: true, } type ezResult struct { - Engine string `json:"engine,omitempty"` - HasToken bool `json:"has_token,omitempty"` - HasCookies bool `json:"has_cookies,omitempty"` - Err *string `json:"error,omitempty"` + Engine string `json:"engine,omitempty"` + HasToken bool `json:"has_token,omitempty"` + HasCookies bool `json:"has_cookies,omitempty"` + Err *string `json:"error,omitempty"` + Credentials *Credentials `json:"credentials,omitempty"` } +type Credentials struct { + Token string `json:"token,omitempty"` + Cookies []*http.Cookie `json:"cookie,omitempty"` +} + +type eztestOpts struct { + printCreds bool + wsp string + legacy bool +} + +var eztestFlags eztestOpts + func init() { CmdEzTest.Flag.Usage = func() { fmt.Fprint(os.Stdout, "usage: slackdump tools eztest [flags]\n\nFlags:\n") CmdEzTest.Flag.PrintDefaults() } + CmdEzTest.Flag.BoolVar(&eztestFlags.printCreds, "p", false, "print credentials") + CmdEzTest.Flag.BoolVar(&eztestFlags.legacy, "legacy-browser", false, "run with playwright") + CmdEzTest.Flag.StringVar(&eztestFlags.wsp, "w", "", "Slack `workspace` to login to.") } func runEzLoginTest(ctx context.Context, cmd *base.Command, args []string) error { lg := logger.FromContext(ctx) - wsp := cmd.Flag.String("w", "", "Slack `workspace` to login to.") - legacy := cmd.Flag.Bool("legacy-browser", false, "run with playwright") - if err := cmd.Flag.Parse(args); err != nil { base.SetExitStatus(base.SInvalidParameters) return err } - if *wsp == "" { + if eztestFlags.wsp == "" { base.SetExitStatus(base.SInvalidParameters) cmd.Flag.Usage() return nil @@ -66,10 +82,10 @@ func runEzLoginTest(ctx context.Context, cmd *base.Command, args []string) error res ezResult ) - if *legacy { - res = tryPlaywrightAuth(ctx, *wsp) + if eztestFlags.legacy { + res = tryPlaywrightAuth(ctx, eztestFlags.wsp, eztestFlags.printCreds) } else { - res = tryRodAuth(ctx, *wsp) + res = tryRodAuth(ctx, eztestFlags.wsp, eztestFlags.printCreds) } enc := json.NewEncoder(os.Stdout) @@ -88,28 +104,34 @@ func runEzLoginTest(ctx context.Context, cmd *base.Command, args []string) error return nil } -func tryPlaywrightAuth(ctx context.Context, wsp string) ezResult { - var res = ezResult{Engine: "playwright"} +func tryPlaywrightAuth(ctx context.Context, wsp string, populateCreds bool) ezResult { + var ret = ezResult{Engine: "playwright"} if err := playwright.Install(&playwright.RunOptions{Browsers: []string{"firefox"}}); err != nil { - res.Err = ptr(fmt.Sprintf("playwright installation error: %s", err)) - return res + ret.Err = ptr(fmt.Sprintf("playwright installation error: %s", err)) + return ret } prov, err := auth.NewBrowserAuth(ctx, auth.BrowserWithWorkspace(wsp)) if err != nil { - res.Err = ptr(err.Error()) - return res + ret.Err = ptr(err.Error()) + return ret } - res.HasToken = len(prov.SlackToken()) > 0 - res.HasCookies = len(prov.Cookies()) > 0 - return res + ret.HasToken = len(prov.SlackToken()) > 0 + ret.HasCookies = len(prov.Cookies()) > 0 + if populateCreds { + ret.Credentials = &Credentials{ + Token: prov.SlackToken(), + Cookies: prov.Cookies(), + } + } + return ret } func ptr[T any](t T) *T { return &t } -func tryRodAuth(ctx context.Context, wsp string) ezResult { +func tryRodAuth(ctx context.Context, wsp string, populateCreds bool) ezResult { ret := ezResult{Engine: "rod"} prov, err := auth.NewRODAuth(ctx, auth.BrowserWithWorkspace(wsp)) if err != nil { @@ -118,5 +140,11 @@ func tryRodAuth(ctx context.Context, wsp string) ezResult { } ret.HasCookies = len(prov.Cookies()) > 0 ret.HasToken = len(prov.SlackToken()) > 0 + if populateCreds { + ret.Credentials = &Credentials{ + Token: prov.SlackToken(), + Cookies: prov.Cookies(), + } + } return ret } diff --git a/cmd/slackdump/internal/export/wizard.go b/cmd/slackdump/internal/export/wizard.go index 0e636fdc..fcf6e7f3 100644 --- a/cmd/slackdump/internal/export/wizard.go +++ b/cmd/slackdump/internal/export/wizard.go @@ -2,8 +2,6 @@ package export import ( "context" - "errors" - "regexp" "github.com/charmbracelet/huh" "github.com/rusq/slackdump/v3/cmd/slackdump/internal/golang/base" @@ -63,22 +61,9 @@ func (fl *exportFlags) configuration() cfgui.Configuration { Value: fl.ExportToken, Description: "File export token to append to each of the file URLs", Inline: true, - Updater: updaters.NewString(&fl.ExportToken, "", false, validateToken), + Updater: updaters.NewString(&fl.ExportToken, "", false, structures.ValidateToken), }, }, }, } } - -// tokenRe is a loose regular expression to match Slack API tokens. -// a - app, b - bot, c - client, e - export, p - legacy -var tokenRE = regexp.MustCompile(`xox[abcep]-[0-9]+-[0-9]+-[0-9]+-[0-9a-z]{64}`) - -var errInvalidToken = errors.New("token must start with xoxa-, xoxb-, xoxc- or xoxe- and be followed by 4 numbers and 64 lowercase letters") - -func validateToken(token string) error { - if !tokenRE.MatchString(token) { - return errInvalidToken - } - return nil -} diff --git a/cmd/slackdump/internal/ui/bubbles/menu/model.go b/cmd/slackdump/internal/ui/bubbles/menu/model.go index 3d1e3544..032d8092 100644 --- a/cmd/slackdump/internal/ui/bubbles/menu/model.go +++ b/cmd/slackdump/internal/ui/bubbles/menu/model.go @@ -8,6 +8,7 @@ import ( "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" + "github.com/rusq/slackdump/v3/cmd/slackdump/internal/ui" "github.com/rusq/slackdump/v3/cmd/slackdump/internal/ui/cfgui" "github.com/rusq/slackdump/v3/cmd/slackdump/internal/ui/updaters" @@ -29,9 +30,18 @@ type Model struct { help help.Model cursor int + last int } func New(title string, items []Item, preview bool) *Model { + var last = len(items) - 1 + for i := last; i >= 0; i++ { + if !items[i].Separator { + break + } + last-- + } + return &Model{ title: title, items: items, @@ -41,6 +51,8 @@ func New(title string, items []Item, preview bool) *Model { focused: true, preview: preview, finishing: false, + cursor: 0, + last: last, } } @@ -76,21 +88,29 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.Selected = m.items[m.cursor] cmds = append(cmds, tea.Quit) case key.Matches(msg, m.Keymap.Up): - for { - if m.cursor > 0 { - m.cursor-- - } - if !m.items[m.cursor].Separator { - break + if m.cursor == 0 { + m.cursor = m.last + } else { + for { + if m.cursor > 0 { + m.cursor-- + } + if !m.items[m.cursor].Separator { + break + } } } case key.Matches(msg, m.Keymap.Down): - for { - if m.cursor < len(m.items)-1 { - m.cursor++ - } - if !m.items[m.cursor].Separator { - break + if m.cursor == m.last { + m.cursor = 0 + } else { + for { + if m.cursor < m.last { + m.cursor++ + } + if !m.items[m.cursor].Separator { + break + } } } case key.Matches(msg, m.Keymap.Select): diff --git a/cmd/slackdump/internal/ui/theme.go b/cmd/slackdump/internal/ui/theme.go index 402495b6..0999b70c 100644 --- a/cmd/slackdump/internal/ui/theme.go +++ b/cmd/slackdump/internal/ui/theme.go @@ -138,8 +138,9 @@ func ThemeBase16Ext() *huh.Theme { t.Focused.SelectedOption = t.Focused.SelectedOption.Foreground(black).Background(green) t.Focused.SelectedPrefix = t.Focused.SelectedPrefix.Foreground(green) t.Focused.UnselectedOption = t.Focused.UnselectedOption.Foreground(white) - t.Focused.FocusedButton = t.Focused.FocusedButton.Foreground(white).Background(purple) + t.Focused.FocusedButton = t.Focused.FocusedButton.Foreground(white).Background(green) t.Focused.BlurredButton = t.Focused.BlurredButton.Foreground(white).Background(black) + t.Focused.NoteTitle = t.Focused.NoteTitle.Foreground(cyan) t.Focused.TextInput.Cursor.Foreground(purple) t.Focused.TextInput.Placeholder.Foreground(gray) diff --git a/cmd/slackdump/internal/workspace/workspaceui/api.go b/cmd/slackdump/internal/workspace/workspaceui/api.go new file mode 100644 index 00000000..ea5ae0c9 --- /dev/null +++ b/cmd/slackdump/internal/workspace/workspaceui/api.go @@ -0,0 +1,34 @@ +package workspaceui + +import ( + "context" + + "github.com/rusq/slackdump/v3/auth" + "github.com/rusq/slackdump/v3/auth/auth_ui" +) + +type manager interface { + SaveProvider(workspace string, p auth.Provider) error + Select(workspace string) error +} + +// createAndSelect creates a new workspace with the given provider and selects it. +// It returns the workspace name on success. +func createAndSelect(ctx context.Context, m manager, prov auth.Provider) (string, error) { + authInfo, err := prov.Test(ctx) + if err != nil { + return "", err + } + + wsp, err := auth_ui.Sanitize(authInfo.URL) + if err != nil { + return "", err + } + if err := m.SaveProvider(wsp, prov); err != nil { + return "", err + } + if err := m.Select(wsp); err != nil { + return "", err + } + return wsp, nil +} diff --git a/cmd/slackdump/internal/workspace/workspaceui/dialogs.go b/cmd/slackdump/internal/workspace/workspaceui/dialogs.go new file mode 100644 index 00000000..85f60aa6 --- /dev/null +++ b/cmd/slackdump/internal/workspace/workspaceui/dialogs.go @@ -0,0 +1,34 @@ +package workspaceui + +import ( + "context" + "fmt" + + "github.com/charmbracelet/huh" + "github.com/rusq/slackdump/v3/cmd/slackdump/internal/ui" +) + +func askRetry(ctx context.Context, name string, err error) (retry bool) { + var msg string = fmt.Sprintf("The following error occurred: %s", err) + if name != "" { + msg = fmt.Sprintf("Error creating workspace %q: %s", name, err) + } + + if err := huh.NewForm(huh.NewGroup( + huh.NewConfirm().Title("Error Creating Workspace"). + Description(msg). + Value(&retry).Affirmative("Retry").Negative("Cancel"), + )).WithTheme(ui.HuhTheme).RunWithContext(ctx); err != nil { + return false + } + return retry +} + +func success(ctx context.Context, workspace string) error { + return huh.NewForm(huh.NewGroup( + huh.NewNote().Title("Great Success!"). + Description(fmt.Sprintf("Workspace %q was added and selected.\n\n", workspace)). + Next(true). + NextLabel("Exit"), + )).WithTheme(ui.HuhTheme).RunWithContext(ctx) +} diff --git a/cmd/slackdump/internal/workspace/workspaceui/tokencookie.go b/cmd/slackdump/internal/workspace/workspaceui/tokencookie.go new file mode 100644 index 00000000..c3dbe9da --- /dev/null +++ b/cmd/slackdump/internal/workspace/workspaceui/tokencookie.go @@ -0,0 +1,132 @@ +package workspaceui + +import ( + "context" + + "github.com/charmbracelet/huh" + "github.com/rusq/slackdump/v3/auth" + "github.com/rusq/slackdump/v3/cmd/slackdump/internal/ui" + "github.com/rusq/slackdump/v3/internal/structures" +) + +const sampleToken = "xoxc-610187951300-604451271234-3473161557912-4c426dd426a45208707725b710302b32dda0ab002b80ccd8c4c8ac9971a11558" + +func prgTokenCookie(ctx context.Context, m manager) error { + var ( + token string + cookie string + workspace string + confirmed bool + ) + for !confirmed { + f := huh.NewForm(huh.NewGroup( + huh.NewInput().Title("Token"). + Description("Token value"). + Placeholder(sampleToken). + Value(&token). + Validate(structures.ValidateToken), + huh.NewInput().Title("Cookie"). + Description("Session cookie"). + Placeholder("xoxd-..."). + Value(&cookie), + huh.NewConfirm().Title("Confirm creation of workspace?"). + Description("Once confirmed this will create a new workspace with the provided token and cookie"). + Value(&confirmed). + Validate(makeValidator(ctx, &token, &cookie, auth.NewValueAuth)), + )).WithTheme(ui.HuhTheme) + if err := f.Run(); err != nil { + return err + } + if !confirmed { + return nil + } + + prov, err := auth.NewValueAuth(token, cookie) + if err != nil { + return err + } + name, err := createAndSelect(ctx, m, prov) + if err != nil { + confirmed = false + retry := askRetry(ctx, name, err) + if !retry { + return nil + } + } else { + workspace = name + break + } + } + + return success(ctx, workspace) +} + +// makeValidator creates a validator function that uses the newProvFn to +// create a new provider and test it. newProvFn should be a function that +// creates a new provider from a token and a value, where value is either a +// cookie or a file with cookies. +func makeValidator[P auth.Provider](ctx context.Context, token *string, val *string, newProvFn func(string, string) (P, error)) func(bool) error { + return func(b bool) error { + if !b { + return nil + } + p, err := newProvFn(*token, *val) + if err != nil { + return err + } + _, err = p.Test(ctx) + if err != nil { + return err + } + return nil + } +} + +func prgTokenCookieFile(ctx context.Context, m manager) error { + var ( + token string + cookiefile string + workspace string + confirmed bool + ) + for !confirmed { + f := huh.NewForm(huh.NewGroup( + huh.NewInput().Title("Token"). + Description("Token value"). + Placeholder(sampleToken). + Value(&token). + Validate(structures.ValidateToken), + huh.NewFilePicker().Title("Cookie File"). + Description("Select a cookies.txt file in Mozilla Format").AllowedTypes([]string{"txt"}). + FileAllowed(true). + ShowSize(true). + ShowPermissions(true). + Value(&cookiefile), + huh.NewConfirm().Title("Is this correct?"). + Description("Once confirmed this will create a new workspace with the provided token and cookie"). + Value(&confirmed). + Validate(makeValidator(ctx, &token, &cookiefile, auth.NewCookieFileAuth)), + )).WithTheme(ui.HuhTheme) + if err := f.Run(); err != nil { + return err + } + + prov, err := auth.NewValueAuth(token, cookiefile) + if err != nil { + return err + } + name, err := createAndSelect(ctx, m, prov) + if err != nil { + confirmed = false + retry := askRetry(ctx, name, err) + if !retry { + return nil + } + } else { + workspace = name + break + } + } + + return success(ctx, workspace) +} diff --git a/cmd/slackdump/internal/workspace/workspaceui/workspaceui.go b/cmd/slackdump/internal/workspace/workspaceui/workspaceui.go new file mode 100644 index 00000000..db91fe05 --- /dev/null +++ b/cmd/slackdump/internal/workspace/workspaceui/workspaceui.go @@ -0,0 +1,95 @@ +package workspaceui + +import ( + "context" + "errors" + + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/huh" + + "github.com/rusq/slackdump/v3/cmd/slackdump/internal/cfg" + "github.com/rusq/slackdump/v3/cmd/slackdump/internal/golang/base" + "github.com/rusq/slackdump/v3/cmd/slackdump/internal/ui/bubbles/menu" + "github.com/rusq/slackdump/v3/internal/cache" +) + +func WorkspaceNew(ctx context.Context, _ *base.Command, _ []string) error { + const ( + actLogin = "ezlogin" + actToken = "token" + actTokenFile = "tokenfile" + actSecrets = "secrets" + actExit = "exit" + ) + + mgr, err := cache.NewManager(cfg.CacheDir()) + if err != nil { + return err + } + + items := []menu.Item{ + { + ID: actLogin, + Name: "Login in Browser", + Help: "Login to Slack in your browser", + }, + { + ID: actToken, + Name: "Token/Cookie", + Help: "Enter token and cookie that you grabbed from the browser", + }, + { + ID: actTokenFile, + Name: "Token/Cookie from file", + Help: "Provide token value and cookies from file", + }, + { + ID: actSecrets, + Name: "From file with secrets", + Help: "Read from secrets.txt or .env file", + }, + { + Separator: true, + }, + { + ID: actExit, + Name: "Exit", + Help: "Exit to main menu", + }, + } + +LOOP: + for { + m := menu.New("New Workspace", items, true) + if _, err := tea.NewProgram(&wizModel{m: m}, tea.WithContext(ctx)).Run(); err != nil { + return err + } + if m.Cancelled { + break LOOP + } + var err error + switch m.Selected.ID { + case actToken: + err = prgTokenCookie(ctx, mgr) + case actTokenFile: + err = prgTokenCookieFile(ctx, mgr) + case actExit: + break LOOP + } + if err != nil { + if errors.Is(err, huh.ErrUserAborted) { + continue + } + return err + } + } + + return nil +} + +// wizModel is a wrapper around the menu. +type wizModel struct{ m *menu.Model } + +func (m *wizModel) Init() tea.Cmd { return m.m.Init() } +func (m *wizModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m.m.Update(msg) } +func (m *wizModel) View() string { return m.m.View() } diff --git a/cmd/slackdump/internal/workspace/workspaceui/wsp_new.go b/cmd/slackdump/internal/workspace/workspaceui/wsp_new.go deleted file mode 100644 index a4627c97..00000000 --- a/cmd/slackdump/internal/workspace/workspaceui/wsp_new.go +++ /dev/null @@ -1,61 +0,0 @@ -package workspaceui - -import ( - "context" - - tea "github.com/charmbracelet/bubbletea" - - "github.com/rusq/slackdump/v3/cmd/slackdump/internal/golang/base" - "github.com/rusq/slackdump/v3/cmd/slackdump/internal/ui/bubbles/menu" -) - -type wizModel struct { - m *menu.Model -} - -func WorkspaceNew(ctx context.Context, _ *base.Command, _ []string) error { - items := []menu.Item{ - { - ID: "ezlogin", - Name: "Login in Browser", - Help: "Login to Slack in your browser", - }, - { - ID: "token", - Name: "Token/Cookie", - Help: "Enter token and cookie that you grabbed from the browser", - }, - { - ID: "secrets", - Name: "From file with secrets", - Help: "Read from secrets.txt or .env file", - }, - { - Separator: true, - }, - { - ID: "exit", - Name: "Exit", - Help: "Exit to main menu", - }, - } - - m := menu.New("New Workspace", items, true) - - if _, err := tea.NewProgram(&wizModel{m: m}, tea.WithContext(ctx)).Run(); err != nil { - return err - } - return nil -} - -func (m *wizModel) Init() tea.Cmd { - return m.m.Init() -} - -func (m *wizModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - return m.m.Update(msg) -} - -func (m *wizModel) View() string { - return m.m.View() -} diff --git a/cmd/slackdump/main.go b/cmd/slackdump/main.go index b8376f00..f8875879 100644 --- a/cmd/slackdump/main.go +++ b/cmd/slackdump/main.go @@ -30,6 +30,7 @@ import ( "github.com/rusq/slackdump/v3/cmd/slackdump/internal/golang/help" "github.com/rusq/slackdump/v3/cmd/slackdump/internal/list" "github.com/rusq/slackdump/v3/cmd/slackdump/internal/man" + "github.com/rusq/slackdump/v3/cmd/slackdump/internal/ui" "github.com/rusq/slackdump/v3/cmd/slackdump/internal/view" "github.com/rusq/slackdump/v3/cmd/slackdump/internal/wizard" "github.com/rusq/slackdump/v3/cmd/slackdump/internal/workspace" @@ -40,6 +41,7 @@ func init() { loadSecrets(secretFiles) base.Slackdump.Commands = []*base.Command{ + workspace.CmdWorkspace, archive.CmdArchive, export.CmdExport, dump.CmdDump, @@ -47,7 +49,6 @@ func init() { convertcmd.CmdConvert, list.CmdList, emoji.CmdEmoji, - workspace.CmdWorkspace, diag.CmdDiag, apiconfig.CmdConfig, format.CmdFormat, @@ -297,13 +298,13 @@ func whatDo() (choice, error) { fmt.Print("\n" + cfg.Version.String() + "\n") var ans choice - err := huh.NewSelect[choice](). + err := huh.NewForm(huh.NewGroup(huh.NewSelect[choice](). Title("What do you want to do?"). Options( huh.NewOption(string(choiceHelp), choiceHelp), huh.NewOption(string(choiceWizard), choiceWizard), huh.NewOption(string(choiceExit), choiceExit), - ).Value(&ans).Run() + ).Value(&ans))).WithTheme(ui.HuhTheme).Run() return ans, err } diff --git a/internal/cache/manager.go b/internal/cache/manager.go index 91006c9a..e46f4d64 100644 --- a/internal/cache/manager.go +++ b/internal/cache/manager.go @@ -140,7 +140,12 @@ func (m *Manager) Auth(ctx context.Context, name string, c Credentials) (auth.Pr // LoadProvider loads the file from disk without any checks. func (m *Manager) LoadProvider(name string) (auth.Provider, error) { - return loadCreds(filer, filepath.Join(m.dir, m.filename(name))) + return loadCreds(filer, m.filepath(name)) +} + +// SaveProvider saves the provider to the file, no questions asked. +func (m *Manager) SaveProvider(name string, p auth.Provider) error { + return saveCreds(filer, m.filepath(name), p) } // ErrWorkspace is the error returned by the workspace manager, it contains the diff --git a/internal/structures/structures.go b/internal/structures/structures.go index 2f24229b..fe99ab28 100644 --- a/internal/structures/structures.go +++ b/internal/structures/structures.go @@ -1,6 +1,11 @@ // Package structures provides functions to parse Slack data types. package structures +import ( + "errors" + "regexp" +) + const ( LatestReplyNoReplies = "0000000000.000000" ) @@ -8,3 +13,16 @@ const ( const ( SubTypeThreadBroadcast = "thread_broadcast" ) + +// tokenRe is a loose regular expression to match Slack API tokens. +// a - app, b - bot, c - client, e - export, p - legacy +var tokenRE = regexp.MustCompile(`xox[abcep]-[0-9]+-[0-9]+-[0-9]+-[0-9a-f]{64}`) + +var errInvalidToken = errors.New("token must start with xoxa-, xoxb-, xoxc-, xoxe- or xoxp- and be followed by 3 group of numbers and then 64 hexadecimal characters") + +func ValidateToken(token string) error { + if !tokenRE.MatchString(token) { + return errInvalidToken + } + return nil +} diff --git a/cmd/slackdump/internal/export/wizard_test.go b/internal/structures/structures_test.go similarity index 89% rename from cmd/slackdump/internal/export/wizard_test.go rename to internal/structures/structures_test.go index 1a0081e8..7166a83e 100644 --- a/cmd/slackdump/internal/export/wizard_test.go +++ b/internal/structures/structures_test.go @@ -1,4 +1,5 @@ -package export +// Package structures provides functions to parse Slack data types. +package structures import ( "testing" @@ -6,7 +7,7 @@ import ( "github.com/rusq/slackdump/v3/internal/fixtures" ) -func Test_validateToken(t *testing.T) { +func TestValidateToken(t *testing.T) { type args struct { token string } @@ -68,7 +69,7 @@ func Test_validateToken(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := validateToken(tt.args.token); (err != nil) != tt.wantErr { + if err := ValidateToken(tt.args.token); (err != nil) != tt.wantErr { t.Errorf("validateToken() error = %v, wantErr %v", err, tt.wantErr) } })