cmd/toot.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 (
	"strings"

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

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

// toot is a kind of alias for status post

var tootAliasFlags *flag.FlagSet

func init() {
	RootCmd.AddCommand(tootAliasCmd)

	tootAliasCmd.Flags().BoolVar(&statusOpts.sensitive, "sensitive", false, "Mark post as sensitive (NSFW)")
	tootAliasCmd.Flags().StringVar(&statusOpts.visibility, "visibility", "", "Visibility (direct|private|unlisted|public)")
	tootAliasCmd.Flags().StringVar(&statusOpts.spoiler, "spoiler", "", "Spoiler warning (CW)")
	tootAliasCmd.Flags().StringVar(&statusOpts.mediaIDs, "media-ids", "", "Comma-separated list of media IDs")
	tootAliasCmd.Flags().StringVarP(&statusOpts.mediaFilePath, "file", "f", "", "Media attachment file name")
	tootAliasCmd.Flags().StringVar(&statusOpts.textFilePath, "text-file", "", "Text file name (message content)")
	tootAliasCmd.Flags().StringVarP(&statusOpts.inReplyToID, "in-reply-to", "r", "", "Status ID to reply to")
	tootAliasCmd.Flags().BoolVar(&statusOpts.stdin, "stdin", false, "Read message content from standard input")
	tootAliasCmd.Flags().BoolVar(&statusOpts.addMentions, "add-mentions", false, "Add mentions when replying")
	tootAliasCmd.Flags().BoolVar(&statusOpts.sameVisibility, "same-visibility", false, "Use same visibility as original message (for replies)")

	// Flag completion
	annotation := make(map[string][]string)
	annotation[cobra.BashCompCustom] = []string{"__madonctl_visibility"}

	tootAliasCmd.Flags().Lookup("visibility").Annotations = annotation

	// This one will be used to check if the options were explicitly set or not
	tootAliasFlags = tootAliasCmd.Flags()
}

var tootAliasCmd = &cobra.Command{
	Use:     "toot",
	Aliases: []string{"post", "pouet"},
	Short:   "Post a message (toot)",
	Example: `  madonctl toot message
  madonctl toot --visibility private "To my followers only"
  madonctl toot --spoiler Warning "Hello, World"
  madonctl status post --media-ids ID1,ID2 "Here are the photos"
  madonctl post --sensitive --file image.jpg Image
  madonctl toot --text-file message.txt
  madonctl toot --in-reply-to STATUSID "@user response"
  madonctl toot --in-reply-to STATUSID --add-mentions "response"
  echo "Hello from #madonctl" | madonctl toot --visibility unlisted --stdin

The default visibility can be set in the configuration file with the option
'default_visibility' (or with an environmnent variable).`,
	RunE: func(cmd *cobra.Command, args []string) error {
		if err := madonInit(true); err != nil {
			return err
		}
		// Update the extra flag to reflect if `in-reply-to` was present or not
		statusOpts._hasReplyTo = cmd.Flags().Lookup("in-reply-to").Changed
		return statusSubcommandRunE("post", args)
	},
}

func toot(tootText string) (*madon.Status, error) {
	opt := statusOpts

	// Get default visibility from configuration
	if opt.visibility == "" {
		if v := viper.GetString("default_visibility"); v != "" {
			opt.visibility = v
		}
	}

	switch opt.visibility {
	case "", "direct", "private", "unlisted", "public":
		// OK
	default:
		return nil, errors.Errorf("invalid visibility argument value '%s'", opt.visibility)
	}

	// Bit of a fudge but there's no easy way to tell if a string flag
	// is empty by default or empty by assignment.  Can't use a pointer
	// and have `nil` be "unset" because Cobra will crash if we send it
	// a `nil` as the recepient for a flag variable.  Hence using an
	// extra struct member as a flag to indicate set/unset.
	if opt._hasReplyTo && opt.inReplyToID == "" {
		return nil, errors.New("invalid in-reply-to argument value")
	}

	ids, err := splitIDs(opt.mediaIDs)
	if err != nil {
		return nil, errors.New("cannot parse media IDs")
	}

	if tootText == "" && len(ids) == 0 && opt.spoiler == "" && opt.mediaFilePath == "" {
		return nil, errors.New("toot is empty")
	}

	if opt.inReplyToID != "" {
		var initialStatus *madon.Status
		var preserveVis bool
		if opt.sameVisibility &&
			!tootAliasFlags.Lookup("visibility").Changed &&
			!statusPostFlags.Lookup("visibility").Changed {
			// Preserve visibility unless the --visibility flag
			// has been used in the command line.
			preserveVis = true
		}
		if preserveVis || opt.addMentions {
			// Fetch original status message
			initialStatus, err = gClient.GetStatus(opt.inReplyToID)
			if err != nil {
				return nil, errors.Wrap(err, "cannot get original message")
			}
		}
		if preserveVis {
			v := initialStatus.Visibility
			// We do not set public visibility unless explicitly requested
			if v == "public" {
				opt.visibility = "unlisted"
			} else {
				opt.visibility = v
			}
		}
		if opt.addMentions {
			mentions, err := mentionsList(initialStatus)
			if err != nil {
				return nil, err
			}
			tootText = mentions + tootText
		}
	}

	// Uploading media file last
	if opt.mediaFilePath != "" {
		if len(ids) > 3 {
			return nil, errors.New("too many media attachments")
		}

		fileMediaID, err := uploadFile(opt.mediaFilePath)
		if err != nil {
			return nil, errors.Wrap(err, "cannot attach media file")
		}
		if fileMediaID != "" {
			ids = append(ids, fileMediaID)
		}
	}

	postParam := madon.PostStatusParams{
		Text:        tootText,
		InReplyTo:   opt.inReplyToID,
		MediaIDs:    ids,
		Sensitive:   opt.sensitive,
		SpoilerText: opt.spoiler,
		Visibility:  opt.visibility,
	}
	return gClient.PostStatus(postParam)
}

func mentionsList(s *madon.Status) (string, error) {
	a, err := gClient.GetCurrentAccount()
	if err != nil {
		return "", errors.Wrap(err, "cannot check account details")
	}

	var mentions []string
	// Add the sender if she is not the connected user
	if s.Account.Acct != a.Acct {
		mentions = append(mentions, "@"+s.Account.Acct)
	}
	for _, m := range s.Mentions {
		if m.Acct != a.Acct {
			mentions = append(mentions, "@"+m.Acct)
		}
	}
	mentionsStr := strings.Join(mentions, " ")
	if len(mentionsStr) > 0 {
		return mentionsStr + " ", nil
	}
	return "", nil
}