feat: add Enbas
Add Enbas code. So far Enbas can: - Allow the user to register the application and log into GTS on their behalf. The scope is limited to read for now. - Show instance details. - Show local and remote accounts.
This commit is contained in:
parent
2544fe4fcf
commit
2c5123253a
21 changed files with 1342 additions and 11 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
/environment/
|
||||||
|
/enbas
|
|
@ -13,10 +13,19 @@ 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
|
||||||
|
|
||||||
linters:
|
linters:
|
||||||
enable-all: true
|
enable-all: true
|
||||||
# disable:
|
disable:
|
||||||
|
#- json
|
||||||
fast: false
|
fast: false
|
||||||
|
|
159
cmd/enbas/login.go
Normal file
159
cmd/enbas/login.go
Normal file
|
@ -0,0 +1,159 @@
|
||||||
|
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
|
||||||
|
instance string
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
errEmptyAccessToken = errors.New("received an empty access token")
|
||||||
|
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
|
||||||
|
the application's login process. Your browser may have opened the link to the consent page already. If not, please
|
||||||
|
copy and paste the link below to your browser:
|
||||||
|
|
||||||
|
%s
|
||||||
|
|
||||||
|
Once you have the code please copy and paste it below.
|
||||||
|
|
||||||
|
`
|
||||||
|
|
||||||
|
func newLoginCommand(name, summary string) *loginCommand {
|
||||||
|
command := loginCommand{
|
||||||
|
FlagSet: flag.NewFlagSet(name, flag.ExitOnError),
|
||||||
|
instance: "",
|
||||||
|
}
|
||||||
|
|
||||||
|
command.StringVar(&command.instance, "instance", "", "specify the instance that you want to login to.")
|
||||||
|
|
||||||
|
command.Usage = commandUsageFunc(name, summary, command.FlagSet)
|
||||||
|
|
||||||
|
return &command
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *loginCommand) Execute() error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if c.instance == "" {
|
||||||
|
return errInstanceNotSet
|
||||||
|
}
|
||||||
|
|
||||||
|
instance := c.instance
|
||||||
|
|
||||||
|
if !strings.HasPrefix(instance, "https") || !strings.HasPrefix(instance, "http") {
|
||||||
|
instance = "https://" + instance
|
||||||
|
}
|
||||||
|
|
||||||
|
for strings.HasSuffix(instance, "/") {
|
||||||
|
instance = instance[:len(instance)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
authentication := config.Authentication{
|
||||||
|
Instance: instance,
|
||||||
|
}
|
||||||
|
|
||||||
|
gtsClient := client.NewClient(authentication)
|
||||||
|
|
||||||
|
if err := gtsClient.Register(); err != nil {
|
||||||
|
return fmt.Errorf("unable to register the application; %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
oauth2Conf := oauth2.Config{
|
||||||
|
ClientID: gtsClient.Authentication.ClientID,
|
||||||
|
ClientSecret: gtsClient.Authentication.ClientSecret,
|
||||||
|
Scopes: []string{"read"},
|
||||||
|
RedirectURL: internal.RedirectUri,
|
||||||
|
Endpoint: oauth2.Endpoint{
|
||||||
|
AuthURL: gtsClient.Authentication.Instance + "/oauth/authorize",
|
||||||
|
TokenURL: gtsClient.Authentication.Instance + "/oauth/token",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
consentPageURL := authCodeURL(oauth2Conf)
|
||||||
|
|
||||||
|
openLink(consentPageURL)
|
||||||
|
|
||||||
|
fmt.Printf(consentMessageFormat, consentPageURL)
|
||||||
|
|
||||||
|
var code string
|
||||||
|
fmt.Print("Out-of-band token: ")
|
||||||
|
|
||||||
|
if _, err := fmt.Scanln(&code); err != nil {
|
||||||
|
return fmt.Errorf("failed to read access code; %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
gtsClient.Authentication, err = addAccessToken(gtsClient.Authentication, oauth2Conf, code)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to get the access token; %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
account, err := gtsClient.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 {
|
||||||
|
return fmt.Errorf("unable to save the authentication details; %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Successfully logged into %s\n", loginName)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func authCodeURL(oauth2Conf oauth2.Config) string {
|
||||||
|
url := oauth2Conf.AuthCodeURL(
|
||||||
|
"state",
|
||||||
|
oauth2.AccessTypeOffline,
|
||||||
|
) + "&client_name=" + internal.ApplicationName
|
||||||
|
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
func addAccessToken(authentication config.Authentication, oauth2Conf oauth2.Config, code string) (config.Authentication, error) {
|
||||||
|
token, err := oauth2Conf.Exchange(context.Background(), code)
|
||||||
|
if err != nil {
|
||||||
|
return config.Authentication{}, fmt.Errorf("unable to exchange the code for an access token; %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if token == nil || token.AccessToken == "" {
|
||||||
|
return config.Authentication{}, errEmptyAccessToken
|
||||||
|
}
|
||||||
|
|
||||||
|
authentication.AccessToken = token.AccessToken
|
||||||
|
|
||||||
|
return authentication, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func openLink(url string) {
|
||||||
|
var open string
|
||||||
|
|
||||||
|
if runtime.GOOS == "linux" {
|
||||||
|
open = "xdg-open"
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
command := exec.Command(open, url)
|
||||||
|
|
||||||
|
_ = command.Start()
|
||||||
|
}
|
75
cmd/enbas/main.go
Normal file
75
cmd/enbas/main.go
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Executor interface {
|
||||||
|
Name() string
|
||||||
|
Parse([]string) error
|
||||||
|
Execute() error
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if err := run(); err != nil {
|
||||||
|
fmt.Printf("ERROR: %v.\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func run() error {
|
||||||
|
const (
|
||||||
|
login string = "login"
|
||||||
|
version string = "version"
|
||||||
|
show string = "show"
|
||||||
|
switchAccount string = "switch"
|
||||||
|
)
|
||||||
|
|
||||||
|
summaries := map[string]string{
|
||||||
|
login: "login to an account on GoToSocial",
|
||||||
|
version: "print the application's version and build information",
|
||||||
|
show: "print details about a specified resource",
|
||||||
|
switchAccount: "switch to an account",
|
||||||
|
}
|
||||||
|
|
||||||
|
flag.Usage = enbasUsageFunc(summaries)
|
||||||
|
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if flag.NArg() < 1 {
|
||||||
|
flag.Usage()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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 show:
|
||||||
|
executor = newShowCommand(show, summaries[show])
|
||||||
|
case switchAccount:
|
||||||
|
executor = newSwitchCommand(switchAccount, summaries[switchAccount])
|
||||||
|
default:
|
||||||
|
flag.Usage()
|
||||||
|
return fmt.Errorf("unknown subcommand %q", subcommand)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := executor.Parse(args); err != nil {
|
||||||
|
return fmt.Errorf("unable to parse the command line flags; %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := executor.Execute(); err != nil {
|
||||||
|
return fmt.Errorf("received an error after executing %q; %w", executor.Name(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
197
cmd/enbas/show.go
Normal file
197
cmd/enbas/show.go
Normal file
|
@ -0,0 +1,197 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
|
"codeflow.dananglin.me.uk/apollo/enbas/internal/client"
|
||||||
|
"codeflow.dananglin.me.uk/apollo/enbas/internal/config"
|
||||||
|
"golang.org/x/net/html"
|
||||||
|
)
|
||||||
|
|
||||||
|
var instanceDetailsFormat = `INSTANCE:
|
||||||
|
%s - %s
|
||||||
|
|
||||||
|
DOMAIN:
|
||||||
|
%s
|
||||||
|
|
||||||
|
VERSION:
|
||||||
|
Running GoToSocial %s
|
||||||
|
|
||||||
|
CONTACT:
|
||||||
|
name: %s
|
||||||
|
username: %s
|
||||||
|
email: %s
|
||||||
|
`
|
||||||
|
|
||||||
|
var accountDetailsFormat = `
|
||||||
|
%s (@%s)
|
||||||
|
|
||||||
|
ACCOUNT ID:
|
||||||
|
%s
|
||||||
|
|
||||||
|
JOINED ON:
|
||||||
|
%s
|
||||||
|
|
||||||
|
STATS:
|
||||||
|
Followers: %d
|
||||||
|
Following: %d
|
||||||
|
Statuses: %d
|
||||||
|
|
||||||
|
BIOGRAPHY:
|
||||||
|
%s
|
||||||
|
|
||||||
|
METADATA: %s
|
||||||
|
|
||||||
|
ACCOUNT URL:
|
||||||
|
%s
|
||||||
|
`
|
||||||
|
|
||||||
|
type showCommand struct {
|
||||||
|
*flag.FlagSet
|
||||||
|
targetType string
|
||||||
|
account string
|
||||||
|
myAccount bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func newShowCommand(name, summary string) *showCommand {
|
||||||
|
command := showCommand{
|
||||||
|
FlagSet: flag.NewFlagSet(name, flag.ExitOnError),
|
||||||
|
targetType: "",
|
||||||
|
myAccount: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
command.StringVar(&command.targetType, "type", "", "specify the type of resource to display")
|
||||||
|
command.StringVar(&command.account, "account", "", "specify the account URI to lookup")
|
||||||
|
command.BoolVar(&command.myAccount, "my-account", false, "set to true to lookup your account")
|
||||||
|
command.Usage = commandUsageFunc(name, summary, command.FlagSet)
|
||||||
|
|
||||||
|
return &command
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *showCommand) Execute() error {
|
||||||
|
gtsClient, err := client.NewClientFromConfig()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to create the GoToSocial client; %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
funcMap := map[string]func(*client.Client) error{
|
||||||
|
"instance": c.showInstance,
|
||||||
|
"account": c.showAccount,
|
||||||
|
}
|
||||||
|
|
||||||
|
doFunc, ok := funcMap[c.targetType]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("unsupported type %q", c.targetType)
|
||||||
|
}
|
||||||
|
|
||||||
|
return doFunc(gtsClient)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *showCommand) showInstance(gts *client.Client) error {
|
||||||
|
instance, err := gts.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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *showCommand) showAccount(gts *client.Client) error {
|
||||||
|
var accountURI string
|
||||||
|
|
||||||
|
if c.myAccount {
|
||||||
|
authConfig, err := config.NewAuthenticationConfigFromFile()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to retrieve the authentication configuration; %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
accountURI = authConfig.CurrentAccount
|
||||||
|
} else {
|
||||||
|
if c.account == "" {
|
||||||
|
return errors.New("the account flag is not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
accountURI = c.account
|
||||||
|
}
|
||||||
|
|
||||||
|
account, err := gts.GetAccount(accountURI)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to retrieve the account details; %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata := ""
|
||||||
|
|
||||||
|
for _, field := range account.Fields {
|
||||||
|
metadata += fmt.Sprintf("\n %s: %s", field.Name, stripHTMLTags(field.Value))
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf(
|
||||||
|
accountDetailsFormat,
|
||||||
|
account.DisplayName,
|
||||||
|
account.Username,
|
||||||
|
account.ID,
|
||||||
|
account.CreatedAt.Format("02 Jan 2006"),
|
||||||
|
account.FollowersCount,
|
||||||
|
account.FollowingCount,
|
||||||
|
account.StatusCount,
|
||||||
|
wrapLine(stripHTMLTags(account.Note), "\n ", 80),
|
||||||
|
metadata,
|
||||||
|
account.URL,
|
||||||
|
)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func stripHTMLTags(text string) string {
|
||||||
|
token := html.NewTokenizer(strings.NewReader(text))
|
||||||
|
|
||||||
|
var builder strings.Builder
|
||||||
|
|
||||||
|
for {
|
||||||
|
tt := token.Next()
|
||||||
|
switch tt {
|
||||||
|
case html.ErrorToken:
|
||||||
|
return builder.String()
|
||||||
|
case html.TextToken:
|
||||||
|
builder.WriteString(token.Token().Data + " ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func wrapLine(line, separator string, charLimit int) string {
|
||||||
|
if len(line) <= charLimit {
|
||||||
|
return line
|
||||||
|
}
|
||||||
|
|
||||||
|
leftcursor, rightcursor := 0, 0
|
||||||
|
|
||||||
|
var builder strings.Builder
|
||||||
|
|
||||||
|
for rightcursor < (len(line) - charLimit) {
|
||||||
|
rightcursor += charLimit
|
||||||
|
for !unicode.IsSpace(rune(line[rightcursor-1])) {
|
||||||
|
rightcursor--
|
||||||
|
}
|
||||||
|
builder.WriteString(line[leftcursor:rightcursor] + separator)
|
||||||
|
leftcursor = rightcursor
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.WriteString(line[rightcursor:])
|
||||||
|
|
||||||
|
return builder.String()
|
||||||
|
}
|
35
cmd/enbas/switch.go
Normal file
35
cmd/enbas/switch.go
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"codeflow.dananglin.me.uk/apollo/enbas/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
type switchCommand struct {
|
||||||
|
*flag.FlagSet
|
||||||
|
toAccount string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSwitchCommand(name, summary string) *switchCommand {
|
||||||
|
command := switchCommand{
|
||||||
|
FlagSet: flag.NewFlagSet(name, flag.ExitOnError),
|
||||||
|
toAccount: "",
|
||||||
|
}
|
||||||
|
|
||||||
|
command.StringVar(&command.toAccount, "to-account", "", "the account to switch to")
|
||||||
|
command.Usage = commandUsageFunc(name, summary, command.FlagSet)
|
||||||
|
|
||||||
|
return &command
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *switchCommand) Execute() error {
|
||||||
|
if err := config.UpdateCurrentAccount(c.toAccount); err != nil {
|
||||||
|
return fmt.Errorf("unable to switch accounts; %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("The current account is now set to %q.\n", c.toAccount)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
76
cmd/enbas/usage.go
Normal file
76
cmd/enbas/usage.go
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
62
cmd/enbas/version.go
Normal file
62
cmd/enbas/version.go
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
binaryVersion string
|
||||||
|
buildTime string
|
||||||
|
goVersion string
|
||||||
|
gitCommit string
|
||||||
|
)
|
||||||
|
|
||||||
|
type versionCommand struct {
|
||||||
|
*flag.FlagSet
|
||||||
|
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,
|
||||||
|
showFullVersion: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
command.BoolVar(&command.showFullVersion, "full", false, "prints the full build information")
|
||||||
|
|
||||||
|
command.Usage = commandUsageFunc(name, 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
|
||||||
|
}
|
13
go.mod
13
go.mod
|
@ -1,3 +1,14 @@
|
||||||
module enbas
|
module codeflow.dananglin.me.uk/apollo/enbas
|
||||||
|
|
||||||
go 1.22.0
|
go 1.22.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
golang.org/x/net v0.21.0
|
||||||
|
golang.org/x/oauth2 v0.17.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/golang/protobuf v1.5.3 // indirect
|
||||||
|
google.golang.org/appengine v1.6.7 // indirect
|
||||||
|
google.golang.org/protobuf v1.31.0 // indirect
|
||||||
|
)
|
||||||
|
|
24
go.sum
Normal file
24
go.sum
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
|
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||||
|
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||||
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||||
|
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||||
|
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
|
||||||
|
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||||
|
golang.org/x/oauth2 v0.17.0 h1:6m3ZPmLEFdVxKKWnKq4VqZ60gutO35zm+zrAHVmHyDQ=
|
||||||
|
golang.org/x/oauth2 v0.17.0/go.mod h1:OzPDGQiuQMguemayvdylqddI7qcD9lnSDb+1FiwQ5HA=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
|
||||||
|
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||||
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
|
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||||
|
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||||
|
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
125
internal/client/client.go
Normal file
125
internal/client/client.go
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"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
|
||||||
|
|
||||||
|
var account model.Account
|
||||||
|
|
||||||
|
if err := g.sendRequest(http.MethodGet, url, nil, &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
|
||||||
|
|
||||||
|
var instance model.InstanceV2
|
||||||
|
|
||||||
|
if err := g.sendRequest(http.MethodGet, url, nil, &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) GetAccount(accountURI string) (model.Account, error) {
|
||||||
|
path := "/api/v1/accounts/lookup"
|
||||||
|
url := g.Authentication.Instance + path + "?acct=" + accountURI
|
||||||
|
|
||||||
|
var account model.Account
|
||||||
|
|
||||||
|
if err := g.sendRequest(http.MethodGet, url, nil, &account); err != nil {
|
||||||
|
return model.Account{}, fmt.Errorf("received an error after sending the request to get the account information; %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return account, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Client) sendRequest(method string, url string, requestBody io.Reader, object any) error {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), g.Timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
request, err := http.NewRequestWithContext(ctx, method, url, requestBody)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to create the HTTP request, %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
48
internal/client/register.go
Normal file
48
internal/client/register.go
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"codeflow.dananglin.me.uk/apollo/enbas/internal"
|
||||||
|
"codeflow.dananglin.me.uk/apollo/enbas/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RegisterRequest struct {
|
||||||
|
ClientName string `json:"client_name"`
|
||||||
|
RedirectUris string `json:"redirect_uris"`
|
||||||
|
Scopes string `json:"scopes"`
|
||||||
|
Website string `json:"website"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Client) Register() error {
|
||||||
|
params := RegisterRequest{
|
||||||
|
ClientName: internal.ApplicationName,
|
||||||
|
RedirectUris: internal.RedirectUri,
|
||||||
|
Scopes: "read write",
|
||||||
|
Website: internal.ApplicationWebsite,
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(params)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to marshal the request body; %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
requestBody := bytes.NewBuffer(data)
|
||||||
|
|
||||||
|
path := "/api/v1/apps"
|
||||||
|
url := g.Authentication.Instance + path
|
||||||
|
|
||||||
|
var app model.Application
|
||||||
|
|
||||||
|
if err := g.sendRequest(http.MethodPost, url, requestBody, &app); err != nil {
|
||||||
|
return fmt.Errorf("received an error after sending the registration request; %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
g.Authentication.ClientID = app.ClientID
|
||||||
|
g.Authentication.ClientSecret = app.ClientSecret
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
151
internal/config/config.go
Normal file
151
internal/config/config.go
Normal file
|
@ -0,0 +1,151 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"codeflow.dananglin.me.uk/apollo/enbas/internal"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AuthenticationConfig struct {
|
||||||
|
CurrentAccount string `json:"currentAccount"`
|
||||||
|
Authentications map[string]Authentication `json:"authentications"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Authentication struct {
|
||||||
|
Instance string `json:"instance"`
|
||||||
|
ClientID string `json:"clientId"`
|
||||||
|
ClientSecret string `json:"clientSecret"`
|
||||||
|
AccessToken string `json:"accessToken"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func SaveAuthentication(username string, authentication Authentication) (string, error) {
|
||||||
|
if err := ensureConfigDir(); err != nil {
|
||||||
|
return "", fmt.Errorf("unable to ensure the configuration directory; %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var authConfig AuthenticationConfig
|
||||||
|
|
||||||
|
filepath := authenticationConfigFile()
|
||||||
|
|
||||||
|
if _, err := os.Stat(filepath); err != nil {
|
||||||
|
if !errors.Is(err, os.ErrNotExist) {
|
||||||
|
return "", fmt.Errorf("unknown error received when running stat on %s; %w", filepath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
authConfig.Authentications = make(map[string]Authentication)
|
||||||
|
} else {
|
||||||
|
authConfig, err = NewAuthenticationConfigFromFile()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("unable to retrieve the existing authentication configuration; %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
instance := ""
|
||||||
|
|
||||||
|
if strings.HasPrefix(authentication.Instance, "https://") {
|
||||||
|
instance = strings.TrimPrefix(authentication.Instance, "https://")
|
||||||
|
} else if strings.HasPrefix(authentication.Instance, "http://") {
|
||||||
|
instance = strings.TrimPrefix(authentication.Instance, "http://")
|
||||||
|
}
|
||||||
|
|
||||||
|
authenticationName := username + "@" + instance
|
||||||
|
|
||||||
|
authConfig.CurrentAccount = authenticationName
|
||||||
|
|
||||||
|
authConfig.Authentications[authenticationName] = authentication
|
||||||
|
|
||||||
|
if err := saveAuthenticationFile(authConfig); err != nil {
|
||||||
|
return "", fmt.Errorf("unable to save the authentication configuration to file; %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return authenticationName, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAuthenticationConfigFromFile() (AuthenticationConfig, error) {
|
||||||
|
path := authenticationConfigFile()
|
||||||
|
|
||||||
|
file, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return AuthenticationConfig{}, fmt.Errorf("unable to open %s, %w", path, err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
var authConfig AuthenticationConfig
|
||||||
|
|
||||||
|
if err := json.NewDecoder(file).Decode(&authConfig); err != nil {
|
||||||
|
return AuthenticationConfig{}, fmt.Errorf("unable to decode the JSON data; %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return authConfig, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateCurrentAccount(account string) error {
|
||||||
|
authConfig, err := NewAuthenticationConfigFromFile()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to retrieve the existing authentication configuration; %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := authConfig.Authentications[account]; !ok {
|
||||||
|
return fmt.Errorf("account %s is not found", account)
|
||||||
|
}
|
||||||
|
|
||||||
|
authConfig.CurrentAccount = account
|
||||||
|
|
||||||
|
if err := saveAuthenticationFile(authConfig); err != nil {
|
||||||
|
return fmt.Errorf("unable to save the authentication configuration to file; %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func authenticationConfigFile() string {
|
||||||
|
return filepath.Join(configDir(), "authentications.json")
|
||||||
|
}
|
||||||
|
|
||||||
|
func configDir() string {
|
||||||
|
rootDir, err := os.UserConfigDir()
|
||||||
|
if err != nil {
|
||||||
|
rootDir = "."
|
||||||
|
}
|
||||||
|
|
||||||
|
return filepath.Join(rootDir, internal.ApplicationName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureConfigDir() error {
|
||||||
|
dir := configDir()
|
||||||
|
|
||||||
|
if _, err := os.Stat(dir); err != nil {
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
if err := os.MkdirAll(dir, 0o750); err != nil {
|
||||||
|
return fmt.Errorf("unable to create %s; %w", dir, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("unknown error received when running stat on %s; %w", dir, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveAuthenticationFile(authConfig AuthenticationConfig) error {
|
||||||
|
file, err := os.Create(authenticationConfigFile())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to open the config file; %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
encoder := json.NewEncoder(file)
|
||||||
|
encoder.SetIndent("", " ")
|
||||||
|
|
||||||
|
if err := encoder.Encode(authConfig); err != nil {
|
||||||
|
return fmt.Errorf("unable to save the JSON data to the authentication config file; %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
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"
|
||||||
|
)
|
52
internal/model/account.go
Normal file
52
internal/model/account.go
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type Account struct {
|
||||||
|
Acct string `json:"acct"`
|
||||||
|
Avatar string `json:"avatar"`
|
||||||
|
AvatarStatic string `json:"avatar_static"`
|
||||||
|
Bot bool `json:"bot"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
CustomCSS string `json:"custom_css"`
|
||||||
|
Discoverable bool `json:"discoverable"`
|
||||||
|
DisplayName string `json:"display_name"`
|
||||||
|
Emojis []Emoji `json:"emojis"`
|
||||||
|
EnableRSS bool `json:"enable_rss"`
|
||||||
|
Fields []Field `json:"fields"`
|
||||||
|
FollowersCount int `json:"followers_count"`
|
||||||
|
FollowingCount int `json:"following_count"`
|
||||||
|
Header string `json:"header"`
|
||||||
|
HeaderStatic string `json:"header_static"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
LastStatusAt string `json:"last_status_at"`
|
||||||
|
Locked bool `json:"locked"`
|
||||||
|
MuteExpiresAt time.Time `json:"mute_expires_at"`
|
||||||
|
Note string `json:"note"`
|
||||||
|
Role AccountRole `json:"role"`
|
||||||
|
Source Source `json:"source"`
|
||||||
|
StatusCount int `json:"statuses_count"`
|
||||||
|
Suspended bool `json:"suspended"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AccountRole struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Source struct {
|
||||||
|
Fields []Field `json:"fields"`
|
||||||
|
FollowRequestCount int `json:"follow_requests_count"`
|
||||||
|
Language string `json:"language"`
|
||||||
|
Note string `json:"note"`
|
||||||
|
Privacy string `json:"string"`
|
||||||
|
Sensitive bool `json:"sensitive"`
|
||||||
|
StatusContentType string `json:"status_content_type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Field struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
VerifiedAt string `json:"verified_at"`
|
||||||
|
}
|
11
internal/model/application.go
Normal file
11
internal/model/application.go
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
type Application struct {
|
||||||
|
ClientID string `json:"client_id"`
|
||||||
|
ClientSecret string `json:"client_secret"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
RedirectUri string `json:"redirect_uri"`
|
||||||
|
VapidKey string `json:"vapid_key"`
|
||||||
|
Website string `json:"website"`
|
||||||
|
}
|
9
internal/model/emoji.go
Normal file
9
internal/model/emoji.go
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
type Emoji struct {
|
||||||
|
Category string `json:"category"`
|
||||||
|
Shortcode string `json:"shortcode"`
|
||||||
|
StaticURL string `json:"static_url"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
VisibleInPicker bool `json:"visible_in_picker"`
|
||||||
|
}
|
106
internal/model/instance_v2.go
Normal file
106
internal/model/instance_v2.go
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
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"`
|
||||||
|
}
|
148
internal/model/status.go
Normal file
148
internal/model/status.go
Normal file
|
@ -0,0 +1,148 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type Status struct {
|
||||||
|
Account Account `json:"account"`
|
||||||
|
Application Application `json:"application"`
|
||||||
|
Bookmarked bool `json:"bookmarked"`
|
||||||
|
Card Card `json:"card"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
Emojis []Emoji `json:"emojis"`
|
||||||
|
Favourited bool `json:"favourited"`
|
||||||
|
FavouritesCount int `json:"favourites_count"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
InReplyToAccountID string `json:"in_reply_to_account_id"`
|
||||||
|
InReplyToID string `json:"in_reply_to_id"`
|
||||||
|
Language string `json:"language"`
|
||||||
|
MediaAttachments []Attachment `json:"media_attachments"`
|
||||||
|
Mentions []Mention `json:"mentions"`
|
||||||
|
Muted bool `json:"muted"`
|
||||||
|
Pinned bool `json:"pinned"`
|
||||||
|
Poll Poll `json:"poll"`
|
||||||
|
Reblog StatusReblogged `json:"reblog"`
|
||||||
|
Reblogged bool `json:"reblogged"`
|
||||||
|
RebloggsCount int `json:"reblogs_count"`
|
||||||
|
RepliesCount int `json:"replies_count"`
|
||||||
|
Sensitive bool `json:"sensitive"`
|
||||||
|
SpolierText string `json:"spoiler_text"`
|
||||||
|
Tags []Tag `json:"tags"`
|
||||||
|
Text string `json:"text"`
|
||||||
|
URI string `json:"uri"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
Visibility string `json:"visibility"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Card struct {
|
||||||
|
AuthorName string `json:"author_name"`
|
||||||
|
AuthorURL string `json:"author_url"`
|
||||||
|
Blurhash string `json:"blurhash"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
EmbedURL string `json:"embed_url"`
|
||||||
|
HTML string `json:"html"`
|
||||||
|
Image string `json:"image"`
|
||||||
|
ProviderName string `json:"provider_name"`
|
||||||
|
ProviderURL string `json:"provider_url"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
Height int `json:"height"`
|
||||||
|
Width int `json:"width"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Mention struct {
|
||||||
|
Acct string `json:"acct"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Poll struct {
|
||||||
|
Emojis []Emoji `json:"emojis"`
|
||||||
|
Expired bool `json:"expired"`
|
||||||
|
Voted bool `json:"voted"`
|
||||||
|
Multiple bool `json:"multiple"`
|
||||||
|
ExpiredAt time.Time `json:"expires_at"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
OwnVotes []int `json:"own_votes"`
|
||||||
|
VotersCount int `json:"voters_count"`
|
||||||
|
VotesCount int `json:"votes_count"`
|
||||||
|
Options []PollOption `json:"options"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PollOption struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
VotesCount string `json:"votes_count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type StatusReblogged struct {
|
||||||
|
Account Account `json:"account"`
|
||||||
|
Application Application `json:"application"`
|
||||||
|
Bookmarked bool `json:"bookmarked"`
|
||||||
|
Card Card `json:"card"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
Emojis []Emoji `json:"emojis"`
|
||||||
|
Favourited bool `json:"favourited"`
|
||||||
|
FavouritesCount int `json:"favourites_count"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
InReplyToAccountID string `json:"in_reply_to_account_id"`
|
||||||
|
InReplyToID string `json:"in_reply_to_id"`
|
||||||
|
Language string `json:"language"`
|
||||||
|
MediaAttachments []Attachment `json:"media_attachments"`
|
||||||
|
Mentions []Mention `json:"mentions"`
|
||||||
|
Muted bool `json:"muted"`
|
||||||
|
Pinned bool `json:"pinned"`
|
||||||
|
Poll Poll `json:"poll"`
|
||||||
|
Reblogged bool `json:"reblogged"`
|
||||||
|
RebloggsCount int `json:"reblogs_count"`
|
||||||
|
RepliesCount int `json:"replies_count"`
|
||||||
|
Sensitive bool `json:"sensitive"`
|
||||||
|
SpolierText string `json:"spoiler_text"`
|
||||||
|
Tags []Tag `json:"tags"`
|
||||||
|
Text string `json:"text"`
|
||||||
|
URI string `json:"uri"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
Visibility string `json:"visibility"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Tag struct {
|
||||||
|
History []any `json:"history"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Attachment struct {
|
||||||
|
Meta MediaMeta `json:"meta"`
|
||||||
|
Blurhash string `json:"blurhash"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
PreviewRemoteURL string `json:"preview_remote_url"`
|
||||||
|
PreviewURL string `json:"preview_url"`
|
||||||
|
RemoteURL string `json:"remote_url"`
|
||||||
|
TextURL string `json:"text_url"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MediaMeta struct {
|
||||||
|
Focus MediaFocus `json:"focus"`
|
||||||
|
Original MediaDimensions `json:"original"`
|
||||||
|
Small MediaDimensions `json:"small"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MediaFocus struct {
|
||||||
|
X float64 `json:"x"`
|
||||||
|
Y float64 `json:"y"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MediaDimensions struct {
|
||||||
|
Aspect float64 `json:"aspect"`
|
||||||
|
Bitrate int `json:"bitrate"`
|
||||||
|
Duration float64 `json:"duration"`
|
||||||
|
FrameRate string `json:"frame_rate"`
|
||||||
|
Size string `json:"size"`
|
||||||
|
Height int `json:"height"`
|
||||||
|
Width int `json:"width"`
|
||||||
|
}
|
7
main.go
7
main.go
|
@ -1,7 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import "fmt"
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
fmt.Println("Hello, Enbas!")
|
|
||||||
}
|
|
Loading…
Reference in a new issue