From bc18c00c6918d37fc63515f6368872e492aa63c5 Mon Sep 17 00:00:00 2001 From: Dan Anglin Date: Tue, 27 Feb 2024 19:52:59 +0000 Subject: [PATCH] feat: add ability to interact with lists Add the ability to create, update, list and delete lists. Note, adding accounts or removing them from lists is not scoped in this PR. --- cmd/enbas/create.go | 75 ++++++++++++++++++++++++++ cmd/enbas/delete.go | 65 +++++++++++++++++++++++ cmd/enbas/main.go | 34 ++++++++---- cmd/enbas/show.go | 35 ++++++++++-- cmd/enbas/update.go | 90 +++++++++++++++++++++++++++++++ internal/client/client.go | 4 ++ internal/client/lists.go | 108 ++++++++++++++++++++++++++++++++++++++ internal/model/list.go | 89 +++++++++++++++++++++++++++++++ 8 files changed, 485 insertions(+), 15 deletions(-) create mode 100644 cmd/enbas/create.go create mode 100644 cmd/enbas/delete.go create mode 100644 cmd/enbas/update.go create mode 100644 internal/client/lists.go create mode 100644 internal/model/list.go diff --git a/cmd/enbas/create.go b/cmd/enbas/create.go new file mode 100644 index 0000000..51da691 --- /dev/null +++ b/cmd/enbas/create.go @@ -0,0 +1,75 @@ +package main + +import ( + "errors" + "flag" + "fmt" + + "codeflow.dananglin.me.uk/apollo/enbas/internal/client" + "codeflow.dananglin.me.uk/apollo/enbas/internal/model" +) + +type createCommand struct { + *flag.FlagSet + + resourceType string + listTitle string + listRepliesPolicy string +} + +func newCreateCommand(name, summary string) *createCommand { + command := createCommand{ + FlagSet: flag.NewFlagSet(name, flag.ExitOnError), + } + + command.StringVar(&command.resourceType, "type", "", "specify the type of resource to create") + command.StringVar(&command.listTitle, "list-title", "", "specify the title of the list") + command.StringVar(&command.listRepliesPolicy, "list-replies-policy", "list", "specify the policy of the replies for this list (valid values are followed, list and none)") + + command.Usage = commandUsageFunc(name, summary, command.FlagSet) + + return &command +} + +func (c *createCommand) Execute() error { + if c.resourceType == "" { + return errors.New("the type field is not set") + } + + 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{ + "lists": c.createLists, + } + + doFunc, ok := funcMap[c.resourceType] + if !ok { + return fmt.Errorf("unsupported type %q", c.resourceType) + } + + return doFunc(gtsClient) +} + +func (c *createCommand) createLists(gtsClient *client.Client) error { + if c.listTitle == "" { + return errors.New("the list-title flag is not set") + } + + repliesPolicy, err := model.ParseListRepliesPolicy(c.listRepliesPolicy) + if err != nil { + return fmt.Errorf("unable to parse the list replies policy; %w", err) + } + + list, err := gtsClient.CreateList(c.listTitle, repliesPolicy) + if err != nil { + return fmt.Errorf("unable to create the list; %w", err) + } + + fmt.Println("Successfully created the following list:") + fmt.Printf("\n%s\n", list) + + return nil +} diff --git a/cmd/enbas/delete.go b/cmd/enbas/delete.go new file mode 100644 index 0000000..4d2e156 --- /dev/null +++ b/cmd/enbas/delete.go @@ -0,0 +1,65 @@ +package main + +import ( + "errors" + "flag" + "fmt" + + "codeflow.dananglin.me.uk/apollo/enbas/internal/client" +) + +type deleteCommand struct { + *flag.FlagSet + + resourceType string + listID string +} + +func newDeleteCommand(name, summary string) *deleteCommand { + command := deleteCommand{ + FlagSet: flag.NewFlagSet(name, flag.ExitOnError), + } + + command.StringVar(&command.resourceType, "type", "", "specify the type of resource to delete") + command.StringVar(&command.listID, "list-id", "", "specify the ID of the list to delete") + + command.Usage = commandUsageFunc(name, summary, command.FlagSet) + + return &command +} + +func (c *deleteCommand) Execute() error { + if c.resourceType == "" { + return errors.New("the type field is not set") + } + + 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{ + "lists": c.deleteList, + } + + doFunc, ok := funcMap[c.resourceType] + if !ok { + return fmt.Errorf("unsupported resource type %q", c.resourceType) + } + + return doFunc(gtsClient) +} + +func (c *deleteCommand) deleteList(gtsClient *client.Client) error { + if c.listID == "" { + return errors.New("the list-id flag is not set") + } + + if err := gtsClient.DeleteList(c.listID); err != nil { + return fmt.Errorf("unable to delete the list; %w", err) + } + + fmt.Println("The list was successfully deleted.") + + return nil +} diff --git a/cmd/enbas/main.go b/cmd/enbas/main.go index b8ffc20..d0e658f 100644 --- a/cmd/enbas/main.go +++ b/cmd/enbas/main.go @@ -8,7 +8,7 @@ import ( type Executor interface { Name() string - Parse([]string) error + Parse(args []string) error Execute() error } @@ -21,17 +21,23 @@ func main() { func run() error { const ( - login string = "login" - version string = "version" - show string = "show" - switchAccount string = "switch" + login string = "login" + version string = "version" + showResource string = "show" + switchAccount string = "switch" + createResource string = "create" + deleteResource string = "delete" + updateResource string = "update" ) summaries := map[string]string{ - 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", + login: "login to an account on GoToSocial", + version: "print the application's version and build information", + showResource: "print details about a specified resource", + switchAccount: "switch to an account", + createResource: "create a specific resource", + deleteResource: "delete a specific resource", + updateResource: "update a specific resource", } flag.Usage = enbasUsageFunc(summaries) @@ -54,10 +60,16 @@ func run() error { executor = newLoginCommand(login, summaries[login]) case version: executor = newVersionCommand(version, summaries[version]) - case show: - executor = newShowCommand(show, summaries[show]) + case showResource: + executor = newShowCommand(showResource, summaries[showResource]) case switchAccount: executor = newSwitchCommand(switchAccount, summaries[switchAccount]) + case createResource: + executor = newCreateCommand(createResource, summaries[createResource]) + case deleteResource: + executor = newDeleteCommand(deleteResource, summaries[deleteResource]) + case updateResource: + executor = newUpdateCommand(updateResource, summaries[updateResource]) default: flag.Usage() return fmt.Errorf("unknown subcommand %q", subcommand) diff --git a/cmd/enbas/show.go b/cmd/enbas/show.go index 5fb63ee..b2ad365 100644 --- a/cmd/enbas/show.go +++ b/cmd/enbas/show.go @@ -8,12 +8,13 @@ import ( "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/utilities" ) type showCommand struct { *flag.FlagSet myAccount bool - targetType string + resourceType string account string statusID string timelineType string @@ -28,7 +29,7 @@ func newShowCommand(name, summary string) *showCommand { } command.BoolVar(&command.myAccount, "my-account", false, "set to true to lookup your account") - command.StringVar(&command.targetType, "type", "", "specify the type of resource to display") + command.StringVar(&command.resourceType, "type", "", "specify the type of resource to display") command.StringVar(&command.account, "account", "", "specify the account URI to lookup") command.StringVar(&command.statusID, "status-id", "", "specify the ID of the status to display") command.StringVar(&command.timelineType, "timeline-type", "home", "specify the type of timeline to display (valid values are home, public, list and tag)") @@ -41,6 +42,10 @@ func newShowCommand(name, summary string) *showCommand { } func (c *showCommand) Execute() error { + if c.resourceType == "" { + return errors.New("the type field is not set") + } + gtsClient, err := client.NewClientFromConfig() if err != nil { return fmt.Errorf("unable to create the GoToSocial client; %w", err) @@ -51,11 +56,12 @@ func (c *showCommand) Execute() error { "account": c.showAccount, "status": c.showStatus, "timeline": c.showTimeline, + "lists": c.showLists, } - doFunc, ok := funcMap[c.targetType] + doFunc, ok := funcMap[c.resourceType] if !ok { - return fmt.Errorf("unsupported type %q", c.targetType) + return fmt.Errorf("unsupported resource type %q", c.resourceType) } return doFunc(gtsClient) @@ -156,3 +162,24 @@ func (c *showCommand) showTimeline(gts *client.Client) error { return nil } + +func (c *showCommand) showLists(gts *client.Client) error { + lists, err := gts.GetAllLists() + if err != nil { + return fmt.Errorf("unable to retrieve the lists; %w", err) + } + + if len(lists) == 0 { + fmt.Println("You have no lists.") + + return nil + } + + fmt.Println(utilities.HeaderFormat("LISTS")) + + for i := range lists { + fmt.Printf("\n%s\n", lists[i]) + } + + return nil +} diff --git a/cmd/enbas/update.go b/cmd/enbas/update.go new file mode 100644 index 0000000..a4afdb4 --- /dev/null +++ b/cmd/enbas/update.go @@ -0,0 +1,90 @@ +package main + +import ( + "errors" + "flag" + "fmt" + + "codeflow.dananglin.me.uk/apollo/enbas/internal/client" + "codeflow.dananglin.me.uk/apollo/enbas/internal/model" +) + +type updateCommand struct { + *flag.FlagSet + + resourceType string + listID string + listTitle string + listRepliesPolicy string +} + +func newUpdateCommand(name, summary string) *updateCommand { + command := updateCommand{ + FlagSet: flag.NewFlagSet(name, flag.ExitOnError), + } + + command.StringVar(&command.resourceType, "type", "", "specify the type of resource to update") + command.StringVar(&command.listID, "list-id", "", "specify the ID of the list to update") + command.StringVar(&command.listTitle, "list-title", "", "specify the title of the list") + command.StringVar(&command.listRepliesPolicy, "list-replies-policy", "", "specify the policy of the replies for this list (valid values are followed, list and none)") + + command.Usage = commandUsageFunc(name, summary, command.FlagSet) + + return &command +} + +func (c *updateCommand) Execute() error { + if c.resourceType == "" { + return errors.New("the type field is not set") + } + + 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{ + "lists": c.updateList, + } + + doFunc, ok := funcMap[c.resourceType] + if !ok { + return fmt.Errorf("unsupported resource type %q", c.resourceType) + } + + return doFunc(gtsClient) +} + +func (c *updateCommand) updateList(gtsClient *client.Client) error { + if c.listID == "" { + return errors.New("the list-id flag is not set") + } + + list, err := gtsClient.GetList(c.listID) + if err != nil { + return fmt.Errorf("unable to get the list; %w", err) + } + + if c.listTitle != "" { + list.Title = c.listTitle + } + + if c.listRepliesPolicy != "" { + repliesPolicy, err := model.ParseListRepliesPolicy(c.listRepliesPolicy) + if err != nil { + return fmt.Errorf("unable to parse the list replies policy; %w", err) + } + + list.RepliesPolicy = repliesPolicy + } + + updatedList, err := gtsClient.UpdateList(list) + if err != nil { + return fmt.Errorf("unable to update the list; %w", err) + } + + fmt.Println("Successfully updated the list.") + fmt.Printf("\n%s\n", updatedList) + + return nil +} diff --git a/internal/client/client.go b/internal/client/client.go index 1878a73..9e0b411 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -198,6 +198,10 @@ func (g *Client) sendRequest(method string, url string, requestBody io.Reader, o ) } + if object == nil { + return nil + } + if err := json.NewDecoder(response.Body).Decode(object); err != nil { return fmt.Errorf( "unable to decode the response from the GoToSocial server; %w", diff --git a/internal/client/lists.go b/internal/client/lists.go new file mode 100644 index 0000000..86f6896 --- /dev/null +++ b/internal/client/lists.go @@ -0,0 +1,108 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + + "codeflow.dananglin.me.uk/apollo/enbas/internal/model" +) + +const ( + listPath string = "/api/v1/lists" +) + +func (g *Client) GetAllLists() ([]model.List, error) { + url := g.Authentication.Instance + listPath + + var lists []model.List + + if err := g.sendRequest(http.MethodGet, url, nil, &lists); err != nil { + return nil, fmt.Errorf( + "received an error after sending the request to get the list of lists; %w", + err, + ) + } + + return lists, nil +} + +func (g *Client) GetList(listID string) (model.List, error) { + url := g.Authentication.Instance + listPath + "/" + listID + + var list model.List + + if err := g.sendRequest(http.MethodGet, url, nil, &list); err != nil { + return model.List{}, fmt.Errorf( + "received an error after sending the request to get the list; %w", + err, + ) + } + + return list, nil +} + +func (g *Client) CreateList(title string, repliesPolicy model.ListRepliesPolicy) (model.List, error) { + params := struct { + Title string `json:"title"` + RepliesPolicy model.ListRepliesPolicy `json:"replies_policy"` + }{ + Title: title, + RepliesPolicy: repliesPolicy, + } + + data, err := json.Marshal(params) + if err != nil { + return model.List{}, fmt.Errorf("unable to marshal the request body; %w", err) + } + + requestBody := bytes.NewBuffer(data) + url := g.Authentication.Instance + "/api/v1/lists" + + var list model.List + + if err := g.sendRequest(http.MethodPost, url, requestBody, &list); err != nil { + return model.List{}, fmt.Errorf( + "received an error after sending the request to create the list; %w", + err, + ) + } + + return list, nil +} + +func (g *Client) UpdateList(listToUpdate model.List) (model.List, error) { + params := struct { + Title string `json:"title"` + RepliesPolicy model.ListRepliesPolicy `json:"replies_policy"` + }{ + Title: listToUpdate.Title, + RepliesPolicy: listToUpdate.RepliesPolicy, + } + + data, err := json.Marshal(params) + if err != nil { + return model.List{}, fmt.Errorf("unable to marshal the request body; %w", err) + } + + requestBody := bytes.NewBuffer(data) + url := g.Authentication.Instance + listPath + "/" + listToUpdate.ID + + var updatedList model.List + + if err := g.sendRequest(http.MethodPut, url, requestBody, &updatedList); err != nil { + return model.List{}, fmt.Errorf( + "received an error after sending the request to update the list; %w", + err, + ) + } + + return updatedList, nil +} + +func (g *Client) DeleteList(listID string) error { + url := g.Authentication.Instance + "/api/v1/lists/" + listID + + return g.sendRequest(http.MethodDelete, url, nil, nil) +} diff --git a/internal/model/list.go b/internal/model/list.go new file mode 100644 index 0000000..9356de8 --- /dev/null +++ b/internal/model/list.go @@ -0,0 +1,89 @@ +package model + +import ( + "encoding/json" + "errors" + "fmt" + + "codeflow.dananglin.me.uk/apollo/enbas/internal/utilities" +) + +type ListRepliesPolicy int + +const ( + ListRepliesPolicyFollowed ListRepliesPolicy = iota + ListRepliesPolicyList + ListRepliesPolicyNone +) + +func ParseListRepliesPolicy(policy string) (ListRepliesPolicy, error) { + switch policy { + case "followed": + return ListRepliesPolicyFollowed, nil + case "list": + return ListRepliesPolicyList, nil + case "none": + return ListRepliesPolicyNone, nil + } + + return ListRepliesPolicy(-1), errors.New("invalid list replies policy") +} + +func (l ListRepliesPolicy) String() string { + switch l { + case ListRepliesPolicyFollowed: + return "followed" + case ListRepliesPolicyList: + return "list" + case ListRepliesPolicyNone: + return "none" + } + + return "" +} + +func (l ListRepliesPolicy) MarshalJSON() ([]byte, error) { + str := l.String() + if str == "" { + return nil, errors.New("invalid list replies policy") + } + + return json.Marshal(str) +} + +func (l *ListRepliesPolicy) UnmarshalJSON(data []byte) error { + var ( + value string + err error + ) + + if err = json.Unmarshal(data, &value); err != nil { + return fmt.Errorf("unable to unmarshal the data; %w", err) + } + + *l, err = ParseListRepliesPolicy(value) + if err != nil { + return fmt.Errorf("unable to parse %s as a list replies policy; %w", value, err) + } + + return nil +} + +type List struct { + ID string `json:"id"` + RepliesPolicy ListRepliesPolicy `json:"replies_policy"` + Title string `json:"title"` +} + +func (l List) String() string { + format := `%s %s +%s %s +%s %s` + + return fmt.Sprintf( + format, + utilities.FieldFormat("List ID:"), l.ID, + utilities.FieldFormat("Title:"), l.Title, + utilities.FieldFormat("Replies Policy:"), l.RepliesPolicy, + ) +}