diff --git a/internal/client/statuses.go b/internal/client/statuses.go index 0a559fc..fee3af4 100644 --- a/internal/client/statuses.go +++ b/internal/client/statuses.go @@ -38,10 +38,18 @@ type CreateStatusForm struct { 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"` } +type CreateStatusPollForm struct { + Options []string `json:"options"` + ExpiresIn int `json:"expires_in"` + Multiple bool `json:"multiple"` + HideTotals bool `json:"hide_totals"` +} + func (g *Client) CreateStatus(form CreateStatusForm) (model.Status, error) { data, err := json.Marshal(form) if err != nil { diff --git a/internal/executor/add.go b/internal/executor/add.go index 9955017..25350dc 100644 --- a/internal/executor/add.go +++ b/internal/executor/add.go @@ -21,8 +21,8 @@ type AddExecutor struct { listID string statusID string pollID string - choices PollChoices - accountNames AccountNames + choices MultiIntFlagValue + accountNames MultiStringFlagValue content string } @@ -31,7 +31,7 @@ func NewAddExecutor(tlf TopLevelFlags, name, summary string) *AddExecutor { addExe := AddExecutor{ FlagSet: flag.NewFlagSet(name, flag.ExitOnError), - accountNames: AccountNames(emptyArr), + accountNames: MultiStringFlagValue(emptyArr), topLevelFlags: tlf, } @@ -265,7 +265,7 @@ func (a *AddExecutor) addVoteToPoll(gtsClient *client.Client) error { } if poll.Expired { - return ExpiredPollError{} + return PollClosedError{} } if !poll.Multiple && len(a.choices) > 1 { diff --git a/internal/executor/create.go b/internal/executor/create.go index d05814c..4385997 100644 --- a/internal/executor/create.go +++ b/internal/executor/create.go @@ -17,21 +17,26 @@ import ( type CreateExecutor struct { *flag.FlagSet - topLevelFlags TopLevelFlags - boostable bool - federated bool - likeable bool - replyable bool - sensitive *bool - content string - contentType string - fromFile string - language string - spoilerText string - resourceType string - listTitle string - listRepliesPolicy string - visibility string + topLevelFlags TopLevelFlags + addPoll bool + boostable bool + federated bool + likeable bool + pollAllowMultipleChoices bool + pollHideVoteCounts bool + replyable bool + sensitive *bool + content string + contentType string + fromFile string + language string + resourceType string + listTitle string + listRepliesPolicy string + spoilerText string + visibility string + pollExpiresIn TimeDurationFlagValue + pollOptions MultiStringFlagValue } func NewCreateExecutor(tlf TopLevelFlags, name, summary string) *CreateExecutor { @@ -45,6 +50,9 @@ func NewCreateExecutor(tlf TopLevelFlags, name, summary string) *CreateExecutor createExe.BoolVar(&createExe.federated, flagEnableFederation, true, "Specify if the status can be federated beyond the local timelines") createExe.BoolVar(&createExe.likeable, flagEnableLikes, true, "Specify if the status can be liked/favourited") createExe.BoolVar(&createExe.replyable, flagEnableReplies, true, "Specify if the status can be replied to") + createExe.BoolVar(&createExe.pollAllowMultipleChoices, flagPollAllowsMultipleChoices, false, "The poll allows viewers to make multiple choices in the poll") + createExe.BoolVar(&createExe.pollHideVoteCounts, flagPollHidesVoteCounts, false, "The poll will hide the vote count until it is closed") + createExe.BoolVar(&createExe.addPoll, flagAddPoll, false, "Add a poll to the status") createExe.StringVar(&createExe.content, flagContent, "", "The content of the status to create") createExe.StringVar(&createExe.contentType, flagContentType, "plain", "The type that the contents should be parsed from (valid values are plain and markdown)") createExe.StringVar(&createExe.fromFile, flagFromFile, "", "The file path where to read the contents from") @@ -54,6 +62,8 @@ func NewCreateExecutor(tlf TopLevelFlags, name, summary string) *CreateExecutor createExe.StringVar(&createExe.resourceType, flagType, "", "Specify the type of resource to create") createExe.StringVar(&createExe.listTitle, flagListTitle, "", "Specify the title of the list") createExe.StringVar(&createExe.listRepliesPolicy, flagListRepliesPolicy, "list", "Specify the policy of the replies for this list (valid values are followed, list and none)") + createExe.Var(&createExe.pollOptions, flagPollOption, "A poll option. Use this multiple times to set multiple options") + createExe.Var(&createExe.pollExpiresIn, flagPollExpiresIn, "The duration in which the poll is open for") createExe.BoolFunc(flagSensitive, "Specify if the status should be marked as sensitive", func(value string) error { boolVal, err := strconv.ParseBool(value) @@ -189,6 +199,22 @@ func (c *CreateExecutor) createStatus(gtsClient *client.Client) error { Replyable: c.replyable, Sensitive: sensitive, Visibility: parsedVisibility, + Poll: nil, + } + + if c.addPoll { + if len(c.pollOptions) == 0 { + return NoPollOptionError{} + } + + poll := client.CreateStatusPollForm{ + Options: c.pollOptions, + Multiple: c.pollAllowMultipleChoices, + HideTotals: c.pollHideVoteCounts, + ExpiresIn: int(c.pollExpiresIn.Duration.Seconds()), + } + + form.Poll = &poll } status, err := gtsClient.CreateStatus(form) diff --git a/internal/executor/errors.go b/internal/executor/errors.go index 9fa9fef..b001de7 100644 --- a/internal/executor/errors.go +++ b/internal/executor/errors.go @@ -32,7 +32,11 @@ type UnsupportedAddOperationError struct { } func (e UnsupportedAddOperationError) Error() string { - return "adding '" + e.ResourceType + "' to '" + e.AddToResourceType + "' is not supported" + return "adding '" + + e.ResourceType + + "' to '" + + e.AddToResourceType + + "' is not supported" } type UnsupportedRemoveOperationError struct { @@ -41,7 +45,11 @@ type UnsupportedRemoveOperationError struct { } func (e UnsupportedRemoveOperationError) Error() string { - return "removing '" + e.ResourceType + "' from '" + e.RemoveFromResourceType + "' is not supported" + return "removing '" + + e.ResourceType + + "' from '" + + e.RemoveFromResourceType + + "' is not supported" } type EmptyContentError struct { @@ -67,10 +75,10 @@ func (e UnknownCommandError) Error() string { return "unknown command '" + e.Command + "'" } -type ExpiredPollError struct{} +type PollClosedError struct{} -func (e ExpiredPollError) Error() string { - return "this poll has expired" +func (e PollClosedError) Error() string { + return "this poll is closed" } type MultipleChoiceError struct{} @@ -78,3 +86,11 @@ type MultipleChoiceError struct{} func (e MultipleChoiceError) Error() string { return "this poll does not allow multiple choices" } + +type NoPollOptionError struct{} + +func (e NoPollOptionError) Error() string { + return "no options were provided for this poll, please use the --" + + flagPollOption + + " flag to add options to the poll" +} diff --git a/internal/executor/flags.go b/internal/executor/flags.go index 42e2052..13227db 100644 --- a/internal/executor/flags.go +++ b/internal/executor/flags.go @@ -8,70 +8,76 @@ import ( "fmt" "strconv" "strings" + "time" ) const ( - flagAccountName = "account-name" - flagBrowser = "browser" - flagChoose = "choose" - flagContentType = "content-type" - flagContent = "content" - flagEnableFederation = "enable-federation" - flagEnableLikes = "enable-likes" - flagEnableReplies = "enable-replies" - flagEnableReposts = "enable-reposts" - flagFrom = "from" - flagFromFile = "from-file" - flagFull = "full" - flagInstance = "instance" - flagLanguage = "language" - flagLimit = "limit" - flagListID = "list-id" - flagListTitle = "list-title" - flagListRepliesPolicy = "list-replies-policy" - flagMyAccount = "my-account" - flagNotify = "notify" - flagPollID = "poll-id" - flagSensitive = "sensitive" - flagSkipRelationship = "skip-relationship" - flagShowPreferences = "show-preferences" - flagShowReposts = "show-reposts" - flagSpoilerText = "spoiler-text" - flagStatusID = "status-id" - flagTag = "tag" - flagTimelineCategory = "timeline-category" - flagTo = "to" - flagType = "type" - flagVisibility = "visibility" + flagAddPoll = "add-poll" + flagAccountName = "account-name" + flagBrowser = "browser" + flagChoose = "choose" + flagContentType = "content-type" + flagContent = "content" + flagEnableFederation = "enable-federation" + flagEnableLikes = "enable-likes" + flagEnableReplies = "enable-replies" + flagEnableReposts = "enable-reposts" + flagFrom = "from" + flagFromFile = "from-file" + flagFull = "full" + flagInstance = "instance" + flagLanguage = "language" + flagLimit = "limit" + flagListID = "list-id" + flagListTitle = "list-title" + flagListRepliesPolicy = "list-replies-policy" + flagMyAccount = "my-account" + flagNotify = "notify" + flagPollAllowsMultipleChoices = "poll-allows-multiple-choices" + flagPollExpiresIn = "poll-expires-in" + flagPollHidesVoteCounts = "poll-hides-vote-counts" + flagPollID = "poll-id" + flagPollOption = "poll-option" + flagSensitive = "sensitive" + flagSkipRelationship = "skip-relationship" + flagShowPreferences = "show-preferences" + flagShowReposts = "show-reposts" + flagSpoilerText = "spoiler-text" + flagStatusID = "status-id" + flagTag = "tag" + flagTimelineCategory = "timeline-category" + flagTo = "to" + flagType = "type" + flagVisibility = "visibility" ) -type AccountNames []string - -func (a *AccountNames) String() string { - return strings.Join(*a, ", ") -} - -func (a *AccountNames) Set(value string) error { - if len(value) > 0 { - *a = append(*a, value) - } - - return nil -} - type TopLevelFlags struct { ConfigDir string NoColor *bool Pager string } -type PollChoices []int +type MultiStringFlagValue []string -func (p *PollChoices) String() string { +func (v *MultiStringFlagValue) String() string { + return strings.Join(*v, ", ") +} + +func (v *MultiStringFlagValue) Set(value string) error { + if len(value) > 0 { + *v = append(*v, value) + } + + return nil +} + +type MultiIntFlagValue []int + +func (v *MultiIntFlagValue) String() string { value := "Choices: " - for ind, vote := range *p { - if ind == len(*p)-1 { + for ind, vote := range *v { + if ind == len(*v)-1 { value += strconv.Itoa(vote) } else { value += strconv.Itoa(vote) + ", " @@ -81,13 +87,32 @@ func (p *PollChoices) String() string { return value } -func (p *PollChoices) Set(text string) error { +func (v *MultiIntFlagValue) Set(text string) error { value, err := strconv.Atoi(text) if err != nil { return fmt.Errorf("unable to parse the value to an integer: %w", err) } - *p = append(*p, value) + *v = append(*v, value) + + return nil +} + +type TimeDurationFlagValue struct { + Duration time.Duration +} + +func (v TimeDurationFlagValue) String() string { + return "" +} + +func (v *TimeDurationFlagValue) Set(text string) error { + duration, err := time.ParseDuration(text) + if err != nil { + return fmt.Errorf("unable to parse the value as time duration: %w", err) + } + + v.Duration = duration return nil } diff --git a/internal/executor/remove.go b/internal/executor/remove.go index 53cdf40..01ecf1f 100644 --- a/internal/executor/remove.go +++ b/internal/executor/remove.go @@ -19,7 +19,7 @@ type RemoveExecutor struct { fromResourceType string listID string statusID string - accountNames AccountNames + accountNames MultiStringFlagValue } func NewRemoveExecutor(tlf TopLevelFlags, name, summary string) *RemoveExecutor { @@ -27,7 +27,7 @@ func NewRemoveExecutor(tlf TopLevelFlags, name, summary string) *RemoveExecutor removeExe := RemoveExecutor{ FlagSet: flag.NewFlagSet(name, flag.ExitOnError), - accountNames: AccountNames(emptyArr), + accountNames: MultiStringFlagValue(emptyArr), topLevelFlags: tlf, }