feat: add support for polls

Add support for creating, viewing and voting in
polls.
This commit is contained in:
Dan Anglin 2024-06-15 18:40:13 +01:00
parent ac2e74cac3
commit 792a93d736
Signed by: dananglin
GPG key ID: 0C1D44CFBEE68638
11 changed files with 483 additions and 126 deletions

55
internal/client/poll.go Normal file
View 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
}

View file

@ -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 {

View file

@ -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
}

View file

@ -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)

View file

@ -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"
}

View file

@ -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
}

View file

@ -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,
} }

View file

@ -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"
) )

View file

@ -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
View 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")
}
}

View file

@ -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")
} }