]> Vexing Labs - dead-tooter.git/commitdiff
working login command, remove hard-coded host
authorAdam Shamblin <adam@vexingworkshop.com>
Fri, 16 Dec 2022 23:45:09 +0000 (16:45 -0700)
committerAdam Shamblin <adam@vexingworkshop.com>
Fri, 16 Dec 2022 23:45:09 +0000 (16:45 -0700)
README.md
cmd/application.go
cmd/login.go
pkg/mastodon/account.go
pkg/mastodon/application.go

index e83c454d95938055b4e9285cc8302826ad259ba2..35b30ecf48329e8f70e2156135f580d67dfc5012 100644 (file)
--- 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.
 
index 50e29d5661fae9c0470f2e3d57ba5a50c9e29a46..31c3d12f8289056c372f6b5634e12fdc6883891c 100644 (file)
@@ -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",
index e6b19ab475aa9698c30383437fac5dc89834cc02..da3591891b377aed4cf66994ff9819f9dc06ab29 100644 (file)
@@ -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())
        }
index 34c5ffd2c0b6cf3b08f6d0e4d8e81a6f85ca2388..690ef32ffe34c3fc761f348d6f225b79086dd8dc 100644 (file)
@@ -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"
index 4c48ce1e0f44c143485e35dc76aa1062b2d9c7bd..e7a5eced20b56c967a6aa297c1b1c2d87af1c335 100644 (file)
@@ -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"