a project structure is forming; very messy ATM; added subcommands login and version
This commit is contained in:
parent
6c297e5242
commit
e354456c0e
13 changed files with 380 additions and 141 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1 +1,2 @@
|
||||||
/environment/
|
/environment/
|
||||||
|
/enbas
|
||||||
|
|
|
@ -13,6 +13,14 @@ output:
|
||||||
sort-results: true
|
sort-results: true
|
||||||
|
|
||||||
linters-settings:
|
linters-settings:
|
||||||
|
depguard:
|
||||||
|
rules:
|
||||||
|
main:
|
||||||
|
files:
|
||||||
|
- $all
|
||||||
|
allow:
|
||||||
|
- $gostd
|
||||||
|
- codeflow.dananglin.me.uk/apollo/enbas
|
||||||
lll:
|
lll:
|
||||||
line-length: 140
|
line-length: 140
|
||||||
|
|
||||||
|
|
83
client.go
83
client.go
|
@ -1,83 +0,0 @@
|
||||||
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 {
|
|
||||||
httpClient := http.Client{}
|
|
||||||
|
|
||||||
client := 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")
|
|
||||||
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
|
|
||||||
}
|
|
|
@ -3,15 +3,26 @@ package main
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"codeflow.dananglin.me.uk/apollo/enbas/internal"
|
||||||
|
"codeflow.dananglin.me.uk/apollo/enbas/internal/client"
|
||||||
|
"codeflow.dananglin.me.uk/apollo/enbas/internal/config"
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type loginCommand struct {
|
||||||
|
*flag.FlagSet
|
||||||
|
summary string
|
||||||
|
instance string
|
||||||
|
}
|
||||||
|
|
||||||
var errEmptyAccessToken = errors.New("received an empty access token")
|
var errEmptyAccessToken = errors.New("received an empty access token")
|
||||||
|
var errInstanceNotSet = errors.New("the instance flag is not set")
|
||||||
|
|
||||||
var consentMessageFormat = `
|
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
|
You'll need to sign into your GoToSocial's consent page in order to generate the out-of-band token to continue with
|
||||||
|
@ -24,17 +35,28 @@ Once you have the code please copy and paste it below.
|
||||||
|
|
||||||
`
|
`
|
||||||
|
|
||||||
func loginWithOauth2() error {
|
func newLoginCommand(name, summary string) *loginCommand {
|
||||||
|
command := loginCommand{
|
||||||
|
FlagSet: flag.NewFlagSet(name, flag.ExitOnError),
|
||||||
|
summary: summary,
|
||||||
|
}
|
||||||
|
|
||||||
|
command.StringVar(&command.instance, "instance", "", "specify the instance that you want to login to.")
|
||||||
|
|
||||||
|
command.Usage = commandUsageFunc(command.Name(), command.summary, command.FlagSet)
|
||||||
|
|
||||||
|
return &command
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *loginCommand) Execute() error {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
var instance string
|
if c.instance == "" {
|
||||||
|
return errInstanceNotSet
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
instance := c.instance
|
||||||
|
|
||||||
if !strings.HasPrefix(instance, "https") || !strings.HasPrefix(instance, "http") {
|
if !strings.HasPrefix(instance, "https") || !strings.HasPrefix(instance, "http") {
|
||||||
instance = "https://" + instance
|
instance = "https://" + instance
|
||||||
}
|
}
|
||||||
|
@ -43,17 +65,17 @@ func loginWithOauth2() error {
|
||||||
instance = instance[:len(instance)-1]
|
instance = instance[:len(instance)-1]
|
||||||
}
|
}
|
||||||
|
|
||||||
authentication := Authentication{
|
authentication := config.Authentication{
|
||||||
Instance: instance,
|
Instance: instance,
|
||||||
}
|
}
|
||||||
|
|
||||||
client := newGtsClient(authentication)
|
gtsClient := client.NewClient(authentication)
|
||||||
|
|
||||||
if err := client.register(); err != nil {
|
if err := gtsClient.Register(); err != nil {
|
||||||
return fmt.Errorf("unable to register the application; %w", err)
|
return fmt.Errorf("unable to register the application; %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
consentPageURL := authCodeURL(client.authentication)
|
consentPageURL := authCodeURL(gtsClient.Authentication)
|
||||||
|
|
||||||
openLink(consentPageURL)
|
openLink(consentPageURL)
|
||||||
|
|
||||||
|
@ -66,17 +88,17 @@ func loginWithOauth2() error {
|
||||||
return fmt.Errorf("failed to read access code; %w", err)
|
return fmt.Errorf("failed to read access code; %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
client.authentication, err = addAccessToken(client.authentication, code)
|
gtsClient.Authentication, err = addAccessToken(gtsClient.Authentication, code)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to get the access token; %w", err)
|
return fmt.Errorf("unable to get the access token; %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
account, err := client.verifyCredentials()
|
account, err := gtsClient.VerifyCredentials()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to verify the credentials; %w", err)
|
return fmt.Errorf("unable to verify the credentials; %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
loginName, err := saveAuthenticationConfig(account.Username, client.authentication)
|
loginName, err := config.SaveAuthentication(account.Username, gtsClient.Authentication)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to save the authentication details; %w", err)
|
return fmt.Errorf("unable to save the authentication details; %w", err)
|
||||||
}
|
}
|
||||||
|
@ -86,42 +108,42 @@ func loginWithOauth2() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func authCodeURL(account Authentication) string {
|
func authCodeURL(account config.Authentication) string {
|
||||||
config := oauth2.Config{
|
config := oauth2.Config{
|
||||||
ClientID: account.ClientID,
|
ClientID: account.ClientID,
|
||||||
ClientSecret: account.ClientSecret,
|
ClientSecret: account.ClientSecret,
|
||||||
Scopes: []string{"read"},
|
Scopes: []string{"read"},
|
||||||
RedirectURL: redirectUri,
|
RedirectURL: internal.RedirectUri,
|
||||||
Endpoint: oauth2.Endpoint{
|
Endpoint: oauth2.Endpoint{
|
||||||
AuthURL: account.Instance + "/oauth/authorize",
|
AuthURL: account.Instance + "/oauth/authorize",
|
||||||
TokenURL: account.Instance + "/oauth/token",
|
TokenURL: account.Instance + "/oauth/token",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
url := config.AuthCodeURL("state", oauth2.AccessTypeOffline) + fmt.Sprintf("&client_name=%s", applicationName)
|
url := config.AuthCodeURL("state", oauth2.AccessTypeOffline) + fmt.Sprintf("&client_name=%s", internal.ApplicationName)
|
||||||
|
|
||||||
return url
|
return url
|
||||||
}
|
}
|
||||||
|
|
||||||
func addAccessToken(authentication Authentication, code string) (Authentication, error) {
|
func addAccessToken(authentication config.Authentication, code string) (config.Authentication, error) {
|
||||||
config := oauth2.Config{
|
ouauth2Conf := oauth2.Config{
|
||||||
ClientID: authentication.ClientID,
|
ClientID: authentication.ClientID,
|
||||||
ClientSecret: authentication.ClientSecret,
|
ClientSecret: authentication.ClientSecret,
|
||||||
Scopes: []string{"read", "write"},
|
Scopes: []string{"read", "write"},
|
||||||
RedirectURL: redirectUri,
|
RedirectURL: internal.RedirectUri,
|
||||||
Endpoint: oauth2.Endpoint{
|
Endpoint: oauth2.Endpoint{
|
||||||
AuthURL: authentication.Instance + "/oauth/authorize",
|
AuthURL: authentication.Instance + "/oauth/authorize",
|
||||||
TokenURL: authentication.Instance + "/oauth/token",
|
TokenURL: authentication.Instance + "/oauth/token",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
token, err := config.Exchange(context.Background(), code)
|
token, err := ouauth2Conf.Exchange(context.Background(), code)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Authentication{}, fmt.Errorf("unable to exchange the code for an access token; %w", err)
|
return config.Authentication{}, fmt.Errorf("unable to exchange the code for an access token; %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if token == nil || token.AccessToken == "" {
|
if token == nil || token.AccessToken == "" {
|
||||||
return Authentication{}, errEmptyAccessToken
|
return config.Authentication{}, errEmptyAccessToken
|
||||||
}
|
}
|
||||||
|
|
||||||
authentication.AccessToken = token.AccessToken
|
authentication.AccessToken = token.AccessToken
|
118
cmd/enbas/main.go
Normal file
118
cmd/enbas/main.go
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
login string = "login"
|
||||||
|
version string = "version"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Executor interface {
|
||||||
|
Parse([]string) error
|
||||||
|
Name() string
|
||||||
|
Execute() error
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
summaries := map[string]string{
|
||||||
|
login: "login to an account on GoToSocial",
|
||||||
|
version: "print the application's version and build information",
|
||||||
|
}
|
||||||
|
|
||||||
|
flag.Usage = enbasUsageFunc(summaries)
|
||||||
|
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if flag.NArg() < 1 {
|
||||||
|
flag.Usage()
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
subcommand := flag.Arg(0)
|
||||||
|
args := flag.Args()[1:]
|
||||||
|
|
||||||
|
var executor Executor
|
||||||
|
|
||||||
|
switch subcommand {
|
||||||
|
case login:
|
||||||
|
executor = newLoginCommand(login, summaries[login])
|
||||||
|
case version:
|
||||||
|
executor = newVersionCommand(version, summaries[version])
|
||||||
|
default:
|
||||||
|
fmt.Printf("ERROR: Unknown subcommand: %s\n", subcommand)
|
||||||
|
flag.Usage()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := executor.Parse(args); err != nil {
|
||||||
|
fmt.Printf("ERROR: Unable to parse the command line flags; %v.\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := executor.Execute(); err != nil {
|
||||||
|
fmt.Printf("ERROR: Unable to run %q; %v.\n", executor.Name(), err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func commandUsageFunc(name, summary string, flagset *flag.FlagSet) func() {
|
||||||
|
return func() {
|
||||||
|
var builder strings.Builder
|
||||||
|
|
||||||
|
fmt.Fprintf(&builder, "SUMMARY:\n %s - %s\n\nUSAGE:\n enbas %s [flags]\n\nFLAGS:", name, summary, name)
|
||||||
|
|
||||||
|
flagset.VisitAll(func(f *flag.Flag) {
|
||||||
|
fmt.Fprintf(&builder, "\n -%s, --%s\n %s", f.Name, f.Name, f.Usage)
|
||||||
|
})
|
||||||
|
|
||||||
|
builder.WriteString("\n")
|
||||||
|
|
||||||
|
w := flag.CommandLine.Output()
|
||||||
|
|
||||||
|
fmt.Fprint(w, builder.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func enbasUsageFunc(summaries map[string]string) func() {
|
||||||
|
cmds := make([]string, len(summaries))
|
||||||
|
ind := 0
|
||||||
|
|
||||||
|
for k := range summaries {
|
||||||
|
cmds[ind] = k
|
||||||
|
ind++
|
||||||
|
}
|
||||||
|
|
||||||
|
slices.Sort(cmds)
|
||||||
|
|
||||||
|
return func() {
|
||||||
|
var builder strings.Builder
|
||||||
|
|
||||||
|
builder.WriteString("SUMMARY:\n enbas - A GoToSocial client for the terminal.\n\n")
|
||||||
|
|
||||||
|
//if binaryVersion != "" {
|
||||||
|
// builder.WriteString("VERSION:\n " + binaryVersion + "\n\n")
|
||||||
|
//}
|
||||||
|
|
||||||
|
builder.WriteString("USAGE:\n enbas [flags]\n enbas [command]\n\nCOMMANDS:")
|
||||||
|
|
||||||
|
for _, cmd := range cmds {
|
||||||
|
fmt.Fprintf(&builder, "\n %s\t%s", cmd, summaries[cmd])
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.WriteString("\n\nFLAGS:\n -help, --help\n print the help message\n")
|
||||||
|
flag.VisitAll(func(f *flag.Flag) {
|
||||||
|
fmt.Fprintf(&builder, "\n -%s, --%s\n %s\n", f.Name, f.Name, f.Usage)
|
||||||
|
})
|
||||||
|
|
||||||
|
builder.WriteString("\nUse \"enbas [command] --help\" for more information about a command.\n")
|
||||||
|
|
||||||
|
w := flag.CommandLine.Output()
|
||||||
|
fmt.Fprint(w, builder.String())
|
||||||
|
}
|
||||||
|
}
|
64
cmd/enbas/version.go
Normal file
64
cmd/enbas/version.go
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
binaryVersion string
|
||||||
|
buildTime string
|
||||||
|
goVersion string
|
||||||
|
gitCommit string
|
||||||
|
)
|
||||||
|
|
||||||
|
type versionCommand struct {
|
||||||
|
*flag.FlagSet
|
||||||
|
summary string
|
||||||
|
showFullVersion bool
|
||||||
|
binaryVersion string
|
||||||
|
buildTime string
|
||||||
|
goVersion string
|
||||||
|
gitCommit string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newVersionCommand(name, summary string) *versionCommand {
|
||||||
|
command := versionCommand{
|
||||||
|
FlagSet: flag.NewFlagSet(name, flag.ExitOnError),
|
||||||
|
binaryVersion: binaryVersion,
|
||||||
|
buildTime: buildTime,
|
||||||
|
goVersion: goVersion,
|
||||||
|
gitCommit: gitCommit,
|
||||||
|
summary: summary,
|
||||||
|
showFullVersion: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
command.BoolVar(&command.showFullVersion, "full", false, "prints the full build information")
|
||||||
|
|
||||||
|
command.Usage = commandUsageFunc(command.Name(), command.summary, command.FlagSet)
|
||||||
|
|
||||||
|
return &command
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *versionCommand) Execute() error {
|
||||||
|
var builder strings.Builder
|
||||||
|
|
||||||
|
if c.showFullVersion {
|
||||||
|
fmt.Fprintf(
|
||||||
|
&builder,
|
||||||
|
"Enbas\n Version: %s\n Git commit: %s\n Go version: %s\n Build date: %s\n",
|
||||||
|
c.binaryVersion,
|
||||||
|
c.gitCommit,
|
||||||
|
c.goVersion,
|
||||||
|
c.buildTime,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintln(&builder, c.binaryVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprint(os.Stdout, builder.String())
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -5,6 +5,8 @@ package main
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"runtime"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/magefile/mage/sh"
|
"github.com/magefile/mage/sh"
|
||||||
)
|
)
|
||||||
|
@ -51,8 +53,8 @@ func Build() error {
|
||||||
return fmt.Errorf("unable to change to the project's root directory; %w", err)
|
return fmt.Errorf("unable to change to the project's root directory; %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
main := "main.go"
|
flags := ldflags()
|
||||||
return sh.Run("go", "build", "-o", binary, main)
|
return sh.Run("go", "build", "-ldflags="+flags, "-a", "-o", binary, "./cmd/enbas")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean clean the workspace.
|
// Clean clean the workspace.
|
||||||
|
@ -79,3 +81,31 @@ func changeToProjectRoot() error {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ldflags returns the build flags.
|
||||||
|
func ldflags() string {
|
||||||
|
ldflagsfmt := "-s -w -X main.binaryVersion=%s -X main.gitCommit=%s -X main.goVersion=%s -X main.buildTime=%s"
|
||||||
|
buildTime := time.Now().UTC().Format(time.RFC3339)
|
||||||
|
|
||||||
|
return fmt.Sprintf(ldflagsfmt, version(), gitCommit(), runtime.Version(), buildTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
// version returns the latest git tag using git describe.
|
||||||
|
func version() string {
|
||||||
|
version, err := sh.Output("git", "describe", "--tags")
|
||||||
|
if err != nil {
|
||||||
|
version = "N/A"
|
||||||
|
}
|
||||||
|
|
||||||
|
return version
|
||||||
|
}
|
||||||
|
|
||||||
|
// gitCommit returns the current git commit
|
||||||
|
func gitCommit() string {
|
||||||
|
commit, err := sh.Output("git", "rev-parse", "--short", "HEAD")
|
||||||
|
if err != nil {
|
||||||
|
commit = "N/A"
|
||||||
|
}
|
||||||
|
|
||||||
|
return commit
|
||||||
|
}
|
||||||
|
|
87
internal/client/client.go
Normal file
87
internal/client/client.go
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"codeflow.dananglin.me.uk/apollo/enbas/internal"
|
||||||
|
"codeflow.dananglin.me.uk/apollo/enbas/internal/config"
|
||||||
|
"codeflow.dananglin.me.uk/apollo/enbas/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
Authentication config.Authentication
|
||||||
|
HTTPClient http.Client
|
||||||
|
UserAgent string
|
||||||
|
Timeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClient(authentication config.Authentication) *Client {
|
||||||
|
httpClient := http.Client{}
|
||||||
|
|
||||||
|
client := Client{
|
||||||
|
Authentication: authentication,
|
||||||
|
HTTPClient: httpClient,
|
||||||
|
UserAgent: internal.UserAgent,
|
||||||
|
Timeout: 5 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
return &client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Client) VerifyCredentials() (model.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 model.Account{}, fmt.Errorf("unable to create the HTTP request; %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var account model.Account
|
||||||
|
|
||||||
|
if err := g.sendRequest(request, &account); err != nil {
|
||||||
|
return model.Account{}, fmt.Errorf("received an error after sending the request to verify the credentials; %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return account, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Client) 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
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package main
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
@ -6,6 +6,8 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"codeflow.dananglin.me.uk/apollo/enbas/internal"
|
||||||
)
|
)
|
||||||
|
|
||||||
type RegisterRequest struct {
|
type RegisterRequest struct {
|
||||||
|
@ -25,12 +27,12 @@ type RegisterResponse struct {
|
||||||
Website string `json:"website"`
|
Website string `json:"website"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *gtsClient) register() error {
|
func (g *Client) Register() error {
|
||||||
params := RegisterRequest{
|
params := RegisterRequest{
|
||||||
ClientName: applicationName,
|
ClientName: internal.ApplicationName,
|
||||||
RedirectUris: redirectUri,
|
RedirectUris: internal.RedirectUri,
|
||||||
Scopes: "read write",
|
Scopes: "read write",
|
||||||
Website: applicationWebsite,
|
Website: internal.ApplicationWebsite,
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := json.Marshal(params)
|
data, err := json.Marshal(params)
|
||||||
|
@ -41,9 +43,9 @@ func (g *gtsClient) register() error {
|
||||||
requestBody := bytes.NewBuffer(data)
|
requestBody := bytes.NewBuffer(data)
|
||||||
|
|
||||||
path := "/api/v1/apps"
|
path := "/api/v1/apps"
|
||||||
url := g.authentication.Instance + path
|
url := g.Authentication.Instance + path
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), g.timeout)
|
ctx, cancel := context.WithTimeout(context.Background(), g.Timeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
request, err := http.NewRequestWithContext(ctx, http.MethodPost, url, requestBody)
|
request, err := http.NewRequestWithContext(ctx, http.MethodPost, url, requestBody)
|
||||||
|
@ -57,8 +59,8 @@ func (g *gtsClient) register() error {
|
||||||
return fmt.Errorf("received an error after sending the registration request; %w", err)
|
return fmt.Errorf("received an error after sending the registration request; %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
g.authentication.ClientID = registerResponse.ClientID
|
g.Authentication.ClientID = registerResponse.ClientID
|
||||||
g.authentication.ClientSecret = registerResponse.ClientSecret
|
g.Authentication.ClientSecret = registerResponse.ClientSecret
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package main
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
@ -7,6 +7,8 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"codeflow.dananglin.me.uk/apollo/enbas/internal"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AuthenticationConfig struct {
|
type AuthenticationConfig struct {
|
||||||
|
@ -21,7 +23,7 @@ type Authentication struct {
|
||||||
AccessToken string `json:"accessToken"`
|
AccessToken string `json:"accessToken"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveAuthenticationConfig(username string, authentication Authentication) (string, error) {
|
func SaveAuthentication(username string, authentication Authentication) (string, error) {
|
||||||
if err := ensureConfigDir(); err != nil {
|
if err := ensureConfigDir(); err != nil {
|
||||||
return "", fmt.Errorf("unable to ensure the configuration directory; %w", err)
|
return "", fmt.Errorf("unable to ensure the configuration directory; %w", err)
|
||||||
}
|
}
|
||||||
|
@ -97,7 +99,7 @@ func configDir() string {
|
||||||
rootDir = "."
|
rootDir = "."
|
||||||
}
|
}
|
||||||
|
|
||||||
return filepath.Join(rootDir, applicationName)
|
return filepath.Join(rootDir, internal.ApplicationName)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ensureConfigDir() error {
|
func ensureConfigDir() error {
|
8
internal/internal.go
Normal file
8
internal/internal.go
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
package internal
|
||||||
|
|
||||||
|
const (
|
||||||
|
ApplicationName = "enbas"
|
||||||
|
ApplicationWebsite = "https://codeflow.dananglin.me.uk/apollo/enbas"
|
||||||
|
RedirectUri = "urn:ietf:wg:oauth:2.0:oob"
|
||||||
|
UserAgent = "Enbas/0.0.0"
|
||||||
|
)
|
|
@ -1,4 +1,4 @@
|
||||||
package main
|
package model
|
||||||
|
|
||||||
type Account struct {
|
type Account struct {
|
||||||
Acct string `json:"acct"`
|
Acct string `json:"acct"`
|
20
main.go
20
main.go
|
@ -1,20 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
applicationName = "enbas"
|
|
||||||
applicationWebsite = "https://codeflow.dananglin.me.uk/apollo/enbas"
|
|
||||||
redirectUri = "urn:ietf:wg:oauth:2.0:oob"
|
|
||||||
userAgent = "Enbas/0.0.0"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
if err := loginWithOauth2(); err != nil {
|
|
||||||
fmt.Printf("ERROR: Unable to register the application; %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in a new issue