diff --git a/account.go b/account.go new file mode 100644 index 0000000..5e21c31 --- /dev/null +++ b/account.go @@ -0,0 +1,58 @@ +package main + +type Account struct { + Acct string `json:"acct"` + Avatar string `json:"avatar"` + AvatarStatic string `json:"avatar_static"` + Bot bool `json:"bot"` + CreatedAt string `json:"created_at"` + CustomCSS string `json:"custom_css"` + Discoverable bool `json:"discoverable"` + DisplayName string `json:"display_name"` + Emojis []Emoji `json:"emojis"` + EnableRSS bool `json:"enable_rss"` + Fields []Field `json:"fields"` + FollowersCount int `json:"followers_count"` + FollowingCount int `json:"following_count"` + Header string `json:"header"` + HeaderStatic string `json:"header_static"` + ID string `json:"id"` + LastStatusAt string `json:"last_status_at"` + Locked bool `json:"locked"` + MuteExpiresAt string `json:"mute_expires_at"` + Note string `json:"note"` + Role AccountRole `json:"role"` + Source Source `json:"source"` + StatusCount int `json:"statuses_count"` + Suspended bool `json:"suspended"` + URL string `json:"url"` + Username string `json:"username"` +} + +type Emoji struct { + Category string `json:"category"` + Shortcode string `json:"shortcode"` + StaticURL string `json:"static_url"` + URL string `json:"url"` + VisibleInPicker bool `json:"visible_in_picker"` +} + +type AccountRole struct { + Name string `json:"name"` +} + +type Source struct { + Fields []Field `json:"fields"` + FollowRequestCount int `json:"follow_requests_count"` + Language string `json:"language"` + Note string `json:"note"` + Privacy string `json:"string"` + Sensitive bool `json:"sensitive"` + StatusContentType string `json:"status_content_type"` +} + +type Field struct { + Name string `json:"name"` + Value string `json:"value"` + VerifiedAt string `json:"verified_at"` +} diff --git a/client.go b/client.go index 864067b..8f23b88 100644 --- a/client.go +++ b/client.go @@ -1,15 +1,18 @@ package main import ( + "context" "encoding/json" "fmt" "net/http" + "time" ) type gtsClient struct { authentication Authentication httpClient http.Client userAgent string + timeout time.Duration } func newGtsClient(authentication Authentication) *gtsClient { @@ -19,11 +22,33 @@ func newGtsClient(authentication Authentication) *gtsClient { authentication: authentication, httpClient: httpClient, userAgent: userAgent, + timeout: 5 * time.Second, } return &client } +func (g *gtsClient) verifyCredentials() (Account, error) { + path := "/api/v1/accounts/verify_credentials" + url := g.authentication.Instance + path + + ctx, cancel := context.WithTimeout(context.Background(), g.timeout) + defer cancel() + + request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return Account{}, fmt.Errorf("unable to create the HTTP request; %w", err) + } + + var account Account + + if err := g.sendRequest(request, &account); err != nil { + return Account{}, fmt.Errorf("received an error after sending the request to verify the credentials; %w", err) + } + + return account, nil +} + 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") diff --git a/config.go b/config.go new file mode 100644 index 0000000..b325198 --- /dev/null +++ b/config.go @@ -0,0 +1,117 @@ +package main + +import ( + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + "strings" +) + +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"` +} + +func saveAuthenticationConfig(username string, authentication Authentication) error { + if err := ensureConfigDir(); err != nil { + return fmt.Errorf("unable to ensure the configuration directory; %w", err) + } + + var config AuthenticationConfig + + filepath := authenticationConfigFile() + + if _, err := os.Stat(filepath); err != nil { + if !errors.Is(err, os.ErrNotExist) { + return fmt.Errorf("unknown error received when running stat on %s; %w", filepath, err) + } + + config.Authentications = make(map[string]Authentication) + } else { + config, err = newAuthenticationConfigFromFile() + if err != nil { + return fmt.Errorf("unable to retrieve the existing authentication configuration; %w", err) + } + } + + instance := "" + + if strings.HasPrefix(authentication.Instance, "https://") { + instance = strings.TrimPrefix(authentication.Instance, "https://") + } else if strings.HasPrefix(authentication.Instance, "http://") { + instance = strings.TrimPrefix(authentication.Instance, "http://") + } + + authenticationName := username + "@" + instance + + config.CurrentAccount = authenticationName + + config.Authentications[authenticationName] = authentication + + file, err := os.Create(authenticationConfigFile()) + if err != nil { + return fmt.Errorf("unable to open the config file; %w", err) + } + + if err := json.NewEncoder(file).Encode(config); err != nil { + return fmt.Errorf("unable to save the JSON data to the authentication config file; %w", err) + } + + return nil +} + +func newAuthenticationConfigFromFile() (AuthenticationConfig, error) { + path := authenticationConfigFile() + + file, err := os.Open(path) + if err != nil { + return AuthenticationConfig{}, fmt.Errorf("unable to open %s, %w", path, err) + } + defer file.Close() + + var config AuthenticationConfig + + if err := json.NewDecoder(file).Decode(&config); err != nil { + return AuthenticationConfig{}, fmt.Errorf("unable to decode the JSON data; %w", err) + } + + return config, nil +} + +func authenticationConfigFile() string { + return filepath.Join(configDir(), "authentications.json") +} + +func configDir() string { + rootDir, err := os.UserConfigDir() + if err != nil { + rootDir = "." + } + + return filepath.Join(rootDir, applicationName) +} + +func ensureConfigDir() error { + dir := configDir() + + if _, err := os.Stat(dir); err != nil { + if errors.Is(err, os.ErrNotExist) { + if err := os.MkdirAll(dir, 0o750); err != nil { + return fmt.Errorf("unable to create %s; %w", dir, err) + } + } else { + return fmt.Errorf("unknown error received when running stat on %s; %w", dir, err) + } + } + + return nil +} diff --git a/login.go b/login.go index f9c1195..ca5602f 100644 --- a/login.go +++ b/login.go @@ -11,18 +11,6 @@ import ( "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 = ` @@ -38,6 +26,7 @@ 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: ") @@ -82,11 +71,14 @@ func loginWithOauth2() error { return fmt.Errorf("unable to get the access token; %w", err) } - fmt.Printf("%+v", client.authentication) + account, err := client.verifyCredentials() + if err != nil { + return fmt.Errorf("unable to verify the credentials; %w", err) + } - // validate authentication and get username for file save - - // save the authentication to a file + if err := saveAuthenticationConfig(account.Username, client.authentication); err != nil { + return fmt.Errorf("unable to save the authentication details; %w", err) + } return nil } diff --git a/register.go b/register.go index 390b029..72e8101 100644 --- a/register.go +++ b/register.go @@ -6,7 +6,6 @@ import ( "encoding/json" "fmt" "net/http" - "time" ) type RegisterRequest struct { @@ -44,7 +43,7 @@ func (g *gtsClient) register() error { path := "/api/v1/apps" url := g.authentication.Instance + path - ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), g.timeout) defer cancel() request, err := http.NewRequestWithContext(ctx, http.MethodPost, url, requestBody)