--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/.gitignore Wed Apr 19 19:08:47 2017 +0200
@@ -0,0 +1,9 @@
+# Binaries
+madonctl
+*.exe
+*.test
+
+*.swp
+
+.goxc*
+doc
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/LICENSE Wed Apr 19 19:08:47 2017 +0200
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2017 Mikael Berthe <mikael@lilotux.net>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/README.md Wed Apr 19 19:08:47 2017 +0200
@@ -0,0 +1,28 @@
+# madonctl
+
+Golang command line interface for the Mastodon API
+
+[![license](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](https://raw.githubusercontent.com/McKael/madonctl/master/LICENSE)
+
+`madonctl` is a [Go](https://golang.org/) CLI tool to use the Mastondon REST API.
+
+## Installation
+
+To install the application (you need to have Go):
+
+ go get github.com/McKael/madonctl
+
+Some pre-built binaries are available on the home page (see below).
+
+## Usage
+
+This section has not been written yet.
+
+Please check the [Homepage](https://lilotux.net/~mikael/pub/madonctl/) for an
+introduction and a few examples.
+
+## References
+
+- [madon](https://github.com/McKael/madon), the Go library for Mastodon API
+- [Mastodon API documentation](https://github.com/tootsuite/documentation/blob/master/Using-the-API/API.md)
+- [Mastodon repository](https://github.com/tootsuite/mastodon)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/cmd/README.md Wed Apr 19 19:08:47 2017 +0200
@@ -0,0 +1,28 @@
+# madonctl
+
+Golang command line interface for the Mastodon API
+
+[![license](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](https://raw.githubusercontent.com/McKael/madonctl/master/LICENSE)
+
+`madonctl` is a [Go](https://golang.org/) CLI tool to use the Mastondon REST API.
+
+## Installation
+
+To install the application (you need to have Go):
+
+ go get github.com/McKael/madonctl
+
+Some pre-built binaries are available on the home page (see below).
+
+## Usage
+
+This section has not been written yet.
+
+Please check the [Homepage](https://lilotux.net/~mikael/pub/madonctl/) for an
+introduction and a few examples.
+
+## References
+
+- [madon](https://github.com/McKael/madon), the Go library for Mastodon API
+- [Mastodon API documentation](https://github.com/tootsuite/documentation/blob/master/Using-the-API/API.md)
+- [Mastodon repository](https://github.com/tootsuite/mastodon)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/cmd/accounts.go Wed Apr 19 19:08:47 2017 +0200
@@ -0,0 +1,391 @@
+// Copyright © 2017 Mikael Berthe <mikael@lilotux.net>
+//
+// Licensed under the MIT license.
+// Please see the LICENSE file is this directory.
+
+package cmd
+
+import (
+ "errors"
+ "strings"
+
+ "github.com/spf13/cobra"
+
+ "github.com/McKael/madon"
+)
+
+var accountsOpts struct {
+ accountID int
+ unset bool // TODO remove eventually?
+ limit int
+ onlyMedia, excludeReplies bool // For acccount statuses
+ remoteUID string // 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
+ show bool // For account reports
+}
+
+func init() {
+ RootCmd.AddCommand(accountsCmd)
+
+ // Subcommands
+ accountsCmd.AddCommand(accountSubcommands...)
+
+ // Global flags
+ accountsCmd.PersistentFlags().IntVarP(&accountsOpts.accountID, "account-id", "a", 0, "Account ID number")
+ //accountsCmd.PersistentFlags().IntVarP(&accountsOpts.limit, "limit", "l", 0, "Limit number of results")
+
+ // Subcommand flags
+ 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")
+ accountMuteSubcommand.Flags().BoolVarP(&accountsOpts.unset, "unset", "", false, "Unmute the account")
+ accountFollowSubcommand.Flags().BoolVarP(&accountsOpts.unset, "unset", "", false, "Unfollow the account")
+ 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")
+}
+
+// accountsCmd represents the accounts command
+// This command does nothing without a subcommand
+var accountsCmd = &cobra.Command{
+ Use: "accounts [--account-id ID] subcommand",
+ Aliases: []string{"account"},
+ Short: "Account-related functions",
+ //Long: `TBW...`, // TODO
+ //PersistentPreRunE: func(cmd *cobra.Command, args []string) error {},
+}
+
+// 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",
+ 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)
+ },
+ },
+ &cobra.Command{
+ Use: "search TEXT",
+ Short: "Search for user accounts",
+ Long: `Search for user accounts.
+The server will lookup an account remotely if the search term is in the
+username@domain format and not yet in the database.`,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ return accountSubcommandsRunE(cmd.Name(), args)
+ },
+ },
+ accountStatusesSubcommand,
+ accountFollowRequestsSubcommand,
+ accountFollowSubcommand,
+ accountBlockSubcommand,
+ accountMuteSubcommand,
+ accountRelationshipsSubcommand,
+ accountReportsSubcommand,
+}
+
+var accountStatusesSubcommand = &cobra.Command{
+ Use: "statuses",
+ Aliases: []string{"st"},
+ Short: "Display the account statuses",
+ 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"},
+ Example: ` madonctl accounts follow-requests --list
+ madonctl accounts follow-requests --account-id X --accept
+ madonctl accounts follow-requests --account-id Y --reject`,
+ Short: "List, accept or deny a follow request",
+ RunE: func(cmd *cobra.Command, args []string) error {
+ return accountSubcommandsRunE("follow-requests", args)
+ },
+}
+var accountFollowSubcommand = &cobra.Command{
+ Use: "follow",
+ Short: "Follow or unfollow the account",
+ RunE: func(cmd *cobra.Command, args []string) error {
+ return accountSubcommandsRunE("follow", args)
+ },
+}
+
+var accountBlockSubcommand = &cobra.Command{
+ Use: "block",
+ Short: "Block or unblock the account",
+ RunE: func(cmd *cobra.Command, args []string) error {
+ return accountSubcommandsRunE("block", args)
+ },
+}
+
+var accountMuteSubcommand = &cobra.Command{
+ Use: "mute",
+ Short: "Mute or unmute the account",
+ RunE: func(cmd *cobra.Command, args []string) error {
+ return accountSubcommandsRunE("mute", 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("relationships", args)
+ },
+}
+
+var accountReportsSubcommand = &cobra.Command{
+ Use: "reports",
+ Short: "List reports or report a user account",
+ Example: ` madonctl accounts reports --list
+ madonctl accounts reports --account-id ACCOUNT --status-ids ID... --comment TEXT`,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ return accountSubcommandsRunE("reports", args)
+ },
+}
+
+// accountSubcommandsRunE is a generic function for status subcommands
+func accountSubcommandsRunE(subcmd string, args []string) error {
+ opt := accountsOpts
+
+ switch subcmd {
+ case "show", "search":
+ // These subcommands do not require an account ID
+ case "favourites", "blocks", "mutes":
+ // Those subcommands can not use an account ID
+ if opt.accountID > 0 {
+ return errors.New("useless account ID")
+ }
+ case "follow":
+ if opt.accountID < 1 && opt.remoteUID == "" {
+ return errors.New("missing account ID or URI")
+ }
+ if opt.accountID > 0 && opt.remoteUID != "" {
+ return errors.New("cannot use both account ID and URI")
+ }
+ if opt.unset && opt.accountID < 1 {
+ 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 < 1 {
+ return errors.New("missing account ID")
+ }
+ }
+ case "relationships":
+ if opt.accountID < 1 && len(opt.accountIDs) == 0 {
+ return errors.New("missing account IDs")
+ }
+ if opt.accountID > 0 && len(opt.accountIDs) > 0 {
+ return errors.New("incompatible options")
+ }
+ case "reports":
+ if opt.list {
+ break // No argument needed
+ }
+ if opt.accountID < 1 || len(opt.statusIDs) == 0 || opt.comment == "" {
+ return errors.New("missing parameter")
+ }
+ default:
+ // The other subcommands here require an account ID
+ if opt.accountID < 1 {
+ return errors.New("missing account ID")
+ }
+ }
+
+ // 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 > 0 {
+ account, err = gClient.GetAccount(opt.accountID)
+ } else {
+ account, err = gClient.GetCurrentAccount()
+ }
+ obj = account
+ case "search":
+ var accountList []madon.Account
+ limit := 0 // TODO use a global flag
+ accountList, err = gClient.SearchAccounts(strings.Join(args, " "), limit)
+ obj = accountList
+ case "followers":
+ var accountList []madon.Account
+ accountList, err = gClient.GetAccountFollowers(opt.accountID)
+ obj = accountList
+ case "following":
+ var accountList []madon.Account
+ accountList, err = gClient.GetAccountFollowing(opt.accountID)
+ obj = accountList
+ case "statuses":
+ var statusList []madon.Status
+ statusList, err = gClient.GetAccountStatuses(opt.accountID, opt.onlyMedia, opt.excludeReplies)
+ obj = statusList
+ case "follow":
+ if opt.unset {
+ err = gClient.UnfollowAccount(opt.accountID)
+ } else {
+ if opt.accountID > 0 {
+ err = gClient.FollowAccount(opt.accountID)
+ } else {
+ var account *madon.Account
+ account, err = gClient.FollowRemoteAccount(opt.remoteUID)
+ obj = account
+ }
+ }
+ case "follow-requests":
+ if opt.list {
+ var followRequests []madon.Account
+ followRequests, err = gClient.GetAccountFollowRequests()
+ obj = followRequests
+ } else {
+ err = gClient.FollowRequestAuthorize(opt.accountID, !opt.rejectFR)
+ }
+ case "block":
+ if opt.unset {
+ err = gClient.UnblockAccount(opt.accountID)
+ } else {
+ err = gClient.BlockAccount(opt.accountID)
+ }
+ case "mute":
+ if opt.unset {
+ err = gClient.UnmuteAccount(opt.accountID)
+ } else {
+ err = gClient.MuteAccount(opt.accountID)
+ }
+ case "favourites":
+ var statusList []madon.Status
+ statusList, err = gClient.GetFavourites()
+ obj = statusList
+ case "blocks":
+ var accountList []madon.Account
+ accountList, err = gClient.GetBlockedAccounts()
+ obj = accountList
+ case "mutes":
+ var accountList []madon.Account
+ accountList, err = gClient.GetMutedAccounts()
+ obj = accountList
+ case "relationships":
+ var ids []int
+ ids, err = splitIDs(opt.accountIDs)
+ if err != nil {
+ return errors.New("cannot parse account IDs")
+ }
+ if opt.accountID > 0 { // Allow --account-id
+ ids = []int{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()
+ obj = reports
+ break
+ }
+ // Send a report
+ var ids []int
+ 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
+ default:
+ return errors.New("accountSubcommand: internal error")
+ }
+
+ if err != nil {
+ errPrint("Error: %s", err.Error())
+ return nil
+ }
+ if obj == nil {
+ return nil
+ }
+
+ p, err := getPrinter()
+ if err != nil {
+ return err
+ }
+ return p.PrintObj(obj, nil, "")
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/cmd/completion.go Wed Apr 19 19:08:47 2017 +0200
@@ -0,0 +1,232 @@
+// Copyright © 2017 Mikael Berthe <mikael@lilotux.net>
+//
+// Licensed under the MIT license.
+// Please see the LICENSE file is this directory.
+
+package cmd
+
+import (
+ "bytes"
+ "io"
+ "os"
+ "strings"
+
+ "github.com/spf13/cobra"
+)
+
+var completionCmd = &cobra.Command{
+ Use: "completion bash|zsh",
+ Short: "Generate shell completion",
+ ValidArgs: []string{"bash", "zsh"},
+ Run: func(cmd *cobra.Command, args []string) {
+ if len(args) < 1 {
+ errPrint("Please specify your shell")
+ os.Exit(1)
+ }
+
+ switch args[0] {
+ case "bash":
+ if err := runCompletionBash(os.Stdout, RootCmd); err != nil {
+ errPrint("Error: %s", err.Error())
+ os.Exit(1)
+ }
+ case "zsh":
+ if err := runCompletionZsh(os.Stdout, RootCmd); err != nil {
+ errPrint("Error: %s", err.Error())
+ os.Exit(1)
+ }
+ default:
+ errPrint("Only bash is supported at the moment")
+ os.Exit(1)
+ }
+ },
+}
+
+func init() {
+ RootCmd.AddCommand(completionCmd)
+}
+
+func runCompletionBash(out io.Writer, c *cobra.Command) error {
+ return c.GenBashCompletion(out)
+}
+
+// Many thanks to the Kubernetes project for this one!
+func runCompletionZsh(out io.Writer, c *cobra.Command) error {
+ const zshInitialization = `# Copyright 2016 The Kubernetes Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+__madonctl_bash_source() {
+ alias shopt=':'
+ alias _expand=_bash_expand
+ alias _complete=_bash_comp
+ emulate -L sh
+ setopt kshglob noshglob braceexpand
+
+ source "$@"
+}
+
+__madonctl_type() {
+ # -t is not supported by zsh
+ if [ "$1" == "-t" ]; then
+ shift
+
+ # fake Bash 4 to disable "complete -o nospace". Instead
+ # "compopt +-o nospace" is used in the code to toggle trailing
+ # spaces. We don't support that, but leave trailing spaces on
+ # all the time
+ if [ "$1" = "__madonctl_compopt" ]; then
+ echo builtin
+ return 0
+ fi
+ fi
+ type "$@"
+}
+
+__madonctl_compgen() {
+ local completions w
+ completions=( $(compgen "$@") ) || return $?
+
+ # filter by given word as prefix
+ while [[ "$1" = -* && "$1" != -- ]]; do
+ shift
+ shift
+ done
+ if [[ "$1" == -- ]]; then
+ shift
+ fi
+ for w in "${completions[@]}"; do
+ if [[ "${w}" = "$1"* ]]; then
+ echo "${w}"
+ fi
+ done
+}
+
+__madonctl_compopt() {
+ true # don't do anything. Not supported by bashcompinit in zsh
+}
+
+__madonctl_declare() {
+ if [ "$1" == "-F" ]; then
+ whence -w "$@"
+ else
+ builtin declare "$@"
+ fi
+}
+
+__madonctl_ltrim_colon_completions()
+{
+ if [[ "$1" == *:* && "$COMP_WORDBREAKS" == *:* ]]; then
+ # Remove colon-word prefix from COMPREPLY items
+ local colon_word=${1%${1##*:}}
+ local i=${#COMPREPLY[*]}
+ while [[ $((--i)) -ge 0 ]]; do
+ COMPREPLY[$i]=${COMPREPLY[$i]#"$colon_word"}
+ done
+ fi
+}
+
+__madonctl_get_comp_words_by_ref() {
+ cur="${COMP_WORDS[COMP_CWORD]}"
+ prev="${COMP_WORDS[${COMP_CWORD}-1]}"
+ words=("${COMP_WORDS[@]}")
+ cword=("${COMP_CWORD[@]}")
+}
+
+__madonctl_filedir() {
+ local RET OLD_IFS w qw
+
+ __debug "_filedir $@ cur=$cur"
+ if [[ "$1" = \~* ]]; then
+ # somehow does not work. Maybe, zsh does not call this at all
+ eval echo "$1"
+ return 0
+ fi
+
+ OLD_IFS="$IFS"
+ IFS=$'\n'
+ if [ "$1" = "-d" ]; then
+ shift
+ RET=( $(compgen -d) )
+ else
+ RET=( $(compgen -f) )
+ fi
+ IFS="$OLD_IFS"
+
+ IFS="," __debug "RET=${RET[@]} len=${#RET[@]}"
+
+ for w in ${RET[@]}; do
+ if [[ ! "${w}" = "${cur}"* ]]; then
+ continue
+ fi
+ if eval "[[ \"\${w}\" = *.$1 || -d \"\${w}\" ]]"; then
+ qw="$(__madonctl_quote "${w}")"
+ if [ -d "${w}" ]; then
+ COMPREPLY+=("${qw}/")
+ else
+ COMPREPLY+=("${qw}")
+ fi
+ fi
+ done
+}
+
+__madonctl_quote() {
+ if [[ $1 == \'* || $1 == \"* ]]; then
+ # Leave out first character
+ printf %q "${1:1}"
+ else
+ printf %q "$1"
+ fi
+}
+
+autoload -U +X bashcompinit && bashcompinit
+
+# use word boundary patterns for BSD or GNU sed
+LWORD='[[:<:]]'
+RWORD='[[:>:]]'
+if sed --help 2>&1 | grep -q GNU; then
+ LWORD='\<'
+ RWORD='\>'
+fi
+
+__madonctl_convert_bash_to_zsh() {
+ sed \
+ -e 's/declare -F/whence -w/' \
+ -e 's/local \([a-zA-Z0-9_]*\)=/local \1; \1=/' \
+ -e 's/flags+=("\(--.*\)=")/flags+=("\1"); two_word_flags+=("\1")/' \
+ -e 's/must_have_one_flag+=("\(--.*\)=")/must_have_one_flag+=("\1")/' \
+ -e "s/${LWORD}_filedir${RWORD}/__madonctl_filedir/g" \
+ -e "s/${LWORD}_get_comp_words_by_ref${RWORD}/__madonctl_get_comp_words_by_ref/g" \
+ -e "s/${LWORD}__ltrim_colon_completions${RWORD}/__madonctl_ltrim_colon_completions/g" \
+ -e "s/${LWORD}compgen${RWORD}/__madonctl_compgen/g" \
+ -e "s/${LWORD}compopt${RWORD}/__madonctl_compopt/g" \
+ -e "s/${LWORD}declare${RWORD}/__madonctl_declare/g" \
+ -e "s/\\\$(type${RWORD}/\$(__madonctl_type/g" \
+ <<'BASH_COMPLETION_EOF'
+`
+
+ const zshTail = `
+BASH_COMPLETION_EOF
+}
+
+__madonctl_bash_source <(__madonctl_convert_bash_to_zsh)
+`
+
+ buf := new(bytes.Buffer)
+ out.Write([]byte(zshInitialization))
+ err := c.GenBashCompletion(buf)
+ script := strings.Replace(buf.String(), "flaghash[${flagname}]", `flaghash[workaround]`, -1)
+ out.Write([]byte(script))
+ out.Write([]byte(zshTail))
+ return err
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/cmd/config.go Wed Apr 19 19:08:47 2017 +0200
@@ -0,0 +1,106 @@
+// Copyright © 2017 Mikael Berthe <mikael@lilotux.net>
+//
+// Licensed under the MIT license.
+// Please see the LICENSE file is this directory.
+
+package cmd
+
+import (
+ "github.com/spf13/cobra"
+ "github.com/spf13/viper"
+
+ "github.com/McKael/madonctl/printer"
+)
+
+var configCmd = &cobra.Command{
+ Use: "config",
+ Short: "Display configuration",
+}
+
+func init() {
+ RootCmd.AddCommand(configCmd)
+
+ // Subcommands
+ configCmd.AddCommand(configSubcommands...)
+}
+
+var configSubcommands = []*cobra.Command{
+ &cobra.Command{
+ Use: "dump",
+ Short: "Dump the configuration",
+ RunE: func(cmd *cobra.Command, args []string) error {
+ return configDump()
+ },
+ },
+ &cobra.Command{
+ Use: "whoami",
+ Aliases: []string{"token"},
+ Short: "Display user token",
+ RunE: func(cmd *cobra.Command, args []string) error {
+ return configDisplayToken()
+ },
+ },
+}
+
+const configurationTemplate = `---
+instance: '{{.InstanceURL}}'
+app_id: '{{.ID}}'
+app_secret: '{{.Secret}}'
+
+{{if .UserToken}}token: {{.UserToken.access_token}}{{else}}#token: ''{{end}}
+#login: ''
+#password: ''
+safe_mode: true
+...
+`
+
+func configDump() error {
+ if viper.GetBool("safe_mode") {
+ errPrint("Cannot dump: disabled by configuration (safe_mode)")
+ return nil
+ }
+
+ if err := madonInitClient(); err != nil {
+ return err
+ }
+ // Try to sign in, but don't mind if it fails
+ if err := madonLogin(); err != nil {
+ errPrint("Info: could not log in: %s", err)
+ }
+
+ var p printer.ResourcePrinter
+ var err error
+
+ if getOutputFormat() == "plain" {
+ cfile := viper.ConfigFileUsed()
+ if cfile == "" {
+ cfile = defaultConfigFile
+ }
+ errPrint("You can copy the following lines into a configuration file.")
+ errPrint("E.g. %s -i INSTANCE -L USERNAME -P PASS config dump > %s\n", AppName, cfile)
+ p, err = printer.NewPrinterTemplate(configurationTemplate)
+ } else {
+ p, err = getPrinter()
+ }
+ if err != nil {
+ return err
+ }
+ return p.PrintObj(gClient, nil, "")
+}
+
+func configDisplayToken() error {
+ if viper.GetBool("safe_mode") {
+ errPrint("Cannot dump: disabled by configuration (safe_mode)")
+ return nil
+ }
+
+ if err := madonInit(true); err != nil {
+ return err
+ }
+
+ p, err := getPrinter()
+ if err != nil {
+ return err
+ }
+ return p.PrintObj(gClient.UserToken, nil, "")
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/cmd/instance.go Wed Apr 19 19:08:47 2017 +0200
@@ -0,0 +1,39 @@
+// Copyright © 2017 Mikael Berthe <mikael@lilotux.net>
+//
+// Licensed under the MIT license.
+// Please see the LICENSE file is this directory.
+
+package cmd
+
+import (
+ "github.com/spf13/cobra"
+)
+
+// timelinesCmd represents the timelines command
+var instanceCmd = &cobra.Command{
+ Use: "instance",
+ Short: "Display current instance information",
+ RunE: instanceRunE,
+}
+
+func init() {
+ RootCmd.AddCommand(instanceCmd)
+}
+
+func instanceRunE(cmd *cobra.Command, args []string) error {
+ if err := madonInit(false); err != nil {
+ return err
+ }
+
+ i, err := gClient.GetCurrentInstance()
+ if err != nil {
+ errPrint("Error: %s", err.Error())
+ return nil
+ }
+
+ p, err := getPrinter()
+ if err != nil {
+ return err
+ }
+ return p.PrintObj(i, nil, "")
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/cmd/madon.go Wed Apr 19 19:08:47 2017 +0200
@@ -0,0 +1,115 @@
+// Copyright © 2017 Mikael Berthe <mikael@lilotux.net>
+//
+// Licensed under the MIT license.
+// Please see the LICENSE file is this directory.
+
+package cmd
+
+import (
+ "errors"
+ "fmt"
+ "strconv"
+ "strings"
+
+ "github.com/McKael/madon"
+ "github.com/spf13/viper"
+)
+
+var scopes = []string{"read", "write", "follow"}
+
+func madonInit(signIn bool) error {
+ if gClient == nil {
+ if err := madonInitClient(); err != nil {
+ return err
+ }
+ }
+ if signIn {
+ return madonLogin()
+ }
+ return nil
+}
+
+func madonInitClient() error {
+ if gClient != nil {
+ return nil
+ }
+ var err error
+
+ // Overwrite variables using Viper
+ instanceURL = viper.GetString("instance")
+ appID = viper.GetString("app_id")
+ appSecret = viper.GetString("app_secret")
+
+ if instanceURL == "" {
+ return errors.New("no instance provided")
+ }
+
+ if appID != "" && appSecret != "" {
+ // We already have an app key/secret pair
+ gClient, err = madon.RestoreApp(AppName, instanceURL, appID, appSecret, nil)
+ if err != nil {
+ return err
+ }
+ // Check instance
+ if _, err := gClient.GetCurrentInstance(); err != nil {
+ return fmt.Errorf("could not use provided app secrets")
+ }
+ if verbose {
+ errPrint("Using provided app secrets")
+ }
+ return nil
+ }
+
+ if appID != "" || appSecret != "" {
+ errPrint("Warning: provided app id/secrets incomplete -- registering again")
+ }
+
+ gClient, err = madon.NewApp(AppName, scopes, madon.NoRedirect, instanceURL)
+ if err != nil {
+ return fmt.Errorf("app registration failed: %s", err.Error())
+ }
+
+ errPrint("Registred new application.")
+ return nil
+}
+
+func madonLogin() error {
+ if gClient == nil {
+ return errors.New("application not registred")
+ }
+
+ token = viper.GetString("token")
+ login = viper.GetString("login")
+ password = viper.GetString("password")
+
+ if token != "" { // TODO check token validity?
+ if verbose {
+ errPrint("Reusing existing token.")
+ }
+ gClient.SetUserToken(token, login, password, []string{})
+ return nil
+ }
+
+ err := gClient.LoginBasic(login, password, scopes)
+ if err == nil {
+ return nil
+ }
+ return fmt.Errorf("login failed: %s", err.Error())
+}
+
+// splitIDs splits a list of IDs into an int array
+func splitIDs(ids string) (list []int, err error) {
+ var i int
+ if ids == "" {
+ return
+ }
+ l := strings.Split(ids, ",")
+ for _, s := range l {
+ i, err = strconv.Atoi(s)
+ if err != nil {
+ return
+ }
+ list = append(list, i)
+ }
+ return
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/cmd/media.go Wed Apr 19 19:08:47 2017 +0200
@@ -0,0 +1,68 @@
+// Copyright © 2017 Mikael Berthe <mikael@lilotux.net>
+//
+// Licensed under the MIT license.
+// Please see the LICENSE file is this directory.
+
+package cmd
+
+import (
+ "errors"
+
+ "github.com/spf13/cobra"
+)
+
+var mediaOpts struct {
+ filePath string
+}
+
+// mediaCmd represents the media command
+var mediaCmd = &cobra.Command{
+ Use: "media --file FILENAME",
+ Aliases: []string{"upload"},
+ Short: "Upload a media attachment",
+ //Long: `TBW...`,
+ RunE: mediaRunE,
+}
+
+func init() {
+ RootCmd.AddCommand(mediaCmd)
+
+ mediaCmd.Flags().StringVar(&mediaOpts.filePath, "file", "", "Path of the media file")
+ mediaCmd.MarkFlagRequired("file")
+}
+
+func mediaRunE(cmd *cobra.Command, args []string) error {
+ opt := mediaOpts
+
+ if opt.filePath == "" {
+ return errors.New("no media file name provided")
+ }
+
+ if err := madonInit(true); err != nil {
+ return err
+ }
+
+ attachment, err := gClient.UploadMedia(opt.filePath)
+ if err != nil {
+ errPrint("Error: %s", err.Error())
+ return nil
+ }
+
+ p, err := getPrinter()
+ if err != nil {
+ return err
+ }
+ return p.PrintObj(attachment, nil, "")
+}
+
+// uploadFile uploads a media file and returns the attachment ID
+func uploadFile(filePath string) (int, error) {
+ attachment, err := gClient.UploadMedia(filePath)
+ if err != nil {
+ return 0, err
+ }
+ if attachment == nil {
+ return 0, nil
+ }
+ return attachment.ID, nil
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/cmd/notifications.go Wed Apr 19 19:08:47 2017 +0200
@@ -0,0 +1,76 @@
+// Copyright © 2017 Mikael Berthe <mikael@lilotux.net>
+//
+// Licensed under the MIT license.
+// Please see the LICENSE file is this directory.
+
+package cmd
+
+import (
+ "errors"
+
+ "github.com/spf13/cobra"
+)
+
+var notificationsOpts struct {
+ list, clear bool
+ notifID int
+}
+
+// notificationsCmd represents the notifications subcommand
+var notificationsCmd = &cobra.Command{
+ Use: "notifications", // XXX
+ Aliases: []string{"notification", "notif"},
+ Short: "Manage notifications",
+ Example: ` madonctl accounts notifications --list
+ madonctl accounts notifications --list --clear
+ madonctl accounts notifications --notification-id N`,
+ //Long: `TBW...`,
+ RunE: notificationRunE,
+}
+
+func init() {
+ accountsCmd.AddCommand(notificationsCmd)
+
+ notificationsCmd.Flags().BoolVar(¬ificationsOpts.list, "list", false, "List all current notifications")
+ notificationsCmd.Flags().BoolVar(¬ificationsOpts.clear, "clear", false, "Clear all current notifications")
+ notificationsCmd.Flags().IntVar(¬ificationsOpts.notifID, "notification-id", 0, "Get a notification")
+}
+
+func notificationRunE(cmd *cobra.Command, args []string) error {
+ opt := notificationsOpts
+
+ if !opt.list && !opt.clear && opt.notifID < 1 {
+ return errors.New("missing parameters")
+ }
+
+ if err := madonInit(true); err != nil {
+ return err
+ }
+
+ var obj interface{}
+ var err error
+
+ if opt.list {
+ obj, err = gClient.GetNotifications()
+ } else if opt.notifID > 0 {
+ obj, err = gClient.GetNotification(opt.notifID)
+ }
+
+ if err == nil && opt.clear {
+ err = gClient.ClearNotifications()
+ }
+
+ if err != nil {
+ errPrint("Error: %s", err.Error())
+ return nil
+ }
+ if obj == nil {
+ return nil
+ }
+
+ p, err := getPrinter()
+ if err != nil {
+ return err
+ }
+ return p.PrintObj(obj, nil, "")
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/cmd/root.go Wed Apr 19 19:08:47 2017 +0200
@@ -0,0 +1,190 @@
+// Copyright © 2017 Mikael Berthe <mikael@lilotux.net>
+//
+// Licensed under the MIT license.
+// Please see the LICENSE file is this directory.
+
+package cmd
+
+import (
+ "fmt"
+ "io/ioutil"
+ "os"
+
+ "github.com/spf13/cobra"
+ "github.com/spf13/viper"
+
+ "github.com/McKael/madon"
+ "github.com/McKael/madonctl/printer"
+)
+
+// AppName is the CLI application name
+const AppName = "madonctl"
+const defaultConfigFile = "$HOME/.config/" + AppName + "/" + AppName + ".yaml"
+
+var cfgFile string
+var safeMode bool
+var instanceURL, appID, appSecret string
+var login, password, token string
+var gClient *madon.Client
+var verbose bool
+var outputFormat string
+var outputTemplate, outputTemplateFile string
+
+// RootCmd represents the base command when called without any subcommands
+var RootCmd = &cobra.Command{
+ Use: AppName,
+ Short: "A CLI utility for Mastodon API",
+ PersistentPreRunE: checkOutputFormat,
+ Long: `madonctl is a CLI tool for the Mastodon REST API.
+
+You can use a configuration file to store common options.
+For example, create ` + defaultConfigFile + ` with the following
+contents:
+
+ ---
+ instance: "INSTANCE"
+ login: "USERNAME"
+ password: "USERPASSWORD"
+ ...
+
+The simplest way to generate a configuration file is to use the 'config dump'
+command.
+
+(Configuration files in JSON are also accepted.)
+
+If you want shell auto-completion (for bash or zsh), you can generate the
+completion scripts with "madonctl completion $SHELL".
+For example if you use bash:
+
+ madonctl completion bash > _bash_madonctl
+ source _bash_madonctl
+
+Now you should have tab completion for subcommands and flags.
+
+Note: Most examples assume the user's credentials are set in the configuration
+file.
+`,
+ Example: ` madonctl instance
+ madonctl toot "Hello, World"
+ madonctl toot --visibility direct "@McKael Hello, You"
+ madonctl toot --visibility private --spoiler CW "The answer was 42"
+ madonctl post --file image.jpg Selfie
+ madonctl --instance INSTANCE --login USERNAME --password PASS timeline
+ madonctl accounts notifications --list --clear
+ madonctl accounts blocked
+ madonctl accounts search Gargron
+ madonctl search --resolve https://mastodon.social/@Gargron
+ madonctl accounts follow --remote Gargron@mastodon.social
+ madonctl accounts --account-id 399 statuses
+ madonctl status --status-id 416671 show
+ madonctl status --status-id 416671 favourite
+ madonctl status --status-id 416671 boost
+ madonctl accounts show
+ madonctl accounts show -o yaml
+ madonctl accounts --account-id 1 followers --template '{{.acct}}{{"\n"}}'
+ madonctl config whoami
+ madonctl timeline :mastodon`,
+}
+
+// Execute adds all child commands to the root command sets flags appropriately.
+// This is called by main.main(). It only needs to happen once to the rootCmd.
+func Execute() {
+ if err := RootCmd.Execute(); err != nil {
+ errPrint("Error: %s", err.Error())
+ os.Exit(-1)
+ }
+}
+
+func init() {
+ cobra.OnInitialize(initConfig)
+
+ // Global flags
+ RootCmd.PersistentFlags().StringVar(&cfgFile, "config", "",
+ "config file (default is "+defaultConfigFile+")")
+ RootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Verbose mode")
+ RootCmd.PersistentFlags().StringVarP(&instanceURL, "instance", "i", "", "Mastodon instance")
+ RootCmd.PersistentFlags().StringVarP(&login, "login", "L", "", "Instance user login")
+ RootCmd.PersistentFlags().StringVarP(&password, "password", "P", "", "Instance user password")
+ RootCmd.PersistentFlags().StringVarP(&token, "token", "t", "", "User token")
+ RootCmd.PersistentFlags().StringVarP(&outputFormat, "output", "o", "plain",
+ "Output format (plain|json|yaml|template)")
+ RootCmd.PersistentFlags().StringVar(&outputTemplate, "template", "",
+ "Go template (for output=template)")
+ RootCmd.PersistentFlags().StringVar(&outputTemplateFile, "template-file", "",
+ "Go template file (for output=template)")
+
+ // Configuration file bindings
+ viper.BindPFlag("output", RootCmd.PersistentFlags().Lookup("output"))
+ viper.BindPFlag("verbose", RootCmd.PersistentFlags().Lookup("verbose"))
+ // XXX viper.BindPFlag("apiKey", RootCmd.PersistentFlags().Lookup("api-key"))
+ viper.BindPFlag("instance", RootCmd.PersistentFlags().Lookup("instance"))
+ viper.BindPFlag("login", RootCmd.PersistentFlags().Lookup("login"))
+ viper.BindPFlag("password", RootCmd.PersistentFlags().Lookup("password"))
+ viper.BindPFlag("token", RootCmd.PersistentFlags().Lookup("token"))
+}
+
+func checkOutputFormat(cmd *cobra.Command, args []string) error {
+ of := viper.GetString("output")
+ switch of {
+ case "", "plain", "json", "yaml", "template":
+ return nil // Accepted
+ }
+ return fmt.Errorf("output format '%s' not supported", of)
+}
+
+// initConfig reads in config file and ENV variables if set.
+func initConfig() {
+ if cfgFile != "" { // enable ability to specify config file via flag
+ viper.SetConfigFile(cfgFile)
+ }
+
+ viper.SetConfigName(AppName) // name of config file (without extension)
+ viper.AddConfigPath("$HOME/.config/" + AppName)
+ viper.AddConfigPath("$HOME/." + AppName)
+
+ // Read in environment variables that match, with a prefix
+ viper.SetEnvPrefix(AppName)
+ viper.AutomaticEnv()
+
+ // If a config file is found, read it in.
+ if err := viper.ReadInConfig(); viper.GetBool("verbose") && err == nil {
+ errPrint("Using config file: %s", viper.ConfigFileUsed())
+ }
+}
+
+// getOutputFormat return the requested output format, defaulting to "plain".
+func getOutputFormat() string {
+ of := viper.GetString("output")
+ if of == "" {
+ of = "plain"
+ }
+ // Override format if a template is provided
+ if of == "plain" && (outputTemplate != "" || outputTemplateFile != "") {
+ // If the format is plain and there is a template option,
+ // set the format to "template".
+ of = "template"
+ }
+ return of
+}
+
+// getPrinter returns a resource printer for the requested output format.
+func getPrinter() (printer.ResourcePrinter, error) {
+ var opt string
+ of := getOutputFormat()
+
+ if of == "template" {
+ opt = outputTemplate
+ if outputTemplateFile != "" {
+ tmpl, err := ioutil.ReadFile(outputTemplateFile)
+ if err != nil {
+ return nil, err
+ }
+ opt = string(tmpl)
+ }
+ }
+ return printer.NewPrinter(of, opt)
+}
+
+func errPrint(format string, a ...interface{}) (n int, err error) {
+ return fmt.Fprintf(os.Stderr, format+"\n", a...)
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/cmd/search.go Wed Apr 19 19:08:47 2017 +0200
@@ -0,0 +1,55 @@
+// Copyright © 2017 Mikael Berthe <mikael@lilotux.net>
+//
+// Licensed under the MIT license.
+// Please see the LICENSE file is this directory.
+
+package cmd
+
+import (
+ "errors"
+ "strings"
+
+ "github.com/spf13/cobra"
+)
+
+var searchOpts struct {
+ resolve bool
+}
+
+// searchCmd represents the search command
+var searchCmd = &cobra.Command{
+ Use: "search [--resolve] STRING",
+ Short: "Search for contents (accounts or statuses)",
+ //Long: `TBW...`,
+ RunE: searchRunE,
+}
+
+func init() {
+ RootCmd.AddCommand(searchCmd)
+
+ searchCmd.Flags().BoolVar(&searchOpts.resolve, "resolve", false, "Resolve non-local accounts")
+}
+
+func searchRunE(cmd *cobra.Command, args []string) error {
+ opt := searchOpts
+
+ if len(args) == 0 {
+ return errors.New("no search string provided")
+ }
+
+ if err := madonInit(true); err != nil {
+ return err
+ }
+
+ results, err := gClient.Search(strings.Join(args, " "), opt.resolve)
+ if err != nil {
+ errPrint("Error: %s", err.Error())
+ return nil
+ }
+
+ p, err := getPrinter()
+ if err != nil {
+ return err
+ }
+ return p.PrintObj(results, nil, "")
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/cmd/status.go Wed Apr 19 19:08:47 2017 +0200
@@ -0,0 +1,218 @@
+// Copyright © 2017 Mikael Berthe <mikael@lilotux.net>
+//
+// Licensed under the MIT license.
+// Please see the LICENSE file is this directory.
+
+package cmd
+
+import (
+ "errors"
+ "strings"
+
+ "github.com/spf13/cobra"
+
+ "github.com/McKael/madon"
+)
+
+var statusOpts struct {
+ statusID int
+ unset bool
+
+ // The following fields are used for the post/toot command
+ visibility string
+ sensitive bool
+ spoiler string
+ inReplyToID int
+ filePath string
+ mediaIDs string
+
+ // TODO
+ limit int
+}
+
+func init() {
+ RootCmd.AddCommand(statusCmd)
+
+ // Subcommands
+ statusCmd.AddCommand(statusSubcommands...)
+
+ // Global flags
+ statusCmd.PersistentFlags().IntVarP(&statusOpts.statusID, "status-id", "s", 0, "Status ID number")
+ //statusCmd.PersistentFlags().IntVarP(&statusOpts.limit, "limit", "l", 0, "Limit number of results")
+
+ statusCmd.MarkPersistentFlagRequired("status-id")
+
+ // Subcommand flags
+ statusReblogSubcommand.Flags().BoolVar(&statusOpts.unset, "unset", false, "Unreblog the status")
+ statusFavouriteSubcommand.Flags().BoolVar(&statusOpts.unset, "unset", false, "Remove the status from the favourites")
+ statusPostSubcommand.Flags().BoolVar(&statusOpts.sensitive, "sensitive", false, "Mark post as sensitive (NSFW)")
+ statusPostSubcommand.Flags().StringVar(&statusOpts.visibility, "visibility", "", "Visibility (direct|private|unlisted|public)")
+ statusPostSubcommand.Flags().StringVar(&statusOpts.spoiler, "spoiler", "", "Spoiler warning (CW)")
+ statusPostSubcommand.Flags().StringVar(&statusOpts.mediaIDs, "media-ids", "", "Comma-separated list of media IDs")
+ statusPostSubcommand.Flags().StringVarP(&statusOpts.filePath, "file", "f", "", "File name")
+ statusPostSubcommand.Flags().IntVarP(&statusOpts.inReplyToID, "in-reply-to", "r", 0, "Status ID to reply to")
+}
+
+// statusCmd represents the status command
+// This command does nothing without a subcommand
+var statusCmd = &cobra.Command{
+ Use: "status --status-id ID subcommand",
+ Aliases: []string{"st"},
+ Short: "Get status details",
+ //Long: `TBW...`, // TODO
+ PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
+ // This is common to status and all status subcommands but "post"
+ if statusOpts.statusID < 1 && cmd.Name() != "post" {
+ return errors.New("missing status ID")
+ }
+ if err := madonInit(true); err != nil {
+ return err
+ }
+ return nil
+ },
+}
+
+var statusSubcommands = []*cobra.Command{
+ &cobra.Command{
+ Use: "show",
+ Aliases: []string{"display"},
+ Short: "Get the status",
+ RunE: func(cmd *cobra.Command, args []string) error {
+ return statusSubcommandRunE(cmd.Name(), args)
+ },
+ },
+ &cobra.Command{
+ Use: "context",
+ Short: "Get the status context",
+ RunE: func(cmd *cobra.Command, args []string) error {
+ return statusSubcommandRunE(cmd.Name(), args)
+ },
+ },
+ &cobra.Command{
+ Use: "card",
+ Short: "Get the status card",
+ RunE: func(cmd *cobra.Command, args []string) error {
+ return statusSubcommandRunE(cmd.Name(), args)
+ },
+ },
+ &cobra.Command{
+ Use: "reblogged-by",
+ Short: "Display accounts which reblogged the status",
+ RunE: func(cmd *cobra.Command, args []string) error {
+ return statusSubcommandRunE(cmd.Name(), args)
+ },
+ },
+ &cobra.Command{
+ Use: "favourited-by",
+ Aliases: []string{"favorited-by"},
+ Short: "Display accounts which favourited the status",
+ RunE: func(cmd *cobra.Command, args []string) error {
+ return statusSubcommandRunE(cmd.Name(), args)
+ },
+ },
+ &cobra.Command{
+ Use: "delete",
+ Aliases: []string{"rm"},
+ Short: "Delete the status",
+ RunE: func(cmd *cobra.Command, args []string) error {
+ return statusSubcommandRunE(cmd.Name(), args)
+ },
+ },
+ statusReblogSubcommand,
+ statusFavouriteSubcommand,
+ statusPostSubcommand,
+}
+
+var statusReblogSubcommand = &cobra.Command{
+ Use: "boost",
+ Aliases: []string{"reblog"},
+ Short: "Boost (reblog) or unreblog the status",
+ RunE: func(cmd *cobra.Command, args []string) error {
+ return statusSubcommandRunE(cmd.Name(), args)
+ },
+}
+
+var statusFavouriteSubcommand = &cobra.Command{
+ Use: "favourite",
+ Aliases: []string{"favorite", "fave"},
+ Short: "Mark/unmark the status as favourite",
+ RunE: func(cmd *cobra.Command, args []string) error {
+ return statusSubcommandRunE(cmd.Name(), args)
+ },
+}
+
+var statusPostSubcommand = &cobra.Command{
+ Use: "post",
+ Aliases: []string{"toot", "pouet"},
+ Short: "Post a message (same as 'madonctl toot')",
+ Example: ` madonctl status post --spoiler Warning "Hello, World"
+ madonctl status toot --sensitive --file image.jpg Image
+ madonctl status post --media-ids ID1,ID2,ID3 Image`,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ return statusSubcommandRunE(cmd.Name(), args)
+ },
+}
+
+func statusSubcommandRunE(subcmd string, args []string) error {
+ opt := statusOpts
+
+ var obj interface{}
+ var err error
+
+ switch subcmd {
+ case "show":
+ var status *madon.Status
+ status, err = gClient.GetStatus(opt.statusID)
+ obj = status
+ case "context":
+ var context *madon.Context
+ context, err = gClient.GetStatusContext(opt.statusID)
+ obj = context
+ case "card":
+ var context *madon.Card
+ context, err = gClient.GetStatusCard(opt.statusID)
+ obj = context
+ case "reblogged-by":
+ var accountList []madon.Account
+ accountList, err = gClient.GetStatusRebloggedBy(opt.statusID)
+ obj = accountList
+ case "favourited-by":
+ var accountList []madon.Account
+ accountList, err = gClient.GetStatusFavouritedBy(opt.statusID)
+ obj = accountList
+ case "delete":
+ err = gClient.DeleteStatus(opt.statusID)
+ case "boost":
+ if opt.unset {
+ err = gClient.UnreblogStatus(opt.statusID)
+ } else {
+ err = gClient.ReblogStatus(opt.statusID)
+ }
+ case "favourite":
+ if opt.unset {
+ err = gClient.UnfavouriteStatus(opt.statusID)
+ } else {
+ err = gClient.FavouriteStatus(opt.statusID)
+ }
+ case "post": // toot
+ var s *madon.Status
+ s, err = toot(strings.Join(args, " "))
+ obj = s
+ default:
+ return errors.New("statusSubcommand: internal error")
+ }
+
+ if err != nil {
+ errPrint("Error: %s", err.Error())
+ return nil
+ }
+ if obj == nil {
+ return nil
+ }
+
+ p, err := getPrinter()
+ if err != nil {
+ return err
+ }
+ return p.PrintObj(obj, nil, "")
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/cmd/stream.go Wed Apr 19 19:08:47 2017 +0200
@@ -0,0 +1,136 @@
+// Copyright © 2017 Mikael Berthe <mikael@lilotux.net>
+//
+// Licensed under the MIT license.
+// Please see the LICENSE file is this directory.
+
+package cmd
+
+import (
+ "errors"
+ "io"
+
+ "github.com/spf13/cobra"
+
+ "github.com/McKael/madon"
+)
+
+/*
+var streamOpts struct {
+ local bool
+}
+*/
+
+// streamCmd represents the stream command
+var streamCmd = &cobra.Command{
+ Use: "stream [user|local|public|:HASHTAG]",
+ Short: "Listen to an event stream",
+ Long: `
+The stream command stays connected to the server and listen to a stream of
+events (user, local or federated).
+It can also get a hashtag-based stream if the keyword or prefixed with
+':' or '#'.`,
+ Example: ` madonctl stream # User timeline stream
+ madonctl stream local # Local timeline stream
+ madonctl stream public # Public timeline stream
+ madonctl stream :mastodon # Hashtag
+ madonctl stream #madonctl`,
+ RunE: streamRunE,
+ ValidArgs: []string{"user", "public"},
+ ArgAliases: []string{"home"},
+}
+
+func init() {
+ RootCmd.AddCommand(streamCmd)
+
+ //streamCmd.Flags().BoolVar(&streamOpts.local, "local", false, "Events from the local instance")
+}
+
+func streamRunE(cmd *cobra.Command, args []string) error {
+ streamName := "user"
+ tag := ""
+
+ if len(args) > 0 {
+ if len(args) != 1 {
+ return errors.New("too many parameters")
+ }
+ arg := args[0]
+ switch arg {
+ case "", "user":
+ case "public":
+ streamName = arg
+ case "local":
+ streamName = "public:local"
+ default:
+ if arg[0] != ':' && arg[0] != '#' {
+ return errors.New("invalid argument")
+ }
+ streamName = "hashtag"
+ tag = arg[1:]
+ if len(tag) == 0 {
+ return errors.New("empty hashtag")
+ }
+ }
+ }
+
+ if err := madonInit(true); err != nil {
+ return err
+ }
+
+ evChan := make(chan madon.StreamEvent, 10)
+ stop := make(chan bool)
+ done := make(chan bool)
+
+ // StreamListener(name string, hashTag string, events chan<- madon.StreamEvent, stopCh <-chan bool, doneCh chan<- bool) error
+ err := gClient.StreamListener(streamName, tag, evChan, stop, done)
+ if err != nil {
+ errPrint("Error: %s", err.Error())
+ return nil
+ }
+
+ p, err := getPrinter()
+ if err != nil {
+ close(stop)
+ <-done
+ close(evChan)
+ return err
+ }
+
+LISTEN:
+ for {
+ select {
+ case _, ok := <-done:
+ if !ok { // done is closed, end of streaming
+ done = nil
+ break LISTEN
+ }
+ case ev := <-evChan:
+ switch ev.Event {
+ case "error":
+ if ev.Error != nil {
+ if ev.Error == io.ErrUnexpectedEOF {
+ errPrint("The stream connection was unexpectedly closed")
+ continue
+ }
+ errPrint("Error event: [%s] %s", ev.Event, ev.Error)
+ continue
+ }
+ errPrint("Event: [%s]", ev.Event)
+ case "update":
+ s := ev.Data.(madon.Status)
+ p.PrintObj(&s, nil, "")
+ continue
+ case "notification":
+ n := ev.Data.(madon.Notification)
+ p.PrintObj(&n, nil, "")
+ continue
+ case "delete":
+ // TODO PrintObj ?
+ errPrint("Event: [%s] Status %d was deleted", ev.Event, ev.Data.(int))
+ default:
+ errPrint("Unhandled event: [%s] %T", ev.Event, ev.Data)
+ }
+ }
+ }
+ close(evChan)
+ return nil
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/cmd/timelines.go Wed Apr 19 19:08:47 2017 +0200
@@ -0,0 +1,62 @@
+// Copyright © 2017 Mikael Berthe <mikael@lilotux.net>
+//
+// Licensed under the MIT license.
+// Please see the LICENSE file is this directory.
+
+package cmd
+
+import (
+ "github.com/spf13/cobra"
+)
+
+var timelineOpts struct {
+ local bool
+}
+
+// timelineCmd represents the timelines command
+var timelineCmd = &cobra.Command{
+ Use: "timeline [home|public|:HASHTAG] [--local]",
+ Aliases: []string{"tl"},
+ Short: "Fetch a timeline",
+ Long: `
+The timeline command fetches a timeline (home, local or federated).
+It can also get a hashtag-based timeline if the keyword or prefixed with
+':' or '#'.`,
+ Example: ` madonctl timeline
+ madonctl timeline public --local
+ madonctl timeline :mastodon`,
+ RunE: timelineRunE,
+ ValidArgs: []string{"home", "public"},
+}
+
+func init() {
+ RootCmd.AddCommand(timelineCmd)
+
+ timelineCmd.Flags().BoolVar(&timelineOpts.local, "local", false, "Posts from the local instance")
+}
+
+func timelineRunE(cmd *cobra.Command, args []string) error {
+ opt := timelineOpts
+
+ tl := "home"
+ if len(args) > 0 {
+ tl = args[0]
+ }
+
+ // The home timeline is the only one requiring to be logged in
+ if err := madonInit(tl == "home"); err != nil {
+ return err
+ }
+
+ sl, err := gClient.GetTimelines(tl, opt.local)
+ if err != nil {
+ errPrint("Error: %s", err.Error())
+ return nil
+ }
+
+ p, err := getPrinter()
+ if err != nil {
+ return err
+ }
+ return p.PrintObj(sl, nil, "")
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/cmd/toot.go Wed Apr 19 19:08:47 2017 +0200
@@ -0,0 +1,79 @@
+// Copyright © 2017 Mikael Berthe <mikael@lilotux.net>
+//
+// Licensed under the MIT license.
+// Please see the LICENSE file is this directory.
+
+package cmd
+
+import (
+ "errors"
+
+ "github.com/spf13/cobra"
+
+ "github.com/McKael/madon"
+)
+
+// toot is a kind of alias for status post
+
+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.filePath, "file", "f", "", "Media attachment file name")
+ tootAliasCmd.Flags().IntVarP(&statusOpts.inReplyToID, "in-reply-to", "r", 0, "Status ID to reply to")
+}
+
+var tootAliasCmd = &cobra.Command{
+ Use: "toot",
+ Aliases: []string{"post", "pouet"},
+ Short: "Post a message (toot)",
+ Example: ` madonctl toot message
+ madonctl toot --spoiler Warning "Hello, World"
+ madonctl status post --media-ids ID1,ID2 "Here are the photos"
+ madonctl post --sensitive --file image.jpg Image`,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ if err := madonInit(true); err != nil {
+ return err
+ }
+ return statusSubcommandRunE("post", args)
+ },
+}
+
+func toot(tootText string) (*madon.Status, error) {
+ opt := statusOpts
+
+ switch opt.visibility {
+ case "", "direct", "private", "unlisted", "public":
+ // OK
+ default:
+ return nil, errors.New("invalid visibility argument value")
+ }
+
+ if opt.inReplyToID < 0 {
+ 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 opt.filePath != "" {
+ if len(ids) > 3 {
+ return nil, errors.New("too many media attachments")
+ }
+
+ fileMediaID, err := uploadFile(opt.filePath)
+ if err != nil {
+ return nil, errors.New("cannot attach media file: " + err.Error())
+ }
+ if fileMediaID > 0 {
+ ids = append(ids, fileMediaID)
+ }
+ }
+
+ return gClient.PostStatus(tootText, opt.inReplyToID, ids, opt.sensitive, opt.spoiler, opt.visibility)
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/cmd/version.go Wed Apr 19 19:08:47 2017 +0200
@@ -0,0 +1,27 @@
+// Copyright © 2017 Mikael Berthe <mikael@lilotux.net>
+//
+// Licensed under the MIT license.
+// Please see the LICENSE file is this directory.
+
+package cmd
+
+import (
+ "fmt"
+
+ "github.com/spf13/cobra"
+)
+
+// VERSION of the madonctl application
+var VERSION = "0.0.9"
+
+var versionCmd = &cobra.Command{
+ Use: "version",
+ Short: "Display " + AppName + " version",
+ Run: func(cmd *cobra.Command, args []string) {
+ fmt.Printf("This is %s version %s\n", AppName, VERSION)
+ },
+}
+
+func init() {
+ RootCmd.AddCommand(versionCmd)
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/main.go Wed Apr 19 19:08:47 2017 +0200
@@ -0,0 +1,29 @@
+// Copyright © 2017 Mikael Berthe <mikael@lilotux.net>
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+package main
+
+import (
+ "github.com/McKael/madonctl/cmd"
+)
+
+func main() {
+ cmd.RootCmd.Execute()
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/printer/README.md Wed Apr 19 19:08:47 2017 +0200
@@ -0,0 +1,28 @@
+# madonctl
+
+Golang command line interface for the Mastodon API
+
+[![license](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](https://raw.githubusercontent.com/McKael/madonctl/master/LICENSE)
+
+`madonctl` is a [Go](https://golang.org/) CLI tool to use the Mastondon REST API.
+
+## Installation
+
+To install the application (you need to have Go):
+
+ go get github.com/McKael/madonctl
+
+Some pre-built binaries are available on the home page (see below).
+
+## Usage
+
+This section has not been written yet.
+
+Please check the [Homepage](https://lilotux.net/~mikael/pub/madonctl/) for an
+introduction and a few examples.
+
+## References
+
+- [madon](https://github.com/McKael/madon), the Go library for Mastodon API
+- [Mastodon API documentation](https://github.com/tootsuite/documentation/blob/master/Using-the-API/API.md)
+- [Mastodon repository](https://github.com/tootsuite/mastodon)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/printer/json.go Wed Apr 19 19:08:47 2017 +0200
@@ -0,0 +1,34 @@
+// Copyright © 2017 Mikael Berthe <mikael@lilotux.net>
+//
+// Licensed under the MIT license.
+// Please see the LICENSE file is this directory.
+
+package printer
+
+import (
+ "encoding/json"
+ "io"
+ "os"
+)
+
+// JSONPrinter represents a JSON printer
+type JSONPrinter struct {
+}
+
+// NewPrinterJSON returns a JSON ResourcePrinter
+func NewPrinterJSON(option string) (*JSONPrinter, error) {
+ return &JSONPrinter{}, nil
+}
+
+// PrintObj sends the object as text to the writer
+// If the writer w is nil, standard output will be used.
+// For JSONPrinter, the option parameter is currently not used.
+func (p *JSONPrinter) PrintObj(obj interface{}, w io.Writer, option string) error {
+ if w == nil {
+ w = os.Stdout
+ }
+
+ jsonEncoder := json.NewEncoder(w)
+ //jsonEncoder.SetIndent("", " ")
+ return jsonEncoder.Encode(obj)
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/printer/plain.go Wed Apr 19 19:08:47 2017 +0200
@@ -0,0 +1,272 @@
+// Copyright © 2017 Mikael Berthe <mikael@lilotux.net>
+//
+// Licensed under the MIT license.
+// Please see the LICENSE file is this directory.
+
+package printer
+
+import (
+ "fmt"
+ "io"
+ "os"
+ "reflect"
+ "time"
+
+ "github.com/jaytaylor/html2text"
+
+ "github.com/McKael/madon"
+)
+
+// PlainPrinter is the default "plain text" printer
+type PlainPrinter struct {
+ Indent string
+ NoSubtitles bool
+}
+
+// NewPrinterPlain returns a plaintext ResourcePrinter
+// For PlainPrinter, the option parameter contains the indent prefix.
+func NewPrinterPlain(option string) (*PlainPrinter, error) {
+ indentInc := " "
+ if option != "" {
+ indentInc = option
+ }
+ return &PlainPrinter{Indent: indentInc}, nil
+}
+
+// PrintObj sends the object as text to the writer
+// If the writer w is nil, standard output will be used.
+// For PlainPrinter, the option parameter contains the initial indent.
+func (p *PlainPrinter) PrintObj(obj interface{}, w io.Writer, initialIndent string) error {
+ if w == nil {
+ w = os.Stdout
+ }
+ switch o := obj.(type) {
+ case []madon.Account, []madon.Attachment, []madon.Card, []madon.Context,
+ []madon.Instance, []madon.Mention, []madon.Notification,
+ []madon.Relationship, []madon.Report, []madon.Results,
+ []madon.Status, []madon.StreamEvent, []madon.Tag:
+ return p.plainForeach(o, w, initialIndent)
+ case *madon.Account:
+ return p.plainPrintAccount(o, w, initialIndent)
+ case madon.Account:
+ return p.plainPrintAccount(&o, w, initialIndent)
+ case *madon.Attachment:
+ return p.plainPrintAttachment(o, w, initialIndent)
+ case madon.Attachment:
+ return p.plainPrintAttachment(&o, w, initialIndent)
+ case *madon.Card:
+ return p.plainPrintCard(o, w, initialIndent)
+ case madon.Card:
+ return p.plainPrintCard(&o, w, initialIndent)
+ case *madon.Context:
+ return p.plainPrintContext(o, w, initialIndent)
+ case madon.Context:
+ return p.plainPrintContext(&o, w, initialIndent)
+ case *madon.Instance:
+ return p.plainPrintInstance(o, w, initialIndent)
+ case madon.Instance:
+ return p.plainPrintInstance(&o, w, initialIndent)
+ case *madon.Notification:
+ return p.plainPrintNotification(o, w, initialIndent)
+ case madon.Notification:
+ return p.plainPrintNotification(&o, w, initialIndent)
+ case *madon.Relationship:
+ return p.plainPrintRelationship(o, w, initialIndent)
+ case madon.Relationship:
+ return p.plainPrintRelationship(&o, w, initialIndent)
+ case *madon.Report:
+ return p.plainPrintReport(o, w, initialIndent)
+ case madon.Report:
+ return p.plainPrintReport(&o, w, initialIndent)
+ case *madon.Results:
+ return p.plainPrintResults(o, w, initialIndent)
+ case madon.Results:
+ return p.plainPrintResults(&o, w, initialIndent)
+ case *madon.Status:
+ return p.plainPrintStatus(o, w, initialIndent)
+ case madon.Status:
+ return p.plainPrintStatus(&o, w, initialIndent)
+ case *madon.UserToken:
+ return p.plainPrintUserToken(o, w, initialIndent)
+ case madon.UserToken:
+ return p.plainPrintUserToken(&o, w, initialIndent)
+ }
+ // TODO: Mention
+ // TODO: StreamEvent
+ // TODO: Tag
+
+ return fmt.Errorf("PlainPrinter not yet implemented for %T (try json or yaml...)", obj)
+}
+
+func (p *PlainPrinter) plainForeach(ol interface{}, w io.Writer, ii string) error {
+ switch reflect.TypeOf(ol).Kind() {
+ case reflect.Slice:
+ s := reflect.ValueOf(ol)
+
+ for i := 0; i < s.Len(); i++ {
+ o := s.Index(i).Interface()
+ if err := p.PrintObj(o, w, ii); err != nil {
+ return err
+ }
+ }
+ }
+ return nil
+}
+
+func html2string(h string) string {
+ t, err := html2text.FromString(h)
+ if err == nil {
+ return t
+ }
+ return h // Failed: return initial string
+}
+
+func indentedPrint(w io.Writer, indent string, title, skipIfEmpty bool, label string, format string, args ...interface{}) {
+ prefix := indent
+ if title {
+ prefix += "- "
+ } else {
+ prefix += " "
+ }
+ value := fmt.Sprintf(format, args...)
+ if !title && skipIfEmpty && len(value) == 0 {
+ return
+ }
+ fmt.Fprintf(w, "%s%s: %s\n", prefix, label, value)
+}
+
+func (p *PlainPrinter) plainPrintAccount(a *madon.Account, w io.Writer, indent string) error {
+ indentedPrint(w, indent, true, false, "Account ID", "%d (%s)", a.ID, a.Username)
+ indentedPrint(w, indent, false, false, "User ID", "%s", a.Acct)
+ indentedPrint(w, indent, false, false, "Display name", "%s", a.DisplayName)
+ indentedPrint(w, indent, false, false, "Creation date", "%v", a.CreatedAt.Local())
+ indentedPrint(w, indent, false, false, "URL", "%s", a.URL)
+ indentedPrint(w, indent, false, false, "Statuses count", "%d", a.StatusesCount)
+ indentedPrint(w, indent, false, false, "Followers count", "%d", a.FollowersCount)
+ indentedPrint(w, indent, false, false, "Following count", "%d", a.FollowingCount)
+ if a.Locked {
+ indentedPrint(w, indent, false, false, "Locked", "%v", a.Locked)
+ }
+ indentedPrint(w, indent, false, true, "User note", "%s", html2string(a.Note)) // XXX too long?
+ return nil
+}
+
+func (p *PlainPrinter) plainPrintAttachment(a *madon.Attachment, w io.Writer, indent string) error {
+ indentedPrint(w, indent, true, false, "Attachment ID", "%d", a.ID)
+ indentedPrint(w, indent, false, false, "Type", "%s", a.Type)
+ indentedPrint(w, indent, false, false, "Local URL", "%s", a.URL)
+ indentedPrint(w, indent, false, true, "Remote URL", "%s", a.RemoteURL)
+ indentedPrint(w, indent, false, true, "Preview URL", "%s", a.PreviewURL)
+ indentedPrint(w, indent, false, true, "Text URL", "%s", a.PreviewURL)
+ return nil
+}
+
+func (p *PlainPrinter) plainPrintCard(c *madon.Card, w io.Writer, indent string) error {
+ indentedPrint(w, indent, true, false, "Card title", "%s", c.Title)
+ indentedPrint(w, indent, false, true, "Description", "%s", c.Description)
+ indentedPrint(w, indent, false, true, "URL", "%s", c.URL)
+ indentedPrint(w, indent, false, true, "Image", "%s", c.Image)
+ return nil
+}
+
+func (p *PlainPrinter) plainPrintContext(c *madon.Context, w io.Writer, indent string) error {
+ indentedPrint(w, indent, true, false, "Context", "%d relative(s)", len(c.Ancestors)+len(c.Descendents))
+ if len(c.Ancestors) > 0 {
+ indentedPrint(w, indent, false, false, "Ancestors", "")
+ p.PrintObj(c.Ancestors, w, indent+p.Indent)
+ }
+ if len(c.Descendents) > 0 {
+ indentedPrint(w, indent, false, false, "Descendents", "")
+ p.PrintObj(c.Descendents, w, indent+p.Indent)
+ }
+ return nil
+}
+
+func (p *PlainPrinter) plainPrintInstance(i *madon.Instance, w io.Writer, indent string) error {
+ indentedPrint(w, indent, true, false, "Instance title", "%s", i.Title)
+ indentedPrint(w, indent, false, true, "Description", "%s", html2string(i.Description))
+ indentedPrint(w, indent, false, true, "URL", "%s", i.URI)
+ indentedPrint(w, indent, false, true, "Email", "%s", i.Email)
+ return nil
+}
+
+func (p *PlainPrinter) plainPrintNotification(n *madon.Notification, w io.Writer, indent string) error {
+ indentedPrint(w, indent, true, false, "Notification ID", "%d", n.ID)
+ indentedPrint(w, indent, false, false, "Type", "%s", n.Type)
+ indentedPrint(w, indent, false, false, "Timestamp", "%v", n.CreatedAt.Local())
+ if n.Account != nil {
+ indentedPrint(w, indent+p.Indent, true, false, "Account", "(%d) @%s - %s",
+ n.Account.ID, n.Account.Acct, n.Account.DisplayName)
+ }
+ if n.Status != nil {
+ p.plainPrintStatus(n.Status, w, indent+p.Indent)
+ }
+ return nil
+}
+
+func (p *PlainPrinter) plainPrintRelationship(r *madon.Relationship, w io.Writer, indent string) error {
+ indentedPrint(w, indent, true, false, "ID", "%d", r.ID)
+ indentedPrint(w, indent, false, false, "Following", "%v", r.Following)
+ indentedPrint(w, indent, false, false, "Followed-by", "%v", r.FollowedBy)
+ indentedPrint(w, indent, false, false, "Blocking", "%v", r.Blocking)
+ indentedPrint(w, indent, false, false, "Muting", "%v", r.Muting)
+ indentedPrint(w, indent, false, false, "Requested", "%v", r.Requested)
+ return nil
+}
+
+func (p *PlainPrinter) plainPrintReport(r *madon.Report, w io.Writer, indent string) error {
+ indentedPrint(w, indent, true, false, "Report ID", "%d", r.ID)
+ indentedPrint(w, indent, false, false, "Action taken", "%s", r.ActionTaken)
+ return nil
+}
+
+func (p *PlainPrinter) plainPrintResults(r *madon.Results, w io.Writer, indent string) error {
+ indentedPrint(w, indent, true, false, "Results", "%d account(s), %d status(es), %d hashtag(s)",
+ len(r.Accounts), len(r.Statuses), len(r.Hashtags))
+ if len(r.Accounts) > 0 {
+ indentedPrint(w, indent, false, false, "Accounts", "")
+ p.PrintObj(r.Accounts, w, indent+p.Indent)
+ }
+ if len(r.Statuses) > 0 {
+ indentedPrint(w, indent, false, false, "Statuses", "")
+ p.PrintObj(r.Statuses, w, indent+p.Indent)
+ }
+ if len(r.Hashtags) > 0 {
+ indentedPrint(w, indent, false, false, "Hashtags", "")
+ for _, tag := range r.Hashtags {
+ indentedPrint(w, indent+p.Indent, true, false, "Tag", "%s", tag)
+ }
+ }
+ return nil
+}
+
+func (p *PlainPrinter) plainPrintStatus(s *madon.Status, w io.Writer, indent string) error {
+ indentedPrint(w, indent, true, false, "Status ID", "%d", s.ID)
+ indentedPrint(w, indent, false, false, "From", "%s", s.Account.Acct)
+ indentedPrint(w, indent, false, false, "Timestamp", "%v", s.CreatedAt.Local())
+
+ if s.Reblog != nil {
+ indentedPrint(w, indent, false, false, "Reblogged from", "%s", s.Reblog.Account.Username)
+ return p.plainPrintStatus(s.Reblog, w, indent+p.Indent)
+ }
+
+ indentedPrint(w, indent, false, false, "Contents", "%s", html2string(s.Content))
+ if s.InReplyToID > 0 {
+ indentedPrint(w, indent, false, false, "In-Reply-To", "%d", s.InReplyToID)
+ }
+ if s.Reblogged {
+ indentedPrint(w, indent, false, false, "Reblogged", "%v", s.Reblogged)
+ }
+ indentedPrint(w, indent, false, false, "URL", "%s", s.URL)
+ return nil
+}
+
+func (p *PlainPrinter) plainPrintUserToken(s *madon.UserToken, w io.Writer, indent string) error {
+ indentedPrint(w, indent, true, false, "User token", "%s", s.AccessToken)
+ indentedPrint(w, indent, false, true, "Type", "%s", s.TokenType)
+ if s.CreatedAt != 0 {
+ indentedPrint(w, indent, false, true, "Timestamp", "%v", time.Unix(int64(s.CreatedAt), 0))
+ }
+ indentedPrint(w, indent, false, true, "Scope", "%s", s.Scope)
+ return nil
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/printer/printer.go Wed Apr 19 19:08:47 2017 +0200
@@ -0,0 +1,33 @@
+// Copyright © 2017 Mikael Berthe <mikael@lilotux.net>
+//
+// Licensed under the MIT license.
+// Please see the LICENSE file is this directory.
+
+package printer
+
+import (
+ "fmt"
+ "io"
+)
+
+// ResourcePrinter is an interface used to print objects.
+type ResourcePrinter interface {
+ // PrintObj receives a runtime object, formats it and prints it to a writer.
+ PrintObj(interface{}, io.Writer, string) error
+}
+
+// NewPrinter returns a ResourcePrinter for the specified kind of output.
+// It returns nil if the output is not supported.
+func NewPrinter(output, option string) (ResourcePrinter, error) {
+ switch output {
+ case "", "plain":
+ return NewPrinterPlain(option)
+ case "json":
+ return NewPrinterJSON(option)
+ case "yaml":
+ return NewPrinterYAML(option)
+ case "template":
+ return NewPrinterTemplate(option)
+ }
+ return nil, fmt.Errorf("unhandled output format")
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/printer/templateprinter.go Wed Apr 19 19:08:47 2017 +0200
@@ -0,0 +1,112 @@
+// Copyright © 2017 Mikael Berthe <mikael@lilotux.net>
+//
+// Licensed under the MIT license.
+// Please see the LICENSE file is this directory.
+
+package printer
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "os"
+ "reflect"
+ "text/template"
+
+ "github.com/McKael/madon"
+)
+
+// TemplatePrinter represents a Template printer
+type TemplatePrinter struct {
+ rawTemplate string
+ template *template.Template
+}
+
+// NewPrinterTemplate returns a Template ResourcePrinter
+// For TemplatePrinter, the option parameter contains the template string.
+func NewPrinterTemplate(option string) (*TemplatePrinter, error) {
+ tmpl := option
+ t, err := template.New("output").Parse(tmpl)
+ if err != nil {
+ return nil, err
+ }
+ return &TemplatePrinter{
+ rawTemplate: tmpl,
+ template: t,
+ }, nil
+}
+
+// PrintObj sends the object as text to the writer
+// If the writer w is nil, standard output will be used.
+func (p *TemplatePrinter) PrintObj(obj interface{}, w io.Writer, tmpl string) error {
+ if w == nil {
+ w = os.Stdout
+ }
+
+ if p.template == nil {
+ return fmt.Errorf("template not built")
+ }
+
+ switch ot := obj.(type) { // I wish I knew a better way...
+ case []madon.Account, []madon.Application, []madon.Attachment, []madon.Card,
+ []madon.Client, []madon.Context, []madon.Instance, []madon.Mention,
+ []madon.Notification, []madon.Relationship, []madon.Report,
+ []madon.Results, []madon.Status, []madon.StreamEvent, []madon.Tag:
+ return p.templateForeach(ot, w)
+ }
+
+ return p.templatePrintSingleObj(obj, w)
+}
+
+func (p *TemplatePrinter) templatePrintSingleObj(obj interface{}, w io.Writer) error {
+ // This code comes from Kubernetes.
+ data, err := json.Marshal(obj)
+ if err != nil {
+ return err
+ }
+ out := map[string]interface{}{}
+ if err := json.Unmarshal(data, &out); err != nil {
+ return err
+ }
+ if err = p.safeExecute(w, out); err != nil {
+ return fmt.Errorf("error executing template %q: %v", p.rawTemplate, err)
+ }
+ return nil
+}
+
+// safeExecute tries to execute the template, but catches panics and returns an error
+// should the template engine panic.
+// This code comes from Kubernetes.
+func (p *TemplatePrinter) safeExecute(w io.Writer, obj interface{}) error {
+ var panicErr error
+ // Sorry for the double anonymous function. There's probably a clever way
+ // to do this that has the defer'd func setting the value to be returned, but
+ // that would be even less obvious.
+ retErr := func() error {
+ defer func() {
+ if x := recover(); x != nil {
+ panicErr = fmt.Errorf("caught panic: %+v", x)
+ }
+ }()
+ return p.template.Execute(w, obj)
+ }()
+ if panicErr != nil {
+ return panicErr
+ }
+ return retErr
+}
+
+func (p *TemplatePrinter) templateForeach(ol interface{}, w io.Writer) error {
+ switch reflect.TypeOf(ol).Kind() {
+ case reflect.Slice:
+ s := reflect.ValueOf(ol)
+
+ for i := 0; i < s.Len(); i++ {
+ o := s.Index(i).Interface()
+ if err := p.templatePrintSingleObj(o, w); err != nil {
+ return err
+ }
+ }
+ }
+ return nil
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/printer/yaml.go Wed Apr 19 19:08:47 2017 +0200
@@ -0,0 +1,42 @@
+// Copyright © 2017 Mikael Berthe <mikael@lilotux.net>
+//
+// Licensed under the MIT license.
+// Please see the LICENSE file is this directory.
+
+package printer
+
+import (
+ "fmt"
+ "io"
+ "os"
+
+ "github.com/ghodss/yaml"
+)
+
+// YAMLPrinter represents a YAML printer
+type YAMLPrinter struct {
+}
+
+// NewPrinterYAML returns a YAML ResourcePrinter
+func NewPrinterYAML(option string) (*YAMLPrinter, error) {
+ return &YAMLPrinter{}, nil
+}
+
+// PrintObj sends the object as text to the writer
+// If the writer w is nil, standard output will be used.
+// For YAMLPrinter, the option parameter is currently not used.
+func (p *YAMLPrinter) PrintObj(obj interface{}, w io.Writer, option string) error {
+ if w == nil {
+ w = os.Stdout
+ }
+
+ //yamlEncoder := yaml.NewEncoder(w)
+ //return yamlEncoder.Encode(obj)
+
+ output, err := yaml.Marshal(obj)
+ if err != nil {
+ return err
+ }
+ _, err = fmt.Fprint(w, string(output))
+ return err
+}