feat: add more support for media attachments

This commit adds more support for interacting with media attachments.
Now users can:

- Upload media to their instances and create media attachments.
- Edit existing media attachments.
- Attach one or more existing media to a new status.
- Upload and attach one or more media files to a new status.

PR: apollo/enbas#42
Resolves: apollo/enbas#29
This commit is contained in:
Dan Anglin 2024-08-15 18:12:12 +01:00
parent 4e76d20a7a
commit b8b03748d7
Signed by: dananglin
GPG key ID: 0C1D44CFBEE68638
8 changed files with 479 additions and 55 deletions

View file

@ -54,8 +54,10 @@
- [Remove accounts from a list](#remove-accounts-from-a-list) - [Remove accounts from a list](#remove-accounts-from-a-list)
- [Timelines](#timelines) - [Timelines](#timelines)
- [View a timeline](#view-a-timeline) - [View a timeline](#view-a-timeline)
- [Media Attachment](#media-attachment) - [Media Attachments](#media-attachments)
- [View media attachment](#view-media-attachment) - [Create a media attachment](#create-a-media-attachment)
- [Edit a media attachment](#edit-a-media-attachment)
- [View a media attachment](#view-a-media-attachment)
- [Media](#media) - [Media](#media)
- [View media from a status](#view-media-from-a-status) - [View media from a status](#view-media-from-a-status)
- [Bookmarks](#bookmarks) - [Bookmarks](#bookmarks)
@ -421,10 +423,28 @@ enbas show --type status --status-id 01J1Z9PT0243JT9QNQ5W96Z8CA
--poll-option "other (please comment)" --poll-option "other (please comment)"
``` ```
![A screenshot of a status with a poll](../assets/images/created_poll.png "A status with a poll") ![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 own.
```
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. The number of `media-description` and `media-focus` flags **must** match the number of `media-file` flags defined.
The first `media-description` and `media-focus` flags correspond to the value defined in 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 | | flag | type | required | description | default |
|------|------|----------|-------------|---------| |------|------|----------|-------------|---------|
| `type` | string | true | The resource you want to create.<br>Here this should be `status`. | | | `type` | string | true | The resource you want to create.<br>Here this should be `status`. | |
| `attachment-id` | string | false | The ID of the media attachment to attach to the status.<br>Use this flag multiple times to attach multiple media. |
| `content` | string | false | The content of the status.<br>This flag takes precedence over `from-file`.| | | `content` | string | false | The content of the status.<br>This flag takes precedence over `from-file`.| |
| `content-type` | string | false | The format that the content is created in.<br>Valid values are `plain` and `markdown`. | plain | | `content-type` | string | false | The format that the content is created in.<br>Valid values are `plain` and `markdown`. | plain |
| `enable-reposts` | boolean | false | The status can be reposted (boosted) by others. | true | | `enable-reposts` | boolean | false | The status can be reposted (boosted) by others. | true |
@ -434,6 +454,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. | | | `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. | | | `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.<br>If this is not specified then the default language from your posting preferences will be used. | | | `language` | string | false | The ISO 639 language code that the status is written in.<br>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.<br>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.<br>To use a description from a text file, use the `flag@` prefix followed by the path to the file (e.g. `file@description.txt`)<br>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).<br>Use this flag multiple times to set multiple focus values. | |
| `sensitive` | string | false | The status should be marked as sensitive.<br>If this is not specified then the default sensitivity from your posting preferences will be used. | | | `sensitive` | string | false | The status should be marked as sensitive.<br>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. | | | `spoiler-text` | string | false | The text to display as the status' warning or subject. | |
| `visibility` | string | false | The visibility of the status.<br>Valid values are `public`, `private`, `unlisted`, `mutuals_only` and `direct`.<br>If this is not specified then the default visibility from your posting preferences will be used. | | | `visibility` | string | false | The visibility of the status.<br>Valid values are `public`, `private`, `unlisted`, `mutuals_only` and `direct`.<br>If this is not specified then the default visibility from your posting preferences will be used. | |
@ -683,9 +706,51 @@ Prints a list of statuses from a timeline.
| `tag` | string | false | The hashtag you want to view.<br>This is only required if `timeline-category` is set to `tag`. | | | `tag` | string | false | The hashtag you want to view.<br>This is only required if `timeline-category` is set to `tag`. | |
| `limit` | integer | false | The maximum number of statuses to print. | 20 | | `limit` | integer | false | The maximum number of statuses to print. | 20 |
## Media Attachment ## Media Attachments
### View media attachment ### 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 to a text file and specify the its 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 that has been written to 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.<br>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.<br>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.<br>Here this should be `media-attachment`. | |
| `media-description` | string | false | The description of the media attachment to edit.<br>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. Prints information about a given media attachment that you own.
You can only see information about the media attachment that you own. You can only see information about the media attachment that you own.

View file

@ -1,8 +1,14 @@
package client package client
import ( import (
"bytes"
"encoding/json"
"fmt" "fmt"
"io"
"mime/multipart"
"net/http" "net/http"
"os"
"path/filepath"
"codeflow.dananglin.me.uk/apollo/enbas/internal/model" "codeflow.dananglin.me.uk/apollo/enbas/internal/model"
) )
@ -31,12 +37,123 @@ func (g *Client) GetMediaAttachment(mediaAttachmentID string) (model.Attachment,
return attachment, nil return attachment, nil
} }
//type CreateMediaAttachmentForm struct { func (g *Client) CreateMediaAttachment(path, description, focus string) (model.Attachment, error) {
// Description string file, err := os.Open(path)
// Focus string if err != nil {
// Filepath string return model.Attachment{}, fmt.Errorf("unable to open the file: %w", err)
//} }
// defer file.Close()
//func (g *Client) CreateMediaAttachment(form CreateMediaAttachmentForm) (model.Attachment, error) {
// return model.Attachment{}, nil // 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
}

View file

@ -50,6 +50,7 @@ type CreateStatusForm struct {
Poll *CreateStatusPollForm `json:"poll,omitempty"` Poll *CreateStatusPollForm `json:"poll,omitempty"`
ContentType model.StatusContentType `json:"content_type"` ContentType model.StatusContentType `json:"content_type"`
Visibility model.StatusVisibility `json:"visibility"` Visibility model.StatusVisibility `json:"visibility"`
AttachmentIDs []string `json:"media_ids,omitempty"`
} }
type CreateStatusPollForm struct { type CreateStatusPollForm struct {

View file

@ -1,6 +1,7 @@
package executor package executor
import ( import (
"errors"
"fmt" "fmt"
"codeflow.dananglin.me.uk/apollo/enbas/internal/client" "codeflow.dananglin.me.uk/apollo/enbas/internal/client"
@ -21,6 +22,7 @@ func (c *CreateExecutor) Execute() error {
funcMap := map[string]func(*client.Client) error{ funcMap := map[string]func(*client.Client) error{
resourceList: c.createList, resourceList: c.createList,
resourceStatus: c.createStatus, resourceStatus: c.createStatus,
resourceMediaAttachment: c.createMediaAttachment,
} }
doFunc, ok := funcMap[c.resourceType] doFunc, ok := funcMap[c.resourceType]
@ -66,20 +68,94 @@ func (c *CreateExecutor) createStatus(gtsClient *client.Client) error {
sensitive bool 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 { switch {
case c.content != "": case c.content != "":
content = c.content content = c.content
case c.fromFile != "": case c.fromFile != "":
content, err = utilities.ReadFile(c.fromFile) content, err = utilities.ReadTextFile(c.fromFile)
if err != nil { if err != nil {
return fmt.Errorf("unable to get the status contents from %q: %w", c.fromFile, err) return fmt.Errorf("unable to get the status contents from %q: %w", c.fromFile, err)
} }
default: default:
if len(attachmentIDs) == 0 {
// TODO: revisit this error type
return EmptyContentError{ return EmptyContentError{
ResourceType: resourceStatus, ResourceType: resourceStatus,
Hint: "please use --" + flagContent + " or --" + flagFromFile, 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() preferences, err := gtsClient.GetUserPreferences()
if err != nil { if err != nil {
@ -127,6 +203,11 @@ func (c *CreateExecutor) createStatus(gtsClient *client.Client) error {
Sensitive: sensitive, Sensitive: sensitive,
Visibility: parsedVisibility, Visibility: parsedVisibility,
Poll: nil, Poll: nil,
AttachmentIDs: nil,
}
if numAttachmentIDs > 0 {
form.AttachmentIDs = attachmentIDs
} }
if c.addPoll { if c.addPoll {
@ -153,3 +234,56 @@ func (c *CreateExecutor) createStatus(gtsClient *client.Client) error {
return nil 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
}

View file

@ -5,6 +5,7 @@ import (
"codeflow.dananglin.me.uk/apollo/enbas/internal/client" "codeflow.dananglin.me.uk/apollo/enbas/internal/client"
"codeflow.dananglin.me.uk/apollo/enbas/internal/model" "codeflow.dananglin.me.uk/apollo/enbas/internal/model"
"codeflow.dananglin.me.uk/apollo/enbas/internal/utilities"
) )
func (e *EditExecutor) Execute() error { func (e *EditExecutor) Execute() error {
@ -14,6 +15,7 @@ func (e *EditExecutor) Execute() error {
funcMap := map[string]func(*client.Client) error{ funcMap := map[string]func(*client.Client) error{
resourceList: e.editList, resourceList: e.editList,
resourceMediaAttachment: e.editMediaAttachment,
} }
doFunc, ok := funcMap[e.resourceType] 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) 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) e.printer.PrintList(updatedList)
return nil 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
}

View file

@ -121,6 +121,7 @@ type CreateExecutor struct {
printer *printer.Printer printer *printer.Printer
config *config.Config config *config.Config
addPoll bool addPoll bool
attachmentIDs internalFlag.StringSliceValue
content string content string
contentType string contentType string
federated bool federated bool
@ -132,6 +133,9 @@ type CreateExecutor struct {
language string language string
listRepliesPolicy string listRepliesPolicy string
listTitle string listTitle string
mediaDescriptions internalFlag.StringSliceValue
mediaFocusValues internalFlag.StringSliceValue
mediaFiles internalFlag.StringSliceValue
pollAllowsMultipleChoices bool pollAllowsMultipleChoices bool
pollExpiresIn internalFlag.TimeDurationValue pollExpiresIn internalFlag.TimeDurationValue
pollHidesVoteCounts bool pollHidesVoteCounts bool
@ -150,6 +154,10 @@ func NewCreateExecutor(
FlagSet: flag.NewFlagSet("create", flag.ExitOnError), FlagSet: flag.NewFlagSet("create", flag.ExitOnError),
printer: printer, printer: printer,
config: config, config: config,
attachmentIDs: internalFlag.NewStringSliceValue(),
mediaDescriptions: internalFlag.NewStringSliceValue(),
mediaFocusValues: internalFlag.NewStringSliceValue(),
mediaFiles: internalFlag.NewStringSliceValue(),
pollExpiresIn: internalFlag.NewTimeDurationValue(), pollExpiresIn: internalFlag.NewTimeDurationValue(),
pollOptions: internalFlag.NewStringSliceValue(), pollOptions: internalFlag.NewStringSliceValue(),
sensitive: internalFlag.NewBoolPtrValue(), sensitive: internalFlag.NewBoolPtrValue(),
@ -158,6 +166,7 @@ func NewCreateExecutor(
exe.Usage = usage.ExecutorUsageFunc("create", "Creates a specific resource", exe.FlagSet) 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.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.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.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") 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.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.listRepliesPolicy, "list-replies-policy", "list", "The replies policy of the list")
exe.StringVar(&exe.listTitle, "list-title", "", "The title 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.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.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") 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 *flag.FlagSet
printer *printer.Printer printer *printer.Printer
config *config.Config config *config.Config
attachmentIDs internalFlag.StringSliceValue
listID string listID string
listTitle string listTitle string
listRepliesPolicy string listRepliesPolicy string
mediaDescriptions internalFlag.StringSliceValue
mediaFocusValues internalFlag.StringSliceValue
resourceType string resourceType string
} }
@ -227,13 +242,19 @@ func NewEditExecutor(
FlagSet: flag.NewFlagSet("edit", flag.ExitOnError), FlagSet: flag.NewFlagSet("edit", flag.ExitOnError),
printer: printer, printer: printer,
config: config, config: config,
attachmentIDs: internalFlag.NewStringSliceValue(),
mediaDescriptions: internalFlag.NewStringSliceValue(),
mediaFocusValues: internalFlag.NewStringSliceValue(),
} }
exe.Usage = usage.ExecutorUsageFunc("edit", "Edit a specific resource", exe.FlagSet) 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.listID, "list-id", "", "The ID of the list in question")
exe.StringVar(&exe.listTitle, "list-title", "", "The title of the list") 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.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)") exe.StringVar(&exe.resourceType, "type", "", "The type of resource you want to action on (e.g. account, status)")
return &exe return &exe

View file

@ -5,9 +5,20 @@ import (
"errors" "errors"
"fmt" "fmt"
"os" "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) file, err := os.Open(path)
if err != nil { if err != nil {
return "", fmt.Errorf("unable to open %q: %w", path, err) return "", fmt.Errorf("unable to open %q: %w", path, err)

View file

@ -96,6 +96,18 @@
"type": "string", "type": "string",
"description": "The replies policy of the list" "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": { "mute-duration": {
"type": "TimeDurationValue", "type": "TimeDurationValue",
"description": "Specify how long the mute should last for. To mute indefinitely, set this to 0s" "description": "Specify how long the mute should last for. To mute indefinitely, set this to 0s"
@ -234,6 +246,7 @@
"additionalFields": [], "additionalFields": [],
"flags": [ "flags": [
{ "flag": "add-poll", "default": "false" }, { "flag": "add-poll", "default": "false" },
{ "flag": "attachment-id", "fieldName": "attachmentIDs" },
{ "flag": "content", "default": "" }, { "flag": "content", "default": "" },
{ "flag": "content-type", "default": "plain" }, { "flag": "content-type", "default": "plain" },
{ "flag": "enable-federation", "fieldName": "federated", "default": "true" }, { "flag": "enable-federation", "fieldName": "federated", "default": "true" },
@ -245,6 +258,9 @@
{ "flag": "language", "default": "" }, { "flag": "language", "default": "" },
{ "flag": "list-replies-policy", "default": "list" }, { "flag": "list-replies-policy", "default": "list" },
{ "flag": "list-title", "default": "" }, { "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-allows-multiple-choices", "default": "false" },
{ "flag": "poll-expires-in" }, { "flag": "poll-expires-in" },
{ "flag": "poll-hides-vote-counts", "default": "false" }, { "flag": "poll-hides-vote-counts", "default": "false" },
@ -271,9 +287,12 @@
"edit": { "edit": {
"additionalFields": [], "additionalFields": [],
"flags": [ "flags": [
{ "flag": "attachment-id", "fieldName": "attachmentIDs" },
{ "flag": "list-id", "fieldName": "listID", "default": ""}, { "flag": "list-id", "fieldName": "listID", "default": ""},
{ "flag": "list-title", "default": "" }, { "flag": "list-title", "default": "" },
{ "flag": "list-replies-policy", "default": "" }, { "flag": "list-replies-policy", "default": "" },
{ "flag": "media-description", "fieldName": "mediaDescriptions" },
{ "flag": "media-focus", "fieldName": "mediaFocusValues" },
{ "flag": "type", "fieldName": "resourceType", "default": "" } { "flag": "type", "fieldName": "resourceType", "default": "" }
], ],
"summary": "Edit a specific resource", "summary": "Edit a specific resource",