Compare commits
No commits in common. "1361482dd4eeb6658c861d751208329fabad861b" and "d3c67562ecd8a27319ce138ca4968ede848357f0" have entirely different histories.
1361482dd4
...
d3c67562ec
15 changed files with 150 additions and 598 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,2 +1 @@
|
||||||
/environment/
|
/environment/
|
||||||
/enbas
|
|
||||||
|
|
|
@ -13,14 +13,6 @@ 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
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package model
|
package main
|
||||||
|
|
||||||
type Account struct {
|
type Account struct {
|
||||||
Acct string `json:"acct"`
|
Acct string `json:"acct"`
|
83
client.go
Normal file
83
client.go
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
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
|
||||||
|
}
|
|
@ -1,64 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"codeflow.dananglin.me.uk/apollo/enbas/internal/client"
|
|
||||||
)
|
|
||||||
|
|
||||||
var instanceDetailsFormat = `INSTANCE:
|
|
||||||
%s - %s
|
|
||||||
|
|
||||||
DOMAIN:
|
|
||||||
%s
|
|
||||||
|
|
||||||
VERSION:
|
|
||||||
Running GoToSocial %s
|
|
||||||
|
|
||||||
CONTACT:
|
|
||||||
name: %s
|
|
||||||
username: %s
|
|
||||||
email: %s
|
|
||||||
`
|
|
||||||
|
|
||||||
type instanceCommand struct {
|
|
||||||
*flag.FlagSet
|
|
||||||
summary string
|
|
||||||
}
|
|
||||||
|
|
||||||
func newInstanceCommand(name, summary string) *instanceCommand {
|
|
||||||
command := instanceCommand{
|
|
||||||
FlagSet: flag.NewFlagSet(name, flag.ExitOnError),
|
|
||||||
summary: summary,
|
|
||||||
}
|
|
||||||
|
|
||||||
command.Usage = commandUsageFunc(command.Name(), command.summary, command.FlagSet)
|
|
||||||
|
|
||||||
return &command
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *instanceCommand) Execute() error {
|
|
||||||
gtsClient, err := client.NewClientFromConfig()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unable to create the GoToSocial client; %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
instance, err := gtsClient.GetInstance()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unable to retrieve the instance details; %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf(
|
|
||||||
instanceDetailsFormat,
|
|
||||||
instance.Title,
|
|
||||||
instance.Description,
|
|
||||||
instance.Domain,
|
|
||||||
instance.Version,
|
|
||||||
instance.Contact.Account.DisplayName,
|
|
||||||
instance.Contact.Account.Username,
|
|
||||||
instance.Contact.Email,
|
|
||||||
)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,122 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"slices"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
login string = "login"
|
|
||||||
version string = "version"
|
|
||||||
instance string = "instance"
|
|
||||||
)
|
|
||||||
|
|
||||||
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",
|
|
||||||
instance: "print the instance 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])
|
|
||||||
case instance:
|
|
||||||
executor = newInstanceCommand(instance, summaries[instance])
|
|
||||||
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())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,64 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
|
@ -1,4 +1,4 @@
|
||||||
package config
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
@ -7,8 +7,6 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"codeflow.dananglin.me.uk/apollo/enbas/internal"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type AuthenticationConfig struct {
|
type AuthenticationConfig struct {
|
||||||
|
@ -23,9 +21,9 @@ type Authentication struct {
|
||||||
AccessToken string `json:"accessToken"`
|
AccessToken string `json:"accessToken"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func SaveAuthentication(username string, authentication Authentication) (string, error) {
|
func saveAuthenticationConfig(username string, authentication Authentication) 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
var config AuthenticationConfig
|
var config AuthenticationConfig
|
||||||
|
@ -34,14 +32,14 @@ func SaveAuthentication(username string, authentication Authentication) (string,
|
||||||
|
|
||||||
if _, err := os.Stat(filepath); err != nil {
|
if _, err := os.Stat(filepath); err != nil {
|
||||||
if !errors.Is(err, os.ErrNotExist) {
|
if !errors.Is(err, os.ErrNotExist) {
|
||||||
return "", fmt.Errorf("unknown error received when running stat on %s; %w", filepath, err)
|
return fmt.Errorf("unknown error received when running stat on %s; %w", filepath, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
config.Authentications = make(map[string]Authentication)
|
config.Authentications = make(map[string]Authentication)
|
||||||
} else {
|
} else {
|
||||||
config, err = NewAuthenticationConfigFromFile()
|
config, err = newAuthenticationConfigFromFile()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("unable to retrieve the existing authentication configuration; %w", err)
|
return fmt.Errorf("unable to retrieve the existing authentication configuration; %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,17 +59,17 @@ func SaveAuthentication(username string, authentication Authentication) (string,
|
||||||
|
|
||||||
file, err := os.Create(authenticationConfigFile())
|
file, err := os.Create(authenticationConfigFile())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("unable to open the config file; %w", err)
|
return fmt.Errorf("unable to open the config file; %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := json.NewEncoder(file).Encode(config); err != nil {
|
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 fmt.Errorf("unable to save the JSON data to the authentication config file; %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return authenticationName, nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAuthenticationConfigFromFile() (AuthenticationConfig, error) {
|
func newAuthenticationConfigFromFile() (AuthenticationConfig, error) {
|
||||||
path := authenticationConfigFile()
|
path := authenticationConfigFile()
|
||||||
|
|
||||||
file, err := os.Open(path)
|
file, err := os.Open(path)
|
||||||
|
@ -99,7 +97,7 @@ func configDir() string {
|
||||||
rootDir = "."
|
rootDir = "."
|
||||||
}
|
}
|
||||||
|
|
||||||
return filepath.Join(rootDir, internal.ApplicationName)
|
return filepath.Join(rootDir, applicationName)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ensureConfigDir() error {
|
func ensureConfigDir() error {
|
|
@ -5,8 +5,6 @@ package main
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/magefile/mage/sh"
|
"github.com/magefile/mage/sh"
|
||||||
)
|
)
|
||||||
|
@ -53,8 +51,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)
|
||||||
}
|
}
|
||||||
|
|
||||||
flags := ldflags()
|
main := "main.go"
|
||||||
return sh.Run("go", "build", "-ldflags="+flags, "-a", "-o", binary, "./cmd/enbas")
|
return sh.Run("go", "build", "-o", binary, main)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean clean the workspace.
|
// Clean clean the workspace.
|
||||||
|
@ -81,31 +79,3 @@ 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
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,119 +0,0 @@
|
||||||
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 NewClientFromConfig() (*Client, error) {
|
|
||||||
config, err := config.NewAuthenticationConfigFromFile()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("unable to get the authentication configuration; %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
currentAuthentication := config.Authentications[config.CurrentAccount]
|
|
||||||
|
|
||||||
return NewClient(currentAuthentication), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
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) GetInstance() (model.InstanceV2, error) {
|
|
||||||
path := "/api/v2/instance"
|
|
||||||
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.InstanceV2{}, fmt.Errorf("unable to create the HTTP request, %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var instance model.InstanceV2
|
|
||||||
|
|
||||||
if err := g.sendRequest(request, &instance); err != nil {
|
|
||||||
return model.InstanceV2{}, fmt.Errorf("received an error after sending the request to get the instance details; %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return instance, 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,8 +0,0 @@
|
||||||
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,106 +0,0 @@
|
||||||
package model
|
|
||||||
|
|
||||||
type InstanceV2 struct {
|
|
||||||
AccountDomain string `json:"account_domain"`
|
|
||||||
Configuration InstanceConfiguration `json:"configuration"`
|
|
||||||
Contact InstanceV2Contact `json:"contact"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
Domain string `json:"domain"`
|
|
||||||
Languages []string `json:"languages"`
|
|
||||||
Registrations InstanceV2Registrations `json:"registrations"`
|
|
||||||
Rules []InstanceRule `json:"rules"`
|
|
||||||
SourceURL string `json:"source_url"`
|
|
||||||
Terms string `json:"terms"`
|
|
||||||
Thumbnail InstanceV2Thumbnail `json:"thumbnail"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Usage InstanceV2Usage `json:"usage"`
|
|
||||||
Version string `json:"version"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type InstanceConfiguration struct {
|
|
||||||
Accounts InstanceConfigurationAccounts `json:"accounts"`
|
|
||||||
Emojis InstanceConfigurationEmojis `json:"emojis"`
|
|
||||||
MediaAttachments InstanceConfigurationMediaAttachments `json:"media_attachments"`
|
|
||||||
Polls InstanceConfigurationPolls `json:"polls"`
|
|
||||||
Statuses InstanceConfigurationStatuses `json:"statuses"`
|
|
||||||
Translation InstanceV2ConfigurationTranslation `json:"translation"`
|
|
||||||
URLs InstanceV2URLs `json:"urls"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type InstanceConfigurationAccounts struct {
|
|
||||||
AllowCustomCSS bool `json:"allow_custom_css"`
|
|
||||||
MaxFeaturedTags int `json:"max_featured_tags"`
|
|
||||||
MaxProfileFields int `json:"max_profile_fields"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type InstanceConfigurationEmojis struct {
|
|
||||||
EmojiSizeLimit int `json:"emoji_size_limit"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type InstanceConfigurationMediaAttachments struct {
|
|
||||||
ImageMatrixLimit int `json:"image_matrix_limit"`
|
|
||||||
ImageSizeLimit int `json:"image_size_limit"`
|
|
||||||
SupportedMimeTypes []string `json:"supported_mime_types"`
|
|
||||||
VideoFrameRateLimit int `json:"video_frame_rate_limit"`
|
|
||||||
VideoMatrixLimit int `json:"video_matrix_limit"`
|
|
||||||
VideoSizeLimit int `json:"video_size_limit"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type InstanceConfigurationPolls struct {
|
|
||||||
MaxCharactersPerOption int `json:"max_characters_per_option"`
|
|
||||||
MaxExpiration int `json:"max_expiration"`
|
|
||||||
MaxOptions int `json:"max_options"`
|
|
||||||
MinExpiration int `json:"min_expiration"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type InstanceConfigurationStatuses struct {
|
|
||||||
CharactersReservedPerURL int `json:"characters_reserved_per_url"`
|
|
||||||
MaxCharacters int `json:"max_characters"`
|
|
||||||
MaxMediaAttachments int `json:"max_media_attachments"`
|
|
||||||
SupportedMimeTypes []string `json:"supported_mime_types"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type InstanceV2ConfigurationTranslation struct {
|
|
||||||
Enabled bool `json:"enabled"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type InstanceV2URLs struct {
|
|
||||||
Streaming string `json:"streaming"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type InstanceV2Contact struct {
|
|
||||||
Account Account `json:"account"`
|
|
||||||
Email string `json:"email"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type InstanceV2Registrations struct {
|
|
||||||
ApprovalRequired bool `json:"approval_required"`
|
|
||||||
Enabled bool `json:"enabled"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type InstanceRule struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Text string `json:"text"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type InstanceV2Thumbnail struct {
|
|
||||||
BlurHash string `json:"blurhash"`
|
|
||||||
ThumbnailDescription string `json:"thumbnail_description"`
|
|
||||||
ThumbnailType string `json:"thumbnail_type"`
|
|
||||||
URL string `json:"url"`
|
|
||||||
Version InstanceV2ThumbnailVersions `json:"versions"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type InstanceV2ThumbnailVersions struct {
|
|
||||||
Size1URL string `json:"@1x"`
|
|
||||||
Size2URL string `json:"@2x"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type InstanceV2Usage struct {
|
|
||||||
Users InstanceV2Users `json:"users"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type InstanceV2Users struct {
|
|
||||||
ActiveMonth int `json:"active_month"`
|
|
||||||
}
|
|
|
@ -3,26 +3,15 @@ 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
|
||||||
|
@ -35,27 +24,16 @@ Once you have the code please copy and paste it below.
|
||||||
|
|
||||||
`
|
`
|
||||||
|
|
||||||
func newLoginCommand(name, summary string) *loginCommand {
|
func loginWithOauth2() error {
|
||||||
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
|
||||||
|
|
||||||
if c.instance == "" {
|
var instance string
|
||||||
return errInstanceNotSet
|
|
||||||
}
|
|
||||||
|
|
||||||
instance := c.instance
|
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") {
|
if !strings.HasPrefix(instance, "https") || !strings.HasPrefix(instance, "http") {
|
||||||
instance = "https://" + instance
|
instance = "https://" + instance
|
||||||
|
@ -65,17 +43,17 @@ func (c *loginCommand) Execute() error {
|
||||||
instance = instance[:len(instance)-1]
|
instance = instance[:len(instance)-1]
|
||||||
}
|
}
|
||||||
|
|
||||||
authentication := config.Authentication{
|
authentication := Authentication{
|
||||||
Instance: instance,
|
Instance: instance,
|
||||||
}
|
}
|
||||||
|
|
||||||
gtsClient := client.NewClient(authentication)
|
client := newGtsClient(authentication)
|
||||||
|
|
||||||
if err := gtsClient.Register(); err != nil {
|
if err := client.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(gtsClient.Authentication)
|
consentPageURL := authCodeURL(client.authentication)
|
||||||
|
|
||||||
openLink(consentPageURL)
|
openLink(consentPageURL)
|
||||||
|
|
||||||
|
@ -88,62 +66,59 @@ func (c *loginCommand) Execute() error {
|
||||||
return fmt.Errorf("failed to read access code; %w", err)
|
return fmt.Errorf("failed to read access code; %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
gtsClient.Authentication, err = addAccessToken(gtsClient.Authentication, code)
|
client.authentication, err = addAccessToken(client.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 := gtsClient.VerifyCredentials()
|
account, err := client.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 := config.SaveAuthentication(account.Username, gtsClient.Authentication)
|
if err := saveAuthenticationConfig(account.Username, client.authentication); 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("Successfully logged into %s\n", loginName)
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func authCodeURL(account config.Authentication) string {
|
func authCodeURL(account 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", "write"},
|
||||||
RedirectURL: internal.RedirectUri,
|
RedirectURL: 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", internal.ApplicationName)
|
url := config.AuthCodeURL("state", oauth2.AccessTypeOffline) + fmt.Sprintf("&client_name=%s", applicationName)
|
||||||
|
|
||||||
return url
|
return url
|
||||||
}
|
}
|
||||||
|
|
||||||
func addAccessToken(authentication config.Authentication, code string) (config.Authentication, error) {
|
func addAccessToken(authentication Authentication, code string) (Authentication, error) {
|
||||||
ouauth2Conf := oauth2.Config{
|
config := oauth2.Config{
|
||||||
ClientID: authentication.ClientID,
|
ClientID: authentication.ClientID,
|
||||||
ClientSecret: authentication.ClientSecret,
|
ClientSecret: authentication.ClientSecret,
|
||||||
Scopes: []string{"read", "write"},
|
Scopes: []string{"read", "write"},
|
||||||
RedirectURL: internal.RedirectUri,
|
RedirectURL: 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 := ouauth2Conf.Exchange(context.Background(), code)
|
token, err := config.Exchange(context.Background(), code)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return config.Authentication{}, fmt.Errorf("unable to exchange the code for an access token; %w", err)
|
return 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 config.Authentication{}, errEmptyAccessToken
|
return Authentication{}, errEmptyAccessToken
|
||||||
}
|
}
|
||||||
|
|
||||||
authentication.AccessToken = token.AccessToken
|
authentication.AccessToken = token.AccessToken
|
20
main.go
Normal file
20
main.go
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package client
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
@ -6,8 +6,6 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"codeflow.dananglin.me.uk/apollo/enbas/internal"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type RegisterRequest struct {
|
type RegisterRequest struct {
|
||||||
|
@ -27,12 +25,12 @@ type RegisterResponse struct {
|
||||||
Website string `json:"website"`
|
Website string `json:"website"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *Client) Register() error {
|
func (g *gtsClient) register() error {
|
||||||
params := RegisterRequest{
|
params := RegisterRequest{
|
||||||
ClientName: internal.ApplicationName,
|
ClientName: applicationName,
|
||||||
RedirectUris: internal.RedirectUri,
|
RedirectUris: redirectUri,
|
||||||
Scopes: "read write",
|
Scopes: "read write",
|
||||||
Website: internal.ApplicationWebsite,
|
Website: applicationWebsite,
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := json.Marshal(params)
|
data, err := json.Marshal(params)
|
||||||
|
@ -43,9 +41,9 @@ func (g *Client) 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)
|
||||||
|
@ -59,8 +57,8 @@ func (g *Client) 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
|
||||||
}
|
}
|
Loading…
Reference in a new issue