cmd/accounts.go
author rjp <zimpenfish@gmail.com>
Mon, 23 Jan 2023 16:39:02 +0000
changeset 267 5b91a65ba95a
parent 239 605a00e9d1ab
child 268 4dd196a4ee7c
permissions -rw-r--r--
Update to handle non-int64 IDs Pleroma/Akkoma and GotoSocial use opaque IDs rather than `int64`s like Mastodon which means that `madon` can't talk to either of those. This commit updates everything that can be an ID to `madon.ActivityID` which is an alias for `string` - can't create a specific type for it since there's more than a few places where they're concatenated directly to strings for URLs, etc. Which means it could just as easily be a direct `string` type itself but I find that having distinct types can often make the code more readable and understandable. One extra bit is that `statusOpts` has grown a `_hasReplyTo` boolean to indicate whether the `--in-reply-to` flag was given or not because we can't distinguish because "empty because default" or "empty because given and empty". Another way around this would be to set the default to some theoretically impossible or unlikely string but you never know when someone might spin up an instance where, e.g., admin posts have negative integer IDs.

// Copyright © 2017-2018 Mikael Berthe <mikael@lilotux.net>
//
// Licensed under the MIT license.
// Please see the LICENSE file is this directory.

package cmd

import (
	"os"
	"strconv"
	"strings"

	"github.com/pkg/errors"
	"github.com/spf13/cobra"
	flag "github.com/spf13/pflag"

	"github.com/McKael/madon/v2"
)

var accountUpdateFlags, accountMuteFlags, accountFollowFlags *flag.FlagSet

var accountsOpts struct {
	accountID             madon.ActivityID
	accountUID            string
	unset                 bool             // TODO remove eventually?
	limit, keep           uint             // Limit the results
	sinceID, maxID        madon.ActivityID // Query boundaries
	all                   bool             // Try to fetch all results
	onlyMedia, onlyPinned bool             // For acccount statuses
	excludeReplies        bool             // For acccount statuses
	remoteUID             string           // For account follow
	reblogs               bool             // For account follow
	acceptFR, rejectFR    bool             // For account follow_requests
	list                  bool             // For account follow_requests/reports
	accountIDs            string           // For account relationships
	statusIDs             string           // For account reports
	comment               string           // For account reports
	displayName, note     string           // For account update
	profileFields         []string         // For account update
	avatar, header        string           // For account update
	defaultLanguage       string           // For account update
	defaultPrivacy        string           // For account update
	defaultSensitive      bool             // For account update
	locked, bot           bool             // For account update
	muteNotifications     bool             // For account mute
	following             bool             // For account search
}

