Compare commits

...

5 commits

7 changed files with 384 additions and 49 deletions

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,6 +68,71 @@ 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
@ -75,11 +142,20 @@ func (c *CreateExecutor) createStatus(gtsClient *client.Client) error {
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,40 @@ 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,
)
}
if !c.mediaDescriptions.ExpectedLength(expectedNumValues) {
return fmt.Errorf(
"received an unexpected number of media descriptions: want %d",
expectedNumValues,
)
}
if !c.mediaFocusValues.ExpectedLength(expectedNumValues) {
return fmt.Errorf(
"received an unexpected number of media focus values: want %d",
expectedNumValues,
)
}
attachment, err := gtsClient.CreateMediaAttachment(
c.mediaFiles[0],
c.mediaDescriptions[0],
c.mediaFocusValues[0],
)
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

@ -14,6 +14,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 +58,55 @@ 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,
)
}
if !e.mediaDescriptions.ExpectedLength(expectedNumValues) {
return fmt.Errorf(
"received an unexpected number of media descriptions: want %d",
expectedNumValues,
)
}
if !e.mediaFocusValues.ExpectedLength(expectedNumValues) {
return fmt.Errorf(
"received an unexpected number of media focus values: 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 := e.mediaDescriptions[0]
if description == "" {
description = attachment.Description
}
focus := e.mediaFocusValues[0]
if focus == "" {
focus = fmt.Sprintf("%f,%f", attachment.Meta.Focus.X, attachment.Meta.Focus.Y)
}
if _, err = gtsClient.UpdateMediaAttachment(e.attachmentIDs[0], 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 that 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 that 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

@ -4,8 +4,19 @@ import (
"errors" "errors"
"fmt" "fmt"
"os" "os"
"strings"
) )
const filePrefix string = "file@"
func ReadContents(text string) (string, error) {
if !strings.HasPrefix(text, filePrefix) {
return text, nil
}
return ReadFile(strings.TrimPrefix(text, filePrefix))
}
func ReadFile(path string) (string, error) { func ReadFile(path string) (string, error) {
data, err := os.ReadFile(path) data, err := os.ReadFile(path)
if err != nil { if err != nil {

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 that 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",