checkpoint: create the client model now
This commit is contained in:
parent
50d2f5f8d2
commit
624cd561ed
4 changed files with 272 additions and 223 deletions
58
client.go
Normal file
58
client.go
Normal file
|
@ -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
|
||||
}
|
149
login.go
Normal file
149
login.go
Normal file
|
@ -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()
|
||||
}
|
223
main.go
223
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()
|
||||
}
|
||||
|
|
65
register.go
Normal file
65
register.go
Normal file
|
@ -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
|
||||
}
|
Loading…
Reference in a new issue