mirror of https://github.com/muesli/mastotool
Use cobra to parse & split functionality up into individual commands
This commit is contained in:
parent
e2f228ce24
commit
968a7e3d82
45
README.md
45
README.md
|
@ -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
154
main.go
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
95
stats.go
95
stats.go
|
@ -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)
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue