diff --git a/cli.go b/cli.go new file mode 100644 index 0000000..ae5ab70 --- /dev/null +++ b/cli.go @@ -0,0 +1,85 @@ +package main + +import ( + "fmt" + "os" + "sort" + "strings" + "sync" + + "github.com/pkg/errors" +) + +type ( + cliRegistry struct { + cmds map[string]cliRegistryEntry + sync.Mutex + } + + cliRegistryEntry struct { + Description string + Name string + Params []string + Run func([]string) error + } +) + +var ( + cli = newCLIRegistry() + errHelpCalled = errors.New("help called") +) + +func newCLIRegistry() *cliRegistry { + return &cliRegistry{ + cmds: make(map[string]cliRegistryEntry), + } +} + +func (c *cliRegistry) Add(e cliRegistryEntry) { + c.Lock() + defer c.Unlock() + + c.cmds[e.Name] = e +} + +func (c *cliRegistry) Call(args []string) error { + c.Lock() + defer c.Unlock() + + cmdEntry := c.cmds[args[0]] + if cmdEntry.Name != args[0] { + c.help() + return errHelpCalled + } + + return cmdEntry.Run(args) +} + +func (c *cliRegistry) help() { + // Called from Call, does not need lock + + var ( + maxCmdLen int + cmds []cliRegistryEntry + ) + + for name := range c.cmds { + entry := c.cmds[name] + if l := len(entry.CommandDisplay()); l > maxCmdLen { + maxCmdLen = l + } + cmds = append(cmds, entry) + } + + sort.Slice(cmds, func(i, j int) bool { return cmds[i].Name < cmds[j].Name }) + + tpl := fmt.Sprintf(" %%-%ds %%s\n", maxCmdLen) + fmt.Fprintln(os.Stdout, "Supported sub-commands are:") + for _, cmd := range cmds { + fmt.Fprintf(os.Stdout, tpl, cmd.CommandDisplay(), cmd.Description) + } +} + +func (c cliRegistryEntry) CommandDisplay() string { + return strings.Join(append([]string{c.Name}, c.Params...), " ") +} diff --git a/cli_actorDocs.go b/cli_actorDocs.go new file mode 100644 index 0000000..9fe03fa --- /dev/null +++ b/cli_actorDocs.go @@ -0,0 +1,26 @@ +package main + +import ( + "bytes" + "os" + + "github.com/pkg/errors" +) + +func init() { + cli.Add(cliRegistryEntry{ + Name: "actor-docs", + Description: "Generate markdown documentation for available actors", + Run: func(args []string) error { + doc, err := generateActorDocs() + if err != nil { + return errors.Wrap(err, "generating actor docs") + } + if _, err = os.Stdout.Write(append(bytes.TrimSpace(doc), '\n')); err != nil { + return errors.Wrap(err, "writing actor docs to stdout") + } + + return nil + }, + }) +} diff --git a/cli_apiToken.go b/cli_apiToken.go new file mode 100644 index 0000000..78b775e --- /dev/null +++ b/cli_apiToken.go @@ -0,0 +1,43 @@ +package main + +import ( + "os" + + "github.com/gofrs/uuid/v3" + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + "gopkg.in/yaml.v2" +) + +func init() { + cli.Add(cliRegistryEntry{ + Name: "api-token", + Description: "Generate an api-token to be entered into the config", + Params: []string{"", "", "[...scope]"}, + Run: func(args []string) error { + if len(args) < 3 { //nolint:gomnd // Just a count of parameters + return errors.New("Usage: twitch-bot api-token [...scope]") + } + + t := configAuthToken{ + Name: args[1], + Modules: args[2:], + } + + if err := fillAuthToken(&t); err != nil { + return errors.Wrap(err, "generating token") + } + + log.WithField("token", t.Token).Info("Token generated, add this to your config:") + if err := yaml.NewEncoder(os.Stdout).Encode(map[string]map[string]configAuthToken{ + "auth_tokens": { + uuid.Must(uuid.NewV4()).String(): t, + }, + }); err != nil { + return errors.Wrap(err, "printing token info") + } + + return nil + }, + }) +} diff --git a/cli_migrateV2.go b/cli_migrateV2.go new file mode 100644 index 0000000..c5b3e37 --- /dev/null +++ b/cli_migrateV2.go @@ -0,0 +1,33 @@ +package main + +import ( + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + + "github.com/Luzifer/twitch-bot/v3/internal/v2migrator" +) + +func init() { + cli.Add(cliRegistryEntry{ + Name: "migrate-v2", + Description: "Migrate old (*.json.gz) storage file into new database", + Params: []string{""}, + Run: func(args []string) error { + if len(args) < 2 { //nolint:gomnd // Just a count of parameters + return errors.New("Usage: twitch-bot migrate-v2 ") + } + + v2s := v2migrator.NewStorageFile() + if err := v2s.Load(args[1], cfg.StorageEncryptionPass); err != nil { + return errors.Wrap(err, "loading v2 storage file") + } + + if err := v2s.Migrate(db); err != nil { + return errors.Wrap(err, "migrating v2 storage file") + } + + log.Info("v2 storage file was migrated") + return nil + }, + }) +} diff --git a/cli_resetSecrets.go b/cli_resetSecrets.go new file mode 100644 index 0000000..ea6af7f --- /dev/null +++ b/cli_resetSecrets.go @@ -0,0 +1,26 @@ +package main + +import ( + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" +) + +func init() { + cli.Add(cliRegistryEntry{ + Name: "reset-secrets", + Description: "Remove encrypted data to reset encryption passphrase", + Run: func(args []string) error { + if err := accessService.RemoveAllExtendedTwitchCredentials(); err != nil { + return errors.Wrap(err, "resetting Twitch credentials") + } + log.Info("removed stored Twitch credentials") + + if err := db.ResetEncryptedCoreMeta(); err != nil { + return errors.Wrap(err, "resetting encrypted meta entries") + } + log.Info("removed encrypted meta entries") + + return nil + }, + }) +} diff --git a/cli_validateConfig.go b/cli_validateConfig.go new file mode 100644 index 0000000..e945600 --- /dev/null +++ b/cli_validateConfig.go @@ -0,0 +1,16 @@ +package main + +import "github.com/pkg/errors" + +func init() { + cli.Add(cliRegistryEntry{ + Name: "validate-config", + Description: "Try to load configuration file and report errors if any", + Run: func(args []string) error { + return errors.Wrap( + loadConfig(cfg.Config), + "loading config", + ) + }, + }) +} diff --git a/main.go b/main.go index 64872a2..c685a08 100644 --- a/main.go +++ b/main.go @@ -1,7 +1,6 @@ package main import ( - "bytes" "context" "crypto/rand" "encoding/hex" @@ -23,14 +22,12 @@ import ( "github.com/pkg/errors" "github.com/robfig/cron/v3" log "github.com/sirupsen/logrus" - "gopkg.in/yaml.v2" "github.com/Luzifer/go_helpers/v2/backoff" "github.com/Luzifer/go_helpers/v2/str" "github.com/Luzifer/rconfig/v2" "github.com/Luzifer/twitch-bot/v3/internal/service/access" "github.com/Luzifer/twitch-bot/v3/internal/service/timer" - "github.com/Luzifer/twitch-bot/v3/internal/v2migrator" "github.com/Luzifer/twitch-bot/v3/pkg/database" "github.com/Luzifer/twitch-bot/v3/pkg/twitch" ) @@ -158,90 +155,6 @@ func getEventSubSecret() (secret, handle string, err error) { return eventSubSecret, eventSubSecret[:5], errors.Wrap(db.StoreEncryptedCoreMeta(coreMetaKeyEventSubSecret, eventSubSecret), "storing secret to database") } -func handleSubCommand(args []string) { - switch args[0] { - - case "actor-docs": - doc, err := generateActorDocs() - if err != nil { - log.WithError(err).Fatal("Unable to generate actor docs") - } - if _, err = os.Stdout.Write(append(bytes.TrimSpace(doc), '\n')); err != nil { - log.WithError(err).Fatal("Unable to write actor docs to stdout") - } - - case "api-token": - if len(args) < 3 { //nolint:gomnd // Just a count of parameters - log.Fatalf("Usage: twitch-bot api-token [...scope]") - } - - t := configAuthToken{ - Name: args[1], - Modules: args[2:], - } - - if err := fillAuthToken(&t); err != nil { - log.WithError(err).Fatal("Unable to generate token") - } - - log.WithField("token", t.Token).Info("Token generated, add this to your config:") - if err := yaml.NewEncoder(os.Stdout).Encode(map[string]map[string]configAuthToken{ - "auth_tokens": { - uuid.Must(uuid.NewV4()).String(): t, - }, - }); err != nil { - log.WithError(err).Fatal("Unable to output token info") - } - - case "help": - fmt.Println("Supported sub-commands are:") - fmt.Println(" actor-docs Generate markdown documentation for available actors") - fmt.Println(" api-token Generate an api-token to be entered into the config") - fmt.Println(" migrate-v2 Migrate old (*.json.gz) storage file into new database") - fmt.Println(" reset-secrets Remove encrypted data to reset encryption passphrase") - fmt.Println(" validate-config Try to load configuration file and report errors if any") - fmt.Println(" help Prints this help message") - - case "migrate-v2": - if len(args) < 2 { //nolint:gomnd // Just a count of parameters - log.Fatalf("Usage: twitch-bot migrate-v2 ") - } - - v2s := v2migrator.NewStorageFile() - if err := v2s.Load(args[1], cfg.StorageEncryptionPass); err != nil { - log.WithError(err).Fatal("loading v2 storage file") - } - - if err := v2s.Migrate(db); err != nil { - log.WithError(err).Fatal("migrating v2 storage file") - } - - log.Info("v2 storage file was migrated") - - case "reset-secrets": - // Nuke permission table entries - if err := accessService.RemoveAllExtendedTwitchCredentials(); err != nil { - log.WithError(err).Fatal("resetting Twitch credentials") - } - log.Info("removed stored Twitch credentials") - - if err := db.ResetEncryptedCoreMeta(); err != nil { - log.WithError(err).Fatal("resetting encrypted meta entries") - } - log.Info("removed encrypted meta entries") - - case "validate-config": - if err := loadConfig(cfg.Config); err != nil { - log.WithError(err).Fatal("loading config") - } - - default: - handleSubCommand([]string{"help"}) - log.Fatalf("Unknown sub-command %q", args[0]) - - } -} - //nolint:funlen,gocognit,gocyclo // Complexity is a little too high but makes no sense to split func main() { var err error @@ -315,7 +228,9 @@ func main() { } if len(rconfig.Args()) > 1 { - handleSubCommand(rconfig.Args()[1:]) + if err = cli.Call(rconfig.Args()[1:]); err != nil { + log.Fatalf("error in command: %s", err) + } return }