Use cobra to parse & split functionality up into individual commands

This commit is contained in:
Christian Muehlhaeuser 2019-08-11 21:46:42 +02:00
parent e2f228ce24
commit 968a7e3d82
No known key found for this signature in database
GPG Key ID: 3CF9FA45CA1EBB7E
4 changed files with 216 additions and 154 deletions

View File

@ -15,22 +15,45 @@ To install mastotool, simply run:
## Usage ## Usage
``` ```
$ mastotool -help Usage:
Usage of ./mastotool: mastotool [command]
-columns int
displays tables with N columns (default 80) Available Commands:
-config string help Help about any command
uses the specified config file (default "mastodon.json") search searches your toots
-recent int stats generates statistics about your account
only account for the N most recent toots (excl replies & boosts)
-top int Flags:
shows the top N items in each category (default 10) -c, --config string uses the specified config file (default "mastodon.json")
-h, --help help for mastotool
Use "mastotool [command] --help" for more information about a command.
```
### Generate Statistics
```
Usage:
mastotool stats [flags]
Flags:
--columns int displays tables with N columns (default 80)
-h, --help help for stats
-r, --recent int only account for the N most recent toots (excl replies & boosts)
-t, --top int shows the top N items in each category (default 10)
```
### Search
```
Usage:
mastotool search <string>
``` ```
## Example ## Example
``` ```
$ mastotool $ mastotool stats
Which instance to connect to: https://mastodon.social Which instance to connect to: https://mastodon.social
Username (email): some_user@domain.tld Username (email): some_user@domain.tld
Password: ******** Password: ********

154
main.go
View File

