feat: add support for polls #24
11 changed files with 483 additions and 126 deletions
55
internal/client/poll.go
Normal file
55
internal/client/poll.go
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Dan Anglin <d.n.i.anglin@gmail.com>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"codeflow.dananglin.me.uk/apollo/enbas/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
pollPath string = "/api/v1/polls"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (g *Client) GetPoll(pollID string) (model.Poll, error) {
|
||||||
|
url := g.Authentication.Instance + pollPath + "/" + pollID
|
||||||
|
|
||||||
|
var poll model.Poll
|
||||||
|
|
||||||
|
if err := g.sendRequest(http.MethodGet, url, nil, &poll); err != nil {
|
||||||
|
return model.Poll{}, fmt.Errorf(
|
||||||
|
"received an error after sending the request to get the poll: %w",
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return poll, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Client) VoteInPoll(pollID string, choices []int) error {
|
||||||
|
form := struct {
|
||||||
|
Choices []int `json:"choices"`
|
||||||
|
}{
|
||||||
|
Choices: choices,
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(form)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to encode the JSON form: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
requestBody := bytes.NewBuffer(data)
|
||||||
|
url := g.Authentication.Instance + pollPath + "/" + pollID + "/votes"
|
||||||
|
|
||||||
|
if err := g.sendRequest(http.MethodPost, url, requestBody, nil); err != nil {
|
||||||
|
return fmt.Errorf("received an error after sending the request to vote in the poll: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -38,10 +38,18 @@ type CreateStatusForm struct {
|
||||||
Likeable bool `json:"likeable"`
|
Likeable bool `json:"likeable"`
|
||||||
Replyable bool `json:"replyable"`
|
Replyable bool `json:"replyable"`
|
||||||
Sensitive bool `json:"sensitive"`
|
Sensitive bool `json:"sensitive"`
|
||||||
|
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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
func (g *Client) CreateStatus(form CreateStatusForm) (model.Status, error) {
|
||||||
data, err := json.Marshal(form)
|
data, err := json.Marshal(form)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
package executor
|
package executor
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
@ -19,7 +20,9 @@ type AddExecutor struct {
|
||||||
toResourceType string
|
toResourceType string
|
||||||
listID string
|
listID string
|
||||||
statusID string
|
statusID string
|
||||||
accountNames AccountNames
|
pollID string
|
||||||
|
choices MultiIntFlagValue
|
||||||
|
accountNames MultiStringFlagValue
|
||||||
content string
|
content string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,16 +31,18 @@ func NewAddExecutor(tlf TopLevelFlags, name, summary string) *AddExecutor {
|
||||||
|
|
||||||
addExe := AddExecutor{
|
addExe := AddExecutor{
|
||||||
FlagSet: flag.NewFlagSet(name, flag.ExitOnError),
|
FlagSet: flag.NewFlagSet(name, flag.ExitOnError),
|
||||||
accountNames: AccountNames(emptyArr),
|
accountNames: MultiStringFlagValue(emptyArr),
|
||||||
topLevelFlags: tlf,
|
topLevelFlags: tlf,
|
||||||
}
|
}
|
||||||
|
|
||||||
addExe.StringVar(&addExe.resourceType, flagType, "", "Specify the resource type to add (e.g. account, note)")
|
addExe.StringVar(&addExe.resourceType, flagType, "", "Specify the resource type to add (e.g. account, note)")
|
||||||
addExe.StringVar(&addExe.toResourceType, flagTo, "", "Specify the target resource type to add to (e.g. list, account, etc)")
|
addExe.StringVar(&addExe.toResourceType, flagTo, "", "Specify the target resource type to add to (e.g. list, account, etc)")
|
||||||
addExe.StringVar(&addExe.listID, flagListID, "", "The ID of the list to add to")
|
addExe.StringVar(&addExe.listID, flagListID, "", "The ID of the list")
|
||||||
addExe.StringVar(&addExe.statusID, flagStatusID, "", "The ID of the status")
|
addExe.StringVar(&addExe.statusID, flagStatusID, "", "The ID of the status")
|
||||||
addExe.Var(&addExe.accountNames, flagAccountName, "The name of the account")
|
|
||||||
addExe.StringVar(&addExe.content, flagContent, "", "The content of the resource")
|
addExe.StringVar(&addExe.content, flagContent, "", "The content of the resource")
|
||||||
|
addExe.StringVar(&addExe.pollID, flagPollID, "", "The ID of the poll")
|
||||||
|
addExe.Var(&addExe.accountNames, flagAccountName, "The name of the account")
|
||||||
|
addExe.Var(&addExe.choices, flagChoose, "Specify your choice ")
|
||||||
|
|
||||||
addExe.Usage = commandUsageFunc(name, summary, addExe.FlagSet)
|
addExe.Usage = commandUsageFunc(name, summary, addExe.FlagSet)
|
||||||
|
|
||||||
|
@ -54,6 +59,7 @@ func (a *AddExecutor) Execute() error {
|
||||||
resourceAccount: a.addToAccount,
|
resourceAccount: a.addToAccount,
|
||||||
resourceBookmarks: a.addToBookmarks,
|
resourceBookmarks: a.addToBookmarks,
|
||||||
resourceStatus: a.addToStatus,
|
resourceStatus: a.addToStatus,
|
||||||
|
resourcePoll: a.addToPoll,
|
||||||
}
|
}
|
||||||
|
|
||||||
doFunc, ok := funcMap[a.toResourceType]
|
doFunc, ok := funcMap[a.toResourceType]
|
||||||
|
@ -227,3 +233,50 @@ func (a *AddExecutor) addBoostToStatus(gtsClient *client.Client) error {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *AddExecutor) addToPoll(gtsClient *client.Client) error {
|
||||||
|
if a.pollID == "" {
|
||||||
|
return FlagNotSetError{flagText: flagPollID}
|
||||||
|
}
|
||||||
|
|
||||||
|
funcMap := map[string]func(*client.Client) error{
|
||||||
|
resourceVote: a.addVoteToPoll,
|
||||||
|
}
|
||||||
|
|
||||||
|
doFunc, ok := funcMap[a.resourceType]
|
||||||
|
if !ok {
|
||||||
|
return UnsupportedAddOperationError{
|
||||||
|
ResourceType: a.resourceType,
|
||||||
|
AddToResourceType: a.toResourceType,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return doFunc(gtsClient)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AddExecutor) addVoteToPoll(gtsClient *client.Client) error {
|
||||||
|
if len(a.choices) == 0 {
|
||||||
|
return errors.New("please use --" + flagChoose + " to make a choice in this poll")
|
||||||
|
}
|
||||||
|
|
||||||
|
poll, err := gtsClient.GetPoll(a.pollID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to retrieve the poll: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if poll.Expired {
|
||||||
|
return PollClosedError{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !poll.Multiple && len(a.choices) > 1 {
|
||||||
|
return MultipleChoiceError{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := gtsClient.VoteInPoll(a.pollID, []int(a.choices)); err != nil {
|
||||||
|
return fmt.Errorf("unable to add your vote(s) to the poll: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Successfully added your vote(s) to the poll.")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -17,21 +17,26 @@ import (
|
||||||
type CreateExecutor struct {
|
type CreateExecutor struct {
|
||||||
*flag.FlagSet
|
*flag.FlagSet
|
||||||
|
|
||||||
topLevelFlags TopLevelFlags
|
topLevelFlags TopLevelFlags
|
||||||
boostable bool
|
addPoll bool
|
||||||
federated bool
|
boostable bool
|
||||||
likeable bool
|
federated bool
|
||||||
replyable bool
|
likeable bool
|
||||||
sensitive *bool
|
pollAllowsMultipleChoices bool
|
||||||
content string
|
pollHidesVoteCounts bool
|
||||||
contentType string
|
replyable bool
|
||||||
fromFile string
|
sensitive *bool
|
||||||
language string
|
content string
|
||||||
spoilerText string
|
contentType string
|
||||||
resourceType string
|
fromFile string
|
||||||
listTitle string
|
language string
|
||||||
listRepliesPolicy string
|
resourceType string
|
||||||
visibility string
|
listTitle string
|
||||||
|
listRepliesPolicy string
|
||||||
|
spoilerText string
|
||||||
|
visibility string
|
||||||
|
pollExpiresIn TimeDurationFlagValue
|
||||||
|
pollOptions MultiStringFlagValue
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCreateExecutor(tlf TopLevelFlags, name, summary string) *CreateExecutor {
|
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.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.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.replyable, flagEnableReplies, true, "Specify if the status can be replied to")
|
||||||
|
createExe.BoolVar(&createExe.pollAllowsMultipleChoices, flagPollAllowsMultipleChoices, false, "The poll allows viewers to make multiple choices in the poll")
|
||||||
|
createExe.BoolVar(&createExe.pollHidesVoteCounts, 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.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.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")
|
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.resourceType, flagType, "", "Specify the type of resource to create")
|
||||||
createExe.StringVar(&createExe.listTitle, flagListTitle, "", "Specify the title of the list")
|
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.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 {
|
createExe.BoolFunc(flagSensitive, "Specify if the status should be marked as sensitive", func(value string) error {
|
||||||
boolVal, err := strconv.ParseBool(value)
|
boolVal, err := strconv.ParseBool(value)
|
||||||
|
@ -189,6 +199,22 @@ func (c *CreateExecutor) createStatus(gtsClient *client.Client) error {
|
||||||
Replyable: c.replyable,
|
Replyable: c.replyable,
|
||||||
Sensitive: sensitive,
|
Sensitive: sensitive,
|
||||||
Visibility: parsedVisibility,
|
Visibility: parsedVisibility,
|
||||||
|
Poll: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.addPoll {
|
||||||
|
if len(c.pollOptions) == 0 {
|
||||||
|
return NoPollOptionError{}
|
||||||
|
}
|
||||||
|
|
||||||
|
poll := client.CreateStatusPollForm{
|
||||||
|
Options: c.pollOptions,
|
||||||
|
Multiple: c.pollAllowsMultipleChoices,
|
||||||
|
HideTotals: c.pollHidesVoteCounts,
|
||||||
|
ExpiresIn: int(c.pollExpiresIn.Duration.Seconds()),
|
||||||
|
}
|
||||||
|
|
||||||
|
form.Poll = &poll
|
||||||
}
|
}
|
||||||
|
|
||||||
status, err := gtsClient.CreateStatus(form)
|
status, err := gtsClient.CreateStatus(form)
|
||||||
|
|
|
@ -32,7 +32,11 @@ type UnsupportedAddOperationError struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e UnsupportedAddOperationError) Error() string {
|
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 {
|
type UnsupportedRemoveOperationError struct {
|
||||||
|
@ -41,7 +45,11 @@ type UnsupportedRemoveOperationError struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e UnsupportedRemoveOperationError) Error() string {
|
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 {
|
type EmptyContentError struct {
|
||||||
|
@ -66,3 +74,23 @@ type UnknownCommandError struct {
|
||||||
func (e UnknownCommandError) Error() string {
|
func (e UnknownCommandError) Error() string {
|
||||||
return "unknown command '" + e.Command + "'"
|
return "unknown command '" + e.Command + "'"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PollClosedError struct{}
|
||||||
|
|
||||||
|
func (e PollClosedError) Error() string {
|
||||||
|
return "this poll is closed"
|
||||||
|
}
|
||||||
|
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
|
|
@ -4,57 +4,115 @@
|
||||||
|
|
||||||
package executor
|
package executor
|
||||||
|
|
||||||
import "strings"
|
import (
|
||||||
|
"fmt"
|
||||||
const (
|
"strconv"
|
||||||
flagAccountName = "account-name"
|
"strings"
|
||||||
flagBrowser = "browser"
|
"time"
|
||||||
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"
|
|
||||||
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
|
const (
|
||||||
|
flagAddPoll = "add-poll"
|
||||||
func (a *AccountNames) String() string {
|
flagAccountName = "account-name"
|
||||||
return strings.Join(*a, ", ")
|
flagBrowser = "browser"
|
||||||
}
|
flagChoose = "choose"
|
||||||
|
flagContentType = "content-type"
|
||||||
func (a *AccountNames) Set(value string) error {
|
flagContent = "content"
|
||||||
if len(value) > 0 {
|
flagEnableFederation = "enable-federation"
|
||||||
*a = append(*a, value)
|
flagEnableLikes = "enable-likes"
|
||||||
}
|
flagEnableReplies = "enable-replies"
|
||||||
|
flagEnableReposts = "enable-reposts"
|
||||||
return nil
|
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 TopLevelFlags struct {
|
type TopLevelFlags struct {
|
||||||
ConfigDir string
|
ConfigDir string
|
||||||
NoColor *bool
|
NoColor *bool
|
||||||
Pager string
|
Pager string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MultiStringFlagValue []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 *v {
|
||||||
|
if ind == len(*v)-1 {
|
||||||
|
value += strconv.Itoa(vote)
|
||||||
|
} else {
|
||||||
|
value += strconv.Itoa(vote) + ", "
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
*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
|
||||||
|
}
|
||||||
|
|
|
@ -19,7 +19,7 @@ type RemoveExecutor struct {
|
||||||
fromResourceType string
|
fromResourceType string
|
||||||
listID string
|
listID string
|
||||||
statusID string
|
statusID string
|
||||||
accountNames AccountNames
|
accountNames MultiStringFlagValue
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRemoveExecutor(tlf TopLevelFlags, name, summary string) *RemoveExecutor {
|
func NewRemoveExecutor(tlf TopLevelFlags, name, summary string) *RemoveExecutor {
|
||||||
|
@ -27,7 +27,7 @@ func NewRemoveExecutor(tlf TopLevelFlags, name, summary string) *RemoveExecutor
|
||||||
|
|
||||||
removeExe := RemoveExecutor{
|
removeExe := RemoveExecutor{
|
||||||
FlagSet: flag.NewFlagSet(name, flag.ExitOnError),
|
FlagSet: flag.NewFlagSet(name, flag.ExitOnError),
|
||||||
accountNames: AccountNames(emptyArr),
|
accountNames: MultiStringFlagValue(emptyArr),
|
||||||
topLevelFlags: tlf,
|
topLevelFlags: tlf,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
package executor
|
package executor
|
||||||
|
|
||||||
const(
|
const (
|
||||||
resourceAccount = "account"
|
resourceAccount = "account"
|
||||||
resourceBlocked = "blocked"
|
resourceBlocked = "blocked"
|
||||||
resourceBookmarks = "bookmarks"
|
resourceBookmarks = "bookmarks"
|
||||||
|
@ -17,8 +17,10 @@ const(
|
||||||
resourceLiked = "liked"
|
resourceLiked = "liked"
|
||||||
resourceList = "list"
|
resourceList = "list"
|
||||||
resourceNote = "note"
|
resourceNote = "note"
|
||||||
|
resourcePoll = "poll"
|
||||||
resourceStatus = "status"
|
resourceStatus = "status"
|
||||||
resourceStar = "star"
|
resourceStar = "star"
|
||||||
resourceStarred = "starred"
|
resourceStarred = "starred"
|
||||||
resourceTimeline = "timeline"
|
resourceTimeline = "timeline"
|
||||||
|
resourceVote = "vote"
|
||||||
)
|
)
|
||||||
|
|
|
@ -26,6 +26,7 @@ type ShowExecutor struct {
|
||||||
timelineCategory string
|
timelineCategory string
|
||||||
listID string
|
listID string
|
||||||
tag string
|
tag string
|
||||||
|
pollID string
|
||||||
limit int
|
limit int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,6 +46,7 @@ func NewShowExecutor(tlf TopLevelFlags, name, summary string) *ShowExecutor {
|
||||||
showExe.StringVar(&showExe.timelineCategory, flagTimelineCategory, model.TimelineCategoryHome, "Specify the timeline category to view")
|
showExe.StringVar(&showExe.timelineCategory, flagTimelineCategory, model.TimelineCategoryHome, "Specify the timeline category to view")
|
||||||
showExe.StringVar(&showExe.listID, flagListID, "", "Specify the ID of the list to display")
|
showExe.StringVar(&showExe.listID, flagListID, "", "Specify the ID of the list to display")
|
||||||
showExe.StringVar(&showExe.tag, flagTag, "", "Specify the name of the tag to use")
|
showExe.StringVar(&showExe.tag, flagTag, "", "Specify the name of the tag to use")
|
||||||
|
showExe.StringVar(&showExe.pollID, flagPollID, "", "Specify the ID of the poll to display")
|
||||||
showExe.IntVar(&showExe.limit, flagLimit, 20, "Specify the limit of items to display")
|
showExe.IntVar(&showExe.limit, flagLimit, 20, "Specify the limit of items to display")
|
||||||
|
|
||||||
showExe.Usage = commandUsageFunc(name, summary, showExe.FlagSet)
|
showExe.Usage = commandUsageFunc(name, summary, showExe.FlagSet)
|
||||||
|
@ -70,6 +72,7 @@ func (s *ShowExecutor) Execute() error {
|
||||||
resourceLiked: s.showLiked,
|
resourceLiked: s.showLiked,
|
||||||
resourceStarred: s.showLiked,
|
resourceStarred: s.showLiked,
|
||||||
resourceFollowRequest: s.showFollowRequests,
|
resourceFollowRequest: s.showFollowRequests,
|
||||||
|
resourcePoll: s.showPoll,
|
||||||
}
|
}
|
||||||
|
|
||||||
doFunc, ok := funcMap[s.resourceType]
|
doFunc, ok := funcMap[s.resourceType]
|
||||||
|
@ -362,3 +365,18 @@ func (s *ShowExecutor) showFollowRequests(gtsClient *client.Client) error {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *ShowExecutor) showPoll(gtsClient *client.Client) error {
|
||||||
|
if s.pollID == "" {
|
||||||
|
return FlagNotSetError{flagText: flagPollID}
|
||||||
|
}
|
||||||
|
|
||||||
|
poll, err := gtsClient.GetPoll(s.pollID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to retrieve the poll: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
utilities.Display(poll, *s.topLevelFlags.NoColor, s.topLevelFlags.Pager)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
119
internal/model/poll.go
Normal file
119
internal/model/poll.go
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"math"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"codeflow.dananglin.me.uk/apollo/enbas/internal/utilities"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Poll struct {
|
||||||
|
Emojis []Emoji `json:"emojis"`
|
||||||
|
Expired bool `json:"expired"`
|
||||||
|
Voted bool `json:"voted"`
|
||||||
|
Multiple bool `json:"multiple"`
|
||||||
|
ExpiredAt time.Time `json:"expires_at"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
OwnVotes []int `json:"own_votes"`
|
||||||
|
VotersCount int `json:"voters_count"`
|
||||||
|
VotesCount int `json:"votes_count"`
|
||||||
|
Options []PollOption `json:"options"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PollOption struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
VotesCount int `json:"votes_count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Poll) Display(noColor bool) string {
|
||||||
|
var builder strings.Builder
|
||||||
|
|
||||||
|
indent := " "
|
||||||
|
|
||||||
|
builder.WriteString(
|
||||||
|
utilities.HeaderFormat(noColor, "POLL ID:") +
|
||||||
|
"\n" + indent + p.ID +
|
||||||
|
"\n\n" + utilities.HeaderFormat(noColor, "OPTIONS:"),
|
||||||
|
)
|
||||||
|
|
||||||
|
displayPollContent(&builder, p, noColor, indent)
|
||||||
|
|
||||||
|
builder.WriteString(
|
||||||
|
"\n\n" +
|
||||||
|
utilities.HeaderFormat(noColor, "MULTIPLE CHOICES ALLOWED:") +
|
||||||
|
"\n" + indent + strconv.FormatBool(p.Multiple) +
|
||||||
|
"\n\n" +
|
||||||
|
utilities.HeaderFormat(noColor, "YOU VOTED:") +
|
||||||
|
"\n" + indent + strconv.FormatBool(p.Voted),
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(p.OwnVotes) > 0 {
|
||||||
|
builder.WriteString("\n\n" + utilities.HeaderFormat(noColor, "YOUR VOTES:"))
|
||||||
|
|
||||||
|
for _, vote := range p.OwnVotes {
|
||||||
|
builder.WriteString("\n" + indent + "[" + strconv.Itoa(vote) + "] " + p.Options[vote].Title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.WriteString(
|
||||||
|
"\n\n" +
|
||||||
|
utilities.HeaderFormat(noColor, "EXPIRED:") +
|
||||||
|
"\n" + indent + strconv.FormatBool(p.Expired),
|
||||||
|
)
|
||||||
|
|
||||||
|
return builder.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func displayPollContent(writer io.StringWriter, poll Poll, noColor bool, indent string) {
|
||||||
|
for ind, option := range poll.Options {
|
||||||
|
var percentage int
|
||||||
|
var calculate float64
|
||||||
|
|
||||||
|
if poll.VotesCount == 0 {
|
||||||
|
percentage = 0
|
||||||
|
} else {
|
||||||
|
calculate = float64(option.VotesCount) / float64(poll.VotesCount)
|
||||||
|
percentage = int(math.Floor(100 * calculate))
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.WriteString("\n\n" + indent + "[" + strconv.Itoa(ind) + "] " + option.Title)
|
||||||
|
drawPollMeter(writer, noColor, calculate, 80, indent)
|
||||||
|
|
||||||
|
writer.WriteString(
|
||||||
|
"\n" + indent + strconv.Itoa(option.VotesCount) + " votes " +
|
||||||
|
"(" + strconv.Itoa(percentage) + "%)",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.WriteString(
|
||||||
|
"\n\n" +
|
||||||
|
indent + utilities.FieldFormat(noColor, "Total votes:") + " " + strconv.Itoa(poll.VotesCount) +
|
||||||
|
"\n" + indent + utilities.FieldFormat(noColor, "Poll ID:") + " " + poll.ID +
|
||||||
|
"\n" + indent + utilities.FieldFormat(noColor, "Poll is open until:") + " " + utilities.FormatTime(poll.ExpiredAt),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func drawPollMeter(writer io.StringWriter, noColor bool, calculated float64, limit int, indent string) {
|
||||||
|
numVoteBlocks := int(math.Floor(float64(limit) * calculated))
|
||||||
|
numBackgroundBlocks := limit - numVoteBlocks
|
||||||
|
blockChar := "\u2501"
|
||||||
|
voteBlockColor := "\033[32;1m"
|
||||||
|
backgroundBlockColor := "\033[90m"
|
||||||
|
|
||||||
|
if noColor {
|
||||||
|
voteBlockColor = "\033[0m"
|
||||||
|
|
||||||
|
if numVoteBlocks == 0 {
|
||||||
|
numVoteBlocks = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.WriteString("\n" + indent + voteBlockColor + strings.Repeat(blockChar, numVoteBlocks) + "\033[0m")
|
||||||
|
|
||||||
|
if !noColor {
|
||||||
|
writer.WriteString(backgroundBlockColor + strings.Repeat(blockChar, numBackgroundBlocks) + "\033[0m")
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,7 +5,7 @@
|
||||||
package model
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@ type Status struct {
|
||||||
Mentions []Mention `json:"mentions"`
|
Mentions []Mention `json:"mentions"`
|
||||||
Muted bool `json:"muted"`
|
Muted bool `json:"muted"`
|
||||||
Pinned bool `json:"pinned"`
|
Pinned bool `json:"pinned"`
|
||||||
Poll Poll `json:"poll"`
|
Poll *Poll `json:"poll"`
|
||||||
Reblog *StatusReblogged `json:"reblog"`
|
Reblog *StatusReblogged `json:"reblog"`
|
||||||
Reblogged bool `json:"reblogged"`
|
Reblogged bool `json:"reblogged"`
|
||||||
ReblogsCount int `json:"reblogs_count"`
|
ReblogsCount int `json:"reblogs_count"`
|
||||||
|
@ -68,24 +68,6 @@ type Mention struct {
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Poll struct {
|
|
||||||
Emojis []Emoji `json:"emojis"`
|
|
||||||
Expired bool `json:"expired"`
|
|
||||||
Voted bool `json:"voted"`
|
|
||||||
Multiple bool `json:"multiple"`
|
|
||||||
ExpiredAt time.Time `json:"expires_at"`
|
|
||||||
ID string `json:"id"`
|
|
||||||
OwnVotes []int `json:"own_votes"`
|
|
||||||
VotersCount int `json:"voters_count"`
|
|
||||||
VotesCount int `json:"votes_count"`
|
|
||||||
Options []PollOption `json:"options"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PollOption struct {
|
|
||||||
Title string `json:"title"`
|
|
||||||
VotesCount int `json:"votes_count"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type StatusReblogged struct {
|
type StatusReblogged struct {
|
||||||
Account Account `json:"account"`
|
Account Account `json:"account"`
|
||||||
Application Application `json:"application"`
|
Application Application `json:"application"`
|
||||||
|
@ -158,47 +140,44 @@ type MediaDimensions struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s Status) Display(noColor bool) string {
|
func (s Status) Display(noColor bool) string {
|
||||||
format := `
|
indent := " "
|
||||||
%s
|
|
||||||
|
|
||||||
%s
|
var builder strings.Builder
|
||||||
%s
|
|
||||||
%s
|
|
||||||
%s
|
|
||||||
|
|
||||||
%s
|
// The account information
|
||||||
%s
|
builder.WriteString(utilities.FullDisplayNameFormat(noColor, s.Account.DisplayName, s.Account.Acct) + "\n\n")
|
||||||
|
|
||||||
%s
|
// The content of the status.
|
||||||
Boosts: %d
|
builder.WriteString(utilities.HeaderFormat(noColor, "CONTENT:"))
|
||||||
Likes: %d
|
builder.WriteString(utilities.WrapLines(utilities.ConvertHTMLToText(s.Content), "\n ", 80))
|
||||||
Replies: %d
|
|
||||||
|
|
||||||
%s
|
// If a poll exists in a status, write the contents to the builder.
|
||||||
%s
|
if s.Poll != nil {
|
||||||
|
displayPollContent(&builder, *s.Poll, noColor, indent)
|
||||||
|
}
|
||||||
|
|
||||||
%s
|
// The ID of the status
|
||||||
%s
|
builder.WriteString("\n\n" + utilities.HeaderFormat(noColor, "STATUS ID:") + "\n" + indent + s.ID)
|
||||||
`
|
|
||||||
|
|
||||||
return fmt.Sprintf(
|
// Status creation time
|
||||||
format,
|
builder.WriteString("\n\n" + utilities.HeaderFormat(noColor, "CREATED AT:") + "\n" + indent + utilities.FormatTime(s.CreatedAt))
|
||||||
utilities.FullDisplayNameFormat(noColor, s.Account.DisplayName, s.Account.Acct),
|
|
||||||
utilities.HeaderFormat(noColor, "CONTENT:"),
|
// Status stats
|
||||||
utilities.WrapLines(utilities.ConvertHTMLToText(s.Content), "\n ", 80),
|
builder.WriteString(
|
||||||
utilities.HeaderFormat(noColor, "STATUS ID:"),
|
"\n\n" +
|
||||||
s.ID,
|
utilities.HeaderFormat(noColor, "STATS:") +
|
||||||
utilities.HeaderFormat(noColor, "CREATED AT:"),
|
"\n" + indent + utilities.FieldFormat(noColor, "Boosts: ") + strconv.Itoa(s.ReblogsCount) +
|
||||||
utilities.FormatTime(s.CreatedAt),
|
"\n" + indent + utilities.FieldFormat(noColor, "Likes: ") + strconv.Itoa(s.FavouritesCount) +
|
||||||
utilities.HeaderFormat(noColor, "STATS:"),
|
"\n" + indent + utilities.FieldFormat(noColor, "Replies: ") + strconv.Itoa(s.RepliesCount),
|
||||||
s.ReblogsCount,
|
|
||||||
s.FavouritesCount,
|
|
||||||
s.RepliesCount,
|
|
||||||
utilities.HeaderFormat(noColor, "VISIBILITY:"),
|
|
||||||
s.Visibility,
|
|
||||||
utilities.HeaderFormat(noColor, "URL:"),
|
|
||||||
s.URL,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Status visibility
|
||||||
|
builder.WriteString("\n\n" + utilities.HeaderFormat(noColor, "VISIBILITY:") + "\n" + indent + s.Visibility.String())
|
||||||
|
|
||||||
|
// Status URL
|
||||||
|
builder.WriteString("\n\n" + utilities.HeaderFormat(noColor, "URL:") + "\n" + indent + s.URL)
|
||||||
|
|
||||||
|
return builder.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
type StatusList struct {
|
type StatusList struct {
|
||||||
|
@ -225,8 +204,19 @@ func (s StatusList) Display(noColor bool) string {
|
||||||
createdAt = status.Reblog.CreatedAt
|
createdAt = status.Reblog.CreatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
builder.WriteString(utilities.WrapLines(utilities.ConvertHTMLToText(status.Content), "\n", 80) + "\n\n")
|
builder.WriteString(utilities.WrapLines(utilities.ConvertHTMLToText(status.Content), "\n", 80))
|
||||||
builder.WriteString(utilities.FieldFormat(noColor, "ID:") + " " + statusID + "\t" + utilities.FieldFormat(noColor, "Created at:") + " " + utilities.FormatTime(createdAt) + "\n")
|
|
||||||
|
if status.Poll != nil {
|
||||||
|
displayPollContent(&builder, *status.Poll, noColor, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.WriteString(
|
||||||
|
"\n\n" +
|
||||||
|
utilities.FieldFormat(noColor, "Status ID:") + " " + statusID + "\t" +
|
||||||
|
utilities.FieldFormat(noColor, "Created at:") + " " + utilities.FormatTime(createdAt) +
|
||||||
|
"\n",
|
||||||
|
)
|
||||||
|
|
||||||
builder.WriteString(separator + "\n")
|
builder.WriteString(separator + "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue