From 624cd561edeee4d724b11dd58144f8018419507d Mon Sep 17 00:00:00 2001 From: Dan Anglin Date: Tue, 20 Feb 2024 14:09:26 +0000 Subject: [PATCH] checkpoint: create the client model now --- client.go | 58 ++++++++++++++ login.go | 149 +++++++++++++++++++++++++++++++++++ main.go | 223 ---------------------------------------------------- register.go | 65 +++++++++++++++ 4 files changed, 272 insertions(+), 223 deletions(-) create mode 100644 client.go create mode 100644 login.go create mode 100644 register.go diff --git a/client.go b/client.go new file mode 100644 index 0000000..864067b --- /dev/null +++ b/client.go @@ -0,0 +1,58 @@ +package main + +import ( + "encoding/json" + "fmt" + "net/http" +) + +type gtsClient struct { + authentication Authentication + httpClient http.Client + userAgent string +} + +func newGtsClient(authentication Authentication) *gtsClient { + httpClient := http.Client{} + + client := gtsClient{ + authentication: authentication, + httpClient: httpClient, + userAgent: userAgent, + } + + return &client +} + +func (g *gtsClient) sendRequest(request *http.Request, object any) error { + request.Header.Set("Content-Type", "application/json; charset=utf-8") + request.Header.Set("Accept", "application/json; charset=utf-8") + request.Header.Set("User-Agent", g.userAgent) + + if len(g.authentication.AccessToken) > 0 { + request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", g.authentication.AccessToken)) + } + + response, err := g.httpClient.Do(request) + if err != nil { + return fmt.Errorf("received an error after sending the request; %w", err) + } + + defer response.Body.Close() + + if response.StatusCode < http.StatusOK || response.StatusCode >= http.StatusBadRequest { + return fmt.Errorf( + "did not receive an OK response from the GoToSocial server; got %d", + response.StatusCode, + ) + } + + if err := json.NewDecoder(response.Body).Decode(object); err != nil { + return fmt.Errorf( + "unable to decode the response from the GoToSocial server; %w", + err, + ) + } + + return nil +} diff --git a/login.go b/login.go new file mode 100644 index 0000000..f9c1195 --- /dev/null +++ b/login.go @@ -0,0 +1,149 @@ +package main + +import ( + "context" + "errors" + "fmt" + "os/exec" + "runtime" + "strings" + + "golang.org/x/oauth2" +) + +type AuthenticationConfig struct { + CurrentAccount string `json:"currentAccount"` + Authentications map[string]Authentication `json:"authentications"` +} + +type Authentication struct { + Instance string `json:"instance"` + ClientID string `json:"clientId"` + ClientSecret string `json:"clientSecret"` + AccessToken string `json:"accessToken"` +} + +var errEmptyAccessToken = errors.New("received an empty access token") + +var consentMessageFormat = ` +You'll need to sign into your GoToSocial's consent page in order to generate the out-of-band token to continue with +the application's login process. Your browser may have opened the link to the consent page already. If not, please +copy and paste the link below to your browser: + +%s + +Once you have the code please copy and paste it below. + +` + +func loginWithOauth2() error { + var err error + var instance string + + fmt.Print("Please enter the instance URL: ") + + if _, err := fmt.Scanln(&instance); err != nil { + return fmt.Errorf("unable to read user input for the instance value; %w", err) + } + + if !strings.HasPrefix(instance, "https") || !strings.HasPrefix(instance, "http") { + instance = "https://" + instance + } + + for strings.HasSuffix(instance, "/") { + instance = instance[:len(instance)-1] + } + + authentication := Authentication{ + Instance: instance, + } + + client := newGtsClient(authentication) + + if err := client.register(); err != nil { + return fmt.Errorf("unable to register the application; %w", err) + } + + consentPageURL := authCodeURL(client.authentication) + + openLink(consentPageURL) + + fmt.Printf(consentMessageFormat, consentPageURL) + + var code string + fmt.Print("Out-of-band token: ") + + if _, err := fmt.Scanln(&code); err != nil { + return fmt.Errorf("failed to read access code; %w", err) + } + + client.authentication, err = addAccessToken(client.authentication, code) + if err != nil { + return fmt.Errorf("unable to get the access token; %w", err) + } + + fmt.Printf("%+v", client.authentication) + + // validate authentication and get username for file save + + // save the authentication to a file + + return nil +} + +func authCodeURL(account Authentication) string { + config := oauth2.Config{ + ClientID: account.ClientID, + ClientSecret: account.ClientSecret, + Scopes: []string{"read", "write"}, + RedirectURL: redirectUri, + Endpoint: oauth2.Endpoint{ + AuthURL: account.Instance + "/oauth/authorize", + TokenURL: account.Instance + "/oauth/token", + }, + } + + url := config.AuthCodeURL("state", oauth2.AccessTypeOffline) + fmt.Sprintf("&client_name=%s", applicationName) + + return url +} + +func addAccessToken(authentication Authentication, code string) (Authentication, error) { + config := oauth2.Config{ + ClientID: authentication.ClientID, + ClientSecret: authentication.ClientSecret, + Scopes: []string{"read", "write"}, + RedirectURL: redirectUri, + Endpoint: oauth2.Endpoint{ + AuthURL: authentication.Instance + "/oauth/authorize", + TokenURL: authentication.Instance + "/oauth/token", + }, + } + + token, err := config.Exchange(context.Background(), code) + if err != nil { + return Authentication{}, fmt.Errorf("unable to exchange the code for an access token; %w", err) + } + + if token == nil || token.AccessToken == "" { + return Authentication{}, errEmptyAccessToken + } + + authentication.AccessToken = token.AccessToken + + return authentication, nil +} + +func openLink(url string) { + var open string + + if runtime.GOOS == "linux" { + open = "xdg-open" + } else { + return + } + + command := exec.Command(open, url) + + _ = command.Start() +} diff --git a/main.go b/main.go index e8d3bec..fa3e2bb 100644 --- a/main.go +++ b/main.go @@ -1,19 +1,8 @@ package main import ( - "bytes" - "context" - "encoding/json" - "errors" "fmt" - "net/http" "os" - "os/exec" - "runtime" - "strings" - "time" - - "golang.org/x/oauth2" ) const ( @@ -23,221 +12,9 @@ const ( userAgent = "Enbas/0.0.0" ) -type AuthenticationConfig struct { - CurrentAccount string `json:"currentAccount"` - Authentications map[string]Authentication `json:"authentications"` -} - -type Authentication struct { - Instance string `json:"instance"` - ClientID string `json:"clientId"` - ClientSecret string `json:"clientSecret"` - AccessToken string `json:"accessToken"` -} - -type RegisterRequest struct { - ClientName string `json:"client_name"` - RedirectUris string `json:"redirect_uris"` - Scopes string `json:"scopes"` - Website string `json:"website"` -} - -type RegisterResponse struct { - ClientID string `json:"client_id"` - ClientSecret string `json:"client_secret"` - ID string `json:"id"` - Name string `json:"name"` - RedirectUri string `json:"redirect_uri"` - VapidKey string `json:"vapid_key"` - Website string `json:"website"` -} - -var errEmptyAccessToken = errors.New("received an empty access token") - -var consentMessageFormat = ` -You'll need to sign into your GoToSocial's consent page in order to generate the out-of-band token to continue with -the application's login process. Your browser may have opened the link to the consent page already. If not, please -copy and paste the link below to your browser: - -%s - -Once you have the code please copy and paste it below. - -` - func main() { if err := loginWithOauth2(); err != nil { fmt.Printf("ERROR: Unable to register the application; %v\n", err) os.Exit(1) } } - -func loginWithOauth2() error { - var instance string - - fmt.Print("Please enter the instance URL: ") - - if _, err := fmt.Scanln(&instance); err != nil { - return fmt.Errorf("unable to read user input for the instance value; %w", err) - } - - authentication, err := register(instance) - if err != nil { - return fmt.Errorf("unable to register the application; %w", err) - } - - consentPageURL := authCodeURL(authentication) - - openLink(consentPageURL) - - fmt.Printf(consentMessageFormat, consentPageURL) - - var code string - fmt.Print("Out-of-band token: ") - - if _, err := fmt.Scanln(&code); err != nil { - return fmt.Errorf("failed to read access code; %w", err) - } - - authentication, err = addAccessToken(authentication, code) - if err != nil { - return fmt.Errorf("unable to get the access token; %w", err) - } - - fmt.Printf("%+v", authentication) - - // validate authentication and get username for file save - - // save the authentication to a file - - return nil -} - -func register(instance string) (Authentication, error) { - if !strings.HasPrefix(instance, "https") || !strings.HasPrefix(instance, "http") { - instance = "https://" + instance - } - - for strings.HasSuffix(instance, "/") { - instance = instance[:len(instance)-1] - } - - request := RegisterRequest{ - ClientName: applicationName, - RedirectUris: redirectUri, - Scopes: "read write", - Website: applicationWebsite, - } - - data, err := json.Marshal(request) - if err != nil { - return Authentication{}, fmt.Errorf("unable to marshal the request body; %w", err) - } - - httpRequestBody := bytes.NewBuffer(data) - - path := "/api/v1/apps" - url := instance + path - - ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) - defer cancel() - - httpRequest, err := http.NewRequestWithContext(ctx, http.MethodPost, url, httpRequestBody) - if err != nil { - return Authentication{}, fmt.Errorf("unable to create the HTTP request; %w", err) - } - - httpRequest.Header.Add("Content-Type", "application/json") - httpRequest.Header.Set("User-Agent", userAgent) - - httpClient := http.Client{} - - httpResponse, err := httpClient.Do(httpRequest) - if err != nil { - return Authentication{}, fmt.Errorf("received an error after sending the request; %w", err) - } - - if httpResponse.StatusCode != http.StatusOK { - return Authentication{}, fmt.Errorf( - "did not receive an OK response from the GoToSocial server; got %d", - httpResponse.StatusCode, - ) - } - - defer httpResponse.Body.Close() - - var registerResponse RegisterResponse - - if err := json.NewDecoder(httpResponse.Body).Decode(®isterResponse); err != nil { - return Authentication{}, fmt.Errorf( - "unable to decode the response from the GoToSocial server; %w", - err, - ) - } - - account := Authentication{ - Instance: instance, - ClientID: registerResponse.ClientID, - ClientSecret: registerResponse.ClientSecret, - AccessToken: "", - } - - return account, nil -} - -func authCodeURL(account Authentication) string { - config := oauth2.Config{ - ClientID: account.ClientID, - ClientSecret: account.ClientSecret, - Scopes: []string{"read", "write"}, - RedirectURL: redirectUri, - Endpoint: oauth2.Endpoint{ - AuthURL: account.Instance + "/oauth/authorize", - TokenURL: account.Instance + "/oauth/token", - }, - } - - url := config.AuthCodeURL("state", oauth2.AccessTypeOffline) + fmt.Sprintf("&client_name=%s", applicationName) - - return url -} - -func addAccessToken(authentication Authentication, code string) (Authentication, error) { - config := oauth2.Config{ - ClientID: authentication.ClientID, - ClientSecret: authentication.ClientSecret, - Scopes: []string{"read", "write"}, - RedirectURL: redirectUri, - Endpoint: oauth2.Endpoint{ - AuthURL: authentication.Instance + "/oauth/authorize", - TokenURL: authentication.Instance + "/oauth/token", - }, - } - - token, err := config.Exchange(context.Background(), code) - if err != nil { - return Authentication{}, fmt.Errorf("unable to exchange the code for an access token; %w", err) - } - - if token == nil || token.AccessToken == "" { - return Authentication{}, errEmptyAccessToken - } - - authentication.AccessToken = token.AccessToken - - return authentication, nil -} - -func openLink(url string) { - var open string - - if runtime.GOOS == "linux" { - open = "xdg-open" - } else { - return - } - - command := exec.Command(open, url) - - command.Start() -} diff --git a/register.go b/register.go new file mode 100644 index 0000000..390b029 --- /dev/null +++ b/register.go @@ -0,0 +1,65 @@ +package main + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "time" +) + +type RegisterRequest struct { + ClientName string `json:"client_name"` + RedirectUris string `json:"redirect_uris"` + Scopes string `json:"scopes"` + Website string `json:"website"` +} + +type RegisterResponse struct { + ClientID string `json:"client_id"` + ClientSecret string `json:"client_secret"` + ID string `json:"id"` + Name string `json:"name"` + RedirectUri string `json:"redirect_uri"` + VapidKey string `json:"vapid_key"` + Website string `json:"website"` +} + +func (g *gtsClient) register() error { + params := RegisterRequest{ + ClientName: applicationName, + RedirectUris: redirectUri, + Scopes: "read write", + Website: applicationWebsite, + } + + data, err := json.Marshal(params) + if err != nil { + return fmt.Errorf("unable to marshal the request body; %w", err) + } + + requestBody := bytes.NewBuffer(data) + + path := "/api/v1/apps" + url := g.authentication.Instance + path + + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + + request, err := http.NewRequestWithContext(ctx, http.MethodPost, url, requestBody) + if err != nil { + return fmt.Errorf("unable to create the HTTP request; %w", err) + } + + var registerResponse RegisterResponse + + if err := g.sendRequest(request, ®isterResponse); err != nil { + return fmt.Errorf("received an error after sending the registration request; %w", err) + } + + g.authentication.ClientID = registerResponse.ClientID + g.authentication.ClientSecret = registerResponse.ClientSecret + + return nil +}