func init() {
	RootCmd.AddCommand(accountsCmd)

	// Subcommands
	accountsCmd.AddCommand(accountSubcommands...)

	// Global flags
	accountsCmd.PersistentFlags().StringVarP(&accountsOpts.accountID, "account-id", "a", "", "Account ID number")
	accountsCmd.PersistentFlags().StringVarP(&accountsOpts.accountUID, "user-id", "u", "", "Account user ID")
	accountsCmd.PersistentFlags().UintVarP(&accountsOpts.limit, "limit", "l", 0, "Limit number of API results")
	accountsCmd.PersistentFlags().UintVarP(&accountsOpts.keep, "keep", "k", 0, "Limit number of results")
	accountsCmd.PersistentFlags().StringVar(&accountsOpts.sinceID, "since-id", "", "Request IDs greater than a value")
	accountsCmd.PersistentFlags().StringVar(&accountsOpts.maxID, "max-id", "", "Request IDs less (or equal) than a value")
	accountsCmd.PersistentFlags().BoolVar(&accountsOpts.all, "all", false, "Fetch all results")

	// Subcommand flags
	accountStatusesSubcommand.Flags().BoolVar(&accountsOpts.onlyPinned, "pinned", false, "Only statuses that have been pinned")
	accountStatusesSubcommand.Flags().BoolVar(&accountsOpts.onlyMedia, "only-media", false, "Only statuses with media attachments")
	accountStatusesSubcommand.Flags().BoolVar(&accountsOpts.excludeReplies, "exclude-replies", false, "Exclude replies to other statuses")

	accountFollowRequestsSubcommand.Flags().BoolVar(&accountsOpts.list, "list", false, "List pending follow requests")
	accountFollowRequestsSubcommand.Flags().BoolVar(&accountsOpts.acceptFR, "accept", false, "Accept the follow request from the account ID")
	accountFollowRequestsSubcommand.Flags().BoolVar(&accountsOpts.rejectFR, "reject", false, "Reject the follow request from the account ID")

	accountBlockSubcommand.Flags().BoolVarP(&accountsOpts.unset, "unset", "", false, "Unblock the account (deprecated)")

	accountMuteSubcommand.Flags().BoolVarP(&accountsOpts.unset, "unset", "", false, "Unmute the account (deprecated)")
	accountMuteSubcommand.Flags().BoolVarP(&accountsOpts.muteNotifications, "notifications", "", true, "Mute the notifications")
	accountFollowSubcommand.Flags().BoolVarP(&accountsOpts.unset, "unset", "", false, "Unfollow the account (deprecated)")
	accountFollowSubcommand.Flags().BoolVarP(&accountsOpts.reblogs, "show-reblogs", "", true, "Follow account's boosts")
	accountFollowSubcommand.Flags().StringVarP(&accountsOpts.remoteUID, "remote", "r", "", "Follow remote account (user@domain)")

	accountRelationshipsSubcommand.Flags().StringVar(&accountsOpts.accountIDs, "account-ids", "", "Comma-separated list of account IDs")

	accountReportsSubcommand.Flags().StringVar(&accountsOpts.statusIDs, "status-ids", "", "Comma-separated list of status IDs")
	accountReportsSubcommand.Flags().StringVar(&accountsOpts.comment, "comment", "", "Report comment")
	accountReportsSubcommand.Flags().BoolVar(&accountsOpts.list, "list", false, "List current user reports")

	accountSearchSubcommand.Flags().BoolVar(&accountsOpts.following, "following", false, "Restrict search to accounts you are following")

	accountUpdateSubcommand.Flags().StringVar(&accountsOpts.displayName, "display-name", "", "User display name")
	accountUpdateSubcommand.Flags().StringVar(&accountsOpts.note, "note", "", "User note (a.k.a. bio)")
	accountUpdateSubcommand.Flags().StringVar(&accountsOpts.avatar, "avatar", "", "User avatar image")
	accountUpdateSubcommand.Flags().StringVar(&accountsOpts.header, "header", "", "User header image")
	accountUpdateSubcommand.Flags().StringArrayVar(&accountsOpts.profileFields, "profile-field", nil, "Profile metadata field (NAME=VALUE)")
	accountUpdateSubcommand.Flags().StringVar(&accountsOpts.defaultLanguage, "default-language", "", "Default toots language (iso 639 code)")
	accountUpdateSubcommand.Flags().StringVar(&accountsOpts.defaultPrivacy, "default-privacy", "", "Default toot privacy (public, unlisted, private)")
	accountUpdateSubcommand.Flags().BoolVar(&accountsOpts.defaultSensitive, "default-sensitive", false, "Mark medias as sensitive by default")
	accountUpdateSubcommand.Flags().BoolVar(&accountsOpts.locked, "locked", false, "Following account requires approval")
	accountUpdateSubcommand.Flags().BoolVar(&accountsOpts.bot, "bot", false, "Set as service (automated) account")

	// Deprecated flags
	accountBlockSubcommand.Flags().MarkDeprecated("unset", "please use unblock instead")
	accountMuteSubcommand.Flags().MarkDeprecated("unset", "please use unmute instead")
	accountFollowSubcommand.Flags().MarkDeprecated("unset", "please use unfollow instead")

	// Those variables will be used to check if the options were
	// explicitly set or not
	accountUpdateFlags = accountUpdateSubcommand.Flags()
	accountMuteFlags = accountMuteSubcommand.Flags()
	accountFollowFlags = accountFollowSubcommand.Flags()
}

// accountsCmd represents the accounts command
// This command does nothing without a subcommand
var accountsCmd = &cobra.Command{
	Use:     "account [--account-id ID] subcommand",
	Aliases: []string{"accounts"},
	Short:   "Account-related functions",
	//Long:    `TBW...`, // TODO
}

// Note: Some account subcommands are not defined in this file.
var accountSubcommands = []*cobra.Command{
	&cobra.Command{
		Use: "show",
		Long: `Displays the details about the requested account.
If no account ID is specified, the current user account is used.`,
		Aliases: []string{"display"},
		Short:   "Display the account",
		Example: `  madonctl account show   # Display your own account

  madonctl account show --account-id 1234
  madonctl account show --user-id Gargron@mastodon.social
  madonctl account show --user-id https://mastodon.social/@Gargron

  madonctl account show 1234
  madonctl account show Gargron@mastodon.social
  madonctl account show https://mastodon.social/@Gargron
`,
		RunE: func(cmd *cobra.Command, args []string) error {
			return accountSubcommandsRunE(cmd.Name(), args)
		},
	},
	&cobra.Command{
		Use:   "followers",
		Short: "Display the accounts following the specified account",
		RunE: func(cmd *cobra.Command, args []string) error {
			return accountSubcommandsRunE(cmd.Name(), args)
		},
	},
	&cobra.Command{
		Use:   "following",
		Short: "Display the accounts followed by the specified account",
		RunE: func(cmd *cobra.Command, args []string) error {
			return accountSubcommandsRunE(cmd.Name(), args)
		},
	},
	&cobra.Command{
		Use:     "favourites",
		Aliases: []string{"favorites", "favourited", "favorited"},
		Short:   "Display the user's favourites",
		RunE: func(cmd *cobra.Command, args []string) error {
			return accountSubcommandsRunE(cmd.Name(), args)
		},
	},
	&cobra.Command{
		Use:     "blocks",
		Aliases: []string{"blocked"},
		Short:   "Display the user's blocked accounts",
		RunE: func(cmd *cobra.Command, args []string) error {
			return accountSubcommandsRunE(cmd.Name(), args)
		},
	},
	&cobra.Command{
		Use:     "mutes",
		Aliases: []string{"muted"},
		Short:   "Display the user's muted accounts",
		RunE: func(cmd *cobra.Command, args []string) error {
			return accountSubcommandsRunE(cmd.Name(), args)
		},
	},
	accountSearchSubcommand,
	accountStatusesSubcommand,
	accountFollowRequestsSubcommand,
	accountFollowSubcommand,
	accountUnfollowSubcommand,
	accountBlockSubcommand,
	accountUnblockSubcommand,
	accountMuteSubcommand,
	accountUnmuteSubcommand,
	accountPinSubcommand,
	accountUnpinSubcommand,
	accountRelationshipsSubcommand,
	accountReportsSubcommand,
	accountUpdateSubcommand,
	accountListEndorsementsSubcommand,
}

var accountSearchSubcommand = &cobra.Command{
	Use:   "search TEXT",
	Short: "Search for user accounts",
	Long: `Search for user accounts.

This command will lookup an account remotely if the search term is in the
@domain format and not yet known to the server.`,
	RunE: func(cmd *cobra.Command, args []string) error {
		return accountSubcommandsRunE(cmd.Name(), args)
	},
}

var accountStatusesSubcommand = &cobra.Command{
	Use:     "statuses",
	Aliases: []string{"st"},
	Short:   "Display the account statuses",
	Example: `  madonctl account statuses
  madonctl account statuses 404                         # local account numeric ID
  madonctl account statuses @McKael                     # local account
  madonctl account statuses Gargron@mastodon.social     # remote (known account)
  madonctl account statuses https://mastodon.social/@Gargron  # any account URL
`,
	RunE: func(cmd *cobra.Command, args []string) error {
		return accountSubcommandsRunE(cmd.Name(), args)
	},
}

