From 32ca448ae7a48798bd1b3a42b2c24b5244a9ef13 Mon Sep 17 00:00:00 2001 From: Dan Anglin Date: Sun, 19 May 2024 11:48:36 +0100 Subject: [PATCH] feat: add and remove accounts from a list Summary: This commit adds the ability to add and remove accounts from a list. The list models has also been updated to change the way lists are displayed on screen. Changes: - Added a subcommand to add accounts to a list. - Added a subcommand to remove accounts from a list. - Added a custom error for unknown subcommands. - Added a custom error when no account IDs are specified when expected. --- cmd/enbas/add.go | 73 ++++++++++++++++++++++++++++++++++++ cmd/enbas/errors.go | 14 +++++++ cmd/enbas/flags.go | 17 +++++++++ cmd/enbas/main.go | 14 ++++++- cmd/enbas/remove.go | 73 ++++++++++++++++++++++++++++++++++++ cmd/enbas/show.go | 35 +++++++++++++++--- cmd/enbas/update.go | 2 +- internal/client/lists.go | 80 ++++++++++++++++++++++++++++++++++++---- internal/model/list.go | 54 +++++++++++++++++++++++---- 9 files changed, 341 insertions(+), 21 deletions(-) create mode 100644 cmd/enbas/add.go create mode 100644 cmd/enbas/flags.go create mode 100644 cmd/enbas/remove.go diff --git a/cmd/enbas/add.go b/cmd/enbas/add.go new file mode 100644 index 0000000..a0612b7 --- /dev/null +++ b/cmd/enbas/add.go @@ -0,0 +1,73 @@ +package main + +import ( + "flag" + "fmt" + + "codeflow.dananglin.me.uk/apollo/enbas/internal/client" +) + +type addCommand struct { + *flag.FlagSet + + toResourceType string + listID string + accountIDs accountIDs +} + +func newAddCommand(name, summary string) *addCommand { + emptyArr := make([]string, 0, 3) + + command := addCommand{ + FlagSet: flag.NewFlagSet(name, flag.ExitOnError), + accountIDs: accountIDs(emptyArr), + } + + command.StringVar(&command.toResourceType, addToFlag, "", "specify the type of resource to add to") + command.StringVar(&command.listID, listIDFlag, "", "the ID of the list to add to") + command.Var(&command.accountIDs, accountIDFlag, "the ID of the account to add to the list") + + command.Usage = commandUsageFunc(name, summary, command.FlagSet) + + return &command +} + +func (c *addCommand) Execute() error { + if c.toResourceType == "" { + return flagNotSetError{flagText: "add-to"} + } + + funcMap := map[string]func(*client.Client) error{ + listResource: c.addAccountsToList, + } + + doFunc, ok := funcMap[c.toResourceType] + if !ok { + return unsupportedResourceTypeError{resourceType: c.toResourceType} + } + + gtsClient, err := client.NewClientFromConfig() + if err != nil { + return fmt.Errorf("unable to create the GoToSocial client; %w", err) + } + + return doFunc(gtsClient) +} + +func (c *addCommand) addAccountsToList(gtsClient *client.Client) error { + if c.listID == "" { + return flagNotSetError{flagText: listIDFlag} + } + + if len(c.accountIDs) == 0 { + return noAccountIDsSpecifiedError{} + } + + if err := gtsClient.AddAccountsToList(c.listID, []string(c.accountIDs)); err != nil { + return fmt.Errorf("unable to add the accounts to the list; %w", err) + } + + fmt.Println("Successfully added the account(s) to the list.") + + return nil +} diff --git a/cmd/enbas/errors.go b/cmd/enbas/errors.go index 9a0db60..8aa5dcc 100644 --- a/cmd/enbas/errors.go +++ b/cmd/enbas/errors.go @@ -23,3 +23,17 @@ type invalidTimelineCategoryError struct { func (e invalidTimelineCategoryError) Error() string { return "'" + e.category + "' is not a valid timeline category (please choose home, public, tag or list)" } + +type unknownSubcommandError struct { + subcommand string +} + +func (e unknownSubcommandError) Error() string { + return "unknown subcommand '" + e.subcommand + "'" +} + +type noAccountIDsSpecifiedError struct{} + +func (e noAccountIDsSpecifiedError) Error() string { + return "no account IDs specified" +} diff --git a/cmd/enbas/flags.go b/cmd/enbas/flags.go new file mode 100644 index 0000000..4856a45 --- /dev/null +++ b/cmd/enbas/flags.go @@ -0,0 +1,17 @@ +package main + +import "strings" + +type accountIDs []string + +func (a *accountIDs) String() string { + return strings.Join(*a, ", ") +} + +func (a *accountIDs) Set(value string) error { + if len(value) > 0 { + *a = append(*a, value) + } + + return nil +} diff --git a/cmd/enbas/main.go b/cmd/enbas/main.go index 5430a0d..b9fa5f7 100644 --- a/cmd/enbas/main.go +++ b/cmd/enbas/main.go @@ -8,11 +8,14 @@ import ( const ( accountFlag = "account" + accountIDFlag = "account-id" + addToFlag = "add-to" instanceFlag = "instance" listIDFlag = "list-id" listTitleFlag = "list-title" listRepliesPolicyFlag = "list-replies-policy" myAccountFlag = "my-account" + removeFromFlag = "remove-from" resourceTypeFlag = "type" statusIDFlag = "status-id" tagFlag = "tag" @@ -52,6 +55,8 @@ func run() error { deleteResource string = "delete" updateResource string = "update" whoami string = "whoami" + add string = "add" + remove string = "remove" ) summaries := map[string]string{ @@ -63,6 +68,8 @@ func run() error { deleteResource: "delete a specific resource", updateResource: "update a specific resource", whoami: "print the account that you are currently logged in to", + add: "add a resource to another resource", + remove: "remove a resource from another resource", } flag.Usage = enbasUsageFunc(summaries) @@ -97,9 +104,14 @@ func run() error { executor = newUpdateCommand(updateResource, summaries[updateResource]) case whoami: executor = newWhoAmICommand(whoami, summaries[whoami]) + case add: + executor = newAddCommand(add, summaries[add]) + case remove: + executor = newRemoveCommand(remove, summaries[remove]) default: flag.Usage() - return fmt.Errorf("unknown subcommand %q", subcommand) + + return unknownSubcommandError{subcommand} } if err := executor.Parse(args); err != nil { diff --git a/cmd/enbas/remove.go b/cmd/enbas/remove.go new file mode 100644 index 0000000..0c79676 --- /dev/null +++ b/cmd/enbas/remove.go @@ -0,0 +1,73 @@ +package main + +import ( + "flag" + "fmt" + + "codeflow.dananglin.me.uk/apollo/enbas/internal/client" +) + +type removeCommand struct { + *flag.FlagSet + + fromResourceType string + listID string + accountIDs accountIDs +} + +func newRemoveCommand(name, summary string) *removeCommand { + emptyArr := make([]string, 0, 3) + + command := removeCommand{ + FlagSet: flag.NewFlagSet(name, flag.ExitOnError), + accountIDs: accountIDs(emptyArr), + } + + command.StringVar(&command.fromResourceType, removeFromFlag, "", "specify the type of resource to remove from") + command.StringVar(&command.listID, listIDFlag, "", "the ID of the list to remove from") + command.Var(&command.accountIDs, accountIDFlag, "the ID of the account to remove from the list") + + command.Usage = commandUsageFunc(name, summary, command.FlagSet) + + return &command +} + +func (c *removeCommand) Execute() error { + if c.fromResourceType == "" { + return flagNotSetError{flagText: "remove-from"} + } + + funcMap := map[string]func(*client.Client) error{ + listResource: c.removeAccountsFromList, + } + + doFunc, ok := funcMap[c.fromResourceType] + if !ok { + return unsupportedResourceTypeError{resourceType: c.fromResourceType} + } + + gtsClient, err := client.NewClientFromConfig() + if err != nil { + return fmt.Errorf("unable to create the GoToSocial client; %w", err) + } + + return doFunc(gtsClient) +} + +func (c *removeCommand) removeAccountsFromList(gtsClient *client.Client) error { + if c.listID == "" { + return flagNotSetError{flagText: listIDFlag} + } + + if len(c.accountIDs) == 0 { + return noAccountIDsSpecifiedError{} + } + + if err := gtsClient.RemoveAccountsFromList(c.listID, []string(c.accountIDs)); err != nil { + return fmt.Errorf("unable to remove the accounts from the list; %w", err) + } + + fmt.Println("Successfully removed the account(s) from the list.") + + return nil +} diff --git a/cmd/enbas/show.go b/cmd/enbas/show.go index ef015c6..8d21d92 100644 --- a/cmd/enbas/show.go +++ b/cmd/enbas/show.go @@ -51,7 +51,7 @@ func (c *showCommand) Execute() error { accountResource: c.showAccount, statusResource: c.showStatus, timelineResource: c.showTimeline, - listResource: c.showLists, + listResource: c.showList, } doFunc, ok := funcMap[c.resourceType] @@ -163,6 +163,34 @@ func (c *showCommand) showTimeline(gts *client.Client) error { return nil } +func (c *showCommand) showList(gts *client.Client) error { + if c.listID == "" { + return c.showLists(gts) + } + + list, err := gts.GetList(c.listID) + if err != nil { + return fmt.Errorf("unable to retrieve the list; %w", err) + } + + accounts, err := gts.GetAccountsFromList(c.listID, 0) + if err != nil { + return fmt.Errorf("unable to retrieve the accounts from the list; %w", err) + } + + if len(accounts) > 0 { + accountMap := make(map[string]string) + for i := range accounts { + accountMap[accounts[i].ID] = accounts[i].Username + } + list.Accounts = accountMap + } + + fmt.Println(list) + + return nil +} + func (c *showCommand) showLists(gts *client.Client) error { lists, err := gts.GetAllLists() if err != nil { @@ -176,10 +204,7 @@ func (c *showCommand) showLists(gts *client.Client) error { } fmt.Println(utilities.HeaderFormat("LISTS")) - - for i := range lists { - fmt.Printf("\n%s\n", lists[i]) - } + fmt.Println(lists) return nil } diff --git a/cmd/enbas/update.go b/cmd/enbas/update.go index c9740f7..0509411 100644 --- a/cmd/enbas/update.go +++ b/cmd/enbas/update.go @@ -83,7 +83,7 @@ func (c *updateCommand) updateList(gtsClient *client.Client) error { } fmt.Println("Successfully updated the list.") - fmt.Printf("\n%s\n", updatedList) + fmt.Println(updatedList) return nil } diff --git a/internal/client/lists.go b/internal/client/lists.go index 86f6896..b1d022d 100644 --- a/internal/client/lists.go +++ b/internal/client/lists.go @@ -13,7 +13,7 @@ const ( listPath string = "/api/v1/lists" ) -func (g *Client) GetAllLists() ([]model.List, error) { +func (g *Client) GetAllLists() (model.Lists, error) { url := g.Authentication.Instance + listPath var lists []model.List @@ -44,7 +44,7 @@ func (g *Client) GetList(listID string) (model.List, error) { } func (g *Client) CreateList(title string, repliesPolicy model.ListRepliesPolicy) (model.List, error) { - params := struct { + form := struct { Title string `json:"title"` RepliesPolicy model.ListRepliesPolicy `json:"replies_policy"` }{ @@ -52,9 +52,9 @@ func (g *Client) CreateList(title string, repliesPolicy model.ListRepliesPolicy) RepliesPolicy: repliesPolicy, } - data, err := json.Marshal(params) + data, err := json.Marshal(form) if err != nil { - return model.List{}, fmt.Errorf("unable to marshal the request body; %w", err) + return model.List{}, fmt.Errorf("unable to marshal the form; %w", err) } requestBody := bytes.NewBuffer(data) @@ -73,7 +73,7 @@ func (g *Client) CreateList(title string, repliesPolicy model.ListRepliesPolicy) } func (g *Client) UpdateList(listToUpdate model.List) (model.List, error) { - params := struct { + form := struct { Title string `json:"title"` RepliesPolicy model.ListRepliesPolicy `json:"replies_policy"` }{ @@ -81,9 +81,9 @@ func (g *Client) UpdateList(listToUpdate model.List) (model.List, error) { RepliesPolicy: listToUpdate.RepliesPolicy, } - data, err := json.Marshal(params) + data, err := json.Marshal(form) if err != nil { - return model.List{}, fmt.Errorf("unable to marshal the request body; %w", err) + return model.List{}, fmt.Errorf("unable to marshal the form; %w", err) } requestBody := bytes.NewBuffer(data) @@ -106,3 +106,69 @@ func (g *Client) DeleteList(listID string) error { return g.sendRequest(http.MethodDelete, url, nil, nil) } + +func (g *Client) AddAccountsToList(listID string, accountIDs []string) error { + form := struct { + AccountIDs []string `json:"account_ids"` + }{ + AccountIDs: accountIDs, + } + + data, err := json.Marshal(form) + if err != nil { + return fmt.Errorf("unable to marshal the form; %w", err) + } + + requestBody := bytes.NewBuffer(data) + url := g.Authentication.Instance + listPath + "/" + listID + "/accounts" + + if err := g.sendRequest(http.MethodPost, url, requestBody, nil); err != nil { + return fmt.Errorf( + "received an error after sending the request to add the accounts to the list; %w", + err, + ) + } + + return nil +} + +func (g *Client) RemoveAccountsFromList(listID string, accountIDs []string) error { + form := struct { + AccountIDs []string `json:"account_ids"` + }{ + AccountIDs: accountIDs, + } + + data, err := json.Marshal(form) + if err != nil { + return fmt.Errorf("unable to marshal the form; %w", err) + } + + requestBody := bytes.NewBuffer(data) + url := g.Authentication.Instance + listPath + "/" + listID + "/accounts" + + if err := g.sendRequest(http.MethodDelete, url, requestBody, nil); err != nil { + return fmt.Errorf( + "received an error after sending the request to remove the accounts from the list; %w", + err, + ) + } + + return nil +} + +func (g *Client) GetAccountsFromList(listID string, limit int) ([]model.Account, error) { + path := fmt.Sprintf("%s/%s/accounts?limit=%d", listPath, listID, limit) + url := g.Authentication.Instance + path + + var accounts []model.Account + + if err := g.sendRequest(http.MethodGet, url, nil, &accounts); err != nil { + return nil, fmt.Errorf( + "received an error after sending the request to get the accounts from the list; %w", + err, + ) + } + + return accounts, nil +} diff --git a/internal/model/list.go b/internal/model/list.go index 9356de8..04c6cbc 100644 --- a/internal/model/list.go +++ b/internal/model/list.go @@ -73,17 +73,57 @@ type List struct { ID string `json:"id"` RepliesPolicy ListRepliesPolicy `json:"replies_policy"` Title string `json:"title"` + Accounts map[string]string } func (l List) String() string { - format := `%s %s -%s %s -%s %s` + format := ` +%s + %s - return fmt.Sprintf( +%s + %s + +%s + %s + +%s` + + output := fmt.Sprintf( format, - utilities.FieldFormat("List ID:"), l.ID, - utilities.FieldFormat("Title:"), l.Title, - utilities.FieldFormat("Replies Policy:"), l.RepliesPolicy, + utilities.HeaderFormat("LIST TITLE:"), l.Title, + utilities.HeaderFormat("LIST ID:"), l.ID, + utilities.HeaderFormat("REPLIES POLICY:"), l.RepliesPolicy, + utilities.HeaderFormat("ADDED ACCOUNTS:"), ) + + if len(l.Accounts) > 0 { + for id, name := range l.Accounts { + output += fmt.Sprintf( + "\n • %s (%s)", + utilities.DisplayNameFormat(name), + id, + ) + } + } else { + output += "\n None" + } + + return output +} + +type Lists []List + +func (l Lists) String() string { + output := "" + + for i := range l { + output += fmt.Sprintf( + "\n%s (%s)", + l[i].Title, + l[i].ID, + ) + } + + return output }