diff --git a/internal/client/poll.go b/internal/client/poll.go new file mode 100644 index 0000000..7e55253 --- /dev/null +++ b/internal/client/poll.go @@ -0,0 +1,55 @@ +// SPDX-FileCopyrightText: 2024 Dan Anglin +// +// 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 +} 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 99946d7..25350dc 100644 --- a/internal/executor/add.go +++ b/internal/executor/add.go @@ -5,6 +5,7 @@ package executor import ( + "errors" "flag" "fmt" @@ -19,7 +20,9 @@ type AddExecutor struct { toResourceType string listID string statusID string - accountNames AccountNames + pollID string + choices MultiIntFlagValue + accountNames MultiStringFlagValue content string } @@ -28,16 +31,18 @@ func NewAddExecutor(tlf TopLevelFlags, name, summary string) *AddExecutor { addExe := AddExecutor{ FlagSet: flag.NewFlagSet(name, flag.ExitOnError), - accountNames: AccountNames(emptyArr), + accountNames: MultiStringFlagValue(emptyArr), topLevelFlags: tlf, } 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.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.Var(&addExe.accountNames, flagAccountName, "The name of the account") 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) @@ -54,6 +59,7 @@ func (a *AddExecutor) Execute() error { resourceAccount: a.addToAccount, resourceBookmarks: a.addToBookmarks, resourceStatus: a.addToStatus, + resourcePoll: a.addToPoll, } doFunc, ok := funcMap[a.toResourceType] @@ -227,3 +233,50 @@ func (a *AddExecutor) addBoostToStatus(gtsClient *client.Client) error { 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 +} diff --git a/internal/executor/create.go b/internal/executor/create.go index d05814c..910508d 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 + pollAllowsMultipleChoices bool + pollHidesVoteCounts 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.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.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.pollAllowsMultipleChoices, + HideTotals: c.pollHidesVoteCounts, + 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 c7dd944..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 { @@ -66,3 +74,23 @@ type UnknownCommandError struct { func (e UnknownCommandError) Error() string { 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" +} diff --git a/internal/executor/flags.go b/internal/executor/flags.go index 90ebab3..13227db 100644 --- a/internal/executor/flags.go +++ b/internal/executor/flags.go @@ -4,57 +4,115 @@ package executor -import "strings" - -const ( - flagAccountName = "account-name" - flagBrowser = "browser" - 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" +import ( + "fmt" + "strconv" + "strings" + "time" ) -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 -} +const ( + 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 TopLevelFlags struct { ConfigDir string NoColor *bool 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 +} 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, } diff --git a/internal/executor/resources.go b/internal/executor/resources.go index 10fce42..a634113 100644 --- a/internal/executor/resources.go +++ b/internal/executor/resources.go @@ -4,7 +4,7 @@ package executor -const( +const ( resourceAccount = "account" resourceBlocked = "blocked" resourceBookmarks = "bookmarks" @@ -17,8 +17,10 @@ const( resourceLiked = "liked" resourceList = "list" resourceNote = "note" + resourcePoll = "poll" resourceStatus = "status" resourceStar = "star" resourceStarred = "starred" resourceTimeline = "timeline" + resourceVote = "vote" ) diff --git a/internal/executor/show.go b/internal/executor/show.go index b4a5660..e9b90d3 100644 --- a/internal/executor/show.go +++ b/internal/executor/show.go @@ -26,6 +26,7 @@ type ShowExecutor struct { timelineCategory string listID string tag string + pollID string 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.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.pollID, flagPollID, "", "Specify the ID of the poll to display") showExe.IntVar(&showExe.limit, flagLimit, 20, "Specify the limit of items to display") showExe.Usage = commandUsageFunc(name, summary, showExe.FlagSet) @@ -70,6 +72,7 @@ func (s *ShowExecutor) Execute() error { resourceLiked: s.showLiked, resourceStarred: s.showLiked, resourceFollowRequest: s.showFollowRequests, + resourcePoll: s.showPoll, } doFunc, ok := funcMap[s.resourceType] @@ -362,3 +365,18 @@ func (s *ShowExecutor) showFollowRequests(gtsClient *client.Client) error { 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 +} diff --git a/internal/model/poll.go b/internal/model/poll.go new file mode 100644 index 0000000..b1c9bec --- /dev/null +++ b/internal/model/poll.go @@ -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") + } +} diff --git a/internal/model/status.go b/internal/model/status.go index 09befcd..d150765 100644 --- a/internal/model/status.go +++ b/internal/model/status.go @@ -5,7 +5,7 @@ package model import ( - "fmt" + "strconv" "strings" "time" @@ -30,7 +30,7 @@ type Status struct { Mentions []Mention `json:"mentions"` Muted bool `json:"muted"` Pinned bool `json:"pinned"` - Poll Poll `json:"poll"` + Poll *Poll `json:"poll"` Reblog *StatusReblogged `json:"reblog"` Reblogged bool `json:"reblogged"` ReblogsCount int `json:"reblogs_count"` @@ -68,24 +68,6 @@ type Mention struct { 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 { Account Account `json:"account"` Application Application `json:"application"` @@ -158,47 +140,44 @@ type MediaDimensions struct { } func (s Status) Display(noColor bool) string { - format := ` -%s + indent := " " -%s - %s -%s - %s + var builder strings.Builder -%s - %s + // The account information + builder.WriteString(utilities.FullDisplayNameFormat(noColor, s.Account.DisplayName, s.Account.Acct) + "\n\n") -%s - Boosts: %d - Likes: %d - Replies: %d + // The content of the status. + builder.WriteString(utilities.HeaderFormat(noColor, "CONTENT:")) + builder.WriteString(utilities.WrapLines(utilities.ConvertHTMLToText(s.Content), "\n ", 80)) -%s - %s + // If a poll exists in a status, write the contents to the builder. + if s.Poll != nil { + displayPollContent(&builder, *s.Poll, noColor, indent) + } -%s - %s -` + // The ID of the status + builder.WriteString("\n\n" + utilities.HeaderFormat(noColor, "STATUS ID:") + "\n" + indent + s.ID) - return fmt.Sprintf( - format, - utilities.FullDisplayNameFormat(noColor, s.Account.DisplayName, s.Account.Acct), - utilities.HeaderFormat(noColor, "CONTENT:"), - utilities.WrapLines(utilities.ConvertHTMLToText(s.Content), "\n ", 80), - utilities.HeaderFormat(noColor, "STATUS ID:"), - s.ID, - utilities.HeaderFormat(noColor, "CREATED AT:"), - utilities.FormatTime(s.CreatedAt), - utilities.HeaderFormat(noColor, "STATS:"), - s.ReblogsCount, - s.FavouritesCount, - s.RepliesCount, - utilities.HeaderFormat(noColor, "VISIBILITY:"), - s.Visibility, - utilities.HeaderFormat(noColor, "URL:"), - s.URL, + // Status creation time + builder.WriteString("\n\n" + utilities.HeaderFormat(noColor, "CREATED AT:") + "\n" + indent + utilities.FormatTime(s.CreatedAt)) + + // Status stats + builder.WriteString( + "\n\n" + + utilities.HeaderFormat(noColor, "STATS:") + + "\n" + indent + utilities.FieldFormat(noColor, "Boosts: ") + strconv.Itoa(s.ReblogsCount) + + "\n" + indent + utilities.FieldFormat(noColor, "Likes: ") + strconv.Itoa(s.FavouritesCount) + + "\n" + indent + utilities.FieldFormat(noColor, "Replies: ") + strconv.Itoa(s.RepliesCount), ) + + // 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 { @@ -225,8 +204,19 @@ func (s StatusList) Display(noColor bool) string { createdAt = status.Reblog.CreatedAt } - builder.WriteString(utilities.WrapLines(utilities.ConvertHTMLToText(status.Content), "\n", 80) + "\n\n") - builder.WriteString(utilities.FieldFormat(noColor, "ID:") + " " + statusID + "\t" + utilities.FieldFormat(noColor, "Created at:") + " " + utilities.FormatTime(createdAt) + "\n") + builder.WriteString(utilities.WrapLines(utilities.ConvertHTMLToText(status.Content), "\n", 80)) + + 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") }