package main import ( "bytes" "context" "encoding/json" "errors" "fmt" "net/http" "os" "os/exec" "runtime" "strings" "time" "golang.org/x/oauth2" ) const ( applicationName = "enbas" applicationWebsite = "https://codeflow.dananglin.me.uk/apollo/enbas" redirectUri = "urn:ietf:wg:oauth:2.0:oob" 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() }