From 84091f398d840fbc9d83aed2be8bf718d140607d Mon Sep 17 00:00:00 2001 From: Dan Anglin Date: Tue, 13 Aug 2024 14:53:26 +0100 Subject: [PATCH] feat: add Enbas CLI schema and code generator Summary: - Created a custom CLI schema for Enbas which will act as the Source of Truth for code and document generation. - Created a code generator which uses the schema to generate the executor definitions and code in the internal usage package. Changes: - Created the Enbas CLI schema as the Source of Truth for Enbas. - Created the code generator that generates the executor definitions and code in the usage package. - Regenerated the executor definitions using the code generator. - Moved the custom flag value types to the new internal flag package. - Created a new flag value type for the bool pointer to replace the flag.BoolFunc() used for the sensitive and no-color flags. - Moved the version and build variables to the new internal version package to simplify the version executor. - Created a new usage package and moved the usage functions there. - Changed the type of the account-name flag from string to the internal StringSliceValue type. --- cmd/enbas-codegen/main.go | 160 +++++ cmd/enbas-codegen/schema.go | 72 ++ .../templates/executor/execute.go.gotmpl | 109 +++ .../templates/executor/executors.go.gotmpl | 109 +++ .../templates/usage/summaries.go.gotmpl | 18 + cmd/enbas/main.go | 205 +----- internal/executor/accept.go | 40 ++ internal/executor/accept_or_reject.go | 91 --- internal/executor/account.go | 88 ++- internal/executor/add.go | 77 +-- internal/executor/block.go | 40 ++ internal/executor/block_or_unblock.go | 95 --- internal/executor/codegen.go | 3 + internal/executor/commands.go | 78 --- internal/executor/create.go | 85 +-- internal/executor/delete.go | 28 - internal/executor/edit.go | 32 - internal/executor/execute.go | 160 +++++ internal/executor/executor.go | 21 - internal/executor/executors.go | 641 ++++++++++++++++++ internal/executor/flags.go | 130 +--- internal/executor/follow.go | 46 ++ internal/executor/follow_or_unfollow.go | 101 --- internal/executor/init.go | 22 - internal/executor/login.go | 26 - internal/executor/mute.go | 45 ++ internal/executor/mute_or_unmute.go | 108 --- internal/executor/reject.go | 40 ++ internal/executor/remove.go | 59 +- internal/executor/show.go | 90 +-- internal/executor/switch.go | 43 +- internal/executor/unblock.go | 40 ++ internal/executor/unfollow.go | 40 ++ internal/executor/unmute.go | 40 ++ internal/executor/version.go | 46 +- internal/executor/whoami.go | 28 +- internal/flag/boolptrvalue.go | 36 + internal/flag/intslice.go | 49 ++ internal/flag/stringslice.go | 31 + internal/flag/timedurationvalue.go | 31 + internal/printer/version.go | 14 +- cmd/enbas/usage.go => internal/usage/app.go | 10 +- internal/usage/codegen.go | 3 + .../{executor/usage.go => usage/executor.go} | 6 +- internal/usage/summaries.go | 28 + internal/version/version.go | 8 + magefiles/mage.go | 15 +- schema/enbas_cli_schema.json | 440 ++++++++++++ 48 files changed, 2386 insertions(+), 1341 deletions(-) create mode 100644 cmd/enbas-codegen/main.go create mode 100644 cmd/enbas-codegen/schema.go create mode 100644 cmd/enbas-codegen/templates/executor/execute.go.gotmpl create mode 100644 cmd/enbas-codegen/templates/executor/executors.go.gotmpl create mode 100644 cmd/enbas-codegen/templates/usage/summaries.go.gotmpl create mode 100644 internal/executor/accept.go delete mode 100644 internal/executor/accept_or_reject.go create mode 100644 internal/executor/block.go delete mode 100644 internal/executor/block_or_unblock.go create mode 100644 internal/executor/codegen.go delete mode 100644 internal/executor/commands.go create mode 100644 internal/executor/execute.go delete mode 100644 internal/executor/executor.go create mode 100644 internal/executor/executors.go create mode 100644 internal/executor/follow.go delete mode 100644 internal/executor/follow_or_unfollow.go create mode 100644 internal/executor/mute.go delete mode 100644 internal/executor/mute_or_unmute.go create mode 100644 internal/executor/reject.go create mode 100644 internal/executor/unblock.go create mode 100644 internal/executor/unfollow.go create mode 100644 internal/executor/unmute.go create mode 100644 internal/flag/boolptrvalue.go create mode 100644 internal/flag/intslice.go create mode 100644 internal/flag/stringslice.go create mode 100644 internal/flag/timedurationvalue.go rename cmd/enbas/usage.go => internal/usage/app.go (82%) create mode 100644 internal/usage/codegen.go rename internal/{executor/usage.go => usage/executor.go} (83%) create mode 100644 internal/usage/summaries.go create mode 100644 internal/version/version.go create mode 100644 schema/enbas_cli_schema.json diff --git a/cmd/enbas-codegen/main.go b/cmd/enbas-codegen/main.go new file mode 100644 index 0000000..b16e60b --- /dev/null +++ b/cmd/enbas-codegen/main.go @@ -0,0 +1,160 @@ +package main + +import ( + "embed" + "errors" + "flag" + "fmt" + "os" + "os/exec" + "strings" + "text/template" + "unicode" +) + +func main() { + var ( + enbasCLISchemaFilepath string + packageName string + ) + + flag.StringVar(&enbasCLISchemaFilepath, "path-to-enbas-cli-schema", "", "The path to the Enbas CLI schema file") + flag.StringVar(&packageName, "package", "", "The name of the internal package") + flag.Parse() + + schema, err := newEnbasCLISchemaFromFile(enbasCLISchemaFilepath) + if err != nil { + fmt.Printf("ERROR: Unable to read the schema file: %v.\n", err) + } + + if err := generateExecutors(schema, packageName); err != nil { + fmt.Printf("ERROR: Unable to generate the executors: %v.\n", err) + } +} + +//go:embed templates/* +var executorTemplates embed.FS + +var errNoPackageFlag = errors.New("the --package flag must be used") + +func generateExecutors(schema enbasCLISchema, packageName string) error { + if packageName == "" { + return errNoPackageFlag + } + + dirName := "templates/" + packageName + + fsDir, err := executorTemplates.ReadDir(dirName) + if err != nil { + return fmt.Errorf("unable to read the template directory in the file system (FS): %w", err) + } + + funcMap := template.FuncMap{ + "capitalise": capitalise, + "flagFieldName": flagFieldName, + "getFlagType": schema.Flags.getType, + "getFlagDescription": schema.Flags.getDescription, + "internalFlagValue": internalFlagValue, + } + + for _, obj := range fsDir { + templateFilename := obj.Name() + + if !strings.HasSuffix(templateFilename, ".go.gotmpl") { + continue + } + + if err := func() error { + tmpl := template.Must(template.New(templateFilename). + Funcs(funcMap). + ParseFS(executorTemplates, dirName+"/"+templateFilename), + ) + + output := strings.TrimSuffix(templateFilename, ".gotmpl") + + file, err := os.Create(output) + if err != nil { + return fmt.Errorf("unable to create the output file: %w", err) + } + defer file.Close() + + if err := tmpl.Execute(file, schema.Commands); err != nil { + return fmt.Errorf("unable to generate the code from the template: %w", err) + } + + if err := runGoImports(output); err != nil { + return fmt.Errorf("unable to run goimports: %w", err) + } + + return nil + }(); err != nil { + return fmt.Errorf("received an error after attempting to generate the code for %q: %w", templateFilename, err) + } + } + + return nil +} + +func runGoImports(path string) error { + imports := exec.Command("goimports", "-w", path) + + if err := imports.Run(); err != nil { + return fmt.Errorf("received an error after running goimports: %w", err) + } + + return nil +} + +func capitalise(str string) string { + runes := []rune(str) + + runes[0] = unicode.ToUpper(runes[0]) + + return string(runes) +} + +func flagFieldName(flagRef enbasCLISchemaFlagReference) string { + if flagRef.FieldName != "" { + return flagRef.FieldName + } + + return convertFlagToMixedCaps(flagRef.Flag) +} + +func convertFlagToMixedCaps(value string) string { + var builder strings.Builder + + runes := []rune(value) + numRunes := len(runes) + cursor := 0 + + for cursor < numRunes { + if runes[cursor] != '-' { + builder.WriteRune(runes[cursor]) + + cursor++ + } else { + if cursor != numRunes-1 && unicode.IsLower(runes[cursor+1]) { + builder.WriteRune(unicode.ToUpper(runes[cursor+1])) + cursor += 2 + } else { + cursor++ + } + } + } + + return builder.String() +} + +func internalFlagValue(flagType string) bool { + internalFlagValues := map[string]struct{}{ + "StringSliceValue": {}, + "IntSliceValue": {}, + "TimeDurationValue": {}, + "BoolPtrValue": {}, + } + + _, exists := internalFlagValues[flagType] + + return exists +} diff --git a/cmd/enbas-codegen/schema.go b/cmd/enbas-codegen/schema.go new file mode 100644 index 0000000..3a71559 --- /dev/null +++ b/cmd/enbas-codegen/schema.go @@ -0,0 +1,72 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" +) + +type enbasCLISchema struct { + Flags enbasCLISchemaFlagMap `json:"flags"` + Commands map[string]enbasCLISchemaCommand `json:"commands"` +} + +func newEnbasCLISchemaFromFile(path string) (enbasCLISchema, error) { + file, err := os.Open(path) + if err != nil { + return enbasCLISchema{}, fmt.Errorf("unable to open the schema file: %w", err) + } + defer file.Close() + + var schema enbasCLISchema + + if err := json.NewDecoder(file).Decode(&schema); err != nil { + return enbasCLISchema{}, fmt.Errorf("unable to decode the JSON data: %w", err) + } + + return schema, nil +} + +type enbasCLISchemaFlag struct { + Type string `json:"type"` + Description string `json:"description"` +} + +type enbasCLISchemaFlagMap map[string]enbasCLISchemaFlag + +func (e enbasCLISchemaFlagMap) getType(name string) string { + flag, ok := e[name] + if !ok { + return "UNKNOWN TYPE" + } + + return flag.Type +} + +func (e enbasCLISchemaFlagMap) getDescription(name string) string { + flag, ok := e[name] + if !ok { + return "UNKNOWN DESCRIPTION" + } + + return flag.Description +} + +type enbasCLISchemaCommand struct { + AdditionalFields []enbasCLISchemaAdditionalFields `json:"additionalFields"` + Flags []enbasCLISchemaFlagReference `json:"flags"` + Summary string `json:"summary"` + UseConfig bool `json:"useConfig"` + UsePrinter bool `json:"usePrinter"` +} + +type enbasCLISchemaFlagReference struct { + Flag string `json:"flag"` + FieldName string `json:"fieldName"` + Default string `json:"default"` +} + +type enbasCLISchemaAdditionalFields struct { + Name string `json:"name"` + Type string `json:"type"` +} diff --git a/cmd/enbas-codegen/templates/executor/execute.go.gotmpl b/cmd/enbas-codegen/templates/executor/execute.go.gotmpl new file mode 100644 index 0000000..7150211 --- /dev/null +++ b/cmd/enbas-codegen/templates/executor/execute.go.gotmpl @@ -0,0 +1,109 @@ +{{- /* vim: set noexpandtab : */ -}} +{{- /* vim: set tabstop=8 : */ -}} +{{- /* vim: set shiftwidth=8 : */ -}} +{{- /* vim: set softtabstop=8 : */ -}} +/* + This file is generated by the enbas-codegen + DO NOT EDIT. +*/ +{{ print "" }} +package executor +{{ print "" }} +{{ print "" }} +import "fmt" +import "codeflow.dananglin.me.uk/apollo/enbas/internal/config" +import "codeflow.dananglin.me.uk/apollo/enbas/internal/printer" +{{ print "" }} +{{ print "" }} +func Execute( + command string, + args []string, + noColor bool, + configDir string, +) error { + var ( + enbasConfig *config.Config + enbasPrinter *printer.Printer + err error + ) + + switch command { + case "init", "version": + enbasPrinter = printer.NewPrinter(noColor, "", 0) + default: + enbasConfig, err = config.NewConfigFromFile(configDir) + if err != nil { + enbasPrinter = printer.NewPrinter(noColor, "", 0) + enbasPrinter.PrintFailure("unable to load the configuration: " + err.Error() + ".") + + return err + } + + enbasPrinter = printer.NewPrinter( + noColor, + enbasConfig.Integrations.Pager, + enbasConfig.LineWrapMaxWidth, + ) + } + + if err = execute( + command, + args, + enbasPrinter, + enbasConfig, + configDir, + ); err != nil { + enbasPrinter.PrintFailure("(" + command + ") " + err.Error() + ".") + + return err + } + + return nil +} +{{ print "" }} +{{ print "" }} +func execute( + command string, + args []string, + enbasPrinter *printer.Printer, + enbasConfig *config.Config, + configDir string, +) error { + executorMap := map[string]Executor{ + {{- range $name, $command := . -}} + {{- $new_executor_function_name := capitalise $name | printf "New%sExecutor" -}} + {{ print "" }} + {{ printf "%q" $name }}: {{ $new_executor_function_name }}( + {{- if $command.UsePrinter -}} + {{ print "" }} + enbasPrinter, + {{- end -}} + {{- if $command.UseConfig -}} + {{ print "" }} + enbasConfig, + {{- end -}} + {{- range $field := $command.AdditionalFields -}} + {{ print "" }} + {{ $field.Name }}, + {{- end -}} + {{ print "" }} + ), + {{- end -}} + {{ print "" }} + } + + exe, ok := executorMap[command] + if !ok { + return UnknownCommandError{Command: command} + } + + if err := exe.Parse(args); err != nil { + return fmt.Errorf("flag parsing error: %w", err) + } + + if err := exe.Execute(); err != nil { + return fmt.Errorf("execution error: %w", err) + } + + return nil +} diff --git a/cmd/enbas-codegen/templates/executor/executors.go.gotmpl b/cmd/enbas-codegen/templates/executor/executors.go.gotmpl new file mode 100644 index 0000000..f95b146 --- /dev/null +++ b/cmd/enbas-codegen/templates/executor/executors.go.gotmpl @@ -0,0 +1,109 @@ +{{- /* vim: set noexpandtab : */ -}} +{{- /* vim: set tabstop=8 : */ -}} +{{- /* vim: set shiftwidth=8 : */ -}} +{{- /* vim: set softtabstop=8 : */ -}} +/* + This file is generated by the enbas-codegen + DO NOT EDIT. +*/ +{{ print "" }} +package executor +{{ print "" }} +{{ print "" }} +import internalFlag "codeflow.dananglin.me.uk/apollo/enbas/internal/flag" +import "codeflow.dananglin.me.uk/apollo/enbas/internal/usage" +{{ print "" }} +{{ print "" }} +type Executor interface { + Name() string + Parse(args []string) error + Execute() error +} +{{ range $name, $command := . }} +{{- $struct_name := capitalise $name | printf "%sExecutor" -}} +{{- $new_executor_function_name := capitalise $name | printf "New%sExecutor" -}} +{{ print "" }} +// {{ $struct_name }} is the executor for the {{ $name }} command. +type {{ $struct_name }} struct { + *flag.FlagSet + {{- if $command.UsePrinter }} + printer *printer.Printer + {{- end }} + {{- if $command.UseConfig }} + config *config.Config + {{- end }} + {{- range $flag := $command.Flags -}} + {{- $flag_type := getFlagType $flag.Flag -}} + {{- if internalFlagValue $flag_type -}} + {{ print "" }} + {{ flagFieldName $flag }} internalFlag.{{ $flag_type }} + {{- else -}} + {{ print "" }} + {{ flagFieldName $flag }} {{ $flag_type }} + {{- end -}} + {{- end -}} + {{- range $field := $command.AdditionalFields -}} + {{ print "" }} + {{ $field.Name }} {{ $field.Type }} + {{- end -}} + {{ print "" }} +} + +func {{ $new_executor_function_name }}( + {{- if $command.UsePrinter }} + printer *printer.Printer, + {{- end }} + {{- if $command.UseConfig }} + config *config.Config, + {{- end }} + {{- range $field := $command.AdditionalFields -}} + {{ print "" }} + {{ $field.Name }} {{ $field.Type }}, + {{- end -}} + {{ print "" }} +) *{{ $struct_name }} { + exe := {{ $struct_name }}{ + FlagSet: flag.NewFlagSet({{ printf "%q" $name }}, flag.ExitOnError), + {{- if $command.UsePrinter }} + printer: printer, + {{- end }} + {{- if $command.UseConfig }} + config: config, + {{- end }} + {{- range $flag := $command.Flags -}} + {{- $flag_type := getFlagType $flag.Flag -}} + {{- if internalFlagValue $flag_type -}} + {{ print "" }} + {{ flagFieldName $flag }}: internalFlag.New{{ $flag_type }}(), + {{- end -}} + {{- end -}} + {{- range $field := $command.AdditionalFields -}} + {{ print "" }} + {{ $field.Name }}: {{ $field.Name }}, + {{- end -}} + {{ print "" }} + } + {{ print "" }} + exe.Usage = usage.ExecutorUsageFunc({{ printf "%q" $name }}, {{ printf "%q" $command.Summary }}, exe.FlagSet) + {{ print "" }} + {{- range $flag := $command.Flags -}} + {{- $flag_type := getFlagType $flag.Flag -}} + {{- if eq $flag_type "string" -}} + {{ print "" }} + exe.StringVar(&exe.{{ flagFieldName $flag }}, {{ printf "%q" $flag.Flag }}, {{ printf "%q" $flag.Default }}, {{ getFlagDescription $flag.Flag | printf "%q" }}) + {{- else if eq $flag_type "bool" -}} + {{ print "" }} + exe.BoolVar(&exe.{{ flagFieldName $flag }}, {{ printf "%q" $flag.Flag }}, {{ $flag.Default }}, {{ getFlagDescription $flag.Flag | printf "%q" }}) + {{- else if eq $flag_type "int" -}} + {{ print "" }} + exe.IntVar(&exe.{{ flagFieldName $flag }}, {{ printf "%q" $flag.Flag }}, {{ $flag.Default }}, {{ getFlagDescription $flag.Flag | printf "%q" }}) + {{- else if internalFlagValue $flag_type -}} + {{ print "" }} + exe.Var(&exe.{{ flagFieldName $flag }}, {{ printf "%q" $flag.Flag }}, {{ getFlagDescription $flag.Flag | printf "%q" }}) + {{- end -}} + {{- end -}} + {{ print "" }} + {{ print "" }} + return &exe +} +{{ end }} diff --git a/cmd/enbas-codegen/templates/usage/summaries.go.gotmpl b/cmd/enbas-codegen/templates/usage/summaries.go.gotmpl new file mode 100644 index 0000000..eb88287 --- /dev/null +++ b/cmd/enbas-codegen/templates/usage/summaries.go.gotmpl @@ -0,0 +1,18 @@ +{{- /* vim: set noexpandtab : */ -}} +{{- /* vim: set tabstop=8 : */ -}} +{{- /* vim: set shiftwidth=8 : */ -}} +{{- /* vim: set softtabstop=8 : */ -}} +/* + This file is generated by the enbas-codegen + DO NOT EDIT. +*/ +{{ print "" }} +package usage +{{ print "" }} +var summaries = map[string]string { + {{- range $name, $command := . -}} + {{ print "" }} + {{ printf "%q" $name }}: {{ printf "%q" $command.Summary }}, + {{- end -}} +{{ print "" }} +} diff --git a/cmd/enbas/main.go b/cmd/enbas/main.go index f8168a9..cd1ddd5 100644 --- a/cmd/enbas/main.go +++ b/cmd/enbas/main.go @@ -2,20 +2,11 @@ package main import ( "flag" - "fmt" "os" - "strconv" - "codeflow.dananglin.me.uk/apollo/enbas/internal/config" "codeflow.dananglin.me.uk/apollo/enbas/internal/executor" - "codeflow.dananglin.me.uk/apollo/enbas/internal/printer" -) - -var ( - binaryVersion string //nolint:gochecknoglobals - buildTime string //nolint:gochecknoglobals - goVersion string //nolint:gochecknoglobals - gitCommit string //nolint:gochecknoglobals + internalFlag "codeflow.dananglin.me.uk/apollo/enbas/internal/flag" + "codeflow.dananglin.me.uk/apollo/enbas/internal/usage" ) func main() { @@ -26,24 +17,14 @@ func main() { func run() error { var ( - configDir string - noColor *bool + configDir string + noColorFlag internalFlag.BoolPtrValue ) flag.StringVar(&configDir, "config-dir", "", "Specify your config directory") - flag.BoolFunc("no-color", "Disable ANSI colour output when displaying text on screen", func(value string) error { - boolVal, err := strconv.ParseBool(value) - if err != nil { - return fmt.Errorf("unable to parse %q as a boolean: %w", value, err) - } + flag.Var(&noColorFlag, "no-color", "Disable ANSI colour output when displaying text on screen") - noColor = new(bool) - *noColor = boolVal - - return nil - }) - - flag.Usage = usageFunc(executor.CommandSummaryMap()) + flag.Usage = usage.AppUsageFunc() flag.Parse() @@ -53,176 +34,16 @@ func run() error { return nil } - // If NoColor is still unspecified, - // check to see if the NO_COLOR environment variable is set - if noColor == nil { - noColor = new(bool) - if os.Getenv("NO_COLOR") != "" { - *noColor = true - } else { - *noColor = false - } + var noColor bool + + if noColorFlag.Value != nil { + noColor = *noColorFlag.Value + } else if os.Getenv("NO_COLOR") != "" { + noColor = true } command := flag.Arg(0) args := flag.Args()[1:] - var ( - enbasConfig *config.Config - enbasPrinter *printer.Printer - err error - ) - - switch command { - case executor.CommandInit, executor.CommandVersion: - enbasPrinter = printer.NewPrinter(*noColor, "", 0) - default: - enbasConfig, err = config.NewConfigFromFile(configDir) - if err != nil { - enbasPrinter = printer.NewPrinter(*noColor, "", 0) - enbasPrinter.PrintFailure("unable to load the configuration: " + err.Error() + ".") - - return err - } - - enbasPrinter = printer.NewPrinter(*noColor, enbasConfig.Integrations.Pager, enbasConfig.LineWrapMaxWidth) - } - - executorMap := map[string]executor.Executor{ - executor.CommandAccept: executor.NewAcceptOrRejectExecutor( - enbasPrinter, - enbasConfig, - executor.CommandAccept, - executor.CommandSummaryLookup(executor.CommandAccept), - ), - executor.CommandAdd: executor.NewAddExecutor( - enbasPrinter, - enbasConfig, - executor.CommandAdd, - executor.CommandSummaryLookup(executor.CommandAdd), - ), - executor.CommandBlock: executor.NewBlockOrUnblockExecutor( - enbasPrinter, - enbasConfig, - executor.CommandBlock, - executor.CommandSummaryLookup(executor.CommandBlock), - ), - executor.CommandCreate: executor.NewCreateExecutor( - enbasPrinter, - enbasConfig, - executor.CommandCreate, - executor.CommandSummaryLookup(executor.CommandCreate), - ), - executor.CommandDelete: executor.NewDeleteExecutor( - enbasPrinter, - enbasConfig, - executor.CommandDelete, - executor.CommandSummaryLookup(executor.CommandDelete), - ), - executor.CommandEdit: executor.NewEditExecutor( - enbasPrinter, - enbasConfig, - executor.CommandEdit, - executor.CommandSummaryLookup(executor.CommandEdit), - ), - executor.CommandFollow: executor.NewFollowOrUnfollowExecutor( - enbasPrinter, - enbasConfig, - executor.CommandFollow, - executor.CommandSummaryLookup(executor.CommandFollow), - ), - executor.CommandInit: executor.NewInitExecutor( - enbasPrinter, - configDir, - executor.CommandInit, - executor.CommandSummaryLookup(executor.CommandInit), - ), - executor.CommandLogin: executor.NewLoginExecutor( - enbasPrinter, - enbasConfig, - executor.CommandLogin, - executor.CommandSummaryLookup(executor.CommandLogin), - ), - executor.CommandMute: executor.NewMuteOrUnmuteExecutor( - enbasPrinter, - enbasConfig, - executor.CommandMute, - executor.CommandSummaryLookup(executor.CommandMute), - ), - executor.CommandReject: executor.NewAcceptOrRejectExecutor( - enbasPrinter, - enbasConfig, - executor.CommandReject, - executor.CommandSummaryLookup(executor.CommandReject), - ), - executor.CommandRemove: executor.NewRemoveExecutor( - enbasPrinter, - enbasConfig, - executor.CommandRemove, - executor.CommandSummaryLookup(executor.CommandRemove), - ), - executor.CommandSwitch: executor.NewSwitchExecutor( - enbasPrinter, - enbasConfig, - executor.CommandSwitch, - executor.CommandSummaryLookup(executor.CommandSwitch), - ), - executor.CommandUnfollow: executor.NewFollowOrUnfollowExecutor( - enbasPrinter, - enbasConfig, - executor.CommandUnfollow, - executor.CommandSummaryLookup(executor.CommandUnfollow), - ), - executor.CommandUnmute: executor.NewMuteOrUnmuteExecutor( - enbasPrinter, - enbasConfig, - executor.CommandUnmute, - executor.CommandSummaryLookup(executor.CommandUnmute), - ), - executor.CommandUnblock: executor.NewBlockOrUnblockExecutor( - enbasPrinter, - enbasConfig, - executor.CommandUnblock, - executor.CommandSummaryLookup(executor.CommandUnblock), - ), - executor.CommandShow: executor.NewShowExecutor( - enbasPrinter, - enbasConfig, - executor.CommandShow, - executor.CommandSummaryLookup(executor.CommandShow), - ), - executor.CommandVersion: executor.NewVersionExecutor( - enbasPrinter, - executor.CommandVersion, - executor.CommandSummaryLookup(executor.CommandVersion), - binaryVersion, - buildTime, - goVersion, - gitCommit, - ), - executor.CommandWhoami: executor.NewWhoAmIExecutor( - enbasPrinter, - enbasConfig, - executor.CommandWhoami, - executor.CommandSummaryLookup(executor.CommandWhoami), - ), - } - - exe, ok := executorMap[command] - if !ok { - err = executor.UnknownCommandError{Command: command} - - enbasPrinter.PrintFailure(err.Error() + ".") - flag.Usage() - - return err - } - - if err = executor.Execute(exe, args); err != nil { - enbasPrinter.PrintFailure("(" + command + ") " + err.Error() + ".") - - return err - } - - return nil + return executor.Execute(command, args, noColor, configDir) } diff --git a/internal/executor/accept.go b/internal/executor/accept.go new file mode 100644 index 0000000..158927d --- /dev/null +++ b/internal/executor/accept.go @@ -0,0 +1,40 @@ +package executor + +import ( + "fmt" + + "codeflow.dananglin.me.uk/apollo/enbas/internal/client" +) + +func (a *AcceptExecutor) Execute() error { + funcMap := map[string]func(*client.Client) error{ + resourceFollowRequest: a.acceptFollowRequest, + } + + doFunc, ok := funcMap[a.resourceType] + if !ok { + return UnsupportedTypeError{resourceType: a.resourceType} + } + + gtsClient, err := client.NewClientFromFile(a.config.CredentialsFile) + if err != nil { + return fmt.Errorf("unable to create the GoToSocial client: %w", err) + } + + return doFunc(gtsClient) +} + +func (a *AcceptExecutor) acceptFollowRequest(gtsClient *client.Client) error { + accountID, err := getAccountID(gtsClient, false, a.accountName, a.config.CredentialsFile) + if err != nil { + return fmt.Errorf("received an error while getting the account ID: %w", err) + } + + if err := gtsClient.AcceptFollowRequest(accountID); err != nil { + return fmt.Errorf("unable to accept the follow request: %w", err) + } + + a.printer.PrintSuccess("Successfully accepted the follow request.") + + return nil +} diff --git a/internal/executor/accept_or_reject.go b/internal/executor/accept_or_reject.go deleted file mode 100644 index 71b3ff7..0000000 --- a/internal/executor/accept_or_reject.go +++ /dev/null @@ -1,91 +0,0 @@ -package executor - -import ( - "flag" - "fmt" - - "codeflow.dananglin.me.uk/apollo/enbas/internal/client" - "codeflow.dananglin.me.uk/apollo/enbas/internal/config" - "codeflow.dananglin.me.uk/apollo/enbas/internal/printer" -) - -type AcceptOrRejectExecutor struct { - *flag.FlagSet - - printer *printer.Printer - config *config.Config - resourceType string - accountName string - command string -} - -func NewAcceptOrRejectExecutor(enbasPrinter *printer.Printer, config *config.Config, name, summary string) *AcceptOrRejectExecutor { - acceptExe := AcceptOrRejectExecutor{ - FlagSet: flag.NewFlagSet(name, flag.ExitOnError), - - printer: enbasPrinter, - config: config, - command: name, - } - - acceptExe.StringVar(&acceptExe.resourceType, flagType, "", "Specify the type of resource to accept or reject") - acceptExe.StringVar(&acceptExe.accountName, flagAccountName, "", "Specify the account name in full (username@domain)") - - acceptExe.Usage = commandUsageFunc(name, summary, acceptExe.FlagSet) - - return &acceptExe -} - -func (a *AcceptOrRejectExecutor) Execute() error { - funcMap := map[string]func(*client.Client) error{ - resourceFollowRequest: a.acceptOrRejectFollowRequest, - } - - doFunc, ok := funcMap[a.resourceType] - if !ok { - return UnsupportedTypeError{resourceType: a.resourceType} - } - - gtsClient, err := client.NewClientFromFile(a.config.CredentialsFile) - if err != nil { - return fmt.Errorf("unable to create the GoToSocial client: %w", err) - } - - return doFunc(gtsClient) -} - -func (a *AcceptOrRejectExecutor) acceptOrRejectFollowRequest(gtsClient *client.Client) error { - accountID, err := getAccountID(gtsClient, false, a.accountName, a.config.CredentialsFile) - if err != nil { - return fmt.Errorf("received an error while getting the account ID: %w", err) - } - - switch a.command { - case CommandAccept: - return a.acceptFollowRequest(gtsClient, accountID) - case CommandReject: - return a.rejectFollowRequest(gtsClient, accountID) - default: - return nil - } -} - -func (a *AcceptOrRejectExecutor) acceptFollowRequest(gtsClient *client.Client, accountID string) error { - if err := gtsClient.AcceptFollowRequest(accountID); err != nil { - return fmt.Errorf("unable to accept the follow request: %w", err) - } - - a.printer.PrintSuccess("Successfully accepted the follow request.") - - return nil -} - -func (a *AcceptOrRejectExecutor) rejectFollowRequest(gtsClient *client.Client, accountID string) error { - if err := gtsClient.RejectFollowRequest(accountID); err != nil { - return fmt.Errorf("unable to reject the follow request: %w", err) - } - - a.printer.PrintSuccess("Successfully rejected the follow request.") - - return nil -} diff --git a/internal/executor/account.go b/internal/executor/account.go index 4185d3e..6660a37 100644 --- a/internal/executor/account.go +++ b/internal/executor/account.go @@ -5,49 +5,51 @@ import ( "codeflow.dananglin.me.uk/apollo/enbas/internal/client" "codeflow.dananglin.me.uk/apollo/enbas/internal/config" + internalFlag "codeflow.dananglin.me.uk/apollo/enbas/internal/flag" "codeflow.dananglin.me.uk/apollo/enbas/internal/model" ) -func getAccountID(gtsClient *client.Client, myAccount bool, accountName, path string) (string, error) { +func getAccountID( + gtsClient *client.Client, + myAccount bool, + accountNames internalFlag.StringSliceValue, + credentialsFile string, +) (string, error) { + account, err := getAccount(gtsClient, myAccount, accountNames, credentialsFile) + if err != nil { + return "", fmt.Errorf("unable to get the account information: %w", err) + } + + return account.ID, nil +} + +func getAccount( + gtsClient *client.Client, + myAccount bool, + accountNames internalFlag.StringSliceValue, + credentialsFile string, +) (model.Account, error) { var ( - accountID string - err error + account model.Account + err error ) switch { case myAccount: - accountID, err = getMyAccountID(gtsClient, path) + account, err = getMyAccount(gtsClient, credentialsFile) if err != nil { - return "", fmt.Errorf("unable to get your account ID: %w", err) + return account, fmt.Errorf("unable to get your account ID: %w", err) } - case accountName != "": - accountID, err = getTheirAccountID(gtsClient, accountName) + case !accountNames.Empty(): + account, err = getOtherAccount(gtsClient, accountNames) if err != nil { - return "", fmt.Errorf("unable to get their account ID: %w", err) + return account, fmt.Errorf("unable to get the account ID: %w", err) } default: - return "", NoAccountSpecifiedError{} + return account, NoAccountSpecifiedError{} } - return accountID, nil -} - -func getTheirAccountID(gtsClient *client.Client, accountURI string) (string, error) { - account, err := getAccount(gtsClient, accountURI) - if err != nil { - return "", fmt.Errorf("unable to retrieve your account: %w", err) - } - - return account.ID, nil -} - -func getMyAccountID(gtsClient *client.Client, path string) (string, error) { - account, err := getMyAccount(gtsClient, path) - if err != nil { - return "", fmt.Errorf("received an error while getting your account details: %w", err) - } - - return account.ID, nil + return account, nil } func getMyAccount(gtsClient *client.Client, path string) (model.Account, error) { @@ -58,7 +60,7 @@ func getMyAccount(gtsClient *client.Client, path string) (model.Account, error) accountURI := authConfig.CurrentAccount - account, err := getAccount(gtsClient, accountURI) + account, err := gtsClient.GetAccount(accountURI) if err != nil { return model.Account{}, fmt.Errorf("unable to retrieve your account: %w", err) } @@ -66,11 +68,35 @@ func getMyAccount(gtsClient *client.Client, path string) (model.Account, error) return account, nil } -func getAccount(gtsClient *client.Client, accountURI string) (model.Account, error) { - account, err := gtsClient.GetAccount(accountURI) +func getOtherAccount(gtsClient *client.Client, accountNames internalFlag.StringSliceValue) (model.Account, error) { + expectedNumAccountNames := 1 + if !accountNames.ExpectedLength(expectedNumAccountNames) { + return model.Account{}, fmt.Errorf( + "received an unexpected number of account names: want %d", + expectedNumAccountNames, + ) + } + + account, err := gtsClient.GetAccount(accountNames[0]) if err != nil { return model.Account{}, fmt.Errorf("unable to retrieve the account details: %w", err) } return account, nil } + +func getOtherAccounts(gtsClient *client.Client, accountNames internalFlag.StringSliceValue) ([]model.Account, error) { + numAccountNames := len(accountNames) + accounts := make([]model.Account, numAccountNames) + + for ind := 0; ind < numAccountNames; ind++ { + var err error + + accounts[ind], err = gtsClient.GetAccount(accountNames[ind]) + if err != nil { + return nil, fmt.Errorf("unable to retrieve the account information for %s: %w", accountNames[ind], err) + } + } + + return accounts, nil +} diff --git a/internal/executor/add.go b/internal/executor/add.go index a9a7231..b46d03b 100644 --- a/internal/executor/add.go +++ b/internal/executor/add.go @@ -2,54 +2,11 @@ package executor import ( "errors" - "flag" "fmt" "codeflow.dananglin.me.uk/apollo/enbas/internal/client" - "codeflow.dananglin.me.uk/apollo/enbas/internal/config" - "codeflow.dananglin.me.uk/apollo/enbas/internal/printer" ) -type AddExecutor struct { - *flag.FlagSet - - printer *printer.Printer - config *config.Config - resourceType string - toResourceType string - listID string - statusID string - pollID string - choices MultiIntFlagValue - accountNames MultiStringFlagValue - content string -} - -func NewAddExecutor(printer *printer.Printer, config *config.Config, name, summary string) *AddExecutor { - emptyArr := make([]string, 0, 3) - - addExe := AddExecutor{ - FlagSet: flag.NewFlagSet(name, flag.ExitOnError), - - printer: printer, - config: config, - accountNames: MultiStringFlagValue(emptyArr), - } - - addExe.StringVar(&addExe.resourceType, flagType, "", "Specify the resource type to add (e.g. account, note)") - addExe.StringVar(&addExe.toResourceType, flagTo, "", "Specify the target resource type to add to (e.g. list, account, etc)") - addExe.StringVar(&addExe.listID, flagListID, "", "The ID of the list") - addExe.StringVar(&addExe.statusID, flagStatusID, "", "The ID of the status") - addExe.StringVar(&addExe.content, flagContent, "", "The content of the resource") - addExe.StringVar(&addExe.pollID, flagPollID, "", "The ID of the poll") - addExe.Var(&addExe.accountNames, flagAccountName, "The name of the account") - addExe.Var(&addExe.choices, flagVote, "Add a vote to an option in a poll") - - addExe.Usage = commandUsageFunc(name, summary, addExe.FlagSet) - - return &addExe -} - func (a *AddExecutor) Execute() error { if a.toResourceType == "" { return FlagNotSetError{flagText: flagTo} @@ -97,28 +54,28 @@ func (a *AddExecutor) addAccountsToList(gtsClient *client.Client) error { return FlagNotSetError{flagText: flagListID} } - if len(a.accountNames) == 0 { + if a.accountNames.Empty() { return NoAccountSpecifiedError{} } - accountIDs := make([]string, len(a.accountNames)) + accounts, err := getOtherAccounts(gtsClient, a.accountNames) + if err != nil { + return fmt.Errorf("unable to get the accounts: %w", err) + } - for ind := range a.accountNames { - accountID, err := getTheirAccountID(gtsClient, a.accountNames[ind]) - if err != nil { - return fmt.Errorf("unable to get the account ID for %s: %w", a.accountNames[ind], err) - } + accountIDs := make([]string, len(accounts)) - relationship, err := gtsClient.GetAccountRelationship(accountID) + for ind := range accounts { + relationship, err := gtsClient.GetAccountRelationship(accounts[ind].ID) if err != nil { - return fmt.Errorf("unable to get your relationship to %s: %w", a.accountNames[ind], err) + return fmt.Errorf("unable to get your relationship to %s: %w", accounts[ind].Acct, err) } if !relationship.Following { - return NotFollowingError{Account: a.accountNames[ind]} + return NotFollowingError{Account: accounts[ind].Acct} } - accountIDs[ind] = accountID + accountIDs[ind] = accounts[ind].ID } if err := gtsClient.AddAccountsToList(a.listID, accountIDs); err != nil { @@ -147,11 +104,7 @@ func (a *AddExecutor) addToAccount(gtsClient *client.Client) error { } func (a *AddExecutor) addNoteToAccount(gtsClient *client.Client) error { - if len(a.accountNames) != 1 { - return fmt.Errorf("unexpected number of accounts specified: want 1, got %d", len(a.accountNames)) - } - - accountID, err := getAccountID(gtsClient, false, a.accountNames[0], a.config.CredentialsFile) + accountID, err := getAccountID(gtsClient, false, a.accountNames, a.config.CredentialsFile) if err != nil { return fmt.Errorf("received an error while getting the account ID: %w", err) } @@ -265,7 +218,7 @@ func (a *AddExecutor) addToPoll(gtsClient *client.Client) error { } func (a *AddExecutor) addVoteToPoll(gtsClient *client.Client) error { - if len(a.choices) == 0 { + if a.votes.Empty() { return errors.New("please use --" + flagVote + " to make a choice in this poll") } @@ -278,11 +231,11 @@ func (a *AddExecutor) addVoteToPoll(gtsClient *client.Client) error { return PollClosedError{} } - if !poll.Multiple && len(a.choices) > 1 { + if !poll.Multiple && !a.votes.ExpectedLength(1) { return MultipleChoiceError{} } - if err := gtsClient.VoteInPoll(a.pollID, []int(a.choices)); err != nil { + if err := gtsClient.VoteInPoll(a.pollID, []int(a.votes)); err != nil { return fmt.Errorf("unable to add your vote(s) to the poll: %w", err) } diff --git a/internal/executor/block.go b/internal/executor/block.go new file mode 100644 index 0000000..1b0eeae --- /dev/null +++ b/internal/executor/block.go @@ -0,0 +1,40 @@ +package executor + +import ( + "fmt" + + "codeflow.dananglin.me.uk/apollo/enbas/internal/client" +) + +func (b *BlockExecutor) Execute() error { + funcMap := map[string]func(*client.Client) error{ + resourceAccount: b.blockAccount, + } + + doFunc, ok := funcMap[b.resourceType] + if !ok { + return UnsupportedTypeError{resourceType: b.resourceType} + } + + gtsClient, err := client.NewClientFromFile(b.config.CredentialsFile) + if err != nil { + return fmt.Errorf("unable to create the GoToSocial client: %w", err) + } + + return doFunc(gtsClient) +} + +func (b *BlockExecutor) blockAccount(gtsClient *client.Client) error { + accountID, err := getAccountID(gtsClient, false, b.accountName, b.config.CredentialsFile) + if err != nil { + return fmt.Errorf("received an error while getting the account ID: %w", err) + } + + if err := gtsClient.BlockAccount(accountID); err != nil { + return fmt.Errorf("unable to block the account: %w", err) + } + + b.printer.PrintSuccess("Successfully blocked the account.") + + return nil +} diff --git a/internal/executor/block_or_unblock.go b/internal/executor/block_or_unblock.go deleted file mode 100644 index 3a38a62..0000000 --- a/internal/executor/block_or_unblock.go +++ /dev/null @@ -1,95 +0,0 @@ -package executor - -import ( - "flag" - "fmt" - - "codeflow.dananglin.me.uk/apollo/enbas/internal/client" - "codeflow.dananglin.me.uk/apollo/enbas/internal/config" - "codeflow.dananglin.me.uk/apollo/enbas/internal/printer" -) - -type BlockOrUnblockExecutor struct { - *flag.FlagSet - - printer *printer.Printer - config *config.Config - resourceType string - accountName string - command string -} - -func NewBlockOrUnblockExecutor(printer *printer.Printer, config *config.Config, name, summary string) *BlockOrUnblockExecutor { - blockExe := BlockOrUnblockExecutor{ - FlagSet: flag.NewFlagSet(name, flag.ExitOnError), - - printer: printer, - config: config, - command: name, - } - - blockExe.StringVar(&blockExe.resourceType, flagType, "", "Specify the type of resource to block or unblock") - blockExe.StringVar(&blockExe.accountName, flagAccountName, "", "Specify the account name in full (username@domain)") - - blockExe.Usage = commandUsageFunc(name, summary, blockExe.FlagSet) - - return &blockExe -} - -func (b *BlockOrUnblockExecutor) Execute() error { - funcMap := map[string]func(*client.Client) error{ - resourceAccount: b.blockOrUnblockAccount, - } - - doFunc, ok := funcMap[b.resourceType] - if !ok { - return UnsupportedTypeError{resourceType: b.resourceType} - } - - gtsClient, err := client.NewClientFromFile(b.config.CredentialsFile) - if err != nil { - return fmt.Errorf("unable to create the GoToSocial client: %w", err) - } - - return doFunc(gtsClient) -} - -func (b *BlockOrUnblockExecutor) blockOrUnblockAccount(gtsClient *client.Client) error { - if b.accountName == "" { - return FlagNotSetError{flagText: flagAccountName} - } - - accountID, err := getAccountID(gtsClient, false, b.accountName, b.config.CredentialsFile) - if err != nil { - return fmt.Errorf("received an error while getting the account ID: %w", err) - } - - switch b.command { - case CommandBlock: - return b.blockAccount(gtsClient, accountID) - case CommandUnblock: - return b.unblockAccount(gtsClient, accountID) - default: - return nil - } -} - -func (b *BlockOrUnblockExecutor) blockAccount(gtsClient *client.Client, accountID string) error { - if err := gtsClient.BlockAccount(accountID); err != nil { - return fmt.Errorf("unable to block the account: %w", err) - } - - b.printer.PrintSuccess("Successfully blocked the account.") - - return nil -} - -func (b *BlockOrUnblockExecutor) unblockAccount(gtsClient *client.Client, accountID string) error { - if err := gtsClient.UnblockAccount(accountID); err != nil { - return fmt.Errorf("unable to unblock the account: %w", err) - } - - b.printer.PrintSuccess("Successfully unblocked the account.") - - return nil -} diff --git a/internal/executor/codegen.go b/internal/executor/codegen.go new file mode 100644 index 0000000..a16ca0b --- /dev/null +++ b/internal/executor/codegen.go @@ -0,0 +1,3 @@ +package executor + +//go:generate go run ../../cmd/enbas-codegen --package executor --path-to-enbas-cli-schema ../../schema/enbas_cli_schema.json diff --git a/internal/executor/commands.go b/internal/executor/commands.go deleted file mode 100644 index 6ca174e..0000000 --- a/internal/executor/commands.go +++ /dev/null @@ -1,78 +0,0 @@ -package executor - -const ( - CommandAccept string = "accept" - CommandAdd string = "add" - CommandBlock string = "block" - CommandCreate string = "create" - CommandDelete string = "delete" - CommandEdit string = "edit" - CommandFollow string = "follow" - CommandInit string = "init" - CommandLogin string = "login" - CommandMute string = "mute" - CommandReject string = "reject" - CommandRemove string = "remove" - CommandShow string = "show" - CommandSwitch string = "switch" - CommandUnblock string = "unblock" - CommandUnfollow string = "unfollow" - CommandUnmute string = "unmute" - CommandVersion string = "version" - CommandWhoami string = "whoami" - - commandAcceptSummary string = "Accept a request (e.g. a follow request)" - commandAddSummary string = "Add a resource to another resource" - commandBlockSummary string = "Block a resource (e.g. an account)" - commandCreateSummary string = "Create a specific resource" - commandDeleteSummary string = "Delete a specific resource" - commandEditSummary string = "Edit a specific resource" - commandFollowSummary string = "Follow a resource (e.g. an account)" - commandInitSummary string = "Create a new configuration file in the specified configuration directory" - commandLoginSummary string = "Login to an account on GoToSocial" - commandMuteSummary string = "Mute a resource (e.g. an account)" - commandRejectSummary string = "Reject a request (e.g. a follow request)" - commandRemoveSummary string = "Remove a resource from another resource" - commandShowSummary string = "Print details about a specified resource" - commandSwitchSummary string = "Perform a switch operation (e.g. switch logged in accounts)" - commandUnblockSummary string = "Unblock a resource (e.g. an account)" - commandUnfollowSummary string = "Unfollow a resource (e.g. an account)" - commandUnmuteSummary string = "Unmute a resource (e.g. an account)" - commandVersionSummary string = "Print the application's version and build information" - commandWhoamiSummary string = "Print the account that you are currently logged in to" -) - -func CommandSummaryMap() map[string]string { - return map[string]string{ - CommandAccept: commandAcceptSummary, - CommandAdd: commandAddSummary, - CommandBlock: commandBlockSummary, - CommandCreate: commandCreateSummary, - CommandDelete: commandDeleteSummary, - CommandEdit: commandEditSummary, - CommandFollow: commandFollowSummary, - CommandInit: commandInitSummary, - CommandLogin: commandLoginSummary, - CommandMute: commandMuteSummary, - CommandReject: commandRejectSummary, - CommandRemove: commandRemoveSummary, - CommandShow: commandShowSummary, - CommandSwitch: commandSwitchSummary, - CommandUnblock: commandUnblockSummary, - CommandUnfollow: commandUnfollowSummary, - CommandUnmute: commandUnmuteSummary, - CommandVersion: commandVersionSummary, - CommandWhoami: commandWhoamiSummary, - } -} - -func CommandSummaryLookup(command string) string { - commandMap := CommandSummaryMap() - - summary, ok := commandMap[command] - if !ok { - return "This command does not have a summary" - } - - return summary -} diff --git a/internal/executor/create.go b/internal/executor/create.go index 0360ab0..5e0e409 100644 --- a/internal/executor/create.go +++ b/internal/executor/create.go @@ -1,94 +1,13 @@ package executor import ( - "flag" "fmt" - "strconv" "codeflow.dananglin.me.uk/apollo/enbas/internal/client" - "codeflow.dananglin.me.uk/apollo/enbas/internal/config" "codeflow.dananglin.me.uk/apollo/enbas/internal/model" - "codeflow.dananglin.me.uk/apollo/enbas/internal/printer" "codeflow.dananglin.me.uk/apollo/enbas/internal/utilities" ) -type CreateExecutor struct { - *flag.FlagSet - - printer *printer.Printer - config *config.Config - addPoll bool - boostable bool - federated bool - likeable bool - pollAllowsMultipleChoices bool - pollHidesVoteCounts bool - replyable bool - sensitive *bool - content string - contentType string - fromFile string - inReplyTo string - language string - resourceType string - listTitle string - listRepliesPolicy string - spoilerText string - visibility string - pollExpiresIn TimeDurationFlagValue - pollOptions MultiStringFlagValue -} - -func NewCreateExecutor(printer *printer.Printer, config *config.Config, name, summary string) *CreateExecutor { - createExe := CreateExecutor{ - FlagSet: flag.NewFlagSet(name, flag.ExitOnError), - - printer: printer, - config: config, - } - - createExe.StringVar(&createExe.resourceType, flagType, "", "Specify the type of resource to create") - - // Flags for statuses - createExe.BoolVar(&createExe.boostable, flagEnableReposts, true, "Specify if the status can be reposted/boosted by others") - createExe.StringVar(&createExe.content, flagContent, "", "The content of the status to create") - createExe.StringVar(&createExe.contentType, flagContentType, "plain", "The type that the contents should be parsed from (valid values are plain and markdown)") - createExe.BoolVar(&createExe.federated, flagEnableFederation, true, "Specify if the status can be federated beyond the local timelines") - createExe.StringVar(&createExe.fromFile, flagFromFile, "", "The file path where to read the contents from") - createExe.StringVar(&createExe.inReplyTo, flagInReplyTo, "", "The ID of the status that you want to reply to") - createExe.StringVar(&createExe.language, flagLanguage, "", "The ISO 639 language code for this status") - createExe.BoolVar(&createExe.likeable, flagEnableLikes, true, "Specify if the status can be liked/favourited") - createExe.BoolVar(&createExe.replyable, flagEnableReplies, true, "Specify if the status can be replied to") - createExe.StringVar(&createExe.spoilerText, flagSpoilerText, "", "The text to display as the status' warning or subject") - createExe.StringVar(&createExe.visibility, flagVisibility, "", "The visibility of the posted status") - createExe.BoolFunc(flagSensitive, "Specify if the status should be marked as sensitive", func(value string) error { - boolVal, err := strconv.ParseBool(value) - if err != nil { - return fmt.Errorf("unable to parse %q as a boolean value: %w", value, err) - } - - createExe.sensitive = new(bool) - *createExe.sensitive = boolVal - - return nil - }) - - // Flags specifically for polls - createExe.BoolVar(&createExe.addPoll, flagAddPoll, false, "Add a poll to the status") - createExe.BoolVar(&createExe.pollAllowsMultipleChoices, flagPollAllowsMultipleChoices, false, "The poll allows viewers to make multiple choices in the poll") - createExe.BoolVar(&createExe.pollHidesVoteCounts, flagPollHidesVoteCounts, false, "The poll will hide the vote count until it is closed") - createExe.Var(&createExe.pollOptions, flagPollOption, "A poll option. Use this multiple times to set multiple options") - createExe.Var(&createExe.pollExpiresIn, flagPollExpiresIn, "The duration in which the poll is open for") - - // Flags for lists - createExe.StringVar(&createExe.listTitle, flagListTitle, "", "Specify the title of the list") - createExe.StringVar(&createExe.listRepliesPolicy, flagListRepliesPolicy, "list", "Specify the policy of the replies for this list (valid values are followed, list and none)") - - createExe.Usage = commandUsageFunc(name, summary, createExe.FlagSet) - - return &createExe -} - func (c *CreateExecutor) Execute() error { if c.resourceType == "" { return FlagNotSetError{flagText: flagType} @@ -179,8 +98,8 @@ func (c *CreateExecutor) createStatus(gtsClient *client.Client) error { visibility = preferences.PostingDefaultVisibility } - if c.sensitive != nil { - sensitive = *c.sensitive + if c.sensitive.Value != nil { + sensitive = *c.sensitive.Value } else { sensitive = preferences.PostingDefaultSensitive } diff --git a/internal/executor/delete.go b/internal/executor/delete.go index d88d381..0b27fc5 100644 --- a/internal/executor/delete.go +++ b/internal/executor/delete.go @@ -1,39 +1,11 @@ package executor import ( - "flag" "fmt" "codeflow.dananglin.me.uk/apollo/enbas/internal/client" - "codeflow.dananglin.me.uk/apollo/enbas/internal/config" - "codeflow.dananglin.me.uk/apollo/enbas/internal/printer" ) -type DeleteExecutor struct { - *flag.FlagSet - - printer *printer.Printer - config *config.Config - resourceType string - listID string -} - -func NewDeleteExecutor(printer *printer.Printer, config *config.Config, name, summary string) *DeleteExecutor { - deleteExe := DeleteExecutor{ - FlagSet: flag.NewFlagSet(name, flag.ExitOnError), - - printer: printer, - config: config, - } - - deleteExe.StringVar(&deleteExe.resourceType, flagType, "", "Specify the type of resource to delete") - deleteExe.StringVar(&deleteExe.listID, flagListID, "", "Specify the ID of the list to delete") - - deleteExe.Usage = commandUsageFunc(name, summary, deleteExe.FlagSet) - - return &deleteExe -} - func (d *DeleteExecutor) Execute() error { if d.resourceType == "" { return FlagNotSetError{flagText: flagType} diff --git a/internal/executor/edit.go b/internal/executor/edit.go index a5f271b..50c7247 100644 --- a/internal/executor/edit.go +++ b/internal/executor/edit.go @@ -1,44 +1,12 @@ package executor import ( - "flag" "fmt" "codeflow.dananglin.me.uk/apollo/enbas/internal/client" - "codeflow.dananglin.me.uk/apollo/enbas/internal/config" "codeflow.dananglin.me.uk/apollo/enbas/internal/model" - "codeflow.dananglin.me.uk/apollo/enbas/internal/printer" ) -type EditExecutor struct { - *flag.FlagSet - - printer *printer.Printer - config *config.Config - resourceType string - listID string - listTitle string - listRepliesPolicy string -} - -func NewEditExecutor(printer *printer.Printer, config *config.Config, name, summary string) *EditExecutor { - editExe := EditExecutor{ - FlagSet: flag.NewFlagSet(name, flag.ExitOnError), - - printer: printer, - config: config, - } - - editExe.StringVar(&editExe.resourceType, flagType, "", "Specify the type of resource to update") - editExe.StringVar(&editExe.listID, flagListID, "", "Specify the ID of the list to update") - editExe.StringVar(&editExe.listTitle, flagListTitle, "", "Specify the title of the list") - editExe.StringVar(&editExe.listRepliesPolicy, flagListRepliesPolicy, "", "Specify the policy of the replies for this list (valid values are followed, list and none)") - - editExe.Usage = commandUsageFunc(name, summary, editExe.FlagSet) - - return &editExe -} - func (e *EditExecutor) Execute() error { if e.resourceType == "" { return FlagNotSetError{flagText: flagType} diff --git a/internal/executor/execute.go b/internal/executor/execute.go new file mode 100644 index 0000000..8ffa917 --- /dev/null +++ b/internal/executor/execute.go @@ -0,0 +1,160 @@ +/* + This file is generated by the enbas-codegen + DO NOT EDIT. +*/ + +package executor + +import ( + "fmt" + + "codeflow.dananglin.me.uk/apollo/enbas/internal/config" + "codeflow.dananglin.me.uk/apollo/enbas/internal/printer" +) + +func Execute( + command string, + args []string, + noColor bool, + configDir string, +) error { + var ( + enbasConfig *config.Config + enbasPrinter *printer.Printer + err error + ) + + switch command { + case "init", "version": + enbasPrinter = printer.NewPrinter(noColor, "", 0) + default: + enbasConfig, err = config.NewConfigFromFile(configDir) + if err != nil { + enbasPrinter = printer.NewPrinter(noColor, "", 0) + enbasPrinter.PrintFailure("unable to load the configuration: " + err.Error() + ".") + + return err + } + + enbasPrinter = printer.NewPrinter( + noColor, + enbasConfig.Integrations.Pager, + enbasConfig.LineWrapMaxWidth, + ) + } + + if err = execute( + command, + args, + enbasPrinter, + enbasConfig, + configDir, + ); err != nil { + enbasPrinter.PrintFailure("(" + command + ") " + err.Error() + ".") + + return err + } + + return nil +} + +func execute( + command string, + args []string, + enbasPrinter *printer.Printer, + enbasConfig *config.Config, + configDir string, +) error { + executorMap := map[string]Executor{ + "accept": NewAcceptExecutor( + enbasPrinter, + enbasConfig, + ), + "add": NewAddExecutor( + enbasPrinter, + enbasConfig, + ), + "block": NewBlockExecutor( + enbasPrinter, + enbasConfig, + ), + "create": NewCreateExecutor( + enbasPrinter, + enbasConfig, + ), + "delete": NewDeleteExecutor( + enbasPrinter, + enbasConfig, + ), + "edit": NewEditExecutor( + enbasPrinter, + enbasConfig, + ), + "follow": NewFollowExecutor( + enbasPrinter, + enbasConfig, + ), + "init": NewInitExecutor( + enbasPrinter, + configDir, + ), + "login": NewLoginExecutor( + enbasPrinter, + enbasConfig, + ), + "mute": NewMuteExecutor( + enbasPrinter, + enbasConfig, + ), + "reject": NewRejectExecutor( + enbasPrinter, + enbasConfig, + ), + "remove": NewRemoveExecutor( + enbasPrinter, + enbasConfig, + ), + "show": NewShowExecutor( + enbasPrinter, + enbasConfig, + ), + "switch": NewSwitchExecutor( + enbasPrinter, + enbasConfig, + ), + "unblock": NewUnblockExecutor( + enbasPrinter, + enbasConfig, + ), + "unfollow": NewUnfollowExecutor( + enbasPrinter, + enbasConfig, + ), + "unmute": NewUnmuteExecutor( + enbasPrinter, + enbasConfig, + ), + "version": NewVersionExecutor( + enbasPrinter, + ), + "whoami": NewWhoamiExecutor( + enbasPrinter, + enbasConfig, + ), + } + + exe, ok := executorMap[command] + if !ok { + return UnknownCommandError{Command: command} + } + + if err := exe.Parse(args); err != nil { + return fmt.Errorf("flag parsing error: %w", err) + } + + if err := exe.Execute(); err != nil { + return fmt.Errorf("execution error: %w", err) + } + + return nil +} diff --git a/internal/executor/executor.go b/internal/executor/executor.go deleted file mode 100644 index 81d73e9..0000000 --- a/internal/executor/executor.go +++ /dev/null @@ -1,21 +0,0 @@ -package executor - -import "fmt" - -type Executor interface { - Name() string - Parse(args []string) error - Execute() error -} - -func Execute(executor Executor, args []string) error { - if err := executor.Parse(args); err != nil { - return fmt.Errorf("flag parsing error: %w", err) - } - - if err := executor.Execute(); err != nil { - return fmt.Errorf("execution error: %w", err) - } - - return nil -} diff --git a/internal/executor/executors.go b/internal/executor/executors.go new file mode 100644 index 0000000..4e7fd14 --- /dev/null +++ b/internal/executor/executors.go @@ -0,0 +1,641 @@ +/* + This file is generated by the enbas-codegen + DO NOT EDIT. +*/ + +package executor + +import ( + "flag" + + "codeflow.dananglin.me.uk/apollo/enbas/internal/config" + internalFlag "codeflow.dananglin.me.uk/apollo/enbas/internal/flag" + "codeflow.dananglin.me.uk/apollo/enbas/internal/printer" + "codeflow.dananglin.me.uk/apollo/enbas/internal/usage" +) + +type Executor interface { + Name() string + Parse(args []string) error + Execute() error +} + +// AcceptExecutor is the executor for the accept command. +type AcceptExecutor struct { + *flag.FlagSet + printer *printer.Printer + config *config.Config + accountName internalFlag.StringSliceValue + resourceType string +} + +func NewAcceptExecutor( + printer *printer.Printer, + config *config.Config, +) *AcceptExecutor { + exe := AcceptExecutor{ + FlagSet: flag.NewFlagSet("accept", flag.ExitOnError), + printer: printer, + config: config, + accountName: internalFlag.NewStringSliceValue(), + } + + exe.Usage = usage.ExecutorUsageFunc("accept", "Accepts a request (e.g. a follow request)", exe.FlagSet) + + exe.Var(&exe.accountName, "account-name", "The name of the account") + exe.StringVar(&exe.resourceType, "type", "", "The type of resource you want to action on (e.g. account, status)") + + return &exe +} + +// AddExecutor is the executor for the add command. +type AddExecutor struct { + *flag.FlagSet + printer *printer.Printer + config *config.Config + accountNames internalFlag.StringSliceValue + content string + listID string + pollID string + statusID string + toResourceType string + resourceType string + votes internalFlag.IntSliceValue +} + +func NewAddExecutor( + printer *printer.Printer, + config *config.Config, +) *AddExecutor { + exe := AddExecutor{ + FlagSet: flag.NewFlagSet("add", flag.ExitOnError), + printer: printer, + config: config, + accountNames: internalFlag.NewStringSliceValue(), + votes: internalFlag.NewIntSliceValue(), + } + + exe.Usage = usage.ExecutorUsageFunc("add", "Adds a resource to another resource", exe.FlagSet) + + exe.Var(&exe.accountNames, "account-name", "The name of the account") + exe.StringVar(&exe.content, "content", "", "The content of the created resource") + exe.StringVar(&exe.listID, "list-id", "", "The ID of the list in question") + exe.StringVar(&exe.pollID, "poll-id", "", "The ID of the poll") + exe.StringVar(&exe.statusID, "status-id", "", "The ID of the status") + exe.StringVar(&exe.toResourceType, "to", "", "TBC") + exe.StringVar(&exe.resourceType, "type", "", "The type of resource you want to action on (e.g. account, status)") + exe.Var(&exe.votes, "vote", "Add a vote to an option in a poll") + + return &exe +} + +// BlockExecutor is the executor for the block command. +type BlockExecutor struct { + *flag.FlagSet + printer *printer.Printer + config *config.Config + accountName internalFlag.StringSliceValue + resourceType string +} + +func NewBlockExecutor( + printer *printer.Printer, + config *config.Config, +) *BlockExecutor { + exe := BlockExecutor{ + FlagSet: flag.NewFlagSet("block", flag.ExitOnError), + printer: printer, + config: config, + accountName: internalFlag.NewStringSliceValue(), + } + + exe.Usage = usage.ExecutorUsageFunc("block", "Blocks a resource (e.g. an account)", exe.FlagSet) + + exe.Var(&exe.accountName, "account-name", "The name of the account") + exe.StringVar(&exe.resourceType, "type", "", "The type of resource you want to action on (e.g. account, status)") + + return &exe +} + +// CreateExecutor is the executor for the create command. +type CreateExecutor struct { + *flag.FlagSet + printer *printer.Printer + config *config.Config + addPoll bool + content string + contentType string + federated bool + likeable bool + replyable bool + boostable bool + fromFile string + inReplyTo string + language string + listRepliesPolicy string + listTitle string + pollAllowsMultipleChoices bool + pollExpiresIn internalFlag.TimeDurationValue + pollHidesVoteCounts bool + pollOptions internalFlag.StringSliceValue + sensitive internalFlag.BoolPtrValue + spoilerText string + resourceType string + visibility string +} + +func NewCreateExecutor( + printer *printer.Printer, + config *config.Config, +) *CreateExecutor { + exe := CreateExecutor{ + FlagSet: flag.NewFlagSet("create", flag.ExitOnError), + printer: printer, + config: config, + pollExpiresIn: internalFlag.NewTimeDurationValue(), + pollOptions: internalFlag.NewStringSliceValue(), + sensitive: internalFlag.NewBoolPtrValue(), + } + + exe.Usage = usage.ExecutorUsageFunc("create", "Creates a specific resource", exe.FlagSet) + + exe.BoolVar(&exe.addPoll, "add-poll", false, "Set to true to add a poll when creating a status") + exe.StringVar(&exe.content, "content", "", "The content of the created resource") + exe.StringVar(&exe.contentType, "content-type", "plain", "The type that the contents should be parsed from (valid values are plain and markdown)") + exe.BoolVar(&exe.federated, "enable-federation", true, "Set to true to federate the status beyond the local timelines") + exe.BoolVar(&exe.likeable, "enable-likes", true, "Set to true to allow the status to be liked (favourited)") + exe.BoolVar(&exe.replyable, "enable-replies", true, "Set to true to allow viewers to reply to the status") + exe.BoolVar(&exe.boostable, "enable-reposts", true, "Set to true to allow the status to be reposted (boosted) by others") + exe.StringVar(&exe.fromFile, "from-file", "", "The file path where to read the contents from") + exe.StringVar(&exe.inReplyTo, "in-reply-to", "", "The ID of the status that you want to reply to") + exe.StringVar(&exe.language, "language", "", "The ISO 639 language code for this status") + exe.StringVar(&exe.listRepliesPolicy, "list-replies-policy", "list", "The replies policy of the list") + exe.StringVar(&exe.listTitle, "list-title", "", "The title of the list") + exe.BoolVar(&exe.pollAllowsMultipleChoices, "poll-allows-multiple-choices", false, "Set to true to allow viewers to make multiple choices in the poll") + exe.Var(&exe.pollExpiresIn, "poll-expires-in", "The duration in which the poll is open for") + exe.BoolVar(&exe.pollHidesVoteCounts, "poll-hides-vote-counts", false, "Set to true to hide the vote count until the poll is closed") + exe.Var(&exe.pollOptions, "poll-option", "A poll option. Use this multiple times to set multiple options") + exe.Var(&exe.sensitive, "sensitive", "Set to true if the status should be marked as sensitive") + exe.StringVar(&exe.spoilerText, "spoiler-text", "", "The text to display as the status' warning or subject") + exe.StringVar(&exe.resourceType, "type", "", "The type of resource you want to action on (e.g. account, status)") + exe.StringVar(&exe.visibility, "visibility", "", "The visibility of the posted status") + + return &exe +} + +// DeleteExecutor is the executor for the delete command. +type DeleteExecutor struct { + *flag.FlagSet + printer *printer.Printer + config *config.Config + listID string + resourceType string +} + +func NewDeleteExecutor( + printer *printer.Printer, + config *config.Config, +) *DeleteExecutor { + exe := DeleteExecutor{ + FlagSet: flag.NewFlagSet("delete", flag.ExitOnError), + printer: printer, + config: config, + } + + exe.Usage = usage.ExecutorUsageFunc("delete", "Deletes a specific resource", exe.FlagSet) + + exe.StringVar(&exe.listID, "list-id", "", "The ID of the list in question") + exe.StringVar(&exe.resourceType, "type", "", "The type of resource you want to action on (e.g. account, status)") + + return &exe +} + +// EditExecutor is the executor for the edit command. +type EditExecutor struct { + *flag.FlagSet + printer *printer.Printer + config *config.Config + listID string + listTitle string + listRepliesPolicy string + resourceType string +} + +func NewEditExecutor( + printer *printer.Printer, + config *config.Config, +) *EditExecutor { + exe := EditExecutor{ + FlagSet: flag.NewFlagSet("edit", flag.ExitOnError), + printer: printer, + config: config, + } + + exe.Usage = usage.ExecutorUsageFunc("edit", "Edit a specific resource", exe.FlagSet) + + exe.StringVar(&exe.listID, "list-id", "", "The ID of the list in question") + exe.StringVar(&exe.listTitle, "list-title", "", "The title of the list") + exe.StringVar(&exe.listRepliesPolicy, "list-replies-policy", "", "The replies policy of the list") + exe.StringVar(&exe.resourceType, "type", "", "The type of resource you want to action on (e.g. account, status)") + + return &exe +} + +// FollowExecutor is the executor for the follow command. +type FollowExecutor struct { + *flag.FlagSet + printer *printer.Printer + config *config.Config + accountName internalFlag.StringSliceValue + notify bool + showReposts bool + resourceType string +} + +func NewFollowExecutor( + printer *printer.Printer, + config *config.Config, +) *FollowExecutor { + exe := FollowExecutor{ + FlagSet: flag.NewFlagSet("follow", flag.ExitOnError), + printer: printer, + config: config, + accountName: internalFlag.NewStringSliceValue(), + } + + exe.Usage = usage.ExecutorUsageFunc("follow", "Follow a resource (e.g. an account)", exe.FlagSet) + + exe.Var(&exe.accountName, "account-name", "The name of the account") + exe.BoolVar(&exe.notify, "notify", false, "Get notifications from statuses from the account you want to follow") + exe.BoolVar(&exe.showReposts, "show-reposts", true, "Show reposts from the account you want to follow") + exe.StringVar(&exe.resourceType, "type", "", "The type of resource you want to action on (e.g. account, status)") + + return &exe +} + +// InitExecutor is the executor for the init command. +type InitExecutor struct { + *flag.FlagSet + printer *printer.Printer + configDir string +} + +func NewInitExecutor( + printer *printer.Printer, + configDir string, +) *InitExecutor { + exe := InitExecutor{ + FlagSet: flag.NewFlagSet("init", flag.ExitOnError), + printer: printer, + configDir: configDir, + } + + exe.Usage = usage.ExecutorUsageFunc("init", "Creates a new configuration file in the specified configuration directory", exe.FlagSet) + + return &exe +} + +// LoginExecutor is the executor for the login command. +type LoginExecutor struct { + *flag.FlagSet + printer *printer.Printer + config *config.Config + instance string +} + +func NewLoginExecutor( + printer *printer.Printer, + config *config.Config, +) *LoginExecutor { + exe := LoginExecutor{ + FlagSet: flag.NewFlagSet("login", flag.ExitOnError), + printer: printer, + config: config, + } + + exe.Usage = usage.ExecutorUsageFunc("login", "Logs into an account on GoToSocial", exe.FlagSet) + + exe.StringVar(&exe.instance, "instance", "", "The instance that you want to log into") + + return &exe +} + +// MuteExecutor is the executor for the mute command. +type MuteExecutor struct { + *flag.FlagSet + printer *printer.Printer + config *config.Config + accountName internalFlag.StringSliceValue + muteDuration internalFlag.TimeDurationValue + muteNotifications bool + resourceType string +} + +func NewMuteExecutor( + printer *printer.Printer, + config *config.Config, +) *MuteExecutor { + exe := MuteExecutor{ + FlagSet: flag.NewFlagSet("mute", flag.ExitOnError), + printer: printer, + config: config, + accountName: internalFlag.NewStringSliceValue(), + muteDuration: internalFlag.NewTimeDurationValue(), + } + + exe.Usage = usage.ExecutorUsageFunc("mute", "Mutes a specific resource (e.g. an account)", exe.FlagSet) + + exe.Var(&exe.accountName, "account-name", "The name of the account") + exe.Var(&exe.muteDuration, "mute-duration", "Specify how long the mute should last for. To mute indefinitely, set this to 0s") + exe.BoolVar(&exe.muteNotifications, "mute-notifications", false, "Set to true to mute notifications as well as posts") + exe.StringVar(&exe.resourceType, "type", "", "The type of resource you want to action on (e.g. account, status)") + + return &exe +} + +// RejectExecutor is the executor for the reject command. +type RejectExecutor struct { + *flag.FlagSet + printer *printer.Printer + config *config.Config + accountName internalFlag.StringSliceValue + resourceType string +} + +func NewRejectExecutor( + printer *printer.Printer, + config *config.Config, +) *RejectExecutor { + exe := RejectExecutor{ + FlagSet: flag.NewFlagSet("reject", flag.ExitOnError), + printer: printer, + config: config, + accountName: internalFlag.NewStringSliceValue(), + } + + exe.Usage = usage.ExecutorUsageFunc("reject", "Rejects a request (e.g. a follow request)", exe.FlagSet) + + exe.Var(&exe.accountName, "account-name", "The name of the account") + exe.StringVar(&exe.resourceType, "type", "", "The type of resource you want to action on (e.g. account, status)") + + return &exe +} + +// RemoveExecutor is the executor for the remove command. +type RemoveExecutor struct { + *flag.FlagSet + printer *printer.Printer + config *config.Config + accountNames internalFlag.StringSliceValue + fromResourceType string + listID string + statusID string + resourceType string +} + +func NewRemoveExecutor( + printer *printer.Printer, + config *config.Config, +) *RemoveExecutor { + exe := RemoveExecutor{ + FlagSet: flag.NewFlagSet("remove", flag.ExitOnError), + printer: printer, + config: config, + accountNames: internalFlag.NewStringSliceValue(), + } + + exe.Usage = usage.ExecutorUsageFunc("remove", "Removes a resource from another resource", exe.FlagSet) + + exe.Var(&exe.accountNames, "account-name", "The name of the account") + exe.StringVar(&exe.fromResourceType, "from", "", "Specify the resource type to action the target resource from") + exe.StringVar(&exe.listID, "list-id", "", "The ID of the list in question") + exe.StringVar(&exe.statusID, "status-id", "", "The ID of the status") + exe.StringVar(&exe.resourceType, "type", "", "The type of resource you want to action on (e.g. account, status)") + + return &exe +} + +// ShowExecutor is the executor for the show command. +type ShowExecutor struct { + *flag.FlagSet + printer *printer.Printer + config *config.Config + accountName internalFlag.StringSliceValue + getAllImages bool + getAllVideos bool + attachmentIDs internalFlag.StringSliceValue + showInBrowser bool + excludeBoosts bool + excludeReplies bool + fromResourceType string + limit int + listID string + myAccount bool + onlyMedia bool + onlyPinned bool + onlyPublic bool + pollID string + showUserPreferences bool + showStatuses bool + skipAccountRelationship bool + statusID string + timelineCategory string + tag string + resourceType string +} + +func NewShowExecutor( + printer *printer.Printer, + config *config.Config, +) *ShowExecutor { + exe := ShowExecutor{ + FlagSet: flag.NewFlagSet("show", flag.ExitOnError), + printer: printer, + config: config, + accountName: internalFlag.NewStringSliceValue(), + attachmentIDs: internalFlag.NewStringSliceValue(), + } + + exe.Usage = usage.ExecutorUsageFunc("show", "Shows details about a specified resource", exe.FlagSet) + + exe.Var(&exe.accountName, "account-name", "The name of the account") + exe.BoolVar(&exe.getAllImages, "all-images", false, "Set to true to show all images from a status") + exe.BoolVar(&exe.getAllVideos, "all-videos", false, "Set to true to show all videos from a status") + exe.Var(&exe.attachmentIDs, "attachment-id", "The ID of the media attachment") + exe.BoolVar(&exe.showInBrowser, "browser", false, "Set to true to view in the your favourite browser") + exe.BoolVar(&exe.excludeBoosts, "exclude-boosts", false, "Set to true to exclude statuses that are boosts of another status") + exe.BoolVar(&exe.excludeReplies, "exclude-replies", false, "Set to true to exclude statuses that are a reply to another status") + exe.StringVar(&exe.fromResourceType, "from", "", "Specify the resource type to action the target resource from") + exe.IntVar(&exe.limit, "limit", 20, "Specify the limit of items to display") + exe.StringVar(&exe.listID, "list-id", "", "The ID of the list in question") + exe.BoolVar(&exe.myAccount, "my-account", false, "Set to true to specify your account") + exe.BoolVar(&exe.onlyMedia, "only-media", false, "Set to true to show only the statuses with media attachments") + exe.BoolVar(&exe.onlyPinned, "only-pinned", false, "Set to true to show only the account's pinned statuses") + exe.BoolVar(&exe.onlyPublic, "only-public", false, "Set to true to show only the account's public posts") + exe.StringVar(&exe.pollID, "poll-id", "", "The ID of the poll") + exe.BoolVar(&exe.showUserPreferences, "show-preferences", false, "Set to true to view your posting preferences when viewing your account information") + exe.BoolVar(&exe.showStatuses, "show-statuses", false, "Set to true to view the statuses created from the account you are viewing") + exe.BoolVar(&exe.skipAccountRelationship, "skip-relationship", false, "Set to true to skip showing your relationship to the account that you are viewing") + exe.StringVar(&exe.statusID, "status-id", "", "The ID of the status") + exe.StringVar(&exe.timelineCategory, "timeline-category", "home", "The timeline category") + exe.StringVar(&exe.tag, "tag", "", "The name of the tag") + exe.StringVar(&exe.resourceType, "type", "", "The type of resource you want to action on (e.g. account, status)") + + return &exe +} + +// SwitchExecutor is the executor for the switch command. +type SwitchExecutor struct { + *flag.FlagSet + printer *printer.Printer + config *config.Config + accountName internalFlag.StringSliceValue + to string +} + +func NewSwitchExecutor( + printer *printer.Printer, + config *config.Config, +) *SwitchExecutor { + exe := SwitchExecutor{ + FlagSet: flag.NewFlagSet("switch", flag.ExitOnError), + printer: printer, + config: config, + accountName: internalFlag.NewStringSliceValue(), + } + + exe.Usage = usage.ExecutorUsageFunc("switch", "Performs a switch operation (e.g. switching between logged in accounts)", exe.FlagSet) + + exe.Var(&exe.accountName, "account-name", "The name of the account") + exe.StringVar(&exe.to, "to", "", "TBC") + + return &exe +} + +// UnblockExecutor is the executor for the unblock command. +type UnblockExecutor struct { + *flag.FlagSet + printer *printer.Printer + config *config.Config + accountName internalFlag.StringSliceValue + resourceType string +} + +func NewUnblockExecutor( + printer *printer.Printer, + config *config.Config, +) *UnblockExecutor { + exe := UnblockExecutor{ + FlagSet: flag.NewFlagSet("unblock", flag.ExitOnError), + printer: printer, + config: config, + accountName: internalFlag.NewStringSliceValue(), + } + + exe.Usage = usage.ExecutorUsageFunc("unblock", "Unblocks a resource (e.g. an account)", exe.FlagSet) + + exe.Var(&exe.accountName, "account-name", "The name of the account") + exe.StringVar(&exe.resourceType, "type", "", "The type of resource you want to action on (e.g. account, status)") + + return &exe +} + +// UnfollowExecutor is the executor for the unfollow command. +type UnfollowExecutor struct { + *flag.FlagSet + printer *printer.Printer + config *config.Config + accountName internalFlag.StringSliceValue + resourceType string +} + +func NewUnfollowExecutor( + printer *printer.Printer, + config *config.Config, +) *UnfollowExecutor { + exe := UnfollowExecutor{ + FlagSet: flag.NewFlagSet("unfollow", flag.ExitOnError), + printer: printer, + config: config, + accountName: internalFlag.NewStringSliceValue(), + } + + exe.Usage = usage.ExecutorUsageFunc("unfollow", "Unfollows a resource (e.g. an account)", exe.FlagSet) + + exe.Var(&exe.accountName, "account-name", "The name of the account") + exe.StringVar(&exe.resourceType, "type", "", "The type of resource you want to action on (e.g. account, status)") + + return &exe +} + +// UnmuteExecutor is the executor for the unmute command. +type UnmuteExecutor struct { + *flag.FlagSet + printer *printer.Printer + config *config.Config + accountName internalFlag.StringSliceValue + resourceType string +} + +func NewUnmuteExecutor( + printer *printer.Printer, + config *config.Config, +) *UnmuteExecutor { + exe := UnmuteExecutor{ + FlagSet: flag.NewFlagSet("unmute", flag.ExitOnError), + printer: printer, + config: config, + accountName: internalFlag.NewStringSliceValue(), + } + + exe.Usage = usage.ExecutorUsageFunc("unmute", "Umutes a specific resource (e.g. an account)", exe.FlagSet) + + exe.Var(&exe.accountName, "account-name", "The name of the account") + exe.StringVar(&exe.resourceType, "type", "", "The type of resource you want to action on (e.g. account, status)") + + return &exe +} + +// VersionExecutor is the executor for the version command. +type VersionExecutor struct { + *flag.FlagSet + printer *printer.Printer + full bool +} + +func NewVersionExecutor( + printer *printer.Printer, +) *VersionExecutor { + exe := VersionExecutor{ + FlagSet: flag.NewFlagSet("version", flag.ExitOnError), + printer: printer, + } + + exe.Usage = usage.ExecutorUsageFunc("version", "Prints the application's version and build information", exe.FlagSet) + + exe.BoolVar(&exe.full, "full", false, "Set to true to print the build information in full") + + return &exe +} + +// WhoamiExecutor is the executor for the whoami command. +type WhoamiExecutor struct { + *flag.FlagSet + printer *printer.Printer + config *config.Config +} + +func NewWhoamiExecutor( + printer *printer.Printer, + config *config.Config, +) *WhoamiExecutor { + exe := WhoamiExecutor{ + FlagSet: flag.NewFlagSet("whoami", flag.ExitOnError), + printer: printer, + config: config, + } + + exe.Usage = usage.ExecutorUsageFunc("whoami", "Prints the account that you are currently logged into", exe.FlagSet) + + return &exe +} diff --git a/internal/executor/flags.go b/internal/executor/flags.go index ceebaa4..37eb7a0 100644 --- a/internal/executor/flags.go +++ b/internal/executor/flags.go @@ -1,120 +1,18 @@ package executor -import ( - "fmt" - "strconv" - "strings" - "time" -) - const ( - flagAddPoll = "add-poll" - flagAccountName = "account-name" - flagAllImages = "all-images" - flagAllVideos = "all-videos" - flagAttachmentID = "attachment-id" - flagBrowser = "browser" - flagContentType = "content-type" - flagContent = "content" - flagEnableFederation = "enable-federation" - flagEnableLikes = "enable-likes" - flagEnableReplies = "enable-replies" - flagEnableReposts = "enable-reposts" - flagExcludeBoosts = "exclude-boosts" - flagExcludeReplies = "exclude-replies" - flagFrom = "from" - flagFromFile = "from-file" - flagFull = "full" - flagInReplyTo = "in-reply-to" - flagInstance = "instance" - flagLanguage = "language" - flagLimit = "limit" - flagListID = "list-id" - flagListTitle = "list-title" - flagListRepliesPolicy = "list-replies-policy" - flagMyAccount = "my-account" - flagMuteDuration = "mute-duration" - flagMuteNotifications = "mute-notifications" - flagNotify = "notify" - flagOnlyMedia = "only-media" - flagOnlyPinned = "only-pinned" - flagOnlyPublic = "only-public" - flagPollAllowsMultipleChoices = "poll-allows-multiple-choices" - flagPollExpiresIn = "poll-expires-in" - flagPollHidesVoteCounts = "poll-hides-vote-counts" - flagPollID = "poll-id" - flagPollOption = "poll-option" - flagSensitive = "sensitive" - flagSkipRelationship = "skip-relationship" - flagShowPreferences = "show-preferences" - flagShowReposts = "show-reposts" - flagShowStatuses = "show-statuses" - flagSpoilerText = "spoiler-text" - flagStatusID = "status-id" - flagTag = "tag" - flagTimelineCategory = "timeline-category" - flagTo = "to" - flagType = "type" - flagVisibility = "visibility" - flagVote = "vote" + flagAttachmentID = "attachment-id" + flagContent = "content" + flagFrom = "from" + flagFromFile = "from-file" + flagInstance = "instance" + flagListID = "list-id" + flagListTitle = "list-title" + flagPollID = "poll-id" + flagPollOption = "poll-option" + flagStatusID = "status-id" + flagTag = "tag" + flagTo = "to" + flagType = "type" + flagVote = "vote" ) - -type MultiStringFlagValue []string - -func (v *MultiStringFlagValue) String() string { - return strings.Join(*v, ", ") -} - -func (v *MultiStringFlagValue) Set(value string) error { - if len(value) > 0 { - *v = append(*v, value) - } - - return nil -} - -type MultiIntFlagValue []int - -func (v *MultiIntFlagValue) String() string { - value := "Choices: " - - for ind, vote := range *v { - if ind == len(*v)-1 { - value += strconv.Itoa(vote) - } else { - value += strconv.Itoa(vote) + ", " - } - } - - return value -} - -func (v *MultiIntFlagValue) Set(text string) error { - value, err := strconv.Atoi(text) - if err != nil { - return fmt.Errorf("unable to parse the value to an integer: %w", err) - } - - *v = append(*v, value) - - return nil -} - -type TimeDurationFlagValue struct { - Duration time.Duration -} - -func (v TimeDurationFlagValue) String() string { - return "" -} - -func (v *TimeDurationFlagValue) Set(text string) error { - duration, err := time.ParseDuration(text) - if err != nil { - return fmt.Errorf("unable to parse the value as time duration: %w", err) - } - - v.Duration = duration - - return nil -} diff --git a/internal/executor/follow.go b/internal/executor/follow.go new file mode 100644 index 0000000..b76ca09 --- /dev/null +++ b/internal/executor/follow.go @@ -0,0 +1,46 @@ +package executor + +import ( + "fmt" + + "codeflow.dananglin.me.uk/apollo/enbas/internal/client" +) + +func (f *FollowExecutor) Execute() error { + funcMap := map[string]func(*client.Client) error{ + resourceAccount: f.followAccount, + } + + doFunc, ok := funcMap[f.resourceType] + if !ok { + return UnsupportedTypeError{resourceType: f.resourceType} + } + + gtsClient, err := client.NewClientFromFile(f.config.CredentialsFile) + if err != nil { + return fmt.Errorf("unable to create the GoToSocial client: %w", err) + } + + return doFunc(gtsClient) +} + +func (f *FollowExecutor) followAccount(gtsClient *client.Client) error { + accountID, err := getAccountID(gtsClient, false, f.accountName, f.config.CredentialsFile) + if err != nil { + return fmt.Errorf("received an error while getting the account ID: %w", err) + } + + form := client.FollowAccountForm{ + AccountID: accountID, + ShowReposts: f.showReposts, + Notify: f.notify, + } + + if err := gtsClient.FollowAccount(form); err != nil { + return fmt.Errorf("unable to follow the account: %w", err) + } + + f.printer.PrintSuccess("Successfully sent the follow request.") + + return nil +} diff --git a/internal/executor/follow_or_unfollow.go b/internal/executor/follow_or_unfollow.go deleted file mode 100644 index eb7d15f..0000000 --- a/internal/executor/follow_or_unfollow.go +++ /dev/null @@ -1,101 +0,0 @@ -package executor - -import ( - "flag" - "fmt" - - "codeflow.dananglin.me.uk/apollo/enbas/internal/client" - "codeflow.dananglin.me.uk/apollo/enbas/internal/config" - "codeflow.dananglin.me.uk/apollo/enbas/internal/printer" -) - -type FollowOrUnfollowExecutor struct { - *flag.FlagSet - - printer *printer.Printer - config *config.Config - resourceType string - accountName string - showReposts bool - notify bool - action string -} - -func NewFollowOrUnfollowExecutor(printer *printer.Printer, config *config.Config, name, summary string) *FollowOrUnfollowExecutor { - command := FollowOrUnfollowExecutor{ - FlagSet: flag.NewFlagSet(name, flag.ExitOnError), - - printer: printer, - config: config, - action: name, - } - - command.StringVar(&command.resourceType, flagType, "", "Specify the type of resource to follow") - command.StringVar(&command.accountName, flagAccountName, "", "Specify the account name in full (username@domain)") - command.BoolVar(&command.showReposts, flagShowReposts, true, "Show reposts from the account you want to follow") - command.BoolVar(&command.notify, flagNotify, false, "Get notifications when the account you want to follow posts a status") - - command.Usage = commandUsageFunc(name, summary, command.FlagSet) - - return &command -} - -func (f *FollowOrUnfollowExecutor) Execute() error { - funcMap := map[string]func(*client.Client) error{ - resourceAccount: f.followOrUnfollowAccount, - } - - doFunc, ok := funcMap[f.resourceType] - if !ok { - return UnsupportedTypeError{resourceType: f.resourceType} - } - - gtsClient, err := client.NewClientFromFile(f.config.CredentialsFile) - if err != nil { - return fmt.Errorf("unable to create the GoToSocial client: %w", err) - } - - return doFunc(gtsClient) -} - -func (f *FollowOrUnfollowExecutor) followOrUnfollowAccount(gtsClient *client.Client) error { - accountID, err := getAccountID(gtsClient, false, f.accountName, f.config.CredentialsFile) - if err != nil { - return fmt.Errorf("received an error while getting the account ID: %w", err) - } - - switch f.action { - case CommandFollow: - return f.followAccount(gtsClient, accountID) - case CommandUnfollow: - return f.unfollowAccount(gtsClient, accountID) - default: - return nil - } -} - -func (f *FollowOrUnfollowExecutor) followAccount(gtsClient *client.Client, accountID string) error { - form := client.FollowAccountForm{ - AccountID: accountID, - ShowReposts: f.showReposts, - Notify: f.notify, - } - - if err := gtsClient.FollowAccount(form); err != nil { - return fmt.Errorf("unable to follow the account: %w", err) - } - - f.printer.PrintSuccess("Successfully sent the follow request.") - - return nil -} - -func (f *FollowOrUnfollowExecutor) unfollowAccount(gtsClient *client.Client, accountID string) error { - if err := gtsClient.UnfollowAccount(accountID); err != nil { - return fmt.Errorf("unable to unfollow the account: %w", err) - } - - f.printer.PrintSuccess("Successfully unfollowed the account.") - - return nil -} diff --git a/internal/executor/init.go b/internal/executor/init.go index 35b64a4..c0f0129 100644 --- a/internal/executor/init.go +++ b/internal/executor/init.go @@ -1,34 +1,12 @@ package executor import ( - "flag" "fmt" "codeflow.dananglin.me.uk/apollo/enbas/internal/config" - "codeflow.dananglin.me.uk/apollo/enbas/internal/printer" "codeflow.dananglin.me.uk/apollo/enbas/internal/utilities" ) -type InitExecutor struct { - *flag.FlagSet - - printer *printer.Printer - configDir string -} - -func NewInitExecutor(printer *printer.Printer, configDir, name, summary string) *InitExecutor { - initExe := InitExecutor{ - FlagSet: flag.NewFlagSet(name, flag.ExitOnError), - - printer: printer, - configDir: configDir, - } - - initExe.Usage = commandUsageFunc(name, summary, initExe.FlagSet) - - return &initExe -} - func (i *InitExecutor) Execute() error { if err := utilities.EnsureDirectory(i.configDir); err != nil { return fmt.Errorf("unable to ensure that the configuration directory is present: %w", err) diff --git a/internal/executor/login.go b/internal/executor/login.go index 971d100..240be48 100644 --- a/internal/executor/login.go +++ b/internal/executor/login.go @@ -1,40 +1,14 @@ package executor import ( - "flag" "fmt" "strings" "codeflow.dananglin.me.uk/apollo/enbas/internal/client" "codeflow.dananglin.me.uk/apollo/enbas/internal/config" - "codeflow.dananglin.me.uk/apollo/enbas/internal/printer" "codeflow.dananglin.me.uk/apollo/enbas/internal/utilities" ) -type LoginExecutor struct { - *flag.FlagSet - - printer *printer.Printer - config *config.Config - instance string -} - -func NewLoginExecutor(printer *printer.Printer, config *config.Config, name, summary string) *LoginExecutor { - loginExe := LoginExecutor{ - FlagSet: flag.NewFlagSet(name, flag.ExitOnError), - - printer: printer, - config: config, - instance: "", - } - - loginExe.StringVar(&loginExe.instance, flagInstance, "", "Specify the instance that you want to login to.") - - loginExe.Usage = commandUsageFunc(name, summary, loginExe.FlagSet) - - return &loginExe -} - func (l *LoginExecutor) Execute() error { var err error diff --git a/internal/executor/mute.go b/internal/executor/mute.go new file mode 100644 index 0000000..73d8f50 --- /dev/null +++ b/internal/executor/mute.go @@ -0,0 +1,45 @@ +package executor + +import ( + "fmt" + + "codeflow.dananglin.me.uk/apollo/enbas/internal/client" +) + +func (m *MuteExecutor) Execute() error { + funcMap := map[string]func(*client.Client) error{ + resourceAccount: m.muteAccount, + } + + doFunc, ok := funcMap[m.resourceType] + if !ok { + return UnsupportedTypeError{resourceType: m.resourceType} + } + + gtsClient, err := client.NewClientFromFile(m.config.CredentialsFile) + if err != nil { + return fmt.Errorf("unable to create the GoToSocial client: %w", err) + } + + return doFunc(gtsClient) +} + +func (m *MuteExecutor) muteAccount(gtsClient *client.Client) error { + accountID, err := getAccountID(gtsClient, false, m.accountName, m.config.CredentialsFile) + if err != nil { + return fmt.Errorf("received an error while getting the account ID: %w", err) + } + + form := client.MuteAccountForm{ + Notifications: m.muteNotifications, + Duration: int(m.muteDuration.Duration.Seconds()), + } + + if err := gtsClient.MuteAccount(accountID, form); err != nil { + return fmt.Errorf("unable to mute the account: %w", err) + } + + m.printer.PrintSuccess("Successfully muted the account.") + + return nil +} diff --git a/internal/executor/mute_or_unmute.go b/internal/executor/mute_or_unmute.go deleted file mode 100644 index 790f04d..0000000 --- a/internal/executor/mute_or_unmute.go +++ /dev/null @@ -1,108 +0,0 @@ -package executor - -import ( - "flag" - "fmt" - "time" - - "codeflow.dananglin.me.uk/apollo/enbas/internal/client" - "codeflow.dananglin.me.uk/apollo/enbas/internal/config" - "codeflow.dananglin.me.uk/apollo/enbas/internal/printer" -) - -type MuteOrUnmuteExecutor struct { - *flag.FlagSet - - printer *printer.Printer - config *config.Config - accountName string - command string - resourceType string - muteDuration TimeDurationFlagValue - muteNotifications bool -} - -func NewMuteOrUnmuteExecutor(printer *printer.Printer, config *config.Config, name, summary string) *MuteOrUnmuteExecutor { - muteDuration := TimeDurationFlagValue{time.Duration(0 * time.Second)} - - exe := MuteOrUnmuteExecutor{ - FlagSet: flag.NewFlagSet(name, flag.ExitOnError), - - printer: printer, - config: config, - command: name, - muteDuration: muteDuration, - } - - exe.StringVar(&exe.accountName, flagAccountName, "", "Specify the account name in full (username@domain)") - exe.StringVar(&exe.resourceType, flagType, "", "Specify the type of resource to mute or unmute") - exe.BoolVar(&exe.muteNotifications, flagMuteNotifications, false, "Mute notifications as well as posts") - exe.Var(&exe.muteDuration, flagMuteDuration, "Specify how long the mute should last for. To mute indefinitely, set this to 0s") - - exe.Usage = commandUsageFunc(name, summary, exe.FlagSet) - - return &exe -} - -func (m *MuteOrUnmuteExecutor) Execute() error { - funcMap := map[string]func(*client.Client) error{ - resourceAccount: m.muteOrUnmuteAccount, - } - - doFunc, ok := funcMap[m.resourceType] - if !ok { - return UnsupportedTypeError{resourceType: m.resourceType} - } - - gtsClient, err := client.NewClientFromFile(m.config.CredentialsFile) - if err != nil { - return fmt.Errorf("unable to create the GoToSocial client: %w", err) - } - - return doFunc(gtsClient) -} - -func (m *MuteOrUnmuteExecutor) muteOrUnmuteAccount(gtsClient *client.Client) error { - if m.accountName == "" { - return FlagNotSetError{flagText: flagAccountName} - } - - accountID, err := getAccountID(gtsClient, false, m.accountName, m.config.CredentialsFile) - if err != nil { - return fmt.Errorf("received an error while getting the account ID: %w", err) - } - - switch m.command { - case CommandMute: - return m.muteAccount(gtsClient, accountID) - case CommandUnmute: - return m.unmuteAccount(gtsClient, accountID) - default: - return nil - } -} - -func (m *MuteOrUnmuteExecutor) muteAccount(gtsClient *client.Client, accountID string) error { - form := client.MuteAccountForm{ - Notifications: m.muteNotifications, - Duration: int(m.muteDuration.Duration.Seconds()), - } - - if err := gtsClient.MuteAccount(accountID, form); err != nil { - return fmt.Errorf("unable to mute the account: %w", err) - } - - m.printer.PrintSuccess("Successfully muted the account.") - - return nil -} - -func (m *MuteOrUnmuteExecutor) unmuteAccount(gtsClient *client.Client, accountID string) error { - if err := gtsClient.UnmuteAccount(accountID); err != nil { - return fmt.Errorf("unable to unmute the account: %w", err) - } - - m.printer.PrintSuccess("Successfully unmuted the account.") - - return nil -} diff --git a/internal/executor/reject.go b/internal/executor/reject.go new file mode 100644 index 0000000..adaf804 --- /dev/null +++ b/internal/executor/reject.go @@ -0,0 +1,40 @@ +package executor + +import ( + "fmt" + + "codeflow.dananglin.me.uk/apollo/enbas/internal/client" +) + +func (r *RejectExecutor) Execute() error { + funcMap := map[string]func(*client.Client) error{ + resourceFollowRequest: r.rejectFollowRequest, + } + + doFunc, ok := funcMap[r.resourceType] + if !ok { + return UnsupportedTypeError{resourceType: r.resourceType} + } + + gtsClient, err := client.NewClientFromFile(r.config.CredentialsFile) + if err != nil { + return fmt.Errorf("unable to create the GoToSocial client: %w", err) + } + + return doFunc(gtsClient) +} + +func (r *RejectExecutor) rejectFollowRequest(gtsClient *client.Client) error { + accountID, err := getAccountID(gtsClient, false, r.accountName, r.config.CredentialsFile) + if err != nil { + return fmt.Errorf("received an error while getting the account ID: %w", err) + } + + if err := gtsClient.RejectFollowRequest(accountID); err != nil { + return fmt.Errorf("unable to reject the follow request: %w", err) + } + + r.printer.PrintSuccess("Successfully rejected the follow request.") + + return nil +} diff --git a/internal/executor/remove.go b/internal/executor/remove.go index bc30e4b..34c770d 100644 --- a/internal/executor/remove.go +++ b/internal/executor/remove.go @@ -1,48 +1,11 @@ package executor import ( - "flag" "fmt" "codeflow.dananglin.me.uk/apollo/enbas/internal/client" - "codeflow.dananglin.me.uk/apollo/enbas/internal/config" - "codeflow.dananglin.me.uk/apollo/enbas/internal/printer" ) -type RemoveExecutor struct { - *flag.FlagSet - - printer *printer.Printer - config *config.Config - resourceType string - fromResourceType string - listID string - statusID string - accountNames MultiStringFlagValue -} - -func NewRemoveExecutor(printer *printer.Printer, config *config.Config, name, summary string) *RemoveExecutor { - emptyArr := make([]string, 0, 3) - - removeExe := RemoveExecutor{ - FlagSet: flag.NewFlagSet(name, flag.ExitOnError), - - printer: printer, - config: config, - accountNames: MultiStringFlagValue(emptyArr), - } - - removeExe.StringVar(&removeExe.resourceType, flagType, "", "Specify the resource type to remove (e.g. account, note)") - removeExe.StringVar(&removeExe.fromResourceType, flagFrom, "", "Specify the resource type to remove from (e.g. list, account, etc)") - removeExe.StringVar(&removeExe.listID, flagListID, "", "The ID of the list to remove from") - removeExe.StringVar(&removeExe.statusID, flagStatusID, "", "The ID of the status") - removeExe.Var(&removeExe.accountNames, flagAccountName, "The name of the account to remove from the resource") - - removeExe.Usage = commandUsageFunc(name, summary, removeExe.FlagSet) - - return &removeExe -} - func (r *RemoveExecutor) Execute() error { if r.fromResourceType == "" { return FlagNotSetError{flagText: flagFrom} @@ -89,19 +52,19 @@ func (r *RemoveExecutor) removeAccountsFromList(gtsClient *client.Client) error return FlagNotSetError{flagText: flagListID} } - if len(r.accountNames) == 0 { + if r.accountNames.Empty() { return NoAccountSpecifiedError{} } - accountIDs := make([]string, len(r.accountNames)) + accounts, err := getOtherAccounts(gtsClient, r.accountNames) + if err != nil { + return fmt.Errorf("unable to get the accounts: %w", err) + } - for ind := range r.accountNames { - accountID, err := getTheirAccountID(gtsClient, r.accountNames[ind]) - if err != nil { - return fmt.Errorf("unable to get the account ID for %s: %w", r.accountNames[ind], err) - } + accountIDs := make([]string, len(accounts)) - accountIDs[ind] = accountID + for ind := range accounts { + accountIDs[ind] = accounts[ind].ID } if err := gtsClient.RemoveAccountsFromList(r.listID, accountIDs); err != nil { @@ -130,11 +93,7 @@ func (r *RemoveExecutor) removeFromAccount(gtsClient *client.Client) error { } func (r *RemoveExecutor) removeNoteFromAccount(gtsClient *client.Client) error { - if len(r.accountNames) != 1 { - return fmt.Errorf("unexpected number of accounts specified: want 1, got %d", len(r.accountNames)) - } - - accountID, err := getAccountID(gtsClient, false, r.accountNames[0], r.config.CredentialsFile) + accountID, err := getAccountID(gtsClient, false, r.accountNames, r.config.CredentialsFile) if err != nil { return fmt.Errorf("received an error while getting the account ID: %w", err) } diff --git a/internal/executor/show.go b/internal/executor/show.go index ea1244e..fafc692 100644 --- a/internal/executor/show.go +++ b/internal/executor/show.go @@ -1,83 +1,15 @@ package executor import ( - "flag" "fmt" "path/filepath" "codeflow.dananglin.me.uk/apollo/enbas/internal/client" - "codeflow.dananglin.me.uk/apollo/enbas/internal/config" "codeflow.dananglin.me.uk/apollo/enbas/internal/media" "codeflow.dananglin.me.uk/apollo/enbas/internal/model" - "codeflow.dananglin.me.uk/apollo/enbas/internal/printer" "codeflow.dananglin.me.uk/apollo/enbas/internal/utilities" ) -type ShowExecutor struct { - *flag.FlagSet - - printer *printer.Printer - config *config.Config - myAccount bool - excludeBoosts bool - excludeReplies bool - onlyMedia bool - onlyPinned bool - onlyPublic bool - showInBrowser bool - showUserPreferences bool - showStatuses bool - skipAccountRelationship bool - getAllImages bool - getAllVideos bool - resourceType string - accountName string - statusID string - timelineCategory string - listID string - tag string - pollID string - fromResourceType string - limit int - attachmentIDs MultiStringFlagValue -} - -func NewShowExecutor(printer *printer.Printer, config *config.Config, name, summary string) *ShowExecutor { - showExe := ShowExecutor{ - FlagSet: flag.NewFlagSet(name, flag.ExitOnError), - - printer: printer, - config: config, - } - - showExe.BoolVar(&showExe.myAccount, flagMyAccount, false, "Set to true to lookup your account") - showExe.BoolVar(&showExe.skipAccountRelationship, flagSkipRelationship, false, "Set to true to skip showing your relationship to the specified account") - showExe.BoolVar(&showExe.showUserPreferences, flagShowPreferences, false, "Show your preferences") - showExe.BoolVar(&showExe.showInBrowser, flagBrowser, false, "Set to true to view in the browser") - showExe.BoolVar(&showExe.showStatuses, flagShowStatuses, false, "Set to true to view the statuses created from the specified account") - showExe.BoolVar(&showExe.excludeReplies, flagExcludeReplies, false, "Set to true to exclude statuses that are a reply to another status") - showExe.BoolVar(&showExe.excludeBoosts, flagExcludeBoosts, false, "Set to true to exclude statuses that are boosts of another status") - showExe.BoolVar(&showExe.onlyPinned, flagOnlyPinned, false, "Set to true to show only the account's pinned statuses") - showExe.BoolVar(&showExe.onlyMedia, flagOnlyMedia, false, "Set to true to show only the statuses with media attachments") - showExe.BoolVar(&showExe.onlyPublic, flagOnlyPublic, false, "Set to true to show only the account's public posts") - showExe.BoolVar(&showExe.getAllImages, flagAllImages, false, "Set to true to show all images from a status") - showExe.BoolVar(&showExe.getAllVideos, flagAllVideos, false, "Set to true to show all videos from a status") - showExe.StringVar(&showExe.resourceType, flagType, "", "Specify the type of resource to display") - showExe.StringVar(&showExe.accountName, flagAccountName, "", "Specify the account name in full (username@domain)") - showExe.StringVar(&showExe.statusID, flagStatusID, "", "Specify the ID of the status to display") - showExe.StringVar(&showExe.timelineCategory, flagTimelineCategory, model.TimelineCategoryHome, "Specify the timeline category to view") - showExe.StringVar(&showExe.listID, flagListID, "", "Specify the ID of the list to display") - showExe.StringVar(&showExe.tag, flagTag, "", "Specify the name of the tag to use") - showExe.StringVar(&showExe.pollID, flagPollID, "", "Specify the ID of the poll to display") - showExe.Var(&showExe.attachmentIDs, flagAttachmentID, "Specify the ID of the media attachment to display") - showExe.StringVar(&showExe.fromResourceType, flagFrom, "", "Specify the resource type to view the target resource from (e.g. status for viewing media from, etc)") - showExe.IntVar(&showExe.limit, flagLimit, 20, "Specify the limit of items to display") - - showExe.Usage = commandUsageFunc(name, summary, showExe.FlagSet) - - return &showExe -} - func (s *ShowExecutor) Execute() error { if s.resourceType == "" { return FlagNotSetError{flagText: flagType} @@ -127,25 +59,9 @@ func (s *ShowExecutor) showInstance(gtsClient *client.Client) error { } func (s *ShowExecutor) showAccount(gtsClient *client.Client) error { - var ( - account model.Account - err error - ) - - if s.myAccount { - account, err = getMyAccount(gtsClient, s.config.CredentialsFile) - if err != nil { - return fmt.Errorf("received an error while getting the account details: %w", err) - } - } else { - if s.accountName == "" { - return FlagNotSetError{flagText: flagAccountName} - } - - account, err = getAccount(gtsClient, s.accountName) - if err != nil { - return fmt.Errorf("received an error while getting the account details: %w", err) - } + account, err := getAccount(gtsClient, s.myAccount, s.accountName, s.config.CredentialsFile) + if err != nil { + return fmt.Errorf("unable to get the account information: %w", err) } if s.showInBrowser { diff --git a/internal/executor/switch.go b/internal/executor/switch.go index bb974d5..b5bc2af 100644 --- a/internal/executor/switch.go +++ b/internal/executor/switch.go @@ -1,61 +1,38 @@ package executor import ( - "flag" "fmt" "codeflow.dananglin.me.uk/apollo/enbas/internal/config" - "codeflow.dananglin.me.uk/apollo/enbas/internal/printer" ) -type SwitchExecutor struct { - *flag.FlagSet - - config *config.Config - printer *printer.Printer - toResourceType string - accountName string -} - -func NewSwitchExecutor(printer *printer.Printer, config *config.Config, name, summary string) *SwitchExecutor { - switchExe := SwitchExecutor{ - FlagSet: flag.NewFlagSet(name, flag.ExitOnError), - - config: config, - printer: printer, - } - - switchExe.StringVar(&switchExe.toResourceType, flagTo, "", "The account to switch to") - switchExe.StringVar(&switchExe.accountName, flagAccountName, "", "The name of the account to switch to") - - switchExe.Usage = commandUsageFunc(name, summary, switchExe.FlagSet) - - return &switchExe -} - func (s *SwitchExecutor) Execute() error { funcMap := map[string]func() error{ resourceAccount: s.switchToAccount, } - doFunc, ok := funcMap[s.toResourceType] + doFunc, ok := funcMap[s.to] if !ok { - return UnsupportedTypeError{resourceType: s.toResourceType} + return UnsupportedTypeError{resourceType: s.to} } return doFunc() } func (s *SwitchExecutor) switchToAccount() error { - if s.accountName == "" { - return NoAccountSpecifiedError{} + expectedNumAccountNames := 1 + if !s.accountName.ExpectedLength(expectedNumAccountNames) { + return fmt.Errorf( + "found an unexpected number of --account-name flags: expected %d", + expectedNumAccountNames, + ) } - if err := config.UpdateCurrentAccount(s.accountName, s.config.CredentialsFile); err != nil { + if err := config.UpdateCurrentAccount(s.accountName[0], s.config.CredentialsFile); err != nil { return fmt.Errorf("unable to switch account to the account: %w", err) } - s.printer.PrintSuccess("The current account is now set to '" + s.accountName + "'.") + s.printer.PrintSuccess("The current account is now set to '" + s.accountName[0] + "'.") return nil } diff --git a/internal/executor/unblock.go b/internal/executor/unblock.go new file mode 100644 index 0000000..831102f --- /dev/null +++ b/internal/executor/unblock.go @@ -0,0 +1,40 @@ +package executor + +import ( + "fmt" + + "codeflow.dananglin.me.uk/apollo/enbas/internal/client" +) + +func (b *UnblockExecutor) Execute() error { + funcMap := map[string]func(*client.Client) error{ + resourceAccount: b.unblockAccount, + } + + doFunc, ok := funcMap[b.resourceType] + if !ok { + return UnsupportedTypeError{resourceType: b.resourceType} + } + + gtsClient, err := client.NewClientFromFile(b.config.CredentialsFile) + if err != nil { + return fmt.Errorf("unable to create the GoToSocial client: %w", err) + } + + return doFunc(gtsClient) +} + +func (b *UnblockExecutor) unblockAccount(gtsClient *client.Client) error { + accountID, err := getAccountID(gtsClient, false, b.accountName, b.config.CredentialsFile) + if err != nil { + return fmt.Errorf("received an error while getting the account ID: %w", err) + } + + if err := gtsClient.UnblockAccount(accountID); err != nil { + return fmt.Errorf("unable to unblock the account: %w", err) + } + + b.printer.PrintSuccess("Successfully unblocked the account.") + + return nil +} diff --git a/internal/executor/unfollow.go b/internal/executor/unfollow.go new file mode 100644 index 0000000..102ce3d --- /dev/null +++ b/internal/executor/unfollow.go @@ -0,0 +1,40 @@ +package executor + +import ( + "fmt" + + "codeflow.dananglin.me.uk/apollo/enbas/internal/client" +) + +func (f *UnfollowExecutor) Execute() error { + funcMap := map[string]func(*client.Client) error{ + resourceAccount: f.unfollowAccount, + } + + doFunc, ok := funcMap[f.resourceType] + if !ok { + return UnsupportedTypeError{resourceType: f.resourceType} + } + + gtsClient, err := client.NewClientFromFile(f.config.CredentialsFile) + if err != nil { + return fmt.Errorf("unable to create the GoToSocial client: %w", err) + } + + return doFunc(gtsClient) +} + +func (f *UnfollowExecutor) unfollowAccount(gtsClient *client.Client) error { + accountID, err := getAccountID(gtsClient, false, f.accountName, f.config.CredentialsFile) + if err != nil { + return fmt.Errorf("received an error while getting the account ID: %w", err) + } + + if err := gtsClient.UnfollowAccount(accountID); err != nil { + return fmt.Errorf("unable to unfollow the account: %w", err) + } + + f.printer.PrintSuccess("Successfully unfollowed the account.") + + return nil +} diff --git a/internal/executor/unmute.go b/internal/executor/unmute.go new file mode 100644 index 0000000..c4f4e83 --- /dev/null +++ b/internal/executor/unmute.go @@ -0,0 +1,40 @@ +package executor + +import ( + "fmt" + + "codeflow.dananglin.me.uk/apollo/enbas/internal/client" +) + +func (m *UnmuteExecutor) Execute() error { + funcMap := map[string]func(*client.Client) error{ + resourceAccount: m.unmuteAccount, + } + + doFunc, ok := funcMap[m.resourceType] + if !ok { + return UnsupportedTypeError{resourceType: m.resourceType} + } + + gtsClient, err := client.NewClientFromFile(m.config.CredentialsFile) + if err != nil { + return fmt.Errorf("unable to create the GoToSocial client: %w", err) + } + + return doFunc(gtsClient) +} + +func (m *UnmuteExecutor) unmuteAccount(gtsClient *client.Client) error { + accountID, err := getAccountID(gtsClient, false, m.accountName, m.config.CredentialsFile) + if err != nil { + return fmt.Errorf("received an error while getting the account ID: %w", err) + } + + if err := gtsClient.UnmuteAccount(accountID); err != nil { + return fmt.Errorf("unable to unmute the account: %w", err) + } + + m.printer.PrintSuccess("Successfully unmuted the account.") + + return nil +} diff --git a/internal/executor/version.go b/internal/executor/version.go index ad89c5a..6fbd357 100644 --- a/internal/executor/version.go +++ b/internal/executor/version.go @@ -1,51 +1,7 @@ package executor -import ( - "flag" - - "codeflow.dananglin.me.uk/apollo/enbas/internal/printer" -) - -type VersionExecutor struct { - *flag.FlagSet - - printer *printer.Printer - showFullVersion bool - binaryVersion string - buildTime string - goVersion string - gitCommit string -} - -func NewVersionExecutor( - printer *printer.Printer, - name, - summary, - binaryVersion, - buildTime, - goVersion, - gitCommit string, -) *VersionExecutor { - command := VersionExecutor{ - FlagSet: flag.NewFlagSet(name, flag.ExitOnError), - - printer: printer, - binaryVersion: binaryVersion, - buildTime: buildTime, - goVersion: goVersion, - gitCommit: gitCommit, - showFullVersion: false, - } - - command.BoolVar(&command.showFullVersion, flagFull, false, "prints the full build information") - - command.Usage = commandUsageFunc(name, summary, command.FlagSet) - - return &command -} - func (v *VersionExecutor) Execute() error { - v.printer.PrintVersion(v.showFullVersion, v.binaryVersion, v.buildTime, v.goVersion, v.gitCommit) + v.printer.PrintVersion(v.full) return nil } diff --git a/internal/executor/whoami.go b/internal/executor/whoami.go index 69677a9..070348c 100644 --- a/internal/executor/whoami.go +++ b/internal/executor/whoami.go @@ -1,40 +1,18 @@ package executor import ( - "flag" "fmt" "codeflow.dananglin.me.uk/apollo/enbas/internal/config" - "codeflow.dananglin.me.uk/apollo/enbas/internal/printer" ) -type WhoAmIExecutor struct { - *flag.FlagSet - - printer *printer.Printer - config *config.Config -} - -func NewWhoAmIExecutor(printer *printer.Printer, config *config.Config, name, summary string) *WhoAmIExecutor { - whoExe := WhoAmIExecutor{ - FlagSet: flag.NewFlagSet(name, flag.ExitOnError), - - printer: printer, - config: config, - } - - whoExe.Usage = commandUsageFunc(name, summary, whoExe.FlagSet) - - return &whoExe -} - -func (c *WhoAmIExecutor) Execute() error { - config, err := config.NewCredentialsConfigFromFile(c.config.CredentialsFile) +func (e *WhoamiExecutor) Execute() error { + config, err := config.NewCredentialsConfigFromFile(e.config.CredentialsFile) if err != nil { return fmt.Errorf("unable to load the credential config: %w", err) } - c.printer.PrintInfo("You are logged in as '" + config.CurrentAccount + "'.\n") + e.printer.PrintInfo("You are logged in as '" + config.CurrentAccount + "'.\n") return nil } diff --git a/internal/flag/boolptrvalue.go b/internal/flag/boolptrvalue.go new file mode 100644 index 0000000..7669c09 --- /dev/null +++ b/internal/flag/boolptrvalue.go @@ -0,0 +1,36 @@ +package flag + +import ( + "fmt" + "strconv" +) + +type BoolPtrValue struct { + Value *bool +} + +func NewBoolPtrValue() BoolPtrValue { + return BoolPtrValue{ + Value: nil, + } +} + +func (b BoolPtrValue) String() string { + if b.Value == nil { + return "NOT SET" + } + + return strconv.FormatBool(*b.Value) +} + +func (b *BoolPtrValue) Set(value string) error { + boolVar, err := strconv.ParseBool(value) + if err != nil { + return fmt.Errorf("unable to parse %q as a boolean value: %w", value, err) + } + + b.Value = new(bool) + *b.Value = boolVar + + return nil +} diff --git a/internal/flag/intslice.go b/internal/flag/intslice.go new file mode 100644 index 0000000..ed21fde --- /dev/null +++ b/internal/flag/intslice.go @@ -0,0 +1,49 @@ +package flag + +import ( + "fmt" + "strconv" + "strings" +) + +type IntSliceValue []int + +func NewIntSliceValue() IntSliceValue { + arr := make([]int, 0, 3) + + return IntSliceValue(arr) +} + +func (v IntSliceValue) String() string { + var builder strings.Builder + + for ind, value := range v { + if ind == len(v)-1 { + builder.WriteString(strconv.Itoa(value)) + } else { + builder.WriteString(strconv.Itoa(value)) + builder.WriteString(", ") + } + } + + return builder.String() +} + +func (v *IntSliceValue) Set(text string) error { + value, err := strconv.Atoi(text) + if err != nil { + return fmt.Errorf("unable to parse the value to an integer: %w", err) + } + + *v = append(*v, value) + + return nil +} + +func (v IntSliceValue) Empty() bool { + return len(v) == 0 +} + +func (v IntSliceValue) ExpectedLength(expectedLength int) bool { + return len(v) == expectedLength +} diff --git a/internal/flag/stringslice.go b/internal/flag/stringslice.go new file mode 100644 index 0000000..7189c2b --- /dev/null +++ b/internal/flag/stringslice.go @@ -0,0 +1,31 @@ +package flag + +import "strings" + +type StringSliceValue []string + +func NewStringSliceValue() StringSliceValue { + arr := make([]string, 0, 3) + + return StringSliceValue(arr) +} + +func (v StringSliceValue) String() string { + return strings.Join(v, ", ") +} + +func (v *StringSliceValue) Set(value string) error { + if len(value) > 0 { + *v = append(*v, value) + } + + return nil +} + +func (v StringSliceValue) Empty() bool { + return len(v) == 0 +} + +func (v StringSliceValue) ExpectedLength(expectedLength int) bool { + return len(v) == expectedLength +} diff --git a/internal/flag/timedurationvalue.go b/internal/flag/timedurationvalue.go new file mode 100644 index 0000000..cdf02ac --- /dev/null +++ b/internal/flag/timedurationvalue.go @@ -0,0 +1,31 @@ +package flag + +import ( + "fmt" + "time" +) + +type TimeDurationValue struct { + Duration time.Duration +} + +func NewTimeDurationValue() TimeDurationValue { + return TimeDurationValue{ + Duration: 0 * time.Second, + } +} + +func (v TimeDurationValue) String() string { + return v.Duration.String() +} + +func (v *TimeDurationValue) Set(text string) error { + duration, err := time.ParseDuration(text) + if err != nil { + return fmt.Errorf("unable to parse the value as time duration: %w", err) + } + + v.Duration = duration + + return nil +} diff --git a/internal/printer/version.go b/internal/printer/version.go index 93e9c5e..430624d 100644 --- a/internal/printer/version.go +++ b/internal/printer/version.go @@ -3,11 +3,13 @@ package printer import ( "strings" "text/tabwriter" + + "codeflow.dananglin.me.uk/apollo/enbas/internal/version" ) -func (p Printer) PrintVersion(showFullVersion bool, binaryVersion, buildTime, goVersion, gitCommit string) { +func (p Printer) PrintVersion(showFullVersion bool) { if !showFullVersion { - printToStdout("Enbas " + binaryVersion + "\n") + printToStdout("Enbas " + version.BinaryVersion + "\n") return } @@ -18,10 +20,10 @@ func (p Printer) PrintVersion(showFullVersion bool, binaryVersion, buildTime, go tableWriter := tabwriter.NewWriter(&builder, 0, 4, 1, ' ', 0) - _, _ = tableWriter.Write([]byte(p.fieldFormat("Version:") + "\t" + binaryVersion + "\n")) - _, _ = tableWriter.Write([]byte(p.fieldFormat("Git commit:") + "\t" + gitCommit + "\n")) - _, _ = tableWriter.Write([]byte(p.fieldFormat("Go version:") + "\t" + goVersion + "\n")) - _, _ = tableWriter.Write([]byte(p.fieldFormat("Build date:") + "\t" + buildTime + "\n")) + _, _ = tableWriter.Write([]byte(p.fieldFormat("Version:") + "\t" + version.BinaryVersion + "\n")) + _, _ = tableWriter.Write([]byte(p.fieldFormat("Git commit:") + "\t" + version.GitCommit + "\n")) + _, _ = tableWriter.Write([]byte(p.fieldFormat("Go version:") + "\t" + version.GoVersion + "\n")) + _, _ = tableWriter.Write([]byte(p.fieldFormat("Build date:") + "\t" + version.BuildTime + "\n")) tableWriter.Flush() diff --git a/cmd/enbas/usage.go b/internal/usage/app.go similarity index 82% rename from cmd/enbas/usage.go rename to internal/usage/app.go index 48a6332..8ab4d09 100644 --- a/cmd/enbas/usage.go +++ b/internal/usage/app.go @@ -1,4 +1,4 @@ -package main +package usage import ( "flag" @@ -6,9 +6,11 @@ import ( "slices" "strings" "text/tabwriter" + + "codeflow.dananglin.me.uk/apollo/enbas/internal/version" ) -func usageFunc(summaries map[string]string) func() { +func AppUsageFunc() func() { cmds := make([]string, len(summaries)) ind := 0 @@ -24,8 +26,8 @@ func usageFunc(summaries map[string]string) func() { builder.WriteString("SUMMARY:\n enbas - A GoToSocial client for the terminal.\n\n") - if binaryVersion != "" { - builder.WriteString("VERSION:\n " + binaryVersion + "\n\n") + if version.BinaryVersion != "" { + builder.WriteString("VERSION:\n " + version.BinaryVersion + "\n\n") } builder.WriteString("USAGE:\n enbas [flags]\n enbas [flags] [command]\n\nCOMMANDS:") diff --git a/internal/usage/codegen.go b/internal/usage/codegen.go new file mode 100644 index 0000000..96bef50 --- /dev/null +++ b/internal/usage/codegen.go @@ -0,0 +1,3 @@ +package usage + +//go:generate go run ../../cmd/enbas-codegen --package usage --path-to-enbas-cli-schema ../../schema/enbas_cli_schema.json diff --git a/internal/executor/usage.go b/internal/usage/executor.go similarity index 83% rename from internal/executor/usage.go rename to internal/usage/executor.go index 67d2718..63bf890 100644 --- a/internal/executor/usage.go +++ b/internal/usage/executor.go @@ -1,4 +1,4 @@ -package executor +package usage import ( "flag" @@ -6,8 +6,8 @@ import ( "strings" ) -// commandUsageFunc returns the function used to print a command's help page. -func commandUsageFunc(name, summary string, flagset *flag.FlagSet) func() { +// ExecutorUsageFunc returns the function used to print a command's help page. +func ExecutorUsageFunc(name, summary string, flagset *flag.FlagSet) func() { return func() { var builder strings.Builder diff --git a/internal/usage/summaries.go b/internal/usage/summaries.go new file mode 100644 index 0000000..837f764 --- /dev/null +++ b/internal/usage/summaries.go @@ -0,0 +1,28 @@ +/* + This file is generated by the enbas-codegen + DO NOT EDIT. +*/ + +package usage + +var summaries = map[string]string{ + "accept": "Accepts a request (e.g. a follow request)", + "add": "Adds a resource to another resource", + "block": "Blocks a resource (e.g. an account)", + "create": "Creates a specific resource", + "delete": "Deletes a specific resource", + "edit": "Edit a specific resource", + "follow": "Follow a resource (e.g. an account)", + "init": "Creates a new configuration file in the specified configuration directory", + "login": "Logs into an account on GoToSocial", + "mute": "Mutes a specific resource (e.g. an account)", + "reject": "Rejects a request (e.g. a follow request)", + "remove": "Removes a resource from another resource", + "show": "Shows details about a specified resource", + "switch": "Performs a switch operation (e.g. switching between logged in accounts)", + "unblock": "Unblocks a resource (e.g. an account)", + "unfollow": "Unfollows a resource (e.g. an account)", + "unmute": "Umutes a specific resource (e.g. an account)", + "version": "Prints the application's version and build information", + "whoami": "Prints the account that you are currently logged into", +} diff --git a/internal/version/version.go b/internal/version/version.go new file mode 100644 index 0000000..65ed05f --- /dev/null +++ b/internal/version/version.go @@ -0,0 +1,8 @@ +package version + +var ( + BinaryVersion string //nolint:gochecknoglobals + BuildTime string //nolint:gochecknoglobals + GoVersion string //nolint:gochecknoglobals + GitCommit string //nolint:gochecknoglobals +) diff --git a/magefiles/mage.go b/magefiles/mage.go index 55ab68b..7f861c1 100644 --- a/magefiles/mage.go +++ b/magefiles/mage.go @@ -110,10 +110,21 @@ func Clean() error { // ldflags returns the build flags. func ldflags() string { - ldflagsfmt := "-s -w -X main.binaryVersion=%s -X main.gitCommit=%s -X main.goVersion=%s -X main.buildTime=%s" + versionPackage := "codeflow.dananglin.me.uk/apollo/enbas/internal/version" + binaryVersionVar := versionPackage + "." + "BinaryVersion" + gitCommitVar := versionPackage + "." + "GitCommit" + goVersionVar := versionPackage + "." + "GoVersion" + buildTimeVar := versionPackage + "." + "BuildTime" + ldflagsfmt := "-s -w -X %s=%s -X %s=%s -X %s=%s -X %s=%s" buildTime := time.Now().UTC().Format(time.RFC3339) - return fmt.Sprintf(ldflagsfmt, version(), gitCommit(), runtime.Version(), buildTime) + return fmt.Sprintf( + ldflagsfmt, + binaryVersionVar, version(), + gitCommitVar, gitCommit(), + goVersionVar, runtime.Version(), + buildTimeVar, buildTime, + ) } // version returns the latest git tag using git describe. diff --git a/schema/enbas_cli_schema.json b/schema/enbas_cli_schema.json new file mode 100644 index 0000000..b441830 --- /dev/null +++ b/schema/enbas_cli_schema.json @@ -0,0 +1,440 @@ +{ + "flags": { + "account-name": { + "type": "StringSliceValue", + "description": "The name of the account" + }, + "all-images": { + "type": "bool", + "description": "Set to true to show all images from a status" + }, + "all-videos": { + "type": "bool", + "description": "Set to true to show all videos from a status" + }, + "attachment-id": { + "type": "StringSliceValue", + "description": "The ID of the media attachment" + }, + "add-poll": { + "type": "bool", + "description": "Set to true to add a poll when creating a status" + }, + "browser": { + "type": "bool", + "description": "Set to true to view in the your favourite browser" + }, + "content": { + "type": "string", + "description": "The content of the created resource" + }, + "content-type": { + "type": "string", + "description": "The type that the contents should be parsed from (valid values are plain and markdown)" + }, + "enable-federation": { + "type": "bool", + "description": "Set to true to federate the status beyond the local timelines" + }, + "enable-likes": { + "type": "bool", + "description": "Set to true to allow the status to be liked (favourited)" + }, + "enable-replies": { + "type": "bool", + "description": "Set to true to allow viewers to reply to the status" + }, + "enable-reposts": { + "type": "bool", + "description": "Set to true to allow the status to be reposted (boosted) by others" + }, + "exclude-boosts": { + "type": "bool", + "description": "Set to true to exclude statuses that are boosts of another status" + }, + "exclude-replies": { + "type": "bool", + "description": "Set to true to exclude statuses that are a reply to another status" + }, + "from": { + "type": "string", + "description": "The resource type to action the target resource from (e.g. status)" + }, + "from-file": { + "type": "string", + "description": "The file path where to read the contents from" + }, + "full": { + "type": "bool", + "description": "Set to true to print the build information in full" + }, + "in-reply-to": { + "type": "string", + "description": "The ID of the status that you want to reply to" + }, + "instance": { + "type": "string", + "description": "The instance that you want to log into" + }, + "language": { + "type": "string", + "description": "The ISO 639 language code for this status" + }, + "limit": { + "type": "int", + "description": "Specify the limit of items to display" + }, + "list-id": { + "type": "string", + "description": "The ID of the list in question" + }, + "list-title": { + "type": "string", + "description": "The title of the list" + }, + "list-replies-policy": { + "type": "string", + "description": "The replies policy of the list" + }, + "mute-duration": { + "type": "TimeDurationValue", + "description": "Specify how long the mute should last for. To mute indefinitely, set this to 0s" + }, + "mute-notifications": { + "type": "bool", + "description": "Set to true to mute notifications as well as posts" + }, + "my-account": { + "type": "bool", + "description": "Set to true to specify your account" + }, + "notify": { + "type": "bool", + "description": "Get notifications from statuses from the account you want to follow" + }, + "only-media": { + "type": "bool", + "description": "Set to true to show only the statuses with media attachments" + }, + "only-pinned": { + "type": "bool", + "description": "Set to true to show only the account's pinned statuses" + }, + "only-public": { + "type": "bool", + "description": "Set to true to show only the account's public posts" + }, + "poll-allows-multiple-choices": { + "type": "bool", + "description": "Set to true to allow viewers to make multiple choices in the poll" + }, + "poll-expires-in": { + "type": "TimeDurationValue", + "description": "The duration in which the poll is open for" + }, + "poll-hides-vote-counts": { + "type": "bool", + "description": "Set to true to hide the vote count until the poll is closed" + }, + "poll-id": { + "type": "string", + "description": "The ID of the poll" + }, + "poll-option": { + "type": "StringSliceValue", + "description": "A poll option. Use this multiple times to set multiple options" + }, + "sensitive": { + "type": "BoolPtrValue", + "description": "Set to true if the status should be marked as sensitive" + }, + "show-preferences": { + "type": "bool", + "description": "Set to true to view your posting preferences when viewing your account information" + }, + "show-reposts": { + "type": "bool", + "description": "Show reposts from the account you want to follow" + }, + "show-statuses": { + "type": "bool", + "description": "Set to true to view the statuses created from the account you are viewing" + }, + "skip-relationship": { + "type": "bool", + "description": "Set to true to skip showing your relationship to the account that you are viewing" + }, + "spoiler-text": { + "type": "string", + "description": "The text to display as the status' warning or subject" + }, + "status-id": { + "type": "string", + "description": "The ID of the status" + }, + "tag": { + "type": "string", + "description": "The name of the tag" + }, + "timeline-category": { + "type": "string", + "description": "The timeline category" + }, + "to": { + "type": "string", + "description": "The resource type to action the target resource to (e.g. status)" + }, + "type": { + "type": "string", + "description": "The type of resource you want to action on (e.g. account, status)" + }, + "visibility": { + "type": "string", + "description": "The visibility of the posted status" + }, + "vote": { + "type": "IntSliceValue", + "description": "Add a vote to an option in a poll" + } + }, + + "commands": { + "accept": { + "additionalFields": [], + "flags": [ + { "flag": "account-name" }, + { "flag": "type", "fieldName": "resourceType", "default": "" } + ], + "summary": "Accepts a request (e.g. a follow request)", + "useConfig": true, + "usePrinter": true + }, + "add": { + "additionalFields": [], + "flags": [ + { "flag": "account-name", "fieldName": "accountNames" }, + { "flag": "content", "default": "" }, + { "flag": "list-id", "fieldName": "listID", "default": "" }, + { "flag": "poll-id", "fieldName": "pollID", "default": "" }, + { "flag": "status-id", "fieldName": "statusID", "default": "" }, + { "flag": "to", "fieldName": "toResourceType", "default": "" }, + { "flag": "type", "fieldName": "resourceType", "default": "" }, + { "flag": "vote", "fieldName": "votes" } + ], + "summary": "Adds a resource to another resource", + "useConfig": true, + "usePrinter": true + }, + "block": { + "additionalFields": [], + "flags": [ + { "flag": "account-name" }, + { "flag": "type", "fieldName": "resourceType", "default": "" } + ], + "summary": "Blocks a resource (e.g. an account)", + "useConfig": true, + "usePrinter": true + }, + "create": { + "additionalFields": [], + "flags": [ + { "flag": "add-poll", "default": "false" }, + { "flag": "content", "default": "" }, + { "flag": "content-type", "default": "plain" }, + { "flag": "enable-federation", "fieldName": "federated", "default": "true" }, + { "flag": "enable-likes", "fieldName": "likeable", "default": "true" }, + { "flag": "enable-replies", "fieldName": "replyable", "default": "true" }, + { "flag": "enable-reposts", "fieldName": "boostable", "default": "true" }, + { "flag": "from-file", "default": "" }, + { "flag": "in-reply-to", "default": "" }, + { "flag": "language", "default": "" }, + { "flag": "list-replies-policy", "default": "list" }, + { "flag": "list-title", "default": "" }, + { "flag": "poll-allows-multiple-choices", "default": "false" }, + { "flag": "poll-expires-in" }, + { "flag": "poll-hides-vote-counts", "default": "false" }, + { "flag": "poll-option", "fieldName": "pollOptions" }, + { "flag": "sensitive" }, + { "flag": "spoiler-text", "default": "" }, + { "flag": "type", "fieldName": "resourceType", "default": "" }, + { "flag": "visibility", "default": "" } + ], + "summary": "Creates a specific resource", + "useConfig": true, + "usePrinter": true + }, + "delete": { + "additionalFields": [], + "flags": [ + { "flag": "list-id", "fieldName": "listID", "default": ""}, + { "flag": "type", "fieldName": "resourceType", "default": "" } + ], + "summary": "Deletes a specific resource", + "useConfig": true, + "usePrinter": true + }, + "edit": { + "additionalFields": [], + "flags": [ + { "flag": "list-id", "fieldName": "listID", "default": ""}, + { "flag": "list-title", "default": "" }, + { "flag": "list-replies-policy", "default": "" }, + { "flag": "type", "fieldName": "resourceType", "default": "" } + ], + "summary": "Edit a specific resource", + "useConfig": true, + "usePrinter": true + }, + "follow": { + "additionalFields": [], + "flags": [ + { "flag": "account-name" }, + { "flag": "notify", "default": "false" }, + { "flag": "show-reposts", "default": "true" }, + { "flag": "type", "fieldName": "resourceType", "default": "" } + ], + "summary": "Follow a resource (e.g. an account)", + "useConfig": true, + "usePrinter": true + }, + "init": { + "additionalFields": [ + { "name": "configDir", "type": "string"} + ], + "flags": [], + "summary": "Creates a new configuration file in the specified configuration directory", + "useConfig": false, + "usePrinter": true + }, + "login": { + "additionalFields": [], + "flags": [ + { "flag": "instance", "default": "" } + ], + "summary": "Logs into an account on GoToSocial", + "useConfig": true, + "usePrinter": true + }, + "mute": { + "additionalFields": [], + "flags": [ + { "flag": "account-name" }, + { "flag": "mute-duration" }, + { "flag": "mute-notifications", "default": "false" }, + { "flag": "type", "fieldName": "resourceType", "default": "" } + ], + "summary": "Mutes a specific resource (e.g. an account)", + "useConfig": true, + "usePrinter": true + }, + "reject": { + "additionalFields": [], + "flags": [ + { "flag": "account-name" }, + { "flag": "type", "fieldName": "resourceType", "default": "" } + ], + "summary": "Rejects a request (e.g. a follow request)", + "useConfig": true, + "usePrinter": true + }, + "remove": { + "additionalFields": [], + "flags": [ + { "flag": "account-name", "fieldName": "accountNames" }, + { "flag": "from", "fieldName": "fromResourceType", "default": "" }, + { "flag": "list-id", "fieldName": "listID", "default": "" }, + { "flag": "status-id", "fieldName": "statusID", "default": "" }, + { "flag": "type", "fieldName": "resourceType", "default": "" } + ], + "summary": "Removes a resource from another resource", + "useConfig": true, + "usePrinter": true + }, + "show": { + "additionalFields": [], + "flags": [ + { "flag": "account-name" }, + { "flag": "all-images", "fieldName": "getAllImages", "default": "false" }, + { "flag": "all-videos", "fieldName": "getAllVideos", "default": "false" }, + { "flag": "attachment-id", "fieldName": "attachmentIDs" }, + { "flag": "browser", "fieldName": "showInBrowser", "default": "false" }, + { "flag": "exclude-boosts", "default": "false" }, + { "flag": "exclude-replies", "default": "false" }, + { "flag": "from", "fieldName": "fromResourceType", "default": "" }, + { "flag": "limit", "default": "20" }, + { "flag": "list-id", "fieldName": "listID", "default": "" }, + { "flag": "my-account", "default": "false" }, + { "flag": "only-media", "default": "false" }, + { "flag": "only-pinned", "default": "false" }, + { "flag": "only-public", "default": "false" }, + { "flag": "poll-id", "fieldName": "pollID", "default": "" }, + { "flag": "show-preferences", "fieldName": "showUserPreferences", "default": "false" }, + { "flag": "show-statuses", "default": "false" }, + { "flag": "skip-relationship", "fieldName": "skipAccountRelationship", "default": "false" }, + { "flag": "status-id", "fieldName": "statusID", "default": "" }, + { "flag": "timeline-category", "default": "home" }, + { "flag": "tag", "default": "" }, + { "flag": "type", "fieldName": "resourceType", "default": "" } + ], + "summary": "Shows details about a specified resource", + "useConfig": true, + "usePrinter": true + }, + "switch": { + "additionalFields": [], + "flags": [ + { "flag": "account-name" }, + { "flag": "to", "default": "" } + ], + "summary": "Performs a switch operation (e.g. switching between logged in accounts)", + "useConfig": true, + "usePrinter": true + }, + "unblock": { + "additionalFields": [], + "flags": [ + { "flag": "account-name" }, + { "flag": "type", "fieldName": "resourceType", "default": "" } + ], + "summary": "Unblocks a resource (e.g. an account)", + "useConfig": true, + "usePrinter": true + }, + "unfollow": { + "additionalFields": [], + "flags": [ + { "flag": "account-name" }, + { "flag": "type", "fieldName": "resourceType", "default": "" } + ], + "summary": "Unfollows a resource (e.g. an account)", + "useConfig": true, + "usePrinter": true + }, + "unmute": { + "additionalFields": [], + "flags": [ + { "flag": "account-name" }, + { "flag": "type", "fieldName": "resourceType", "default": "" } + ], + "summary": "Umutes a specific resource (e.g. an account)", + "useConfig": true, + "usePrinter": true + }, + "version": { + "additionalFields": [], + "flags": [ + { "flag": "full", "default": "false" } + ], + "summary": "Prints the application's version and build information", + "useConfig": false, + "usePrinter": true + }, + "whoami": { + "additionalFields": [], + "flags": [], + "summary": "Prints the account that you are currently logged into", + "useConfig": true, + "usePrinter": true + } + } +}