@ -3,27 +3,25 @@ package main
import ( import (
"bufio" "bufio"
"context" "context"
"flag"
"fmt" "fmt"
"math"
"os" "os"
"strings"
"time"
mastodon "github.com/mattn/go-mastodon" mastodon "github.com/mattn/go-mastodon"
"github.com/muesli/goprogressbar" "github.com/spf13/cobra"
) )
var ( var (
client *mastodon.Client client *mastodon.Client
self *mastodon.Account self *mastodon.Account
configFile string
topN = flag.Int("top", 10, "shows the top N items in each category") // RootCmd is the core command used for cli-arg parsing
maxToots = flag.Int("recent", 0, "only account for the N most recent toots (excl replies & boosts)") RootCmd = &cobra.Command{
columns = flag.Int("columns", 80, "displays tables with N columns") Use: "mastotool",
configFile = flag.String("config", "mastodon.json", "uses the specified config file") Short: "mastotool offers a collection of tools to work with your Mastodon account",
search = flag.String("search", "", "searches toots containing string") SilenceErrors: true,
// user = flag.String("user", "@fribbledom@mastodon.social", "shows stats for this user") SilenceUsage: true,
}
) )
func registerApp(config *Config) (string, error) { func registerApp(config *Config) (string, error) {
@ -47,7 +45,7 @@ func registerApp(config *Config) (string, error) {
func initClient() error { func initClient() error {
var err error var err error
var instance, token, redirectURI, authURI, id, secret string var instance, token, redirectURI, authURI, id, secret string
config, err := LoadConfig(*configFile) config, err := LoadConfig(configFile)
if err == nil { if err == nil {
instance = config.Value("instance").(string) instance = config.Value("instance").(string)
id = config.Value("id").(string) id = config.Value("id").(string)
@ -100,7 +98,7 @@ func initClient() error {
} }
config.Set("token", mConfig.AccessToken) config.Set("token", mConfig.AccessToken)
err = config.Save(*configFile) err = config.Save(configFile)
if err != nil { if err != nil {
return fmt.Errorf("Can't save config: %s", err) return fmt.Errorf("Can't save config: %s", err)
} }
@ -109,144 +107,22 @@ func initClient() error {
return nil return nil
} }
func searchToots() error {
pb := &goprogressbar.ProgressBar{
Text: fmt.Sprintf("Searching toots for %s", *search),
Total: self.StatusesCount,
PrependTextFunc: func(p *goprogressbar.ProgressBar) string {
return fmt.Sprintf("%d of %d", p.Current, int64(math.Max(float64(p.Current), float64(self.StatusesCount))))
},
Current: 0,
Width: 40,
}
var pg mastodon.Pagination
for {
pg.SinceID = ""
pg.MinID = ""
pg.Limit = 40
statuses, err := client.GetAccountStatuses(context.Background(), self.ID, &pg)
if err != nil {
return fmt.Errorf("Can't retrieve statuses: %s", err)
}
abort := false
for _, s := range statuses {
if strings.Contains(strings.ToLower(cleanupContent(s.Content)), *search) {
fmt.Println("\nFound toot:", cleanupContent(s.Content))
fmt.Println()
}
pb.Current += 1
pb.LazyPrint()
}
// For some reason, either because it's Pleroma or because I have too few toots,
// `pg.MaxID` never equals `""` and we get stuck looping forever. Add a simple
// break condition on "no statuses fetched" to avoid the issue.
if abort || pg.MaxID == "" || len(statuses) == 0 {
break
}
}
return nil
}
func gatherStats() error {
stats := &stats{
DaysActive: int(time.Since(self.CreatedAt).Hours() / 24),
Followers: self.FollowersCount,
Following: self.FollowingCount,
Toots: make(map[string]*tootStat),
Tags: make(map[string]*tootStat),
Replies: make(map[string]*tootStat),
Mentions: make(map[string]int64),
Boosts: make(map[string]int64),
Responses: make(map[string]int64),
}
pb := &goprogressbar.ProgressBar{
Text: fmt.Sprintf("Loading toots for %s", self.Username),
Total: self.StatusesCount,
PrependTextFunc: func(p *goprogressbar.ProgressBar) string {
return fmt.Sprintf("%d of %d", p.Current, int64(math.Max(float64(p.Current), float64(self.StatusesCount))))
},
Current: 0,
Width: 40,
}
var pg mastodon.Pagination
for {
pg.SinceID = ""
pg.MinID = ""
pg.Limit = 40
statuses, err := client.GetAccountStatuses(context.Background(), self.ID, &pg)
if err != nil {
return fmt.Errorf("Can't retrieve statuses: %s", err)
}
abort := false
for _, s := range statuses {
err = parseToot(s, stats)
if err != nil {
return fmt.Errorf("Can't parse toot: %s", err)
}
pb.Current += 1
pb.LazyPrint()
if *maxToots > 0 && len(stats.Toots) >= *maxToots {
abort = true
break
}
}
// For some reason, either because it's Pleroma or because I have too few toots,
// `pg.MaxID` never equals `""` and we get stuck looping forever. Add a simple
// break condition on "no statuses fetched" to avoid the issue.
if abort || pg.MaxID == "" || len(statuses) == 0 {
break
}
}
// print out stats we gathered
fmt.Printf("\n\n")
printAccountStats(stats)
printInteractionStats(stats)
printTootStats(stats)
printTagStats(stats)
return nil
}
func main() { func main() {
flag.Parse() RootCmd.PersistentFlags().StringVarP(&configFile, "config", "c", "mastodon.json", "uses the specified config file")
*search = strings.ToLower(*search)
if err := initClient(); err != nil { if err := initClient(); err != nil {
fmt.Println(err) fmt.Println(err)
os.Exit(1) os.Exit(1)
} }
var err error var err error
self, err = client.GetAccountCurrentUser(context.Background()) self, err = client.GetAccountCurrentUser(context.Background())
if err != nil { if err != nil {
fmt.Printf("Can't retrieve user: %s\n", err) fmt.Printf("Can't retrieve user: %s\n", err)
os.Exit(1) os.Exit(1)
} }
/*
accounts, err := client.AccountsSearch(context.Background(), *user, 1)
if err != nil {
panic(err)
}
self = accounts[0]
*/
if len(*search) > 0 { if err := RootCmd.Execute(); err != nil {
err = searchToots()
} else {
err = gatherStats()
}
if err != nil {
fmt.Println(err) fmt.Println(err)
os.Exit(1) os.Exit(-1)
} }
} }

72
search.go Normal file
View File

@ -0,0 +1,72 @@
package main
import (
"context"
"fmt"
"math"
"strings"
mastodon "github.com/mattn/go-mastodon"
"github.com/muesli/goprogressbar"
"github.com/spf13/cobra"
)
var (
searchCmd = &cobra.Command{
Use: "search <string>",
Short: "searches your toots",
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return fmt.Errorf("search requires a search token")
}
return search(args[0])
},
}
)
func search(token string) error {
pb := &goprogressbar.ProgressBar{
Text: fmt.Sprintf("Searching toots for %s", token),
Total: self.StatusesCount,
PrependTextFunc: func(p *goprogressbar.ProgressBar) string {
return fmt.Sprintf("%d of %d", p.Current, int64(math.Max(float64(p.Current), float64(self.StatusesCount))))
},
Current: 0,
Width: 40,
}
var pg mastodon.Pagination
for {
pg.SinceID = ""
pg.MinID = ""
pg.Limit = 40
statuses, err := client.GetAccountStatuses(context.Background(), self.ID, &pg)
if err != nil {
return fmt.Errorf("Can't retrieve statuses: %s", err)
}
abort := false
for _, s := range statuses {
if strings.Contains(strings.ToLower(cleanupContent(s.Content)), token) {
fmt.Println("\nFound toot:", cleanupContent(s.Content))
fmt.Println()
}
pb.Current += 1
pb.LazyPrint()
}
// For some reason, either because it's Pleroma or because I have too few toots,
// `pg.MaxID` never equals `""` and we get stuck looping forever. Add a simple
// break condition on "no statuses fetched" to avoid the issue.
if abort || pg.MaxID == "" || len(statuses) == 0 {
break
}
}
return nil
}
func init() {
RootCmd.AddCommand(searchCmd)
}

View File

@ -4,16 +4,32 @@ import (
"context" "context"
"fmt" "fmt"
"html" "html"
"math"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
"time"
mastodon "github.com/mattn/go-mastodon" mastodon "github.com/mattn/go-mastodon"
"github.com/microcosm-cc/bluemonday" "github.com/microcosm-cc/bluemonday"
"github.com/muesli/goprogressbar"
"github.com/muesli/gotable" "github.com/muesli/gotable"
"github.com/spf13/cobra"
) )
var ( var (
topN int
maxToots int
columns int
statsCmd = &cobra.Command{
Use: "stats",
Short: "generates statistics about your account",
RunE: func(cmd *cobra.Command, args []string) error {
return gatherStats()
},
}
stripper = bluemonday.StrictPolicy() stripper = bluemonday.StrictPolicy()
) )
@ -42,6 +58,72 @@ type stats struct {
Responses map[string]int64 Responses map[string]int64
} }
func gatherStats() error {
stats := &stats{
DaysActive: int(time.Since(self.CreatedAt).Hours() / 24),
Followers: self.FollowersCount,
Following: self.FollowingCount,
Toots: make(map[string]*tootStat),
Tags: make(map[string]*tootStat),
Replies: make(map[string]*tootStat),
Mentions: make(map[string]int64),
Boosts: make(map[string]int64),
Responses: make(map[string]int64),
}
pb := &goprogressbar.ProgressBar{
Text: fmt.Sprintf("Loading toots for %s", self.Username),
Total: self.StatusesCount,
PrependTextFunc: func(p *goprogressbar.ProgressBar) string {
return fmt.Sprintf("%d of %d", p.Current, int64(math.Max(float64(p.Current), float64(self.StatusesCount))))
},
Current: 0,
Width: 40,
}
var pg mastodon.Pagination
for {
pg.SinceID = ""
pg.MinID = ""
pg.Limit = 40
statuses, err := client.GetAccountStatuses(context.Background(), self.ID, &pg)
if err != nil {
return fmt.Errorf("Can't retrieve statuses: %s", err)
}
abort := false
for _, s := range statuses {
err = parseToot(s, stats)
if err != nil {
return fmt.Errorf("Can't parse toot: %s", err)
}
pb.Current += 1
pb.LazyPrint()
if maxToots > 0 && len(stats.Toots) >= maxToots {
abort = true
break
}
}
// For some reason, either because it's Pleroma or because I have too few toots,
// `pg.MaxID` never equals `""` and we get stuck looping forever. Add a simple
// break condition on "no statuses fetched" to avoid the issue.
if abort || pg.MaxID == "" || len(statuses) == 0 {
break
}
}
// print out stats we gathered
fmt.Printf("\n\n")
printAccountStats(stats)
printInteractionStats(stats)
printTootStats(stats)
printTagStats(stats)
return nil
}
func cleanupContent(content string) string { func cleanupContent(content string) string {
// clean up toot for terminal output // clean up toot for terminal output
content = strings.Replace(content, "<br>", "\n", -1) content = strings.Replace(content, "<br>", "\n", -1)
@ -164,14 +246,14 @@ func printTable(cols []string, emptyText string, data []kv) {
return data[i].Value > data[j].Value return data[i].Value > data[j].Value
}) })
col1 := *columns - len(cols[1]) col1 := columns - len(cols[1])
col2 := len(cols[1]) col2 := len(cols[1])
tab := gotable.NewTable(cols, tab := gotable.NewTable(cols,
[]int64{-int64(col1), int64(col2)}, []int64{-int64(col1), int64(col2)},
emptyText) emptyText)
for i, kv := range data { for i, kv := range data {
if i >= *topN { if i >= topN {
break break
} }
if len(kv.Key) > col1-4 { if len(kv.Key) > col1-4 {
@ -320,3 +402,12 @@ func printTagStats(stats *stats) {
"No toots found.", "No toots found.",
tags, tagStats, SortByBoosts) tags, tagStats, SortByBoosts)
} }
func init() {
statsCmd.Flags().IntVarP(&topN, "top", "t", 10, "shows the top N items in each category")
statsCmd.Flags().IntVarP(&maxToots, "recent", "r", 0, "only account for the N most recent toots (excl replies & boosts)")
statsCmd.Flags().IntVarP(&columns, "columns", "", 80, "displays tables with N columns")
// user = flag.String("user", "@fribbledom@mastodon.social", "shows stats for this user")
RootCmd.AddCommand(statsCmd)
}