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
This commit is contained in:
Dan Anglin 2024-08-14 11:18:38 +01:00
parent a0eab3b6ae
commit eb016b96e9
Signed by: dananglin
GPG key ID: 0C1D44CFBEE68638
13 changed files with 215 additions and 215 deletions

View file

@ -39,6 +39,7 @@
- [View a list of statuses that you've liked](#view-a-list-of-statuses-that-youve-liked)
- [Mute a status](#mute-a-status)
- [Unmute a status](#unmute-a-status)
- [Vote in a poll within a status](#vote-in-a-poll-within-a-status)
- [Polls](#polls)
- [Create a poll](#create-a-poll)
- [View a poll](#view-a-poll)
@ -528,6 +529,21 @@ _Not yet supported_
_Not yet supported_
### Vote in a poll within a status
Adds your vote(s) to a poll within a status.
```
enbas add --type vote --to status --status-id 01J55XVV2MM6MKQ7QHFBAVAE8R --vote 3
```
| flag | type | required | description | default |
|------|------|----------|-------------|---------|
| `type` | string | true | The resource you want to add.<br>Here this should be `vote`. | |
| `to` | string | true | The resource you want to add the vote to.<br>Here this should be `status`. | |
| `status-id` | string | true | The ID of the poll you want to add the votes to. | |
| `vote` | int | true | The ID of the option that you want to vote for.<br>You can use this flag multiple times to vote for more than one option if the poll allows multiple choices. | |
## Polls
### Create a poll
@ -536,31 +552,11 @@ See [Create a status](#create-a-status).
### View a poll
Prints the poll information to the screen.
```
enbas show --type poll --poll-id 01J0CEEZBZ6E6AYQSJPHCQYBDA
```
| flag | type | required | description | default |
|------|------|----------|-------------|---------|
| `type` | string | true | The resource you want to view.<br>Here this should be `poll`. | |
| `poll-id` | string | true | The ID of the poll that you want to view. | |
You can view a poll within a [status](#view-a-status) or within a [timeline](#view-a-timeline).
### Vote in a poll
Add your vote(s) to a poll.
```
enbas add --type vote --to poll --poll-id 01J1TVJ705VV3VP02FVVBSMX7E --vote 3
```
| flag | type | required | description | default |
|------|------|----------|-------------|---------|
| `type` | string | true | The resource you want to add.<br>Here this should be `vote`. | |
| `to` | string | true | The resource you want to add the vote to.<br>Here this should be `poll`. | |
| `poll-id` | string | true | The ID of the poll you want to add the votes to. | |
| `vote` | int | true | The ID of the option that you want to vote for.<br>You can use this flag multiple times to vote for more than one option if the poll allows multiple choices. | |
See [Vote in a poll within a status](#vote-in-a-poll-within-a-status)
## Lists

View file

@ -5,36 +5,34 @@ import (
"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
params := requestParameters{
httpMethod: http.MethodGet,
url: url,
requestBody: nil,
contentType: "",
output: &poll,
}
if err := g.sendRequest(params); 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) GetPoll(pollID string) (model.Poll, error) {
// url := g.Authentication.Instance + pollPath + "/" + pollID
//
// var poll model.Poll
//
// params := requestParameters{
// httpMethod: http.MethodGet,
// url: url,
// requestBody: nil,
// contentType: "",
// output: &poll,
// }
//
// if err := g.sendRequest(params); 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 {

View file

@ -1,7 +1,6 @@
package executor
import (
"errors"
"fmt"
"codeflow.dananglin.me.uk/apollo/enbas/internal/client"
@ -17,7 +16,6 @@ func (a *AddExecutor) Execute() error {
resourceAccount: a.addToAccount,
resourceBookmarks: a.addToBookmarks,
resourceStatus: a.addToStatus,
resourcePoll: a.addToPoll,
}
doFunc, ok := funcMap[a.toResourceType]
@ -164,6 +162,7 @@ func (a *AddExecutor) addToStatus(gtsClient *client.Client) error {
resourceStar: a.addStarToStatus,
resourceLike: a.addStarToStatus,
resourceBoost: a.addBoostToStatus,
resourceVote: a.addVoteToStatus,
}
doFunc, ok := funcMap[a.resourceType]
@ -197,45 +196,40 @@ 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 {
func (a *AddExecutor) addVoteToStatus(gtsClient *client.Client) error {
if a.votes.Empty() {
return errors.New("please use --" + flagVote + " to make a choice in this poll")
return NoVotesError{}
}
poll, err := gtsClient.GetPoll(a.pollID)
status, err := gtsClient.GetStatus(a.statusID)
if err != nil {
return fmt.Errorf("unable to retrieve the poll: %w", err)
return fmt.Errorf("unable to get the status: %w", err)
}
if poll.Expired {
if status.Poll == nil {
return NoPollInStatusError{}
}
if status.Poll.Expired {
return PollClosedError{}
}
if !poll.Multiple && !a.votes.ExpectedLength(1) {
if !status.Poll.Multiple && !a.votes.ExpectedLength(1) {
return MultipleChoiceError{}
}
if err := gtsClient.VoteInPoll(a.pollID, []int(a.votes)); err != nil {
myAccountID, err := getAccountID(gtsClient, true, nil)
if err != nil {
return fmt.Errorf("unable to get your account ID: %w", err)
}
if status.Account.ID == myAccountID {
return PollOwnerVoteError{}
}
pollID := status.Poll.ID
if err := gtsClient.VoteInPoll(pollID, []int(a.votes)); err != nil {
return fmt.Errorf("unable to add your vote(s) to the poll: %w", err)
}

View file

@ -149,8 +149,7 @@ func (c *CreateExecutor) createStatus(gtsClient *client.Client) error {
return fmt.Errorf("unable to create the status: %w", err)
}
c.printer.PrintSuccess("Successfully created the following status:")
c.printer.PrintStatus(status)
c.printer.PrintSuccess("Successfully created the status with ID: " + status.ID)
return nil
}

View file

@ -104,6 +104,24 @@ func (e NoPollOptionError) Error() string {
" flag to add options to the poll"
}
type NoVotesError struct{}
func (e NoVotesError) Error() string {
return "no votes were made, please add your vote(s) using the --vote flag"
}
type NoPollInStatusError struct{}
func (e NoPollInStatusError) Error() string {
return "this status does not have a poll"
}
type PollOwnerVoteError struct{}
func (e PollOwnerVoteError) Error() string {
return "you cannot vote in your own poll"
}
type NotFollowingError struct {
Account string
}

View file

@ -56,7 +56,6 @@ type AddExecutor struct {
accountNames internalFlag.StringSliceValue
content string
listID string
pollID string
statusID string
toResourceType string
resourceType string
@ -80,7 +79,6 @@ func NewAddExecutor(
exe.Var(&exe.accountNames, "account-name", "The name of the account")
exe.StringVar(&exe.content, "content", "", "The content of the created resource")
exe.StringVar(&exe.listID, "list-id", "", "The ID of the list in question")
exe.StringVar(&exe.pollID, "poll-id", "", "The ID of the poll")
exe.StringVar(&exe.statusID, "status-id", "", "The ID of the status")
exe.StringVar(&exe.toResourceType, "to", "", "The resource type to action the target resource to (e.g. status)")
exe.StringVar(&exe.resourceType, "type", "", "The type of resource you want to action on (e.g. account, status)")
@ -434,7 +432,6 @@ type ShowExecutor struct {
onlyMedia bool
onlyPinned bool
onlyPublic bool
pollID string
showUserPreferences bool
showStatuses bool
skipAccountRelationship bool
@ -472,7 +469,6 @@ func NewShowExecutor(
exe.BoolVar(&exe.onlyMedia, "only-media", false, "Set to true to show only the statuses with media attachments")
exe.BoolVar(&exe.onlyPinned, "only-pinned", false, "Set to true to show only the account's pinned statuses")
exe.BoolVar(&exe.onlyPublic, "only-public", false, "Set to true to show only the account's public posts")
exe.StringVar(&exe.pollID, "poll-id", "", "The ID of the poll")
exe.BoolVar(&exe.showUserPreferences, "show-preferences", false, "Set to true to view your posting preferences when viewing your account information")
exe.BoolVar(&exe.showStatuses, "show-statuses", false, "Set to true to view the statuses created from the account you are viewing")
exe.BoolVar(&exe.skipAccountRelationship, "skip-relationship", false, "Set to true to skip showing your relationship to the account that you are viewing")

View file

@ -8,11 +8,9 @@ const (
flagInstance = "instance"
flagListID = "list-id"
flagListTitle = "list-title"
flagPollID = "poll-id"
flagPollOption = "poll-option"
flagStatusID = "status-id"
flagTag = "tag"
flagTo = "to"
flagType = "type"
flagVote = "vote"
)

View file

@ -28,7 +28,6 @@ func (s *ShowExecutor) Execute() error {
resourceLiked: s.showLiked,
resourceStarred: s.showLiked,
resourceFollowRequest: s.showFollowRequests,
resourcePoll: s.showPoll,
resourceMutedAccounts: s.showMutedAccounts,
resourceMedia: s.showMedia,
resourceMediaAttachment: s.showMediaAttachment,
@ -76,6 +75,7 @@ func (s *ShowExecutor) showAccount(gtsClient *client.Client) error {
relationship *model.AccountRelationship
preferences *model.Preferences
statuses *model.StatusList
myAccountID string
)
if !s.myAccount && !s.skipAccountRelationship {
@ -85,10 +85,13 @@ func (s *ShowExecutor) showAccount(gtsClient *client.Client) error {
}
}
if s.myAccount && s.showUserPreferences {
preferences, err = gtsClient.GetUserPreferences()
if err != nil {
return fmt.Errorf("unable to retrieve the user preferences: %w", err)
if s.myAccount {
myAccountID = account.ID
if s.showUserPreferences {
preferences, err = gtsClient.GetUserPreferences()
if err != nil {
return fmt.Errorf("unable to retrieve the user preferences: %w", err)
}
}
}
@ -109,7 +112,7 @@ func (s *ShowExecutor) showAccount(gtsClient *client.Client) error {
}
}
s.printer.PrintAccount(account, relationship, preferences, statuses)
s.printer.PrintAccount(account, relationship, preferences, statuses, myAccountID)
return nil
}
@ -132,7 +135,12 @@ func (s *ShowExecutor) showStatus(gtsClient *client.Client) error {
return nil
}
s.printer.PrintStatus(status)
myAccountID, err := getAccountID(gtsClient, true, nil)
if err != nil {
return fmt.Errorf("unable to get your account ID: %w", err)
}
s.printer.PrintStatus(status, myAccountID)
return nil
}
@ -181,7 +189,12 @@ func (s *ShowExecutor) showTimeline(gtsClient *client.Client) error {
return nil
}
s.printer.PrintStatusList(timeline)
myAccountID, err := getAccountID(gtsClient, true, nil)
if err != nil {
return fmt.Errorf("unable to get your account ID: %w", err)
}
s.printer.PrintStatusList(timeline, myAccountID)
return nil
}
@ -334,7 +347,12 @@ func (s *ShowExecutor) showBookmarks(gtsClient *client.Client) error {
}
if len(bookmarks.Statuses) > 0 {
s.printer.PrintStatusList(bookmarks)
myAccountID, err := getAccountID(gtsClient, true, nil)
if err != nil {
return fmt.Errorf("unable to get your account ID: %w", err)
}
s.printer.PrintStatusList(bookmarks, myAccountID)
} else {
s.printer.PrintInfo("You have no bookmarks.\n")
}
@ -349,7 +367,12 @@ func (s *ShowExecutor) showLiked(gtsClient *client.Client) error {
}
if len(liked.Statuses) > 0 {
s.printer.PrintStatusList(liked)
myAccountID, err := getAccountID(gtsClient, true, nil)
if err != nil {
return fmt.Errorf("unable to get your account ID: %w", err)
}
s.printer.PrintStatusList(liked, myAccountID)
} else {
s.printer.PrintInfo("You have no " + s.resourceType + " statuses.\n")
}
@ -372,21 +395,6 @@ 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)
}
s.printer.PrintPoll(poll)
return nil
}
func (s *ShowExecutor) showMutedAccounts(gtsClient *client.Client) error {
muted, err := gtsClient.GetMutedAccounts(s.limit)
if err != nil {

View file

@ -12,6 +12,7 @@ func (p Printer) PrintAccount(
relationship *model.AccountRelationship,
preferences *model.Preferences,
statuses *model.StatusList,
userAccountID string,
) {
var builder strings.Builder
@ -47,7 +48,7 @@ func (p Printer) PrintAccount(
}
if statuses != nil {
builder.WriteString("\n\n" + p.statusList(*statuses))
builder.WriteString("\n\n" + p.statusList(*statuses, userAccountID))
}
builder.WriteString("\n\n")

View file

@ -1,91 +0,0 @@
package printer
import (
"math"
"strconv"
"strings"
"codeflow.dananglin.me.uk/apollo/enbas/internal/model"
)
func (p Printer) PrintPoll(poll model.Poll) {
var builder strings.Builder
builder.WriteString("\n" + p.headerFormat("POLL ID:"))
builder.WriteString("\n" + poll.ID)
builder.WriteString("\n\n" + p.headerFormat("OPTIONS:"))
builder.WriteString(p.pollOptions(poll))
builder.WriteString("\n\n" + p.headerFormat("MULTIPLE CHOICES ALLOWED:"))
builder.WriteString("\n" + strconv.FormatBool(poll.Multiple))
builder.WriteString("\n\n" + p.headerFormat("YOU VOTED:"))
builder.WriteString("\n" + strconv.FormatBool(poll.Voted))
if len(poll.OwnVotes) > 0 {
builder.WriteString("\n\n" + p.headerFormat("YOUR VOTES:"))
for _, vote := range poll.OwnVotes {
builder.WriteString("\n" + "[" + strconv.Itoa(vote) + "] " + poll.Options[vote].Title)
}
}
builder.WriteString("\n\n" + p.headerFormat("EXPIRED:"))
builder.WriteString("\n" + strconv.FormatBool(poll.Expired))
builder.WriteString("\n\n")
p.print(builder.String())
}
func (p Printer) pollOptions(poll model.Poll) string {
var builder strings.Builder
for ind, option := range poll.Options {
var (
votage float64
percentage int
)
if poll.VotesCount == 0 {
percentage = 0
} else {
votage = float64(option.VotesCount) / float64(poll.VotesCount)
percentage = int(math.Floor(100 * votage))
}
builder.WriteString("\n\n" + "[" + strconv.Itoa(ind) + "] " + option.Title)
builder.WriteString(p.pollMeter(votage))
builder.WriteString("\n" + strconv.Itoa(option.VotesCount) + " votes " + "(" + strconv.Itoa(percentage) + "%)")
}
builder.WriteString("\n\n" + p.fieldFormat("Total votes:") + " " + strconv.Itoa(poll.VotesCount))
builder.WriteString("\n" + p.fieldFormat("Poll ID:") + " " + poll.ID)
builder.WriteString("\n" + p.fieldFormat("Poll is open until:") + " " + p.formatDateTime(poll.ExpiredAt))
return builder.String()
}
func (p Printer) pollMeter(votage float64) string {
numVoteBlocks := int(math.Floor(float64(p.lineWrapCharacterLimit) * votage))
numBackgroundBlocks := p.lineWrapCharacterLimit - numVoteBlocks
voteBlockColor := p.theme.boldgreen
backgroundBlockColor := p.theme.grey
if p.noColor {
voteBlockColor = p.theme.reset
if numVoteBlocks == 0 {
numVoteBlocks = 1
}
}
meter := "\n" + voteBlockColor + strings.Repeat(symbolPollMeter, numVoteBlocks) + p.theme.reset
if !p.noColor {
meter += backgroundBlockColor + strings.Repeat(symbolPollMeter, numBackgroundBlocks) + p.theme.reset
}
return meter
}

View file

@ -13,7 +13,7 @@ const (
noMediaDescription = "This media attachment has no description."
symbolBullet = "\u2022"
symbolPollMeter = "\u2501"
symbolSuccess = "\u2714"
symbolCheckMark = "\u2714"
symbolFailure = "\u2717"
symbolImage = "\uf03e"
symbolLiked = "\uf51f"
@ -78,9 +78,9 @@ func NewPrinter(
}
func (p Printer) PrintSuccess(text string) {
success := p.theme.boldgreen + symbolSuccess + p.theme.reset
success := p.theme.boldgreen + symbolCheckMark + p.theme.reset
if p.noColor {
success = symbolSuccess
success = symbolCheckMark
}
printToStdout(success + " " + text + "\n")

View file

@ -1,13 +1,14 @@
package printer
import (
"math"
"strconv"
"strings"
"codeflow.dananglin.me.uk/apollo/enbas/internal/model"
)
func (p Printer) PrintStatus(status model.Status) {
func (p Printer) PrintStatus(status model.Status, userAccountID string) {
var builder strings.Builder
// The account information
@ -41,7 +42,13 @@ func (p Printer) PrintStatus(status model.Status) {
// If a poll exists in a status, write the contents to the builder.
if status.Poll != nil {
builder.WriteString(p.pollOptions(*status.Poll))
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
@ -72,17 +79,18 @@ func (p Printer) PrintStatus(status model.Status) {
p.print(builder.String())
}
func (p Printer) PrintStatusList(list model.StatusList) {
p.print(p.statusList(list))
func (p Printer) PrintStatusList(list model.StatusList, userAccountID string) {
p.print(p.statusList(list, userAccountID))
}
func (p Printer) statusList(list model.StatusList) string {
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
@ -100,6 +108,7 @@ func (p Printer) statusList(list model.StatusList) string {
))
statusID = status.Reblog.ID
statusOwnerID = status.Reblog.Account.ID
createdAt = p.formatDateTime(status.Reblog.CreatedAt)
boostedAt = p.formatDateTime(status.CreatedAt)
content = status.Reblog.Content
@ -120,7 +129,12 @@ func (p Printer) statusList(list model.StatusList) string {
builder.WriteString("\n" + p.convertHTMLToText(content, true))
if poll != nil {
builder.WriteString(p.pollOptions(*poll))
pollOwner := false
if statusOwnerID == userAccountID {
pollOwner = true
}
builder.WriteString(p.pollDetails(*poll, pollOwner))
}
for _, media := range mediaAttachments {
@ -172,3 +186,78 @@ func (p Printer) statusList(list model.StatusList) string {
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
}

View file

@ -136,10 +136,6 @@
"type": "bool",
"description": "Set to true to hide the vote count until the poll is closed"
},
"poll-id": {
"type": "string",
"description": "The ID of the poll"
},
"poll-option": {
"type": "StringSliceValue",
"description": "A poll option. Use this multiple times to set multiple options"
@ -215,7 +211,6 @@
{ "flag": "account-name", "fieldName": "accountNames" },
{ "flag": "content", "default": "" },
{ "flag": "list-id", "fieldName": "listID", "default": "" },
{ "flag": "poll-id", "fieldName": "pollID", "default": "" },
{ "flag": "status-id", "fieldName": "statusID", "default": "" },
{ "flag": "to", "fieldName": "toResourceType", "default": "" },
{ "flag": "type", "fieldName": "resourceType", "default": "" },
@ -367,7 +362,6 @@
{ "flag": "only-media", "default": "false" },
{ "flag": "only-pinned", "default": "false" },
{ "flag": "only-public", "default": "false" },
{ "flag": "poll-id", "fieldName": "pollID", "default": "" },
{ "flag": "show-preferences", "fieldName": "showUserPreferences", "default": "false" },
{ "flag": "show-statuses", "default": "false" },
{ "flag": "skip-relationship", "fieldName": "skipAccountRelationship", "default": "false" },