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.
This commit is contained in:
Dan Anglin 2024-02-27 19:52:59 +00:00
parent c38689fe28
commit bc18c00c69
Signed by: dananglin
GPG key ID: 0C1D44CFBEE68638
8 changed files with 485 additions and 15 deletions

75
cmd/enbas/create.go Normal file
View file

@ -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
}

65
cmd/enbas/delete.go Normal file
View file

@ -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
}

View file

@ -8,7 +8,7 @@ import (
type Executor interface { type Executor interface {
Name() string Name() string
Parse([]string) error Parse(args []string) error
Execute() error Execute() error
} }
@ -23,15 +23,21 @@ func run() error {
const ( const (
login string = "login" login string = "login"
version string = "version" version string = "version"
show string = "show" showResource string = "show"
switchAccount string = "switch" switchAccount string = "switch"
createResource string = "create"
deleteResource string = "delete"
updateResource string = "update"
) )
summaries := map[string]string{ summaries := map[string]string{
login: "login to an account on GoToSocial", login: "login to an account on GoToSocial",
version: "print the application's version and build information", version: "print the application's version and build information",
show: "print details about a specified resource", showResource: "print details about a specified resource",
switchAccount: "switch to an account", switchAccount: "switch to an account",
createResource: "create a specific resource",
deleteResource: "delete a specific resource",
updateResource: "update a specific resource",
} }
flag.Usage = enbasUsageFunc(summaries) flag.Usage = enbasUsageFunc(summaries)
@ -54,10 +60,16 @@ func run() error {
executor = newLoginCommand(login, summaries[login]) executor = newLoginCommand(login, summaries[login])
case version: case version:
executor = newVersionCommand(version, summaries[version]) executor = newVersionCommand(version, summaries[version])
case show: case showResource:
executor = newShowCommand(show, summaries[show]) executor = newShowCommand(showResource, summaries[showResource])
case switchAccount: case switchAccount:
executor = newSwitchCommand(switchAccount, summaries[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: default:
flag.Usage() flag.Usage()
return fmt.Errorf("unknown subcommand %q", subcommand) return fmt.Errorf("unknown subcommand %q", subcommand)

View file

@ -8,12 +8,13 @@ import (
"codeflow.dananglin.me.uk/apollo/enbas/internal/client" "codeflow.dananglin.me.uk/apollo/enbas/internal/client"
"codeflow.dananglin.me.uk/apollo/enbas/internal/config" "codeflow.dananglin.me.uk/apollo/enbas/internal/config"
"codeflow.dananglin.me.uk/apollo/enbas/internal/model" "codeflow.dananglin.me.uk/apollo/enbas/internal/model"
"codeflow.dananglin.me.uk/apollo/enbas/internal/utilities"
) )
type showCommand struct { type showCommand struct {
*flag.FlagSet *flag.FlagSet
myAccount bool myAccount bool
targetType string resourceType string
account string account string
statusID string statusID string
timelineType 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.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.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.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)") 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 { func (c *showCommand) Execute() error {
if c.resourceType == "" {
return errors.New("the type field is not set")
}
gtsClient, err := client.NewClientFromConfig() gtsClient, err := client.NewClientFromConfig()
if err != nil { if err != nil {
return fmt.Errorf("unable to create the GoToSocial client; %w", err) return fmt.Errorf("unable to create the GoToSocial client; %w", err)
@ -51,11 +56,12 @@ func (c *showCommand) Execute() error {
"account": c.showAccount, "account": c.showAccount,
"status": c.showStatus, "status": c.showStatus,
"timeline": c.showTimeline, "timeline": c.showTimeline,
"lists": c.showLists,
} }
doFunc, ok := funcMap[c.targetType] doFunc, ok := funcMap[c.resourceType]
if !ok { if !ok {
return fmt.Errorf("unsupported type %q", c.targetType) return fmt.Errorf("unsupported resource type %q", c.resourceType)
} }
return doFunc(gtsClient) return doFunc(gtsClient)
@ -156,3 +162,24 @@ func (c *showCommand) showTimeline(gts *client.Client) error {
return nil 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
}

90
cmd/enbas/update.go Normal file
View file

@ -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
}

View file

@ -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 { if err := json.NewDecoder(response.Body).Decode(object); err != nil {
return fmt.Errorf( return fmt.Errorf(
"unable to decode the response from the GoToSocial server; %w", "unable to decode the response from the GoToSocial server; %w",

108
internal/client/lists.go Normal file
View file

@ -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)
}

89
internal/model/list.go Normal file
View file

@ -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,
)
}