From 7d2f7157154eb92e67636fcb23eb828326735354 Mon Sep 17 00:00:00 2001 From: Adam Shamblin Date: Fri, 16 Dec 2022 16:45:09 -0700 Subject: [PATCH] working login command, remove hard-coded host --- README.md | 2 +- cmd/application.go | 4 ---- cmd/login.go | 22 ++++++++++++++----- pkg/mastodon/account.go | 37 ++++++++++++++++++++++--------- pkg/mastodon/application.go | 44 ++++++++++++++++++++++++------------- 5 files changed, 74 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index e83c454..35b30ec 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ There are a lot of people who have moved to the Fediverse from Twitter, and for some this is very exciting. The increase in volume has affected the administrators more than anyone, no -doubht, but the users have also been affected. While Mastodon's filters and feed +doubt, but the users have also been affected. While Mastodon's filters and feed settings are exceedingly powerful, there are a few more things I'd like to be able to do with my Fedi data. diff --git a/cmd/application.go b/cmd/application.go index 50e29d5..31c3d12 100644 --- a/cmd/application.go +++ b/cmd/application.go @@ -6,10 +6,6 @@ func init() { rootCmd.AddCommand(cmdApp) } -/* -dead-tooter app create --client-name "" -*/ - var cmdApp = &cobra.Command{ Use: "application", Short: "Register and perform application-level actions", diff --git a/cmd/login.go b/cmd/login.go index e6b19ab..da35918 100644 --- a/cmd/login.go +++ b/cmd/login.go @@ -5,7 +5,13 @@ import ( "github.com/spf13/cobra" ) +var host string + func init() { + loginCmd.Flags().StringVarP(&host, + "host", "H", "", "Mastodon host where your account lives.") + loginCmd.MarkFlagRequired("host") + rootCmd.AddCommand(loginCmd) } @@ -14,6 +20,7 @@ var loginCmd = &cobra.Command{ Short: "Login to Mastodon", Long: "Initiate login to your Mastodon server of choice", Run: func(cmd *cobra.Command, args []string) { + login() }, } @@ -26,21 +33,26 @@ func login() { Scopes: "read write follow push", Website: "https://dead-tooter.vexingworkshop.com", } - app, err = mastodon.Create(client) + + app, err = mastodon.Create(host, client) + if err != nil { + panic(err.Error()) + } + + err = app.Save() if err != nil { panic(err.Error()) } - app.Save() } var account mastodon.Account - code := account.Authorize(app) - token, err := account.RequestToken(app, code) + code := account.Authorize(host, app) + token, err := account.RequestToken(host, app, code) if err != nil { panic(err.Error()) } - err = account.VerifyCredentials(token) + err = account.VerifyCredentials(host, token) if err != nil { panic(err.Error()) } diff --git a/pkg/mastodon/account.go b/pkg/mastodon/account.go index 34c5ffd..690ef32 100644 --- a/pkg/mastodon/account.go +++ b/pkg/mastodon/account.go @@ -10,6 +10,7 @@ import ( "os/exec" ) +// Account represents a user of Mastodon and their associated profile. type Account struct { ID string `json:"id"` UserName string `json:"username"` @@ -33,6 +34,8 @@ type Account struct { Fields []Field `json:"fields"` } +// Source is an extra attribute that contains source values to be used with API +// methods that verify and update credentials. type Source struct { Privacy string `json:"privacy"` Sensitive bool `json:"sensitive"` @@ -42,27 +45,31 @@ type Source struct { FollowRequestsCount int `json:"follow_requests_count"` } +// Field stores custom field data on the user account. type Field struct { Name string `json:"name"` Value string `json:"value"` VerifiedAt string `json:"verified_at"` } +// Emoji is the emoji portion of the user account. type Emoji struct { ShortCode string `json:"shortcode"` - Url string `json:"url"` - StaticUrl string `json:"static_url"` + URL string `json:"url"` + StaticURL string `json:"static_url"` VisibleInPicker bool `json:"visible_in_picker"` } -func (a *Account) Authorize(app Application) (code string) { +// Authorize opens the default browser to initiate the authorization flow +// for the current user. +func (a *Account) Authorize(host string, app Application) (code string) { v := url.Values{} v.Set("client_id", app.ClientID) v.Set("response_type", "code") v.Set("redirect_uri", RedirectUris) u := url.URL{ - Host: mastohost, + Host: host, Path: "oauth/authorize", RawQuery: v.Encode(), } @@ -82,7 +89,12 @@ func (a *Account) Authorize(app Application) (code string) { return } -func (a *Account) RequestToken(app Application, code string) (token Token, err error) { +// RequestToken takes the provided authorization code and returns a structure +// containing a bearer token necessary to make future, authenticated requests +// on the part of the user. +func (a *Account) RequestToken( + host string, app Application, code string) (token Token, err error) { + v := url.Values{} v.Set("client_id", app.ClientID) v.Set("client_secret", app.ClientSecret) @@ -91,7 +103,7 @@ func (a *Account) RequestToken(app Application, code string) (token Token, err e v.Set("grant_type", "authorization_code") u := url.URL{ - Host: mastohost, + Host: host, Path: "oauth/token", RawQuery: v.Encode(), } @@ -120,11 +132,13 @@ func (a *Account) RequestToken(app Application, code string) (token Token, err e return } -func (a *Account) VerifyCredentials(token Token) (err error) { +// VerifyCredentials hydrates the account object by validating the bearer token +// against the Mastodon API +func (a *Account) VerifyCredentials(host string, token Token) (err error) { client := &http.Client{} u := url.URL{ - Host: mastohost, + Host: host, Path: "api/v1/accounts/verify_credentials", } u.Scheme = "https" @@ -158,7 +172,10 @@ func (a *Account) VerifyCredentials(token Token) (err error) { return } -func (a *Account) GetFollowers(token Token) (followers []Account, err error) { +// GetFollowers returns a list of all accounts following the logged in user +func (a *Account) GetFollowers( + host string, token Token) (followers []Account, err error) { + client := &http.Client{} id := url.PathEscape(a.ID) @@ -168,7 +185,7 @@ func (a *Account) GetFollowers(token Token) (followers []Account, err error) { } u := url.URL{ - Host: mastohost, + Host: host, Path: path, } u.Scheme = "https" diff --git a/pkg/mastodon/application.go b/pkg/mastodon/application.go index 4c48ce1..e7a5ece 100644 --- a/pkg/mastodon/application.go +++ b/pkg/mastodon/application.go @@ -10,7 +10,6 @@ import ( ) const configdir = "foodir/" -const mastohost = "hackers.town" // RedirectUris when passed to the redirect_uris parameter, will // show return the authorization code instead of redirecting the client. @@ -24,6 +23,13 @@ type Token struct { CreatedAt int `json:"created_at"` } +// APIError contains the application specific error message returned +// by the Mastodon API. +type APIError struct { + Error string `json:"error"` + ErrorDescription string `json:"error_description"` +} + // Application represents the data and functions that establish a // Mastodon API application. type Application struct { @@ -46,7 +52,7 @@ type Client struct { } // Create calls the Mastodon API to regsiter the application. -func Create(client Client) (app Application, err error) { +func Create(host string, client Client) (app Application, err error) { v := url.Values{} v.Set("client_name", client.ClientName) v.Set("redirect_uris", client.RedirectUris) @@ -54,19 +60,25 @@ func Create(client Client) (app Application, err error) { v.Set("website", client.Website) u := url.URL{ - Host: mastohost, - Path: "oauth/token", - RawQuery: v.Encode(), + Host: host, + Path: "api/v1/apps", } u.Scheme = "https" resp, err := http.PostForm(u.String(), v) if err != nil { - panic(err.Error()) + return + } + if resp.StatusCode != 200 { + err = errors.New(resp.Status) + return } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) + if err != nil { + return + } err = json.Unmarshal(body, &app) return @@ -89,26 +101,28 @@ func Load(name string) (app Application, err error) { } // Save will store a serialized version of the Application struct to a file. -func (a *Application) Save() { - err := os.Mkdir(configdir, 0750) +func (a *Application) Save() (err error) { + err = os.Mkdir(configdir, 0750) if err != nil && !os.IsExist(err) { - panic(err.Error()) + return } data, err := json.Marshal(a) if err != nil { - panic(err.Error()) + return } err = os.WriteFile(configdir+a.Name, data, 0666) if err != nil { - panic(err.Error()) + return } + + return nil } // Login authenticates the application to the Mastodon API, returning // a bearer token to be used with future requests. -func (a *Application) Login() (token Token, err error) { +func (a *Application) Login(host string) (token Token, err error) { v := url.Values{} v.Set("client_id", a.ClientID) v.Set("client_secret", a.ClientSecret) @@ -116,7 +130,7 @@ func (a *Application) Login() (token Token, err error) { v.Set("grant_type", "client_credentials") u := url.URL{ - Host: mastohost, + Host: host, Path: "oauth/token", RawQuery: v.Encode(), } @@ -148,11 +162,11 @@ func (a *Application) Login() (token Token, err error) { // VerifyCredentials accepts a Token object and validates the contained // token against the Mastodon API. -func (a *Application) VerifyCredentials(token Token) (err error) { +func (a *Application) VerifyCredentials(host string, token Token) (err error) { client := &http.Client{} u := url.URL{ - Host: mastohost, + Host: host, Path: "api/v1/apps/verify_credentials", } u.Scheme = "https" -- 2.39.5