enbas/internal/printer/status.go
Dan Anglin eb016b96e9
fix(BREAKING): update poll interaction
Summary:

This commit updates and enhances poll interaction. From now on users
will interact with a poll via the status that contains it. Direct
interaction with the poll (via the poll's ID) is no longer supported.
This helps resolve an issue where it wasn't possible to find the owner
of the poll when interacting with it directly.

Changes:

- Users can no longer view a poll directly using the Poll ID.
  Instead polls can be viewed when viewing statuses or timelines.
- More details about a poll is shown in statuses and timelines.
- Votes are now added to polls via statuses.
- Poll results are hidden unless the following conditions are met.
    - The user is the owner of the poll.
    - The poll has expired.
    - The user has already voted in the poll.
- Enbas can now detect and stop a poll owner from voting in their own
  poll.
- When a status is created Enbas will now only print the ID of the
  created status instead of the whole thing.

PR: apollo/enbas#43

Resolves apollo/enbas#39
2024-08-14 11:29:30 +01:00

263 lines
8 KiB
Go

package printer
import (
"math"
"strconv"
"strings"
"codeflow.dananglin.me.uk/apollo/enbas/internal/model"
)
func (p Printer) PrintStatus(status model.Status, userAccountID string) {
var builder strings.Builder
// The account information
builder.WriteString("\n" + p.fullDisplayNameFormat(status.Account.DisplayName, status.Account.Acct))
// The ID of the status
builder.WriteString("\n\n" + p.headerFormat("STATUS ID:"))
builder.WriteString("\n" + status.ID)
// The content of the status.
builder.WriteString("\n\n" + p.headerFormat("CONTENT:"))
builder.WriteString(p.convertHTMLToText(status.Content, true))
// Details of media attachments (if any).
if len(status.MediaAttachments) > 0 {
builder.WriteString("\n\n" + p.headerFormat("MEDIA ATTACHMENTS:"))
for ind, media := range status.MediaAttachments {
builder.WriteString("\n\n[" + strconv.Itoa(ind) + "] " + p.fieldFormat("ID:") + " " + media.ID)
builder.WriteString("\n " + p.fieldFormat("Type:") + " " + media.Type)
description := media.Description
if description == "" {
description = noMediaDescription
}
builder.WriteString("\n " + p.fieldFormat("Description:") + " " + description)
builder.WriteString("\n " + p.fieldFormat("Media URL:") + " " + media.URL)
}
}
// If a poll exists in a status, write the contents to the builder.
if status.Poll != nil {
pollOwner := false
if status.Account.ID == userAccountID {
pollOwner = true
}
builder.WriteString("\n\n" + p.headerFormat("POLL DETAILS:"))
builder.WriteString(p.pollDetails(*status.Poll, pollOwner))
}
// Status creation time
builder.WriteString("\n\n" + p.headerFormat("CREATED AT:"))
builder.WriteString("\n" + p.formatDateTime(status.CreatedAt))
// Status stats
builder.WriteString("\n\n" + p.headerFormat("STATS:"))
builder.WriteString("\n" + p.fieldFormat("Boosts: ") + strconv.Itoa(status.ReblogsCount))
builder.WriteString("\n" + p.fieldFormat("Likes: ") + strconv.Itoa(status.FavouritesCount))
builder.WriteString("\n" + p.fieldFormat("Replies: ") + strconv.Itoa(status.RepliesCount))
// The user's actions on the status
builder.WriteString("\n\n" + p.headerFormat("YOUR ACTIONS:"))
builder.WriteString("\n" + p.fieldFormat("Boosted: ") + strconv.FormatBool(status.Reblogged))
builder.WriteString("\n" + p.fieldFormat("Liked: ") + strconv.FormatBool(status.Favourited))
builder.WriteString("\n" + p.fieldFormat("Bookmarked: ") + strconv.FormatBool(status.Bookmarked))
// Status visibility
builder.WriteString("\n\n" + p.headerFormat("VISIBILITY:"))
builder.WriteString("\n" + status.Visibility.String())
// Status URL
builder.WriteString("\n\n" + p.headerFormat("URL:"))
builder.WriteString("\n" + status.URL)
builder.WriteString("\n\n")
p.print(builder.String())
}
func (p Printer) PrintStatusList(list model.StatusList, userAccountID string) {
p.print(p.statusList(list, userAccountID))
}
func (p Printer) statusList(list model.StatusList, userAccountID string) string {
var builder strings.Builder
builder.WriteString(p.headerFormat(list.Name) + "\n")
for _, status := range list.Statuses {
statusID := status.ID
statusOwnerID := status.Account.ID
createdAt := p.formatDateTime(status.CreatedAt)
boostedAt := ""
content := status.Content
poll := status.Poll
mediaAttachments := status.MediaAttachments
switch {
case status.Reblog != nil:
builder.WriteString("\n" + p.wrapLines(
p.fullDisplayNameFormat(status.Account.DisplayName, status.Account.Acct)+
" boosted this status from "+
p.fullDisplayNameFormat(status.Reblog.Account.DisplayName, status.Reblog.Account.Acct)+
":",
0,
))
statusID = status.Reblog.ID
statusOwnerID = status.Reblog.Account.ID
createdAt = p.formatDateTime(status.Reblog.CreatedAt)
boostedAt = p.formatDateTime(status.CreatedAt)
content = status.Reblog.Content
poll = status.Reblog.Poll
mediaAttachments = status.Reblog.MediaAttachments
case status.InReplyToID != "":
builder.WriteString("\n" + p.wrapLines(
p.fullDisplayNameFormat(status.Account.DisplayName, status.Account.Acct)+
" posted in reply to "+
status.InReplyToID+
":",
0,
))
default:
builder.WriteString("\n" + p.fullDisplayNameFormat(status.Account.DisplayName, status.Account.Acct) + " posted:")
}
builder.WriteString("\n" + p.convertHTMLToText(content, true))
if poll != nil {
pollOwner := false
if statusOwnerID == userAccountID {
pollOwner = true
}
builder.WriteString(p.pollDetails(*poll, pollOwner))
}
for _, media := range mediaAttachments {
builder.WriteString("\n\n" + symbolImage + " " + p.fieldFormat("Media attachment: ") + media.ID)
builder.WriteString("\n " + p.fieldFormat("Media type: ") + media.Type + "\n")
description := " " + p.fieldFormat("Description: ")
if media.Description == "" {
description += noMediaDescription
} else {
description += media.Description
}
builder.WriteString(p.wrapLines(description, 2))
}
boosted := symbolBoosted
if status.Reblogged {
boosted = p.theme.boldyellow + symbolBoosted + p.theme.reset
}
liked := symbolNotLiked
if status.Favourited {
liked = p.theme.boldyellow + symbolLiked + p.theme.reset
}
bookmarked := symbolNotBookmarked
if status.Bookmarked {
bookmarked = p.theme.boldyellow + symbolBookmarked + p.theme.reset
}
builder.WriteString("\n\n" + boosted + " " + p.fieldFormat("boosted: ") + strconv.FormatBool(status.Reblogged))
builder.WriteString("\n" + liked + " " + p.fieldFormat("liked: ") + strconv.FormatBool(status.Favourited))
builder.WriteString("\n" + bookmarked + " " + p.fieldFormat("bookmarked: ") + strconv.FormatBool(status.Bookmarked))
builder.WriteString(
"\n\n" +
p.fieldFormat("Status ID: ") + statusID +
"\n" + p.fieldFormat("Created at: ") + createdAt,
)
if boostedAt != "" {
builder.WriteString("\n" + p.fieldFormat("Boosted at: ") + boostedAt)
}
builder.WriteString("\n" + p.statusSeparator + "\n")
}
return builder.String()
}
func (p Printer) pollDetails(poll model.Poll, owner bool) string {
var builder strings.Builder
for ind, option := range poll.Options {
var (
votage float64
percentage int
)
// Show the poll results under any of the following conditions:
// - the user is the owner of the poll
// - the poll has expired
// - the user has voted in the poll
if owner || poll.Expired || poll.Voted {
if poll.VotesCount == 0 {
percentage = 0
} else {
votage = float64(option.VotesCount) / float64(poll.VotesCount)
percentage = int(math.Floor(100 * votage))
}
optionTitle := "\n\n" + "[" + strconv.Itoa(ind) + "] " + option.Title
for _, vote := range poll.OwnVotes {
if ind == vote {
optionTitle += " " + symbolCheckMark
break
}
}
builder.WriteString(optionTitle)
builder.WriteString(p.pollMeter(votage))
builder.WriteString("\n" + strconv.Itoa(option.VotesCount) + " votes " + "(" + strconv.Itoa(percentage) + "%)")
} else {
builder.WriteString("\n" + "[" + strconv.Itoa(ind) + "] " + option.Title)
}
}
pollStatusField := "Poll is open until: "
if poll.Expired {
pollStatusField = "Poll was closed on: "
}
builder.WriteString("\n\n" + p.fieldFormat(pollStatusField) + p.formatDateTime(poll.ExpiredAt))
builder.WriteString("\n" + p.fieldFormat("Total votes: ") + strconv.Itoa(poll.VotesCount))
builder.WriteString("\n" + p.fieldFormat("Multiple choices allowed: ") + strconv.FormatBool(poll.Multiple))
return builder.String()
}
func (p Printer) pollMeter(votage float64) string {
numVoteBlocks := int(math.Floor(float64(p.lineWrapCharacterLimit) * votage))
numBackgroundBlocks := p.lineWrapCharacterLimit - numVoteBlocks
voteBlockColour := p.theme.boldgreen
backgroundBlockColor := p.theme.grey
if p.noColor {
voteBlockColour = p.theme.reset
if numVoteBlocks == 0 {
numVoteBlocks = 1
}
}
meter := "\n" + voteBlockColour + strings.Repeat(symbolPollMeter, numVoteBlocks) + p.theme.reset
if !p.noColor {
meter += backgroundBlockColor + strings.Repeat(symbolPollMeter, numBackgroundBlocks) + p.theme.reset
}
return meter
}