diff --git a/docs/manual.md b/docs/manual.md index 95ebaf3..6a8e864 100644 --- a/docs/manual.md +++ b/docs/manual.md @@ -28,6 +28,10 @@ - [View your follow requests](#view-your-follow-requests) - [Accept a follow request](#accept-a-follow-request) - [Reject a follow request](#reject-a-follow-request) +- [Media Attachments](#media-attachments) + - [Create a media attachment](#create-a-media-attachment) + - [Edit a media attachment](#edit-a-media-attachment) + - [View a media attachment](#view-a-media-attachment) - [Statuses](#statuses) - [View a status](#view-a-status) - [Create a status](#create-a-status) @@ -54,8 +58,6 @@ - [Remove accounts from a list](#remove-accounts-from-a-list) - [Timelines](#timelines) - [View a timeline](#view-a-timeline) -- [Media Attachment](#media-attachment) - - [View media attachment](#view-media-attachment) - [Media](#media) - [View media from a status](#view-media-from-a-status) - [Bookmarks](#bookmarks) @@ -370,6 +372,64 @@ enbas reject --type follow-request --account-name @person.example.social | `type` | string | true | The resource you want to accept.
Here this should be `follow-request`. | | | `account-name` | string | true | The name of the account that you want to reject. | | +## Media Attachments + +### Create a media attachment + +Uploads media from a file to the instance and creates a media attachment. +You can write a description of the media in a text file and specify the path with the `media-description` flag (see the examples below). + +- Create a media attachment with a simple description and a focus of x=-0.1, y=0.5 + ``` + enbas create --type media-attachment \ + --media-file picture.png \ + --media-description "A picture of an old, slanted wooden bench in front of the woods." \ + --media-focus "-0.1,0.5" + ``` +- Create a media attachment using a description written in the `description.txt` text file. + ``` + enbas create --type media-attachment \ + --media-file picture.png \ + --media-description file@description.txt + ``` + +| flag | type | required | description | default | +|------|------|----------|-------------|---------| +| `type` | string | true | The resource you want to create.
Here this should be `media-attachment`. | | +| `media-file` | string | true | The path to the media file. | | +| `media-description` | string | false | The description of the media attachment which will be used as the media's alt-text.
To use a description from a text file, use the `flag@` prefix followed by the path to the file (e.g. `file@description.txt`)| | +| `media-focus` | string | false | The media's focus values. This should be in the form of two comma-separated numbers between -1 and 1 (e.g. 0.25,-0.34) | | + +### Edit a media attachment + +Edits the description and/or the focus of a media attachment that you own. + +``` +enbas edit --type media-attachment \ + --attachment-id 01J5B9A8WFK59W11MS6AHPYWBR \ + --media-description "An updated description of a picture." +``` + +| flag | type | required | description | default | +|------|------|----------|-------------|---------| +| `type` | string | true | The resource you want to edit.
Here this should be `media-attachment`. | | +| `media-description` | string | false | The description of the media attachment to edit.
To use a description from a text file, use the `flag@` prefix followed by the path to the file (e.g. `file@description.txt`)| | +| `media-focus` | string | false | The media's focus values. This should be in the form of two comma-separated numbers between -1 and 1 (e.g. 0.25,-0.34) | | + +### View a media attachment + +Prints information about a given media attachment that you own. +You can only see information about the media attachment that you own. + +``` +enbas show --type media-attachment --attachment-id 01J0N0RQSJ7CFGKHA30F7GBQXT +``` + +| flag | type | required | description | default | +|------|------|----------|-------------|---------| +| `type` | string | true | The resource you want to view.
Here this should be `media`. | | +| `attachment-id` | string | true | The ID of the media attachment to view. | | + ## Statuses ### View a status @@ -421,10 +481,28 @@ enbas show --type status --status-id 01J1Z9PT0243JT9QNQ5W96Z8CA --poll-option "other (please comment)" ``` ![A screenshot of a status with a poll](../assets/images/created_poll.png "A status with a poll") +- Create a status with a media attachment that you have created. + ``` + enbas create \ + --type status \ + --attachment-id 01J5BDHYJ7MWMMG76FP49H7SWD \ + --content "I went out for a walk in the woods and found this interesting looking wooden bench." + ``` +- Upload and attach 4 media files to a new status. You must set the same number of `media-description` and `media-focus` flags **must** as the `media-file` flags. + The first `media-description` and `media-focus` flags correspond to the first `media-file` flag and so on. + ``` + enbas create --type status --visibility public \ + --content "This post has a picture of a cat, a dog, a bee and a bird." \ + --media-file cat.jpg --media-description file@cat.txt --media-focus "0,0" \ + --media-file dog.jpg --media-description file@dog.txt --media-focus "-0.1,0.25" \ + --media-file bee.jpg --media-description file@bee.txt --media-focus "1,1" \ + --media-file bird.webp --media-description file@bird.txt --media-focus "0,0" + ``` | flag | type | required | description | default | |------|------|----------|-------------|---------| | `type` | string | true | The resource you want to create.
Here this should be `status`. | | +| `attachment-id` | string | false | The ID of the media attachment to attach to the status.
Use this flag multiple times to attach multiple media. | | `content` | string | false | The content of the status.
This flag takes precedence over `from-file`.| | | `content-type` | string | false | The format that the content is created in.
Valid values are `plain` and `markdown`. | plain | | `enable-reposts` | boolean | false | The status can be reposted (boosted) by others. | true | @@ -434,6 +512,9 @@ enbas show --type status --status-id 01J1Z9PT0243JT9QNQ5W96Z8CA | `from-file` | string | false | The path to the file where to read the contents of the status from. | | | `in-reply-to` | string | false | The ID of the status that you want to reply to. | | | `language` | string | false | The ISO 639 language code that the status is written in.
If this is not specified then the default language from your posting preferences will be used. | | +| `media-file` | string | false | The path to the media file.
Use this flag multiple times to upload multiple media files. | | +| `media-description` | string | false | The description of the media attachment which will be used as the media's alt-text.
To use a description from a text file, use the `flag@` prefix followed by the path to the file (e.g. `file@description.txt`)
Use this flag multiple times to set multiple descriptions.| | +| `media-focus` | string | false | The media's focus values. This should be in the form of two comma-separated numbers between -1 and 1 (e.g. 0.25,-0.34).
Use this flag multiple times to set multiple focus values. | | | `sensitive` | string | false | The status should be marked as sensitive.
If this is not specified then the default sensitivity from your posting preferences will be used. | | | `spoiler-text` | string | false | The text to display as the status' warning or subject. | | | `visibility` | string | false | The visibility of the status.
Valid values are `public`, `private`, `unlisted`, `mutuals_only` and `direct`.
If this is not specified then the default visibility from your posting preferences will be used. | | @@ -683,22 +764,6 @@ Prints a list of statuses from a timeline. | `tag` | string | false | The hashtag you want to view.
This is only required if `timeline-category` is set to `tag`. | | | `limit` | integer | false | The maximum number of statuses to print. | 20 | -## Media Attachment - -### View media attachment - -Prints information about a given media attachment that you own. -You can only see information about the media attachment that you own. - -``` -enbas show --type media-attachment --attachment-id 01J0N0RQSJ7CFGKHA30F7GBQXT -``` - -| flag | type | required | description | default | -|------|------|----------|-------------|---------| -| `type` | string | true | The resource you want to view.
Here this should be `media`. | | -| `attachment-id` | string | true | The ID of the media attachment to view. | | - ## Media ### View media from a status diff --git a/internal/client/media.go b/internal/client/media.go index a855071..182c7d8 100644 --- a/internal/client/media.go +++ b/internal/client/media.go @@ -1,8 +1,14 @@ package client import ( + "bytes" + "encoding/json" "fmt" + "io" + "mime/multipart" "net/http" + "os" + "path/filepath" "codeflow.dananglin.me.uk/apollo/enbas/internal/model" ) @@ -31,12 +37,123 @@ func (g *Client) GetMediaAttachment(mediaAttachmentID string) (model.Attachment, return attachment, nil } -//type CreateMediaAttachmentForm struct { -// Description string -// Focus string -// Filepath string -//} -// -//func (g *Client) CreateMediaAttachment(form CreateMediaAttachmentForm) (model.Attachment, error) { -// return model.Attachment{}, nil -//} +func (g *Client) CreateMediaAttachment(path, description, focus string) (model.Attachment, error) { + file, err := os.Open(path) + if err != nil { + return model.Attachment{}, fmt.Errorf("unable to open the file: %w", err) + } + defer file.Close() + + // create the request body using a writer from the multipart package + requestBody := bytes.Buffer{} + requestBodyWriter := multipart.NewWriter(&requestBody) + + filename := filepath.Base(path) + + part, err := requestBodyWriter.CreateFormFile("file", filename) + if err != nil { + return model.Attachment{}, fmt.Errorf("unable to create the new part: %w", err) + } + + if _, err := io.Copy(part, file); err != nil { + return model.Attachment{}, fmt.Errorf("unable to copy the file contents to the form: %w", err) + } + + // add the description + if description != "" { + descriptionFormFieldWriter, err := requestBodyWriter.CreateFormField("description") + if err != nil { + return model.Attachment{}, fmt.Errorf( + "unable to create the writer for the 'description' form field: %w", + err, + ) + } + + if _, err := io.WriteString(descriptionFormFieldWriter, description); err != nil { + return model.Attachment{}, fmt.Errorf( + "unable to write the description to the form: %w", + err, + ) + } + } + + // add the focus values + if focus != "" { + focusFormFieldWriter, err := requestBodyWriter.CreateFormField("focus") + if err != nil { + return model.Attachment{}, fmt.Errorf( + "unable to create the writer for the 'focus' form field: %w", + err, + ) + } + + if _, err := io.WriteString(focusFormFieldWriter, focus); err != nil { + return model.Attachment{}, fmt.Errorf( + "unable to write the focus values to the form: %w", + err, + ) + } + } + + if err := requestBodyWriter.Close(); err != nil { + return model.Attachment{}, fmt.Errorf("unable to close the writer: %w", err) + } + + url := g.Authentication.Instance + baseMediaPath + + var attachment model.Attachment + + params := requestParameters{ + httpMethod: http.MethodPost, + url: url, + requestBody: &requestBody, + contentType: requestBodyWriter.FormDataContentType(), + output: &attachment, + } + + if err := g.sendRequest(params); err != nil { + return model.Attachment{}, fmt.Errorf( + "received an error after sending the request to create the media attachment: %w", + err, + ) + } + + return attachment, nil +} + +func (g *Client) UpdateMediaAttachment(mediaAttachmentID, description, focus string) (model.Attachment, error) { + form := struct { + Description string `json:"description"` + Focus string `json:"focus"` + }{ + Description: description, + Focus: focus, + } + + data, err := json.Marshal(form) + if err != nil { + return model.Attachment{}, fmt.Errorf("unable to marshal the form: %w", err) + } + + requestBody := bytes.NewBuffer(data) + url := g.Authentication.Instance + baseMediaPath + "/" + mediaAttachmentID + + var updatedMediaAttachment model.Attachment + + params := requestParameters{ + httpMethod: http.MethodPut, + url: url, + requestBody: requestBody, + contentType: applicationJSON, + output: &updatedMediaAttachment, + } + + if err := g.sendRequest(params); err != nil { + return model.Attachment{}, fmt.Errorf( + "received an error after sending the request to update the media attachment: %w", + err, + ) + } + + return updatedMediaAttachment, nil +} diff --git a/internal/client/statuses.go b/internal/client/statuses.go index 6e1a59e..0b8d60a 100644 --- a/internal/client/statuses.go +++ b/internal/client/statuses.go @@ -38,18 +38,19 @@ func (g *Client) GetStatus(statusID string) (model.Status, error) { } type CreateStatusForm struct { - Content string `json:"status"` - InReplyTo string `json:"in_reply_to_id"` - Language string `json:"language"` - SpoilerText string `json:"spoiler_text"` - Boostable bool `json:"boostable"` - Federated bool `json:"federated"` - Likeable bool `json:"likeable"` - Replyable bool `json:"replyable"` - Sensitive bool `json:"sensitive"` - Poll *CreateStatusPollForm `json:"poll,omitempty"` - ContentType model.StatusContentType `json:"content_type"` - Visibility model.StatusVisibility `json:"visibility"` + Content string `json:"status"` + InReplyTo string `json:"in_reply_to_id"` + Language string `json:"language"` + SpoilerText string `json:"spoiler_text"` + Boostable bool `json:"boostable"` + Federated bool `json:"federated"` + Likeable bool `json:"likeable"` + Replyable bool `json:"replyable"` + Sensitive bool `json:"sensitive"` + Poll *CreateStatusPollForm `json:"poll,omitempty"` + ContentType model.StatusContentType `json:"content_type"` + Visibility model.StatusVisibility `json:"visibility"` + AttachmentIDs []string `json:"media_ids,omitempty"` } type CreateStatusPollForm struct { diff --git a/internal/executor/create.go b/internal/executor/create.go index 194400a..6e0472a 100644 --- a/internal/executor/create.go +++ b/internal/executor/create.go @@ -1,6 +1,7 @@ package executor import ( + "errors" "fmt" "codeflow.dananglin.me.uk/apollo/enbas/internal/client" @@ -19,8 +20,9 @@ func (c *CreateExecutor) Execute() error { } funcMap := map[string]func(*client.Client) error{ - resourceList: c.createList, - resourceStatus: c.createStatus, + resourceList: c.createList, + resourceStatus: c.createStatus, + resourceMediaAttachment: c.createMediaAttachment, } doFunc, ok := funcMap[c.resourceType] @@ -66,21 +68,95 @@ func (c *CreateExecutor) createStatus(gtsClient *client.Client) error { sensitive bool ) + attachmentIDs := []string(c.attachmentIDs) + + if !c.mediaFiles.Empty() { + descriptionsExists := false + focusValuesExists := false + expectedLength := len(c.mediaFiles) + mediaDescriptions := make([]string, expectedLength) + + if !c.mediaDescriptions.Empty() { + descriptionsExists = true + + if !c.mediaDescriptions.ExpectedLength(expectedLength) { + return errors.New("the number of media descriptions does not match the number of media files") + } + } + + if !c.mediaFocusValues.Empty() { + focusValuesExists = true + + if !c.mediaFocusValues.ExpectedLength(expectedLength) { + return errors.New("the number of media focus values does not match the number of media files") + } + } + + if descriptionsExists { + for ind := 0; ind < expectedLength; ind++ { + content, err := utilities.ReadContents(c.mediaDescriptions[ind]) + if err != nil { + return fmt.Errorf("unable to read the contents from %s: %w", c.mediaDescriptions[ind], err) + } + + mediaDescriptions[ind] = content + } + } + + for ind := 0; ind < expectedLength; ind++ { + var ( + mediaFile string + description string + focus string + ) + + mediaFile = c.mediaFiles[ind] + + if descriptionsExists { + description = mediaDescriptions[ind] + } + + if focusValuesExists { + focus = c.mediaFocusValues[ind] + } + + attachment, err := gtsClient.CreateMediaAttachment( + mediaFile, + description, + focus, + ) + if err != nil { + return fmt.Errorf("unable to create the media attachment for %s: %w", mediaFile, err) + } + + attachmentIDs = append(attachmentIDs, attachment.ID) + } + } + switch { case c.content != "": content = c.content case c.fromFile != "": - content, err = utilities.ReadFile(c.fromFile) + content, err = utilities.ReadTextFile(c.fromFile) if err != nil { return fmt.Errorf("unable to get the status contents from %q: %w", c.fromFile, err) } default: - return EmptyContentError{ - ResourceType: resourceStatus, - Hint: "please use --" + flagContent + " or --" + flagFromFile, + if len(attachmentIDs) == 0 { + // TODO: revisit this error type + return EmptyContentError{ + ResourceType: resourceStatus, + Hint: "please use --" + flagContent + " or --" + flagFromFile, + } } } + numAttachmentIDs := len(attachmentIDs) + + if c.addPoll && numAttachmentIDs > 0 { + return fmt.Errorf("attaching media to a poll is not allowed") + } + preferences, err := gtsClient.GetUserPreferences() if err != nil { fmt.Println("WARNING: Unable to get your posting preferences: %w", err) @@ -115,18 +191,23 @@ func (c *CreateExecutor) createStatus(gtsClient *client.Client) error { } form := client.CreateStatusForm{ - Content: content, - ContentType: parsedContentType, - Language: language, - SpoilerText: c.spoilerText, - Boostable: c.boostable, - Federated: c.federated, - InReplyTo: c.inReplyTo, - Likeable: c.likeable, - Replyable: c.replyable, - Sensitive: sensitive, - Visibility: parsedVisibility, - Poll: nil, + Content: content, + ContentType: parsedContentType, + Language: language, + SpoilerText: c.spoilerText, + Boostable: c.boostable, + Federated: c.federated, + InReplyTo: c.inReplyTo, + Likeable: c.likeable, + Replyable: c.replyable, + Sensitive: sensitive, + Visibility: parsedVisibility, + Poll: nil, + AttachmentIDs: nil, + } + + if numAttachmentIDs > 0 { + form.AttachmentIDs = attachmentIDs } if c.addPoll { @@ -153,3 +234,56 @@ func (c *CreateExecutor) createStatus(gtsClient *client.Client) error { return nil } + +func (c *CreateExecutor) createMediaAttachment(gtsClient *client.Client) error { + expectedNumValues := 1 + if !c.mediaFiles.ExpectedLength(expectedNumValues) { + return fmt.Errorf( + "received an unexpected number of media files: want %d", + expectedNumValues, + ) + } + + description := "" + if !c.mediaDescriptions.Empty() { + if !c.mediaDescriptions.ExpectedLength(expectedNumValues) { + return fmt.Errorf( + "received an unexpected number of media descriptions: want %d", + expectedNumValues, + ) + } + + var err error + description, err = utilities.ReadContents(c.mediaDescriptions[0]) + if err != nil { + return fmt.Errorf( + "unable to read the contents from %s: %w", + c.mediaDescriptions[0], + ) + } + } + + focus := "" + if !c.mediaFocusValues.Empty() { + if !c.mediaFocusValues.ExpectedLength(expectedNumValues) { + return fmt.Errorf( + "received an unexpected number of media focus values: want %d", + expectedNumValues, + ) + } + focus = c.mediaFocusValues[0] + } + + attachment, err := gtsClient.CreateMediaAttachment( + c.mediaFiles[0], + description, + focus, + ) + if err != nil { + return fmt.Errorf("unable to create the media attachment: %w", err) + } + + c.printer.PrintSuccess("Successfully created the media attachment with ID: " + attachment.ID) + + return nil +} diff --git a/internal/executor/edit.go b/internal/executor/edit.go index 50c7247..b6097de 100644 --- a/internal/executor/edit.go +++ b/internal/executor/edit.go @@ -5,6 +5,7 @@ import ( "codeflow.dananglin.me.uk/apollo/enbas/internal/client" "codeflow.dananglin.me.uk/apollo/enbas/internal/model" + "codeflow.dananglin.me.uk/apollo/enbas/internal/utilities" ) func (e *EditExecutor) Execute() error { @@ -13,7 +14,8 @@ func (e *EditExecutor) Execute() error { } funcMap := map[string]func(*client.Client) error{ - resourceList: e.editList, + resourceList: e.editList, + resourceMediaAttachment: e.editMediaAttachment, } doFunc, ok := funcMap[e.resourceType] @@ -57,8 +59,62 @@ func (e *EditExecutor) editList(gtsClient *client.Client) error { return fmt.Errorf("unable to update the list: %w", err) } - e.printer.PrintSuccess("Successfully updated the list.") + e.printer.PrintSuccess("Successfully edited the list.") e.printer.PrintList(updatedList) return nil } + +func (e *EditExecutor) editMediaAttachment(gtsClient *client.Client) error { + expectedNumValues := 1 + + if !e.attachmentIDs.ExpectedLength(expectedNumValues) { + return fmt.Errorf( + "received an unexpected number of media attachment IDs: want %d", + expectedNumValues, + ) + } + + attachment, err := gtsClient.GetMediaAttachment(e.attachmentIDs[0]) + if err != nil { + return fmt.Errorf("unable to get the media attachment: %w", err) + } + + description := attachment.Description + if !e.mediaDescriptions.Empty() { + if !e.mediaDescriptions.ExpectedLength(expectedNumValues) { + return fmt.Errorf( + "received an unexpected number of media descriptions: want %d", + expectedNumValues, + ) + } + + var err error + description, err = utilities.ReadContents(e.mediaDescriptions[0]) + if err != nil { + return fmt.Errorf( + "unable to read the contents from %s: %w", + e.mediaDescriptions[0], + ) + } + } + + focus := fmt.Sprintf("%f,%f", attachment.Meta.Focus.X, attachment.Meta.Focus.Y) + if !e.mediaFocusValues.Empty() { + if !e.mediaFocusValues.ExpectedLength(expectedNumValues) { + return fmt.Errorf( + "received an unexpected number of media focus values: want %d", + expectedNumValues, + ) + } + focus = e.mediaFocusValues[0] + } + + if _, err = gtsClient.UpdateMediaAttachment(attachment.ID, description, focus); err != nil { + return fmt.Errorf("unable to update the media attachment: %w", err) + } + + e.printer.PrintSuccess("Successfully edited the media attachment.") + + return nil +} diff --git a/internal/executor/executors.go b/internal/executor/executors.go index 4ce5cfc..1ef1ee1 100644 --- a/internal/executor/executors.go +++ b/internal/executor/executors.go @@ -121,6 +121,7 @@ type CreateExecutor struct { printer *printer.Printer config *config.Config addPoll bool + attachmentIDs internalFlag.StringSliceValue content string contentType string federated bool @@ -132,6 +133,9 @@ type CreateExecutor struct { language string listRepliesPolicy string listTitle string + mediaDescriptions internalFlag.StringSliceValue + mediaFocusValues internalFlag.StringSliceValue + mediaFiles internalFlag.StringSliceValue pollAllowsMultipleChoices bool pollExpiresIn internalFlag.TimeDurationValue pollHidesVoteCounts bool @@ -147,17 +151,22 @@ func NewCreateExecutor( 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(), + FlagSet: flag.NewFlagSet("create", flag.ExitOnError), + printer: printer, + config: config, + attachmentIDs: internalFlag.NewStringSliceValue(), + mediaDescriptions: internalFlag.NewStringSliceValue(), + mediaFocusValues: internalFlag.NewStringSliceValue(), + mediaFiles: internalFlag.NewStringSliceValue(), + 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.Var(&exe.attachmentIDs, "attachment-id", "The ID of the media attachment") 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") @@ -169,6 +178,9 @@ func NewCreateExecutor( 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.Var(&exe.mediaDescriptions, "media-description", "The description of the media attachment which will be used as the alt-text") + exe.Var(&exe.mediaFocusValues, "media-focus", "The focus of the media file") + exe.Var(&exe.mediaFiles, "media-file", "The path to the media file") 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") @@ -213,9 +225,12 @@ type EditExecutor struct { *flag.FlagSet printer *printer.Printer config *config.Config + attachmentIDs internalFlag.StringSliceValue listID string listTitle string listRepliesPolicy string + mediaDescriptions internalFlag.StringSliceValue + mediaFocusValues internalFlag.StringSliceValue resourceType string } @@ -224,16 +239,22 @@ func NewEditExecutor( config *config.Config, ) *EditExecutor { exe := EditExecutor{ - FlagSet: flag.NewFlagSet("edit", flag.ExitOnError), - printer: printer, - config: config, + FlagSet: flag.NewFlagSet("edit", flag.ExitOnError), + printer: printer, + config: config, + attachmentIDs: internalFlag.NewStringSliceValue(), + mediaDescriptions: internalFlag.NewStringSliceValue(), + mediaFocusValues: internalFlag.NewStringSliceValue(), } exe.Usage = usage.ExecutorUsageFunc("edit", "Edit a specific resource", exe.FlagSet) + exe.Var(&exe.attachmentIDs, "attachment-id", "The ID of the media attachment") 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.Var(&exe.mediaDescriptions, "media-description", "The description of the media attachment which will be used as the alt-text") + exe.Var(&exe.mediaFocusValues, "media-focus", "The focus of the media file") exe.StringVar(&exe.resourceType, "type", "", "The type of resource you want to action on (e.g. account, status)") return &exe diff --git a/internal/utilities/file.go b/internal/utilities/file.go index d6e29a7..ffbdc2c 100644 --- a/internal/utilities/file.go +++ b/internal/utilities/file.go @@ -5,9 +5,20 @@ import ( "errors" "fmt" "os" + "strings" ) -func ReadFile(path string) (string, error) { +const filePrefix string = "file@" + +func ReadContents(text string) (string, error) { + if !strings.HasPrefix(text, filePrefix) { + return text, nil + } + + return ReadTextFile(strings.TrimPrefix(text, filePrefix)) +} + +func ReadTextFile(path string) (string, error) { file, err := os.Open(path) if err != nil { return "", fmt.Errorf("unable to open %q: %w", path, err) diff --git a/schema/enbas_cli_schema.json b/schema/enbas_cli_schema.json index bbed96d..3199e0a 100644 --- a/schema/enbas_cli_schema.json +++ b/schema/enbas_cli_schema.json @@ -96,6 +96,18 @@ "type": "string", "description": "The replies policy of the list" }, + "media-description": { + "type": "StringSliceValue", + "description": "The description of the media attachment which will be used as the alt-text" + }, + "media-file": { + "type": "StringSliceValue", + "description": "The path to the media file" + }, + "media-focus": { + "type": "StringSliceValue", + "description": "The focus of the media file" + }, "mute-duration": { "type": "TimeDurationValue", "description": "Specify how long the mute should last for. To mute indefinitely, set this to 0s" @@ -234,6 +246,7 @@ "additionalFields": [], "flags": [ { "flag": "add-poll", "default": "false" }, + { "flag": "attachment-id", "fieldName": "attachmentIDs" }, { "flag": "content", "default": "" }, { "flag": "content-type", "default": "plain" }, { "flag": "enable-federation", "fieldName": "federated", "default": "true" }, @@ -245,6 +258,9 @@ { "flag": "language", "default": "" }, { "flag": "list-replies-policy", "default": "list" }, { "flag": "list-title", "default": "" }, + { "flag": "media-description", "fieldName": "mediaDescriptions" }, + { "flag": "media-focus", "fieldName": "mediaFocusValues" }, + { "flag": "media-file", "fieldName": "mediaFiles" }, { "flag": "poll-allows-multiple-choices", "default": "false" }, { "flag": "poll-expires-in" }, { "flag": "poll-hides-vote-counts", "default": "false" }, @@ -271,9 +287,12 @@ "edit": { "additionalFields": [], "flags": [ + { "flag": "attachment-id", "fieldName": "attachmentIDs" }, { "flag": "list-id", "fieldName": "listID", "default": ""}, { "flag": "list-title", "default": "" }, { "flag": "list-replies-policy", "default": "" }, + { "flag": "media-description", "fieldName": "mediaDescriptions" }, + { "flag": "media-focus", "fieldName": "mediaFocusValues" }, { "flag": "type", "fieldName": "resourceType", "default": "" } ], "summary": "Edit a specific resource",