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/
|
||||
/enbas
|
||||
|
|
|
@ -13,14 +13,6 @@ output:
|
|||
sort-results: true
|
||||
|
||||
linters-settings:
|
||||
depguard:
|
||||
rules:
|
||||
main:
|
||||
files:
|
||||
- $all
|
||||
allow:
|
||||
- $gostd
|
||||
- codeflow.dananglin.me.uk/apollo/enbas
|
||||
lll:
|
||||
line-length: 140
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package model
|
||||
package main
|
||||
|
||||
type Account struct {
|
||||
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 (
|
||||
"encoding/json"
|
||||
|
@ -7,8 +7,6 @@ import (
|
|||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"codeflow.dananglin.me.uk/apollo/enbas/internal"
|
||||
)
|
||||
|
||||
type AuthenticationConfig struct {
|
||||
|
@ -23,9 +21,9 @@ type Authentication struct {
|
|||
AccessToken string `json:"accessToken"`
|
||||
}
|
||||
|
||||
func SaveAuthentication(username string, authentication Authentication) (string, error) {
|
||||
func saveAuthenticationConfig(username string, authentication Authentication) error {
|
||||
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
|
||||
|
@ -34,14 +32,14 @@ func SaveAuthentication(username string, authentication Authentication) (string,
|
|||
|
||||
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)
|
||||
return fmt.Errorf("unknown error received when running stat on %s; %w", filepath, err)
|
||||
}
|
||||
|
||||
config.Authentications = make(map[string]Authentication)
|
||||
} else {
|
||||
config, err = NewAuthenticationConfigFromFile()
|
||||
config, err = newAuthenticationConfigFromFile()
|
||||
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())
|
||||
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 {
|
||||
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()
|
||||
|
||||
file, err := os.Open(path)
|
||||
|
@ -99,7 +97,7 @@ func configDir() string {
|
|||
rootDir = "."
|
||||
}
|
||||
|
||||
return filepath.Join(rootDir, internal.ApplicationName)
|
||||
return filepath.Join(rootDir, applicationName)
|
||||
}
|
||||
|
||||
func ensureConfigDir() error {
|
|
@ -5,8 +5,6 @@ package main
|
|||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"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)
|
||||
}
|
||||
|
||||
flags := ldflags()
|
||||
return sh.Run("go", "build", "-ldflags="+flags, "-a", "-o", binary, "./cmd/enbas")
|
||||
main := "main.go"
|
||||
return sh.Run("go", "build", "-o", binary, main)
|
||||
}
|
||||
|
||||
// Clean clean the workspace.
|
||||
|
@ -81,31 +79,3 @@ func changeToProjectRoot() error {
|
|||
|
||||
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 (
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"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"
|
||||
)
|
||||
|
||||
type loginCommand struct {
|
||||
*flag.FlagSet
|
||||
summary string
|
||||
instance string
|
||||
}
|
||||
|
||||
var errEmptyAccessToken = errors.New("received an empty access token")
|
||||
var errInstanceNotSet = errors.New("the instance flag is not set")
|
||||
|
||||
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
|
||||
|
@ -35,27 +24,16 @@ Once you have the code please copy and paste it below.
|
|||
|
||||
`
|
||||
|
||||
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 {
|
||||
func loginWithOauth2() error {
|
||||
var err error
|
||||
|
||||
if c.instance == "" {
|
||||
return errInstanceNotSet
|
||||
}
|
||||
var instance string
|
||||
|
||||
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") {
|
||||
instance = "https://" + instance
|
||||
|
@ -65,17 +43,17 @@ func (c *loginCommand) Execute() error {
|
|||
instance = instance[:len(instance)-1]
|
||||
}
|
||||
|
||||
authentication := config.Authentication{
|
||||
authentication := Authentication{
|
||||
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)
|
||||
}
|
||||
|
||||
consentPageURL := authCodeURL(gtsClient.Authentication)
|
||||
consentPageURL := authCodeURL(client.authentication)
|
||||
|
||||
openLink(consentPageURL)
|
||||
|
||||
|
@ -88,62 +66,59 @@ func (c *loginCommand) Execute() error {
|
|||
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 {
|
||||
return fmt.Errorf("unable to get the access token; %w", err)
|
||||
}
|
||||
|
||||
account, err := gtsClient.VerifyCredentials()
|
||||
account, err := client.verifyCredentials()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to verify the credentials; %w", err)
|
||||
}
|
||||
|
||||
loginName, err := config.SaveAuthentication(account.Username, gtsClient.Authentication)
|
||||
if err != nil {
|
||||
if err := saveAuthenticationConfig(account.Username, client.authentication); err != nil {
|
||||
return fmt.Errorf("unable to save the authentication details; %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Successfully logged into %s\n", loginName)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func authCodeURL(account config.Authentication) string {
|
||||
func authCodeURL(account Authentication) string {
|
||||
config := oauth2.Config{
|
||||
ClientID: account.ClientID,
|
||||
ClientSecret: account.ClientSecret,
|
||||
Scopes: []string{"read"},
|
||||
RedirectURL: internal.RedirectUri,
|
||||
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", internal.ApplicationName)
|
||||
url := config.AuthCodeURL("state", oauth2.AccessTypeOffline) + fmt.Sprintf("&client_name=%s", applicationName)
|
||||
|
||||
return url
|
||||
}
|
||||
|
||||
func addAccessToken(authentication config.Authentication, code string) (config.Authentication, error) {
|
||||
ouauth2Conf := oauth2.Config{
|
||||
func addAccessToken(authentication Authentication, code string) (Authentication, error) {
|
||||
config := oauth2.Config{
|
||||
ClientID: authentication.ClientID,
|
||||
ClientSecret: authentication.ClientSecret,
|
||||
Scopes: []string{"read", "write"},
|
||||
RedirectURL: internal.RedirectUri,
|
||||
RedirectURL: redirectUri,
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: authentication.Instance + "/oauth/authorize",
|
||||
TokenURL: authentication.Instance + "/oauth/token",
|
||||
},
|
||||
}
|
||||
|
||||
token, err := ouauth2Conf.Exchange(context.Background(), code)
|
||||
token, err := config.Exchange(context.Background(), code)
|
||||
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 == "" {
|
||||
return config.Authentication{}, errEmptyAccessToken
|
||||
return Authentication{}, errEmptyAccessToken
|
||||
}
|
||||
|
||||
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 (
|
||||
"bytes"
|
||||
|
@ -6,8 +6,6 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"codeflow.dananglin.me.uk/apollo/enbas/internal"
|
||||
)
|
||||
|
||||
type RegisterRequest struct {
|
||||
|
@ -27,12 +25,12 @@ type RegisterResponse struct {
|
|||
Website string `json:"website"`
|
||||
}
|
||||
|
||||
func (g *Client) Register() error {
|
||||
func (g *gtsClient) register() error {
|
||||
params := RegisterRequest{
|
||||
ClientName: internal.ApplicationName,
|
||||
RedirectUris: internal.RedirectUri,
|
||||
ClientName: applicationName,
|
||||
RedirectUris: redirectUri,
|
||||
Scopes: "read write",
|
||||
Website: internal.ApplicationWebsite,
|
||||
Website: applicationWebsite,
|
||||
}
|
||||
|
||||
data, err := json.Marshal(params)
|
||||
|
@ -43,9 +41,9 @@ func (g *Client) Register() error {
|
|||
requestBody := bytes.NewBuffer(data)
|
||||
|
||||
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()
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
g.Authentication.ClientID = registerResponse.ClientID
|
||||
g.Authentication.ClientSecret = registerResponse.ClientSecret
|
||||
g.authentication.ClientID = registerResponse.ClientID
|
||||
g.authentication.ClientSecret = registerResponse.ClientSecret
|
||||
|
||||
return nil
|
||||
}
|
Loading…
Reference in a new issue