From 310ecb0b39b1e60c267a0e487e79e4ffadd55530 Mon Sep 17 00:00:00 2001 From: Dan Anglin Date: Thu, 22 Feb 2024 11:56:20 +0000 Subject: [PATCH] checkpoint: some progress was made today - You can now update the current account. - Enbas now shows local and remote account information. - A lot of refactoring was performed to update codebase. --- cmd/enbas/instance.go | 64 ---------------- cmd/enbas/login.go | 13 ++-- cmd/enbas/main.go | 87 +++++++++++++-------- cmd/enbas/show.go | 155 ++++++++++++++++++++++++++++++++++++++ cmd/enbas/switch.go | 35 +++++++++ cmd/enbas/version.go | 4 +- internal/client/client.go | 21 ++++++ internal/config/config.go | 62 +++++++++++---- 8 files changed, 321 insertions(+), 120 deletions(-) delete mode 100644 cmd/enbas/instance.go create mode 100644 cmd/enbas/show.go create mode 100644 cmd/enbas/switch.go diff --git a/cmd/enbas/instance.go b/cmd/enbas/instance.go deleted file mode 100644 index c8094f6..0000000 --- a/cmd/enbas/instance.go +++ /dev/null @@ -1,64 +0,0 @@ -package main - -import ( - "flag" - "fmt" - - "codeflow.dananglin.me.uk/apollo/enbas/internal/client" -) - -var instanceDetailsFormat = `INSTANCE: - %s - %s - -DOMAIN: - %s - -VERSION: - Running GoToSocial %s - -CONTACT: - name: %s - username: %s - email: %s -` - -type instanceCommand struct { - *flag.FlagSet - summary string -} - -func newInstanceCommand(name, summary string) *instanceCommand { - command := instanceCommand{ - FlagSet: flag.NewFlagSet(name, flag.ExitOnError), - summary: summary, - } - - command.Usage = commandUsageFunc(command.Name(), command.summary, command.FlagSet) - - return &command -} - -func (c *instanceCommand) Execute() error { - gtsClient, err := client.NewClientFromConfig() - if err != nil { - return fmt.Errorf("unable to create the GoToSocial client; %w", err) - } - - instance, err := gtsClient.GetInstance() - if err != nil { - return fmt.Errorf("unable to retrieve the instance details; %w", err) - } - - fmt.Printf( - instanceDetailsFormat, - instance.Title, - instance.Description, - instance.Domain, - instance.Version, - instance.Contact.Account.DisplayName, - instance.Contact.Account.Username, - instance.Contact.Email, - ) - - return nil -} diff --git a/cmd/enbas/login.go b/cmd/enbas/login.go index c91cf20..1fc7266 100644 --- a/cmd/enbas/login.go +++ b/cmd/enbas/login.go @@ -17,12 +17,13 @@ import ( type loginCommand struct { *flag.FlagSet - summary string instance string } -var errEmptyAccessToken = errors.New("received an empty access token") -var errInstanceNotSet = errors.New("the instance flag is not set") +var ( + errEmptyAccessToken = errors.New("received an empty access token") + errInstanceNotSet = errors.New("the instance flag is not set") +) var consentMessageFormat = ` You'll need to sign into your GoToSocial's consent page in order to generate the out-of-band token to continue with @@ -37,13 +38,13 @@ Once you have the code please copy and paste it below. func newLoginCommand(name, summary string) *loginCommand { command := loginCommand{ - FlagSet: flag.NewFlagSet(name, flag.ExitOnError), - summary: summary, + FlagSet: flag.NewFlagSet(name, flag.ExitOnError), + instance: "", } command.StringVar(&command.instance, "instance", "", "specify the instance that you want to login to.") - command.Usage = commandUsageFunc(command.Name(), command.summary, command.FlagSet) + command.Usage = commandUsageFunc(name, summary, command.FlagSet) return &command } diff --git a/cmd/enbas/main.go b/cmd/enbas/main.go index 2753cbb..66157ed 100644 --- a/cmd/enbas/main.go +++ b/cmd/enbas/main.go @@ -8,23 +8,32 @@ import ( "strings" ) -const ( - login string = "login" - version string = "version" - instance string = "instance" -) - type Executor interface { - Parse([]string) error Name() string + Parse([]string) error Execute() error } func main() { + if err := run(); err != nil { + fmt.Printf("ERROR: %v.\n", err) + os.Exit(1) + } +} + +func run() error { + const ( + login string = "login" + version string = "version" + show string = "show" + switchAccount string = "switch" + ) + summaries := map[string]string{ - login: "login to an account on GoToSocial", - version: "print the application's version and build information", - instance: "print the instance information", + login: "login to an account on GoToSocial", + version: "print the application's version and build information", + show: "print details about a specified resource", + switchAccount: "switch to an account", } flag.Usage = enbasUsageFunc(summaries) @@ -33,7 +42,8 @@ func main() { if flag.NArg() < 1 { flag.Usage() - os.Exit(0) + + return nil } subcommand := flag.Arg(0) @@ -46,33 +56,46 @@ func main() { executor = newLoginCommand(login, summaries[login]) case version: executor = newVersionCommand(version, summaries[version]) - case instance: - executor = newInstanceCommand(instance, summaries[instance]) + case show: + executor = newShowCommand(show, summaries[show]) + case switchAccount: + executor = newSwitchCommand(switchAccount, summaries[switchAccount]) default: - fmt.Printf("ERROR: Unknown subcommand: %s\n", subcommand) flag.Usage() - os.Exit(1) + return fmt.Errorf("unknown subcommand %q", subcommand) } if err := executor.Parse(args); err != nil { - fmt.Printf("ERROR: Unable to parse the command line flags; %v.\n", err) - os.Exit(1) + return fmt.Errorf("unable to parse the command line flags; %w", err) } if err := executor.Execute(); err != nil { - fmt.Printf("ERROR: Unable to run %q; %v.\n", executor.Name(), err) - os.Exit(1) + return fmt.Errorf("received an error after executing %q; %w", executor.Name(), err) } + + return nil } func commandUsageFunc(name, summary string, flagset *flag.FlagSet) func() { return func() { var builder strings.Builder - fmt.Fprintf(&builder, "SUMMARY:\n %s - %s\n\nUSAGE:\n enbas %s [flags]\n\nFLAGS:", name, summary, name) + fmt.Fprintf( + &builder, + "SUMMARY:\n %s - %s\n\nUSAGE:\n enbas %s [flags]\n\nFLAGS:", + name, + summary, + name, + ) flagset.VisitAll(func(f *flag.Flag) { - fmt.Fprintf(&builder, "\n -%s, --%s\n %s", f.Name, f.Name, f.Usage) + fmt.Fprintf( + &builder, + "\n -%s, --%s\n %s", + f.Name, + f.Name, + f.Usage, + ) }) builder.WriteString("\n") @@ -97,21 +120,21 @@ func enbasUsageFunc(summaries map[string]string) func() { return func() { var builder strings.Builder - builder.WriteString("SUMMARY:\n enbas - A GoToSocial client for the terminal.\n\n") + builder.WriteString("SUMMARY:\n enbas - A GoToSocial client for the terminal.\n\n") - //if binaryVersion != "" { - // builder.WriteString("VERSION:\n " + binaryVersion + "\n\n") - //} - - builder.WriteString("USAGE:\n enbas [flags]\n enbas [command]\n\nCOMMANDS:") - - for _, cmd := range cmds { - fmt.Fprintf(&builder, "\n %s\t%s", cmd, summaries[cmd]) + if binaryVersion != "" { + builder.WriteString("VERSION:\n " + binaryVersion + "\n\n") } - builder.WriteString("\n\nFLAGS:\n -help, --help\n print the help message\n") + builder.WriteString("USAGE:\n enbas [flags]\n enbas [command]\n\nCOMMANDS:") + + for _, cmd := range cmds { + fmt.Fprintf(&builder, "\n %s\t%s", cmd, summaries[cmd]) + } + + builder.WriteString("\n\nFLAGS:\n -help, --help\n print the help message\n") flag.VisitAll(func(f *flag.Flag) { - fmt.Fprintf(&builder, "\n -%s, --%s\n %s\n", f.Name, f.Name, f.Usage) + fmt.Fprintf(&builder, "\n -%s, --%s\n %s\n", f.Name, f.Name, f.Usage) }) builder.WriteString("\nUse \"enbas [command] --help\" for more information about a command.\n") diff --git a/cmd/enbas/show.go b/cmd/enbas/show.go new file mode 100644 index 0000000..a814ed6 --- /dev/null +++ b/cmd/enbas/show.go @@ -0,0 +1,155 @@ +package main + +import ( + "errors" + "flag" + "fmt" + + "codeflow.dananglin.me.uk/apollo/enbas/internal/client" + "codeflow.dananglin.me.uk/apollo/enbas/internal/config" +) + +var instanceDetailsFormat = `INSTANCE: + %s - %s + +DOMAIN: + %s + +VERSION: + Running GoToSocial %s + +CONTACT: + name: %s + username: %s + email: %s +` + +var accountDetailsFormat = ` +%s (@%s) + +ACCOUNT ID: + %s + +CREATED AT: + %s + +STATS: + Followers: %d + Following: %d + Statuses: %d + +BIOGRAPHY: + %s + +METADATA: %s + +ACCOUNT URL: + %s +` + +type showCommand struct { + *flag.FlagSet + targetType string + account string + myAccount bool +} + +func newShowCommand(name, summary string) *showCommand { + command := showCommand{ + FlagSet: flag.NewFlagSet(name, flag.ExitOnError), + targetType: "", + myAccount: false, + } + + command.StringVar(&command.targetType, "type", "", "specify the type of resource to display") + command.StringVar(&command.account, "account", "", "specify the account URI to lookup") + command.BoolVar(&command.myAccount, "my-account", false, "set to true to lookup your account") + command.Usage = commandUsageFunc(name, summary, command.FlagSet) + + return &command +} + +func (c *showCommand) Execute() error { + gtsClient, err := client.NewClientFromConfig() + if err != nil { + return fmt.Errorf("unable to create the GoToSocial client; %w", err) + } + + funcMap := map[string]func(*client.Client) error{ + "instance": c.showInstance, + "account": c.showAccount, + } + + doFunc, ok := funcMap[c.targetType] + if !ok { + return fmt.Errorf("unsupported type %q", c.targetType) + } + + return doFunc(gtsClient) +} + +func (c *showCommand) showInstance(gts *client.Client) error { + instance, err := gts.GetInstance() + if err != nil { + return fmt.Errorf("unable to retrieve the instance details; %w", err) + } + + fmt.Printf( + instanceDetailsFormat, + instance.Title, + instance.Description, + instance.Domain, + instance.Version, + instance.Contact.Account.DisplayName, + instance.Contact.Account.Username, + instance.Contact.Email, + ) + + return nil +} + +func (c *showCommand) showAccount(gts *client.Client) error { + var accountURI string + + if c.myAccount { + authConfig, err := config.NewAuthenticationConfigFromFile() + if err != nil { + return fmt.Errorf("unable to retrieve the authentication configuration; %w", err) + } + + accountURI = authConfig.CurrentAccount + } else { + if c.account == "" { + return errors.New("the account flag is not set") + } + + accountURI = c.account + } + + account, err := gts.GetAccount(accountURI) + if err != nil { + return fmt.Errorf("unable to retrieve the account details; %w", err) + } + + metadata := "" + + for _, field := range account.Fields { + metadata += fmt.Sprintf("\n %s: %s", field.Name, field.Value) + } + + fmt.Printf( + accountDetailsFormat, + account.DisplayName, + account.Username, + account.ID, + account.CreatedAt, + account.FollowersCount, + account.FollowingCount, + account.StatusCount, + account.Note, + metadata, + account.URL, + ) + + return nil +} diff --git a/cmd/enbas/switch.go b/cmd/enbas/switch.go new file mode 100644 index 0000000..19155e4 --- /dev/null +++ b/cmd/enbas/switch.go @@ -0,0 +1,35 @@ +package main + +import ( + "flag" + "fmt" + + "codeflow.dananglin.me.uk/apollo/enbas/internal/config" +) + +type switchCommand struct { + *flag.FlagSet + toAccount string +} + +func newSwitchCommand(name, summary string) *switchCommand { + command := switchCommand{ + FlagSet: flag.NewFlagSet(name, flag.ExitOnError), + toAccount: "", + } + + command.StringVar(&command.toAccount, "to-account", "", "the account to switch to") + command.Usage = commandUsageFunc(name, summary, command.FlagSet) + + return &command +} + +func (c *switchCommand) Execute() error { + if err := config.UpdateCurrentAccount(c.toAccount); err != nil { + return fmt.Errorf("unable to switch accounts; %w", err) + } + + fmt.Printf("The current account is now set to %q.\n", c.toAccount) + + return nil +} diff --git a/cmd/enbas/version.go b/cmd/enbas/version.go index 61bebe0..f41c65c 100644 --- a/cmd/enbas/version.go +++ b/cmd/enbas/version.go @@ -16,7 +16,6 @@ var ( type versionCommand struct { *flag.FlagSet - summary string showFullVersion bool binaryVersion string buildTime string @@ -31,13 +30,12 @@ func newVersionCommand(name, summary string) *versionCommand { buildTime: buildTime, goVersion: goVersion, gitCommit: gitCommit, - summary: summary, showFullVersion: false, } command.BoolVar(&command.showFullVersion, "full", false, "prints the full build information") - command.Usage = commandUsageFunc(command.Name(), command.summary, command.FlagSet) + command.Usage = commandUsageFunc(name, summary, command.FlagSet) return &command } diff --git a/internal/client/client.go b/internal/client/client.go index 566cf39..dd26194 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -85,6 +85,27 @@ func (g *Client) GetInstance() (model.InstanceV2, error) { return instance, nil } +func (g *Client) GetAccount(accountURI string) (model.Account, error) { + path := "/api/v1/accounts/lookup" + url := g.Authentication.Instance + path + "?acct=" + accountURI + + ctx, cancel := context.WithTimeout(context.Background(), g.Timeout) + defer cancel() + + request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return model.Account{}, fmt.Errorf("unable to create the HTTP request, %w", err) + } + + var account model.Account + + if err := g.sendRequest(request, &account); err != nil { + return model.Account{}, fmt.Errorf("received an error after sending the request to get the account information; %w", err) + } + + return account, nil +} + func (g *Client) sendRequest(request *http.Request, object any) error { request.Header.Set("Content-Type", "application/json; charset=utf-8") request.Header.Set("Accept", "application/json; charset=utf-8") diff --git a/internal/config/config.go b/internal/config/config.go index 78b30c9..d98538e 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -28,7 +28,7 @@ func SaveAuthentication(username string, authentication Authentication) (string, return "", fmt.Errorf("unable to ensure the configuration directory; %w", err) } - var config AuthenticationConfig + var authConfig AuthenticationConfig filepath := authenticationConfigFile() @@ -37,9 +37,9 @@ func SaveAuthentication(username string, authentication Authentication) (string, return "", fmt.Errorf("unknown error received when running stat on %s; %w", filepath, err) } - config.Authentications = make(map[string]Authentication) + authConfig.Authentications = make(map[string]Authentication) } else { - config, err = NewAuthenticationConfigFromFile() + authConfig, err = NewAuthenticationConfigFromFile() if err != nil { return "", fmt.Errorf("unable to retrieve the existing authentication configuration; %w", err) } @@ -55,17 +55,12 @@ func SaveAuthentication(username string, authentication Authentication) (string, authenticationName := username + "@" + instance - config.CurrentAccount = authenticationName + authConfig.CurrentAccount = authenticationName - config.Authentications[authenticationName] = authentication + authConfig.Authentications[authenticationName] = authentication - file, err := os.Create(authenticationConfigFile()) - if err != nil { - return "", fmt.Errorf("unable to open the config file; %w", err) - } - - if err := json.NewEncoder(file).Encode(config); err != nil { - return "", fmt.Errorf("unable to save the JSON data to the authentication config file; %w", err) + if err := saveAuthenticationFile(authConfig); err != nil { + return "", fmt.Errorf("unable to save the authentication configuration to file; %w", err) } return authenticationName, nil @@ -80,13 +75,32 @@ func NewAuthenticationConfigFromFile() (AuthenticationConfig, error) { } defer file.Close() - var config AuthenticationConfig + var authConfig AuthenticationConfig - if err := json.NewDecoder(file).Decode(&config); err != nil { + if err := json.NewDecoder(file).Decode(&authConfig); err != nil { return AuthenticationConfig{}, fmt.Errorf("unable to decode the JSON data; %w", err) } - return config, nil + return authConfig, nil +} + +func UpdateCurrentAccount(account string) error { + authConfig, err := NewAuthenticationConfigFromFile() + if err != nil { + return fmt.Errorf("unable to retrieve the existing authentication configuration; %w", err) + } + + if _, ok := authConfig.Authentications[account]; !ok { + return fmt.Errorf("account %s is not found", account) + } + + authConfig.CurrentAccount = account + + if err := saveAuthenticationFile(authConfig); err != nil { + return fmt.Errorf("unable to save the authentication configuration to file; %w", err) + } + + return nil } func authenticationConfigFile() string { @@ -117,3 +131,21 @@ func ensureConfigDir() error { return nil } + +func saveAuthenticationFile(authConfig AuthenticationConfig) error { + file, err := os.Create(authenticationConfigFile()) + if err != nil { + return fmt.Errorf("unable to open the config file; %w", err) + } + + defer file.Close() + + encoder := json.NewEncoder(file) + encoder.SetIndent("", " ") + + if err := encoder.Encode(authConfig); err != nil { + return fmt.Errorf("unable to save the JSON data to the authentication config file; %w", err) + } + + return nil +}