diff --git a/.gitignore b/.gitignore
index 722d5e7..adfefb3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
.vscode
+dist/
diff --git a/client/authorization.go b/client/authorization.go
new file mode 100644
index 0000000..014f04d
--- /dev/null
+++ b/client/authorization.go
@@ -0,0 +1,123 @@
+package client
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "net"
+ "net/http"
+ "os"
+ "os/signal"
+ "syscall"
+ "time"
+
+ "github.com/mattn/go-mastodon"
+ "github.com/pkg/browser"
+ log "github.com/sirupsen/logrus"
+)
+
+func RegisterNewClient(serverName string) (*mastodon.Client, error) {
+ done := make(chan os.Signal, 1)
+
+ // Start temporary server to capture auth code
+ var authCode string
+
+ // Listen on default port
+ listener := *newListener()
+ listenerPort := listener.Addr().(*net.TCPAddr).Port
+ listenerHost := fmt.Sprintf("%s:%v", "localhost", listenerPort)
+
+ go func() {
+ mux := http.NewServeMux()
+
+ // Handle client-side redirect to extract 'auth' code, and close window
+ mux.HandleFunc("/auth", func(w http.ResponseWriter, r *http.Request) {
+ authCode = r.URL.Query().Get("code")
+ w.Write([]byte(`
+
+
+ It is safe to close this window..
+
+
+ `))
+ done <- os.Interrupt
+ })
+
+ log.Debugf("listening for auth response on port %v", listenerPort)
+ if err := http.Serve(listener, mux); err != nil && err != http.ErrServerClosed {
+ log.Fatalf("authentication listener failed to start: %s\n", err)
+ }
+ }()
+
+ signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
+ startTimeout(done)
+
+ app, err := mastodon.RegisterApp(context.Background(), &mastodon.AppConfig{
+ Server: serverURL(serverName),
+ ClientName: "Links From Bookmarks",
+ Scopes: "read:bookmarks read:favourites",
+ Website: "https://github.com/ivan3bx/proma",
+ RedirectURIs: fmt.Sprintf("http://%s/auth", listenerHost),
+ })
+
+ if err != nil {
+ return nil, err
+ }
+
+ log.Debugf("client-id : %s", app.ClientID)
+ log.Debugf("client-secret: %s", app.ClientSecret)
+
+ if err := browser.OpenURL(app.AuthURI); err != nil {
+ return nil, err
+ }
+
+ <-done
+
+ log.Debug("listener stopped")
+
+ if authCode == "" {
+ return nil, errors.New("auth code was not present, or was blank")
+ }
+
+ // Create mastodon client
+ client := mastodon.NewClient(&mastodon.Config{
+ Server: serverURL(serverName),
+ ClientID: app.ClientID,
+ ClientSecret: app.ClientSecret,
+ })
+
+ if err = client.AuthenticateToken(context.Background(), authCode, app.RedirectURI); err != nil {
+ return nil, err
+ }
+
+ log.Infof("authenticated to %s\n", client.Config.Server)
+
+ return client, nil
+}
+
+func startTimeout(done chan<- os.Signal) {
+ timer := time.NewTimer(time.Second * 60)
+
+ go func() {
+ <-timer.C
+ log.Debug("timeout exceeded. canceling authentication")
+ done <- os.Interrupt
+ }()
+}
+
+func serverURL(serverName string) string {
+ return fmt.Sprintf("https://%s", serverName)
+}
+
+func newListener() *net.Listener {
+ listener, err := net.Listen("tcp", "localhost:3334")
+
+ if err != nil {
+ // attempt to use next available port
+ listener, err = net.Listen("tcp", "localhost:0")
+ if err != nil {
+ panic(err)
+ }
+ }
+ return &listener
+}
diff --git a/client/authorization_test.go b/client/authorization_test.go
new file mode 100644
index 0000000..7845ae2
--- /dev/null
+++ b/client/authorization_test.go
@@ -0,0 +1,25 @@
+package client
+
+import "testing"
+
+func TestServerURL(t *testing.T) {
+ testCases := []struct {
+ name string
+ input string
+ expected string
+ }{
+ {
+ name: "success",
+ input: "mastodon.social",
+ expected: "https://mastodon.social",
+ },
+ }
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ actual := serverURL(tc.input)
+ if actual != tc.expected {
+ t.Errorf("expected URL to match (%v / %v)\n", tc.expected, actual)
+ }
+ })
+ }
+}
diff --git a/cmd/authenticate.go b/cmd/authenticate.go
index 9a07cba..bea0b44 100644
--- a/cmd/authenticate.go
+++ b/cmd/authenticate.go
@@ -5,19 +5,11 @@ package cmd
import (
"bufio"
- "context"
- "errors"
- "fmt"
- "net"
- "net/http"
"os"
- "os/signal"
- "syscall"
- "time"
"github.com/fatih/color"
- "github.com/mattn/go-mastodon"
- "github.com/pkg/browser"
+ "github.com/ivan3bx/proma/client"
+ log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
@@ -38,17 +30,17 @@ Examples:
and saves an AccessToken to the config file.
`,
Run: func(cmd *cobra.Command, args []string) {
- fmt.Printf("Server: %s\n", yellow(serverName))
- fmt.Printf("Re-run this command with '-server' to use a different server.\n\n")
- fmt.Printf("This will launch a browser window in order to authorize this app.\n")
- fmt.Println("Hit to continue...")
+ log.Infof("Server: %s\n", yellow(serverName))
+ log.Infof("Re-run this command with '-server' to use a different server.\n\n")
+ log.Infof("This will launch a browser window in order to authorize this app.\n")
+ log.Infof("Hit to continue...")
bufio.NewReader(os.Stdin).ReadBytes('\n')
var err error
- client, err = RegisterNewClient()
+ c, err := client.RegisterNewClient(serverName)
cobra.CheckErr(err)
- v.Set(serverName, client.Config)
+ v.Set(serverName, c.Config)
cobra.CheckErr(v.WriteConfig())
},
@@ -58,116 +50,9 @@ func init() {
rootCmd.AddCommand(authenticateCmd)
}
-func RegisterNewClient() (*mastodon.Client, error) {
- done := make(chan os.Signal, 1)
-
- // Start temporary server to capture auth code
- var authCode string
-
- // Listen on default port
- listener := *newListener()
- listenerPort := listener.Addr().(*net.TCPAddr).Port
- listenerHost := fmt.Sprintf("%s:%v", "localhost", listenerPort)
-
- go func() {
- mux := http.NewServeMux()
-
- // Handle client-side redirect to extract 'auth' code, and close window
- mux.HandleFunc("/auth", func(w http.ResponseWriter, r *http.Request) {
- authCode = r.URL.Query().Get("code")
- w.Write([]byte(`
-
-
- It is safe to close this window..
-
-
- `))
- done <- os.Interrupt
- })
-
- debugf("listening for auth response on port %v", listenerPort)
- if err := http.Serve(listener, mux); err != nil && err != http.ErrServerClosed {
- fmt.Printf("authentication listener failed to start: %s\n", err)
- os.Exit(1)
- }
- }()
-
- signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
- startTimeout(done)
-
- app, err := mastodon.RegisterApp(context.Background(), &mastodon.AppConfig{
- Server: serverURL(),
- ClientName: "Links From Bookmarks",
- Scopes: "read:bookmarks read:favourites",
- Website: "https://github.com/ivan3bx/proma",
- RedirectURIs: fmt.Sprintf("http://%s/auth", listenerHost),
- })
-
- if err != nil {
- return nil, err
- }
-
- debugf("client-id : %s", app.ClientID)
- debugf("client-secret: %s", app.ClientSecret)
-
- if err := browser.OpenURL(app.AuthURI); err != nil {
- return nil, err
- }
-
- <-done
-
- debug("listener stopped")
-
- if authCode == "" {
- return nil, errors.New("auth code was not present, or was blank")
- }
-
- // Create mastodon client
- client := mastodon.NewClient(&mastodon.Config{
- Server: serverURL(),
- ClientID: app.ClientID,
- ClientSecret: app.ClientSecret,
- })
-
- if err = client.AuthenticateToken(context.Background(), authCode, app.RedirectURI); err != nil {
- return nil, err
- }
-
- fmt.Printf("authenticated to %s\n", client.Config.Server)
-
- return client, nil
-}
-
-func startTimeout(done chan<- os.Signal) {
- timer := time.NewTimer(time.Second * 60)
-
- go func() {
- <-timer.C
- debug("timeout exceeded. canceling authentication")
- done <- os.Interrupt
- }()
-}
-
-func serverURL() string {
- return fmt.Sprintf("https://%s", serverName)
-}
-
-func newListener() *net.Listener {
- listener, err := net.Listen("tcp", "localhost:3334")
-
- if err != nil {
- // attempt to use next available port
- listener, err = net.Listen("tcp", "localhost:0")
- if err != nil {
- panic(err)
- }
- }
- return &listener
-}
-
func requireClient(cmd *cobra.Command, args []string) {
- if client == nil {
- fmt.Printf("See 'auth -h' to authenticate\n")
+ if mClient == nil {
+ log.Infof("See 'auth -h' to authenticate\n")
os.Exit(1)
}
}
diff --git a/cmd/links.go b/cmd/links.go
index f3c1e57..04e309d 100644
--- a/cmd/links.go
+++ b/cmd/links.go
@@ -10,6 +10,8 @@ import (
"os"
"strings"
+ log "github.com/sirupsen/logrus"
+
"github.com/PuerkitoBio/goquery"
"github.com/mattn/go-mastodon"
"github.com/spf13/cobra"
@@ -29,7 +31,7 @@ var linksCmd = &cobra.Command{
Collects links embedded in the content of your saved bookmarks.`,
PreRun: requireClient,
Run: func(cmd *cobra.Command, args []string) {
- st, err := client.GetBookmarks(cmd.Context(), &mastodon.Pagination{Limit: limit})
+ st, err := mClient.GetBookmarks(cmd.Context(), &mastodon.Pagination{Limit: limit})
cobra.CheckErr(err)
outputLinks(st)
@@ -60,7 +62,7 @@ func outputLinks(status []*mastodon.Status) {
if href, ok := s.Attr("href"); ok {
if strings.HasPrefix(href, origin) {
- debug("skipping internal href: ", href)
+ log.Debug("skipping internal href: ", href)
return
}
diff --git a/cmd/root.go b/cmd/root.go
index a5c5e5b..86b3e77 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -25,12 +25,20 @@ import (
"fmt"
"os"
+ log "github.com/sirupsen/logrus"
+
"github.com/mattn/go-mastodon"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"golang.org/x/exp/maps"
)
+type LogFormatter struct{}
+
+func (f *LogFormatter) Format(entry *log.Entry) ([]byte, error) {
+ return []byte(fmt.Sprintf("%s\n", entry.Message)), nil
+}
+
// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "proma",
@@ -56,7 +64,7 @@ var (
serverName string
cfgFile string
verbose bool
- client *mastodon.Client
+ mClient *mastodon.Client
)
func init() {
@@ -64,7 +72,20 @@ func init() {
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose mode")
rootCmd.PersistentFlags().StringVarP(&serverName, "server", "s", "mastodon.social", "server name")
- cobra.OnInitialize(initConfig)
+ cobra.OnInitialize(initLogging, initConfig)
+}
+
+func initLogging() {
+ if verbose {
+ log.SetFormatter(&log.TextFormatter{
+ DisableTimestamp: true,
+ PadLevelText: true,
+ })
+ log.Info("Debug logs enabled")
+ log.SetLevel(log.DebugLevel)
+ } else {
+ log.SetFormatter(&LogFormatter{})
+ }
}
func initConfig() {
@@ -85,15 +106,15 @@ func initConfig() {
err := v.ReadInConfig()
if err != nil {
- debugf("creating config file: %v", v.ConfigFileUsed())
+ log.Debugf("creating config file: %v", v.ConfigFileUsed())
cobra.CheckErr(v.SafeWriteConfig())
}
- debugf("reading config file: %v", v.ConfigFileUsed())
+ log.Debugf("reading config file: %v", v.ConfigFileUsed())
if len(v.AllSettings()) > 0 && !rootCmd.Flags().Changed("server") {
serverName = maps.Keys(v.AllSettings())[0]
- debug("using default serverName: ", serverName)
+ log.Info("using default serverName: ", serverName)
}
if v.InConfig(serverName) {
@@ -106,20 +127,8 @@ func initConfig() {
AccessToken: configValues["accesstoken"],
}
- client = mastodon.NewClient(clientConfig)
+ mClient = mastodon.NewClient(clientConfig)
} else {
- fmt.Printf("Credentials missing for server '%s'\n\n", serverName)
- }
-}
-
-func debug(a ...any) {
- if verbose {
- fmt.Println(a...)
- }
-}
-
-func debugf(format string, a ...any) {
- if verbose {
- debug(fmt.Sprintf(format, a...))
+ log.Errorf("Credentials missing for server '%s'\n", serverName)
}
}
diff --git a/go.mod b/go.mod
index db5d81a..29a44d1 100644
--- a/go.mod
+++ b/go.mod
@@ -7,6 +7,7 @@ require (
github.com/fatih/color v1.13.0
github.com/mattn/go-mastodon v0.0.5
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8
+ github.com/sirupsen/logrus v1.9.0
github.com/spf13/cobra v1.6.1
github.com/spf13/viper v1.14.0
golang.org/x/exp v0.0.0-20221106115401-f9659909a136
@@ -31,7 +32,7 @@ require (
github.com/subosito/gotenv v1.4.1 // indirect
github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 // indirect
golang.org/x/net v0.1.0 // indirect
- golang.org/x/sys v0.1.0 // indirect
+ golang.org/x/sys v0.2.0 // indirect
golang.org/x/text v0.4.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
diff --git a/go.sum b/go.sum
index f434e11..f731aee 100644
--- a/go.sum
+++ b/go.sum
@@ -168,6 +168,8 @@ github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
+github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/afero v1.9.2 h1:j49Hj62F0n+DaZ1dDCvhABaPNSGNkt32oRFxI33IEMw=
github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
@@ -337,9 +339,10 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
-golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
+golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=