var accountFollowRequestsSubcommand = &cobra.Command{
	Use:     "follow-requests",
	Aliases: []string{"follow-request", "fr"},
	Short:   "List, accept or deny a follow request",
	Example: `  madonctl account follow-requests --list
  madonctl account follow-requests --account-id X --accept
  madonctl account follow-requests --account-id Y --reject`,
	RunE: func(cmd *cobra.Command, args []string) error {
		return accountSubcommandsRunE(cmd.Name(), args)
	},
}
var accountFollowSubcommand = &cobra.Command{
	Use:   "follow",
	Short: "Follow an account",
	Example: `# Argument type can be set explicitly:
  madonctl account follow --account-id 1234
  madonctl account follow --remote Gargron@mastodon.social

# Or argument type can be guessed:
  madonctl account follow 4800
  madonctl account follow Gargron@mastodon.social --show-reblogs=false
  madonctl account follow https://mastodon.social/@Gargron
`,
	RunE: func(cmd *cobra.Command, args []string) error {
		return accountSubcommandsRunE(cmd.Name(), args)
	},
}

var accountUnfollowSubcommand = &cobra.Command{
	Use:   "unfollow",
	Short: "Stop following an account",
	Example: `  madonctl account unfollow --account-id 1234

Same usage as madonctl follow.
`,
	RunE: func(cmd *cobra.Command, args []string) error {
		return accountSubcommandsRunE(cmd.Name(), args)
	},
}

var accountBlockSubcommand = &cobra.Command{
	Use:   "block",
	Short: "Block the account",
	RunE: func(cmd *cobra.Command, args []string) error {
		return accountSubcommandsRunE(cmd.Name(), args)
	},
}

var accountUnblockSubcommand = &cobra.Command{
	Use:   "unblock",
	Short: "Unblock the account",
	RunE: func(cmd *cobra.Command, args []string) error {
		return accountSubcommandsRunE(cmd.Name(), args)
	},
}

var accountMuteSubcommand = &cobra.Command{
	Use:   "mute",
	Short: "Mute the account",
	RunE: func(cmd *cobra.Command, args []string) error {
		return accountSubcommandsRunE(cmd.Name(), args)
	},
}

var accountUnmuteSubcommand = &cobra.Command{
	Use:   "unmute",
	Short: "Unmute the account",
	RunE: func(cmd *cobra.Command, args []string) error {
		return accountSubcommandsRunE(cmd.Name(), args)
	},
}

var accountPinSubcommand = &cobra.Command{
	Use:     "pin",
	Short:   "Endorse (pin) the account",
	Aliases: []string{"endorse"},
	RunE: func(cmd *cobra.Command, args []string) error {
		return accountSubcommandsRunE(cmd.Name(), args)
	},
}

var accountUnpinSubcommand = &cobra.Command{
	Use:     "unpin",
	Short:   "Cancel endorsement of an account",
	Aliases: []string{"disavow"},
	RunE: func(cmd *cobra.Command, args []string) error {
		return accountSubcommandsRunE(cmd.Name(), args)
	},
}

var accountListEndorsementsSubcommand = &cobra.Command{
	Use:     "pinned",
	Short:   `Display the list of pinned (endorsed) accounts`,
	Aliases: []string{"list-endorsements", "get-endorsements"},
	RunE: func(cmd *cobra.Command, args []string) error {
		return accountSubcommandsRunE(cmd.Name(), args)
	},
}

var accountRelationshipsSubcommand = &cobra.Command{
	Use:   "relationships --account-ids ACC1,ACC2...",
	Short: "List relationships with the accounts",
	RunE: func(cmd *cobra.Command, args []string) error {
		return accountSubcommandsRunE(cmd.Name(), args)
	},
}

var accountReportsSubcommand = &cobra.Command{
	Use:   "reports",
	Short: "List reports or report a user account",
	Example: `  madonctl account reports --list
  madonctl account reports --account-id ACCOUNT --status-ids ID... --comment TEXT`,
	RunE: func(cmd *cobra.Command, args []string) error {
		return accountSubcommandsRunE(cmd.Name(), args)
	},
}

var accountUpdateSubcommand = &cobra.Command{
	Use:   "update",
	Short: "Update connected user account",
	Long: `Update connected user account

All flags are optional (set to an empty string if you want to delete a field).
The options --avatar and --header should be paths to image files.

Please note the avatar and header images cannot be removed, they can only be
replaced.`,
	Example: `  madonctl account update --display-name "Mr President"
  madonctl account update --note "I like madonctl"
  madonctl account update --avatar happyface.png`,
	RunE: func(cmd *cobra.Command, args []string) error {
		return accountSubcommandsRunE(cmd.Name(), args)
	},
}

// accountSubcommandsRunE is a generic function for status subcommands
func accountSubcommandsRunE(subcmd string, args []string) error {
	opt := accountsOpts

	if len(args) > 1 {
		return errors.New("too many arguments")
	}

	userInArg := false

	if len(args) == 1 {
		if len(args[0]) > 0 {
			userInArg = true
		} else {
			return errors.New("invalid argument (empty)")
		}
	}

	// Check account is provided in only one way
	aCounter := 0
	if opt.accountID != "" {
		aCounter++
	}
	if opt.accountUID != "" {
		aCounter++
	}
	if opt.remoteUID != "" {
		aCounter++
	}
	if userInArg {
		aCounter++
	}

	if aCounter > 1 {
		return errors.New("too many account identifiers provided")
	}

	if userInArg {
		// Is the argument an account ID?
		if _, err := strconv.ParseInt(args[0], 10, 64); err == nil {
			opt.accountID = args[0]
		} else if strings.HasPrefix(args[0], "https://") || strings.HasPrefix(args[0], "http://") {
			// That is not a remote UID scheme
			opt.accountUID = args[0]
		} else if subcmd == "follow" {
			// For the follow API, got to be a remote UID...
			opt.remoteUID = args[0]
			// ... unless it's local (i.e. no '@' in the identifier)...
			fid := strings.TrimLeft(args[0], "@")
			if !strings.ContainsRune(fid, '@') {
				opt.accountUID = args[0]
				opt.remoteUID = ""
			}
		} else {
			// Fall back to account UID
			opt.accountUID = args[0]
		}
	}

	if opt.accountUID != "" {
		if opt.accountID != "" {
			return errors.New("cannot use both account ID and UID")
		}
		// Sign in early to look the user id up
		var err error
		if err = madonInit(true); err != nil {
			return err
		}
		opt.accountID, err = accountLookupUser(opt.accountUID)
		if err != nil || opt.accountID == "" {
			if err != nil {
				errPrint("Cannot find user '%s': %v", opt.accountUID, err)
			} else {
				errPrint("Cannot find user '%s'", opt.accountUID)
			}
			os.Exit(1)
		}
	}

	switch subcmd {
	case "show", "search", "update":
		// These subcommands do not require an account ID
	case "favourites", "blocks", "mutes", "pinned":
		// Those subcommands can not use an account ID
		if opt.accountID != "" {
			return errors.New("useless account ID")
		}
	case "follow", "unfollow":
		// We need an account ID or a remote UID
		if opt.accountID == "" && opt.remoteUID == "" {
			return errors.New("missing account ID or URI")
		}
		if opt.accountID != "" && opt.remoteUID != "" {
			return errors.New("cannot use both account ID and URI")
		}
		if (opt.unset || subcmd == "unfollow") && opt.accountID == "" {
			return errors.New("unfollowing requires an account ID")
		}
	case "follow-requests":
		if opt.list {
			if opt.acceptFR || opt.rejectFR {
				return errors.New("incompatible options")
			}
		} else {
			if !opt.acceptFR && !opt.rejectFR { // No flag
				return errors.New("missing parameter (--list, --accept or --reject)")
			}
			// This is a FR reply
			if opt.acceptFR && opt.rejectFR {
				return errors.New("incompatible options")
			}
			if opt.accountID == "" {
				return errors.New("missing account ID")
			}
		}
	case "relationships":
		if opt.accountID == "" && len(opt.accountIDs) == 0 {
			return errors.New("missing account IDs")
		}
		if opt.accountID != "" && len(opt.accountIDs) > 0 {
			return errors.New("incompatible options")
		}
	case "reports":
		if opt.list {
			break // No argument needed
		}
		if opt.accountID == "" || len(opt.statusIDs) == 0 || opt.comment == "" {
			return errors.New("missing parameter")
		}
	case "followers", "following", "statuses":
		// If the user's account ID is missing, get it
		if opt.accountID == "" {
			// Sign in now to look the user id up
			if err := madonInit(true); err != nil {
				return err
			}
			account, err := gClient.GetCurrentAccount()
			if err != nil {
				return err
			}
			opt.accountID = account.ID
			if verbose {
				errPrint("User account ID: %d", opt.accountID)
			}
		}
	default:
		// The other subcommands here require an account ID
		if opt.accountID == "" {
			return errors.New("missing account ID")
		}
	}

	var limOpts *madon.LimitParams
	if opt.all || opt.limit > 0 || opt.sinceID != "" || opt.maxID != "" {
		limOpts = new(madon.LimitParams)
		limOpts.All = opt.all
	}

	if opt.limit > 0 {
		limOpts.Limit = int(opt.limit)
	}
	if opt.maxID != "" {
		limOpts.MaxID = opt.maxID
	}
	if opt.sinceID != "" {
		limOpts.SinceID = opt.sinceID
	}

	// All account subcommands need to have signed in
	if err := madonInit(true); err != nil {
		return err
	}

	var obj interface{}
	var err error

	switch subcmd {
	case "show":
		var account *madon.Account
		if opt.accountID != "" {
			account, err = gClient.GetAccount(opt.accountID)
		} else {
			account, err = gClient.GetCurrentAccount()
		}
		obj = account
	case "search":
		var accountList []madon.Account
		accountList, err = gClient.SearchAccounts(strings.Join(args, " "), opt.following, limOpts)
		obj = accountList
	case "followers":
		var accountList []madon.Account
		accountList, err = gClient.GetAccountFollowers(opt.accountID, limOpts)
		if opt.keep > 0 && len(accountList) > int(opt.keep) {
			accountList = accountList[:opt.keep]
		}
		obj = accountList
	case "following":
		var accountList []madon.Account
		accountList, err = gClient.GetAccountFollowing(opt.accountID, limOpts)
		if opt.keep > 0 && len(accountList) > int(opt.keep) {
			accountList = accountList[:opt.keep]
		}
		obj = accountList
	case "statuses":
		var statusList []madon.Status
		statusList, err = gClient.GetAccountStatuses(opt.accountID, opt.onlyPinned, opt.onlyMedia, opt.excludeReplies, limOpts)
		if opt.keep > 0 && len(statusList) > int(opt.keep) {
			statusList = statusList[:opt.keep]
		}
		obj = statusList
	case "follow", "unfollow":
		var relationship *madon.Relationship
		if opt.unset || subcmd == "unfollow" {
			relationship, err = gClient.UnfollowAccount(opt.accountID)
			obj = relationship
			break
		}
		if opt.accountID == "" {
			if opt.remoteUID != "" {
				// Remote account
				var account *madon.Account
				account, err = gClient.FollowRemoteAccount(opt.remoteUID)
				obj = account
				break
			}
			return errors.New("error: no usable parameter")
		}

		// Locally-known account
		var followReblogs *bool
		if accountFollowFlags.Lookup("show-reblogs").Changed {
			// Set followReblogs as it's been explicitly requested
			followReblogs = &opt.reblogs
		}
		relationship, err = gClient.FollowAccount(opt.accountID, followReblogs)
		obj = relationship
	case "follow-requests":
		if opt.list {
			var followRequests []madon.Account
			followRequests, err = gClient.GetAccountFollowRequests(limOpts)
			if opt.accountID != "" { // Display a specific request
				var fRequest *madon.Account
				for _, fr := range followRequests {
					if fr.ID == opt.accountID {
						fRequest = &fr
						break
					}
				}
				if fRequest != nil {
					followRequests = []madon.Account{*fRequest}
				} else {
					followRequests = []madon.Account{}
				}
			} else {
				if opt.keep > 0 && len(followRequests) > int(opt.keep) {
					followRequests = followRequests[:opt.keep]
				}
			}
			obj = followRequests
		} else {
			err = gClient.FollowRequestAuthorize(opt.accountID, !opt.rejectFR)
		}
	case "block", "unblock":
		var relationship *madon.Relationship
		if opt.unset || subcmd == "unblock" {
			relationship, err = gClient.UnblockAccount(opt.accountID)
		} else {
			relationship, err = gClient.BlockAccount(opt.accountID)
		}
		obj = relationship
	case "mute", "unmute":
		var relationship *madon.Relationship
		if opt.unset || subcmd == "unmute" {
			relationship, err = gClient.UnmuteAccount(opt.accountID)
		} else {
			var muteNotif *bool
			if accountMuteFlags.Lookup("notifications").Changed {
				muteNotif = &opt.muteNotifications
			}
			relationship, err = gClient.MuteAccount(opt.accountID, muteNotif)
		}
		obj = relationship
	case "pin", "unpin":
		var relationship *madon.Relationship
		if subcmd == "unpin" {
			relationship, err = gClient.UnpinAccount(opt.accountID)
		} else {
			relationship, err = gClient.PinAccount(opt.accountID)
		}
		obj = relationship
	case "favourites":
		var statusList []madon.Status
		statusList, err = gClient.GetFavourites(limOpts)
		if opt.keep > 0 && len(statusList) > int(opt.keep) {
			statusList = statusList[:opt.keep]
		}
		obj = statusList
	case "blocks":
		var accountList []madon.Account
		accountList, err = gClient.GetBlockedAccounts(limOpts)
		if opt.keep > 0 && len(accountList) > int(opt.keep) {
			accountList = accountList[:opt.keep]
		}
		obj = accountList
	case "mutes":
		var accountList []madon.Account
		accountList, err = gClient.GetMutedAccounts(limOpts)
		if opt.keep > 0 && len(accountList) > int(opt.keep) {
			accountList = accountList[:opt.keep]
		}
		obj = accountList
	case "pinned":
		var accountList []madon.Account
		accountList, err = gClient.GetEndorsements(limOpts)
		if opt.keep > 0 && len(accountList) > int(opt.keep) {
			accountList = accountList[:opt.keep]
		}
		obj = accountList
	case "relationships":
		var ids []madon.ActivityID
		ids, err = splitIDs(opt.accountIDs)
		if err != nil {
			return errors.New("cannot parse account IDs")
		}
		if opt.accountID != "" { // Allow --account-id
			ids = []madon.ActivityID{opt.accountID}
		}
		if len(ids) < 1 {
			return errors.New("missing account IDs")
		}
		var relationships []madon.Relationship
		relationships, err = gClient.GetAccountRelationships(ids)
		obj = relationships
	case "reports":
		if opt.list {
			var reports []madon.Report
			reports, err = gClient.GetReports(limOpts)
			if opt.keep > 0 && len(reports) > int(opt.keep) {
				reports = reports[:opt.keep]
			}
			obj = reports
			break
		}
		// Send a report
		var ids []madon.ActivityID
		ids, err = splitIDs(opt.statusIDs)
		if err != nil {
			return errors.New("cannot parse status IDs")
		}
		if len(ids) < 1 {
			return errors.New("missing status IDs")
		}
		var report *madon.Report
		report, err = gClient.ReportUser(opt.accountID, ids, opt.comment)
		obj = report
	case "update":
		var updateParams madon.UpdateAccountParams
		var source *madon.SourceParams
		change := false

		if accountUpdateFlags.Lookup("display-name").Changed {
			updateParams.DisplayName = &opt.displayName
			change = true
		}
		if accountUpdateFlags.Lookup("note").Changed {
			updateParams.Note = &opt.note
			change = true
		}
		if accountUpdateFlags.Lookup("avatar").Changed {
			updateParams.AvatarImagePath = &opt.avatar
			change = true
		}
		if accountUpdateFlags.Lookup("header").Changed {
			updateParams.HeaderImagePath = &opt.header
			change = true
		}
		if accountUpdateFlags.Lookup("locked").Changed {
			updateParams.Locked = &opt.locked
			change = true
		}
		if accountUpdateFlags.Lookup("bot").Changed {
			updateParams.Bot = &opt.bot
			change = true
		}
		if accountUpdateFlags.Lookup("default-language").Changed {
			if source == nil {
				source = &madon.SourceParams{}
			}
			source.Language = &opt.defaultLanguage
			change = true
		}
		if accountUpdateFlags.Lookup("default-privacy").Changed {
			if source == nil {
				source = &madon.SourceParams{}
			}
			source.Privacy = &opt.defaultPrivacy
			change = true
		}
		if accountUpdateFlags.Lookup("default-sensitive").Changed {
			if source == nil {
				source = &madon.SourceParams{}
			}
			source.Sensitive = &opt.defaultSensitive
			change = true
		}
		if accountUpdateFlags.Lookup("profile-field").Changed {
			var fa = []madon.Field{}
			for _, f := range opt.profileFields {
				kv := strings.SplitN(f, "=", 2)
				if len(kv) != 2 {
					return errors.New("cannot parse field")
				}
				fa = append(fa, madon.Field{Name: kv[0], Value: kv[1]})
			}
			updateParams.FieldsAttributes = &fa
			change = true
		}

		if !change { // We want at least one update
			return errors.New("missing parameters")
		}

		updateParams.Source = source

		var account *madon.Account
		account, err = gClient.UpdateAccount(updateParams)
		obj = account
	default:
		return errors.New("accountSubcommand: internal error")
	}

	if err != nil {
		errPrint("Error: %s", err.Error())
		os.Exit(1)
	}
	if obj == nil {
		return nil
	}

	p, err := getPrinter()
	if err != nil {
		errPrint("Error: %s", err.Error())
		os.Exit(1)
	}
	return p.printObj(obj)
}

// accountLookupUser tries to find a (single) user matching 'user'
// If the user is an HTTP URL, it will use the search API, else
// it will use the accounts/search API.
func accountLookupUser(user string) (madon.ActivityID, error) {
	var accID madon.ActivityID

	if strings.HasPrefix(user, "https://") || strings.HasPrefix(user, "http://") {
		res, err := gClient.Search(user, true)
		if err != nil {
			return "", err
		}
		if res != nil {
			if len(res.Accounts) > 1 {
				return "", errors.New("several results")
			}
			if len(res.Accounts) == 1 {
				accID = res.Accounts[0].ID
			}
		}
	} else {
		// Remove leading '@'
		user = strings.TrimLeft(user, "@")

		accList, err := gClient.SearchAccounts(user, false, &madon.LimitParams{Limit: 2})
		if err != nil {
			return "", err
		}
		for _, u := range accList {
			if u.Acct == user {
				accID = u.ID
				break
			}
		}
	}

	if accID == "" {
		return "", errors.New("user not found")
	}
	if verbose {
		errPrint("User '%s' is account ID %d", user, user)
	}
	return accID, nil
}