Compare commits
87 commits
notificati
...
main
Author | SHA1 | Date | |
---|---|---|---|
5043c2a4d1 | |||
e3a0436027 | |||
fa58e5b719 | |||
e898a1ded5 | |||
987f8caa1c | |||
cc5e3f0044 | |||
89e53bcc9f | |||
74f32aab53 | |||
0ad02e0af4 | |||
b558c5adff | |||
61a00d7a5b | |||
d38431d9e8 | |||
15b9761497 | |||
277bec49ef | |||
3c8633ff04 | |||
6e5e0c4c5a | |||
7e4b8bb05f | |||
7fd93e8778 | |||
009515ddb4 | |||
dd6c21afe8 | |||
f223e0417c | |||
ee13dcde00 | |||
39d9ef1a38 | |||
c0f1f7d03a | |||
9bcb924ac0 | |||
0a41516ae9 | |||
3037af60ed | |||
42f54c6020 | |||
4e76d20a7a | |||
eb016b96e9 | |||
a0eab3b6ae | |||
878a898d4c | |||
b77bbaa6e0 | |||
84091f398d | |||
299b134b58 | |||
cb7ac4175d | |||
2ccfdc4336 | |||
bad22ecd70 | |||
3d20adfa57 | |||
60aeec06f9 | |||
6bc00138ab | |||
c468d1fb62 | |||
6e260266b1 | |||
2bb801b6d0 | |||
ec282e207f | |||
28bf902599 | |||
c72340490d | |||
e0b4174a76 | |||
181384fea3 | |||
d52bb3fdf4 | |||
e5eb2d72a8 | |||
f64f8a43a6 | |||
a8aeec2fbf | |||
5ec1cc0e18 | |||
1541aa6936 | |||
c12a2ce293 | |||
4d0a42ba23 | |||
e4cce2cae4 | |||
23ec6048b5 | |||
b03b3c31cc | |||
c8892a6535 | |||
b4cb362a7c | |||
42251f6df8 | |||
63f0526f39 | |||
632a620180 | |||
67d4caf4cb | |||
96ad6a7b55 | |||
e8114f8d22 | |||
2a386fcda5 | |||
56445601a3 | |||
48666645c7 | |||
ccdd8b6530 | |||
f73f1f5872 | |||
eb2d9f44f6 | |||
792a93d736 | |||
ac2e74cac3 | |||
c04cd7e8a5 | |||
ec706b43e9 | |||
5fb55ed2cf | |||
d21f1fbf6a | |||
b48fac3341 | |||
db16e4aa16 | |||
9c8476fa97 | |||
c8187587a8 | |||
64ecc90499 | |||
d452122b58 | |||
66c530d524 |
126 changed files with 8503 additions and 2418 deletions
16
.forgejo/workflows/REUSE_Compliance_Check.yaml
Normal file
16
.forgejo/workflows/REUSE_Compliance_Check.yaml
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
---
|
||||||
|
name: REUSE Compliance Check
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- "main"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check:
|
||||||
|
runs-on: docker
|
||||||
|
steps:
|
||||||
|
- name: Checkout Repository
|
||||||
|
uses: https://code.forgejo.org/actions/checkout@v4
|
||||||
|
- name: REUSE Compliance Check
|
||||||
|
uses: https://github.com/fsfe/reuse-action@v4
|
22
.forgejo/workflows/Tests.yaml
Normal file
22
.forgejo/workflows/Tests.yaml
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
---
|
||||||
|
name: Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types:
|
||||||
|
- opened
|
||||||
|
- synchronize
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
if: ${{ ! github.event.pull_request.draft }}
|
||||||
|
runs-on: docker
|
||||||
|
steps:
|
||||||
|
- name: Checkout Repository
|
||||||
|
uses: https://code.forgejo.org/actions/checkout@v4
|
||||||
|
- name: Test
|
||||||
|
uses: https://codeflow.dananglin.me.uk/actions/mage-ci@main
|
||||||
|
with:
|
||||||
|
target: test
|
||||||
|
env:
|
||||||
|
ENBAS_TEST_COVER: "1"
|
|
@ -10,7 +10,7 @@ run:
|
||||||
tests: true
|
tests: true
|
||||||
|
|
||||||
output:
|
output:
|
||||||
format: colored-line-number
|
formats: colored-line-number
|
||||||
print-issues-lines: true
|
print-issues-lines: true
|
||||||
print-linter-name: true
|
print-linter-name: true
|
||||||
uniq-by-line: true
|
uniq-by-line: true
|
||||||
|
@ -31,5 +31,9 @@ linters-settings:
|
||||||
linters:
|
linters:
|
||||||
enable-all: true
|
enable-all: true
|
||||||
disable:
|
disable:
|
||||||
#- json
|
- execinquery
|
||||||
|
- exhaustruct
|
||||||
|
- gomnd
|
||||||
|
- mnd
|
||||||
|
- tagliatelle
|
||||||
fast: false
|
fast: false
|
||||||
|
|
10
.reuse/dep5
10
.reuse/dep5
|
@ -1,10 +0,0 @@
|
||||||
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
|
||||||
Upstream-Name: enbas
|
|
||||||
Upstream-Contact: Dan Anglin <d.n.i.anglin@gmail.com>
|
|
||||||
Source: https://codeflow.dananglin.me.uk/apollo/enbas
|
|
||||||
|
|
||||||
# Sample paragraph, commented out:
|
|
||||||
#
|
|
||||||
# Files: src/*
|
|
||||||
# Copyright: $YEAR $NAME <$CONTACT>
|
|
||||||
# License: ...
|
|
256
README.asciidoc
256
README.asciidoc
|
@ -1,256 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2024 Dan Anglin <d.n.i.anglin@gmail.com>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: CC-BY-4.0
|
|
||||||
|
|
||||||
= Enbas
|
|
||||||
:toc: left
|
|
||||||
:toclevels: 3
|
|
||||||
:toc-title: Table of Contents
|
|
||||||
|
|
||||||
== Overview
|
|
||||||
|
|
||||||
Enbas is a https://docs.gotosocial.org/en/latest/[GoToSocial] client for your terminal written
|
|
||||||
in https://go.dev[Go]. The project is in its experimental stages of development so bugs and breaking
|
|
||||||
changes may appear. Enbas has limited functionality at the moment and it is **not** recommended for use
|
|
||||||
with your production GoToSocial servers.
|
|
||||||
|
|
||||||
This project is licensed under the GNU General Public License V3 or later which you can view link:LICENSES/GPL-3.0-or-later.txt[here].
|
|
||||||
|
|
||||||
=== Repository mirrors
|
|
||||||
|
|
||||||
- **Code Flow:** https://codeflow.dananglin.me.uk/apollo/enbas
|
|
||||||
- **Codeberg:** https://codeberg.org/dananglin/enbas
|
|
||||||
- **GitHub:** https://github.com/dananglin/enbas
|
|
||||||
|
|
||||||
== Installation
|
|
||||||
|
|
||||||
=== Download
|
|
||||||
|
|
||||||
Pre-built binaries will be available on the release page on both Codeberg and GitHub when the first
|
|
||||||
release is made.
|
|
||||||
|
|
||||||
=== Build from source
|
|
||||||
|
|
||||||
==== Requirements
|
|
||||||
|
|
||||||
===== Go
|
|
||||||
|
|
||||||
A minimum version of Go 1.22.0 is required for installing spruce.
|
|
||||||
Please go https://go.dev/dl/[here] to download the latest version.
|
|
||||||
|
|
||||||
===== Mage (Optional)
|
|
||||||
|
|
||||||
The project includes mage targets for building and installing the binary.
|
|
||||||
The main advantage of using mage over `go install` is that the build information is baked into the binary
|
|
||||||
during compilation. You can visit the https://magefile.org/[Mage website] for instructions on how to install Mage.
|
|
||||||
|
|
||||||
==== Install with mage
|
|
||||||
|
|
||||||
You can install Enbas with Mage using the following commands:
|
|
||||||
|
|
||||||
[source,console]
|
|
||||||
----
|
|
||||||
git clone https://github.com/dananglin/enbas.git
|
|
||||||
mage install
|
|
||||||
----
|
|
||||||
|
|
||||||
By default Mage will attempt to install Enbas to `/usr/local/bin/enbas` which will most likely fail as you won't
|
|
||||||
the permission to write to `/usr/local/bin/`. You will need to either run `sudo mage install`, or you can
|
|
||||||
(preferably) change the install prefix to a directory that you have permission to write to using
|
|
||||||
the `ENBAS_INSTALL_PREFIX` environment variable. For example:
|
|
||||||
|
|
||||||
[source,console]
|
|
||||||
----
|
|
||||||
ENBAS_INSTALL_PREFIX=${HOME}/.local mage install
|
|
||||||
----
|
|
||||||
|
|
||||||
This will install Enbas to `~/.local/bin/enbas`.
|
|
||||||
|
|
||||||
===== Environment variables you can use with Mage
|
|
||||||
|
|
||||||
[%header,cols=2*]
|
|
||||||
|===
|
|
||||||
|Environment Variable
|
|
||||||
|Description
|
|
||||||
|
|
||||||
|`ENBAS_INSTALL_PREFIX`
|
|
||||||
|Set this to your preferred the installation prefix (default: `/usr/local`).
|
|
||||||
|
|
||||||
|`ENBAS_BUILD_REBUILD_ALL`
|
|
||||||
|Set this to `1` to rebuild all packages even if they are already up-to-date.
|
|
||||||
|
|
||||||
|`ENBAS_BUILD_VERBOSE`
|
|
||||||
|Set this to `1` to enable verbose logging when building the binary.
|
|
||||||
|===
|
|
||||||
|
|
||||||
==== Install with go
|
|
||||||
|
|
||||||
If your `GOBIN` directory is included in your `PATH` then you can install Enbas with Go.
|
|
||||||
|
|
||||||
[source,console]
|
|
||||||
----
|
|
||||||
git clone https://github.com/dananglin/enbas.git
|
|
||||||
cd enbas
|
|
||||||
go install ./cmd/enbas
|
|
||||||
----
|
|
||||||
|
|
||||||
== Configuration
|
|
||||||
|
|
||||||
Enbas uses Go's https://pkg.go.dev/os#UserConfigDir[os.UserConfigDir()] function to determine the
|
|
||||||
location of your configuration directory.
|
|
||||||
|
|
||||||
If you've set the `XDG_CONFIG_HOME` environment variable, the configuration files will be stored in the `$XDG_CONFIG_HOME/enbas` directory.
|
|
||||||
|
|
||||||
If this is not set:
|
|
||||||
|
|
||||||
- on Linux the configuration directory will be set to `$HOME/.config/enbas`.
|
|
||||||
- on Darwin (MacOS) the configuration directory will be set to `$HOME/Library/Application Support/enbas`.
|
|
||||||
- on Windows the configuration directory will be set within the `%AppData%` directory.
|
|
||||||
|
|
||||||
If, for whatever reason, any of the above cannot be determined the configuration directory will be set to
|
|
||||||
the current working directory.
|
|
||||||
|
|
||||||
== Usage
|
|
||||||
|
|
||||||
=== Check the build information
|
|
||||||
|
|
||||||
You can view the application's version and build information using the `--version` flag.
|
|
||||||
The build information is correctly displayed if you've downloaded the binary from Codeberg or GitHub,
|
|
||||||
or if you've built it with Mage.
|
|
||||||
|
|
||||||
[source,console]
|
|
||||||
----
|
|
||||||
$ enbas version --full
|
|
||||||
Enbas
|
|
||||||
Version: v0.0.0-13-g26a909d
|
|
||||||
Git commit: 26a909d
|
|
||||||
Go version: go1.22.0
|
|
||||||
Build date: 2024-02-25T15:22:55Z
|
|
||||||
----
|
|
||||||
|
|
||||||
=== Check out the help documentation
|
|
||||||
|
|
||||||
You can view the help documentation with the `--help` flag.
|
|
||||||
You can also use this flag to view the help documentation for any of the commands.
|
|
||||||
|
|
||||||
[source,console]
|
|
||||||
----
|
|
||||||
$ enbas --help
|
|
||||||
SUMMARY:
|
|
||||||
enbas - A GoToSocial client for the terminal.
|
|
||||||
|
|
||||||
VERSION:
|
|
||||||
v0.0.0-13-g26a909d
|
|
||||||
|
|
||||||
USAGE:
|
|
||||||
enbas [flags]
|
|
||||||
enbas [command]
|
|
||||||
|
|
||||||
COMMANDS:
|
|
||||||
login login to an account on GoToSocial
|
|
||||||
show print details about a specified resource
|
|
||||||
switch switch to an account
|
|
||||||
version print the application's version and build information
|
|
||||||
|
|
||||||
FLAGS:
|
|
||||||
--help
|
|
||||||
print the help message
|
|
||||||
|
|
||||||
Use "enbas [command] --help" for more information about a command.
|
|
||||||
----
|
|
||||||
|
|
||||||
=== Log into your GoToSocial account
|
|
||||||
|
|
||||||
Enbas uses the Oauth2 authentication flow to log into your account on GoToSocial.
|
|
||||||
This process requires your input to give consent to allow Enbas access to your account.
|
|
||||||
|
|
||||||
[WARNING]
|
|
||||||
====
|
|
||||||
As of writing GoToSocial does not currently support scoped authorization tokens so even if we request read-only
|
|
||||||
tokens, the application will be able to perform any actions within the limitations of your account
|
|
||||||
(including admin actions if you are an admin).
|
|
||||||
You can read more about this https://docs.gotosocial.org/en/latest/api/authentication/[here].
|
|
||||||
====
|
|
||||||
|
|
||||||
The login flow is completed using the following steps:
|
|
||||||
|
|
||||||
1. You start by using the `login` command specifying the instance that you want to log into.
|
|
||||||
+
|
|
||||||
[source,console]
|
|
||||||
----
|
|
||||||
enbas login --instance gotosocial-01.social.example
|
|
||||||
----
|
|
||||||
|
|
||||||
2. The application will register itself and the GoToSocial server will create a new client ID and secret that the app needs for authentication.
|
|
||||||
|
|
||||||
3. The application will then generate a link to the consent form for you to access in your browser.
|
|
||||||
This link will be printed on your terminal screen along with a message explaining that you need to obtain the `out-of-band` token to continue.
|
|
||||||
If you have the `BROWSER` environment variable set or if you're using Linux, the link will open in a new browser tab for you to sign into your account.
|
|
||||||
If the browser tab doesn't open, you can manually copy and paste the link in your favourite browser.
|
|
||||||
|
|
||||||
4. Once you've signed into GoToSocial on your browser, you will be informed that Enbas would like to perform actions on your behalf.
|
|
||||||
If you're happy with this then click on the `Allow` button.
|
|
||||||
+
|
|
||||||
image::assets/images/consent_form.png[A screenshot of the consent form]
|
|
||||||
|
|
||||||
5. The `out-of-band` token will be printed for you at this point. Copy it and return to your terminal.
|
|
||||||
|
|
||||||
6. Paste the token into the prompt and press `ENTER`.
|
|
||||||
Enbas will then exchange the token for an access token which will be used to authentication to the
|
|
||||||
GoToSocial server on your behalf.
|
|
||||||
Enbas will then verify the access token, save the credentials to the `credentials.json` file in your configuration directory,
|
|
||||||
and confirm that you have successfully logged into your account.
|
|
||||||
+
|
|
||||||
[source,console]
|
|
||||||
----
|
|
||||||
$ enbas login --instance gotosocial-01.social.example
|
|
||||||
|
|
||||||
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:
|
|
||||||
|
|
||||||
https://gotosocial-01.social.example/oauth/authorize?client_id=01RHK48N1KH9SFNH2VVZR414BJ&redirect_uri=urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob&response_type=code
|
|
||||||
|
|
||||||
Once you have the code please copy and paste it below.
|
|
||||||
|
|
||||||
Out-of-band token: ZGJKNDA2YWMTNGEYMS0ZZJLJLWJHNDITM2IZYJJLNJM3YJBK
|
|
||||||
Successfully logged into bobby@gotosocial-01.social.example
|
|
||||||
----
|
|
||||||
|
|
||||||
=== Common actions
|
|
||||||
|
|
||||||
* View your account information
|
|
||||||
+
|
|
||||||
[source,console]
|
|
||||||
----
|
|
||||||
enbas show --type account --my-account
|
|
||||||
----
|
|
||||||
|
|
||||||
* View a local or remote account
|
|
||||||
+
|
|
||||||
[source,console]
|
|
||||||
----
|
|
||||||
enbas show --type account --account teddy@gotosocial-01.social.example
|
|
||||||
----
|
|
||||||
|
|
||||||
* View your home timeline
|
|
||||||
+
|
|
||||||
[source,console]
|
|
||||||
----
|
|
||||||
enbas show --type timeline
|
|
||||||
----
|
|
||||||
|
|
||||||
* View the details of a status
|
|
||||||
+
|
|
||||||
[source,console]
|
|
||||||
----
|
|
||||||
enbas show --type status --status-id 01HQE43KT5YEDN4RGMT7BC63PF
|
|
||||||
----
|
|
||||||
|
|
||||||
== Inspirations
|
|
||||||
|
|
||||||
This project was inspired from the following projects:
|
|
||||||
|
|
||||||
* **madonctl:** https://github.com/McKael/madonctl[A Mastodon CLI client written in Go.]
|
|
||||||
* **toot:** https://pypi.org/project/toot/[A Mastodon CLI and TUI written in Python.]
|
|
||||||
* **tut:** https://github.com/RasmusLindroth/tut[A Mastodon TUI written in Go.]
|
|
43
README.md
Normal file
43
README.md
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
# Enbas
|
||||||
|
|
||||||
|
### Overview
|
||||||
|
|
||||||
|
Enbas is a [GoToSocial](https://docs.gotosocial.org/en/latest/) client for your terminal written
|
||||||
|
in [Go](https://go.dev).
|
||||||
|
|
||||||
|
The project is in its early stages of development so expect bugs, breaking changes and limited
|
||||||
|
functionality.
|
||||||
|
At this time it is not recommended for use this with your production GoToSocial instances.
|
||||||
|
|
||||||
|
### Repository mirrors
|
||||||
|
|
||||||
|
Enbas is actively developed in [Code Flow](https://codeflow.dananglin.me.uk/apollo/enbas) with
|
||||||
|
the `main` branch mirrored to the following forges:
|
||||||
|
|
||||||
|
- **Codeberg**: https://codeberg.org/dananglin/enbas
|
||||||
|
- **Radicle**: https://app.radicle.xyz/nodes/seed.radicle.garden/rad:zhqv2orTvTh2x2d7kYky9NhctrpK
|
||||||
|
- **GitHub**: https://github.com/dananglin/enbas
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- **[Installation guide](docs/installation.md)**: A guide for installing Enbas on your machine.
|
||||||
|
- **[Getting started guide](docs/getting_started.md)**: A guide to help you get started on using Enbas.
|
||||||
|
- **[Configuration reference](docs/configuration.md)**: The configuration reference documentation.
|
||||||
|
- **[User manual](docs/manual.md)**: The user manual.
|
||||||
|
- **[Tips and Tricks](docs/tips_and_tricks.md)**: Additional tips and tricks.
|
||||||
|
|
||||||
|
### Licensing
|
||||||
|
|
||||||
|
The licensing information associated with each file is specified in the [REUSE.toml](REUSE.toml) file,
|
||||||
|
but in general:
|
||||||
|
|
||||||
|
- All original source code is licensed under GPL-3.0-or-later.
|
||||||
|
- All documentation is licensed under CC-BY-4.0.
|
||||||
|
|
||||||
|
### Inspirations
|
||||||
|
|
||||||
|
This project was inspired by the following projects:
|
||||||
|
|
||||||
|
* **[madonctl](https://github.com/McKael/madonctl)**: A Mastodon CLI client written in Go.
|
||||||
|
* **[tut](https://github.com/RasmusLindroth/tut)**: A Mastodon TUI written in Go.
|
||||||
|
* **[toot](https://pypi.org/project/toot/)**: A Mastodon CLI and TUI written in Python.
|
42
REUSE.toml
Normal file
42
REUSE.toml
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
# SPDX-FileCopyrightText: 2024 Dan Anglin <d.n.i.anglin@gmail.com>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: CC0-1.0
|
||||||
|
|
||||||
|
version = 1
|
||||||
|
SPDX-PackageName = "enbas"
|
||||||
|
SPDX-PackageSupplier = "Dan Anglin <d.n.i.anglin@gmail.com>"
|
||||||
|
SPDX-PackageDownloadLocation = "https://codeflow.dananglin.me.uk/apollo/enbas"
|
||||||
|
|
||||||
|
[[annotations]]
|
||||||
|
path = [
|
||||||
|
"**.go",
|
||||||
|
"go.mod",
|
||||||
|
"magefiles/go.mod",
|
||||||
|
"cmd/enbas-codegen/templates/**/*.go.gotmpl",
|
||||||
|
"schema/enbas_cli_schema.json",
|
||||||
|
".forgejo/workflows/*.yaml",
|
||||||
|
".forgejo/actions/**/action.yaml",
|
||||||
|
".forgejo/actions/**/Dockerfile",
|
||||||
|
]
|
||||||
|
precedence = "override"
|
||||||
|
SPDX-FileCopyrightText = "2024 Dan Anglin <d.n.i.anglin@gmail.com>"
|
||||||
|
SPDX-License-Identifier = "GPL-3.0-or-later"
|
||||||
|
|
||||||
|
[[annotations]]
|
||||||
|
path = [
|
||||||
|
"README.md",
|
||||||
|
"docs/*.md",
|
||||||
|
"assets/images/*.png",
|
||||||
|
]
|
||||||
|
precedence = "override"
|
||||||
|
SPDX-FileCopyrightText = "2024 Dan Anglin <d.n.i.anglin@gmail.com>"
|
||||||
|
SPDX-License-Identifier = "CC-BY-4.0"
|
||||||
|
|
||||||
|
[[annotations]]
|
||||||
|
path = [
|
||||||
|
"go.sum",
|
||||||
|
"magefiles/go.sum",
|
||||||
|
]
|
||||||
|
precedence = "override"
|
||||||
|
SPDX-FileCopyrightText = "2024 Dan Anglin <d.n.i.anglin@gmail.com>"
|
||||||
|
SPDX-License-Identifier = "CC0-1.0"
|
BIN
assets/images/consent_form.png
(Stored with Git LFS)
BIN
assets/images/consent_form.png
(Stored with Git LFS)
Binary file not shown.
|
@ -1,3 +0,0 @@
|
||||||
SPDX-FileCopyrightText: 2024 Dan Anglin <d.n.i.anglin@gmail.com>
|
|
||||||
|
|
||||||
SPDX-License-Identifier: CC-BY-4.0
|
|
BIN
assets/images/created_poll.png
(Stored with Git LFS)
Normal file
BIN
assets/images/created_poll.png
(Stored with Git LFS)
Normal file
Binary file not shown.
162
cmd/enbas-codegen/main.go
Normal file
162
cmd/enbas-codegen/main.go
Normal file
|
@ -0,0 +1,162 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"errors"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
"text/template"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var (
|
||||||
|
enbasCLISchemaFilepath string
|
||||||
|
packageName string
|
||||||
|
)
|
||||||
|
|
||||||
|
flag.StringVar(&enbasCLISchemaFilepath, "path-to-enbas-cli-schema", "", "The path to the Enbas CLI schema file")
|
||||||
|
flag.StringVar(&packageName, "package", "", "The name of the internal package")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
schema, err := newEnbasCLISchemaFromFile(enbasCLISchemaFilepath)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "ERROR: Unable to read the schema file: %v.\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := generateExecutors(schema, packageName); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "ERROR: Unable to generate the executors: %v.\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//go:embed templates/*
|
||||||
|
var executorTemplates embed.FS
|
||||||
|
|
||||||
|
var errNoPackageFlag = errors.New("the --package flag must be used")
|
||||||
|
|
||||||
|
func generateExecutors(schema enbasCLISchema, packageName string) error {
|
||||||
|
if packageName == "" {
|
||||||
|
return errNoPackageFlag
|
||||||
|
}
|
||||||
|
|
||||||
|
dirName := "templates/" + packageName
|
||||||
|
|
||||||
|
fsDir, err := executorTemplates.ReadDir(dirName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to read the template directory in the file system (FS): %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
funcMap := template.FuncMap{
|
||||||
|
"capitalise": capitalise,
|
||||||
|
"flagFieldName": flagFieldName,
|
||||||
|
"getFlagType": schema.Flags.getType,
|
||||||
|
"getFlagDescription": schema.Flags.getDescription,
|
||||||
|
"internalFlagValue": internalFlagValue,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, obj := range fsDir {
|
||||||
|
templateFilename := obj.Name()
|
||||||
|
|
||||||
|
if !strings.HasSuffix(templateFilename, ".go.gotmpl") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := func() error {
|
||||||
|
tmpl := template.Must(template.New(templateFilename).
|
||||||
|
Funcs(funcMap).
|
||||||
|
ParseFS(executorTemplates, dirName+"/"+templateFilename),
|
||||||
|
)
|
||||||
|
|
||||||
|
output := strings.TrimSuffix(templateFilename, ".gotmpl")
|
||||||
|
|
||||||
|
file, err := os.Create(output)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to create the output file: %w", err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
if err := tmpl.Execute(file, schema.Commands); err != nil {
|
||||||
|
return fmt.Errorf("unable to generate the code from the template: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := runGoImports(output); err != nil {
|
||||||
|
return fmt.Errorf("unable to run goimports: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}(); err != nil {
|
||||||
|
return fmt.Errorf("received an error after attempting to generate the code for %q: %w", templateFilename, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func runGoImports(path string) error {
|
||||||
|
imports := exec.Command("goimports", "-w", path)
|
||||||
|
|
||||||
|
if err := imports.Run(); err != nil {
|
||||||
|
return fmt.Errorf("received an error after running goimports: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func capitalise(str string) string {
|
||||||
|
runes := []rune(str)
|
||||||
|
|
||||||
|
runes[0] = unicode.ToUpper(runes[0])
|
||||||
|
|
||||||
|
return string(runes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func flagFieldName(flagRef enbasCLISchemaFlagReference) string {
|
||||||
|
if flagRef.FieldName != "" {
|
||||||
|
return flagRef.FieldName
|
||||||
|
}
|
||||||
|
|
||||||
|
return convertFlagToMixedCaps(flagRef.Flag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertFlagToMixedCaps(value string) string {
|
||||||
|
var builder strings.Builder
|
||||||
|
|
||||||
|
runes := []rune(value)
|
||||||
|
numRunes := len(runes)
|
||||||
|
cursor := 0
|
||||||
|
|
||||||
|
for cursor < numRunes {
|
||||||
|
if runes[cursor] != '-' {
|
||||||
|
builder.WriteRune(runes[cursor])
|
||||||
|
|
||||||
|
cursor++
|
||||||
|
} else {
|
||||||
|
if cursor != numRunes-1 && unicode.IsLower(runes[cursor+1]) {
|
||||||
|
builder.WriteRune(unicode.ToUpper(runes[cursor+1]))
|
||||||
|
cursor += 2
|
||||||
|
} else {
|
||||||
|
cursor++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func internalFlagValue(flagType string) bool {
|
||||||
|
internalFlagValues := map[string]struct{}{
|
||||||
|
"StringSliceValue": {},
|
||||||
|
"IntSliceValue": {},
|
||||||
|
"TimeDurationValue": {},
|
||||||
|
"BoolPtrValue": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, exists := internalFlagValues[flagType]
|
||||||
|
|
||||||
|
return exists
|
||||||
|
}
|
72
cmd/enbas-codegen/schema.go
Normal file
72
cmd/enbas-codegen/schema.go
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type enbasCLISchema struct {
|
||||||
|
Flags enbasCLISchemaFlagMap `json:"flags"`
|
||||||
|
Commands map[string]enbasCLISchemaCommand `json:"commands"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func newEnbasCLISchemaFromFile(path string) (enbasCLISchema, error) {
|
||||||
|
file, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return enbasCLISchema{}, fmt.Errorf("unable to open the schema file: %w", err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
var schema enbasCLISchema
|
||||||
|
|
||||||
|
if err := json.NewDecoder(file).Decode(&schema); err != nil {
|
||||||
|
return enbasCLISchema{}, fmt.Errorf("unable to decode the JSON data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return schema, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type enbasCLISchemaFlag struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type enbasCLISchemaFlagMap map[string]enbasCLISchemaFlag
|
||||||
|
|
||||||
|
func (e enbasCLISchemaFlagMap) getType(name string) string {
|
||||||
|
flag, ok := e[name]
|
||||||
|
if !ok {
|
||||||
|
return "UNKNOWN TYPE"
|
||||||
|
}
|
||||||
|
|
||||||
|
return flag.Type
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e enbasCLISchemaFlagMap) getDescription(name string) string {
|
||||||
|
flag, ok := e[name]
|
||||||
|
if !ok {
|
||||||
|
return "UNKNOWN DESCRIPTION"
|
||||||
|
}
|
||||||
|
|
||||||
|
return flag.Description
|
||||||
|
}
|
||||||
|
|
||||||
|
type enbasCLISchemaCommand struct {
|
||||||
|
AdditionalFields []enbasCLISchemaAdditionalFields `json:"additionalFields"`
|
||||||
|
Flags []enbasCLISchemaFlagReference `json:"flags"`
|
||||||
|
Summary string `json:"summary"`
|
||||||
|
UseConfig bool `json:"useConfig"`
|
||||||
|
UsePrinter bool `json:"usePrinter"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type enbasCLISchemaFlagReference struct {
|
||||||
|
Flag string `json:"flag"`
|
||||||
|
FieldName string `json:"fieldName"`
|
||||||
|
Default string `json:"default"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type enbasCLISchemaAdditionalFields struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
}
|
175
cmd/enbas-codegen/templates/executor/executor.go.gotmpl
Normal file
175
cmd/enbas-codegen/templates/executor/executor.go.gotmpl
Normal file
|
@ -0,0 +1,175 @@
|
||||||
|
/*
|
||||||
|
This file is generated by the enbas-codegen
|
||||||
|
DO NOT EDIT.
|
||||||
|
*/
|
||||||
|
{{ print "" }}
|
||||||
|
package executor
|
||||||
|
{{ print "" }}
|
||||||
|
{{ print "" }}
|
||||||
|
import "fmt"
|
||||||
|
import "flag"
|
||||||
|
import "os"
|
||||||
|
import "codeflow.dananglin.me.uk/apollo/enbas/internal/config"
|
||||||
|
import internalFlag "codeflow.dananglin.me.uk/apollo/enbas/internal/flag"
|
||||||
|
import "codeflow.dananglin.me.uk/apollo/enbas/internal/printer"
|
||||||
|
import "codeflow.dananglin.me.uk/apollo/enbas/internal/usage"
|
||||||
|
{{ print "" }}
|
||||||
|
{{ print "" }}
|
||||||
|
func Execute() error {
|
||||||
|
var (
|
||||||
|
configDir string
|
||||||
|
noColorFlag internalFlag.BoolPtrValue
|
||||||
|
noColor bool
|
||||||
|
)
|
||||||
|
|
||||||
|
flag.StringVar(&configDir, "config-dir", "", "The path to your configuration directory")
|
||||||
|
flag.Var(&noColorFlag, "no-color", "Set to true to disable ANSI colour output when displaying text on screen")
|
||||||
|
|
||||||
|
flag.Usage = usage.AppUsageFunc()
|
||||||
|
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if flag.NArg() < 1 {
|
||||||
|
flag.Usage()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if noColorFlag.Value != nil {
|
||||||
|
noColor = *noColorFlag.Value
|
||||||
|
} else if os.Getenv("NO_COLOR") != "" {
|
||||||
|
noColor = true
|
||||||
|
}
|
||||||
|
|
||||||
|
command := flag.Arg(0)
|
||||||
|
args := flag.Args()[1:]
|
||||||
|
|
||||||
|
executorMap := map[string]func(string, bool, []string) error {
|
||||||
|
{{- range $name, $command := . -}}
|
||||||
|
{{- $execute_command_function_name := capitalise $name | printf "Execute%sCommand" -}}
|
||||||
|
{{ print "" }}
|
||||||
|
{{ printf "%q" $name }}: {{ $execute_command_function_name }},
|
||||||
|
{{- end -}}
|
||||||
|
{{ print "" }}
|
||||||
|
}
|
||||||
|
|
||||||
|
executorFunc, ok := executorMap[command]
|
||||||
|
if !ok {
|
||||||
|
err := UnknownCommandError{command: command}
|
||||||
|
printer.NewPrinter(noColor, "", 0).PrintFailure("Error: "+err.Error())
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return executorFunc(configDir, noColor, args)
|
||||||
|
}
|
||||||
|
{{ print "" }}
|
||||||
|
{{ range $name, $command := . }}
|
||||||
|
{{- $struct_name := capitalise $name | printf "%sExecutor" -}}
|
||||||
|
{{- $execute_command_function_name := capitalise $name | printf "Execute%sCommand" -}}
|
||||||
|
{{ print "" }}
|
||||||
|
// {{ $struct_name }} is the executor for the {{ $name }} command.
|
||||||
|
type {{ $struct_name }} struct {
|
||||||
|
*flag.FlagSet
|
||||||
|
printer *printer.Printer
|
||||||
|
config *config.Config
|
||||||
|
{{- range $flag := $command.Flags -}}
|
||||||
|
{{- $flag_type := getFlagType $flag.Flag -}}
|
||||||
|
{{- if internalFlagValue $flag_type -}}
|
||||||
|
{{ print "" }}
|
||||||
|
{{ flagFieldName $flag }} internalFlag.{{ $flag_type }}
|
||||||
|
{{- else -}}
|
||||||
|
{{ print "" }}
|
||||||
|
{{ flagFieldName $flag }} {{ $flag_type }}
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- range $field := $command.AdditionalFields -}}
|
||||||
|
{{ print "" }}
|
||||||
|
{{ $field.Name }} {{ $field.Type }}
|
||||||
|
{{- end -}}
|
||||||
|
{{ print "" }}
|
||||||
|
}
|
||||||
|
|
||||||
|
// {{ $execute_command_function_name }} initialises and runs the executor for the {{ $name }} command.
|
||||||
|
func {{ $execute_command_function_name }}(
|
||||||
|
configDir string,
|
||||||
|
noColor bool,
|
||||||
|
args []string,
|
||||||
|
) error {
|
||||||
|
exe := {{ $struct_name }}{
|
||||||
|
FlagSet: flag.NewFlagSet({{ printf "%q" $name }}, flag.ExitOnError),
|
||||||
|
printer: nil,
|
||||||
|
config: nil,
|
||||||
|
configDir: configDir,
|
||||||
|
{{- range $flag := $command.Flags -}}
|
||||||
|
{{- $flag_type := getFlagType $flag.Flag -}}
|
||||||
|
{{- if internalFlagValue $flag_type -}}
|
||||||
|
{{ print "" }}
|
||||||
|
{{ flagFieldName $flag }}: internalFlag.New{{ $flag_type }}(),
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{ print "" }}
|
||||||
|
}
|
||||||
|
{{ print "" }}
|
||||||
|
exe.Usage = usage.ExecutorUsageFunc({{ printf "%q" $name }}, {{ printf "%q" $command.Summary }}, exe.FlagSet)
|
||||||
|
{{ print "" }}
|
||||||
|
{{- range $flag := $command.Flags -}}
|
||||||
|
{{- $flag_type := getFlagType $flag.Flag -}}
|
||||||
|
{{- if eq $flag_type "string" -}}
|
||||||
|
{{ print "" }}
|
||||||
|
exe.StringVar(&exe.{{ flagFieldName $flag }}, {{ printf "%q" $flag.Flag }}, {{ printf "%q" $flag.Default }}, {{ getFlagDescription $flag.Flag | printf "%q" }})
|
||||||
|
{{- else if eq $flag_type "bool" -}}
|
||||||
|
{{ print "" }}
|
||||||
|
exe.BoolVar(&exe.{{ flagFieldName $flag }}, {{ printf "%q" $flag.Flag }}, {{ $flag.Default }}, {{ getFlagDescription $flag.Flag | printf "%q" }})
|
||||||
|
{{- else if eq $flag_type "int" -}}
|
||||||
|
{{ print "" }}
|
||||||
|
exe.IntVar(&exe.{{ flagFieldName $flag }}, {{ printf "%q" $flag.Flag }}, {{ $flag.Default }}, {{ getFlagDescription $flag.Flag | printf "%q" }})
|
||||||
|
{{- else if internalFlagValue $flag_type -}}
|
||||||
|
{{ print "" }}
|
||||||
|
exe.Var(&exe.{{ flagFieldName $flag }}, {{ printf "%q" $flag.Flag }}, {{ getFlagDescription $flag.Flag | printf "%q" }})
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{ print "" }}
|
||||||
|
{{ print "" }}
|
||||||
|
// Parse the remaining arguments.
|
||||||
|
if err := exe.Parse(args); err != nil {
|
||||||
|
printer.NewPrinter(noColor, "", 0).PrintFailure("({{ $name }}) flag parsing error: " + err.Error() + ".")
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
{{- if $command.UseConfig -}}
|
||||||
|
{{ print "" }}
|
||||||
|
{{ print "" }}
|
||||||
|
// Load the configuration from file.
|
||||||
|
exeConfig, err := config.NewConfigFromFile(exe.configDir)
|
||||||
|
if err != nil {
|
||||||
|
printer.NewPrinter(noColor, "", 0).PrintFailure("({{ $name }}) unable to load configuration: " + err.Error() + ".")
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
exe.config = exeConfig
|
||||||
|
|
||||||
|
// Create the printer for the executor.
|
||||||
|
exe.printer = printer.NewPrinter(
|
||||||
|
noColor,
|
||||||
|
exe.config.Integrations.Pager,
|
||||||
|
exe.config.LineWrapMaxWidth,
|
||||||
|
)
|
||||||
|
{{- else -}}
|
||||||
|
{{ print "" }}
|
||||||
|
{{ print "" }}
|
||||||
|
// Create the printer for the executor.
|
||||||
|
exe.printer = printer.NewPrinter(noColor, "", 0)
|
||||||
|
{{- end -}}
|
||||||
|
{{ print "" }}
|
||||||
|
{{ print "" }}
|
||||||
|
// Run the executor.
|
||||||
|
if err := exe.Execute(); err != nil {
|
||||||
|
exe.printer.PrintFailure("({{ $name }}) execution error: " + err.Error() + ".")
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
{{ end }}
|
14
cmd/enbas-codegen/templates/usage/summaries.go.gotmpl
Normal file
14
cmd/enbas-codegen/templates/usage/summaries.go.gotmpl
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
/*
|
||||||
|
This file is generated by the enbas-codegen
|
||||||
|
DO NOT EDIT.
|
||||||
|
*/
|
||||||
|
{{ print "" }}
|
||||||
|
package usage
|
||||||
|
{{ print "" }}
|
||||||
|
var summaries = map[string]string {
|
||||||
|
{{- range $name, $command := . -}}
|
||||||
|
{{ print "" }}
|
||||||
|
{{ printf "%q" $name }}: {{ printf "%q" $command.Summary }},
|
||||||
|
{{- end -}}
|
||||||
|
{{ print "" }}
|
||||||
|
}
|
|
@ -1,13 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2024 Dan Anglin <d.n.i.anglin@gmail.com>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
type unknownCommandError struct {
|
|
||||||
subcommand string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e unknownCommandError) Error() string {
|
|
||||||
return "unknown command '" + e.subcommand + "'"
|
|
||||||
}
|
|
|
@ -1,207 +1,13 @@
|
||||||
// SPDX-FileCopyrightText: 2024 Dan Anglin <d.n.i.anglin@gmail.com>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"codeflow.dananglin.me.uk/apollo/enbas/internal/executor"
|
"codeflow.dananglin.me.uk/apollo/enbas/internal/executor"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
commandLogin string = "login"
|
|
||||||
commandVersion string = "version"
|
|
||||||
commandShow string = "show"
|
|
||||||
commandSwitch string = "switch"
|
|
||||||
commandCreate string = "create"
|
|
||||||
commandDelete string = "delete"
|
|
||||||
commandEdit string = "edit"
|
|
||||||
commandWhoami string = "whoami"
|
|
||||||
commandAdd string = "add"
|
|
||||||
commandRemove string = "remove"
|
|
||||||
commandFollow string = "follow"
|
|
||||||
commandUnfollow string = "unfollow"
|
|
||||||
commandBlock string = "block"
|
|
||||||
commandUnblock string = "unblock"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
binaryVersion string
|
|
||||||
buildTime string
|
|
||||||
goVersion string
|
|
||||||
gitCommit string
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if err := run(); err != nil {
|
if err := executor.Execute(); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "ERROR: %v.\n", err)
|
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func run() error {
|
|
||||||
commandSummaries := map[string]string{
|
|
||||||
commandLogin: "login to an account on GoToSocial",
|
|
||||||
commandVersion: "print the application's version and build information",
|
|
||||||
commandShow: "print details about a specified resource",
|
|
||||||
commandSwitch: "perform a switch operation (e.g. switch logged in accounts)",
|
|
||||||
commandCreate: "create a specific resource",
|
|
||||||
commandDelete: "delete a specific resource",
|
|
||||||
commandEdit: "edit a specific resource",
|
|
||||||
commandWhoami: "print the account that you are currently logged in to",
|
|
||||||
commandAdd: "add a resource to another resource",
|
|
||||||
commandRemove: "remove a resource from another resource",
|
|
||||||
commandFollow: "follow a resource (e.g. an account)",
|
|
||||||
commandUnfollow: "unfollow a resource (e.g. an account)",
|
|
||||||
commandBlock: "block a resource (e.g. an account)",
|
|
||||||
commandUnblock: "unblock a resource (e.g. an account)",
|
|
||||||
}
|
|
||||||
|
|
||||||
topLevelFlags := executor.TopLevelFlags{
|
|
||||||
ConfigDir: "",
|
|
||||||
NoColor: nil,
|
|
||||||
}
|
|
||||||
|
|
||||||
flag.StringVar(&topLevelFlags.ConfigDir, "config-dir", "", "specify your config directory")
|
|
||||||
flag.BoolFunc("no-color", "disable ANSI colour output when displaying text on screen", func(value string) error {
|
|
||||||
boolVal, err := strconv.ParseBool(value)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unable to parse %q as a boolean: %w", value, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
topLevelFlags.NoColor = new(bool)
|
|
||||||
*topLevelFlags.NoColor = boolVal
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
flag.Usage = usageFunc(commandSummaries)
|
|
||||||
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
if flag.NArg() < 1 {
|
|
||||||
flag.Usage()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// If NoColor is still unspecified, check to see if the NO_COLOR environment variable is set
|
|
||||||
if topLevelFlags.NoColor == nil {
|
|
||||||
topLevelFlags.NoColor = new(bool)
|
|
||||||
if os.Getenv("NO_COLOR") != "" {
|
|
||||||
*topLevelFlags.NoColor = true
|
|
||||||
} else {
|
|
||||||
*topLevelFlags.NoColor = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
command := flag.Arg(0)
|
|
||||||
args := flag.Args()[1:]
|
|
||||||
|
|
||||||
var err error
|
|
||||||
|
|
||||||
switch command {
|
|
||||||
case commandAdd:
|
|
||||||
exe := executor.NewAddExecutor(
|
|
||||||
topLevelFlags,
|
|
||||||
commandAdd,
|
|
||||||
commandSummaries[commandAdd],
|
|
||||||
)
|
|
||||||
err = executor.Execute(exe, args)
|
|
||||||
case commandBlock:
|
|
||||||
exe := executor.NewBlockExecutor(
|
|
||||||
topLevelFlags,
|
|
||||||
commandBlock,
|
|
||||||
commandSummaries[commandBlock],
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
err = executor.Execute(exe, args)
|
|
||||||
case commandCreate:
|
|
||||||
exe := executor.NewCreateExecutor(
|
|
||||||
topLevelFlags,
|
|
||||||
commandCreate,
|
|
||||||
commandSummaries[commandCreate],
|
|
||||||
)
|
|
||||||
err = executor.Execute(exe, args)
|
|
||||||
case commandDelete:
|
|
||||||
exe := executor.NewDeleteExecutor(
|
|
||||||
topLevelFlags,
|
|
||||||
commandDelete,
|
|
||||||
commandSummaries[commandDelete],
|
|
||||||
)
|
|
||||||
err = executor.Execute(exe, args)
|
|
||||||
case commandEdit:
|
|
||||||
exe := executor.NewEditExecutor(
|
|
||||||
topLevelFlags,
|
|
||||||
commandEdit,
|
|
||||||
commandSummaries[commandEdit],
|
|
||||||
)
|
|
||||||
err = executor.Execute(exe, args)
|
|
||||||
case commandFollow:
|
|
||||||
exe := executor.NewFollowExecutor(
|
|
||||||
topLevelFlags,
|
|
||||||
commandFollow,
|
|
||||||
commandSummaries[commandFollow],
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
err = executor.Execute(exe, args)
|
|
||||||
case commandLogin:
|
|
||||||
exe := executor.NewLoginExecutor(
|
|
||||||
topLevelFlags,
|
|
||||||
commandLogin,
|
|
||||||
commandSummaries[commandLogin],
|
|
||||||
)
|
|
||||||
err = executor.Execute(exe, args)
|
|
||||||
case commandRemove:
|
|
||||||
exe := executor.NewRemoveExecutor(
|
|
||||||
topLevelFlags,
|
|
||||||
commandRemove,
|
|
||||||
commandSummaries[commandRemove],
|
|
||||||
)
|
|
||||||
err = executor.Execute(exe, args)
|
|
||||||
case commandSwitch:
|
|
||||||
exe := executor.NewSwitchExecutor(
|
|
||||||
topLevelFlags,
|
|
||||||
commandSwitch,
|
|
||||||
commandSummaries[commandSwitch],
|
|
||||||
)
|
|
||||||
err = executor.Execute(exe, args)
|
|
||||||
case commandUnfollow:
|
|
||||||
exe := executor.NewFollowExecutor(topLevelFlags, commandUnfollow, commandSummaries[commandUnfollow], true)
|
|
||||||
err = executor.Execute(exe, args)
|
|
||||||
case commandUnblock:
|
|
||||||
exe := executor.NewBlockExecutor(topLevelFlags, commandUnblock, commandSummaries[commandUnblock], true)
|
|
||||||
err = executor.Execute(exe, args)
|
|
||||||
case commandShow:
|
|
||||||
exe := executor.NewShowExecutor(topLevelFlags, commandShow, commandSummaries[commandShow])
|
|
||||||
err = executor.Execute(exe, args)
|
|
||||||
case commandVersion:
|
|
||||||
exe := executor.NewVersionExecutor(
|
|
||||||
commandVersion,
|
|
||||||
commandSummaries[commandVersion],
|
|
||||||
binaryVersion,
|
|
||||||
buildTime,
|
|
||||||
goVersion,
|
|
||||||
gitCommit,
|
|
||||||
)
|
|
||||||
err = executor.Execute(exe, args)
|
|
||||||
case commandWhoami:
|
|
||||||
exe := executor.NewWhoAmIExecutor(topLevelFlags, commandWhoami, commandSummaries[commandWhoami])
|
|
||||||
err = executor.Execute(exe, args)
|
|
||||||
default:
|
|
||||||
flag.Usage()
|
|
||||||
|
|
||||||
return unknownCommandError{command}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("(%s) %w", command, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,50 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2024 Dan Anglin <d.n.i.anglin@gmail.com>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"slices"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func usageFunc(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\n print the help message\n")
|
|
||||||
flag.VisitAll(func(f *flag.Flag) {
|
|
||||||
fmt.Fprintf(&builder, "\n --%s\n %s\n", 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
docs/changelog.md
Normal file
1
docs/changelog.md
Normal file
|
@ -0,0 +1 @@
|
||||||
|
# Changelog
|
29
docs/configuration.md
Normal file
29
docs/configuration.md
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
# Configuration reference
|
||||||
|
|
||||||
|
## Config
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|--------------------|-------------------------------|--------------------------------------------------|
|
||||||
|
| `credentialsFile` | string | The (absolute) path to your credentials file. |
|
||||||
|
| `cacheDirectory` | string | The (absolute) path to the root cache directory. |
|
||||||
|
| `lineWrapMaxWidth` | int | The character limit used for line wrapping. |
|
||||||
|
| `http` | [HTTPConfig](#httpconfig) | HTTP settings. |
|
||||||
|
| `integrations` | [Integrations](#integrations) | Specify your integrations with Enbas. |
|
||||||
|
|
||||||
|
|
||||||
|
## HTTPConfig
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|----------------|------|---------------------------------------------------------------------|
|
||||||
|
| `timeout` | int | The timeout (in seconds) for normal HTTP requests to your instance. |
|
||||||
|
| `mediaTimeout` | int | The timeout (in seconds) for retrieving media from your instance. |
|
||||||
|
|
||||||
|
## Integrations
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|---------------|--------|--------------------------------------------------------------------------------------------------------|
|
||||||
|
| `browser` | string | The browser used for opening URLs (e.g. URL to a remote account). |
|
||||||
|
| `editor` | string | The text editor used for writing statuses (not yet implemented). |
|
||||||
|
| `pager` | string | The pager used for paging through long outputs (e.g. status timelines). Leave blank to disable paging. |
|
||||||
|
| `imageViewer` | string | The image viewer used for viewing images from media attachments. |
|
||||||
|
| `videoPlayer` | string | The video player used for viewing videos from media attachments. |
|
152
docs/getting_started.md
Normal file
152
docs/getting_started.md
Normal file
|
@ -0,0 +1,152 @@
|
||||||
|
# Getting Started
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
In this guide we are going to log into an account on a private GoToSocial server.
|
||||||
|
|
||||||
|
Follow along to log into your own account.
|
||||||
|
|
||||||
|
## Your configuration directory
|
||||||
|
|
||||||
|
You can use the `--config-dir` global flag to specify the path to your configuration directory.
|
||||||
|
Alternatively Enbas tries to set the directory based on your home configuration directory using Go's [os.UserConfigDir()](https://pkg.go.dev/os#UserConfigDir) function.
|
||||||
|
|
||||||
|
If you've set the `XDG_CONFIG_HOME` environment variable, the configuration directory will be set to `$XDG_CONFIG_HOME/enbas`.
|
||||||
|
|
||||||
|
If this is not set, then:
|
||||||
|
|
||||||
|
- on Linux the configuration directory will be set to `$HOME/.config/enbas`.
|
||||||
|
- on Darwin (MacOS) the configuration directory will be set to `$HOME/Library/Application Support/enbas`.
|
||||||
|
- on Windows the configuration directory will be set within the `%AppData%` directory.
|
||||||
|
|
||||||
|
## Generate your configuration file
|
||||||
|
|
||||||
|
Run the `init` command to generate your configuration file.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
enbas init
|
||||||
|
```
|
||||||
|
|
||||||
|
Use the `--config-dir` flag if you want to generate it in a specific directory
|
||||||
|
|
||||||
|
```bash
|
||||||
|
enbas --config-dir ./config init
|
||||||
|
```
|
||||||
|
|
||||||
|
You should now see a file called `config.json` in your configuration directory.
|
||||||
|
Feel free to edit the file to your preferences.
|
||||||
|
The [configuration reference page](./configuration.md) should help you with this.
|
||||||
|
|
||||||
|
For this 'Getting Started' guide you may want to specify your preferred browser in the configuration to allow
|
||||||
|
Enbas to open the link to your instance's authorisation page.
|
||||||
|
If you prefer to open the link manually then you can leave it blank.
|
||||||
|
|
||||||
|
## Log into your GoToSocial account
|
||||||
|
|
||||||
|
Enbas uses the Oauth2 authentication flow to log into your account on GoToSocial.
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> As of writing GoToSocial does not currently support scoped authorization tokens so even if we request read-only
|
||||||
|
> tokens, the application will be able to perform any actions within the limitations of your account
|
||||||
|
> (including admin actions if you are an admin).
|
||||||
|
> You can read more about this [here](https://docs.gotosocial.org/en/latest/api/authentication/).
|
||||||
|
|
||||||
|
Follow the below steps to log into your account:
|
||||||
|
|
||||||
|
1. Run the `login` command specifying the instance that you want to log into.
|
||||||
|
```bash
|
||||||
|
enbas login --instance gts.enbas-demo.private
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Enbas will send a registration request to your instance and receive a new client ID and secret that it
|
||||||
|
needs for authentication.
|
||||||
|
|
||||||
|
3. Enbas will then generate a link to the consent form for you to access in your browser and print it to
|
||||||
|
your terminal screen along with a message explaining that you need to obtain the `out-of-band` token
|
||||||
|
to continue.
|
||||||
|
|
||||||
|
The link will open in a tab in your preferred browser if you've specified it in your configuration,
|
||||||
|
otherwise you can manually open it yourself.
|
||||||
|
|
||||||
|
If the browser tab doesn't open for you as expected you can still manually open it yourself.
|
||||||
|
|
||||||
|
4. Once you've signed into GoToSocial on your browser,
|
||||||
|
you will be informed that Enbas would like to perform actions on your behalf.
|
||||||
|
If you're happy with this then click on the `Allow` button.
|
||||||
|
![A screenshot of the consent form](../assets/images/consent_form.png "Consent Form")
|
||||||
|
|
||||||
|
5. The `out-of-band` token from your instance will be displayed to you in your browser.
|
||||||
|
Copy it and return to your terminal.
|
||||||
|
|
||||||
|
6. Paste the token into the prompt and press `ENTER`.
|
||||||
|
Enbas will then exchange the token for an access token which will be used to authenticate
|
||||||
|
to your instance on your behalf.
|
||||||
|
|
||||||
|
7. Enbas will then verify the access token, save the credentials to the `credentials.json` file
|
||||||
|
in your configuration directory, and inform you that you have successfully logged into your account.
|
||||||
|
|
||||||
|
### Example login flow
|
||||||
|
|
||||||
|
```
|
||||||
|
$ enbas login --instance gts.enbas-demo.private
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
https://gts.enbas-demo.private/oauth/authorize?client_id=019RD0WVA903F773T5F9D9EYHP&redirect_uri=urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob&response_type=code
|
||||||
|
|
||||||
|
Once you have the code please copy and paste it below.
|
||||||
|
Out-of-band token: ZDRKOTE0NMUTZGVHZC0ZNJVJLWJINTMTMWE1M2UWYWFHOTQY
|
||||||
|
âś” You have successfully logged as percy@gts.enbas-demo.private.
|
||||||
|
```
|
||||||
|
|
||||||
|
## View your account information
|
||||||
|
|
||||||
|
You can verify that you have successfully logged in by viewing your account information.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
enbas show --type account --my-account
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```
|
||||||
|
$ enbas show --type account --my-account
|
||||||
|
|
||||||
|
Percy Cade (@percy)
|
||||||
|
|
||||||
|
ACCOUNT ID:
|
||||||
|
01629QXYN8X597CZDAH4BTY32R
|
||||||
|
|
||||||
|
JOINED ON:
|
||||||
|
29 Jun 2024
|
||||||
|
|
||||||
|
STATS:
|
||||||
|
Followers: 0
|
||||||
|
Following: 0
|
||||||
|
Statuses: 0
|
||||||
|
|
||||||
|
BIOGRAPHY:
|
||||||
|
Hey there, the name's Percy.
|
||||||
|
|
||||||
|
I've been a Platform Engineer in the Healthcare industry for 5 years and
|
||||||
|
counting. I love containerising anything and everything with Docker and
|
||||||
|
Kubernetes, and often find myself dabbling with Python, Go and Rust.
|
||||||
|
|
||||||
|
In my free time I like to cook, blog about FOSS software news and make videos
|
||||||
|
documenting my travels across the UK.
|
||||||
|
|
||||||
|
|
||||||
|
METADATA:
|
||||||
|
Pronouns: he/him
|
||||||
|
Location: Hertfordshire, UK
|
||||||
|
My website: https://percycade.me.private
|
||||||
|
My blogs: https://blogs.percycade.me.private
|
||||||
|
My photos: https://photos.percycade.me.private
|
||||||
|
My videos: https://videos.percycade.me.private
|
||||||
|
|
||||||
|
ACCOUNT URL:
|
||||||
|
https://gts.enbas-demo.private/@percy
|
||||||
|
```
|
||||||
|
|
||||||
|
Now that you have successfully logged into your account proceed to the [user manual](./manual.md).
|
118
docs/installation.md
Normal file
118
docs/installation.md
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
# Installation Guide
|
||||||
|
|
||||||
|
## Download
|
||||||
|
|
||||||
|
Pre-built binaries will soon be available on the release page on both Codeberg and GitHub.
|
||||||
|
|
||||||
|
## Build from source
|
||||||
|
|
||||||
|
### Build requirements
|
||||||
|
|
||||||
|
- **Go:** A minimum version of Go 1.23.0 is required for installing Enbas.
|
||||||
|
Please go [here](https://go.dev/dl/) to download the latest version.
|
||||||
|
|
||||||
|
- **Mage (Optional):** The project includes mage targets for building and installing the binary. The main
|
||||||
|
advantage of using mage over `go install` is that the build information is baked into the binary during
|
||||||
|
compilation. You can visit the [Mage website](https://magefile.org/) for instructions on how to install Mage.
|
||||||
|
|
||||||
|
### Install with mage
|
||||||
|
|
||||||
|
You can install Enbas with Mage using the following commands:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/dananglin/enbas.git
|
||||||
|
mage install
|
||||||
|
```
|
||||||
|
|
||||||
|
By default Mage will attempt to install Enbas to `/usr/local/bin/enbas` which will most likely fail as you won't
|
||||||
|
the permission to write to `/usr/local/bin/`. You will need to either run `sudo mage install`, or you can
|
||||||
|
(preferably) change the install prefix to a directory that you have permission to write to using
|
||||||
|
the `ENBAS_INSTALL_PREFIX` environment variable. For example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ENBAS_INSTALL_PREFIX=${HOME}/.local mage install
|
||||||
|
```
|
||||||
|
|
||||||
|
This will install Enbas to `~/.local/bin/enbas`.
|
||||||
|
|
||||||
|
The table below shows all the environment variables you can use when building with Mage.
|
||||||
|
|
||||||
|
| Environment Variable | Description |
|
||||||
|
|--------------------------|------------------------------------------------------------------------------|
|
||||||
|
|`ENBAS_INSTALL_PREFIX` | Set this to your preferred the installation prefix (default: `/usr/local`). |
|
||||||
|
|`ENBAS_BUILD_REBUILD_ALL` | Set this to `1` to rebuild all packages even if they are already up-to-date. |
|
||||||
|
|`ENBAS_BUILD_VERBOSE` | Set this to `1` to enable verbose logging when building the binary. |
|
||||||
|
|
||||||
|
### Install with go
|
||||||
|
|
||||||
|
If your `GOBIN` directory is included in your `PATH` then you can install Enbas with Go.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/dananglin/enbas.git
|
||||||
|
cd enbas
|
||||||
|
go install ./cmd/enbas
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verify the installation
|
||||||
|
|
||||||
|
Type `enbas` from your terminal to verify that the installation was successful. You should see the help documentation.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ enbas
|
||||||
|
SUMMARY:
|
||||||
|
enbas - A GoToSocial client for the terminal.
|
||||||
|
|
||||||
|
VERSION:
|
||||||
|
v0.2.0
|
||||||
|
|
||||||
|
USAGE:
|
||||||
|
enbas [flags]
|
||||||
|
enbas [flags] [command]
|
||||||
|
|
||||||
|
COMMANDS:
|
||||||
|
accept Accepts a request (e.g. a follow request)
|
||||||
|
add Adds a resource to another resource
|
||||||
|
block Blocks a resource (e.g. an account)
|
||||||
|
create Creates a specific resource
|
||||||
|
delete Deletes a specific resource
|
||||||
|
edit Edit a specific resource
|
||||||
|
follow Follow a resource (e.g. an account)
|
||||||
|
init Creates a new configuration file in the specified configuration directory
|
||||||
|
login Logs into an account on GoToSocial
|
||||||
|
mute Mutes a specific resource (e.g. an account)
|
||||||
|
reject Rejects a request (e.g. a follow request)
|
||||||
|
remove Removes a resource from another resource
|
||||||
|
show Shows details about a specified resource
|
||||||
|
switch Performs a switch operation (e.g. switching between logged in accounts)
|
||||||
|
unblock Unblocks a resource (e.g. an account)
|
||||||
|
unfollow Unfollows a resource (e.g. an account)
|
||||||
|
unmute Umutes a specific resource (e.g. an account)
|
||||||
|
version Prints the application's version and build information
|
||||||
|
whoami Prints the account that you are currently logged into
|
||||||
|
|
||||||
|
FLAGS:
|
||||||
|
--help
|
||||||
|
print the help message
|
||||||
|
--config-dir
|
||||||
|
The path to your configuration directory
|
||||||
|
--no-color
|
||||||
|
Set to true to disable ANSI colour output when displaying text on screen
|
||||||
|
|
||||||
|
Use "enbas [command] --help" for more information about a command.
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also view the application's version and build information using the `version` command.
|
||||||
|
The build information is correctly displayed if you've downloaded the binary from Codeberg or GitHub,
|
||||||
|
or if you've built it with Mage.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ enbas version --full
|
||||||
|
Enbas
|
||||||
|
|
||||||
|
Version: v0.2.0
|
||||||
|
Git commit: fa58e5b
|
||||||
|
Go version: go1.23.0
|
||||||
|
Build date: 2024-08-29T07:24:53Z
|
||||||
|
```
|
||||||
|
|
||||||
|
Once you have completed the installation proceed to the [Getting Started guide](./getting_started.md).
|
875
docs/manual.md
Normal file
875
docs/manual.md
Normal file
|
@ -0,0 +1,875 @@
|
||||||
|
# User Manual
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
- [Global flags](#global-flags)
|
||||||
|
- [Version](#version)
|
||||||
|
- [Print the application version](#print-the-application-version)
|
||||||
|
- [Init](#init)
|
||||||
|
- [Authentication](#authentication)
|
||||||
|
- [Logging into an account](#logging-into-an-account)
|
||||||
|
- [Switch between accounts](#switch-between-accounts)
|
||||||
|
- [See the account that you are currently logged in as](#see-the-account-that-you-are-currently-logged-in-as)
|
||||||
|
- [Accounts](#accounts)
|
||||||
|
- [View account information](#view-account-information)
|
||||||
|
- [Follow an account](#follow-an-account)
|
||||||
|
- [Unfollow an account](#unfollow-an-account)
|
||||||
|
- [Show an account's followers](#show-an-accounts-followers)
|
||||||
|
- [Show account's followings](#show-accounts-followings)
|
||||||
|
- [Block an account](#block-an-account)
|
||||||
|
- [Unblock an account](#unblock-an-account)
|
||||||
|
- [View blocked accounts](#view-blocked-accounts)
|
||||||
|
- [Mute an account](#mute-an-account)
|
||||||
|
- [Unmute an account](#unmute-an-account)
|
||||||
|
- [View muted accounts](#view-muted-accounts)
|
||||||
|
- [Add a private note to an account](#add-a-private-note-to-an-account)
|
||||||
|
- [Remove the private note from an account](#remove-the-private-note-from-an-account)
|
||||||
|
- [Follow requests](#follow-requests)
|
||||||
|
- [View your follow requests](#view-your-follow-requests)
|
||||||
|
- [Accept a follow request](#accept-a-follow-request)
|
||||||
|
- [Reject a follow request](#reject-a-follow-request)
|
||||||
|
- [Media Attachments](#media-attachments)
|
||||||
|
- [Create a media attachment](#create-a-media-attachment)
|
||||||
|
- [Edit a media attachment](#edit-a-media-attachment)
|
||||||
|
- [View a media attachment](#view-a-media-attachment)
|
||||||
|
- [Statuses](#statuses)
|
||||||
|
- [View a status](#view-a-status)
|
||||||
|
- [Create a status](#create-a-status)
|
||||||
|
- [Delete a status](#delete-a-status)
|
||||||
|
- [Boost (Repost) a status](#boost-repost-a-status)
|
||||||
|
- [Un-boost (Un-repost) a status](#un-boost-un-repost-a-status)
|
||||||
|
- [Like a status](#like-a-status)
|
||||||
|
- [Unlike a status](#unlike-a-status)
|
||||||
|
- [View a list of statuses that you've liked](#view-a-list-of-statuses-that-youve-liked)
|
||||||
|
- [Mute a status](#mute-a-status)
|
||||||
|
- [Unmute a status](#unmute-a-status)
|
||||||
|
- [Vote in a poll within a status](#vote-in-a-poll-within-a-status)
|
||||||
|
- [Polls](#polls)
|
||||||
|
- [Create a poll](#create-a-poll)
|
||||||
|
- [View a poll](#view-a-poll)
|
||||||
|
- [Vote in a poll](#vote-in-a-poll)
|
||||||
|
- [Lists](#lists)
|
||||||
|
- [Create a list](#create-a-list)
|
||||||
|
- [View a list of your lists](#view-a-list-of-your-lists)
|
||||||
|
- [View a specific list](#view-a-specific-list)
|
||||||
|
- [Edit a list](#edit-a-list)
|
||||||
|
- [Delete a list](#delete-a-list)
|
||||||
|
- [Add accounts to a list](#add-accounts-to-a-list)
|
||||||
|
- [Remove accounts from a list](#remove-accounts-from-a-list)
|
||||||
|
- [Timelines](#timelines)
|
||||||
|
- [View a timeline](#view-a-timeline)
|
||||||
|
- [Media](#media)
|
||||||
|
- [View media from a status](#view-media-from-a-status)
|
||||||
|
- [Bookmarks](#bookmarks)
|
||||||
|
- [View your bookmarks](#view-your-bookmarks)
|
||||||
|
- [Add a status to your bookmarks](#add-a-status-to-your-bookmarks)
|
||||||
|
- [Remove a status from your bookmarks](#remove-a-status-from-your-bookmarks)
|
||||||
|
- [Notifications](#notifications)
|
||||||
|
|
||||||
|
## Global flags
|
||||||
|
|
||||||
|
| flag | type | required | description | default |
|
||||||
|
|------|------|----------|-------------|---------|
|
||||||
|
| `config-dir` | string | false | The configuration directory. | |
|
||||||
|
| `no-color` | boolean | false | Disables ANSI colour output when displaying text on screen<br>You can also set `NO_COLOR` to any value for the same effect. | false |
|
||||||
|
|
||||||
|
## Version
|
||||||
|
|
||||||
|
### Print the application version
|
||||||
|
|
||||||
|
View the application's version and build information
|
||||||
|
|
||||||
|
```
|
||||||
|
enbas version --full
|
||||||
|
```
|
||||||
|
|
||||||
|
| flag | type | required | description | default |
|
||||||
|
|------|------|----------|-------------|---------|
|
||||||
|
| `full` | boolean | false | Prints the full build information. | false |
|
||||||
|
|
||||||
|
## Init
|
||||||
|
|
||||||
|
Initialises the app by creating a configuration file in the configuration directory.
|
||||||
|
If you want to use a specific directory then use the global `--config-dir` flag.
|
||||||
|
|
||||||
|
```
|
||||||
|
enbas init
|
||||||
|
```
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
### Logging into an account
|
||||||
|
|
||||||
|
Log into your GoToSocial account. You can run this multiple times to log into multiple accounts.
|
||||||
|
|
||||||
|
```
|
||||||
|
enbas login --instance gts.enbas-demo.private
|
||||||
|
```
|
||||||
|
|
||||||
|
| flag | type | required | description | default |
|
||||||
|
|------|------|----------|-------------|---------|
|
||||||
|
| `instance` | string | true | The instance that you want to log into. | |
|
||||||
|
|
||||||
|
### Switch between accounts
|
||||||
|
|
||||||
|
Switch between your logged in accounts.
|
||||||
|
|
||||||
|
```
|
||||||
|
enbas switch --to account --account-name vincent@gts.enbas-demo.private
|
||||||
|
```
|
||||||
|
|
||||||
|
| flag | type | required | description | default |
|
||||||
|
|------|------|----------|-------------|---------|
|
||||||
|
| `to` | string | true | The resource you want to switch to. In this case you want `account`. | |
|
||||||
|
| `account-name` | string | true | The name of the account you want to switch to. | |
|
||||||
|
|
||||||
|
### See the account that you are currently logged in as
|
||||||
|
|
||||||
|
```
|
||||||
|
enbas whoami
|
||||||
|
```
|
||||||
|
|
||||||
|
## Accounts
|
||||||
|
|
||||||
|
### View account information
|
||||||
|
|
||||||
|
- View information from your own account
|
||||||
|
```
|
||||||
|
enbas show --type account --my-account
|
||||||
|
```
|
||||||
|
- View information from a local or remote account.
|
||||||
|
```
|
||||||
|
enbas show --type account --account-name @name@example.social
|
||||||
|
```
|
||||||
|
- View an account and show the public statuses that it has created.
|
||||||
|
```
|
||||||
|
enbas show --type account --account-name @name@example.social --show-statuses --only-public
|
||||||
|
```
|
||||||
|
|
||||||
|
| flag | type | required | description | default |
|
||||||
|
|------|------|----------|-------------|---------|
|
||||||
|
| `type` | string | true | The resource you want to view. Here this should be `account`. | |
|
||||||
|
| `my-account` | boolean | true | Set to `true` to view your own account. | |
|
||||||
|
| `show-preferences` | boolean | false | Show your posting preferences. Only applicable with the `my-account` flag. | false |
|
||||||
|
| `account-name` | string | false | The name of the account to view. This is not required with the `my-account` flag. | |
|
||||||
|
| `skip-relationship` | boolean | false | Set to `true` to skip viewing your relationship to the account you are viewing (including the private note if you've created one). | false |
|
||||||
|
|
||||||
|
Additional flags for viewing an account's statuses.
|
||||||
|
|
||||||
|
| flag | type | required | description | default |
|
||||||
|
|------|------|----------|-------------|---------|
|
||||||
|
| `show-statuses` | bool | false | Set to `true` to view the statuses created from this account. | false |
|
||||||
|
| `limit` | integer | false | The maximum amount of statuses to show from this account. | 20 |
|
||||||
|
| `exclude-replies` | bool | false | Set to `true` to exclude replies. | false |
|
||||||
|
| `exclude-boosts` | bool | false | Set to `true` to exclude boosts. | false |
|
||||||
|
| `only-pinned` | bool | false | Set to `true` to view only pinned statuses. | false |
|
||||||
|
| `only-media` | bool | false | Set to `true` to view only statuses with media attachments. | false |
|
||||||
|
| `only-public` | bool | false | Set to `true` to view only public statuses. | false |
|
||||||
|
|
||||||
|
### Follow an account
|
||||||
|
|
||||||
|
Sends a follow request to the account you want to follow.
|
||||||
|
|
||||||
|
```
|
||||||
|
enbas follow --type account --account-name @name@example.social
|
||||||
|
```
|
||||||
|
|
||||||
|
| flag | type | required | description | default |
|
||||||
|
|------|------|----------|-------------|---------|
|
||||||
|
| `type` | string | true | The resource you want to follow. Here this should be `account`. | |
|
||||||
|
| `account-name` | string | true | The name of the account to follow. | |
|
||||||
|
| `show-reposts` | boolean | false | Show reposts from the account you want to follow. | true |
|
||||||
|
| `notify` | boolean | false | Get notifications when the account you want to follow posts a status. | false |
|
||||||
|
|
||||||
|
### Unfollow an account
|
||||||
|
|
||||||
|
Unfollows the account that you are currently following.
|
||||||
|
If you have a follow request pending for the account in question,
|
||||||
|
performing an unfollow action will remove said follow request.
|
||||||
|
|
||||||
|
```
|
||||||
|
enbas unfollow --type account --account-name @name@example.social
|
||||||
|
```
|
||||||
|
|
||||||
|
| flag | type | required | description | default |
|
||||||
|
|------|------|----------|-------------|---------|
|
||||||
|
| `type` | string | true | The resource you want to unfollow. Here this should be `account`. | |
|
||||||
|
| `account-name` | string | true | The name of the account to unfollow. | |
|
||||||
|
|
||||||
|
### Show an account's followers
|
||||||
|
|
||||||
|
- View followers of your own account.
|
||||||
|
```
|
||||||
|
enbas show --type followers --from account --my-account
|
||||||
|
```
|
||||||
|
- View followers of another account.
|
||||||
|
```
|
||||||
|
enbas show --type followers --from account --account-name @name@example.social
|
||||||
|
```
|
||||||
|
|
||||||
|
| flag | type | required | description | default |
|
||||||
|
|------|------|----------|-------------|---------|
|
||||||
|
| `type` | string | true | The resource you want to view. Here this should be `followers`. | |
|
||||||
|
| `from` | string | true | The resource you want to view followers from.<br>Here this should be `account`. | |
|
||||||
|
| `my-account` | boolean | false | Set to `true` to view followers from your own account.<br>This takes precendence over `account-name`.| false |
|
||||||
|
| `account-name` | string | true | The name of the account to get the followers from. | |
|
||||||
|
|
||||||
|
### Show account's followings
|
||||||
|
|
||||||
|
- View the accounts that you are following.
|
||||||
|
```
|
||||||
|
enbas show --type following --from account --my-account
|
||||||
|
```
|
||||||
|
- View the accounts that another account is following.
|
||||||
|
```
|
||||||
|
enbas show --type following --from account --account-name @name@example.social
|
||||||
|
```
|
||||||
|
|
||||||
|
| flag | type | required | description | default |
|
||||||
|
|------|------|----------|-------------|---------|
|
||||||
|
| `type` | string | true | The resource you want to view. Here this should be `following`. | |
|
||||||
|
| `from` | string | true | The resource you want to view the followings from.<br>Here this should be `account`. | |
|
||||||
|
| `my-account` | boolean | false | Set to `true` to view the list from your own account.<br>This takes precendence over `account-name`.| false |
|
||||||
|
| `account-name` | string | true | The name of the account to get the list from. | |
|
||||||
|
|
||||||
|
### Block an account
|
||||||
|
|
||||||
|
```
|
||||||
|
enbas block --type account --account-name @name@example.social
|
||||||
|
```
|
||||||
|
|
||||||
|
| flag | type | required | description | default |
|
||||||
|
|------|------|----------|-------------|---------|
|
||||||
|
| `type` | string | true | The resource you want to block. Here this should be `account`. | |
|
||||||
|
| `account-name` | string | true | The name of the account to block. | |
|
||||||
|
|
||||||
|
### Unblock an account
|
||||||
|
|
||||||
|
```
|
||||||
|
enbas unblock --type account --account-name @name@example.social
|
||||||
|
```
|
||||||
|
|
||||||
|
| flag | type | required | description | default |
|
||||||
|
|------|------|----------|-------------|---------|
|
||||||
|
| `type` | string | true | The resource you want to unblock. Here this should be `account`. | |
|
||||||
|
| `account-name` | string | true | The name of the account to unblock. | |
|
||||||
|
|
||||||
|
### View blocked accounts
|
||||||
|
|
||||||
|
Prints a list of accounts that you are currently blocking.
|
||||||
|
|
||||||
|
```
|
||||||
|
enbas show --type blocked
|
||||||
|
```
|
||||||
|
|
||||||
|
| flag | type | required | description | default |
|
||||||
|
|------|------|----------|-------------|---------|
|
||||||
|
| `type` | string | true | The resource you want to view.<br>Here this should be `blocked` for blocked accounts. | |
|
||||||
|
| `limit` | integer | false | The maximum number of accounts to list. | 20 |
|
||||||
|
|
||||||
|
### Mute an account
|
||||||
|
|
||||||
|
- Mute an account indefinitely.
|
||||||
|
```
|
||||||
|
enbas mute --type account --account-name @name@example.social --mute-notifications"
|
||||||
|
```
|
||||||
|
- Mute an account for 1 and a half hours.
|
||||||
|
```
|
||||||
|
enbas mute --type account --account-name @name@example.social --mute-notifications --mute-duration="1 hour and 30 minutes"
|
||||||
|
```
|
||||||
|
|
||||||
|
| flag | type | required | description | default |
|
||||||
|
|------|------|----------|-------------|---------|
|
||||||
|
| `type` | string | true | The resource you want to mute.<br>Here this should be `account`. | |
|
||||||
|
| `account-name` | string | true | The name of the account to mute. | |
|
||||||
|
| `mute-notifications` | boolean | false | Set to `true` to mute notifications as well as statuses. | false |
|
||||||
|
| `mute-duration` | [time duration value](tips_and_tricks.md#the-time-duration-value) | false | Specify how long the account should be muted for.<br>Set to `0 seconds` to mute indefinitely | 0 seconds (indefinitely). |
|
||||||
|
|
||||||
|
### Unmute an account
|
||||||
|
|
||||||
|
```
|
||||||
|
enbas unmute --type account --account-name @name@example.social
|
||||||
|
```
|
||||||
|
|
||||||
|
| flag | type | required | description | default |
|
||||||
|
|------|------|----------|-------------|---------|
|
||||||
|
| `type` | string | true | The resource you want to unmute.<br>Here this should be `account`. | |
|
||||||
|
| `account-name` | string | true | The name of the account to unmute. | |
|
||||||
|
|
||||||
|
### View muted accounts
|
||||||
|
|
||||||
|
Prints a list of accounts that you have muted.
|
||||||
|
|
||||||
|
```
|
||||||
|
enbas show --type muted-accounts
|
||||||
|
```
|
||||||
|
|
||||||
|
| flag | type | required | description | default |
|
||||||
|
|------|------|----------|-------------|---------|
|
||||||
|
| `type` | string | true | The resource you want to view.<br>Here this should be `muted-accounts`. | |
|
||||||
|
| `limit` | integer | false | The maximum number of accounts to print. | 20 |
|
||||||
|
|
||||||
|
### Add a private note to an account
|
||||||
|
|
||||||
|
Adds a private note to an account. Private notes can only be viewed by you.
|
||||||
|
|
||||||
|
```
|
||||||
|
enbas add --type note --to account --account-name @name@example.social --content "This person is awesome."
|
||||||
|
```
|
||||||
|
|
||||||
|
| flag | type | required | description | default |
|
||||||
|
|------|------|----------|-------------|---------|
|
||||||
|
| `type` | string | true | The resource you want to add.<br>Here this should be `note`. | |
|
||||||
|
| `to` | string | true | The resource you want to add the note to.<br>Here this should be `account`. | |
|
||||||
|
| `account-name` | string | true | The name of the account that you want to add the note to. | |
|
||||||
|
| `content` | string | true | The content of the note. | |
|
||||||
|
|
||||||
|
### Remove the private note from an account
|
||||||
|
|
||||||
|
```
|
||||||
|
enbas remove --type note --from account --account-name @name@example.social
|
||||||
|
```
|
||||||
|
|
||||||
|
| flag | type | required | description | default |
|
||||||
|
|------|------|----------|-------------|---------|
|
||||||
|
| `type` | string | true | The resource you want to remove.<br>Here this should be `note`. | |
|
||||||
|
| `from` | string | true | The resource you want to remove the note to.<br>Here this should be `account`. | |
|
||||||
|
| `account-name` | string | true | The name of the account that you want to remove the note from. | |
|
||||||
|
|
||||||
|
## Follow requests
|
||||||
|
|
||||||
|
### View your follow requests
|
||||||
|
|
||||||
|
Prints a list of accounts that are requesting to follow you.
|
||||||
|
|
||||||
|
```
|
||||||
|
enbas show --type follow-request
|
||||||
|
```
|
||||||
|
|
||||||
|
| flag | type | required | description | default |
|
||||||
|
|------|------|----------|-------------|---------|
|
||||||
|
| `type` | string | true | The resource you want to view.<br>Here this should be `follow-request`. | |
|
||||||
|
| `limit` | integer | false | The maximum number of accounts to print. | 20 |
|
||||||
|
|
||||||
|
### Accept a follow request
|
||||||
|
|
||||||
|
Accepts the request from the account that wants to follow you.
|
||||||
|
|
||||||
|
```
|
||||||
|
enbas accept --type follow-request --account-name @person.example.social
|
||||||
|
```
|
||||||
|
|
||||||
|
| flag | type | required | description | default |
|
||||||
|
|------|------|----------|-------------|---------|
|
||||||
|
| `type` | string | true | The resource you want to accept.<br>Here this should be `follow-request`. | |
|
||||||
|
| `account-name` | string | true | The name of the account that you want to accept. | |
|
||||||
|
|
||||||
|
### Reject a follow request
|
||||||
|
|
||||||
|
Rejects the request from the account that wants to follow you.
|
||||||
|
|
||||||
|
```
|
||||||
|
enbas reject --type follow-request --account-name @person.example.social
|
||||||
|
```
|
||||||
|
|
||||||
|
| flag | type | required | description | default |
|
||||||
|
|------|------|----------|-------------|---------|
|
||||||
|
| `type` | string | true | The resource you want to accept.<br>Here this should be `follow-request`. | |
|
||||||
|
| `account-name` | string | true | The name of the account that you want to reject. | |
|
||||||
|
|
||||||
|
## Media Attachments
|
||||||
|
|
||||||
|
### Create a media attachment
|
||||||
|
|
||||||
|
Uploads media from a file to the instance and creates a media attachment.
|
||||||
|
You can write a description of the media in a text file and specify the path with the `media-description` flag (see the examples below).
|
||||||
|
|
||||||
|
- Create a media attachment with a simple description and a focus of x=-0.1, y=0.5
|
||||||
|
```
|
||||||
|
enbas create --type media-attachment \
|
||||||
|
--media-file picture.png \
|
||||||
|
--media-description "A picture of an old, slanted wooden bench in front of the woods." \
|
||||||
|
--media-focus "-0.1,0.5"
|
||||||
|
```
|
||||||
|
- Create a media attachment using a description written in the `description.txt` text file.
|
||||||
|
```
|
||||||
|
enbas create --type media-attachment \
|
||||||
|
--media-file picture.png \
|
||||||
|
--media-description file@description.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
| flag | type | required | description | default |
|
||||||
|
|------|------|----------|-------------|---------|
|
||||||
|
| `type` | string | true | The resource you want to create.<br>Here this should be `media-attachment`. | |
|
||||||
|
| `media-file` | string | true | The path to the media file. | |
|
||||||
|
| `media-description` | string | false | The description of the media attachment which will be used as the media's alt-text.<br>To use a description from a text file, use the `flag@` prefix followed by the path to the file (e.g. `file@description.txt`)| |
|
||||||
|
| `media-focus` | string | false | The media's focus values. This should be in the form of two comma-separated numbers between -1 and 1 (e.g. 0.25,-0.34) | |
|
||||||
|
|
||||||
|
### Edit a media attachment
|
||||||
|
|
||||||
|
Edits the description and/or the focus of a media attachment that you own.
|
||||||
|
|
||||||
|
```
|
||||||
|
enbas edit --type media-attachment \
|
||||||
|
--attachment-id 01J5B9A8WFK59W11MS6AHPYWBR \
|
||||||
|
--media-description "An updated description of a picture."
|
||||||
|
```
|
||||||
|
|
||||||
|
| flag | type | required | description | default |
|
||||||
|
|------|------|----------|-------------|---------|
|
||||||
|
| `type` | string | true | The resource you want to edit.<br>Here this should be `media-attachment`. | |
|
||||||
|
| `media-description` | string | false | The description of the media attachment to edit.<br>To use a description from a text file, use the `flag@` prefix followed by the path to the file (e.g. `file@description.txt`)| |
|
||||||
|
| `media-focus` | string | false | The media's focus values. This should be in the form of two comma-separated numbers between -1 and 1 (e.g. 0.25,-0.34) | |
|
||||||
|
|
||||||
|
### View a media attachment
|
||||||
|
|
||||||
|
Prints information about a given media attachment that you own.
|
||||||
|
You can only see information about the media attachment that you own.
|
||||||
|
|
||||||
|
```
|
||||||
|
enbas show --type media-attachment --attachment-id 01J0N0RQSJ7CFGKHA30F7GBQXT
|
||||||
|
```
|
||||||
|
|
||||||
|
| flag | type | required | description | default |
|
||||||
|
|------|------|----------|-------------|---------|
|
||||||
|
| `type` | string | true | The resource you want to view.<br>Here this should be `media`. | |
|
||||||
|
| `attachment-id` | string | true | The ID of the media attachment to view. | |
|
||||||
|
|
||||||
|
## Statuses
|
||||||
|
|
||||||
|
### View a status
|
||||||
|
|
||||||
|
Prints information of a status on screen.
|
||||||
|
If the `--browser` flag is used, the link to the status is opened instead.
|
||||||
|
To enable browser support you must specify the browser in your configuration.
|
||||||
|
|
||||||
|
See the [configuration reference page](configuration.md#integration) on how to set up integration with
|
||||||
|
your browser if you have not done so already.
|
||||||
|
|
||||||
|
```
|
||||||
|
enbas show --type status --status-id 01J1Z9PT0243JT9QNQ5W96Z8CA
|
||||||
|
```
|
||||||
|
|
||||||
|
| flag | type | required | description | default |
|
||||||
|
|------|------|----------|-------------|---------|
|
||||||
|
| `type` | string | true | The resource you want to view.<br>Here this should be `status`. | |
|
||||||
|
| `status-id` | string | true | The ID of the status that you want to view. | |
|
||||||
|
| `browser` | boolean | false | Set to true to open the link to the status in your browser. | false |
|
||||||
|
|
||||||
|
### Create a status
|
||||||
|
|
||||||
|
Creates a new status.
|
||||||
|
|
||||||
|
- Create a simple status that is publicly visible.
|
||||||
|
```
|
||||||
|
enbas create --type status --content-type plain --visibility public --content "Hello, Fediverse!"
|
||||||
|
```
|
||||||
|
- Create a private status from a file.
|
||||||
|
```
|
||||||
|
enbas create --type status --content-type markdown --visibility private --content file@status.md
|
||||||
|
```
|
||||||
|
- Reply to another status.
|
||||||
|
```
|
||||||
|
enbas create --type status --in-reply-to 01J2A86E3M7WWH37H1QENT7CSH --content "@bernie thanks for this! Looking forward to trying this out."
|
||||||
|
```
|
||||||
|
- Create a status with a poll
|
||||||
|
```
|
||||||
|
enbas create \
|
||||||
|
--type status \
|
||||||
|
--content-type plain \
|
||||||
|
--visibility public \
|
||||||
|
--content "The age-old question: which text editor do you prefer?" \
|
||||||
|
--add-poll \
|
||||||
|
--poll-allows-multiple-choices=false \
|
||||||
|
--poll-expires-in "7 days" \
|
||||||
|
--poll-option "emacs" \
|
||||||
|
--poll-option "vim/neovim" \
|
||||||
|
--poll-option "nano" \
|
||||||
|
--poll-option "other (please comment)"
|
||||||
|
```
|
||||||
|
![A screenshot of a status with a poll](../assets/images/created_poll.png "A status with a poll")
|
||||||
|
- Create a status with a media attachment that you have created.
|
||||||
|
```
|
||||||
|
enbas create \
|
||||||
|
--type status \
|
||||||
|
--attachment-id 01J5BDHYJ7MWMMG76FP49H7SWD \
|
||||||
|
--content "I went out for a walk in the woods and found this interesting looking wooden bench."
|
||||||
|
```
|
||||||
|
- Upload and attach 4 media files to a new status. You must set the same number of `media-description` and `media-focus` flags **must** as the `media-file` flags.
|
||||||
|
The first `media-description` and `media-focus` flags correspond to the first `media-file` flag and so on.
|
||||||
|
```
|
||||||
|
enbas create --type status --visibility public \
|
||||||
|
--content "This post has a picture of a cat, a dog, a bee and a bird." \
|
||||||
|
--media-file cat.jpg --media-description file@cat.txt --media-focus "0,0" \
|
||||||
|
--media-file dog.jpg --media-description file@dog.txt --media-focus "-0.1,0.25" \
|
||||||
|
--media-file bee.jpg --media-description file@bee.txt --media-focus "1,1" \
|
||||||
|
--media-file bird.webp --media-description file@bird.txt --media-focus "0,0"
|
||||||
|
```
|
||||||
|
|
||||||
|
| flag | type | required | description | default |
|
||||||
|
|------|------|----------|-------------|---------|
|
||||||
|
| `type` | string | true | The resource you want to create.<br>Here this should be `status`. | |
|
||||||
|
| `attachment-id` | string | false | The ID of the media attachment to attach to the status.<br>Use this flag multiple times to attach multiple media. |
|
||||||
|
| `content` | string | false | The content of the status.<br>To read the content from a text file, use the `flag@` prefix followed by the path to the file (e.g. `file@status.md`).| |
|
||||||
|
| `content-type` | string | false | The format that the content is created in.<br>Valid values are `plain` and `markdown`. | plain |
|
||||||
|
| `enable-reposts` | boolean | false | The status can be reposted (boosted) by others. | true |
|
||||||
|
| `enable-federation` | boolean | false | The status can be federated beyond the local timelines. | true |
|
||||||
|
| `enable-likes` | boolean | false | The status can be liked (favourtied). | true |
|
||||||
|
| `enable-replies` | boolean | false | The status can be replied to. | true |
|
||||||
|
| `in-reply-to` | string | false | The ID of the status that you want to reply to. | |
|
||||||
|
| `language` | string | false | The ISO 639 language code that the status is written in.<br>If this is not specified then the default language from your posting preferences will be used. | |
|
||||||
|
| `media-file` | string | false | The path to the media file.<br>Use this flag multiple times to upload multiple media files. | |
|
||||||
|
| `media-description` | string | false | The description of the media attachment which will be used as the media's alt-text.<br>To use a description from a text file, use the `flag@` prefix followed by the path to the file (e.g. `file@description.txt`)<br>Use this flag multiple times to set multiple descriptions.| |
|
||||||
|
| `media-focus` | string | false | The media's focus values. This should be in the form of two comma-separated numbers between -1 and 1 (e.g. 0.25,-0.34).<br>Use this flag multiple times to set multiple focus values. | |
|
||||||
|
| `sensitive` | string | false | The status should be marked as sensitive.<br>If this is not specified then the default sensitivity from your posting preferences will be used. | |
|
||||||
|
| `summary` | string | false | The summary of the status (a.k.a the subject, spoiler text or content warning). | |
|
||||||
|
| `visibility` | string | false | The visibility of the status.<br>Valid values are `public`, `private`, `unlisted`, `mutuals_only` and `direct`.<br>If this is not specified then the default visibility from your posting preferences will be used. | |
|
||||||
|
|
||||||
|
Additional flags for polls.
|
||||||
|
|
||||||
|
| flag | type | required | description | default |
|
||||||
|
|------|------|----------|-------------|---------|
|
||||||
|
| `add-poll` | boolean | false | Set to `true` to add a poll to the status. | false |
|
||||||
|
| `poll-allows-multiple-choices` | boolean | false | Set to `true` to allow users to make multiple choices. | false |
|
||||||
|
| `poll-hides-vote-counts` | boolean | false | Set to `true` to hide the vote count until the poll is closed. | false |
|
||||||
|
| `poll-option` | string | true | An option in the poll. Use this flag multiple times to set multiple options. | |
|
||||||
|
| `poll-expires-in` | [time duration value](tips_and_tricks.md#the-time-duration-value) | false | The duration in which the poll is open for. | |
|
||||||
|
|
||||||
|
### Delete a status
|
||||||
|
|
||||||
|
Deletes a status that belongs to you.
|
||||||
|
You can optionally save the text of the deleted status for redrafting purposes.
|
||||||
|
The saved text will be written to a text file within your cache directory.
|
||||||
|
|
||||||
|
```
|
||||||
|
enbas delete --type status --status-id 01J5B0N6DKZGYPQEZW9HWKV0VA
|
||||||
|
```
|
||||||
|
|
||||||
|
| flag | type | required | description | default |
|
||||||
|
|------|------|----------|-------------|---------|
|
||||||
|
| `type` | string | true | The resource you want to delete.<br>Here this should be `status`. | |
|
||||||
|
| `status-id` | string | true | The ID of the status that you want to delete. | |
|
||||||
|
| `save-text` | bool | false | Set to `true` to save the text of the deleted status. | false |
|
||||||
|
|
||||||
|
### Boost (Repost) a status
|
||||||
|
|
||||||
|
To boost a status, simply add a `boost` to it.
|
||||||
|
|
||||||
|
```
|
||||||
|
enbas add --type boost --to status --status-id 01J17FH1KD9CN6J9Q01011NE0D
|
||||||
|
```
|
||||||
|
|
||||||
|
| flag | type | required | description | default |
|
||||||
|
|------|------|----------|-------------|---------|
|
||||||
|
| `type` | string | true | The resource you want to add.<br>Here this should be `boost`. | |
|
||||||
|
| `to` | string | true | The resource you want to add the boost to.<br>Here this should be `status`. | |
|
||||||
|
| `status-id` | string | true | The ID of the status that you want to boost. | |
|
||||||
|
|
||||||
|
### Un-boost (Un-repost) a status
|
||||||
|
|
||||||
|
To un-boost a status that you've boosted, simply remove the `boost` from it.
|
||||||
|
|
||||||
|
```
|
||||||
|
enbas remove --type boost --from status --status-id 01J17FH1KD9CN6J9Q01011NE0D
|
||||||
|
```
|
||||||
|
|
||||||
|
| flag | type | required | description | default |
|
||||||
|
|------|------|----------|-------------|---------|
|
||||||
|
| `type` | string | true | The resource you want to add.<br>Here this should be `boost`. | |
|
||||||
|
| `from` | string | true | The resource you want to remove the boost from.<br>Here this should be `status`. | |
|
||||||
|
| `status-id` | string | true | The ID of the status that you want to un-boost. | |
|
||||||
|
|
||||||
|
### Like a status
|
||||||
|
|
||||||
|
To like (favourite) a status, simply add a `like` or a `star` to it.
|
||||||
|
|
||||||
|
```
|
||||||
|
enbas add --type star --to status --status-id 01J17FH1KD9CN6J9Q01011NE0D
|
||||||
|
```
|
||||||
|
|
||||||
|
| flag | type | required | description | default |
|
||||||
|
|------|------|----------|-------------|---------|
|
||||||
|
| `type` | string | true | The resource you want to add.<br>Here this should either be `like` or `star`. | |
|
||||||
|
| `to` | string | true | The resource you want to like.<br>Here this should be `status`. | |
|
||||||
|
| `status-id` | string | true | The ID of the status that you want to like. | |
|
||||||
|
|
||||||
|
### Unlike a status
|
||||||
|
|
||||||
|
To unlike (un-favourite) a status that you've previously liked, simply remove the `like` or `star` from it.
|
||||||
|
|
||||||
|
```
|
||||||
|
enbas remove --type star --from status --status-id 01J17FH1KD9CN6J9Q01011NE0D
|
||||||
|
```
|
||||||
|
|
||||||
|
| flag | type | required | description | default |
|
||||||
|
|------|------|----------|-------------|---------|
|
||||||
|
| `type` | string | true | The resource you want to add.<br>Here this should either be `like` or `star`. | |
|
||||||
|
| `from` | string | true | The resource you want to remove the like from.<br>Here this should be `status`. | |
|
||||||
|
| `status-id` | string | true | The ID of the status that you want to remove the like from. | |
|
||||||
|
|
||||||
|
### View a list of statuses that you've liked
|
||||||
|
|
||||||
|
Prints the list of statuses that you've liked.
|
||||||
|
|
||||||
|
```
|
||||||
|
enbas show --type liked
|
||||||
|
```
|
||||||
|
|
||||||
|
| flag | type | required | description | default |
|
||||||
|
|------|------|----------|-------------|---------|
|
||||||
|
| `type` | string | true | The resource you want to view.<br>Here this should either be `liked` or `starred`. | |
|
||||||
|
| `limit` | integer | false | The maximum number of statuses to print. | 20 |
|
||||||
|
|
||||||
|
### Mute a status
|
||||||
|
|
||||||
|
Mutes a status in order to stop receiving future notifications for replies, likes, boosts, etc.
|
||||||
|
|
||||||
|
```
|
||||||
|
enbas mute --type status --status-id 01J56ZJAGEWG967GS1EK0TV3GA
|
||||||
|
```
|
||||||
|
|
||||||
|
| flag | type | required | description | default |
|
||||||
|
|------|------|----------|-------------|---------|
|
||||||
|
| `type` | string | true | The resource you want to mute.<br>Here this should be `status`. | |
|
||||||
|
| `status-id` | string | true | The ID of the status that you want to mute. | |
|
||||||
|
|
||||||
|
### Unmute a status
|
||||||
|
|
||||||
|
Unmute a status in order to start receiving future notification from the status' thread.
|
||||||
|
|
||||||
|
```
|
||||||
|
enbas mute --type status --status-id 01J56ZJAGEWG967GS1EK0TV3GA
|
||||||
|
```
|
||||||
|
|
||||||
|
| flag | type | required | description | default |
|
||||||
|
|------|------|----------|-------------|---------|
|
||||||
|
| `type` | string | true | The resource you want to unmute.<br>Here this should be `status`. | |
|
||||||
|
| `status-id` | string | true | The ID of the status that you want to unmute. | |
|
||||||
|
|
||||||
|
### Vote in a poll within a status
|
||||||
|
|
||||||
|
Adds your vote(s) to a poll within a status.
|
||||||
|
|
||||||
|
```
|
||||||
|
enbas add --type vote --to status --status-id 01J55XVV2MM6MKQ7QHFBAVAE8R --vote 3
|
||||||
|
```
|
||||||
|
|
||||||
|
| flag | type | required | description | default |
|
||||||
|
|------|------|----------|-------------|---------|
|
||||||
|
| `type` | string | true | The resource you want to add.<br>Here this should be `vote`. | |
|
||||||
|
| `to` | string | true | The resource you want to add the vote to.<br>Here this should be `status`. | |
|
||||||
|
| `status-id` | string | true | The ID of the poll you want to add the votes to. | |
|
||||||
|
| `vote` | int | true | The ID of the option that you want to vote for.<br>You can use this flag multiple times to vote for more than one option if the poll allows multiple choices. | |
|
||||||
|
|
||||||
|
## Polls
|
||||||
|
|
||||||
|
### Create a poll
|
||||||
|
|
||||||
|
See [Create a status](#create-a-status).
|
||||||
|
|
||||||
|
### View a poll
|
||||||
|
|
||||||
|
You can view a poll within a [status](#view-a-status) or within a [timeline](#view-a-timeline).
|
||||||
|
|
||||||
|
### Vote in a poll
|
||||||
|
|
||||||
|
See [Vote in a poll within a status](#vote-in-a-poll-within-a-status)
|
||||||
|
|
||||||
|
## Lists
|
||||||
|
|
||||||
|
### Create a list
|
||||||
|
|
||||||
|
```
|
||||||
|
enbas create --type list --list-title "My Favourite People" --list-replies-policy list
|
||||||
|
```
|
||||||
|
|
||||||
|
| flag | type | required | description | default |
|
||||||
|
|------|------|----------|-------------|---------|
|
||||||
|
| `type` | string | true | The resource you want to create.<br>Here this should be `list`. | |
|
||||||
|
| `list-title` | string | true | The title of the list that you want to create. | |
|
||||||
|
| `list-replies-policy` | string | false | The policy of the replies for this list.<br>Valid values are `followed`, `list` and `none`. | list |
|
||||||
|
|
||||||
|
### View a list of your lists
|
||||||
|
|
||||||
|
```
|
||||||
|
enbas show --type list
|
||||||
|
```
|
||||||
|
|
||||||
|
| flag | type | required | description | default |
|
||||||
|
|------|------|----------|-------------|---------|
|
||||||
|
| `type` | string | true | The resource you want to view.<br>Here this should be `list`. | |
|
||||||
|
|
||||||
|
### View a specific list
|
||||||
|
|
||||||
|
Prints the information of the specified list to screen along with all the accounts added to it (if any).
|
||||||
|
|
||||||
|
```
|
||||||
|
enbas show --type list --list-id 01J1T9DWR20DC36QWZFKHWZJ3H
|
||||||
|
```
|
||||||
|
|
||||||
|
| flag | type | required | description | default |
|
||||||
|
|------|------|----------|-------------|---------|
|
||||||
|
| `type` | string | true | The resource you want to view.<br>Here this should be `list`. | |
|
||||||
|
| `list-id` | string | false | The ID of the list you want to view. If this is not specified then a list of your lists will be printed instead. | |
|
||||||
|
|
||||||
|
### Edit a list
|
||||||
|
|
||||||
|
Edits the title and/or the replies policy of a list.
|
||||||
|
|
||||||
|
```
|
||||||
|
enbas edit --type list --list-id 01J1T9DWR20DC36QWZFKHWZJ3H --list-title "My Favourite People (in the world)"
|
||||||
|
```
|
||||||
|
|
||||||
|
| flag | type | required | description | default |
|
||||||
|
|------|------|----------|-------------|---------|
|
||||||
|
| `type` | string | true | The resource you want to edit.<br>Here this should be `list`. | |
|
||||||
|
| `list-title` | string | false | The title of the list that you want to edit. | |
|
||||||
|
| `list-replies-policy` | string | false | The policy of the replies for this list that you want to change to.<br>Valid values are `followed`, `list` and `none`. | |
|
||||||
|
|
||||||
|
### Delete a list
|
||||||
|
|
||||||
|
Deletes a list.
|
||||||
|
|
||||||
|
```
|
||||||
|
enbas delete --type list --list-id 01J1T9DWR20DC36QWZFKHWZJ3H
|
||||||
|
```
|
||||||
|
|
||||||
|
| flag | type | required | description | default |
|
||||||
|
|------|------|----------|-------------|---------|
|
||||||
|
| `type` | string | true | The resource you want to delete.<br>Here this should be `list`. | |
|
||||||
|
| `list-id` | string | true | The ID of the list you want to delete. | |
|
||||||
|
|
||||||
|
### Add accounts to a list
|
||||||
|
|
||||||
|
Adds one or more accounts to a list.
|
||||||
|
|
||||||
|
```
|
||||||
|
enbas add --type account --account-name @name@example.social --account-name @person@mastodon.example --to list --list-id 01J1T9DWR20DC36QWZFKHWZJ3H
|
||||||
|
```
|
||||||
|
|
||||||
|
| flag | type | required | description | default |
|
||||||
|
|------|------|----------|-------------|---------|
|
||||||
|
| `type` | string | true | The resource you want to add.<br>Here this should be `account`. | |
|
||||||
|
| `account-name` | string | true | The name of the account you want to add to the list.<br>Use multiple times to specify multiple accounts. | |
|
||||||
|
| `to` | string | true | The resource you want to add the accounts to.<br>Here this should be `list`. | |
|
||||||
|
| `list-id` | string | true | The ID of the list that you want to add the accounts to. | |
|
||||||
|
|
||||||
|
### Remove accounts from a list
|
||||||
|
|
||||||
|
Removes one or more accounts from a list.
|
||||||
|
|
||||||
|
```
|
||||||
|
enbas remove --type account --account-name @person@mastodon.example --from list --list-id 01J1T9DWR20DC36QWZFKHWZJ3H
|
||||||
|
```
|
||||||
|
|
||||||
|
| flag | type | required | description | default |
|
||||||
|
|------|------|----------|-------------|---------|
|
||||||
|
| `type` | string | true | The resource you want to add.<br>Here this should be `account`. | |
|
||||||
|
| `account-name` | string | true | The name of the account you want to remove from the list.<br>Use multiple times to specify multiple accounts. | |
|
||||||
|
| `to` | string | true | The resource you want to remove the accounts from.<br>Here this should be `list`. | |
|
||||||
|
| `list-id` | string | true | The ID of the list that you want to remove the accounts from. | |
|
||||||
|
|
||||||
|
## Timelines
|
||||||
|
|
||||||
|
### View a timeline
|
||||||
|
|
||||||
|
Prints a list of statuses from a timeline.
|
||||||
|
|
||||||
|
- View your home timeline.
|
||||||
|
```
|
||||||
|
enbas show --type timeline --timeline-category home
|
||||||
|
```
|
||||||
|
- View a maximum of 5 statuses from your instance's public timeline.
|
||||||
|
```
|
||||||
|
enbas show --type timeline --timeline-category public --limit 5
|
||||||
|
```
|
||||||
|
- View a timeline from one of your lists.
|
||||||
|
```
|
||||||
|
enbas show --type timeline --timeline-category list --list-id 01J1T9DWR20DC36QWZFKHWZJ3H
|
||||||
|
```
|
||||||
|
- View a timeline from a hashtag.
|
||||||
|
```
|
||||||
|
enbas show --type timeline --timeline-category tag --tag caturday
|
||||||
|
```
|
||||||
|
|
||||||
|
| flag | type | required | description | default |
|
||||||
|
|------|------|----------|-------------|---------|
|
||||||
|
| `type` | string | true | The resource you want to view.<br>Here this should be `timeline`. | |
|
||||||
|
| `timeline-category` | string | false | The type of timeline you want to view.<br>Valid values are `home`, `public`, `list` and `tag`. | home |
|
||||||
|
| `list-id` | string | false | The ID of the list you want to view.<br>This is only required if `timeline-category` is set to `list`. | |
|
||||||
|
| `tag` | string | false | The hashtag you want to view.<br>This is only required if `timeline-category` is set to `tag`. | |
|
||||||
|
| `limit` | integer | false | The maximum number of statuses to print. | 20 |
|
||||||
|
|
||||||
|
## Media
|
||||||
|
|
||||||
|
### View media from a status
|
||||||
|
|
||||||
|
Downloads and opens media attachment(s) from a status.
|
||||||
|
Enbas currently supports viewing images and videos.
|
||||||
|
The media is downloaded to your cache directory before Enbas opens it with your preferred media player.
|
||||||
|
In order to view images and videos, you must specify your image viewer and
|
||||||
|
video player in your configuration file respectively.
|
||||||
|
|
||||||
|
See the [configuration reference page](configuration.md#integration) on how to set up integration with
|
||||||
|
your media players.
|
||||||
|
|
||||||
|
- View a specific media attachment from a specific status
|
||||||
|
```
|
||||||
|
enbas show --type media --from status --status-id 01J0N11V4V7PWH0DDRAVT7TCFK --attachment-id 01J0N0RQSJ7CFGKHA30F7GBQXT
|
||||||
|
```
|
||||||
|
- View all image attachments from a specific status
|
||||||
|
```
|
||||||
|
enbas show --type media --from status --status-id 01J0N11V4V7PWH0DDRAVT7TCFK --all-images
|
||||||
|
```
|
||||||
|
|
||||||
|
| flag | type | required | description | default |
|
||||||
|
|------|------|----------|-------------|---------|
|
||||||
|
| `type` | string | true | The resource you want to view.<br>Here this should be `media`. | |
|
||||||
|
| `from` | string | true | The resource you want to view the media from.<br>Here this should be `status`. | |
|
||||||
|
| `status-id` | string | true | The ID of the status that you want to view the media from. | |
|
||||||
|
| `attachment-id` | string | false | The ID of the media attachment to download and view.<br>Use this flag multiple times to specify multiple media attachments. | |
|
||||||
|
| `all-images` | boolean | false | Set to `true` to show all images from the status. | false |
|
||||||
|
| `all-videos` | boolean | false | Set to `true` to show all videos from the status. | false |
|
||||||
|
|
||||||
|
## Bookmarks
|
||||||
|
|
||||||
|
### View your bookmarks
|
||||||
|
|
||||||
|
Prints a list of statuses that you have bookmarked.
|
||||||
|
|
||||||
|
```
|
||||||
|
enbas show --type bookmarks
|
||||||
|
```
|
||||||
|
|
||||||
|
| flag | type | required | description | default |
|
||||||
|
|------|------|----------|-------------|---------|
|
||||||
|
| `type` | string | true | The resource you want to view.<br>Here this should be `bookmarks`. | |
|
||||||
|
| `limit` | integer | false | The maximum number of bookmarks to show. | 20 |
|
||||||
|
|
||||||
|
### Add a status to your bookmarks
|
||||||
|
|
||||||
|
```
|
||||||
|
enbas add --type status --status-id 01J17FH1KD9CN6J9Q01011NE0D --to bookmarks
|
||||||
|
```
|
||||||
|
|
||||||
|
| flag | type | required | description | default |
|
||||||
|
|------|------|----------|-------------|---------|
|
||||||
|
| `type` | string | true | The resource you want to add.<br>Here this should be `status`. | |
|
||||||
|
| `status-id` | string | true | The ID of the status. | |
|
||||||
|
| `to` | string | true | The resource you want to add the status to.<br>Here this should be `bookmarks`. | |
|
||||||
|
|
||||||
|
### Remove a status from your bookmarks
|
||||||
|
|
||||||
|
```
|
||||||
|
enbas remove --type status --status-id 01J17FH1KD9CN6J9Q01011NE0D --from bookmarks
|
||||||
|
```
|
||||||
|
|
||||||
|
| flag | type | required | description | default |
|
||||||
|
|------|------|----------|-------------|---------|
|
||||||
|
| `type` | string | true | The resource you want to remove.<br>Here this should be `status`. | |
|
||||||
|
| `status-id` | string | true | The ID of the status. | |
|
||||||
|
| `from` | string | true | The resource you want to remove the status to.<br>Here this should be `bookmarks`. | |
|
||||||
|
|
||||||
|
## Notifications
|
||||||
|
|
||||||
|
_Not yet supported_
|
24
docs/tips_and_tricks.md
Normal file
24
docs/tips_and_tricks.md
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
# Tips and Tricks
|
||||||
|
|
||||||
|
## The time duration value
|
||||||
|
|
||||||
|
The time duration value is a custom [flag value](https://pkg.go.dev/flag#Value) that converts a string input into a duration of time.
|
||||||
|
A typical string input would be in the form of something like `"3 days, 12 hours and 39 minutes"`.
|
||||||
|
The value can convert units in days, hours, minutes and seconds.
|
||||||
|
|
||||||
|
To ensure that your string input is converted correctly there are simple rules to follow.
|
||||||
|
|
||||||
|
- The input must be wrapped in quotes.
|
||||||
|
- Use `day` or `days` to convert the number of days.
|
||||||
|
- Use `hour` or `hours` to convert the number of hours.
|
||||||
|
- Use `minute` or `minutes` to convert the number of minutes.
|
||||||
|
- Use `second` or `seconds` to convert the number of seconds.
|
||||||
|
- There must be at least one space between the number and the unit of time.<br>
|
||||||
|
E.g. `"7 days"` is valid, but `"7days"` is invalid.
|
||||||
|
|
||||||
|
### Example valid string inputs
|
||||||
|
|
||||||
|
- `"3 days"`
|
||||||
|
- `"6 hours, 45 minutes and 1 second"`
|
||||||
|
- `"1 day, 15 hours 31 minutes and 12 seconds"`
|
||||||
|
- `"(7 days) (1 hour) (21 minutes) (35 seconds)"`
|
8
go.mod
8
go.mod
|
@ -1,9 +1,5 @@
|
||||||
// SPDX-FileCopyrightText: 2024 Dan Anglin <d.n.i.anglin@gmail.com>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
module codeflow.dananglin.me.uk/apollo/enbas
|
module codeflow.dananglin.me.uk/apollo/enbas
|
||||||
|
|
||||||
go 1.22.0
|
go 1.23.0
|
||||||
|
|
||||||
require golang.org/x/net v0.21.0
|
require golang.org/x/net v0.28.0
|
||||||
|
|
4
go.sum
4
go.sum
|
@ -1,2 +1,2 @@
|
||||||
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
|
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
|
||||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
SPDX-FileCopyrightText: 2024 Dan Anglin <d.n.i.anglin@gmail.com>
|
|
||||||
|
|
||||||
SPDX-License-Identifier: CC0-1.0
|
|
|
@ -1,7 +1,3 @@
|
||||||
// SPDX-FileCopyrightText: 2024 Dan Anglin <d.n.i.anglin@gmail.com>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -13,13 +9,25 @@ import (
|
||||||
"codeflow.dananglin.me.uk/apollo/enbas/internal/model"
|
"codeflow.dananglin.me.uk/apollo/enbas/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
baseAccountsPath = "/api/v1/accounts"
|
||||||
|
baseFollowRequestsPath = "/api/v1/follow_requests"
|
||||||
|
)
|
||||||
|
|
||||||
func (g *Client) VerifyCredentials() (model.Account, error) {
|
func (g *Client) VerifyCredentials() (model.Account, error) {
|
||||||
path := "/api/v1/accounts/verify_credentials"
|
url := g.Authentication.Instance + baseAccountsPath + "/verify_credentials"
|
||||||
url := g.Authentication.Instance + path
|
|
||||||
|
|
||||||
var account model.Account
|
var account model.Account
|
||||||
|
|
||||||
if err := g.sendRequest(http.MethodGet, url, nil, &account); err != nil {
|
params := requestParameters{
|
||||||
|
httpMethod: http.MethodGet,
|
||||||
|
url: url,
|
||||||
|
requestBody: nil,
|
||||||
|
contentType: "",
|
||||||
|
output: &account,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.sendRequest(params); err != nil {
|
||||||
return model.Account{}, fmt.Errorf("received an error after sending the request to verify the credentials: %w", err)
|
return model.Account{}, fmt.Errorf("received an error after sending the request to verify the credentials: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,33 +35,53 @@ func (g *Client) VerifyCredentials() (model.Account, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *Client) GetAccount(accountURI string) (model.Account, error) {
|
func (g *Client) GetAccount(accountURI string) (model.Account, error) {
|
||||||
path := "/api/v1/accounts/lookup?acct=" + accountURI
|
url := g.Authentication.Instance + baseAccountsPath + "/lookup?acct=" + accountURI
|
||||||
url := g.Authentication.Instance + path
|
|
||||||
|
|
||||||
var account model.Account
|
var account model.Account
|
||||||
|
|
||||||
if err := g.sendRequest(http.MethodGet, url, nil, &account); err != nil {
|
params := requestParameters{
|
||||||
|
httpMethod: http.MethodGet,
|
||||||
|
url: url,
|
||||||
|
requestBody: nil,
|
||||||
|
contentType: "",
|
||||||
|
output: &account,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.sendRequest(params); err != nil {
|
||||||
return model.Account{}, fmt.Errorf("received an error after sending the request to get the account information: %w", err)
|
return model.Account{}, fmt.Errorf("received an error after sending the request to get the account information: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return account, nil
|
return account, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *Client) GetAccountRelationship(accountID string) (model.AccountRelationship, error) {
|
func (g *Client) GetAccountRelationship(accountID string) (*model.AccountRelationship, error) {
|
||||||
path := "/api/v1/accounts/relationships?id=" + accountID
|
url := g.Authentication.Instance + baseAccountsPath + "/relationships?id=" + accountID
|
||||||
url := g.Authentication.Instance + path
|
|
||||||
|
|
||||||
var relationships []model.AccountRelationship
|
var relationships []model.AccountRelationship
|
||||||
|
|
||||||
if err := g.sendRequest(http.MethodGet, url, nil, &relationships); err != nil {
|
params := requestParameters{
|
||||||
return model.AccountRelationship{}, fmt.Errorf("received an error after sending the request to get the account relationship: %w", err)
|
httpMethod: http.MethodGet,
|
||||||
|
url: url,
|
||||||
|
requestBody: nil,
|
||||||
|
contentType: "",
|
||||||
|
output: &relationships,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.sendRequest(params); err != nil {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"received an error after sending the request to get the account relationship: %w",
|
||||||
|
err,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(relationships) != 1 {
|
if len(relationships) != 1 {
|
||||||
return model.AccountRelationship{}, fmt.Errorf("unexpected number of account relationships returned: want 1, got %d", len(relationships))
|
return nil, fmt.Errorf(
|
||||||
|
"unexpected number of account relationships returned: want 1, got %d",
|
||||||
|
len(relationships),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return relationships[0], nil
|
return &relationships[0], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type FollowAccountForm struct {
|
type FollowAccountForm struct {
|
||||||
|
@ -69,9 +97,17 @@ func (g *Client) FollowAccount(form FollowAccountForm) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
requestBody := bytes.NewBuffer(data)
|
requestBody := bytes.NewBuffer(data)
|
||||||
url := g.Authentication.Instance + fmt.Sprintf("/api/v1/accounts/%s/follow", form.AccountID)
|
url := g.Authentication.Instance + baseAccountsPath + "/" + form.AccountID + "/follow"
|
||||||
|
|
||||||
if err := g.sendRequest(http.MethodPost, url, requestBody, nil); err != nil {
|
params := requestParameters{
|
||||||
|
httpMethod: http.MethodPost,
|
||||||
|
url: url,
|
||||||
|
requestBody: requestBody,
|
||||||
|
contentType: applicationJSON,
|
||||||
|
output: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.sendRequest(params); err != nil {
|
||||||
return fmt.Errorf("received an error after sending the follow request: %w", err)
|
return fmt.Errorf("received an error after sending the follow request: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,9 +115,17 @@ func (g *Client) FollowAccount(form FollowAccountForm) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *Client) UnfollowAccount(accountID string) error {
|
func (g *Client) UnfollowAccount(accountID string) error {
|
||||||
url := g.Authentication.Instance + fmt.Sprintf("/api/v1/accounts/%s/unfollow", accountID)
|
url := g.Authentication.Instance + baseAccountsPath + "/" + accountID + "/unfollow"
|
||||||
|
|
||||||
if err := g.sendRequest(http.MethodPost, url, nil, nil); err != nil {
|
params := requestParameters{
|
||||||
|
httpMethod: http.MethodPost,
|
||||||
|
url: url,
|
||||||
|
requestBody: nil,
|
||||||
|
contentType: "",
|
||||||
|
output: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.sendRequest(params); err != nil {
|
||||||
return fmt.Errorf("received an error after sending the request to unfollow the account: %w", err)
|
return fmt.Errorf("received an error after sending the request to unfollow the account: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,11 +133,19 @@ func (g *Client) UnfollowAccount(accountID string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *Client) GetFollowers(accountID string, limit int) (model.AccountList, error) {
|
func (g *Client) GetFollowers(accountID string, limit int) (model.AccountList, error) {
|
||||||
url := g.Authentication.Instance + fmt.Sprintf("/api/v1/accounts/%s/followers?limit=%d", accountID, limit)
|
url := g.Authentication.Instance + fmt.Sprintf("%s/%s/followers?limit=%d", baseAccountsPath, accountID, limit)
|
||||||
|
|
||||||
accounts := make([]model.Account, limit)
|
accounts := make([]model.Account, limit)
|
||||||
|
|
||||||
if err := g.sendRequest(http.MethodGet, url, nil, &accounts); err != nil {
|
params := requestParameters{
|
||||||
|
httpMethod: http.MethodGet,
|
||||||
|
url: url,
|
||||||
|
requestBody: nil,
|
||||||
|
contentType: "",
|
||||||
|
output: &accounts,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.sendRequest(params); err != nil {
|
||||||
return model.AccountList{}, fmt.Errorf("received an error after sending the request to get the list of followers: %w", err)
|
return model.AccountList{}, fmt.Errorf("received an error after sending the request to get the list of followers: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -106,11 +158,19 @@ func (g *Client) GetFollowers(accountID string, limit int) (model.AccountList, e
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *Client) GetFollowing(accountID string, limit int) (model.AccountList, error) {
|
func (g *Client) GetFollowing(accountID string, limit int) (model.AccountList, error) {
|
||||||
url := g.Authentication.Instance + fmt.Sprintf("/api/v1/accounts/%s/following?limit=%d", accountID, limit)
|
url := g.Authentication.Instance + fmt.Sprintf("%s/%s/following?limit=%d", baseAccountsPath, accountID, limit)
|
||||||
|
|
||||||
accounts := make([]model.Account, limit)
|
accounts := make([]model.Account, limit)
|
||||||
|
|
||||||
if err := g.sendRequest(http.MethodGet, url, nil, &accounts); err != nil {
|
params := requestParameters{
|
||||||
|
httpMethod: http.MethodGet,
|
||||||
|
url: url,
|
||||||
|
requestBody: nil,
|
||||||
|
contentType: "",
|
||||||
|
output: &accounts,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.sendRequest(params); err != nil {
|
||||||
return model.AccountList{}, fmt.Errorf("received an error after sending the request to get the list of followed accounts: %w", err)
|
return model.AccountList{}, fmt.Errorf("received an error after sending the request to get the list of followed accounts: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -123,9 +183,17 @@ func (g *Client) GetFollowing(accountID string, limit int) (model.AccountList, e
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *Client) BlockAccount(accountID string) error {
|
func (g *Client) BlockAccount(accountID string) error {
|
||||||
url := g.Authentication.Instance + fmt.Sprintf("/api/v1/accounts/%s/block", accountID)
|
url := g.Authentication.Instance + baseAccountsPath + "/" + accountID + "/block"
|
||||||
|
|
||||||
if err := g.sendRequest(http.MethodPost, url, nil, nil); err != nil {
|
params := requestParameters{
|
||||||
|
httpMethod: http.MethodPost,
|
||||||
|
url: url,
|
||||||
|
requestBody: nil,
|
||||||
|
contentType: "",
|
||||||
|
output: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.sendRequest(params); err != nil {
|
||||||
return fmt.Errorf("received an error after sending the request to block the account: %w", err)
|
return fmt.Errorf("received an error after sending the request to block the account: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -133,9 +201,17 @@ func (g *Client) BlockAccount(accountID string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *Client) UnblockAccount(accountID string) error {
|
func (g *Client) UnblockAccount(accountID string) error {
|
||||||
url := g.Authentication.Instance + fmt.Sprintf("/api/v1/accounts/%s/unblock", accountID)
|
url := g.Authentication.Instance + baseAccountsPath + "/" + accountID + "/unblock"
|
||||||
|
|
||||||
if err := g.sendRequest(http.MethodPost, url, nil, nil); err != nil {
|
params := requestParameters{
|
||||||
|
httpMethod: http.MethodPost,
|
||||||
|
url: url,
|
||||||
|
requestBody: nil,
|
||||||
|
contentType: "",
|
||||||
|
output: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.sendRequest(params); err != nil {
|
||||||
return fmt.Errorf("received an error after sending the request to unblock the account: %w", err)
|
return fmt.Errorf("received an error after sending the request to unblock the account: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -145,9 +221,17 @@ func (g *Client) UnblockAccount(accountID string) error {
|
||||||
func (g *Client) GetBlockedAccounts(limit int) (model.AccountList, error) {
|
func (g *Client) GetBlockedAccounts(limit int) (model.AccountList, error) {
|
||||||
url := g.Authentication.Instance + fmt.Sprintf("/api/v1/blocks?limit=%d", limit)
|
url := g.Authentication.Instance + fmt.Sprintf("/api/v1/blocks?limit=%d", limit)
|
||||||
|
|
||||||
accounts := make([]model.Account, limit)
|
var accounts []model.Account
|
||||||
|
|
||||||
if err := g.sendRequest(http.MethodGet, url, nil, &accounts); err != nil {
|
params := requestParameters{
|
||||||
|
httpMethod: http.MethodGet,
|
||||||
|
url: url,
|
||||||
|
requestBody: nil,
|
||||||
|
contentType: "",
|
||||||
|
output: &accounts,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.sendRequest(params); err != nil {
|
||||||
return model.AccountList{}, fmt.Errorf("received an error after sending the request to get the list of blocked accounts: %w", err)
|
return model.AccountList{}, fmt.Errorf("received an error after sending the request to get the list of blocked accounts: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -172,11 +256,197 @@ func (g *Client) SetPrivateNote(accountID, note string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
requestBody := bytes.NewBuffer(data)
|
requestBody := bytes.NewBuffer(data)
|
||||||
url := g.Authentication.Instance + fmt.Sprintf("/api/v1/accounts/%s/note", accountID)
|
url := g.Authentication.Instance + baseAccountsPath + "/" + accountID + "/note"
|
||||||
|
|
||||||
if err := g.sendRequest(http.MethodPost, url, requestBody, nil); err != nil {
|
params := requestParameters{
|
||||||
|
httpMethod: http.MethodPost,
|
||||||
|
url: url,
|
||||||
|
requestBody: requestBody,
|
||||||
|
contentType: applicationJSON,
|
||||||
|
output: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.sendRequest(params); err != nil {
|
||||||
return fmt.Errorf("received an error after sending the request to set the private note: %w", err)
|
return fmt.Errorf("received an error after sending the request to set the private note: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (g *Client) GetFollowRequests(limit int) (model.AccountList, error) {
|
||||||
|
url := g.Authentication.Instance + fmt.Sprintf("%s?limit=%d", baseFollowRequestsPath, limit)
|
||||||
|
|
||||||
|
var accounts []model.Account
|
||||||
|
|
||||||
|
params := requestParameters{
|
||||||
|
httpMethod: http.MethodGet,
|
||||||
|
url: url,
|
||||||
|
requestBody: nil,
|
||||||
|
contentType: "",
|
||||||
|
output: &accounts,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.sendRequest(params); err != nil {
|
||||||
|
return model.AccountList{}, fmt.Errorf("received an error after sending the request to get the list of follow requests: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
requests := model.AccountList{
|
||||||
|
Type: model.AccountListFollowRequests,
|
||||||
|
Accounts: accounts,
|
||||||
|
}
|
||||||
|
|
||||||
|
return requests, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Client) AcceptFollowRequest(accountID string) error {
|
||||||
|
url := g.Authentication.Instance + baseFollowRequestsPath + "/" + accountID + "/authorize"
|
||||||
|
|
||||||
|
params := requestParameters{
|
||||||
|
httpMethod: http.MethodPost,
|
||||||
|
url: url,
|
||||||
|
requestBody: nil,
|
||||||
|
contentType: "",
|
||||||
|
output: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.sendRequest(params); err != nil {
|
||||||
|
return fmt.Errorf("received an error after sending the request to accept the follow request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Client) RejectFollowRequest(accountID string) error {
|
||||||
|
url := g.Authentication.Instance + baseFollowRequestsPath + "/" + accountID + "/reject"
|
||||||
|
|
||||||
|
params := requestParameters{
|
||||||
|
httpMethod: http.MethodPost,
|
||||||
|
url: url,
|
||||||
|
requestBody: nil,
|
||||||
|
contentType: "",
|
||||||
|
output: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.sendRequest(params); err != nil {
|
||||||
|
return fmt.Errorf("received an error after sending the request to reject the follow request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Client) GetMutedAccounts(limit int) (model.AccountList, error) {
|
||||||
|
url := g.Authentication.Instance + fmt.Sprintf("/api/v1/mutes?limit=%d", limit)
|
||||||
|
|
||||||
|
var accounts []model.Account
|
||||||
|
|
||||||
|
params := requestParameters{
|
||||||
|
httpMethod: http.MethodGet,
|
||||||
|
url: url,
|
||||||
|
requestBody: nil,
|
||||||
|
contentType: "",
|
||||||
|
output: &accounts,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.sendRequest(params); err != nil {
|
||||||
|
return model.AccountList{}, fmt.Errorf("received an error after sending the request to get the list of muted accounts: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
muted := model.AccountList{
|
||||||
|
Type: model.AccountListMuted,
|
||||||
|
Accounts: accounts,
|
||||||
|
}
|
||||||
|
|
||||||
|
return muted, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type MuteAccountForm struct {
|
||||||
|
Notifications bool `json:"notifications"`
|
||||||
|
Duration int `json:"duration"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Client) MuteAccount(accountID string, form MuteAccountForm) error {
|
||||||
|
data, err := json.Marshal(form)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to marshal the form: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
requestBody := bytes.NewBuffer(data)
|
||||||
|
url := g.Authentication.Instance + baseAccountsPath + "/" + accountID + "/mute"
|
||||||
|
|
||||||
|
params := requestParameters{
|
||||||
|
httpMethod: http.MethodPost,
|
||||||
|
url: url,
|
||||||
|
requestBody: requestBody,
|
||||||
|
contentType: applicationJSON,
|
||||||
|
output: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.sendRequest(params); err != nil {
|
||||||
|
return fmt.Errorf("received an error after sending the request to mute the account: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Client) UnmuteAccount(accountID string) error {
|
||||||
|
url := g.Authentication.Instance + baseAccountsPath + "/" + accountID + "/unmute"
|
||||||
|
|
||||||
|
params := requestParameters{
|
||||||
|
httpMethod: http.MethodPost,
|
||||||
|
url: url,
|
||||||
|
requestBody: nil,
|
||||||
|
contentType: "",
|
||||||
|
output: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.sendRequest(params); err != nil {
|
||||||
|
return fmt.Errorf("received an error after sending the request to unmute the account: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetAccountStatusesForm struct {
|
||||||
|
AccountID string
|
||||||
|
Limit int
|
||||||
|
ExcludeReplies bool
|
||||||
|
ExcludeReblogs bool
|
||||||
|
Pinned bool
|
||||||
|
OnlyMedia bool
|
||||||
|
OnlyPublic bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Client) GetAccountStatuses(form GetAccountStatusesForm) (*model.StatusList, error) {
|
||||||
|
path := baseAccountsPath + "/" + form.AccountID + "/statuses"
|
||||||
|
query := fmt.Sprintf(
|
||||||
|
"?limit=%d&exclude_replies=%t&exclude_reblogs=%t&pinned=%t&only_media=%t&only_public=%t",
|
||||||
|
form.Limit,
|
||||||
|
form.ExcludeReplies,
|
||||||
|
form.ExcludeReblogs,
|
||||||
|
form.Pinned,
|
||||||
|
form.OnlyMedia,
|
||||||
|
form.OnlyPublic,
|
||||||
|
)
|
||||||
|
url := g.Authentication.Instance + path + query
|
||||||
|
|
||||||
|
var statuses []model.Status
|
||||||
|
|
||||||
|
params := requestParameters{
|
||||||
|
httpMethod: http.MethodGet,
|
||||||
|
url: url,
|
||||||
|
requestBody: nil,
|
||||||
|
contentType: "",
|
||||||
|
output: &statuses,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.sendRequest(params); err != nil {
|
||||||
|
return nil, fmt.Errorf("received an error after sending the request to get the account's statuses: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
statusList := model.StatusList{
|
||||||
|
Name: "STATUSES:",
|
||||||
|
Statuses: statuses,
|
||||||
|
}
|
||||||
|
|
||||||
|
return &statusList, nil
|
||||||
|
}
|
||||||
|
|
|
@ -1,7 +1,3 @@
|
||||||
// SPDX-FileCopyrightText: 2024 Dan Anglin <d.n.i.anglin@gmail.com>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -11,12 +7,18 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"codeflow.dananglin.me.uk/apollo/enbas/internal"
|
|
||||||
"codeflow.dananglin.me.uk/apollo/enbas/internal/config"
|
"codeflow.dananglin.me.uk/apollo/enbas/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
applicationJSON string = "application/json; charset=utf-8"
|
||||||
|
redirectURI string = "urn:ietf:wg:oauth:2.0:oob"
|
||||||
|
userAgent string = "Enbas/0.2.0"
|
||||||
|
)
|
||||||
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
Authentication config.Credentials
|
Authentication config.Credentials
|
||||||
HTTPClient http.Client
|
HTTPClient http.Client
|
||||||
|
@ -24,8 +26,8 @@ type Client struct {
|
||||||
Timeout time.Duration
|
Timeout time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewClientFromConfig(configDir string) (*Client, error) {
|
func NewClientFromFile(path string) (*Client, error) {
|
||||||
config, err := config.NewCredentialsConfigFromFile(configDir)
|
config, err := config.NewCredentialsConfigFromFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("unable to get the authentication configuration: %w", err)
|
return nil, fmt.Errorf("unable to get the authentication configuration: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -41,7 +43,7 @@ func NewClient(authentication config.Credentials) *Client {
|
||||||
gtsClient := Client{
|
gtsClient := Client{
|
||||||
Authentication: authentication,
|
Authentication: authentication,
|
||||||
HTTPClient: httpClient,
|
HTTPClient: httpClient,
|
||||||
UserAgent: internal.UserAgent,
|
UserAgent: userAgent,
|
||||||
Timeout: 5 * time.Second,
|
Timeout: 5 * time.Second,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,7 +52,7 @@ func NewClient(authentication config.Credentials) *Client {
|
||||||
|
|
||||||
func (g *Client) AuthCodeURL() string {
|
func (g *Client) AuthCodeURL() string {
|
||||||
format := "%s/oauth/authorize?client_id=%s&redirect_uri=%s&response_type=code"
|
format := "%s/oauth/authorize?client_id=%s&redirect_uri=%s&response_type=code"
|
||||||
escapedRedirectURI := url.QueryEscape(internal.RedirectUri)
|
escapedRedirectURI := url.QueryEscape(redirectURI)
|
||||||
|
|
||||||
return fmt.Sprintf(
|
return fmt.Sprintf(
|
||||||
format,
|
format,
|
||||||
|
@ -60,17 +62,64 @@ func (g *Client) AuthCodeURL() string {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *Client) sendRequest(method string, url string, requestBody io.Reader, object any) error {
|
func (g *Client) DownloadMedia(url, path string) error {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to create the HTTP request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
request.Header.Set("User-Agent", g.UserAgent)
|
||||||
|
|
||||||
|
response, err := g.HTTPClient.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("received an error after attempting the download: %w", err)
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
|
||||||
|
if response.StatusCode != http.StatusOK {
|
||||||
|
return BadStatusCodeError{
|
||||||
|
statusCode: response.StatusCode,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to create %s: %w", path, err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
if _, err = io.Copy(file, response.Body); err != nil {
|
||||||
|
return fmt.Errorf("unable to save the download to %s: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type requestParameters struct {
|
||||||
|
httpMethod string
|
||||||
|
url string
|
||||||
|
contentType string
|
||||||
|
requestBody io.Reader
|
||||||
|
output any
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Client) sendRequest(params requestParameters) error {
|
||||||
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, method, url, requestBody)
|
request, err := http.NewRequestWithContext(ctx, params.httpMethod, params.url, params.requestBody)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to create the HTTP request, %w", err)
|
return fmt.Errorf("unable to create the HTTP request: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
request.Header.Set("Content-Type", "application/json; charset=utf-8")
|
if params.contentType != "" {
|
||||||
request.Header.Set("Accept", "application/json; charset=utf-8")
|
request.Header.Set("Content-Type", params.contentType)
|
||||||
|
}
|
||||||
|
|
||||||
|
request.Header.Set("Accept", applicationJSON)
|
||||||
request.Header.Set("User-Agent", g.UserAgent)
|
request.Header.Set("User-Agent", g.UserAgent)
|
||||||
|
|
||||||
if len(g.Authentication.AccessToken) > 0 {
|
if len(g.Authentication.AccessToken) > 0 {
|
||||||
|
@ -85,17 +134,32 @@ func (g *Client) sendRequest(method string, url string, requestBody io.Reader, o
|
||||||
defer response.Body.Close()
|
defer response.Body.Close()
|
||||||
|
|
||||||
if response.StatusCode < http.StatusOK || response.StatusCode >= http.StatusBadRequest {
|
if response.StatusCode < http.StatusOK || response.StatusCode >= http.StatusBadRequest {
|
||||||
return fmt.Errorf(
|
message := struct {
|
||||||
"did not receive an OK response from the GoToSocial server: got %d",
|
Error string `json:"error"`
|
||||||
response.StatusCode,
|
}{
|
||||||
)
|
Error: "",
|
||||||
}
|
}
|
||||||
|
|
||||||
if object == nil {
|
if err := json.NewDecoder(response.Body).Decode(&message); err != nil {
|
||||||
|
return ResponseError{
|
||||||
|
StatusCode: response.StatusCode,
|
||||||
|
Message: "",
|
||||||
|
MessageDecodeErr: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResponseError{
|
||||||
|
StatusCode: response.StatusCode,
|
||||||
|
Message: message.Error,
|
||||||
|
MessageDecodeErr: nil,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if params.output == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := json.NewDecoder(response.Body).Decode(object); err != nil {
|
if err := json.NewDecoder(response.Body).Decode(params.output); err != nil {
|
||||||
return fmt.Errorf(
|
return fmt.Errorf(
|
||||||
"unable to decode the response from the GoToSocial server: %w",
|
"unable to decode the response from the GoToSocial server: %w",
|
||||||
err,
|
err,
|
||||||
|
|
51
internal/client/errors.go
Normal file
51
internal/client/errors.go
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
package client
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
type ResponseError struct {
|
||||||
|
StatusCode int
|
||||||
|
Message string
|
||||||
|
MessageDecodeErr error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ResponseError) Error() string {
|
||||||
|
if e.MessageDecodeErr != nil {
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"received HTTP code %d from the instance but was unable to decode the error message: %v",
|
||||||
|
e.StatusCode,
|
||||||
|
e.MessageDecodeErr,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Message == "" {
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"received HTTP code %d from the instance but no error message was provided",
|
||||||
|
e.StatusCode,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"message received from the instance: (%d) %q",
|
||||||
|
e.StatusCode,
|
||||||
|
e.Message,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type BadStatusCodeError struct {
|
||||||
|
statusCode int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e BadStatusCodeError) Error() string {
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"did not receive an OK response from the GoToSocial server: got %d",
|
||||||
|
e.statusCode,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Error struct {
|
||||||
|
message string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e Error) Error() string {
|
||||||
|
return e.message
|
||||||
|
}
|
|
@ -1,7 +1,3 @@
|
||||||
// SPDX-FileCopyrightText: 2024 Dan Anglin <d.n.i.anglin@gmail.com>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -17,7 +13,15 @@ func (g *Client) GetInstance() (model.InstanceV2, error) {
|
||||||
|
|
||||||
var instance model.InstanceV2
|
var instance model.InstanceV2
|
||||||
|
|
||||||
if err := g.sendRequest(http.MethodGet, url, nil, &instance); err != nil {
|
params := requestParameters{
|
||||||
|
httpMethod: http.MethodGet,
|
||||||
|
url: url,
|
||||||
|
requestBody: nil,
|
||||||
|
contentType: "",
|
||||||
|
output: &instance,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.sendRequest(params); err != nil {
|
||||||
return model.InstanceV2{}, fmt.Errorf("received an error after sending the request to get the instance details: %w", err)
|
return model.InstanceV2{}, fmt.Errorf("received an error after sending the request to get the instance details: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,3 @@
|
||||||
// SPDX-FileCopyrightText: 2024 Dan Anglin <d.n.i.anglin@gmail.com>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -14,15 +10,23 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
listPath string = "/api/v1/lists"
|
baseListPath string = "/api/v1/lists"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (g *Client) GetAllLists() (model.Lists, error) {
|
func (g *Client) GetAllLists() ([]model.List, error) {
|
||||||
url := g.Authentication.Instance + listPath
|
url := g.Authentication.Instance + baseListPath
|
||||||
|
|
||||||
var lists []model.List
|
var lists []model.List
|
||||||
|
|
||||||
if err := g.sendRequest(http.MethodGet, url, nil, &lists); err != nil {
|
params := requestParameters{
|
||||||
|
httpMethod: http.MethodGet,
|
||||||
|
url: url,
|
||||||
|
requestBody: nil,
|
||||||
|
contentType: "",
|
||||||
|
output: &lists,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.sendRequest(params); err != nil {
|
||||||
return nil, fmt.Errorf(
|
return nil, fmt.Errorf(
|
||||||
"received an error after sending the request to get the list of lists: %w",
|
"received an error after sending the request to get the list of lists: %w",
|
||||||
err,
|
err,
|
||||||
|
@ -33,11 +37,19 @@ func (g *Client) GetAllLists() (model.Lists, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *Client) GetList(listID string) (model.List, error) {
|
func (g *Client) GetList(listID string) (model.List, error) {
|
||||||
url := g.Authentication.Instance + listPath + "/" + listID
|
url := g.Authentication.Instance + baseListPath + "/" + listID
|
||||||
|
|
||||||
var list model.List
|
var list model.List
|
||||||
|
|
||||||
if err := g.sendRequest(http.MethodGet, url, nil, &list); err != nil {
|
params := requestParameters{
|
||||||
|
httpMethod: http.MethodGet,
|
||||||
|
url: url,
|
||||||
|
requestBody: nil,
|
||||||
|
contentType: "",
|
||||||
|
output: &list,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.sendRequest(params); err != nil {
|
||||||
return model.List{}, fmt.Errorf(
|
return model.List{}, fmt.Errorf(
|
||||||
"received an error after sending the request to get the list: %w",
|
"received an error after sending the request to get the list: %w",
|
||||||
err,
|
err,
|
||||||
|
@ -59,11 +71,19 @@ func (g *Client) CreateList(form CreateListForm) (model.List, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
requestBody := bytes.NewBuffer(data)
|
requestBody := bytes.NewBuffer(data)
|
||||||
url := g.Authentication.Instance + "/api/v1/lists"
|
url := g.Authentication.Instance + baseListPath
|
||||||
|
|
||||||
var list model.List
|
var list model.List
|
||||||
|
|
||||||
if err := g.sendRequest(http.MethodPost, url, requestBody, &list); err != nil {
|
params := requestParameters{
|
||||||
|
httpMethod: http.MethodPost,
|
||||||
|
url: url,
|
||||||
|
requestBody: requestBody,
|
||||||
|
contentType: applicationJSON,
|
||||||
|
output: &list,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.sendRequest(params); err != nil {
|
||||||
return model.List{}, fmt.Errorf(
|
return model.List{}, fmt.Errorf(
|
||||||
"received an error after sending the request to create the list: %w",
|
"received an error after sending the request to create the list: %w",
|
||||||
err,
|
err,
|
||||||
|
@ -88,11 +108,19 @@ func (g *Client) UpdateList(listToUpdate model.List) (model.List, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
requestBody := bytes.NewBuffer(data)
|
requestBody := bytes.NewBuffer(data)
|
||||||
url := g.Authentication.Instance + listPath + "/" + listToUpdate.ID
|
url := g.Authentication.Instance + baseListPath + "/" + listToUpdate.ID
|
||||||
|
|
||||||
var updatedList model.List
|
var updatedList model.List
|
||||||
|
|
||||||
if err := g.sendRequest(http.MethodPut, url, requestBody, &updatedList); err != nil {
|
params := requestParameters{
|
||||||
|
httpMethod: http.MethodPut,
|
||||||
|
url: url,
|
||||||
|
requestBody: requestBody,
|
||||||
|
contentType: applicationJSON,
|
||||||
|
output: &updatedList,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.sendRequest(params); err != nil {
|
||||||
return model.List{}, fmt.Errorf(
|
return model.List{}, fmt.Errorf(
|
||||||
"received an error after sending the request to update the list: %w",
|
"received an error after sending the request to update the list: %w",
|
||||||
err,
|
err,
|
||||||
|
@ -103,9 +131,24 @@ func (g *Client) UpdateList(listToUpdate model.List) (model.List, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *Client) DeleteList(listID string) error {
|
func (g *Client) DeleteList(listID string) error {
|
||||||
url := g.Authentication.Instance + "/api/v1/lists/" + listID
|
url := g.Authentication.Instance + baseListPath + "/" + listID
|
||||||
|
|
||||||
return g.sendRequest(http.MethodDelete, url, nil, nil)
|
params := requestParameters{
|
||||||
|
httpMethod: http.MethodDelete,
|
||||||
|
url: url,
|
||||||
|
requestBody: nil,
|
||||||
|
contentType: "",
|
||||||
|
output: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.sendRequest(params); err != nil {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"received an error after sending the request to delete the list: %w",
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *Client) AddAccountsToList(listID string, accountIDs []string) error {
|
func (g *Client) AddAccountsToList(listID string, accountIDs []string) error {
|
||||||
|
@ -121,9 +164,17 @@ func (g *Client) AddAccountsToList(listID string, accountIDs []string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
requestBody := bytes.NewBuffer(data)
|
requestBody := bytes.NewBuffer(data)
|
||||||
url := g.Authentication.Instance + listPath + "/" + listID + "/accounts"
|
url := g.Authentication.Instance + baseListPath + "/" + listID + "/accounts"
|
||||||
|
|
||||||
if err := g.sendRequest(http.MethodPost, url, requestBody, nil); err != nil {
|
params := requestParameters{
|
||||||
|
httpMethod: http.MethodPost,
|
||||||
|
url: url,
|
||||||
|
requestBody: requestBody,
|
||||||
|
contentType: applicationJSON,
|
||||||
|
output: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.sendRequest(params); err != nil {
|
||||||
return fmt.Errorf(
|
return fmt.Errorf(
|
||||||
"received an error after sending the request to add the accounts to the list: %w",
|
"received an error after sending the request to add the accounts to the list: %w",
|
||||||
err,
|
err,
|
||||||
|
@ -146,9 +197,17 @@ func (g *Client) RemoveAccountsFromList(listID string, accountIDs []string) erro
|
||||||
}
|
}
|
||||||
|
|
||||||
requestBody := bytes.NewBuffer(data)
|
requestBody := bytes.NewBuffer(data)
|
||||||
url := g.Authentication.Instance + listPath + "/" + listID + "/accounts"
|
url := g.Authentication.Instance + baseListPath + "/" + listID + "/accounts"
|
||||||
|
|
||||||
if err := g.sendRequest(http.MethodDelete, url, requestBody, nil); err != nil {
|
params := requestParameters{
|
||||||
|
httpMethod: http.MethodDelete,
|
||||||
|
url: url,
|
||||||
|
requestBody: requestBody,
|
||||||
|
contentType: applicationJSON,
|
||||||
|
output: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.sendRequest(params); err != nil {
|
||||||
return fmt.Errorf(
|
return fmt.Errorf(
|
||||||
"received an error after sending the request to remove the accounts from the list: %w",
|
"received an error after sending the request to remove the accounts from the list: %w",
|
||||||
err,
|
err,
|
||||||
|
@ -159,12 +218,20 @@ func (g *Client) RemoveAccountsFromList(listID string, accountIDs []string) erro
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *Client) GetAccountsFromList(listID string, limit int) ([]model.Account, error) {
|
func (g *Client) GetAccountsFromList(listID string, limit int) ([]model.Account, error) {
|
||||||
path := fmt.Sprintf("%s/%s/accounts?limit=%d", listPath, listID, limit)
|
path := fmt.Sprintf("%s/%s/accounts?limit=%d", baseListPath, listID, limit)
|
||||||
url := g.Authentication.Instance + path
|
url := g.Authentication.Instance + path
|
||||||
|
|
||||||
var accounts []model.Account
|
var accounts []model.Account
|
||||||
|
|
||||||
if err := g.sendRequest(http.MethodGet, url, nil, &accounts); err != nil {
|
params := requestParameters{
|
||||||
|
httpMethod: http.MethodGet,
|
||||||
|
url: url,
|
||||||
|
requestBody: nil,
|
||||||
|
contentType: "",
|
||||||
|
output: &accounts,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.sendRequest(params); err != nil {
|
||||||
return nil, fmt.Errorf(
|
return nil, fmt.Errorf(
|
||||||
"received an error after sending the request to get the accounts from the list: %w",
|
"received an error after sending the request to get the accounts from the list: %w",
|
||||||
err,
|
err,
|
||||||
|
|
159
internal/client/media.go
Normal file
159
internal/client/media.go
Normal file
|
@ -0,0 +1,159 @@
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"mime/multipart"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"codeflow.dananglin.me.uk/apollo/enbas/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
baseMediaPath string = "/api/v1/media"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (g *Client) GetMediaAttachment(mediaAttachmentID string) (model.Attachment, error) {
|
||||||
|
url := g.Authentication.Instance + baseMediaPath + "/" + mediaAttachmentID
|
||||||
|
|
||||||
|
var attachment model.Attachment
|
||||||
|
|
||||||
|
params := requestParameters{
|
||||||
|
httpMethod: http.MethodGet,
|
||||||
|
url: url,
|
||||||
|
requestBody: nil,
|
||||||
|
contentType: "",
|
||||||
|
output: &attachment,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.sendRequest(params); err != nil {
|
||||||
|
return model.Attachment{}, fmt.Errorf("received an error after sending the request to get the media attachment: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return attachment, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Client) CreateMediaAttachment(path, description, focus string) (model.Attachment, error) {
|
||||||
|
file, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return model.Attachment{}, fmt.Errorf("unable to open the file: %w", err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
// create the request body using a writer from the multipart package
|
||||||
|
requestBody := bytes.Buffer{}
|
||||||
|
requestBodyWriter := multipart.NewWriter(&requestBody)
|
||||||
|
|
||||||
|
filename := filepath.Base(path)
|
||||||
|
|
||||||
|
part, err := requestBodyWriter.CreateFormFile("file", filename)
|
||||||
|
if err != nil {
|
||||||
|
return model.Attachment{}, fmt.Errorf("unable to create the new part: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := io.Copy(part, file); err != nil {
|
||||||
|
return model.Attachment{}, fmt.Errorf("unable to copy the file contents to the form: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// add the description
|
||||||
|
if description != "" {
|
||||||
|
descriptionFormFieldWriter, err := requestBodyWriter.CreateFormField("description")
|
||||||
|
if err != nil {
|
||||||
|
return model.Attachment{}, fmt.Errorf(
|
||||||
|
"unable to create the writer for the 'description' form field: %w",
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := io.WriteString(descriptionFormFieldWriter, description); err != nil {
|
||||||
|
return model.Attachment{}, fmt.Errorf(
|
||||||
|
"unable to write the description to the form: %w",
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add the focus values
|
||||||
|
if focus != "" {
|
||||||
|
focusFormFieldWriter, err := requestBodyWriter.CreateFormField("focus")
|
||||||
|
if err != nil {
|
||||||
|
return model.Attachment{}, fmt.Errorf(
|
||||||
|
"unable to create the writer for the 'focus' form field: %w",
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := io.WriteString(focusFormFieldWriter, focus); err != nil {
|
||||||
|
return model.Attachment{}, fmt.Errorf(
|
||||||
|
"unable to write the focus values to the form: %w",
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := requestBodyWriter.Close(); err != nil {
|
||||||
|
return model.Attachment{}, fmt.Errorf("unable to close the writer: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
url := g.Authentication.Instance + baseMediaPath
|
||||||
|
|
||||||
|
var attachment model.Attachment
|
||||||
|
|
||||||
|
params := requestParameters{
|
||||||
|
httpMethod: http.MethodPost,
|
||||||
|
url: url,
|
||||||
|
requestBody: &requestBody,
|
||||||
|
contentType: requestBodyWriter.FormDataContentType(),
|
||||||
|
output: &attachment,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.sendRequest(params); err != nil {
|
||||||
|
return model.Attachment{}, fmt.Errorf(
|
||||||
|
"received an error after sending the request to create the media attachment: %w",
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return attachment, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Client) UpdateMediaAttachment(mediaAttachmentID, description, focus string) (model.Attachment, error) {
|
||||||
|
form := struct {
|
||||||
|
Description string `json:"description"`
|
||||||
|
Focus string `json:"focus"`
|
||||||
|
}{
|
||||||
|
Description: description,
|
||||||
|
Focus: focus,
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(form)
|
||||||
|
if err != nil {
|
||||||
|
return model.Attachment{}, fmt.Errorf("unable to marshal the form: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
requestBody := bytes.NewBuffer(data)
|
||||||
|
url := g.Authentication.Instance + baseMediaPath + "/" + mediaAttachmentID
|
||||||
|
|
||||||
|
var updatedMediaAttachment model.Attachment
|
||||||
|
|
||||||
|
params := requestParameters{
|
||||||
|
httpMethod: http.MethodPut,
|
||||||
|
url: url,
|
||||||
|
requestBody: requestBody,
|
||||||
|
contentType: applicationJSON,
|
||||||
|
output: &updatedMediaAttachment,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.sendRequest(params); err != nil {
|
||||||
|
return model.Attachment{}, fmt.Errorf(
|
||||||
|
"received an error after sending the request to update the media attachment: %w",
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedMediaAttachment, nil
|
||||||
|
}
|
|
@ -1,33 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2024 Dan Anglin <d.n.i.anglin@gmail.com>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"codeflow.dananglin.me.uk/apollo/enbas/internal/model"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (g *Client) GetNotifications(limit int) ([]model.Notification, error) {
|
|
||||||
path := fmt.Sprintf("/api/v1/notifications?limit=%d", limit)
|
|
||||||
url := g.Authentication.Instance + path
|
|
||||||
|
|
||||||
var notifications []model.Notification
|
|
||||||
|
|
||||||
if err := g.sendRequest(http.MethodGet, url, nil, ¬ifications); err != nil {
|
|
||||||
return nil, fmt.Errorf("received an error after sending the request to get your notifications: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return notifications, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *Client) GetNotification() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *Client) DeleteAllNotifications() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
65
internal/client/poll.go
Normal file
65
internal/client/poll.go
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
pollPath string = "/api/v1/polls"
|
||||||
|
)
|
||||||
|
|
||||||
|
// func (g *Client) GetPoll(pollID string) (model.Poll, error) {
|
||||||
|
// url := g.Authentication.Instance + pollPath + "/" + pollID
|
||||||
|
//
|
||||||
|
// var poll model.Poll
|
||||||
|
//
|
||||||
|
// params := requestParameters{
|
||||||
|
// httpMethod: http.MethodGet,
|
||||||
|
// url: url,
|
||||||
|
// requestBody: nil,
|
||||||
|
// contentType: "",
|
||||||
|
// output: &poll,
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// if err := g.sendRequest(params); err != nil {
|
||||||
|
// return model.Poll{}, fmt.Errorf(
|
||||||
|
// "received an error after sending the request to get the poll: %w",
|
||||||
|
// err,
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// return poll, nil
|
||||||
|
// }
|
||||||
|
|
||||||
|
func (g *Client) VoteInPoll(pollID string, choices []int) error {
|
||||||
|
form := struct {
|
||||||
|
Choices []int `json:"choices"`
|
||||||
|
}{
|
||||||
|
Choices: choices,
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(form)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to encode the JSON form: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
requestBody := bytes.NewBuffer(data)
|
||||||
|
url := g.Authentication.Instance + pollPath + "/" + pollID + "/votes"
|
||||||
|
|
||||||
|
params := requestParameters{
|
||||||
|
httpMethod: http.MethodPost,
|
||||||
|
url: url,
|
||||||
|
requestBody: requestBody,
|
||||||
|
contentType: applicationJSON,
|
||||||
|
output: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.sendRequest(params); err != nil {
|
||||||
|
return fmt.Errorf("received an error after sending the request to vote in the poll: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -1,7 +1,3 @@
|
||||||
// SPDX-FileCopyrightText: 2024 Dan Anglin <d.n.i.anglin@gmail.com>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -11,14 +7,25 @@ import (
|
||||||
"codeflow.dananglin.me.uk/apollo/enbas/internal/model"
|
"codeflow.dananglin.me.uk/apollo/enbas/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (g *Client) GetUserPreferences() (model.Preferences, error) {
|
func (g *Client) GetUserPreferences() (*model.Preferences, error) {
|
||||||
url := g.Authentication.Instance + "/api/v1/preferences"
|
url := g.Authentication.Instance + "/api/v1/preferences"
|
||||||
|
|
||||||
var preferences model.Preferences
|
var preferences model.Preferences
|
||||||
|
|
||||||
if err := g.sendRequest(http.MethodGet, url, nil, &preferences); err != nil {
|
params := requestParameters{
|
||||||
return model.Preferences{}, fmt.Errorf("received an error after sending the request to get the user preferences: %w", err)
|
httpMethod: http.MethodGet,
|
||||||
|
url: url,
|
||||||
|
requestBody: nil,
|
||||||
|
contentType: "",
|
||||||
|
output: &preferences,
|
||||||
}
|
}
|
||||||
|
|
||||||
return preferences, nil
|
if err := g.sendRequest(params); err != nil {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"received an error after sending the request to get the user preferences: %w",
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &preferences, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,3 @@
|
||||||
// SPDX-FileCopyrightText: 2024 Dan Anglin <d.n.i.anglin@gmail.com>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -10,7 +6,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"codeflow.dananglin.me.uk/apollo/enbas/internal"
|
"codeflow.dananglin.me.uk/apollo/enbas/internal/info"
|
||||||
"codeflow.dananglin.me.uk/apollo/enbas/internal/model"
|
"codeflow.dananglin.me.uk/apollo/enbas/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -22,14 +18,14 @@ type registerRequest struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *Client) Register() error {
|
func (g *Client) Register() error {
|
||||||
params := registerRequest{
|
registerParams := registerRequest{
|
||||||
ClientName: internal.ApplicationName,
|
ClientName: info.ApplicationName,
|
||||||
RedirectUris: internal.RedirectUri,
|
RedirectUris: redirectURI,
|
||||||
Scopes: "read write",
|
Scopes: "read write",
|
||||||
Website: internal.ApplicationWebsite,
|
Website: info.ApplicationWebsite,
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := json.Marshal(params)
|
data, err := json.Marshal(registerParams)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to marshal the request body: %w", err)
|
return fmt.Errorf("unable to marshal the request body: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -39,7 +35,15 @@ func (g *Client) Register() error {
|
||||||
|
|
||||||
var app model.Application
|
var app model.Application
|
||||||
|
|
||||||
if err := g.sendRequest(http.MethodPost, url, requestBody, &app); err != nil {
|
requestParams := requestParameters{
|
||||||
|
httpMethod: http.MethodPost,
|
||||||
|
url: url,
|
||||||
|
requestBody: requestBody,
|
||||||
|
contentType: applicationJSON,
|
||||||
|
output: &app,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.sendRequest(requestParams); err != nil {
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,3 @@
|
||||||
// SPDX-FileCopyrightText: 2024 Dan Anglin <d.n.i.anglin@gmail.com>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -13,13 +9,25 @@ import (
|
||||||
"codeflow.dananglin.me.uk/apollo/enbas/internal/model"
|
"codeflow.dananglin.me.uk/apollo/enbas/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
baseStatusesPath string = "/api/v1/statuses"
|
||||||
|
)
|
||||||
|
|
||||||
func (g *Client) GetStatus(statusID string) (model.Status, error) {
|
func (g *Client) GetStatus(statusID string) (model.Status, error) {
|
||||||
path := "/api/v1/statuses/" + statusID
|
path := baseStatusesPath + "/" + statusID
|
||||||
url := g.Authentication.Instance + path
|
url := g.Authentication.Instance + path
|
||||||
|
|
||||||
var status model.Status
|
var status model.Status
|
||||||
|
|
||||||
if err := g.sendRequest(http.MethodGet, url, nil, &status); err != nil {
|
params := requestParameters{
|
||||||
|
httpMethod: http.MethodGet,
|
||||||
|
url: url,
|
||||||
|
requestBody: nil,
|
||||||
|
contentType: "",
|
||||||
|
output: &status,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.sendRequest(params); err != nil {
|
||||||
return model.Status{}, fmt.Errorf(
|
return model.Status{}, fmt.Errorf(
|
||||||
"received an error after sending the request to get the status information: %w",
|
"received an error after sending the request to get the status information: %w",
|
||||||
err,
|
err,
|
||||||
|
@ -31,6 +39,7 @@ func (g *Client) GetStatus(statusID string) (model.Status, error) {
|
||||||
|
|
||||||
type CreateStatusForm struct {
|
type CreateStatusForm struct {
|
||||||
Content string `json:"status"`
|
Content string `json:"status"`
|
||||||
|
InReplyTo string `json:"in_reply_to_id"`
|
||||||
Language string `json:"language"`
|
Language string `json:"language"`
|
||||||
SpoilerText string `json:"spoiler_text"`
|
SpoilerText string `json:"spoiler_text"`
|
||||||
Boostable bool `json:"boostable"`
|
Boostable bool `json:"boostable"`
|
||||||
|
@ -38,8 +47,17 @@ type CreateStatusForm struct {
|
||||||
Likeable bool `json:"likeable"`
|
Likeable bool `json:"likeable"`
|
||||||
Replyable bool `json:"replyable"`
|
Replyable bool `json:"replyable"`
|
||||||
Sensitive bool `json:"sensitive"`
|
Sensitive bool `json:"sensitive"`
|
||||||
|
Poll *CreateStatusPollForm `json:"poll,omitempty"`
|
||||||
ContentType model.StatusContentType `json:"content_type"`
|
ContentType model.StatusContentType `json:"content_type"`
|
||||||
Visibility model.StatusVisibility `json:"visibility"`
|
Visibility model.StatusVisibility `json:"visibility"`
|
||||||
|
AttachmentIDs []string `json:"media_ids,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateStatusPollForm struct {
|
||||||
|
Options []string `json:"options"`
|
||||||
|
ExpiresIn int `json:"expires_in"`
|
||||||
|
Multiple bool `json:"multiple"`
|
||||||
|
HideTotals bool `json:"hide_totals"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *Client) CreateStatus(form CreateStatusForm) (model.Status, error) {
|
func (g *Client) CreateStatus(form CreateStatusForm) (model.Status, error) {
|
||||||
|
@ -49,11 +67,19 @@ func (g *Client) CreateStatus(form CreateStatusForm) (model.Status, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
requestBody := bytes.NewBuffer(data)
|
requestBody := bytes.NewBuffer(data)
|
||||||
url := g.Authentication.Instance + "/api/v1/statuses"
|
url := g.Authentication.Instance + baseStatusesPath
|
||||||
|
|
||||||
var status model.Status
|
var status model.Status
|
||||||
|
|
||||||
if err := g.sendRequest(http.MethodPost, url, requestBody, &status); err != nil {
|
params := requestParameters{
|
||||||
|
httpMethod: http.MethodPost,
|
||||||
|
url: url,
|
||||||
|
requestBody: requestBody,
|
||||||
|
contentType: applicationJSON,
|
||||||
|
output: &status,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.sendRequest(params); err != nil {
|
||||||
return model.Status{}, fmt.Errorf(
|
return model.Status{}, fmt.Errorf(
|
||||||
"received an error after sending the request to create the status: %w",
|
"received an error after sending the request to create the status: %w",
|
||||||
err,
|
err,
|
||||||
|
@ -68,12 +94,19 @@ func (g *Client) GetBookmarks(limit int) (model.StatusList, error) {
|
||||||
url := g.Authentication.Instance + path
|
url := g.Authentication.Instance + path
|
||||||
|
|
||||||
bookmarks := model.StatusList{
|
bookmarks := model.StatusList{
|
||||||
Type: model.StatusListBookMarks,
|
Name: "Your Bookmarks",
|
||||||
Name: "BOOKMARKS",
|
|
||||||
Statuses: nil,
|
Statuses: nil,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := g.sendRequest(http.MethodGet, url, nil, &bookmarks.Statuses); err != nil {
|
params := requestParameters{
|
||||||
|
httpMethod: http.MethodGet,
|
||||||
|
url: url,
|
||||||
|
requestBody: nil,
|
||||||
|
contentType: "",
|
||||||
|
output: &bookmarks.Statuses,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.sendRequest(params); err != nil {
|
||||||
return bookmarks, fmt.Errorf(
|
return bookmarks, fmt.Errorf(
|
||||||
"received an error after sending the request to get the bookmarks: %w",
|
"received an error after sending the request to get the bookmarks: %w",
|
||||||
err,
|
err,
|
||||||
|
@ -87,7 +120,15 @@ func (g *Client) AddStatusToBookmarks(statusID string) error {
|
||||||
path := fmt.Sprintf("/api/v1/statuses/%s/bookmark", statusID)
|
path := fmt.Sprintf("/api/v1/statuses/%s/bookmark", statusID)
|
||||||
url := g.Authentication.Instance + path
|
url := g.Authentication.Instance + path
|
||||||
|
|
||||||
if err := g.sendRequest(http.MethodPost, url, nil, nil); err != nil {
|
params := requestParameters{
|
||||||
|
httpMethod: http.MethodPost,
|
||||||
|
url: url,
|
||||||
|
requestBody: nil,
|
||||||
|
contentType: "",
|
||||||
|
output: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.sendRequest(params); err != nil {
|
||||||
return fmt.Errorf(
|
return fmt.Errorf(
|
||||||
"received an error after sending the request to add the status to the list of bookmarks: %w",
|
"received an error after sending the request to add the status to the list of bookmarks: %w",
|
||||||
err,
|
err,
|
||||||
|
@ -101,7 +142,15 @@ func (g *Client) RemoveStatusFromBookmarks(statusID string) error {
|
||||||
path := fmt.Sprintf("/api/v1/statuses/%s/unbookmark", statusID)
|
path := fmt.Sprintf("/api/v1/statuses/%s/unbookmark", statusID)
|
||||||
url := g.Authentication.Instance + path
|
url := g.Authentication.Instance + path
|
||||||
|
|
||||||
if err := g.sendRequest(http.MethodPost, url, nil, nil); err != nil {
|
params := requestParameters{
|
||||||
|
httpMethod: http.MethodPost,
|
||||||
|
url: url,
|
||||||
|
requestBody: nil,
|
||||||
|
contentType: "",
|
||||||
|
output: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.sendRequest(params); err != nil {
|
||||||
return fmt.Errorf(
|
return fmt.Errorf(
|
||||||
"received an error after sending the request to remove the status from the list of bookmarks: %w",
|
"received an error after sending the request to remove the status from the list of bookmarks: %w",
|
||||||
err,
|
err,
|
||||||
|
@ -110,3 +159,178 @@ func (g *Client) RemoveStatusFromBookmarks(statusID string) error {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (g *Client) LikeStatus(statusID string) error {
|
||||||
|
url := g.Authentication.Instance + baseStatusesPath + "/" + statusID + "/favourite"
|
||||||
|
|
||||||
|
params := requestParameters{
|
||||||
|
httpMethod: http.MethodPost,
|
||||||
|
url: url,
|
||||||
|
requestBody: nil,
|
||||||
|
contentType: "",
|
||||||
|
output: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.sendRequest(params); err != nil {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"received an error after sending the request to like the status: %w",
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Client) UnlikeStatus(statusID string) error {
|
||||||
|
url := g.Authentication.Instance + baseStatusesPath + "/" + statusID + "/unfavourite"
|
||||||
|
|
||||||
|
params := requestParameters{
|
||||||
|
httpMethod: http.MethodPost,
|
||||||
|
url: url,
|
||||||
|
requestBody: nil,
|
||||||
|
contentType: "",
|
||||||
|
output: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.sendRequest(params); err != nil {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"received an error after sending the request to unlike the status: %w",
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Client) GetLikedStatuses(limit int, resourceName string) (model.StatusList, error) {
|
||||||
|
url := g.Authentication.Instance + fmt.Sprintf("/api/v1/favourites?limit=%d", limit)
|
||||||
|
|
||||||
|
liked := model.StatusList{
|
||||||
|
Name: "Your " + resourceName + " statuses",
|
||||||
|
Statuses: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
params := requestParameters{
|
||||||
|
httpMethod: http.MethodGet,
|
||||||
|
url: url,
|
||||||
|
requestBody: nil,
|
||||||
|
contentType: "",
|
||||||
|
output: &liked.Statuses,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.sendRequest(params); err != nil {
|
||||||
|
return model.StatusList{}, fmt.Errorf(
|
||||||
|
"received an error after sending the request to get the list of statuses: %w",
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return liked, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Client) ReblogStatus(statusID string) error {
|
||||||
|
url := g.Authentication.Instance + baseStatusesPath + "/" + statusID + "/reblog"
|
||||||
|
|
||||||
|
params := requestParameters{
|
||||||
|
httpMethod: http.MethodPost,
|
||||||
|
url: url,
|
||||||
|
requestBody: nil,
|
||||||
|
contentType: "",
|
||||||
|
output: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.sendRequest(params); err != nil {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"received an error after sending the request to reblog the status: %w",
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Client) UnreblogStatus(statusID string) error {
|
||||||
|
url := g.Authentication.Instance + baseStatusesPath + "/" + statusID + "/unreblog"
|
||||||
|
|
||||||
|
params := requestParameters{
|
||||||
|
httpMethod: http.MethodPost,
|
||||||
|
url: url,
|
||||||
|
requestBody: nil,
|
||||||
|
contentType: "",
|
||||||
|
output: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.sendRequest(params); err != nil {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"received an error after sending the request to un-reblog the status: %w",
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Client) MuteStatus(statusID string) error {
|
||||||
|
url := g.Authentication.Instance + baseStatusesPath + "/" + statusID + "/mute"
|
||||||
|
|
||||||
|
params := requestParameters{
|
||||||
|
httpMethod: http.MethodPost,
|
||||||
|
url: url,
|
||||||
|
requestBody: nil,
|
||||||
|
contentType: "",
|
||||||
|
output: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.sendRequest(params); err != nil {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"received an error after sending the request to mute the status: %w",
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Client) UnmuteStatus(statusID string) error {
|
||||||
|
url := g.Authentication.Instance + baseStatusesPath + "/" + statusID + "/unmute"
|
||||||
|
|
||||||
|
params := requestParameters{
|
||||||
|
httpMethod: http.MethodPost,
|
||||||
|
url: url,
|
||||||
|
requestBody: nil,
|
||||||
|
contentType: "",
|
||||||
|
output: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.sendRequest(params); err != nil {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"received an error after sending the request to unmute the status: %w",
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Client) DeleteStatus(statusID string) (string, error) {
|
||||||
|
url := g.Authentication.Instance + baseStatusesPath + "/" + statusID
|
||||||
|
|
||||||
|
var status model.Status
|
||||||
|
|
||||||
|
params := requestParameters{
|
||||||
|
httpMethod: http.MethodDelete,
|
||||||
|
url: url,
|
||||||
|
requestBody: nil,
|
||||||
|
contentType: "",
|
||||||
|
output: &status,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.sendRequest(params); err != nil {
|
||||||
|
return "", fmt.Errorf(
|
||||||
|
"received an error after sending the request to delete the status: %w",
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return status.Text, nil
|
||||||
|
}
|
||||||
|
|
|
@ -1,7 +1,3 @@
|
||||||
// SPDX-FileCopyrightText: 2024 Dan Anglin <d.n.i.anglin@gmail.com>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -15,8 +11,7 @@ func (g *Client) GetHomeTimeline(limit int) (model.StatusList, error) {
|
||||||
path := fmt.Sprintf("/api/v1/timelines/home?limit=%d", limit)
|
path := fmt.Sprintf("/api/v1/timelines/home?limit=%d", limit)
|
||||||
|
|
||||||
timeline := model.StatusList{
|
timeline := model.StatusList{
|
||||||
Type: model.StatusListTimeline,
|
Name: "Timeline: Home",
|
||||||
Name: "HOME",
|
|
||||||
Statuses: nil,
|
Statuses: nil,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,8 +22,7 @@ func (g *Client) GetPublicTimeline(limit int) (model.StatusList, error) {
|
||||||
path := fmt.Sprintf("/api/v1/timelines/public?limit=%d", limit)
|
path := fmt.Sprintf("/api/v1/timelines/public?limit=%d", limit)
|
||||||
|
|
||||||
timeline := model.StatusList{
|
timeline := model.StatusList{
|
||||||
Type: model.StatusListTimeline,
|
Name: "Timeline: Public",
|
||||||
Name: "PUBLIC",
|
|
||||||
Statuses: nil,
|
Statuses: nil,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,8 +33,7 @@ func (g *Client) GetListTimeline(listID, title string, limit int) (model.StatusL
|
||||||
path := fmt.Sprintf("/api/v1/timelines/list/%s?limit=%d", listID, limit)
|
path := fmt.Sprintf("/api/v1/timelines/list/%s?limit=%d", listID, limit)
|
||||||
|
|
||||||
timeline := model.StatusList{
|
timeline := model.StatusList{
|
||||||
Type: model.StatusListTimeline,
|
Name: "Timeline: List (" + title + ")",
|
||||||
Name: "LIST (" + title + ")",
|
|
||||||
Statuses: nil,
|
Statuses: nil,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,8 +44,7 @@ func (g *Client) GetTagTimeline(tag string, limit int) (model.StatusList, error)
|
||||||
path := fmt.Sprintf("/api/v1/timelines/tag/%s?limit=%d", tag, limit)
|
path := fmt.Sprintf("/api/v1/timelines/tag/%s?limit=%d", tag, limit)
|
||||||
|
|
||||||
timeline := model.StatusList{
|
timeline := model.StatusList{
|
||||||
Type: model.StatusListTimeline,
|
Name: "Timeline: Tag (" + tag + ")",
|
||||||
Name: "TAG (" + tag + ")",
|
|
||||||
Statuses: nil,
|
Statuses: nil,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,7 +56,15 @@ func (g *Client) getTimeline(path string, timeline model.StatusList) (model.Stat
|
||||||
|
|
||||||
var statuses []model.Status
|
var statuses []model.Status
|
||||||
|
|
||||||
if err := g.sendRequest(http.MethodGet, url, nil, &statuses); err != nil {
|
params := requestParameters{
|
||||||
|
httpMethod: http.MethodGet,
|
||||||
|
url: url,
|
||||||
|
requestBody: nil,
|
||||||
|
contentType: "",
|
||||||
|
output: &statuses,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.sendRequest(params); err != nil {
|
||||||
return timeline, fmt.Errorf("received an error after sending the request to get the timeline: %w", err)
|
return timeline, fmt.Errorf("received an error after sending the request to get the timeline: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,23 +1,14 @@
|
||||||
// SPDX-FileCopyrightText: 2024 Dan Anglin <d.n.i.anglin@gmail.com>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"codeflow.dananglin.me.uk/apollo/enbas/internal"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var errEmptyAccessToken = errors.New("received an empty access token")
|
|
||||||
|
|
||||||
type tokenRequest struct {
|
type tokenRequest struct {
|
||||||
RedirectUri string `json:"redirect_uri"`
|
RedirectURI string `json:"redirect_uri"`
|
||||||
ClientID string `json:"client_id"`
|
ClientID string `json:"client_id"`
|
||||||
ClientSecret string `json:"client_secret"`
|
ClientSecret string `json:"client_secret"`
|
||||||
GrantType string `json:"grant_type"`
|
GrantType string `json:"grant_type"`
|
||||||
|
@ -32,15 +23,15 @@ type tokenResponse struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *Client) UpdateToken(code string) error {
|
func (g *Client) UpdateToken(code string) error {
|
||||||
params := tokenRequest{
|
tokenReq := tokenRequest{
|
||||||
RedirectUri: internal.RedirectUri,
|
RedirectURI: redirectURI,
|
||||||
ClientID: g.Authentication.ClientID,
|
ClientID: g.Authentication.ClientID,
|
||||||
ClientSecret: g.Authentication.ClientSecret,
|
ClientSecret: g.Authentication.ClientSecret,
|
||||||
GrantType: "authorization_code",
|
GrantType: "authorization_code",
|
||||||
Code: code,
|
Code: code,
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := json.Marshal(params)
|
data, err := json.Marshal(tokenReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to marshal the request body: %w", err)
|
return fmt.Errorf("unable to marshal the request body: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -50,12 +41,20 @@ func (g *Client) UpdateToken(code string) error {
|
||||||
|
|
||||||
var response tokenResponse
|
var response tokenResponse
|
||||||
|
|
||||||
if err := g.sendRequest(http.MethodPost, url, requestBody, &response); err != nil {
|
params := requestParameters{
|
||||||
|
httpMethod: http.MethodPost,
|
||||||
|
url: url,
|
||||||
|
requestBody: requestBody,
|
||||||
|
contentType: applicationJSON,
|
||||||
|
output: &response,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.sendRequest(params); err != nil {
|
||||||
return fmt.Errorf("received an error after sending the token request: %w", err)
|
return fmt.Errorf("received an error after sending the token request: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if response.AccessToken == "" {
|
if response.AccessToken == "" {
|
||||||
return errEmptyAccessToken
|
return Error{"received an empty access token"}
|
||||||
}
|
}
|
||||||
|
|
||||||
g.Authentication.AccessToken = response.AccessToken
|
g.Authentication.AccessToken = response.AccessToken
|
||||||
|
|
128
internal/config/config.go
Normal file
128
internal/config/config.go
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"codeflow.dananglin.me.uk/apollo/enbas/internal/utilities"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
configFileName string = "config.json"
|
||||||
|
|
||||||
|
defaultHTTPTimeout int = 5
|
||||||
|
defaultHTTPMediaTimeout int = 30
|
||||||
|
defaultLineWrapMaxWidth int = 80
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
CredentialsFile string `json:"credentialsFile"`
|
||||||
|
CacheDirectory string `json:"cacheDirectory"`
|
||||||
|
LineWrapMaxWidth int `json:"lineWrapMaxWidth"`
|
||||||
|
HTTP HTTPConfig `json:"http"`
|
||||||
|
Integrations Integrations `json:"integrations"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HTTPConfig struct {
|
||||||
|
Timeout int `json:"timeout"`
|
||||||
|
MediaTimeout int `json:"mediaTimeout"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Integrations struct {
|
||||||
|
Browser string `json:"browser"`
|
||||||
|
Editor string `json:"editor"`
|
||||||
|
Pager string `json:"pager"`
|
||||||
|
ImageViewer string `json:"imageViewer"`
|
||||||
|
VideoPlayer string `json:"videoPlayer"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConfigFromFile(configDir string) (*Config, error) {
|
||||||
|
path, err := configPath(configDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to calculate the path to your config file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to open %s: %w", path, err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
var config Config
|
||||||
|
|
||||||
|
if err := json.NewDecoder(file).Decode(&config); err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to decode the JSON data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func FileExists(configDir string) (bool, error) {
|
||||||
|
path, err := configPath(configDir)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("unable to calculate the path to your config file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return utilities.FileExists(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SaveDefaultConfigToFile(configDir string) error {
|
||||||
|
path, err := configPath(configDir)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to calculate the path to your config file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to create the file at %s: %w", path, err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
config := defaultConfig()
|
||||||
|
|
||||||
|
credentialsFilePath, err := defaultCredentialsConfigFile(configDir)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to calculate the path to the credentials file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
config.CredentialsFile = credentialsFilePath
|
||||||
|
|
||||||
|
encoder := json.NewEncoder(file)
|
||||||
|
encoder.SetIndent("", " ")
|
||||||
|
|
||||||
|
if err := encoder.Encode(config); err != nil {
|
||||||
|
return fmt.Errorf("unable to save the JSON data to the config file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func configPath(configDir string) (string, error) {
|
||||||
|
configDir, err := utilities.CalculateConfigDir(configDir)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("unable to get the config directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return filepath.Join(configDir, configFileName), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultConfig() Config {
|
||||||
|
return Config{
|
||||||
|
CredentialsFile: "",
|
||||||
|
CacheDirectory: "",
|
||||||
|
HTTP: HTTPConfig{
|
||||||
|
Timeout: defaultHTTPTimeout,
|
||||||
|
MediaTimeout: defaultHTTPMediaTimeout,
|
||||||
|
},
|
||||||
|
LineWrapMaxWidth: defaultLineWrapMaxWidth,
|
||||||
|
Integrations: Integrations{
|
||||||
|
Browser: "",
|
||||||
|
Editor: "",
|
||||||
|
Pager: "",
|
||||||
|
ImageViewer: "",
|
||||||
|
VideoPlayer: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
83
internal/config/config_test.go
Normal file
83
internal/config/config_test.go
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
package config_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"codeflow.dananglin.me.uk/apollo/enbas/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConfigFile(t *testing.T) {
|
||||||
|
t.Log("Testing saving and loading the configuration")
|
||||||
|
|
||||||
|
projectDir, err := projectRoot()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unable to get the project root directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
configDir := filepath.Join(projectDir, "test", "config")
|
||||||
|
|
||||||
|
t.Run("Save the default configuration to file", testSaveDefaultConfigToFile(configDir))
|
||||||
|
t.Run("Load the configuration from file", testLoadConfigFromFile(configDir))
|
||||||
|
|
||||||
|
expectedConfigFile := filepath.Join(configDir, "config.json")
|
||||||
|
if err := os.Remove(expectedConfigFile); err != nil {
|
||||||
|
t.Fatalf(
|
||||||
|
"received an error after trying to clean up the test configuration at %q: %v",
|
||||||
|
expectedConfigFile,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSaveDefaultConfigToFile(configDir string) func(t *testing.T) {
|
||||||
|
return func(t *testing.T) {
|
||||||
|
if err := config.SaveDefaultConfigToFile(configDir); err != nil {
|
||||||
|
t.Fatalf("Unable to save the default configuration within %q: %v", configDir, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fileExists, err := config.FileExists(configDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unable to determine if the configuration exists within %q: %v", configDir, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !fileExists {
|
||||||
|
t.Fatalf("The configuration does not appear to exist within %q", configDir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testLoadConfigFromFile(configDir string) func(t *testing.T) {
|
||||||
|
return func(t *testing.T) {
|
||||||
|
config, err := config.NewConfigFromFile(configDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unable to load the configuration from file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedDefaultHTTPTimeout := 5
|
||||||
|
|
||||||
|
if config.HTTP.Timeout != expectedDefaultHTTPTimeout {
|
||||||
|
t.Errorf(
|
||||||
|
"Unexpected HTTP Timeout settings loaded from the configuration: want %d, got %d",
|
||||||
|
expectedDefaultHTTPTimeout,
|
||||||
|
config.HTTP.Timeout,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
t.Logf(
|
||||||
|
"Expected HTTP Timeout settings loaded from the configuration: got %d",
|
||||||
|
config.HTTP.Timeout,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func projectRoot() (string, error) {
|
||||||
|
cwd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("unable to get the current working directory, %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return filepath.Join(cwd, "..", ".."), nil
|
||||||
|
}
|
|
@ -1,7 +1,3 @@
|
||||||
// SPDX-FileCopyrightText: 2024 Dan Anglin <d.n.i.anglin@gmail.com>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -10,11 +6,12 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
|
||||||
|
"codeflow.dananglin.me.uk/apollo/enbas/internal/utilities"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
credentialsFileName = "credentials.json"
|
defaultCredentialsFileName = "credentials.json"
|
||||||
)
|
)
|
||||||
|
|
||||||
type CredentialsConfig struct {
|
type CredentialsConfig struct {
|
||||||
|
@ -40,35 +37,35 @@ func (e CredentialsNotFoundError) Error() string {
|
||||||
// SaveCredentials saves the credentials into the credentials file within the specified configuration
|
// SaveCredentials saves the credentials into the credentials file within the specified configuration
|
||||||
// directory. If the directory is not specified then the default directory is used. If the directory
|
// directory. If the directory is not specified then the default directory is used. If the directory
|
||||||
// is not present, it will be created.
|
// is not present, it will be created.
|
||||||
func SaveCredentials(configDir, username string, credentials Credentials) (string, error) {
|
func SaveCredentials(filePath, username string, credentials Credentials) (string, error) {
|
||||||
if err := ensureConfigDir(calculateConfigDir(configDir)); err != nil {
|
part := filepath.Dir(filePath)
|
||||||
|
|
||||||
|
// ensure that the directory exists.
|
||||||
|
credentialsDir, err := utilities.CalculateConfigDir(part)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("unable to calculate the directory to your credentials file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := utilities.EnsureDirectory(credentialsDir); 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 authConfig CredentialsConfig
|
var authConfig CredentialsConfig
|
||||||
|
|
||||||
filepath := credentialsConfigFile(configDir)
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
authConfig.Credentials = make(map[string]Credentials)
|
authConfig.Credentials = make(map[string]Credentials)
|
||||||
} else {
|
} else {
|
||||||
authConfig, err = NewCredentialsConfigFromFile(configDir)
|
authConfig, err = NewCredentialsConfigFromFile(filePath)
|
||||||
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
instance := ""
|
instance := utilities.GetFQDN(credentials.Instance)
|
||||||
|
|
||||||
if strings.HasPrefix(credentials.Instance, "https://") {
|
|
||||||
instance = strings.TrimPrefix(credentials.Instance, "https://")
|
|
||||||
} else if strings.HasPrefix(credentials.Instance, "http://") {
|
|
||||||
instance = strings.TrimPrefix(credentials.Instance, "http://")
|
|
||||||
}
|
|
||||||
|
|
||||||
authenticationName := username + "@" + instance
|
authenticationName := username + "@" + instance
|
||||||
|
|
||||||
|
@ -76,15 +73,15 @@ func SaveCredentials(configDir, username string, credentials Credentials) (strin
|
||||||
|
|
||||||
authConfig.Credentials[authenticationName] = credentials
|
authConfig.Credentials[authenticationName] = credentials
|
||||||
|
|
||||||
if err := saveCredentialsConfigFile(authConfig, configDir); err != nil {
|
if err := saveCredentialsConfigFile(authConfig, filePath); err != nil {
|
||||||
return "", fmt.Errorf("unable to save the authentication configuration to file: %w", err)
|
return "", fmt.Errorf("unable to save the authentication configuration to file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return authenticationName, nil
|
return authenticationName, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func UpdateCurrentAccount(account string, configDir string) error {
|
func UpdateCurrentAccount(account string, filePath string) error {
|
||||||
credentialsConfig, err := NewCredentialsConfigFromFile(configDir)
|
credentialsConfig, err := NewCredentialsConfigFromFile(filePath)
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
@ -95,19 +92,19 @@ func UpdateCurrentAccount(account string, configDir string) error {
|
||||||
|
|
||||||
credentialsConfig.CurrentAccount = account
|
credentialsConfig.CurrentAccount = account
|
||||||
|
|
||||||
if err := saveCredentialsConfigFile(credentialsConfig, configDir); err != nil {
|
if err := saveCredentialsConfigFile(credentialsConfig, filePath); err != nil {
|
||||||
return fmt.Errorf("unable to save the authentication configuration to file: %w", err)
|
return fmt.Errorf("unable to save the authentication configuration to file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCredentialsConfigFromFile(configDir string) (CredentialsConfig, error) {
|
// NewCredentialsConfigFromFile creates a new CredentialsConfig value from reading
|
||||||
path := credentialsConfigFile(configDir)
|
// the credentials file.
|
||||||
|
func NewCredentialsConfigFromFile(filePath string) (CredentialsConfig, error) {
|
||||||
file, err := os.Open(path)
|
file, err := os.Open(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return CredentialsConfig{}, fmt.Errorf("unable to open %s, %w", path, err)
|
return CredentialsConfig{}, fmt.Errorf("unable to open %s: %w", filePath, err)
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
|
@ -120,14 +117,11 @@ func NewCredentialsConfigFromFile(configDir string) (CredentialsConfig, error) {
|
||||||
return authConfig, nil
|
return authConfig, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveCredentialsConfigFile(authConfig CredentialsConfig, configDir string) error {
|
func saveCredentialsConfigFile(authConfig CredentialsConfig, filePath string) error {
|
||||||
path := credentialsConfigFile(configDir)
|
file, err := os.Create(filePath)
|
||||||
|
|
||||||
file, err := os.Create(path)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to open %s: %w", path, err)
|
return fmt.Errorf("unable to create the file at %s: %w", filePath, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
encoder := json.NewEncoder(file)
|
encoder := json.NewEncoder(file)
|
||||||
|
@ -140,6 +134,16 @@ func saveCredentialsConfigFile(authConfig CredentialsConfig, configDir string) e
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func credentialsConfigFile(configDir string) string {
|
func defaultCredentialsConfigFile(configDir string) (string, error) {
|
||||||
return filepath.Join(calculateConfigDir(configDir), credentialsFileName)
|
dir, err := utilities.CalculateConfigDir(configDir)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("unable to calculate the config directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
path, err := utilities.AbsolutePath(filepath.Join(dir, defaultCredentialsFileName))
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("unable to get the absolute path to the credentials config file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return path, nil
|
||||||
}
|
}
|
||||||
|
|
107
internal/config/credentials_test.go
Normal file
107
internal/config/credentials_test.go
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
package config_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"maps"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"codeflow.dananglin.me.uk/apollo/enbas/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCredentialsFile(t *testing.T) {
|
||||||
|
t.Log("Testing saving and loading credentials from file")
|
||||||
|
|
||||||
|
projectDir, err := projectRoot()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unable to get the project root directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
credentialsFile := filepath.Join(projectDir, "test", "config", "credentials.json")
|
||||||
|
|
||||||
|
credentialsMap := map[string]config.Credentials{
|
||||||
|
"admin": {
|
||||||
|
Instance: "https://gts.red-crow.private",
|
||||||
|
ClientID: "01EOB91DVQGPA364QK32TM3LXR1998BMXSZE4",
|
||||||
|
ClientSecret: "ffd76025-4b23-4ce6-b8ea-077ce3cadf5a",
|
||||||
|
AccessToken: "C9VDXGGRPZ0448SH562N6N6893VNPGJMGJ336TXLMH8RXGWF4",
|
||||||
|
},
|
||||||
|
"bobby": {
|
||||||
|
Instance: "https://gts.red-crow.private",
|
||||||
|
ClientID: "01CUVHR6LIST7Q6R25Z9Y14WZK780V91S9VQB",
|
||||||
|
ClientSecret: "379fc272-c7cc-4ccb-8461-f3f71207f798",
|
||||||
|
AccessToken: "F0YWQG1R4DDAMXGBZ514BCW7ATWN6JRGLDRUZO4RFAMTT6J38",
|
||||||
|
},
|
||||||
|
"app": {
|
||||||
|
Instance: "https://gts.red-crow.private",
|
||||||
|
ClientID: "01HLZY7XCD60564OP3RG6FZTOAD3LGF0R8SEK",
|
||||||
|
ClientSecret: "dfd8e954-53b1-4f00-9c09-0b181f44bb79",
|
||||||
|
AccessToken: "JZ2PZ4YNE1BB38VMRIQ7DNWXKZE6B1EBV310RNC53KQCVHXGB",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("Saving credentials to file", testSaveCredentials(credentialsFile, credentialsMap))
|
||||||
|
|
||||||
|
expectedCurrentAccount := "bobby@gts.red-crow.private"
|
||||||
|
t.Run("Updating the current account in the credentials file", testUpdateCurrentAccount(expectedCurrentAccount, credentialsFile))
|
||||||
|
|
||||||
|
t.Run("Loading the credentials from file", testLoadCredentialsConfigFromFile(credentialsFile, expectedCurrentAccount))
|
||||||
|
|
||||||
|
if err := os.Remove(credentialsFile); err != nil {
|
||||||
|
t.Fatalf(
|
||||||
|
"received an error after trying to clean up the test configuration at %q: %v",
|
||||||
|
credentialsFile,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSaveCredentials(credentialsFile string, credentialsMap map[string]config.Credentials) func(t *testing.T) {
|
||||||
|
return func(t *testing.T) {
|
||||||
|
for username, credentials := range maps.All(credentialsMap) {
|
||||||
|
if _, err := config.SaveCredentials(credentialsFile, username, credentials); err != nil {
|
||||||
|
t.Fatalf(
|
||||||
|
"Unable to save the credentials for %s to %q: %v",
|
||||||
|
username,
|
||||||
|
credentialsFile,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Log("All credentials saved to file.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testUpdateCurrentAccount(account, credentialsFile string) func(t *testing.T) {
|
||||||
|
return func(t *testing.T) {
|
||||||
|
if err := config.UpdateCurrentAccount(account, credentialsFile); err != nil {
|
||||||
|
t.Fatalf("Unable to update the current account to %q: %v", account, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Successfully updated the current account.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testLoadCredentialsConfigFromFile(credentialsFile string, expectedCurrentAccount string) func(t *testing.T) {
|
||||||
|
return func(t *testing.T) {
|
||||||
|
credentials, err := config.NewCredentialsConfigFromFile(credentialsFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf(
|
||||||
|
"Unable to load the credentials configuration from %q: %v",
|
||||||
|
credentialsFile,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if credentials.CurrentAccount != expectedCurrentAccount {
|
||||||
|
t.Errorf(
|
||||||
|
"Unexpected current account found in the credentials configuration file: want %s, got %s",
|
||||||
|
expectedCurrentAccount,
|
||||||
|
credentials.CurrentAccount,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
t.Logf("Expected current account found in the credentials configuration file: got %s", credentials.CurrentAccount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,41 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2024 Dan Anglin <d.n.i.anglin@gmail.com>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"codeflow.dananglin.me.uk/apollo/enbas/internal"
|
|
||||||
)
|
|
||||||
|
|
||||||
func calculateConfigDir(configDir string) string {
|
|
||||||
if configDir != "" {
|
|
||||||
return configDir
|
|
||||||
}
|
|
||||||
|
|
||||||
rootDir, err := os.UserConfigDir()
|
|
||||||
if err != nil {
|
|
||||||
rootDir = "."
|
|
||||||
}
|
|
||||||
|
|
||||||
return filepath.Join(rootDir, internal.ApplicationName)
|
|
||||||
}
|
|
||||||
|
|
||||||
func ensureConfigDir(configDir string) error {
|
|
||||||
if _, err := os.Stat(configDir); err != nil {
|
|
||||||
if errors.Is(err, os.ErrNotExist) {
|
|
||||||
if err := os.MkdirAll(configDir, 0o750); err != nil {
|
|
||||||
return fmt.Errorf("unable to create %s: %w", configDir, err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return fmt.Errorf("unknown error received after getting the config directory information: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
40
internal/executor/accept.go
Normal file
40
internal/executor/accept.go
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
package executor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"codeflow.dananglin.me.uk/apollo/enbas/internal/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (a *AcceptExecutor) Execute() error {
|
||||||
|
funcMap := map[string]func(*client.Client) error{
|
||||||
|
resourceFollowRequest: a.acceptFollowRequest,
|
||||||
|
}
|
||||||
|
|
||||||
|
doFunc, ok := funcMap[a.resourceType]
|
||||||
|
if !ok {
|
||||||
|
return UnsupportedTypeError{resourceType: a.resourceType}
|
||||||
|
}
|
||||||
|
|
||||||
|
gtsClient, err := client.NewClientFromFile(a.config.CredentialsFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to create the GoToSocial client: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return doFunc(gtsClient)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AcceptExecutor) acceptFollowRequest(gtsClient *client.Client) error {
|
||||||
|
accountID, err := getAccountID(gtsClient, false, a.accountName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("received an error while getting the account ID: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := gtsClient.AcceptFollowRequest(accountID); err != nil {
|
||||||
|
return fmt.Errorf("unable to accept the follow request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
a.printer.PrintSuccess("Successfully accepted the follow request.")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -1,68 +1,56 @@
|
||||||
// SPDX-FileCopyrightText: 2024 Dan Anglin <d.n.i.anglin@gmail.com>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
package executor
|
package executor
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"codeflow.dananglin.me.uk/apollo/enbas/internal/client"
|
"codeflow.dananglin.me.uk/apollo/enbas/internal/client"
|
||||||
"codeflow.dananglin.me.uk/apollo/enbas/internal/config"
|
internalFlag "codeflow.dananglin.me.uk/apollo/enbas/internal/flag"
|
||||||
"codeflow.dananglin.me.uk/apollo/enbas/internal/model"
|
"codeflow.dananglin.me.uk/apollo/enbas/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
func getAccountID(gtsClient *client.Client, myAccount bool, accountName, configDir string) (string, error) {
|
func getAccountID(
|
||||||
|
gtsClient *client.Client,
|
||||||
|
myAccount bool,
|
||||||
|
accountNames internalFlag.StringSliceValue,
|
||||||
|
) (string, error) {
|
||||||
|
account, err := getAccount(gtsClient, myAccount, accountNames)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("unable to get the account information: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return account.ID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAccount(
|
||||||
|
gtsClient *client.Client,
|
||||||
|
myAccount bool,
|
||||||
|
accountNames internalFlag.StringSliceValue,
|
||||||
|
) (model.Account, error) {
|
||||||
var (
|
var (
|
||||||
accountID string
|
account model.Account
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case myAccount:
|
case myAccount:
|
||||||
accountID, err = getMyAccountID(gtsClient, configDir)
|
account, err = getMyAccount(gtsClient)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("unable to get your account ID: %w", err)
|
return account, fmt.Errorf("unable to get your account ID: %w", err)
|
||||||
}
|
}
|
||||||
case accountName != "":
|
case !accountNames.Empty():
|
||||||
accountID, err = getTheirAccountID(gtsClient, accountName)
|
account, err = getOtherAccount(gtsClient, accountNames)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("unable to get their account ID: %w", err)
|
return account, fmt.Errorf("unable to get the account ID: %w", err)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return "", NoAccountSpecifiedError{}
|
return account, NoAccountSpecifiedError{}
|
||||||
}
|
}
|
||||||
|
|
||||||
return accountID, nil
|
return account, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getTheirAccountID(gtsClient *client.Client, accountURI string) (string, error) {
|
func getMyAccount(gtsClient *client.Client) (model.Account, error) {
|
||||||
account, err := getAccount(gtsClient, accountURI)
|
account, err := gtsClient.VerifyCredentials()
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("unable to retrieve your account: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return account.ID, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getMyAccountID(gtsClient *client.Client, configDir string) (string, error) {
|
|
||||||
account, err := getMyAccount(gtsClient, configDir)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("received an error while getting your account details: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return account.ID, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getMyAccount(gtsClient *client.Client, configDir string) (model.Account, error) {
|
|
||||||
authConfig, err := config.NewCredentialsConfigFromFile(configDir)
|
|
||||||
if err != nil {
|
|
||||||
return model.Account{}, fmt.Errorf("unable to retrieve the authentication configuration: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
accountURI := authConfig.CurrentAccount
|
|
||||||
|
|
||||||
account, err := getAccount(gtsClient, accountURI)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return model.Account{}, fmt.Errorf("unable to retrieve your account: %w", err)
|
return model.Account{}, fmt.Errorf("unable to retrieve your account: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -70,11 +58,35 @@ func getMyAccount(gtsClient *client.Client, configDir string) (model.Account, er
|
||||||
return account, nil
|
return account, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAccount(gtsClient *client.Client, accountURI string) (model.Account, error) {
|
func getOtherAccount(gtsClient *client.Client, accountNames internalFlag.StringSliceValue) (model.Account, error) {
|
||||||
account, err := gtsClient.GetAccount(accountURI)
|
expectedNumAccountNames := 1
|
||||||
|
if !accountNames.ExpectedLength(expectedNumAccountNames) {
|
||||||
|
return model.Account{}, fmt.Errorf(
|
||||||
|
"received an unexpected number of account names: want %d",
|
||||||
|
expectedNumAccountNames,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
account, err := gtsClient.GetAccount(accountNames[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return model.Account{}, fmt.Errorf("unable to retrieve the account details: %w", err)
|
return model.Account{}, fmt.Errorf("unable to retrieve the account details: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return account, nil
|
return account, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getOtherAccounts(gtsClient *client.Client, accountNames internalFlag.StringSliceValue) ([]model.Account, error) {
|
||||||
|
numAccountNames := len(accountNames)
|
||||||
|
accounts := make([]model.Account, numAccountNames)
|
||||||
|
|
||||||
|
for ind := range numAccountNames {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
accounts[ind], err = gtsClient.GetAccount(accountNames[ind])
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to retrieve the account information for %s: %w", accountNames[ind], err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return accounts, nil
|
||||||
|
}
|
||||||
|
|
|
@ -1,49 +1,11 @@
|
||||||
// SPDX-FileCopyrightText: 2024 Dan Anglin <d.n.i.anglin@gmail.com>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
package executor
|
package executor
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"flag"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"codeflow.dananglin.me.uk/apollo/enbas/internal/client"
|
"codeflow.dananglin.me.uk/apollo/enbas/internal/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AddExecutor struct {
|
|
||||||
*flag.FlagSet
|
|
||||||
|
|
||||||
topLevelFlags TopLevelFlags
|
|
||||||
resourceType string
|
|
||||||
toResourceType string
|
|
||||||
listID string
|
|
||||||
statusID string
|
|
||||||
accountNames AccountNames
|
|
||||||
content string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewAddExecutor(tlf TopLevelFlags, name, summary string) *AddExecutor {
|
|
||||||
emptyArr := make([]string, 0, 3)
|
|
||||||
|
|
||||||
addExe := AddExecutor{
|
|
||||||
FlagSet: flag.NewFlagSet(name, flag.ExitOnError),
|
|
||||||
accountNames: AccountNames(emptyArr),
|
|
||||||
topLevelFlags: tlf,
|
|
||||||
}
|
|
||||||
|
|
||||||
addExe.StringVar(&addExe.resourceType, flagType, "", "specify the resource type to add (e.g. account, note)")
|
|
||||||
addExe.StringVar(&addExe.toResourceType, flagTo, "", "specify the target resource type to add to (e.g. list, account, etc)")
|
|
||||||
addExe.StringVar(&addExe.listID, flagListID, "", "the ID of the list to add to")
|
|
||||||
addExe.StringVar(&addExe.statusID, flagStatusID, "", "the ID of the status")
|
|
||||||
addExe.Var(&addExe.accountNames, flagAccountName, "the name of the account")
|
|
||||||
addExe.StringVar(&addExe.content, flagContent, "", "the content of the note")
|
|
||||||
|
|
||||||
addExe.Usage = commandUsageFunc(name, summary, addExe.FlagSet)
|
|
||||||
|
|
||||||
return &addExe
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *AddExecutor) Execute() error {
|
func (a *AddExecutor) Execute() error {
|
||||||
if a.toResourceType == "" {
|
if a.toResourceType == "" {
|
||||||
return FlagNotSetError{flagText: flagTo}
|
return FlagNotSetError{flagText: flagTo}
|
||||||
|
@ -53,6 +15,7 @@ func (a *AddExecutor) Execute() error {
|
||||||
resourceList: a.addToList,
|
resourceList: a.addToList,
|
||||||
resourceAccount: a.addToAccount,
|
resourceAccount: a.addToAccount,
|
||||||
resourceBookmarks: a.addToBookmarks,
|
resourceBookmarks: a.addToBookmarks,
|
||||||
|
resourceStatus: a.addToStatus,
|
||||||
}
|
}
|
||||||
|
|
||||||
doFunc, ok := funcMap[a.toResourceType]
|
doFunc, ok := funcMap[a.toResourceType]
|
||||||
|
@ -60,7 +23,7 @@ func (a *AddExecutor) Execute() error {
|
||||||
return UnsupportedTypeError{resourceType: a.toResourceType}
|
return UnsupportedTypeError{resourceType: a.toResourceType}
|
||||||
}
|
}
|
||||||
|
|
||||||
gtsClient, err := client.NewClientFromConfig(a.topLevelFlags.ConfigDir)
|
gtsClient, err := client.NewClientFromFile(a.config.CredentialsFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to create the GoToSocial client: %w", err)
|
return fmt.Errorf("unable to create the GoToSocial client: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -69,6 +32,13 @@ func (a *AddExecutor) Execute() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *AddExecutor) addToList(gtsClient *client.Client) error {
|
func (a *AddExecutor) addToList(gtsClient *client.Client) error {
|
||||||
|
if a.listID == "" {
|
||||||
|
return MissingIDError{
|
||||||
|
resource: resourceList,
|
||||||
|
action: "add to",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
funcMap := map[string]func(*client.Client) error{
|
funcMap := map[string]func(*client.Client) error{
|
||||||
resourceAccount: a.addAccountsToList,
|
resourceAccount: a.addAccountsToList,
|
||||||
}
|
}
|
||||||
|
@ -76,8 +46,8 @@ func (a *AddExecutor) addToList(gtsClient *client.Client) error {
|
||||||
doFunc, ok := funcMap[a.resourceType]
|
doFunc, ok := funcMap[a.resourceType]
|
||||||
if !ok {
|
if !ok {
|
||||||
return UnsupportedAddOperationError{
|
return UnsupportedAddOperationError{
|
||||||
ResourceType: a.resourceType,
|
resourceType: a.resourceType,
|
||||||
AddToResourceType: a.toResourceType,
|
addToResourceType: a.toResourceType,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,30 +55,35 @@ func (a *AddExecutor) addToList(gtsClient *client.Client) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *AddExecutor) addAccountsToList(gtsClient *client.Client) error {
|
func (a *AddExecutor) addAccountsToList(gtsClient *client.Client) error {
|
||||||
if a.listID == "" {
|
if a.accountNames.Empty() {
|
||||||
return FlagNotSetError{flagText: flagListID}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(a.accountNames) == 0 {
|
|
||||||
return NoAccountSpecifiedError{}
|
return NoAccountSpecifiedError{}
|
||||||
}
|
}
|
||||||
|
|
||||||
accountIDs := make([]string, len(a.accountNames))
|
accounts, err := getOtherAccounts(gtsClient, a.accountNames)
|
||||||
|
|
||||||
for ind := range a.accountNames {
|
|
||||||
accountID, err := getTheirAccountID(gtsClient, a.accountNames[ind])
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to get the account ID for %s, %w", a.accountNames[ind], err)
|
return fmt.Errorf("unable to get the accounts: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
accountIDs[ind] = accountID
|
accountIDs := make([]string, len(accounts))
|
||||||
|
|
||||||
|
for ind := range accounts {
|
||||||
|
relationship, err := gtsClient.GetAccountRelationship(accounts[ind].ID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to get your relationship to %s: %w", accounts[ind].Acct, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !relationship.Following {
|
||||||
|
return NotFollowingError{account: accounts[ind].Acct}
|
||||||
|
}
|
||||||
|
|
||||||
|
accountIDs[ind] = accounts[ind].ID
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := gtsClient.AddAccountsToList(a.listID, accountIDs); err != nil {
|
if err := gtsClient.AddAccountsToList(a.listID, accountIDs); err != nil {
|
||||||
return fmt.Errorf("unable to add the accounts to the list: %w", err)
|
return fmt.Errorf("unable to add the accounts to the list: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("Successfully added the account(s) to the list.")
|
a.printer.PrintSuccess("Successfully added the account(s) to the list.")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -121,8 +96,8 @@ func (a *AddExecutor) addToAccount(gtsClient *client.Client) error {
|
||||||
doFunc, ok := funcMap[a.resourceType]
|
doFunc, ok := funcMap[a.resourceType]
|
||||||
if !ok {
|
if !ok {
|
||||||
return UnsupportedAddOperationError{
|
return UnsupportedAddOperationError{
|
||||||
ResourceType: a.resourceType,
|
resourceType: a.resourceType,
|
||||||
AddToResourceType: a.toResourceType,
|
addToResourceType: a.toResourceType,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -130,27 +105,20 @@ func (a *AddExecutor) addToAccount(gtsClient *client.Client) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *AddExecutor) addNoteToAccount(gtsClient *client.Client) error {
|
func (a *AddExecutor) addNoteToAccount(gtsClient *client.Client) error {
|
||||||
if len(a.accountNames) != 1 {
|
accountID, err := getAccountID(gtsClient, false, a.accountNames)
|
||||||
return fmt.Errorf("unexpected number of accounts specified: want 1, got %d", len(a.accountNames))
|
|
||||||
}
|
|
||||||
|
|
||||||
accountID, err := getAccountID(gtsClient, false, a.accountNames[0], a.topLevelFlags.ConfigDir)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("received an error while getting the account ID: %w", err)
|
return fmt.Errorf("received an error while getting the account ID: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if a.content == "" {
|
if a.content == "" {
|
||||||
return EmptyContentError{
|
return Error{"please add content to the note you want to add"}
|
||||||
ResourceType: resourceNote,
|
|
||||||
Hint: "please use --" + flagContent,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := gtsClient.SetPrivateNote(accountID, a.content); err != nil {
|
if err := gtsClient.SetPrivateNote(accountID, a.content); err != nil {
|
||||||
return fmt.Errorf("unable to add the private note to the account: %w", err)
|
return fmt.Errorf("unable to add the private note to the account: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("Successfully added the private note to the account.")
|
a.printer.PrintSuccess("Successfully added the private note to the account.")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -163,8 +131,8 @@ func (a *AddExecutor) addToBookmarks(gtsClient *client.Client) error {
|
||||||
doFunc, ok := funcMap[a.resourceType]
|
doFunc, ok := funcMap[a.resourceType]
|
||||||
if !ok {
|
if !ok {
|
||||||
return UnsupportedAddOperationError{
|
return UnsupportedAddOperationError{
|
||||||
ResourceType: a.resourceType,
|
resourceType: a.resourceType,
|
||||||
AddToResourceType: a.toResourceType,
|
addToResourceType: a.toResourceType,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -173,14 +141,105 @@ func (a *AddExecutor) addToBookmarks(gtsClient *client.Client) error {
|
||||||
|
|
||||||
func (a *AddExecutor) addStatusToBookmarks(gtsClient *client.Client) error {
|
func (a *AddExecutor) addStatusToBookmarks(gtsClient *client.Client) error {
|
||||||
if a.statusID == "" {
|
if a.statusID == "" {
|
||||||
return FlagNotSetError{flagText: flagStatusID}
|
return MissingIDError{
|
||||||
|
resource: resourceStatus,
|
||||||
|
action: "add to your bookmarks",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := gtsClient.AddStatusToBookmarks(a.statusID); err != nil {
|
if err := gtsClient.AddStatusToBookmarks(a.statusID); err != nil {
|
||||||
return fmt.Errorf("unable to add the status to your bookmarks: %w", err)
|
return fmt.Errorf("unable to add the status to your bookmarks: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("Successfully added the status to your bookmarks.")
|
a.printer.PrintSuccess("Successfully added the status to your bookmarks.")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AddExecutor) addToStatus(gtsClient *client.Client) error {
|
||||||
|
if a.statusID == "" {
|
||||||
|
return MissingIDError{
|
||||||
|
resource: resourceStatus,
|
||||||
|
action: "add to",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
funcMap := map[string]func(*client.Client) error{
|
||||||
|
resourceStar: a.addStarToStatus,
|
||||||
|
resourceLike: a.addStarToStatus,
|
||||||
|
resourceBoost: a.addBoostToStatus,
|
||||||
|
resourceVote: a.addVoteToStatus,
|
||||||
|
}
|
||||||
|
|
||||||
|
doFunc, ok := funcMap[a.resourceType]
|
||||||
|
if !ok {
|
||||||
|
return UnsupportedAddOperationError{
|
||||||
|
resourceType: a.resourceType,
|
||||||
|
addToResourceType: a.toResourceType,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return doFunc(gtsClient)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AddExecutor) addStarToStatus(gtsClient *client.Client) error {
|
||||||
|
if err := gtsClient.LikeStatus(a.statusID); err != nil {
|
||||||
|
return fmt.Errorf("unable to add the %s to the status: %w", a.resourceType, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
a.printer.PrintSuccess("Successfully added a " + a.resourceType + " to the status.")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AddExecutor) addBoostToStatus(gtsClient *client.Client) error {
|
||||||
|
if err := gtsClient.ReblogStatus(a.statusID); err != nil {
|
||||||
|
return fmt.Errorf("unable to add the boost to the status: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
a.printer.PrintSuccess("Successfully added the boost to the status.")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AddExecutor) addVoteToStatus(gtsClient *client.Client) error {
|
||||||
|
if a.votes.Empty() {
|
||||||
|
return Error{"please add your vote(s) to the poll"}
|
||||||
|
}
|
||||||
|
|
||||||
|
status, err := gtsClient.GetStatus(a.statusID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to get the status: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if status.Poll == nil {
|
||||||
|
return Error{"this status does not have a poll"}
|
||||||
|
}
|
||||||
|
|
||||||
|
if status.Poll.Expired {
|
||||||
|
return Error{"this poll is closed"}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !status.Poll.Multiple && !a.votes.ExpectedLength(1) {
|
||||||
|
return Error{"this poll does not allow multiple choices"}
|
||||||
|
}
|
||||||
|
|
||||||
|
myAccountID, err := getAccountID(gtsClient, true, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to get your account ID: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if status.Account.ID == myAccountID {
|
||||||
|
return Error{"you cannot vote in your own poll"}
|
||||||
|
}
|
||||||
|
|
||||||
|
pollID := status.Poll.ID
|
||||||
|
|
||||||
|
if err := gtsClient.VoteInPoll(pollID, []int(a.votes)); err != nil {
|
||||||
|
return fmt.Errorf("unable to add your vote(s) to the poll: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
a.printer.PrintSuccess("Successfully added your vote(s) to the poll.")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,41 +1,11 @@
|
||||||
// SPDX-FileCopyrightText: 2024 Dan Anglin <d.n.i.anglin@gmail.com>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
package executor
|
package executor
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"flag"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"codeflow.dananglin.me.uk/apollo/enbas/internal/client"
|
"codeflow.dananglin.me.uk/apollo/enbas/internal/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
type BlockExecutor struct {
|
|
||||||
*flag.FlagSet
|
|
||||||
|
|
||||||
topLevelFlags TopLevelFlags
|
|
||||||
resourceType string
|
|
||||||
accountName string
|
|
||||||
unblock bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewBlockExecutor(tlf TopLevelFlags, name, summary string, unblock bool) *BlockExecutor {
|
|
||||||
blockExe := BlockExecutor{
|
|
||||||
FlagSet: flag.NewFlagSet(name, flag.ExitOnError),
|
|
||||||
|
|
||||||
topLevelFlags: tlf,
|
|
||||||
unblock: unblock,
|
|
||||||
}
|
|
||||||
|
|
||||||
blockExe.StringVar(&blockExe.resourceType, flagType, "", "specify the type of resource to block or unblock")
|
|
||||||
blockExe.StringVar(&blockExe.accountName, flagAccountName, "", "specify the account name in full (username@domain)")
|
|
||||||
|
|
||||||
blockExe.Usage = commandUsageFunc(name, summary, blockExe.FlagSet)
|
|
||||||
|
|
||||||
return &blockExe
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *BlockExecutor) Execute() error {
|
func (b *BlockExecutor) Execute() error {
|
||||||
funcMap := map[string]func(*client.Client) error{
|
funcMap := map[string]func(*client.Client) error{
|
||||||
resourceAccount: b.blockAccount,
|
resourceAccount: b.blockAccount,
|
||||||
|
@ -46,7 +16,7 @@ func (b *BlockExecutor) Execute() error {
|
||||||
return UnsupportedTypeError{resourceType: b.resourceType}
|
return UnsupportedTypeError{resourceType: b.resourceType}
|
||||||
}
|
}
|
||||||
|
|
||||||
gtsClient, err := client.NewClientFromConfig(b.topLevelFlags.ConfigDir)
|
gtsClient, err := client.NewClientFromFile(b.config.CredentialsFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to create the GoToSocial client: %w", err)
|
return fmt.Errorf("unable to create the GoToSocial client: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -55,30 +25,16 @@ func (b *BlockExecutor) Execute() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *BlockExecutor) blockAccount(gtsClient *client.Client) error {
|
func (b *BlockExecutor) blockAccount(gtsClient *client.Client) error {
|
||||||
accountID, err := getAccountID(gtsClient, false, b.accountName, b.topLevelFlags.ConfigDir)
|
accountID, err := getAccountID(gtsClient, false, b.accountName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("received an error while getting the account ID: %w", err)
|
return fmt.Errorf("received an error while getting the account ID: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if b.unblock {
|
|
||||||
return b.unblockAccount(gtsClient, accountID)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := gtsClient.BlockAccount(accountID); err != nil {
|
if err := gtsClient.BlockAccount(accountID); err != nil {
|
||||||
return fmt.Errorf("unable to block the account: %w", err)
|
return fmt.Errorf("unable to block the account: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("Successfully blocked the account.")
|
b.printer.PrintSuccess("Successfully blocked the account.")
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *BlockExecutor) unblockAccount(gtsClient *client.Client, accountID string) error {
|
|
||||||
if err := gtsClient.UnblockAccount(accountID); err != nil {
|
|
||||||
return fmt.Errorf("unable to unblock the account: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println("Successfully unblocked the account.")
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
3
internal/executor/codegen.go
Normal file
3
internal/executor/codegen.go
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
package executor
|
||||||
|
|
||||||
|
//go:generate go run ../../cmd/enbas-codegen --package executor --path-to-enbas-cli-schema ../../schema/enbas_cli_schema.json
|
|
@ -1,49 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2024 Dan Anglin <d.n.i.anglin@gmail.com>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
package executor
|
|
||||||
|
|
||||||
const (
|
|
||||||
flagAccountName = "account-name"
|
|
||||||
flagBrowser = "browser"
|
|
||||||
flagContentType = "content-type"
|
|
||||||
flagContent = "content"
|
|
||||||
flagEnableFederation = "enable-federation"
|
|
||||||
flagEnableLikes = "enable-likes"
|
|
||||||
flagEnableReplies = "enable-replies"
|
|
||||||
flagEnableReposts = "enable-reposts"
|
|
||||||
flagFrom = "from"
|
|
||||||
flagFromFile = "from-file"
|
|
||||||
flagInstance = "instance"
|
|
||||||
flagLanguage = "language"
|
|
||||||
flagLimit = "limit"
|
|
||||||
flagListID = "list-id"
|
|
||||||
flagListTitle = "list-title"
|
|
||||||
flagListRepliesPolicy = "list-replies-policy"
|
|
||||||
flagMyAccount = "my-account"
|
|
||||||
flagNotify = "notify"
|
|
||||||
flagSensitive = "sensitive"
|
|
||||||
flagSkipRelationship = "skip-relationship"
|
|
||||||
flagShowPreferences = "show-preferences"
|
|
||||||
flagShowReposts = "show-reposts"
|
|
||||||
flagSpoilerText = "spoiler-text"
|
|
||||||
flagStatusID = "status-id"
|
|
||||||
flagTag = "tag"
|
|
||||||
flagTimelineCategory = "timeline-category"
|
|
||||||
flagTo = "to"
|
|
||||||
flagType = "type"
|
|
||||||
flagVisibility = "visibility"
|
|
||||||
|
|
||||||
resourceAccount = "account"
|
|
||||||
resourceBlocked = "blocked"
|
|
||||||
resourceFollowers = "followers"
|
|
||||||
resourceFollowing = "following"
|
|
||||||
resourceInstance = "instance"
|
|
||||||
resourceList = "list"
|
|
||||||
resourceNote = "note"
|
|
||||||
resourceStatus = "status"
|
|
||||||
resourceTimeline = "timeline"
|
|
||||||
resourceBookmarks = "bookmarks"
|
|
||||||
resourceNotification = "notification"
|
|
||||||
)
|
|
29
internal/executor/consts.go
Normal file
29
internal/executor/consts.go
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
package executor
|
||||||
|
|
||||||
|
const (
|
||||||
|
flagFrom string = "from"
|
||||||
|
flagTo string = "to"
|
||||||
|
flagType string = "type"
|
||||||
|
|
||||||
|
resourceAccount string = "account"
|
||||||
|
resourceBlocked string = "blocked"
|
||||||
|
resourceBookmarks string = "bookmarks"
|
||||||
|
resourceBoost string = "boost"
|
||||||
|
resourceFollowers string = "followers"
|
||||||
|
resourceFollowing string = "following"
|
||||||
|
resourceFollowRequest string = "follow-request"
|
||||||
|
resourceInstance string = "instance"
|
||||||
|
resourceLike string = "like"
|
||||||
|
resourceLiked string = "liked"
|
||||||
|
resourceList string = "list"
|
||||||
|
resourceMedia string = "media"
|
||||||
|
resourceMediaAttachment string = "media-attachment"
|
||||||
|
resourceMutedAccounts string = "muted-accounts"
|
||||||
|
resourceNote string = "note"
|
||||||
|
resourcePoll string = "poll"
|
||||||
|
resourceStatus string = "status"
|
||||||
|
resourceStar string = "star"
|
||||||
|
resourceStarred string = "starred"
|
||||||
|
resourceTimeline string = "timeline"
|
||||||
|
resourceVote string = "vote"
|
||||||
|
)
|
|
@ -1,83 +1,19 @@
|
||||||
// SPDX-FileCopyrightText: 2024 Dan Anglin <d.n.i.anglin@gmail.com>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
package executor
|
package executor
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"flag"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"codeflow.dananglin.me.uk/apollo/enbas/internal/client"
|
"codeflow.dananglin.me.uk/apollo/enbas/internal/client"
|
||||||
"codeflow.dananglin.me.uk/apollo/enbas/internal/model"
|
"codeflow.dananglin.me.uk/apollo/enbas/internal/model"
|
||||||
"codeflow.dananglin.me.uk/apollo/enbas/internal/utilities"
|
"codeflow.dananglin.me.uk/apollo/enbas/internal/utilities"
|
||||||
)
|
)
|
||||||
|
|
||||||
type CreateExecutor struct {
|
|
||||||
*flag.FlagSet
|
|
||||||
|
|
||||||
topLevelFlags TopLevelFlags
|
|
||||||
boostable bool
|
|
||||||
federated bool
|
|
||||||
likeable bool
|
|
||||||
replyable bool
|
|
||||||
sensitive *bool
|
|
||||||
content string
|
|
||||||
contentType string
|
|
||||||
fromFile string
|
|
||||||
language string
|
|
||||||
spoilerText string
|
|
||||||
resourceType string
|
|
||||||
listTitle string
|
|
||||||
listRepliesPolicy string
|
|
||||||
visibility string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewCreateExecutor(tlf TopLevelFlags, name, summary string) *CreateExecutor {
|
|
||||||
createExe := CreateExecutor{
|
|
||||||
FlagSet: flag.NewFlagSet(name, flag.ExitOnError),
|
|
||||||
|
|
||||||
topLevelFlags: tlf,
|
|
||||||
}
|
|
||||||
|
|
||||||
createExe.BoolVar(&createExe.boostable, flagEnableReposts, true, "specify if the status can be reposted/boosted by others")
|
|
||||||
createExe.BoolVar(&createExe.federated, flagEnableFederation, true, "specify if the status can be federated beyond the local timelines")
|
|
||||||
createExe.BoolVar(&createExe.likeable, flagEnableLikes, true, "specify if the status can be liked/favourited")
|
|
||||||
createExe.BoolVar(&createExe.replyable, flagEnableReplies, true, "specify if the status can be replied to")
|
|
||||||
createExe.StringVar(&createExe.content, flagContent, "", "the content of the status to create")
|
|
||||||
createExe.StringVar(&createExe.contentType, flagContentType, "plain", "the type that the contents should be parsed from (valid values are plain and markdown)")
|
|
||||||
createExe.StringVar(&createExe.fromFile, flagFromFile, "", "the file path where to read the contents from")
|
|
||||||
createExe.StringVar(&createExe.language, flagLanguage, "", "the ISO 639 language code for this status")
|
|
||||||
createExe.StringVar(&createExe.spoilerText, flagSpoilerText, "", "the text to display as the status' warning or subject")
|
|
||||||
createExe.StringVar(&createExe.visibility, flagVisibility, "", "the visibility of the posted status")
|
|
||||||
createExe.StringVar(&createExe.resourceType, flagType, "", "specify the type of resource to create")
|
|
||||||
createExe.StringVar(&createExe.listTitle, flagListTitle, "", "specify the title of the list")
|
|
||||||
createExe.StringVar(&createExe.listRepliesPolicy, flagListRepliesPolicy, "list", "specify the policy of the replies for this list (valid values are followed, list and none)")
|
|
||||||
|
|
||||||
createExe.BoolFunc(flagSensitive, "specify if the status should be marked as sensitive", func(value string) error {
|
|
||||||
boolVal, err := strconv.ParseBool(value)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unable to parse %q as a boolean value: %w", value, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
createExe.sensitive = new(bool)
|
|
||||||
*createExe.sensitive = boolVal
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
createExe.Usage = commandUsageFunc(name, summary, createExe.FlagSet)
|
|
||||||
|
|
||||||
return &createExe
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *CreateExecutor) Execute() error {
|
func (c *CreateExecutor) Execute() error {
|
||||||
if c.resourceType == "" {
|
if c.resourceType == "" {
|
||||||
return FlagNotSetError{flagText: flagType}
|
return FlagNotSetError{flagText: flagType}
|
||||||
}
|
}
|
||||||
|
|
||||||
gtsClient, err := client.NewClientFromConfig(c.topLevelFlags.ConfigDir)
|
gtsClient, err := client.NewClientFromFile(c.config.CredentialsFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to create the GoToSocial client: %w", err)
|
return fmt.Errorf("unable to create the GoToSocial client: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -85,6 +21,7 @@ func (c *CreateExecutor) Execute() error {
|
||||||
funcMap := map[string]func(*client.Client) error{
|
funcMap := map[string]func(*client.Client) error{
|
||||||
resourceList: c.createList,
|
resourceList: c.createList,
|
||||||
resourceStatus: c.createStatus,
|
resourceStatus: c.createStatus,
|
||||||
|
resourceMediaAttachment: c.createMediaAttachment,
|
||||||
}
|
}
|
||||||
|
|
||||||
doFunc, ok := funcMap[c.resourceType]
|
doFunc, ok := funcMap[c.resourceType]
|
||||||
|
@ -97,12 +34,12 @@ func (c *CreateExecutor) Execute() error {
|
||||||
|
|
||||||
func (c *CreateExecutor) createList(gtsClient *client.Client) error {
|
func (c *CreateExecutor) createList(gtsClient *client.Client) error {
|
||||||
if c.listTitle == "" {
|
if c.listTitle == "" {
|
||||||
return FlagNotSetError{flagText: flagListTitle}
|
return Error{"please provide the title of the list that you want to create"}
|
||||||
}
|
}
|
||||||
|
|
||||||
parsedListRepliesPolicy, err := model.ParseListRepliesPolicy(c.listRepliesPolicy)
|
parsedListRepliesPolicy, err := model.ParseListRepliesPolicy(c.listRepliesPolicy)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err //nolint:wrapcheck
|
||||||
}
|
}
|
||||||
|
|
||||||
form := client.CreateListForm{
|
form := client.CreateListForm{
|
||||||
|
@ -115,8 +52,8 @@ func (c *CreateExecutor) createList(gtsClient *client.Client) error {
|
||||||
return fmt.Errorf("unable to create the list: %w", err)
|
return fmt.Errorf("unable to create the list: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("Successfully created the following list:")
|
c.printer.PrintSuccess("Successfully created the following list:")
|
||||||
utilities.Display(list, *c.topLevelFlags.NoColor)
|
c.printer.PrintList(list)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -124,30 +61,102 @@ func (c *CreateExecutor) createList(gtsClient *client.Client) error {
|
||||||
func (c *CreateExecutor) createStatus(gtsClient *client.Client) error {
|
func (c *CreateExecutor) createStatus(gtsClient *client.Client) error {
|
||||||
var (
|
var (
|
||||||
err error
|
err error
|
||||||
content string
|
|
||||||
language string
|
language string
|
||||||
visibility string
|
visibility string
|
||||||
sensitive bool
|
sensitive bool
|
||||||
)
|
)
|
||||||
|
|
||||||
switch {
|
attachmentIDs := []string(c.attachmentIDs)
|
||||||
case c.content != "":
|
|
||||||
content = c.content
|
if !c.mediaFiles.Empty() {
|
||||||
case c.fromFile != "":
|
descriptionsExists := false
|
||||||
content, err = utilities.ReadFile(c.fromFile)
|
focusValuesExists := false
|
||||||
|
numMediaFiles := len(c.mediaFiles)
|
||||||
|
mediaDescriptions := make([]string, numMediaFiles)
|
||||||
|
|
||||||
|
if !c.mediaDescriptions.Empty() {
|
||||||
|
descriptionsExists = true
|
||||||
|
|
||||||
|
if !c.mediaDescriptions.ExpectedLength(numMediaFiles) {
|
||||||
|
return MismatchedNumMediaValuesError{
|
||||||
|
valueType: "media descriptions",
|
||||||
|
numValues: len(c.mediaDescriptions),
|
||||||
|
numMediaFiles: numMediaFiles,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !c.mediaFocusValues.Empty() {
|
||||||
|
focusValuesExists = true
|
||||||
|
|
||||||
|
if !c.mediaFocusValues.ExpectedLength(numMediaFiles) {
|
||||||
|
return MismatchedNumMediaValuesError{
|
||||||
|
valueType: "media focus values",
|
||||||
|
numValues: len(c.mediaFocusValues),
|
||||||
|
numMediaFiles: numMediaFiles,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if descriptionsExists {
|
||||||
|
for ind := range numMediaFiles {
|
||||||
|
mediaDesc, err := utilities.ReadContents(c.mediaDescriptions[ind])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to get the status contents from %q: %w", c.fromFile, err)
|
return fmt.Errorf("unable to read the contents from %s: %w", c.mediaDescriptions[ind], err)
|
||||||
}
|
}
|
||||||
default:
|
|
||||||
return EmptyContentError{
|
mediaDescriptions[ind] = mediaDesc
|
||||||
ResourceType: resourceStatus,
|
|
||||||
Hint: "please use --" + flagContent + " or --" + flagFromFile,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for ind := range numMediaFiles {
|
||||||
|
var (
|
||||||
|
mediaFile string
|
||||||
|
description string
|
||||||
|
focus string
|
||||||
|
)
|
||||||
|
|
||||||
|
mediaFile = c.mediaFiles[ind]
|
||||||
|
|
||||||
|
if descriptionsExists {
|
||||||
|
description = mediaDescriptions[ind]
|
||||||
|
}
|
||||||
|
|
||||||
|
if focusValuesExists {
|
||||||
|
focus = c.mediaFocusValues[ind]
|
||||||
|
}
|
||||||
|
|
||||||
|
attachment, err := gtsClient.CreateMediaAttachment(
|
||||||
|
mediaFile,
|
||||||
|
description,
|
||||||
|
focus,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to create the media attachment for %s: %w", mediaFile, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
attachmentIDs = append(attachmentIDs, attachment.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.content == "" && len(attachmentIDs) == 0 {
|
||||||
|
return Error{"please add content to the status that you want to create"}
|
||||||
|
}
|
||||||
|
|
||||||
|
content, err := utilities.ReadContents(c.content)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to read the contents from %s: %w", c.content, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
numAttachmentIDs := len(attachmentIDs)
|
||||||
|
|
||||||
|
if c.addPoll && numAttachmentIDs > 0 {
|
||||||
|
return Error{"attaching media to a poll is not allowed"}
|
||||||
|
}
|
||||||
|
|
||||||
preferences, err := gtsClient.GetUserPreferences()
|
preferences, err := gtsClient.GetUserPreferences()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("WARNING: Unable to get your posting preferences: %w", err)
|
c.printer.PrintInfo("WARNING: Unable to get your posting preferences: " + err.Error() + ".\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.language != "" {
|
if c.language != "" {
|
||||||
|
@ -162,33 +171,55 @@ func (c *CreateExecutor) createStatus(gtsClient *client.Client) error {
|
||||||
visibility = preferences.PostingDefaultVisibility
|
visibility = preferences.PostingDefaultVisibility
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.sensitive != nil {
|
if c.sensitive.Value != nil {
|
||||||
sensitive = *c.sensitive
|
sensitive = *c.sensitive.Value
|
||||||
} else {
|
} else {
|
||||||
sensitive = preferences.PostingDefaultSensitive
|
sensitive = preferences.PostingDefaultSensitive
|
||||||
}
|
}
|
||||||
|
|
||||||
parsedVisibility, err := model.ParseStatusVisibility(visibility)
|
parsedVisibility, err := model.ParseStatusVisibility(visibility)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err //nolint:wrapcheck
|
||||||
}
|
}
|
||||||
|
|
||||||
parsedContentType, err := model.ParseStatusContentType(c.contentType)
|
parsedContentType, err := model.ParseStatusContentType(c.contentType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err //nolint:wrapcheck
|
||||||
}
|
}
|
||||||
|
|
||||||
form := client.CreateStatusForm{
|
form := client.CreateStatusForm{
|
||||||
Content: content,
|
Content: content,
|
||||||
ContentType: parsedContentType,
|
ContentType: parsedContentType,
|
||||||
Language: language,
|
Language: language,
|
||||||
SpoilerText: c.spoilerText,
|
SpoilerText: c.summary,
|
||||||
Boostable: c.boostable,
|
Boostable: c.boostable,
|
||||||
Federated: c.federated,
|
Federated: c.federated,
|
||||||
|
InReplyTo: c.inReplyTo,
|
||||||
Likeable: c.likeable,
|
Likeable: c.likeable,
|
||||||
Replyable: c.replyable,
|
Replyable: c.replyable,
|
||||||
Sensitive: sensitive,
|
Sensitive: sensitive,
|
||||||
Visibility: parsedVisibility,
|
Visibility: parsedVisibility,
|
||||||
|
Poll: nil,
|
||||||
|
AttachmentIDs: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
if numAttachmentIDs > 0 {
|
||||||
|
form.AttachmentIDs = attachmentIDs
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.addPoll {
|
||||||
|
if len(c.pollOptions) == 0 {
|
||||||
|
return Error{"no options were provided for this poll"}
|
||||||
|
}
|
||||||
|
|
||||||
|
poll := client.CreateStatusPollForm{
|
||||||
|
Options: c.pollOptions,
|
||||||
|
Multiple: c.pollAllowsMultipleChoices,
|
||||||
|
HideTotals: c.pollHidesVoteCounts,
|
||||||
|
ExpiresIn: int(c.pollExpiresIn.Duration.Seconds()),
|
||||||
|
}
|
||||||
|
|
||||||
|
form.Poll = &poll
|
||||||
}
|
}
|
||||||
|
|
||||||
status, err := gtsClient.CreateStatus(form)
|
status, err := gtsClient.CreateStatus(form)
|
||||||
|
@ -196,8 +227,67 @@ func (c *CreateExecutor) createStatus(gtsClient *client.Client) error {
|
||||||
return fmt.Errorf("unable to create the status: %w", err)
|
return fmt.Errorf("unable to create the status: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("Successfully created the following status:")
|
c.printer.PrintSuccess("Successfully created the status with ID: " + status.ID)
|
||||||
utilities.Display(status, *c.topLevelFlags.NoColor)
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CreateExecutor) createMediaAttachment(gtsClient *client.Client) error {
|
||||||
|
expectedNumValues := 1
|
||||||
|
|
||||||
|
if !c.mediaFiles.ExpectedLength(expectedNumValues) {
|
||||||
|
return UnexpectedNumValuesError{
|
||||||
|
name: "media files",
|
||||||
|
expected: expectedNumValues,
|
||||||
|
actual: len(c.mediaFiles),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
description := ""
|
||||||
|
if !c.mediaDescriptions.Empty() {
|
||||||
|
if !c.mediaDescriptions.ExpectedLength(expectedNumValues) {
|
||||||
|
return UnexpectedNumValuesError{
|
||||||
|
name: "media descriptions",
|
||||||
|
expected: expectedNumValues,
|
||||||
|
actual: len(c.mediaDescriptions),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
description, err = utilities.ReadContents(c.mediaDescriptions[0])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"unable to read the contents from %s: %w",
|
||||||
|
c.mediaDescriptions[0],
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
focus := ""
|
||||||
|
if !c.mediaFocusValues.Empty() {
|
||||||
|
if !c.mediaFocusValues.ExpectedLength(expectedNumValues) {
|
||||||
|
return UnexpectedNumValuesError{
|
||||||
|
name: "media focus values",
|
||||||
|
expected: expectedNumValues,
|
||||||
|
actual: len(c.mediaFocusValues),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
focus = c.mediaFocusValues[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
attachment, err := gtsClient.CreateMediaAttachment(
|
||||||
|
c.mediaFiles[0],
|
||||||
|
description,
|
||||||
|
focus,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to create the media attachment: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.printer.PrintSuccess("Successfully created the media attachment with ID: " + attachment.ID)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,38 +1,13 @@
|
||||||
// SPDX-FileCopyrightText: 2024 Dan Anglin <d.n.i.anglin@gmail.com>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
package executor
|
package executor
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"flag"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
"codeflow.dananglin.me.uk/apollo/enbas/internal/client"
|
"codeflow.dananglin.me.uk/apollo/enbas/internal/client"
|
||||||
|
"codeflow.dananglin.me.uk/apollo/enbas/internal/utilities"
|
||||||
)
|
)
|
||||||
|
|
||||||
type DeleteExecutor struct {
|
|
||||||
*flag.FlagSet
|
|
||||||
|
|
||||||
topLevelFlags TopLevelFlags
|
|
||||||
resourceType string
|
|
||||||
listID string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewDeleteExecutor(tlf TopLevelFlags, name, summary string) *DeleteExecutor {
|
|
||||||
deleteExe := DeleteExecutor{
|
|
||||||
FlagSet: flag.NewFlagSet(name, flag.ExitOnError),
|
|
||||||
topLevelFlags: tlf,
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteExe.StringVar(&deleteExe.resourceType, flagType, "", "specify the type of resource to delete")
|
|
||||||
deleteExe.StringVar(&deleteExe.listID, flagListID, "", "specify the ID of the list to delete")
|
|
||||||
|
|
||||||
deleteExe.Usage = commandUsageFunc(name, summary, deleteExe.FlagSet)
|
|
||||||
|
|
||||||
return &deleteExe
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DeleteExecutor) Execute() error {
|
func (d *DeleteExecutor) Execute() error {
|
||||||
if d.resourceType == "" {
|
if d.resourceType == "" {
|
||||||
return FlagNotSetError{flagText: flagType}
|
return FlagNotSetError{flagText: flagType}
|
||||||
|
@ -40,6 +15,7 @@ func (d *DeleteExecutor) Execute() error {
|
||||||
|
|
||||||
funcMap := map[string]func(*client.Client) error{
|
funcMap := map[string]func(*client.Client) error{
|
||||||
resourceList: d.deleteList,
|
resourceList: d.deleteList,
|
||||||
|
resourceStatus: d.deleteStatus,
|
||||||
}
|
}
|
||||||
|
|
||||||
doFunc, ok := funcMap[d.resourceType]
|
doFunc, ok := funcMap[d.resourceType]
|
||||||
|
@ -47,7 +23,7 @@ func (d *DeleteExecutor) Execute() error {
|
||||||
return UnsupportedTypeError{resourceType: d.resourceType}
|
return UnsupportedTypeError{resourceType: d.resourceType}
|
||||||
}
|
}
|
||||||
|
|
||||||
gtsClient, err := client.NewClientFromConfig(d.topLevelFlags.ConfigDir)
|
gtsClient, err := client.NewClientFromFile(d.config.CredentialsFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to create the GoToSocial client: %w", err)
|
return fmt.Errorf("unable to create the GoToSocial client: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -57,14 +33,68 @@ func (d *DeleteExecutor) Execute() error {
|
||||||
|
|
||||||
func (d *DeleteExecutor) deleteList(gtsClient *client.Client) error {
|
func (d *DeleteExecutor) deleteList(gtsClient *client.Client) error {
|
||||||
if d.listID == "" {
|
if d.listID == "" {
|
||||||
return FlagNotSetError{flagText: flagListID}
|
return MissingIDError{
|
||||||
|
resource: resourceList,
|
||||||
|
action: "delete",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := gtsClient.DeleteList(d.listID); err != nil {
|
if err := gtsClient.DeleteList(d.listID); err != nil {
|
||||||
return fmt.Errorf("unable to delete the list: %w", err)
|
return fmt.Errorf("unable to delete the list: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("The list was successfully deleted.")
|
d.printer.PrintSuccess("The list was successfully deleted.")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DeleteExecutor) deleteStatus(gtsClient *client.Client) error {
|
||||||
|
if d.statusID == "" {
|
||||||
|
return MissingIDError{
|
||||||
|
resource: resourceStatus,
|
||||||
|
action: "delete",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
status, err := gtsClient.GetStatus(d.statusID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to get the status: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
myAccountID, err := getAccountID(gtsClient, true, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to get your account ID: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if status.Account.ID != myAccountID {
|
||||||
|
return Error{"unable to delete the status because the status does not belong to you"}
|
||||||
|
}
|
||||||
|
|
||||||
|
text, err := gtsClient.DeleteStatus(d.statusID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to delete the status: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
d.printer.PrintSuccess("The status was successfully deleted.")
|
||||||
|
|
||||||
|
if d.saveText {
|
||||||
|
cacheDir, err := utilities.CalculateStatusesCacheDir(d.config.CacheDirectory, gtsClient.Authentication.Instance)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to get the cache directory for the status: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := utilities.EnsureDirectory(cacheDir); err != nil {
|
||||||
|
return fmt.Errorf("unable to ensure the existence of the directory %q: %w", cacheDir, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
path := filepath.Join(cacheDir, fmt.Sprintf("deleted-status-%s.txt", d.statusID))
|
||||||
|
|
||||||
|
if err := utilities.SaveTextToFile(path, text); err != nil {
|
||||||
|
return fmt.Errorf("unable to save the text to %q: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
d.printer.PrintSuccess("The text was successfully saved to '" + path + "'.")
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,6 @@
|
||||||
// SPDX-FileCopyrightText: 2024 Dan Anglin <d.n.i.anglin@gmail.com>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
package executor
|
package executor
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"flag"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"codeflow.dananglin.me.uk/apollo/enbas/internal/client"
|
"codeflow.dananglin.me.uk/apollo/enbas/internal/client"
|
||||||
|
@ -13,32 +8,6 @@ import (
|
||||||
"codeflow.dananglin.me.uk/apollo/enbas/internal/utilities"
|
"codeflow.dananglin.me.uk/apollo/enbas/internal/utilities"
|
||||||
)
|
)
|
||||||
|
|
||||||
type EditExecutor struct {
|
|
||||||
*flag.FlagSet
|
|
||||||
|
|
||||||
topLevelFlags TopLevelFlags
|
|
||||||
resourceType string
|
|
||||||
listID string
|
|
||||||
listTitle string
|
|
||||||
listRepliesPolicy string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewEditExecutor(tlf TopLevelFlags, name, summary string) *EditExecutor {
|
|
||||||
editExe := EditExecutor{
|
|
||||||
FlagSet: flag.NewFlagSet(name, flag.ExitOnError),
|
|
||||||
topLevelFlags: tlf,
|
|
||||||
}
|
|
||||||
|
|
||||||
editExe.StringVar(&editExe.resourceType, flagType, "", "specify the type of resource to update")
|
|
||||||
editExe.StringVar(&editExe.listID, flagListID, "", "specify the ID of the list to update")
|
|
||||||
editExe.StringVar(&editExe.listTitle, flagListTitle, "", "specify the title of the list")
|
|
||||||
editExe.StringVar(&editExe.listRepliesPolicy, flagListRepliesPolicy, "", "specify the policy of the replies for this list (valid values are followed, list and none)")
|
|
||||||
|
|
||||||
editExe.Usage = commandUsageFunc(name, summary, editExe.FlagSet)
|
|
||||||
|
|
||||||
return &editExe
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *EditExecutor) Execute() error {
|
func (e *EditExecutor) Execute() error {
|
||||||
if e.resourceType == "" {
|
if e.resourceType == "" {
|
||||||
return FlagNotSetError{flagText: flagType}
|
return FlagNotSetError{flagText: flagType}
|
||||||
|
@ -46,6 +15,7 @@ func (e *EditExecutor) Execute() error {
|
||||||
|
|
||||||
funcMap := map[string]func(*client.Client) error{
|
funcMap := map[string]func(*client.Client) error{
|
||||||
resourceList: e.editList,
|
resourceList: e.editList,
|
||||||
|
resourceMediaAttachment: e.editMediaAttachment,
|
||||||
}
|
}
|
||||||
|
|
||||||
doFunc, ok := funcMap[e.resourceType]
|
doFunc, ok := funcMap[e.resourceType]
|
||||||
|
@ -53,7 +23,7 @@ func (e *EditExecutor) Execute() error {
|
||||||
return UnsupportedTypeError{resourceType: e.resourceType}
|
return UnsupportedTypeError{resourceType: e.resourceType}
|
||||||
}
|
}
|
||||||
|
|
||||||
gtsClient, err := client.NewClientFromConfig(e.topLevelFlags.ConfigDir)
|
gtsClient, err := client.NewClientFromFile(e.config.CredentialsFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to create the GoToSocial client: %w", err)
|
return fmt.Errorf("unable to create the GoToSocial client: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -63,7 +33,10 @@ func (e *EditExecutor) Execute() error {
|
||||||
|
|
||||||
func (e *EditExecutor) editList(gtsClient *client.Client) error {
|
func (e *EditExecutor) editList(gtsClient *client.Client) error {
|
||||||
if e.listID == "" {
|
if e.listID == "" {
|
||||||
return FlagNotSetError{flagText: flagListID}
|
return MissingIDError{
|
||||||
|
resource: resourceList,
|
||||||
|
action: "edit",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
list, err := gtsClient.GetList(e.listID)
|
list, err := gtsClient.GetList(e.listID)
|
||||||
|
@ -89,8 +62,68 @@ func (e *EditExecutor) editList(gtsClient *client.Client) error {
|
||||||
return fmt.Errorf("unable to update the list: %w", err)
|
return fmt.Errorf("unable to update the list: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("Successfully updated the list.")
|
e.printer.PrintSuccess("Successfully edited the list.")
|
||||||
utilities.Display(updatedList, *e.topLevelFlags.NoColor)
|
e.printer.PrintList(updatedList)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *EditExecutor) editMediaAttachment(gtsClient *client.Client) error {
|
||||||
|
expectedNumValues := 1
|
||||||
|
|
||||||
|
if !e.attachmentIDs.ExpectedLength(expectedNumValues) {
|
||||||
|
return UnexpectedNumValuesError{
|
||||||
|
name: "media attachment IDs",
|
||||||
|
expected: expectedNumValues,
|
||||||
|
actual: len(e.attachmentIDs),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
attachment, err := gtsClient.GetMediaAttachment(e.attachmentIDs[0])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to get the media attachment: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
description := attachment.Description
|
||||||
|
if !e.mediaDescriptions.Empty() {
|
||||||
|
if !e.mediaDescriptions.ExpectedLength(expectedNumValues) {
|
||||||
|
return UnexpectedNumValuesError{
|
||||||
|
name: "media description",
|
||||||
|
expected: expectedNumValues,
|
||||||
|
actual: len(e.mediaDescriptions),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
description, err = utilities.ReadContents(e.mediaDescriptions[0])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"unable to read the contents from %s: %w",
|
||||||
|
e.mediaDescriptions[0],
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
focus := fmt.Sprintf("%f,%f", attachment.Meta.Focus.X, attachment.Meta.Focus.Y)
|
||||||
|
if !e.mediaFocusValues.Empty() {
|
||||||
|
if !e.mediaFocusValues.ExpectedLength(expectedNumValues) {
|
||||||
|
return UnexpectedNumValuesError{
|
||||||
|
name: "media focus values",
|
||||||
|
expected: expectedNumValues,
|
||||||
|
actual: len(e.mediaFocusValues),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
focus = e.mediaFocusValues[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = gtsClient.UpdateMediaAttachment(attachment.ID, description, focus); err != nil {
|
||||||
|
return fmt.Errorf("unable to update the media attachment: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
e.printer.PrintSuccess("Successfully edited the media attachment.")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,15 @@
|
||||||
// SPDX-FileCopyrightText: 2024 Dan Anglin <d.n.i.anglin@gmail.com>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
package executor
|
package executor
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
type Error struct {
|
||||||
|
message string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e Error) Error() string {
|
||||||
|
return e.message
|
||||||
|
}
|
||||||
|
|
||||||
type FlagNotSetError struct {
|
type FlagNotSetError struct {
|
||||||
flagText string
|
flagText string
|
||||||
}
|
}
|
||||||
|
@ -27,34 +33,96 @@ func (e NoAccountSpecifiedError) Error() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
type UnsupportedAddOperationError struct {
|
type UnsupportedAddOperationError struct {
|
||||||
ResourceType string
|
resourceType string
|
||||||
AddToResourceType string
|
addToResourceType string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e UnsupportedAddOperationError) Error() string {
|
func (e UnsupportedAddOperationError) Error() string {
|
||||||
return "adding '" + e.ResourceType + "' to '" + e.AddToResourceType + "' is not supported"
|
return "adding '" +
|
||||||
|
e.resourceType +
|
||||||
|
"' to '" +
|
||||||
|
e.addToResourceType +
|
||||||
|
"' is not supported"
|
||||||
}
|
}
|
||||||
|
|
||||||
type UnsupportedRemoveOperationError struct {
|
type UnsupportedRemoveOperationError struct {
|
||||||
ResourceType string
|
resourceType string
|
||||||
RemoveFromResourceType string
|
removeFromResourceType string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e UnsupportedRemoveOperationError) Error() string {
|
func (e UnsupportedRemoveOperationError) Error() string {
|
||||||
return "removing '" + e.ResourceType + "' from '" + e.RemoveFromResourceType + "' is not supported"
|
return "removing '" +
|
||||||
|
e.resourceType +
|
||||||
|
"' from '" +
|
||||||
|
e.removeFromResourceType +
|
||||||
|
"' is not supported"
|
||||||
}
|
}
|
||||||
|
|
||||||
type EmptyContentError struct {
|
type UnsupportedShowOperationError struct {
|
||||||
ResourceType string
|
resourceType string
|
||||||
Hint string
|
showFromResourceType string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e EmptyContentError) Error() string {
|
func (e UnsupportedShowOperationError) Error() string {
|
||||||
message := "the content of this " + e.ResourceType + " should not be empty"
|
return "showing '" +
|
||||||
|
e.resourceType +
|
||||||
if e.Hint != "" {
|
"' from '" +
|
||||||
message += ", " + e.Hint
|
e.showFromResourceType +
|
||||||
|
"' is not supported"
|
||||||
}
|
}
|
||||||
|
|
||||||
return message
|
type UnknownCommandError struct {
|
||||||
|
command string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e UnknownCommandError) Error() string {
|
||||||
|
return "unknown command '" + e.command + "'"
|
||||||
|
}
|
||||||
|
|
||||||
|
type NotFollowingError struct {
|
||||||
|
account string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e NotFollowingError) Error() string {
|
||||||
|
return "you are not following " + e.account
|
||||||
|
}
|
||||||
|
|
||||||
|
type MismatchedNumMediaValuesError struct {
|
||||||
|
valueType string
|
||||||
|
numValues int
|
||||||
|
numMediaFiles int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e MismatchedNumMediaValuesError) Error() string {
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"unexpected number of %s: received %d media files but got %d %s",
|
||||||
|
e.valueType,
|
||||||
|
e.numMediaFiles,
|
||||||
|
e.numValues,
|
||||||
|
e.valueType,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type UnexpectedNumValuesError struct {
|
||||||
|
name string
|
||||||
|
actual int
|
||||||
|
expected int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e UnexpectedNumValuesError) Error() string {
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"received an unexpected number of %s: received %d, expected %d",
|
||||||
|
e.name,
|
||||||
|
e.actual,
|
||||||
|
e.expected,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type MissingIDError struct {
|
||||||
|
resource string
|
||||||
|
action string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e MissingIDError) Error() string {
|
||||||
|
return "please provide the ID of the " + e.resource + " you want to " + e.action
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,26 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2024 Dan Anglin <d.n.i.anglin@gmail.com>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
package executor
|
|
||||||
|
|
||||||
import "strings"
|
|
||||||
|
|
||||||
type AccountNames []string
|
|
||||||
|
|
||||||
func (a *AccountNames) String() string {
|
|
||||||
return strings.Join(*a, ", ")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *AccountNames) Set(value string) error {
|
|
||||||
if len(value) > 0 {
|
|
||||||
*a = append(*a, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type TopLevelFlags struct {
|
|
||||||
ConfigDir string
|
|
||||||
NoColor *bool
|
|
||||||
}
|
|
|
@ -1,55 +1,22 @@
|
||||||
// SPDX-FileCopyrightText: 2024 Dan Anglin <d.n.i.anglin@gmail.com>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
package executor
|
package executor
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"flag"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"codeflow.dananglin.me.uk/apollo/enbas/internal/client"
|
"codeflow.dananglin.me.uk/apollo/enbas/internal/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
type FollowExecutor struct {
|
func (f *FollowExecutor) Execute() error {
|
||||||
*flag.FlagSet
|
|
||||||
|
|
||||||
topLevelFlags TopLevelFlags
|
|
||||||
resourceType string
|
|
||||||
accountName string
|
|
||||||
showReposts bool
|
|
||||||
notify bool
|
|
||||||
unfollow bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewFollowExecutor(tlf TopLevelFlags, name, summary string, unfollow bool) *FollowExecutor {
|
|
||||||
command := FollowExecutor{
|
|
||||||
FlagSet: flag.NewFlagSet(name, flag.ExitOnError),
|
|
||||||
unfollow: unfollow,
|
|
||||||
topLevelFlags: tlf,
|
|
||||||
}
|
|
||||||
|
|
||||||
command.StringVar(&command.resourceType, flagType, "", "specify the type of resource to follow")
|
|
||||||
command.StringVar(&command.accountName, flagAccountName, "", "specify the account name in full (username@domain)")
|
|
||||||
command.BoolVar(&command.showReposts, flagShowReposts, true, "show reposts from the account you want to follow")
|
|
||||||
command.BoolVar(&command.notify, flagNotify, false, "get notifications when the account you want to follow posts a status")
|
|
||||||
|
|
||||||
command.Usage = commandUsageFunc(name, summary, command.FlagSet)
|
|
||||||
|
|
||||||
return &command
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *FollowExecutor) Execute() error {
|
|
||||||
funcMap := map[string]func(*client.Client) error{
|
funcMap := map[string]func(*client.Client) error{
|
||||||
resourceAccount: c.followAccount,
|
resourceAccount: f.followAccount,
|
||||||
}
|
}
|
||||||
|
|
||||||
doFunc, ok := funcMap[c.resourceType]
|
doFunc, ok := funcMap[f.resourceType]
|
||||||
if !ok {
|
if !ok {
|
||||||
return UnsupportedTypeError{resourceType: c.resourceType}
|
return UnsupportedTypeError{resourceType: f.resourceType}
|
||||||
}
|
}
|
||||||
|
|
||||||
gtsClient, err := client.NewClientFromConfig(c.topLevelFlags.ConfigDir)
|
gtsClient, err := client.NewClientFromFile(f.config.CredentialsFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to create the GoToSocial client: %w", err)
|
return fmt.Errorf("unable to create the GoToSocial client: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -57,37 +24,23 @@ func (c *FollowExecutor) Execute() error {
|
||||||
return doFunc(gtsClient)
|
return doFunc(gtsClient)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *FollowExecutor) followAccount(gtsClient *client.Client) error {
|
func (f *FollowExecutor) followAccount(gtsClient *client.Client) error {
|
||||||
accountID, err := getAccountID(gtsClient, false, c.accountName, c.topLevelFlags.ConfigDir)
|
accountID, err := getAccountID(gtsClient, false, f.accountName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("received an error while getting the account ID: %w", err)
|
return fmt.Errorf("received an error while getting the account ID: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.unfollow {
|
|
||||||
return c.unfollowAccount(gtsClient, accountID)
|
|
||||||
}
|
|
||||||
|
|
||||||
form := client.FollowAccountForm{
|
form := client.FollowAccountForm{
|
||||||
AccountID: accountID,
|
AccountID: accountID,
|
||||||
ShowReposts: c.showReposts,
|
ShowReposts: f.showReposts,
|
||||||
Notify: c.notify,
|
Notify: f.notify,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := gtsClient.FollowAccount(form); err != nil {
|
if err := gtsClient.FollowAccount(form); err != nil {
|
||||||
return fmt.Errorf("unable to follow the account: %w", err)
|
return fmt.Errorf("unable to follow the account: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("The follow request was sent successfully.")
|
f.printer.PrintSuccess("Successfully sent the follow request.")
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *FollowExecutor) unfollowAccount(gtsClient *client.Client, accountID string) error {
|
|
||||||
if err := gtsClient.UnfollowAccount(accountID); err != nil {
|
|
||||||
return fmt.Errorf("unable to unfollow the account: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println("Successfully unfollowed the account.")
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
35
internal/executor/init.go
Normal file
35
internal/executor/init.go
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
package executor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"codeflow.dananglin.me.uk/apollo/enbas/internal/config"
|
||||||
|
"codeflow.dananglin.me.uk/apollo/enbas/internal/utilities"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (i *InitExecutor) Execute() error {
|
||||||
|
if err := utilities.EnsureDirectory(i.configDir); err != nil {
|
||||||
|
return fmt.Errorf("unable to ensure that the configuration directory is present: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
i.printer.PrintSuccess("The configuration directory is present.")
|
||||||
|
|
||||||
|
fileExists, err := config.FileExists(i.configDir)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to check if the config file exists: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if fileExists {
|
||||||
|
i.printer.PrintInfo("The configuration file is already present in " + i.configDir + "\n")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := config.SaveDefaultConfigToFile(i.configDir); err != nil {
|
||||||
|
return fmt.Errorf("unable to create a new configuration file in %s: %w", i.configDir, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
i.printer.PrintSuccess("Successfully created a new configuration file in " + i.configDir)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -1,11 +1,6 @@
|
||||||
// SPDX-FileCopyrightText: 2024 Dan Anglin <d.n.i.anglin@gmail.com>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
package executor
|
package executor
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"flag"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
@ -14,35 +9,14 @@ import (
|
||||||
"codeflow.dananglin.me.uk/apollo/enbas/internal/utilities"
|
"codeflow.dananglin.me.uk/apollo/enbas/internal/utilities"
|
||||||
)
|
)
|
||||||
|
|
||||||
type LoginExecutor struct {
|
func (l *LoginExecutor) Execute() error {
|
||||||
*flag.FlagSet
|
|
||||||
|
|
||||||
topLevelFlags TopLevelFlags
|
|
||||||
instance string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewLoginExecutor(tlf TopLevelFlags, name, summary string) *LoginExecutor {
|
|
||||||
command := LoginExecutor{
|
|
||||||
FlagSet: flag.NewFlagSet(name, flag.ExitOnError),
|
|
||||||
topLevelFlags: tlf,
|
|
||||||
instance: "",
|
|
||||||
}
|
|
||||||
|
|
||||||
command.StringVar(&command.instance, flagInstance, "", "specify the instance that you want to login to.")
|
|
||||||
|
|
||||||
command.Usage = commandUsageFunc(name, summary, command.FlagSet)
|
|
||||||
|
|
||||||
return &command
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *LoginExecutor) Execute() error {
|
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
if c.instance == "" {
|
if l.instance == "" {
|
||||||
return FlagNotSetError{flagText: flagInstance}
|
return Error{"please specify the instance that you want to log into"}
|
||||||
}
|
}
|
||||||
|
|
||||||
instance := c.instance
|
instance := l.instance
|
||||||
|
|
||||||
if !strings.HasPrefix(instance, "https") || !strings.HasPrefix(instance, "http") {
|
if !strings.HasPrefix(instance, "https") || !strings.HasPrefix(instance, "http") {
|
||||||
instance = "https://" + instance
|
instance = "https://" + instance
|
||||||
|
@ -64,23 +38,19 @@ func (c *LoginExecutor) Execute() error {
|
||||||
|
|
||||||
consentPageURL := gtsClient.AuthCodeURL()
|
consentPageURL := gtsClient.AuthCodeURL()
|
||||||
|
|
||||||
utilities.OpenLink(consentPageURL)
|
_ = utilities.OpenLink(l.config.Integrations.Browser, consentPageURL)
|
||||||
|
|
||||||
consentMessageFormat := `
|
var builder strings.Builder
|
||||||
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
|
builder.WriteString("\nYou'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.")
|
||||||
|
builder.WriteString("\nYour browser may have opened the link to the consent page already. If not, please copy and paste the link below to your browser:")
|
||||||
|
builder.WriteString("\n\n" + consentPageURL)
|
||||||
|
builder.WriteString("\n\n" + "Once you have the code please copy and paste it below.")
|
||||||
|
builder.WriteString("\n" + "Out-of-band token: ")
|
||||||
|
|
||||||
Once you have the code please copy and paste it below.
|
l.printer.PrintInfo(builder.String())
|
||||||
|
|
||||||
`
|
|
||||||
|
|
||||||
fmt.Printf(consentMessageFormat, consentPageURL)
|
|
||||||
|
|
||||||
var code string
|
var code string
|
||||||
fmt.Print("Out-of-band token: ")
|
|
||||||
|
|
||||||
if _, err := fmt.Scanln(&code); err != nil {
|
if _, err := fmt.Scanln(&code); err != nil {
|
||||||
return fmt.Errorf("failed to read access code: %w", err)
|
return fmt.Errorf("failed to read access code: %w", err)
|
||||||
|
@ -95,12 +65,12 @@ Once you have the code please copy and paste it below.
|
||||||
return fmt.Errorf("unable to verify the credentials: %w", err)
|
return fmt.Errorf("unable to verify the credentials: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
loginName, err := config.SaveCredentials(c.topLevelFlags.ConfigDir, account.Username, gtsClient.Authentication)
|
loginName, err := config.SaveCredentials(l.config.CredentialsFile, account.Username, gtsClient.Authentication)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to save the authentication details: %w", err)
|
return fmt.Errorf("unable to save the authentication details: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("Successfully logged into %s\n", loginName)
|
l.printer.PrintSuccess("You have successfully logged as " + loginName + ".")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
91
internal/executor/mute.go
Normal file
91
internal/executor/mute.go
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
package executor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"codeflow.dananglin.me.uk/apollo/enbas/internal/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (m *MuteExecutor) Execute() error {
|
||||||
|
funcMap := map[string]func(*client.Client) error{
|
||||||
|
resourceAccount: m.muteAccount,
|
||||||
|
resourceStatus: m.muteStatus,
|
||||||
|
}
|
||||||
|
|
||||||
|
doFunc, ok := funcMap[m.resourceType]
|
||||||
|
if !ok {
|
||||||
|
return UnsupportedTypeError{resourceType: m.resourceType}
|
||||||
|
}
|
||||||
|
|
||||||
|
gtsClient, err := client.NewClientFromFile(m.config.CredentialsFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to create the GoToSocial client: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return doFunc(gtsClient)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MuteExecutor) muteAccount(gtsClient *client.Client) error {
|
||||||
|
accountID, err := getAccountID(gtsClient, false, m.accountName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("received an error while getting the account ID: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
form := client.MuteAccountForm{
|
||||||
|
Notifications: m.muteNotifications,
|
||||||
|
Duration: int(m.muteDuration.Duration.Seconds()),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := gtsClient.MuteAccount(accountID, form); err != nil {
|
||||||
|
return fmt.Errorf("unable to mute the account: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.printer.PrintSuccess("Successfully muted the account.")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MuteExecutor) muteStatus(gtsClient *client.Client) error {
|
||||||
|
if m.statusID == "" {
|
||||||
|
return MissingIDError{
|
||||||
|
resource: resourceStatus,
|
||||||
|
action: "mute",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
status, err := gtsClient.GetStatus(m.statusID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to retrieve the status: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
myAccountID, err := getAccountID(gtsClient, true, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to get your account ID: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
canMute := false
|
||||||
|
|
||||||
|
if status.Account.ID == myAccountID {
|
||||||
|
canMute = true
|
||||||
|
} else {
|
||||||
|
for _, mention := range status.Mentions {
|
||||||
|
if mention.ID == myAccountID {
|
||||||
|
canMute = true
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !canMute {
|
||||||
|
return Error{"unable to mute the status because the status does not belong to you nor are you mentioned in it"}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := gtsClient.MuteStatus(m.statusID); err != nil {
|
||||||
|
return fmt.Errorf("unable to mute the status: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.printer.PrintSuccess("Successfully muted the status.")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
40
internal/executor/reject.go
Normal file
40
internal/executor/reject.go
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
package executor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"codeflow.dananglin.me.uk/apollo/enbas/internal/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (r *RejectExecutor) Execute() error {
|
||||||
|
funcMap := map[string]func(*client.Client) error{
|
||||||
|
resourceFollowRequest: r.rejectFollowRequest,
|
||||||
|
}
|
||||||
|
|
||||||
|
doFunc, ok := funcMap[r.resourceType]
|
||||||
|
if !ok {
|
||||||
|
return UnsupportedTypeError{resourceType: r.resourceType}
|
||||||
|
}
|
||||||
|
|
||||||
|
gtsClient, err := client.NewClientFromFile(r.config.CredentialsFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to create the GoToSocial client: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return doFunc(gtsClient)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RejectExecutor) rejectFollowRequest(gtsClient *client.Client) error {
|
||||||
|
accountID, err := getAccountID(gtsClient, false, r.accountName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("received an error while getting the account ID: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := gtsClient.RejectFollowRequest(accountID); err != nil {
|
||||||
|
return fmt.Errorf("unable to reject the follow request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.printer.PrintSuccess("Successfully rejected the follow request.")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -1,47 +1,11 @@
|
||||||
// SPDX-FileCopyrightText: 2024 Dan Anglin <d.n.i.anglin@gmail.com>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
package executor
|
package executor
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"flag"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"codeflow.dananglin.me.uk/apollo/enbas/internal/client"
|
"codeflow.dananglin.me.uk/apollo/enbas/internal/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
type RemoveExecutor struct {
|
|
||||||
*flag.FlagSet
|
|
||||||
|
|
||||||
topLevelFlags TopLevelFlags
|
|
||||||
resourceType string
|
|
||||||
fromResourceType string
|
|
||||||
listID string
|
|
||||||
statusID string
|
|
||||||
accountNames AccountNames
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewRemoveExecutor(tlf TopLevelFlags, name, summary string) *RemoveExecutor {
|
|
||||||
emptyArr := make([]string, 0, 3)
|
|
||||||
|
|
||||||
removeExe := RemoveExecutor{
|
|
||||||
FlagSet: flag.NewFlagSet(name, flag.ExitOnError),
|
|
||||||
accountNames: AccountNames(emptyArr),
|
|
||||||
topLevelFlags: tlf,
|
|
||||||
}
|
|
||||||
|
|
||||||
removeExe.StringVar(&removeExe.resourceType, flagType, "", "specify the resource type to remove (e.g. account, note)")
|
|
||||||
removeExe.StringVar(&removeExe.fromResourceType, flagFrom, "", "specify the resource type to remove from (e.g. list, account, etc)")
|
|
||||||
removeExe.StringVar(&removeExe.listID, flagListID, "", "the ID of the list to remove from")
|
|
||||||
removeExe.StringVar(&removeExe.statusID, flagStatusID, "", "the ID of the status")
|
|
||||||
removeExe.Var(&removeExe.accountNames, flagAccountName, "the name of the account to remove from the resource")
|
|
||||||
|
|
||||||
removeExe.Usage = commandUsageFunc(name, summary, removeExe.FlagSet)
|
|
||||||
|
|
||||||
return &removeExe
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RemoveExecutor) Execute() error {
|
func (r *RemoveExecutor) Execute() error {
|
||||||
if r.fromResourceType == "" {
|
if r.fromResourceType == "" {
|
||||||
return FlagNotSetError{flagText: flagFrom}
|
return FlagNotSetError{flagText: flagFrom}
|
||||||
|
@ -51,6 +15,7 @@ func (r *RemoveExecutor) Execute() error {
|
||||||
resourceList: r.removeFromList,
|
resourceList: r.removeFromList,
|
||||||
resourceAccount: r.removeFromAccount,
|
resourceAccount: r.removeFromAccount,
|
||||||
resourceBookmarks: r.removeFromBookmarks,
|
resourceBookmarks: r.removeFromBookmarks,
|
||||||
|
resourceStatus: r.removeFromStatus,
|
||||||
}
|
}
|
||||||
|
|
||||||
doFunc, ok := funcMap[r.fromResourceType]
|
doFunc, ok := funcMap[r.fromResourceType]
|
||||||
|
@ -58,7 +23,7 @@ func (r *RemoveExecutor) Execute() error {
|
||||||
return UnsupportedTypeError{resourceType: r.fromResourceType}
|
return UnsupportedTypeError{resourceType: r.fromResourceType}
|
||||||
}
|
}
|
||||||
|
|
||||||
gtsClient, err := client.NewClientFromConfig(r.topLevelFlags.ConfigDir)
|
gtsClient, err := client.NewClientFromFile(r.config.CredentialsFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to create the GoToSocial client: %w", err)
|
return fmt.Errorf("unable to create the GoToSocial client: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -74,8 +39,8 @@ func (r *RemoveExecutor) removeFromList(gtsClient *client.Client) error {
|
||||||
doFunc, ok := funcMap[r.resourceType]
|
doFunc, ok := funcMap[r.resourceType]
|
||||||
if !ok {
|
if !ok {
|
||||||
return UnsupportedRemoveOperationError{
|
return UnsupportedRemoveOperationError{
|
||||||
ResourceType: r.resourceType,
|
resourceType: r.resourceType,
|
||||||
RemoveFromResourceType: r.fromResourceType,
|
removeFromResourceType: r.fromResourceType,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -84,29 +49,32 @@ func (r *RemoveExecutor) removeFromList(gtsClient *client.Client) error {
|
||||||
|
|
||||||
func (r *RemoveExecutor) removeAccountsFromList(gtsClient *client.Client) error {
|
func (r *RemoveExecutor) removeAccountsFromList(gtsClient *client.Client) error {
|
||||||
if r.listID == "" {
|
if r.listID == "" {
|
||||||
return FlagNotSetError{flagText: flagListID}
|
return MissingIDError{
|
||||||
|
resource: resourceList,
|
||||||
|
action: "remove from",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(r.accountNames) == 0 {
|
if r.accountNames.Empty() {
|
||||||
return NoAccountSpecifiedError{}
|
return NoAccountSpecifiedError{}
|
||||||
}
|
}
|
||||||
|
|
||||||
accountIDs := make([]string, len(r.accountNames))
|
accounts, err := getOtherAccounts(gtsClient, r.accountNames)
|
||||||
|
|
||||||
for ind := range r.accountNames {
|
|
||||||
accountID, err := getTheirAccountID(gtsClient, r.accountNames[ind])
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to get the account ID for %s: %w", r.accountNames[ind], err)
|
return fmt.Errorf("unable to get the accounts: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
accountIDs[ind] = accountID
|
accountIDs := make([]string, len(accounts))
|
||||||
|
|
||||||
|
for ind := range accounts {
|
||||||
|
accountIDs[ind] = accounts[ind].ID
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := gtsClient.RemoveAccountsFromList(r.listID, accountIDs); err != nil {
|
if err := gtsClient.RemoveAccountsFromList(r.listID, accountIDs); err != nil {
|
||||||
return fmt.Errorf("unable to remove the accounts from the list: %w", err)
|
return fmt.Errorf("unable to remove the accounts from the list: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("Successfully removed the account(s) from the list.")
|
r.printer.PrintSuccess("Successfully removed the account(s) from the list.")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -119,8 +87,8 @@ func (r *RemoveExecutor) removeFromAccount(gtsClient *client.Client) error {
|
||||||
doFunc, ok := funcMap[r.resourceType]
|
doFunc, ok := funcMap[r.resourceType]
|
||||||
if !ok {
|
if !ok {
|
||||||
return UnsupportedRemoveOperationError{
|
return UnsupportedRemoveOperationError{
|
||||||
ResourceType: r.resourceType,
|
resourceType: r.resourceType,
|
||||||
RemoveFromResourceType: r.fromResourceType,
|
removeFromResourceType: r.fromResourceType,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -128,11 +96,7 @@ func (r *RemoveExecutor) removeFromAccount(gtsClient *client.Client) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RemoveExecutor) removeNoteFromAccount(gtsClient *client.Client) error {
|
func (r *RemoveExecutor) removeNoteFromAccount(gtsClient *client.Client) error {
|
||||||
if len(r.accountNames) != 1 {
|
accountID, err := getAccountID(gtsClient, false, r.accountNames)
|
||||||
return fmt.Errorf("unexpected number of accounts specified: want 1, got %d", len(r.accountNames))
|
|
||||||
}
|
|
||||||
|
|
||||||
accountID, err := getAccountID(gtsClient, false, r.accountNames[0], r.topLevelFlags.ConfigDir)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("received an error while getting the account ID: %w", err)
|
return fmt.Errorf("received an error while getting the account ID: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -141,7 +105,7 @@ func (r *RemoveExecutor) removeNoteFromAccount(gtsClient *client.Client) error {
|
||||||
return fmt.Errorf("unable to remove the private note from the account: %w", err)
|
return fmt.Errorf("unable to remove the private note from the account: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("Successfully removed the private note from the account.")
|
r.printer.PrintSuccess("Successfully removed the private note from the account.")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -154,8 +118,8 @@ func (r *RemoveExecutor) removeFromBookmarks(gtsClient *client.Client) error {
|
||||||
doFunc, ok := funcMap[r.resourceType]
|
doFunc, ok := funcMap[r.resourceType]
|
||||||
if !ok {
|
if !ok {
|
||||||
return UnsupportedRemoveOperationError{
|
return UnsupportedRemoveOperationError{
|
||||||
ResourceType: r.resourceType,
|
resourceType: r.resourceType,
|
||||||
RemoveFromResourceType: r.fromResourceType,
|
removeFromResourceType: r.fromResourceType,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -164,14 +128,62 @@ func (r *RemoveExecutor) removeFromBookmarks(gtsClient *client.Client) error {
|
||||||
|
|
||||||
func (r *RemoveExecutor) removeStatusFromBookmarks(gtsClient *client.Client) error {
|
func (r *RemoveExecutor) removeStatusFromBookmarks(gtsClient *client.Client) error {
|
||||||
if r.statusID == "" {
|
if r.statusID == "" {
|
||||||
return FlagNotSetError{flagText: flagStatusID}
|
return MissingIDError{
|
||||||
|
resource: resourceStatus,
|
||||||
|
action: "remove",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := gtsClient.RemoveStatusFromBookmarks(r.statusID); err != nil {
|
if err := gtsClient.RemoveStatusFromBookmarks(r.statusID); err != nil {
|
||||||
return fmt.Errorf("unable to remove the status from your bookmarks: %w", err)
|
return fmt.Errorf("unable to remove the status from your bookmarks: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("Successfully removed the status from your bookmarks.")
|
r.printer.PrintSuccess("Successfully removed the status from your bookmarks.")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RemoveExecutor) removeFromStatus(gtsClient *client.Client) error {
|
||||||
|
if r.statusID == "" {
|
||||||
|
return MissingIDError{
|
||||||
|
resource: resourceStatus,
|
||||||
|
action: "remove from",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
funcMap := map[string]func(*client.Client) error{
|
||||||
|
resourceStar: r.removeStarFromStatus,
|
||||||
|
resourceLike: r.removeStarFromStatus,
|
||||||
|
resourceBoost: r.removeBoostFromStatus,
|
||||||
|
}
|
||||||
|
|
||||||
|
doFunc, ok := funcMap[r.resourceType]
|
||||||
|
if !ok {
|
||||||
|
return UnsupportedRemoveOperationError{
|
||||||
|
resourceType: r.resourceType,
|
||||||
|
removeFromResourceType: r.fromResourceType,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return doFunc(gtsClient)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RemoveExecutor) removeStarFromStatus(gtsClient *client.Client) error {
|
||||||
|
if err := gtsClient.UnlikeStatus(r.statusID); err != nil {
|
||||||
|
return fmt.Errorf("unable to remove the %s from the status: %w", r.resourceType, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.printer.PrintSuccess("Successfully removed the " + r.resourceType + " from the status.")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RemoveExecutor) removeBoostFromStatus(gtsClient *client.Client) error {
|
||||||
|
if err := gtsClient.UnreblogStatus(r.statusID); err != nil {
|
||||||
|
return fmt.Errorf("unable to remove the boost from the status: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.printer.PrintSuccess("Successfully removed the boost from the status.")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,57 +1,14 @@
|
||||||
// SPDX-FileCopyrightText: 2024 Dan Anglin <d.n.i.anglin@gmail.com>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
package executor
|
package executor
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"flag"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"codeflow.dananglin.me.uk/apollo/enbas/internal/client"
|
"codeflow.dananglin.me.uk/apollo/enbas/internal/client"
|
||||||
|
"codeflow.dananglin.me.uk/apollo/enbas/internal/media"
|
||||||
"codeflow.dananglin.me.uk/apollo/enbas/internal/model"
|
"codeflow.dananglin.me.uk/apollo/enbas/internal/model"
|
||||||
"codeflow.dananglin.me.uk/apollo/enbas/internal/utilities"
|
"codeflow.dananglin.me.uk/apollo/enbas/internal/utilities"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ShowExecutor struct {
|
|
||||||
*flag.FlagSet
|
|
||||||
topLevelFlags TopLevelFlags
|
|
||||||
myAccount bool
|
|
||||||
skipAccountRelationship bool
|
|
||||||
showUserPreferences bool
|
|
||||||
showInBrowser bool
|
|
||||||
resourceType string
|
|
||||||
accountName string
|
|
||||||
statusID string
|
|
||||||
timelineCategory string
|
|
||||||
listID string
|
|
||||||
tag string
|
|
||||||
limit int
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewShowExecutor(tlf TopLevelFlags, name, summary string) *ShowExecutor {
|
|
||||||
showExe := ShowExecutor{
|
|
||||||
FlagSet: flag.NewFlagSet(name, flag.ExitOnError),
|
|
||||||
topLevelFlags: tlf,
|
|
||||||
}
|
|
||||||
|
|
||||||
showExe.BoolVar(&showExe.myAccount, flagMyAccount, false, "set to true to lookup your account")
|
|
||||||
showExe.BoolVar(&showExe.skipAccountRelationship, flagSkipRelationship, false, "set to true to skip showing your relationship to the specified account")
|
|
||||||
showExe.BoolVar(&showExe.showUserPreferences, flagShowPreferences, false, "show your preferences")
|
|
||||||
showExe.BoolVar(&showExe.showInBrowser, flagBrowser, false, "set to true to view in the browser")
|
|
||||||
showExe.StringVar(&showExe.resourceType, flagType, "", "specify the type of resource to display")
|
|
||||||
showExe.StringVar(&showExe.accountName, flagAccountName, "", "specify the account name in full (username@domain)")
|
|
||||||
showExe.StringVar(&showExe.statusID, flagStatusID, "", "specify the ID of the status to display")
|
|
||||||
showExe.StringVar(&showExe.timelineCategory, flagTimelineCategory, model.TimelineCategoryHome, "specify the timeline category to view")
|
|
||||||
showExe.StringVar(&showExe.listID, flagListID, "", "specify the ID of the list to display")
|
|
||||||
showExe.StringVar(&showExe.tag, flagTag, "", "specify the name of the tag to use")
|
|
||||||
showExe.IntVar(&showExe.limit, flagLimit, 20, "specify the limit of items to display")
|
|
||||||
|
|
||||||
showExe.Usage = commandUsageFunc(name, summary, showExe.FlagSet)
|
|
||||||
|
|
||||||
return &showExe
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ShowExecutor) Execute() error {
|
func (s *ShowExecutor) Execute() error {
|
||||||
if s.resourceType == "" {
|
if s.resourceType == "" {
|
||||||
return FlagNotSetError{flagText: flagType}
|
return FlagNotSetError{flagText: flagType}
|
||||||
|
@ -67,7 +24,12 @@ func (s *ShowExecutor) Execute() error {
|
||||||
resourceFollowing: s.showFollowing,
|
resourceFollowing: s.showFollowing,
|
||||||
resourceBlocked: s.showBlocked,
|
resourceBlocked: s.showBlocked,
|
||||||
resourceBookmarks: s.showBookmarks,
|
resourceBookmarks: s.showBookmarks,
|
||||||
resourceNotification: s.showNotifications,
|
resourceLiked: s.showLiked,
|
||||||
|
resourceStarred: s.showLiked,
|
||||||
|
resourceFollowRequest: s.showFollowRequests,
|
||||||
|
resourceMutedAccounts: s.showMutedAccounts,
|
||||||
|
resourceMedia: s.showMedia,
|
||||||
|
resourceMediaAttachment: s.showMediaAttachment,
|
||||||
}
|
}
|
||||||
|
|
||||||
doFunc, ok := funcMap[s.resourceType]
|
doFunc, ok := funcMap[s.resourceType]
|
||||||
|
@ -75,7 +37,7 @@ func (s *ShowExecutor) Execute() error {
|
||||||
return UnsupportedTypeError{resourceType: s.resourceType}
|
return UnsupportedTypeError{resourceType: s.resourceType}
|
||||||
}
|
}
|
||||||
|
|
||||||
gtsClient, err := client.NewClientFromConfig(s.topLevelFlags.ConfigDir)
|
gtsClient, err := client.NewClientFromFile(s.config.CredentialsFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to create the GoToSocial client: %w", err)
|
return fmt.Errorf("unable to create the GoToSocial client: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -89,65 +51,77 @@ func (s *ShowExecutor) showInstance(gtsClient *client.Client) error {
|
||||||
return fmt.Errorf("unable to retrieve the instance details: %w", err)
|
return fmt.Errorf("unable to retrieve the instance details: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
utilities.Display(instance, *s.topLevelFlags.NoColor)
|
s.printer.PrintInstance(instance)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ShowExecutor) showAccount(gtsClient *client.Client) error {
|
func (s *ShowExecutor) showAccount(gtsClient *client.Client) error {
|
||||||
var (
|
account, err := getAccount(gtsClient, s.myAccount, s.accountName)
|
||||||
account model.Account
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
|
|
||||||
if s.myAccount {
|
|
||||||
account, err = getMyAccount(gtsClient, s.topLevelFlags.ConfigDir)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("received an error while getting the account details: %w", err)
|
return fmt.Errorf("unable to get the account information: %w", err)
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if s.accountName == "" {
|
|
||||||
return FlagNotSetError{flagText: flagAccountName}
|
|
||||||
}
|
|
||||||
|
|
||||||
account, err = getAccount(gtsClient, s.accountName)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("received an error while getting the account details: %w", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.showInBrowser {
|
if s.showInBrowser {
|
||||||
utilities.OpenLink(account.URL)
|
if err := utilities.OpenLink(s.config.Integrations.Browser, account.URL); err != nil {
|
||||||
|
return fmt.Errorf("unable to open link: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
utilities.Display(account, *s.topLevelFlags.NoColor)
|
var (
|
||||||
|
relationship *model.AccountRelationship
|
||||||
|
preferences *model.Preferences
|
||||||
|
statuses *model.StatusList
|
||||||
|
myAccountID string
|
||||||
|
)
|
||||||
|
|
||||||
if !s.myAccount && !s.skipAccountRelationship {
|
if !s.myAccount && !s.skipAccountRelationship {
|
||||||
relationship, err := gtsClient.GetAccountRelationship(account.ID)
|
relationship, err = gtsClient.GetAccountRelationship(account.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to retrieve the relationship to this account: %w", err)
|
return fmt.Errorf("unable to retrieve the relationship to this account: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
utilities.Display(relationship, *s.topLevelFlags.NoColor)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.myAccount && s.showUserPreferences {
|
if s.myAccount {
|
||||||
preferences, err := gtsClient.GetUserPreferences()
|
myAccountID = account.ID
|
||||||
|
if s.showUserPreferences {
|
||||||
|
preferences, err = gtsClient.GetUserPreferences()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to retrieve the user preferences: %w", err)
|
return fmt.Errorf("unable to retrieve the user preferences: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
utilities.Display(preferences, *s.topLevelFlags.NoColor)
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.showStatuses {
|
||||||
|
form := client.GetAccountStatusesForm{
|
||||||
|
AccountID: account.ID,
|
||||||
|
Limit: s.limit,
|
||||||
|
ExcludeReplies: s.excludeReplies,
|
||||||
|
ExcludeReblogs: s.excludeBoosts,
|
||||||
|
Pinned: s.onlyPinned,
|
||||||
|
OnlyMedia: s.onlyMedia,
|
||||||
|
OnlyPublic: s.onlyPublic,
|
||||||
|
}
|
||||||
|
|
||||||
|
statuses, err = gtsClient.GetAccountStatuses(form)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to retrieve the account's statuses: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s.printer.PrintAccount(account, relationship, preferences, statuses, myAccountID)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ShowExecutor) showStatus(gtsClient *client.Client) error {
|
func (s *ShowExecutor) showStatus(gtsClient *client.Client) error {
|
||||||
if s.statusID == "" {
|
if s.statusID == "" {
|
||||||
return FlagNotSetError{flagText: flagStatusID}
|
return MissingIDError{
|
||||||
|
resource: resourceStatus,
|
||||||
|
action: "view",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
status, err := gtsClient.GetStatus(s.statusID)
|
status, err := gtsClient.GetStatus(s.statusID)
|
||||||
|
@ -156,12 +130,19 @@ func (s *ShowExecutor) showStatus(gtsClient *client.Client) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.showInBrowser {
|
if s.showInBrowser {
|
||||||
utilities.OpenLink(status.URL)
|
if err := utilities.OpenLink(s.config.Integrations.Browser, status.URL); err != nil {
|
||||||
|
return fmt.Errorf("unable to open link: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
utilities.Display(status, *s.topLevelFlags.NoColor)
|
myAccountID, err := getAccountID(gtsClient, true, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to get your account ID: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.printer.PrintStatus(status, myAccountID)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -179,7 +160,10 @@ func (s *ShowExecutor) showTimeline(gtsClient *client.Client) error {
|
||||||
timeline, err = gtsClient.GetPublicTimeline(s.limit)
|
timeline, err = gtsClient.GetPublicTimeline(s.limit)
|
||||||
case model.TimelineCategoryList:
|
case model.TimelineCategoryList:
|
||||||
if s.listID == "" {
|
if s.listID == "" {
|
||||||
return FlagNotSetError{flagText: flagListID}
|
return MissingIDError{
|
||||||
|
resource: resourceList,
|
||||||
|
action: "view the timeline in",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var list model.List
|
var list model.List
|
||||||
|
@ -192,7 +176,7 @@ func (s *ShowExecutor) showTimeline(gtsClient *client.Client) error {
|
||||||
timeline, err = gtsClient.GetListTimeline(list.ID, list.Title, s.limit)
|
timeline, err = gtsClient.GetListTimeline(list.ID, list.Title, s.limit)
|
||||||
case model.TimelineCategoryTag:
|
case model.TimelineCategoryTag:
|
||||||
if s.tag == "" {
|
if s.tag == "" {
|
||||||
return FlagNotSetError{flagText: flagTag}
|
return Error{"please provide the name of the tag"}
|
||||||
}
|
}
|
||||||
|
|
||||||
timeline, err = gtsClient.GetTagTimeline(s.tag, s.limit)
|
timeline, err = gtsClient.GetTagTimeline(s.tag, s.limit)
|
||||||
|
@ -205,12 +189,17 @@ func (s *ShowExecutor) showTimeline(gtsClient *client.Client) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(timeline.Statuses) == 0 {
|
if len(timeline.Statuses) == 0 {
|
||||||
fmt.Println("There are no statuses in this timeline.")
|
s.printer.PrintInfo("There are no statuses in this timeline.\n")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
utilities.Display(timeline, *s.topLevelFlags.NoColor)
|
myAccountID, err := getAccountID(gtsClient, true, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to get your account ID: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.printer.PrintStatusList(timeline, myAccountID)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -239,7 +228,7 @@ func (s *ShowExecutor) showList(gtsClient *client.Client) error {
|
||||||
list.Accounts = accountMap
|
list.Accounts = accountMap
|
||||||
}
|
}
|
||||||
|
|
||||||
utilities.Display(list, *s.topLevelFlags.NoColor)
|
s.printer.PrintList(list)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -251,18 +240,38 @@ func (s *ShowExecutor) showLists(gtsClient *client.Client) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(lists) == 0 {
|
if len(lists) == 0 {
|
||||||
fmt.Println("You have no lists.")
|
s.printer.PrintInfo("You have no lists.\n")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
utilities.Display(lists, *s.topLevelFlags.NoColor)
|
s.printer.PrintLists(lists)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ShowExecutor) showFollowers(gtsClient *client.Client) error {
|
func (s *ShowExecutor) showFollowers(gtsClient *client.Client) error {
|
||||||
accountID, err := getAccountID(gtsClient, s.myAccount, s.accountName, s.topLevelFlags.ConfigDir)
|
if s.fromResourceType == "" {
|
||||||
|
return FlagNotSetError{flagText: flagFrom}
|
||||||
|
}
|
||||||
|
|
||||||
|
funcMap := map[string]func(*client.Client) error{
|
||||||
|
resourceAccount: s.showFollowersFromAccount,
|
||||||
|
}
|
||||||
|
|
||||||
|
doFunc, ok := funcMap[s.fromResourceType]
|
||||||
|
if !ok {
|
||||||
|
return UnsupportedShowOperationError{
|
||||||
|
resourceType: s.resourceType,
|
||||||
|
showFromResourceType: s.fromResourceType,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return doFunc(gtsClient)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ShowExecutor) showFollowersFromAccount(gtsClient *client.Client) error {
|
||||||
|
accountID, err := getAccountID(gtsClient, s.myAccount, s.accountName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("received an error while getting the account ID: %w", err)
|
return fmt.Errorf("received an error while getting the account ID: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -273,16 +282,36 @@ func (s *ShowExecutor) showFollowers(gtsClient *client.Client) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(followers.Accounts) > 0 {
|
if len(followers.Accounts) > 0 {
|
||||||
utilities.Display(followers, *s.topLevelFlags.NoColor)
|
s.printer.PrintAccountList(followers)
|
||||||
} else {
|
} else {
|
||||||
fmt.Println("There are no followers for this account or the list is hidden.")
|
s.printer.PrintInfo("There are no followers for this account (or the list is hidden).\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ShowExecutor) showFollowing(gtsClient *client.Client) error {
|
func (s *ShowExecutor) showFollowing(gtsClient *client.Client) error {
|
||||||
accountID, err := getAccountID(gtsClient, s.myAccount, s.accountName, s.topLevelFlags.ConfigDir)
|
if s.fromResourceType == "" {
|
||||||
|
return FlagNotSetError{flagText: flagFrom}
|
||||||
|
}
|
||||||
|
|
||||||
|
funcMap := map[string]func(*client.Client) error{
|
||||||
|
resourceAccount: s.showFollowingFromAccount,
|
||||||
|
}
|
||||||
|
|
||||||
|
doFunc, ok := funcMap[s.fromResourceType]
|
||||||
|
if !ok {
|
||||||
|
return UnsupportedShowOperationError{
|
||||||
|
resourceType: s.resourceType,
|
||||||
|
showFromResourceType: s.fromResourceType,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return doFunc(gtsClient)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ShowExecutor) showFollowingFromAccount(gtsClient *client.Client) error {
|
||||||
|
accountID, err := getAccountID(gtsClient, s.myAccount, s.accountName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("received an error while getting the account ID: %w", err)
|
return fmt.Errorf("received an error while getting the account ID: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -293,9 +322,9 @@ func (s *ShowExecutor) showFollowing(gtsClient *client.Client) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(following.Accounts) > 0 {
|
if len(following.Accounts) > 0 {
|
||||||
utilities.Display(following, *s.topLevelFlags.NoColor)
|
s.printer.PrintAccountList(following)
|
||||||
} else {
|
} else {
|
||||||
fmt.Println("This account is not following anyone or the list is hidden.")
|
s.printer.PrintInfo("This account is not following anyone or the list is hidden.\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -308,9 +337,9 @@ func (s *ShowExecutor) showBlocked(gtsClient *client.Client) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(blocked.Accounts) > 0 {
|
if len(blocked.Accounts) > 0 {
|
||||||
utilities.Display(blocked, *s.topLevelFlags.NoColor)
|
s.printer.PrintAccountList(blocked)
|
||||||
} else {
|
} else {
|
||||||
fmt.Println("You have no blocked accounts.")
|
s.printer.PrintInfo("You have no blocked accounts.\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -323,22 +352,153 @@ func (s *ShowExecutor) showBookmarks(gtsClient *client.Client) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(bookmarks.Statuses) > 0 {
|
if len(bookmarks.Statuses) > 0 {
|
||||||
utilities.Display(bookmarks, *s.topLevelFlags.NoColor)
|
myAccountID, err := getAccountID(gtsClient, true, nil)
|
||||||
} else {
|
|
||||||
fmt.Println("You have no bookmarks.")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ShowExecutor) showNotifications(gts *client.Client) error {
|
|
||||||
notifications, err := gts.GetNotifications(s.limit)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to retrieve your notifications: %w", err)
|
return fmt.Errorf("unable to get your account ID: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := range notifications {
|
s.printer.PrintStatusList(bookmarks, myAccountID)
|
||||||
utilities.Display(notifications[i], *s.topLevelFlags.NoColor)
|
} else {
|
||||||
|
s.printer.PrintInfo("You have no bookmarks.\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ShowExecutor) showLiked(gtsClient *client.Client) error {
|
||||||
|
liked, err := gtsClient.GetLikedStatuses(s.limit, s.resourceType)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to retrieve the list of your %s statuses: %w", s.resourceType, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(liked.Statuses) > 0 {
|
||||||
|
myAccountID, err := getAccountID(gtsClient, true, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to get your account ID: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.printer.PrintStatusList(liked, myAccountID)
|
||||||
|
} else {
|
||||||
|
s.printer.PrintInfo("You have no " + s.resourceType + " statuses.\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ShowExecutor) showFollowRequests(gtsClient *client.Client) error {
|
||||||
|
accounts, err := gtsClient.GetFollowRequests(s.limit)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to retrieve the list of follow requests: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(accounts.Accounts) > 0 {
|
||||||
|
s.printer.PrintAccountList(accounts)
|
||||||
|
} else {
|
||||||
|
s.printer.PrintInfo("You have no follow requests.\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ShowExecutor) showMutedAccounts(gtsClient *client.Client) error {
|
||||||
|
muted, err := gtsClient.GetMutedAccounts(s.limit)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to retrieve the list of muted accounts: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(muted.Accounts) > 0 {
|
||||||
|
s.printer.PrintAccountList(muted)
|
||||||
|
} else {
|
||||||
|
s.printer.PrintInfo("You have not muted any accounts.\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ShowExecutor) showMediaAttachment(gtsClient *client.Client) error {
|
||||||
|
if len(s.attachmentIDs) != 1 {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"unexpected number of attachment IDs received: want 1, got %d",
|
||||||
|
len(s.attachmentIDs),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
attachment, err := gtsClient.GetMediaAttachment(s.attachmentIDs[0])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to retrieve the media attachment: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.printer.PrintMediaAttachment(attachment)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ShowExecutor) showMedia(gtsClient *client.Client) error {
|
||||||
|
if s.fromResourceType == "" {
|
||||||
|
return FlagNotSetError{flagText: flagFrom}
|
||||||
|
}
|
||||||
|
|
||||||
|
funcMap := map[string]func(*client.Client) error{
|
||||||
|
resourceStatus: s.showMediaFromStatus,
|
||||||
|
}
|
||||||
|
|
||||||
|
doFunc, ok := funcMap[s.fromResourceType]
|
||||||
|
if !ok {
|
||||||
|
return UnsupportedShowOperationError{
|
||||||
|
resourceType: s.resourceType,
|
||||||
|
showFromResourceType: s.fromResourceType,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return doFunc(gtsClient)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ShowExecutor) showMediaFromStatus(gtsClient *client.Client) error {
|
||||||
|
if s.statusID == "" {
|
||||||
|
return MissingIDError{
|
||||||
|
resource: resourceStatus,
|
||||||
|
action: "view the media from",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
status, err := gtsClient.GetStatus(s.statusID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to retrieve the status: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheDir, err := utilities.CalculateMediaCacheDir(s.config.CacheDirectory, gtsClient.Authentication.Instance)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to get the media cache directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := utilities.EnsureDirectory(cacheDir); err != nil {
|
||||||
|
return fmt.Errorf("unable to ensure the existence of the directory %q: %w", cacheDir, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaBundle := media.NewBundle(
|
||||||
|
cacheDir,
|
||||||
|
status.MediaAttachments,
|
||||||
|
s.getAllImages,
|
||||||
|
s.getAllVideos,
|
||||||
|
s.attachmentIDs,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := mediaBundle.Download(gtsClient); err != nil {
|
||||||
|
return fmt.Errorf("unable to download the media bundle: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
imageFiles := mediaBundle.ImageFiles()
|
||||||
|
if len(imageFiles) > 0 {
|
||||||
|
if err := utilities.OpenMedia(s.config.Integrations.ImageViewer, imageFiles); err != nil {
|
||||||
|
return fmt.Errorf("unable to open the image viewer: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
videoFiles := mediaBundle.VideoFiles()
|
||||||
|
if len(videoFiles) > 0 {
|
||||||
|
if err := utilities.OpenMedia(s.config.Integrations.VideoPlayer, videoFiles); err != nil {
|
||||||
|
return fmt.Errorf("unable to open the video player: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -1,61 +1,38 @@
|
||||||
// SPDX-FileCopyrightText: 2024 Dan Anglin <d.n.i.anglin@gmail.com>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
package executor
|
package executor
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"flag"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"codeflow.dananglin.me.uk/apollo/enbas/internal/config"
|
"codeflow.dananglin.me.uk/apollo/enbas/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SwitchExecutor struct {
|
|
||||||
*flag.FlagSet
|
|
||||||
|
|
||||||
topLevelFlags TopLevelFlags
|
|
||||||
toResourceType string
|
|
||||||
accountName string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewSwitchExecutor(tlf TopLevelFlags, name, summary string) *SwitchExecutor {
|
|
||||||
switchExe := SwitchExecutor{
|
|
||||||
FlagSet: flag.NewFlagSet(name, flag.ExitOnError),
|
|
||||||
topLevelFlags: tlf,
|
|
||||||
}
|
|
||||||
|
|
||||||
switchExe.StringVar(&switchExe.toResourceType, flagTo, "", "the account to switch to")
|
|
||||||
switchExe.StringVar(&switchExe.accountName, flagAccountName, "", "the name of the account to switch to")
|
|
||||||
|
|
||||||
switchExe.Usage = commandUsageFunc(name, summary, switchExe.FlagSet)
|
|
||||||
|
|
||||||
return &switchExe
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SwitchExecutor) Execute() error {
|
func (s *SwitchExecutor) Execute() error {
|
||||||
funcMap := map[string]func() error{
|
funcMap := map[string]func() error{
|
||||||
resourceAccount: s.switchToAccount,
|
resourceAccount: s.switchToAccount,
|
||||||
}
|
}
|
||||||
|
|
||||||
doFunc, ok := funcMap[s.toResourceType]
|
doFunc, ok := funcMap[s.to]
|
||||||
if !ok {
|
if !ok {
|
||||||
return UnsupportedTypeError{resourceType: s.toResourceType}
|
return UnsupportedTypeError{resourceType: s.to}
|
||||||
}
|
}
|
||||||
|
|
||||||
return doFunc()
|
return doFunc()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SwitchExecutor) switchToAccount() error {
|
func (s *SwitchExecutor) switchToAccount() error {
|
||||||
if s.accountName == "" {
|
expectedNumAccountNames := 1
|
||||||
return NoAccountSpecifiedError{}
|
if !s.accountName.ExpectedLength(expectedNumAccountNames) {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"found an unexpected number of --account-name flags: expected %d",
|
||||||
|
expectedNumAccountNames,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := config.UpdateCurrentAccount(s.accountName, s.topLevelFlags.ConfigDir); err != nil {
|
if err := config.UpdateCurrentAccount(s.accountName[0], s.config.CredentialsFile); err != nil {
|
||||||
return fmt.Errorf("unable to switch account to the account: %w", err)
|
return fmt.Errorf("unable to switch account to the account: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("The current account is now set to %q.\n", s.accountName)
|
s.printer.PrintSuccess("The current account is now set to '" + s.accountName[0] + "'.")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
40
internal/executor/unblock.go
Normal file
40
internal/executor/unblock.go
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
package executor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"codeflow.dananglin.me.uk/apollo/enbas/internal/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (b *UnblockExecutor) Execute() error {
|
||||||
|
funcMap := map[string]func(*client.Client) error{
|
||||||
|
resourceAccount: b.unblockAccount,
|
||||||
|
}
|
||||||
|
|
||||||
|
doFunc, ok := funcMap[b.resourceType]
|
||||||
|
if !ok {
|
||||||
|
return UnsupportedTypeError{resourceType: b.resourceType}
|
||||||
|
}
|
||||||
|
|
||||||
|
gtsClient, err := client.NewClientFromFile(b.config.CredentialsFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to create the GoToSocial client: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return doFunc(gtsClient)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *UnblockExecutor) unblockAccount(gtsClient *client.Client) error {
|
||||||
|
accountID, err := getAccountID(gtsClient, false, b.accountName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("received an error while getting the account ID: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := gtsClient.UnblockAccount(accountID); err != nil {
|
||||||
|
return fmt.Errorf("unable to unblock the account: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
b.printer.PrintSuccess("Successfully unblocked the account.")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
40
internal/executor/unfollow.go
Normal file
40
internal/executor/unfollow.go
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
package executor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"codeflow.dananglin.me.uk/apollo/enbas/internal/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (f *UnfollowExecutor) Execute() error {
|
||||||
|
funcMap := map[string]func(*client.Client) error{
|
||||||
|
resourceAccount: f.unfollowAccount,
|
||||||
|
}
|
||||||
|
|
||||||
|
doFunc, ok := funcMap[f.resourceType]
|
||||||
|
if !ok {
|
||||||
|
return UnsupportedTypeError{resourceType: f.resourceType}
|
||||||
|
}
|
||||||
|
|
||||||
|
gtsClient, err := client.NewClientFromFile(f.config.CredentialsFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to create the GoToSocial client: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return doFunc(gtsClient)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *UnfollowExecutor) unfollowAccount(gtsClient *client.Client) error {
|
||||||
|
accountID, err := getAccountID(gtsClient, false, f.accountName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("received an error while getting the account ID: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := gtsClient.UnfollowAccount(accountID); err != nil {
|
||||||
|
return fmt.Errorf("unable to unfollow the account: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
f.printer.PrintSuccess("Successfully unfollowed the account.")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
86
internal/executor/unmute.go
Normal file
86
internal/executor/unmute.go
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
package executor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"codeflow.dananglin.me.uk/apollo/enbas/internal/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (m *UnmuteExecutor) Execute() error {
|
||||||
|
funcMap := map[string]func(*client.Client) error{
|
||||||
|
resourceAccount: m.unmuteAccount,
|
||||||
|
resourceStatus: m.unmuteStatus,
|
||||||
|
}
|
||||||
|
|
||||||
|
doFunc, ok := funcMap[m.resourceType]
|
||||||
|
if !ok {
|
||||||
|
return UnsupportedTypeError{resourceType: m.resourceType}
|
||||||
|
}
|
||||||
|
|
||||||
|
gtsClient, err := client.NewClientFromFile(m.config.CredentialsFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to create the GoToSocial client: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return doFunc(gtsClient)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *UnmuteExecutor) unmuteAccount(gtsClient *client.Client) error {
|
||||||
|
accountID, err := getAccountID(gtsClient, false, m.accountName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("received an error while getting the account ID: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := gtsClient.UnmuteAccount(accountID); err != nil {
|
||||||
|
return fmt.Errorf("unable to unmute the account: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.printer.PrintSuccess("Successfully unmuted the account.")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *UnmuteExecutor) unmuteStatus(gtsClient *client.Client) error {
|
||||||
|
if m.statusID == "" {
|
||||||
|
return MissingIDError{
|
||||||
|
resource: resourceStatus,
|
||||||
|
action: "unmute",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
status, err := gtsClient.GetStatus(m.statusID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to retrieve the status: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
myAccountID, err := getAccountID(gtsClient, true, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to get your account ID: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
canUnmute := false
|
||||||
|
|
||||||
|
if status.Account.ID == myAccountID {
|
||||||
|
canUnmute = true
|
||||||
|
} else {
|
||||||
|
for _, mention := range status.Mentions {
|
||||||
|
if mention.ID == myAccountID {
|
||||||
|
canUnmute = true
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !canUnmute {
|
||||||
|
return Error{"unable to unmute the status because the status does not belong to you nor are you mentioned in it"}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := gtsClient.UnmuteStatus(m.statusID); err != nil {
|
||||||
|
return fmt.Errorf("unable to unmute the status: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.printer.PrintSuccess("Successfully unmuted the status.")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -1,41 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2024 Dan Anglin <d.n.i.anglin@gmail.com>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
package executor
|
|
||||||
|
|
||||||
import (
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// commandUsageFunc returns the function used to print a command's help page.
|
|
||||||
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\n %s",
|
|
||||||
f.Name,
|
|
||||||
f.Usage,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
builder.WriteString("\n")
|
|
||||||
|
|
||||||
w := flag.CommandLine.Output()
|
|
||||||
|
|
||||||
fmt.Fprint(w, builder.String())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,59 +1,7 @@
|
||||||
// SPDX-FileCopyrightText: 2024 Dan Anglin <d.n.i.anglin@gmail.com>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
package executor
|
package executor
|
||||||
|
|
||||||
import (
|
func (v *VersionExecutor) Execute() error {
|
||||||
"flag"
|
v.printer.PrintVersion(v.full)
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
type VersionExecutor struct {
|
|
||||||
*flag.FlagSet
|
|
||||||
showFullVersion bool
|
|
||||||
binaryVersion string
|
|
||||||
buildTime string
|
|
||||||
goVersion string
|
|
||||||
gitCommit string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewVersionExecutor(name, summary, binaryVersion, buildTime, goVersion, gitCommit string) *VersionExecutor {
|
|
||||||
command := VersionExecutor{
|
|
||||||
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 *VersionExecutor) 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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,40 +1,18 @@
|
||||||
// SPDX-FileCopyrightText: 2024 Dan Anglin <d.n.i.anglin@gmail.com>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
package executor
|
package executor
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"flag"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"codeflow.dananglin.me.uk/apollo/enbas/internal/config"
|
"codeflow.dananglin.me.uk/apollo/enbas/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
type WhoAmIExecutor struct {
|
func (e *WhoamiExecutor) Execute() error {
|
||||||
*flag.FlagSet
|
config, err := config.NewCredentialsConfigFromFile(e.config.CredentialsFile)
|
||||||
|
|
||||||
topLevelFlags TopLevelFlags
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewWhoAmIExecutor(tlf TopLevelFlags, name, summary string) *WhoAmIExecutor {
|
|
||||||
whoExe := WhoAmIExecutor{
|
|
||||||
FlagSet: flag.NewFlagSet(name, flag.ExitOnError),
|
|
||||||
topLevelFlags: tlf,
|
|
||||||
}
|
|
||||||
|
|
||||||
whoExe.Usage = commandUsageFunc(name, summary, whoExe.FlagSet)
|
|
||||||
|
|
||||||
return &whoExe
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *WhoAmIExecutor) Execute() error {
|
|
||||||
config, err := config.NewCredentialsConfigFromFile(c.topLevelFlags.ConfigDir)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to load the credential config: %w", err)
|
return fmt.Errorf("unable to load the credential config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("You are logged in as %q.\n", config.CurrentAccount)
|
e.printer.PrintInfo("You are logged in as '" + config.CurrentAccount + "'.\n")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
38
internal/flag/boolptrvalue.go
Normal file
38
internal/flag/boolptrvalue.go
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
package flag
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BoolPtrValue struct {
|
||||||
|
Value *bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBoolPtrValue() BoolPtrValue {
|
||||||
|
return BoolPtrValue{
|
||||||
|
Value: nil,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b BoolPtrValue) String() string {
|
||||||
|
if b.Value == nil {
|
||||||
|
return "NOT SET"
|
||||||
|
}
|
||||||
|
|
||||||
|
return strconv.FormatBool(*b.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BoolPtrValue) Set(value string) error {
|
||||||
|
boolVar, err := strconv.ParseBool(value)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to parse %q as a boolean value: %w", value, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
b.Value = new(bool)
|
||||||
|
*b.Value = boolVar
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BoolPtrValue) IsBoolFlag() bool { return true }
|
100
internal/flag/boolptrvalue_test.go
Normal file
100
internal/flag/boolptrvalue_test.go
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
package flag_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"slices"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
internalFlag "codeflow.dananglin.me.uk/apollo/enbas/internal/flag"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBoolPtrValue(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
input string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
input: "True",
|
||||||
|
want: "true",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "true",
|
||||||
|
want: "true",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "1",
|
||||||
|
want: "true",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "False",
|
||||||
|
want: "false",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "false",
|
||||||
|
want: "false",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "0",
|
||||||
|
want: "false",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range slices.All(tests) {
|
||||||
|
args := []string{"--boolean-value=" + test.input}
|
||||||
|
|
||||||
|
t.Run("Flag parsing test: "+test.input, testBoolPtrValueParsing(args, test.want))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testBoolPtrValueParsing(args []string, want string) func(t *testing.T) {
|
||||||
|
return func(t *testing.T) {
|
||||||
|
flagset := flag.NewFlagSet("test", flag.ExitOnError)
|
||||||
|
boolVal := internalFlag.NewBoolPtrValue()
|
||||||
|
|
||||||
|
flagset.Var(&boolVal, "boolean-value", "Boolean value")
|
||||||
|
|
||||||
|
if err := flagset.Parse(args); err != nil {
|
||||||
|
t.Fatalf("Received an error parsing the flag: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
got := boolVal.String()
|
||||||
|
|
||||||
|
if got != want {
|
||||||
|
t.Errorf(
|
||||||
|
"Unexpected boolean value found after parsing BoolPtrValue: want %s, got %s",
|
||||||
|
want,
|
||||||
|
got,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
t.Logf(
|
||||||
|
"Expected boolean value found after parsing BoolPtrValue: got %s",
|
||||||
|
got,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNotSetBoolPtrValue(t *testing.T) {
|
||||||
|
flagset := flag.NewFlagSet("test", flag.ExitOnError)
|
||||||
|
boolVal := internalFlag.NewBoolPtrValue()
|
||||||
|
|
||||||
|
var otherVal string
|
||||||
|
|
||||||
|
flagset.Var(&boolVal, "boolean-value", "Boolean value")
|
||||||
|
flagset.StringVar(&otherVal, "other-value", "", "Another value")
|
||||||
|
|
||||||
|
args := []string{"--other-value", "other-value"}
|
||||||
|
|
||||||
|
if err := flagset.Parse(args); err != nil {
|
||||||
|
t.Fatalf("Received an error parsing the flag: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := "NOT SET"
|
||||||
|
got := boolVal.String()
|
||||||
|
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("Unexpected string returned from the nil value; want %s, got %s", want, got)
|
||||||
|
} else {
|
||||||
|
t.Logf("Expected string returned from the nil value; got %s", got)
|
||||||
|
}
|
||||||
|
}
|
49
internal/flag/intslice.go
Normal file
49
internal/flag/intslice.go
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
package flag
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IntSliceValue []int
|
||||||
|
|
||||||
|
func NewIntSliceValue() IntSliceValue {
|
||||||
|
arr := make([]int, 0, 3)
|
||||||
|
|
||||||
|
return IntSliceValue(arr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v IntSliceValue) String() string {
|
||||||
|
var builder strings.Builder
|
||||||
|
|
||||||
|
for ind, value := range v {
|
||||||
|
if ind == len(v)-1 {
|
||||||
|
builder.WriteString(strconv.Itoa(value))
|
||||||
|
} else {
|
||||||
|
builder.WriteString(strconv.Itoa(value))
|
||||||
|
builder.WriteString(", ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *IntSliceValue) Set(text string) error {
|
||||||
|
value, err := strconv.Atoi(text)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to parse the value to an integer: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
*v = append(*v, value)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v IntSliceValue) Empty() bool {
|
||||||
|
return len(v) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v IntSliceValue) ExpectedLength(expectedLength int) bool {
|
||||||
|
return len(v) == expectedLength
|
||||||
|
}
|
56
internal/flag/intslice_test.go
Normal file
56
internal/flag/intslice_test.go
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
package flag_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
internalFlag "codeflow.dananglin.me.uk/apollo/enbas/internal/flag"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIntSliceValue(t *testing.T) {
|
||||||
|
flagset := flag.NewFlagSet("test", flag.ExitOnError)
|
||||||
|
intSliceVal := internalFlag.NewIntSliceValue()
|
||||||
|
|
||||||
|
if !intSliceVal.Empty() {
|
||||||
|
t.Fatalf("The initialised IntSliceValue is not empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
flagset.Var(&intSliceVal, "int-value", "Integer value")
|
||||||
|
|
||||||
|
args := []string{
|
||||||
|
"--int-value", "0",
|
||||||
|
"--int-value", "1",
|
||||||
|
"--int-value", "2",
|
||||||
|
"--int-value", "3",
|
||||||
|
"--int-value", "4",
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := flagset.Parse(args); err != nil {
|
||||||
|
t.Fatalf("Received an error parsing the flag: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
wantLength := 5
|
||||||
|
if !intSliceVal.ExpectedLength(wantLength) {
|
||||||
|
t.Fatalf(
|
||||||
|
"Error: intSliceVal.ExpectedLength(%d) == false: actual length is %d",
|
||||||
|
wantLength,
|
||||||
|
len(intSliceVal),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := "0, 1, 2, 3, 4"
|
||||||
|
got := intSliceVal.String()
|
||||||
|
|
||||||
|
if got != want {
|
||||||
|
t.Errorf(
|
||||||
|
"Unexpected result after parsing IntSliceValue: want %s, got %s",
|
||||||
|
want,
|
||||||
|
got,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
t.Logf(
|
||||||
|
"Expected result after parsing IntSliceValue: got %s",
|
||||||
|
got,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
31
internal/flag/stringslice.go
Normal file
31
internal/flag/stringslice.go
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
package flag
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
type StringSliceValue []string
|
||||||
|
|
||||||
|
func NewStringSliceValue() StringSliceValue {
|
||||||
|
arr := make([]string, 0, 3)
|
||||||
|
|
||||||
|
return StringSliceValue(arr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v StringSliceValue) String() string {
|
||||||
|
return strings.Join(v, ", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *StringSliceValue) Set(value string) error {
|
||||||
|
if len(value) > 0 {
|
||||||
|
*v = append(*v, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v StringSliceValue) Empty() bool {
|
||||||
|
return len(v) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v StringSliceValue) ExpectedLength(expectedLength int) bool {
|
||||||
|
return len(v) == expectedLength
|
||||||
|
}
|
57
internal/flag/stringslice_test.go
Normal file
57
internal/flag/stringslice_test.go
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
package flag_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
internalFlag "codeflow.dananglin.me.uk/apollo/enbas/internal/flag"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStringSliceValue(t *testing.T) {
|
||||||
|
flagset := flag.NewFlagSet("test", flag.ExitOnError)
|
||||||
|
stringSliceVal := internalFlag.NewStringSliceValue()
|
||||||
|
|
||||||
|
if !stringSliceVal.Empty() {
|
||||||
|
t.Fatalf("The initialised StringSliceValue is not empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
flagset.Var(&stringSliceVal, "colour", "String value")
|
||||||
|
|
||||||
|
args := []string{
|
||||||
|
"--colour", "orange",
|
||||||
|
"--colour", "blue",
|
||||||
|
"--colour", "magenta",
|
||||||
|
"--colour", "red",
|
||||||
|
"--colour", "green",
|
||||||
|
"--colour", "silver",
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := flagset.Parse(args); err != nil {
|
||||||
|
t.Fatalf("Received an error parsing the flag: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
wantLength := 6
|
||||||
|
if !stringSliceVal.ExpectedLength(wantLength) {
|
||||||
|
t.Fatalf(
|
||||||
|
"Error: intSliceVal.ExpectedLength(%d) == false: actual length is %d",
|
||||||
|
wantLength,
|
||||||
|
len(stringSliceVal),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := "orange, blue, magenta, red, green, silver"
|
||||||
|
got := stringSliceVal.String()
|
||||||
|
|
||||||
|
if got != want {
|
||||||
|
t.Errorf(
|
||||||
|
"Unexpected result after parsing StringSliceValue: want %s, got %s",
|
||||||
|
want,
|
||||||
|
got,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
t.Logf(
|
||||||
|
"Expected result after parsing StringSliceValue: got %s",
|
||||||
|
got,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
79
internal/flag/timedurationvalue.go
Normal file
79
internal/flag/timedurationvalue.go
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
package flag
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const timeDurationRegexPattern string = `[0-9]{1,4}\s+(days?|hours?|minutes?|seconds?)`
|
||||||
|
|
||||||
|
type TimeDurationValue struct {
|
||||||
|
Duration time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTimeDurationValue() TimeDurationValue {
|
||||||
|
return TimeDurationValue{
|
||||||
|
Duration: time.Duration(0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v TimeDurationValue) String() string {
|
||||||
|
return v.Duration.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *TimeDurationValue) Set(value string) error {
|
||||||
|
pattern := regexp.MustCompile(timeDurationRegexPattern)
|
||||||
|
matches := pattern.FindAllString(value, -1)
|
||||||
|
|
||||||
|
days, hours, minutes, seconds := 0, 0, 0, 0
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
for ind := range len(matches) {
|
||||||
|
switch {
|
||||||
|
case strings.Contains(matches[ind], "day"):
|
||||||
|
days, err = parseInt(matches[ind])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to parse the number of days from %s: %w", matches[ind], err)
|
||||||
|
}
|
||||||
|
case strings.Contains(matches[ind], "hour"):
|
||||||
|
hours, err = parseInt(matches[ind])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to parse the number of hours from %s: %w", matches[ind], err)
|
||||||
|
}
|
||||||
|
case strings.Contains(matches[ind], "minute"):
|
||||||
|
minutes, err = parseInt(matches[ind])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to parse the number of minutes from %s: %w", matches[ind], err)
|
||||||
|
}
|
||||||
|
case strings.Contains(matches[ind], "second"):
|
||||||
|
seconds, err = parseInt(matches[ind])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to parse the number of seconds from %s: %w", matches[ind], err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
durationValue := (days * 86400) + (hours * 3600) + (minutes * 60) + seconds
|
||||||
|
|
||||||
|
v.Duration = time.Duration(durationValue) * time.Second
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseInt(text string) (int, error) {
|
||||||
|
split := strings.SplitN(text, " ", 2)
|
||||||
|
if len(split) != 2 {
|
||||||
|
return 0, fmt.Errorf("unexpected number of split for %s: want 2, got %d", text, len(split))
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err := strconv.Atoi(split[0])
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("unable to convert %s to an integer: %w", text, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return output, nil
|
||||||
|
}
|
67
internal/flag/timedurationvalue_test.go
Normal file
67
internal/flag/timedurationvalue_test.go
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
package flag_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"slices"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
internalFlag "codeflow.dananglin.me.uk/apollo/enbas/internal/flag"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTimeDurationValue(t *testing.T) {
|
||||||
|
parsingTests := []struct {
|
||||||
|
input string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
input: `"1 day"`,
|
||||||
|
want: "24h0m0s",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `"3 days, 5 hours, 39 minutes and 6 seconds"`,
|
||||||
|
want: "77h39m6s",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `"1 minute and 30 seconds"`,
|
||||||
|
want: "1m30s",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `"(7 seconds) (21 hours) (41 days)"`,
|
||||||
|
want: "1005h0m7s",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range slices.All(parsingTests) {
|
||||||
|
args := []string{"--duration", test.input}
|
||||||
|
|
||||||
|
t.Run("Flag parsing test: "+test.input, testTimeDurationValueParsing(args, test.want))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testTimeDurationValueParsing(args []string, want string) func(t *testing.T) {
|
||||||
|
return func(t *testing.T) {
|
||||||
|
flagset := flag.NewFlagSet("test", flag.ExitOnError)
|
||||||
|
duration := internalFlag.NewTimeDurationValue()
|
||||||
|
|
||||||
|
flagset.Var(&duration, "duration", "Duration value")
|
||||||
|
|
||||||
|
if err := flagset.Parse(args); err != nil {
|
||||||
|
t.Fatalf("Received an error parsing the flag: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
got := duration.String()
|
||||||
|
|
||||||
|
if got != want {
|
||||||
|
t.Errorf(
|
||||||
|
"Unexpected duration parsed from the flag: want %s, got %s",
|
||||||
|
want,
|
||||||
|
got,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
t.Logf(
|
||||||
|
"Expected duration parsed from the flag: got %s",
|
||||||
|
got,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
13
internal/info/info.go
Normal file
13
internal/info/info.go
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
package info
|
||||||
|
|
||||||
|
const (
|
||||||
|
ApplicationName string = "enbas"
|
||||||
|
ApplicationWebsite string = "https://codeflow.dananglin.me.uk/apollo/enbas"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
BinaryVersion string //nolint:gochecknoglobals
|
||||||
|
BuildTime string //nolint:gochecknoglobals
|
||||||
|
GoVersion string //nolint:gochecknoglobals
|
||||||
|
GitCommit string //nolint:gochecknoglobals
|
||||||
|
)
|
|
@ -1,12 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2024 Dan Anglin <d.n.i.anglin@gmail.com>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
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"
|
|
||||||
)
|
|
170
internal/media/media.go
Normal file
170
internal/media/media.go
Normal file
|
@ -0,0 +1,170 @@
|
||||||
|
package media
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"codeflow.dananglin.me.uk/apollo/enbas/internal/client"
|
||||||
|
"codeflow.dananglin.me.uk/apollo/enbas/internal/model"
|
||||||
|
"codeflow.dananglin.me.uk/apollo/enbas/internal/utilities"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
mediaTypeImage string = "image"
|
||||||
|
mediaTypeVideo string = "video"
|
||||||
|
)
|
||||||
|
|
||||||
|
type media struct {
|
||||||
|
source string
|
||||||
|
destination string
|
||||||
|
mediaType string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *media) download(gtsClient *client.Client) error {
|
||||||
|
fileExists, err := utilities.FileExists(m.destination)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"unable to determine if %s exists: %w",
|
||||||
|
m.destination,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if fileExists {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := gtsClient.DownloadMedia(m.source, m.destination); err != nil {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"downloading %s -> %s failed: %w",
|
||||||
|
m.source,
|
||||||
|
m.destination,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMediaHashmap(cacheDir string, attachments []model.Attachment) map[string]media {
|
||||||
|
hashmap := make(map[string]media)
|
||||||
|
|
||||||
|
for ind := range attachments {
|
||||||
|
hashmap[attachments[ind].ID] = media{
|
||||||
|
source: attachments[ind].URL,
|
||||||
|
destination: mediaFilepath(cacheDir, attachments[ind].URL),
|
||||||
|
mediaType: attachments[ind].Type,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return hashmap
|
||||||
|
}
|
||||||
|
|
||||||
|
type Bundle struct {
|
||||||
|
images []media
|
||||||
|
videos []media
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBundle(
|
||||||
|
cacheDir string,
|
||||||
|
attachments []model.Attachment,
|
||||||
|
getAllImages bool,
|
||||||
|
getAllVideos bool,
|
||||||
|
attachmentIDs []string,
|
||||||
|
) Bundle {
|
||||||
|
mediaHashmap := newMediaHashmap(cacheDir, attachments)
|
||||||
|
images := make([]media, 0)
|
||||||
|
videos := make([]media, 0)
|
||||||
|
|
||||||
|
if !getAllImages && !getAllVideos && len(attachmentIDs) == 0 {
|
||||||
|
return Bundle{
|
||||||
|
images: images,
|
||||||
|
videos: videos,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if getAllImages || getAllVideos {
|
||||||
|
if getAllImages {
|
||||||
|
for _, m := range mediaHashmap {
|
||||||
|
if m.mediaType == mediaTypeImage {
|
||||||
|
images = append(images, m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if getAllVideos {
|
||||||
|
for _, m := range mediaHashmap {
|
||||||
|
if m.mediaType == mediaTypeVideo {
|
||||||
|
videos = append(videos, m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Bundle{
|
||||||
|
images: images,
|
||||||
|
videos: videos,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, attachmentID := range attachmentIDs {
|
||||||
|
obj, ok := mediaHashmap[attachmentID]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch obj.mediaType {
|
||||||
|
case mediaTypeImage:
|
||||||
|
images = append(images, obj)
|
||||||
|
case mediaTypeVideo:
|
||||||
|
videos = append(videos, obj)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Bundle{
|
||||||
|
images: images,
|
||||||
|
videos: videos,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Bundle) Download(gtsClient *client.Client) error {
|
||||||
|
for ind := range m.images {
|
||||||
|
if err := m.images[ind].download(gtsClient); err != nil {
|
||||||
|
return fmt.Errorf("received an error trying to download the image files: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for ind := range m.videos {
|
||||||
|
if err := m.videos[ind].download(gtsClient); err != nil {
|
||||||
|
return fmt.Errorf("received an error trying to download the video files: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Bundle) ImageFiles() []string {
|
||||||
|
filepaths := make([]string, len(m.images))
|
||||||
|
|
||||||
|
for ind := range m.images {
|
||||||
|
filepaths[ind] = m.images[ind].destination
|
||||||
|
}
|
||||||
|
|
||||||
|
return filepaths
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Bundle) VideoFiles() []string {
|
||||||
|
filepaths := make([]string, len(m.videos))
|
||||||
|
|
||||||
|
for ind := range m.videos {
|
||||||
|
filepaths[ind] = m.videos[ind].destination
|
||||||
|
}
|
||||||
|
|
||||||
|
return filepaths
|
||||||
|
}
|
||||||
|
|
||||||
|
func mediaFilepath(cacheDir, mediaURL string) string {
|
||||||
|
split := strings.Split(mediaURL, "/")
|
||||||
|
|
||||||
|
return filepath.Join(cacheDir, split[len(split)-1])
|
||||||
|
}
|
|
@ -1,14 +1,7 @@
|
||||||
// SPDX-FileCopyrightText: 2024 Dan Anglin <d.n.i.anglin@gmail.com>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
package model
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"codeflow.dananglin.me.uk/apollo/enbas/internal/utilities"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Account struct {
|
type Account struct {
|
||||||
|
@ -63,60 +56,6 @@ type Field struct {
|
||||||
VerifiedAt string `json:"verified_at"`
|
VerifiedAt string `json:"verified_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a Account) Display(noColor bool) string {
|
|
||||||
format := `
|
|
||||||
%s (@%s)
|
|
||||||
|
|
||||||
%s
|
|
||||||
%s
|
|
||||||
|
|
||||||
%s
|
|
||||||
%s
|
|
||||||
|
|
||||||
%s
|
|
||||||
%s %d
|
|
||||||
%s %d
|
|
||||||
%s %d
|
|
||||||
|
|
||||||
%s
|
|
||||||
%s
|
|
||||||
|
|
||||||
%s %s
|
|
||||||
|
|
||||||
%s
|
|
||||||
%s`
|
|
||||||
|
|
||||||
metadata := ""
|
|
||||||
|
|
||||||
for _, field := range a.Fields {
|
|
||||||
metadata += fmt.Sprintf(
|
|
||||||
"\n %s: %s",
|
|
||||||
utilities.FieldFormat(noColor, field.Name),
|
|
||||||
utilities.ConvertHTMLToText(field.Value),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf(
|
|
||||||
format,
|
|
||||||
utilities.DisplayNameFormat(noColor, a.DisplayName),
|
|
||||||
a.Username,
|
|
||||||
utilities.HeaderFormat(noColor, "ACCOUNT ID:"),
|
|
||||||
a.ID,
|
|
||||||
utilities.HeaderFormat(noColor, "JOINED ON:"),
|
|
||||||
utilities.FormatDate(a.CreatedAt),
|
|
||||||
utilities.HeaderFormat(noColor, "STATS:"),
|
|
||||||
utilities.FieldFormat(noColor, "Followers:"), a.FollowersCount,
|
|
||||||
utilities.FieldFormat(noColor, "Following:"), a.FollowingCount,
|
|
||||||
utilities.FieldFormat(noColor, "Statuses:"), a.StatusCount,
|
|
||||||
utilities.HeaderFormat(noColor, "BIOGRAPHY:"),
|
|
||||||
utilities.WrapLines(utilities.ConvertHTMLToText(a.Note), "\n ", 80),
|
|
||||||
utilities.HeaderFormat(noColor, "METADATA:"),
|
|
||||||
metadata,
|
|
||||||
utilities.HeaderFormat(noColor, "ACCOUNT URL:"),
|
|
||||||
a.URL,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
type AccountRelationship struct {
|
type AccountRelationship struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
PrivateNote string `json:"note"`
|
PrivateNote string `json:"note"`
|
||||||
|
@ -134,97 +73,17 @@ type AccountRelationship struct {
|
||||||
ShowingReblogs bool `json:"showing_reblogs"`
|
ShowingReblogs bool `json:"showing_reblogs"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a AccountRelationship) Display(noColor bool) string {
|
|
||||||
format := `
|
|
||||||
%s
|
|
||||||
%s: %t
|
|
||||||
%s: %t
|
|
||||||
%s: %t
|
|
||||||
%s: %t
|
|
||||||
%s: %t
|
|
||||||
%s: %t
|
|
||||||
%s: %t
|
|
||||||
%s: %t
|
|
||||||
%s: %t
|
|
||||||
%s: %t
|
|
||||||
%s: %t`
|
|
||||||
|
|
||||||
privateNoteFormat := `
|
|
||||||
%s
|
|
||||||
%s`
|
|
||||||
|
|
||||||
output := fmt.Sprintf(
|
|
||||||
format,
|
|
||||||
utilities.HeaderFormat(noColor, "YOUR RELATIONSHIP WITH THIS ACCOUNT:"),
|
|
||||||
utilities.FieldFormat(noColor, "Following"), a.Following,
|
|
||||||
utilities.FieldFormat(noColor, "Is following you"), a.FollowedBy,
|
|
||||||
utilities.FieldFormat(noColor, "A follow request was sent and is pending"), a.FollowRequested,
|
|
||||||
utilities.FieldFormat(noColor, "Received a pending follow request"), a.FollowRequestedBy,
|
|
||||||
utilities.FieldFormat(noColor, "Endorsed"), a.Endorsed,
|
|
||||||
utilities.FieldFormat(noColor, "Showing Reposts (boosts)"), a.ShowingReblogs,
|
|
||||||
utilities.FieldFormat(noColor, "Muted"), a.Muting,
|
|
||||||
utilities.FieldFormat(noColor, "Notifications muted"), a.MutingNotifications,
|
|
||||||
utilities.FieldFormat(noColor, "Blocking"), a.Blocking,
|
|
||||||
utilities.FieldFormat(noColor, "Is blocking you"), a.BlockedBy,
|
|
||||||
utilities.FieldFormat(noColor, "Blocking account's domain"), a.DomainBlocking,
|
|
||||||
)
|
|
||||||
|
|
||||||
if a.PrivateNote != "" {
|
|
||||||
output += "\n"
|
|
||||||
output += fmt.Sprintf(
|
|
||||||
privateNoteFormat,
|
|
||||||
utilities.HeaderFormat(noColor, "YOUR PRIVATE NOTE ABOUT THIS ACCOUNT:"),
|
|
||||||
utilities.WrapLines(a.PrivateNote, "\n ", 80),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return output
|
|
||||||
}
|
|
||||||
|
|
||||||
type AccountListType int
|
type AccountListType int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
AccountListFollowers AccountListType = iota
|
AccountListFollowers AccountListType = iota
|
||||||
AccountListFollowing
|
AccountListFollowing
|
||||||
AccountListBlockedAccount
|
AccountListBlockedAccount
|
||||||
|
AccountListFollowRequests
|
||||||
|
AccountListMuted
|
||||||
)
|
)
|
||||||
|
|
||||||
type AccountList struct {
|
type AccountList struct {
|
||||||
Type AccountListType
|
Type AccountListType
|
||||||
Accounts []Account
|
Accounts []Account
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a AccountList) Display(noColor bool) string {
|
|
||||||
output := "\n"
|
|
||||||
|
|
||||||
switch a.Type {
|
|
||||||
case AccountListFollowers:
|
|
||||||
output += utilities.HeaderFormat(noColor, "FOLLOWED BY:")
|
|
||||||
case AccountListFollowing:
|
|
||||||
output += utilities.HeaderFormat(noColor, "FOLLOWING:")
|
|
||||||
case AccountListBlockedAccount:
|
|
||||||
output += utilities.HeaderFormat(noColor, "BLOCKED ACCOUNTS:")
|
|
||||||
default:
|
|
||||||
output += utilities.HeaderFormat(noColor, "ACCOUNTS:")
|
|
||||||
}
|
|
||||||
|
|
||||||
if a.Type == AccountListBlockedAccount {
|
|
||||||
for i := range a.Accounts {
|
|
||||||
output += fmt.Sprintf(
|
|
||||||
"\n • %s (%s)",
|
|
||||||
a.Accounts[i].Acct,
|
|
||||||
a.Accounts[i].ID,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for i := range a.Accounts {
|
|
||||||
output += fmt.Sprintf(
|
|
||||||
"\n • %s (%s)",
|
|
||||||
utilities.DisplayNameFormat(noColor, a.Accounts[i].DisplayName),
|
|
||||||
a.Accounts[i].Acct,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return output
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,7 +1,3 @@
|
||||||
// SPDX-FileCopyrightText: 2024 Dan Anglin <d.n.i.anglin@gmail.com>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
package model
|
package model
|
||||||
|
|
||||||
type Application struct {
|
type Application struct {
|
||||||
|
@ -9,7 +5,7 @@ type Application struct {
|
||||||
ClientSecret string `json:"client_secret"`
|
ClientSecret string `json:"client_secret"`
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
RedirectUri string `json:"redirect_uri"`
|
RedirectURI string `json:"redirect_uri"`
|
||||||
VapidKey string `json:"vapid_key"`
|
VapidKey string `json:"vapid_key"`
|
||||||
Website string `json:"website"`
|
Website string `json:"website"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,3 @@
|
||||||
// SPDX-FileCopyrightText: 2024 Dan Anglin <d.n.i.anglin@gmail.com>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
package model
|
package model
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
|
@ -1,7 +1,3 @@
|
||||||
// SPDX-FileCopyrightText: 2024 Dan Anglin <d.n.i.anglin@gmail.com>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
package model
|
package model
|
||||||
|
|
||||||
type Emoji struct {
|
type Emoji struct {
|
||||||
|
|
|
@ -1,15 +1,5 @@
|
||||||
// SPDX-FileCopyrightText: 2024 Dan Anglin <d.n.i.anglin@gmail.com>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
package model
|
package model
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"codeflow.dananglin.me.uk/apollo/enbas/internal/utilities"
|
|
||||||
)
|
|
||||||
|
|
||||||
type InstanceV2 struct {
|
type InstanceV2 struct {
|
||||||
AccountDomain string `json:"account_domain"`
|
AccountDomain string `json:"account_domain"`
|
||||||
Configuration InstanceConfiguration `json:"configuration"`
|
Configuration InstanceConfiguration `json:"configuration"`
|
||||||
|
@ -116,48 +106,3 @@ type InstanceV2Usage struct {
|
||||||
type InstanceV2Users struct {
|
type InstanceV2Users struct {
|
||||||
ActiveMonth int `json:"active_month"`
|
ActiveMonth int `json:"active_month"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i InstanceV2) Display(noColor bool) string {
|
|
||||||
format := `
|
|
||||||
%s
|
|
||||||
%s
|
|
||||||
|
|
||||||
%s
|
|
||||||
%s
|
|
||||||
|
|
||||||
%s
|
|
||||||
%s
|
|
||||||
|
|
||||||
%s
|
|
||||||
%s
|
|
||||||
|
|
||||||
%s
|
|
||||||
Running GoToSocial %s
|
|
||||||
|
|
||||||
%s
|
|
||||||
%s %s
|
|
||||||
%s %s
|
|
||||||
%s %s
|
|
||||||
`
|
|
||||||
|
|
||||||
return fmt.Sprintf(
|
|
||||||
format,
|
|
||||||
utilities.HeaderFormat(noColor, "INSTANCE TITLE:"),
|
|
||||||
i.Title,
|
|
||||||
utilities.HeaderFormat(noColor, "INSTANCE DESCRIPTION:"),
|
|
||||||
utilities.WrapLines(i.DescriptionText, "\n ", 80),
|
|
||||||
utilities.HeaderFormat(noColor, "DOMAIN:"),
|
|
||||||
i.Domain,
|
|
||||||
utilities.HeaderFormat(noColor, "TERMS AND CONDITIONS:"),
|
|
||||||
utilities.WrapLines(i.TermsText, "\n ", 80),
|
|
||||||
utilities.HeaderFormat(noColor, "VERSION:"),
|
|
||||||
i.Version,
|
|
||||||
utilities.HeaderFormat(noColor, "CONTACT:"),
|
|
||||||
utilities.FieldFormat(noColor, "Name:"),
|
|
||||||
utilities.DisplayNameFormat(noColor, i.Contact.Account.DisplayName),
|
|
||||||
utilities.FieldFormat(noColor, "Username:"),
|
|
||||||
i.Contact.Account.Username,
|
|
||||||
utilities.FieldFormat(noColor, "Email:"),
|
|
||||||
i.Contact.Email,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,14 +1,8 @@
|
||||||
// SPDX-FileCopyrightText: 2024 Dan Anglin <d.n.i.anglin@gmail.com>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
package model
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"codeflow.dananglin.me.uk/apollo/enbas/internal/utilities"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ListRepliesPolicy int
|
type ListRepliesPolicy int
|
||||||
|
@ -107,57 +101,3 @@ type List struct {
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Accounts map[string]string
|
Accounts map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l List) Display(noColor bool) string {
|
|
||||||
format := `
|
|
||||||
%s
|
|
||||||
%s
|
|
||||||
|
|
||||||
%s
|
|
||||||
%s
|
|
||||||
|
|
||||||
%s
|
|
||||||
%s
|
|
||||||
|
|
||||||
%s`
|
|
||||||
|
|
||||||
output := fmt.Sprintf(
|
|
||||||
format,
|
|
||||||
utilities.HeaderFormat(noColor, "LIST TITLE:"), l.Title,
|
|
||||||
utilities.HeaderFormat(noColor, "LIST ID:"), l.ID,
|
|
||||||
utilities.HeaderFormat(noColor, "REPLIES POLICY:"), l.RepliesPolicy,
|
|
||||||
utilities.HeaderFormat(noColor, "ADDED ACCOUNTS:"),
|
|
||||||
)
|
|
||||||
|
|
||||||
if len(l.Accounts) > 0 {
|
|
||||||
for acct, name := range l.Accounts {
|
|
||||||
output += fmt.Sprintf(
|
|
||||||
"\n • %s (%s)",
|
|
||||||
utilities.DisplayNameFormat(noColor, name),
|
|
||||||
acct,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
output += "\n None"
|
|
||||||
}
|
|
||||||
|
|
||||||
output += "\n"
|
|
||||||
|
|
||||||
return output
|
|
||||||
}
|
|
||||||
|
|
||||||
type Lists []List
|
|
||||||
|
|
||||||
func (l Lists) Display(noColor bool) string {
|
|
||||||
output := "\n" + utilities.HeaderFormat(noColor, "LISTS")
|
|
||||||
|
|
||||||
for i := range l {
|
|
||||||
output += fmt.Sprintf(
|
|
||||||
"\n • %s (%s)",
|
|
||||||
l[i].Title,
|
|
||||||
l[i].ID,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return output
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,127 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2024 Dan Anglin <d.n.i.anglin@gmail.com>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
package model
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"codeflow.dananglin.me.uk/apollo/enbas/internal/utilities"
|
|
||||||
)
|
|
||||||
|
|
||||||
type NotificationType int
|
|
||||||
|
|
||||||
const (
|
|
||||||
NotificationTypeFollow NotificationType = iota
|
|
||||||
NotificationTypeFollowRequest
|
|
||||||
NotificationTypeMention
|
|
||||||
NotificationTypeReblog
|
|
||||||
NotificationTypeFavourite
|
|
||||||
NotificationTypePoll
|
|
||||||
NotificationTypeStatus
|
|
||||||
NotificationTypeUnknown
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
notificationFollow = "follow"
|
|
||||||
notificationFollowRequest = "follow_request"
|
|
||||||
notificationMention = "mention"
|
|
||||||
notificationReblog = "reblog"
|
|
||||||
notificationFavourite = "favourite"
|
|
||||||
notificationPoll = "poll"
|
|
||||||
notificationStatus = "status"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (n NotificationType) MessageFormat() string {
|
|
||||||
mapped := map[NotificationType]string{
|
|
||||||
NotificationTypeFollow: "%s followed you",
|
|
||||||
NotificationTypeFollowRequest: "%s has requested to follow you",
|
|
||||||
NotificationTypeMention: "%s has mentioned you in this status",
|
|
||||||
NotificationTypeReblog: "%s reposted this status from you",
|
|
||||||
NotificationTypeFavourite: "%s liked this status from you",
|
|
||||||
NotificationTypePoll: "A poll from %s that you have voted in has ended",
|
|
||||||
NotificationTypeStatus: "%s has posted this status",
|
|
||||||
}
|
|
||||||
|
|
||||||
output, ok := mapped[n]
|
|
||||||
if !ok {
|
|
||||||
return "You have received a notification of an unknown type"
|
|
||||||
}
|
|
||||||
|
|
||||||
return output
|
|
||||||
}
|
|
||||||
|
|
||||||
func ParseNotificationType(value string) (NotificationType, error) {
|
|
||||||
mapped := map[string]NotificationType{
|
|
||||||
notificationFollow: NotificationTypeFollow,
|
|
||||||
notificationFollowRequest: NotificationTypeFollowRequest,
|
|
||||||
notificationMention: NotificationTypeMention,
|
|
||||||
notificationReblog: NotificationTypeReblog,
|
|
||||||
notificationFavourite: NotificationTypeFavourite,
|
|
||||||
notificationPoll: NotificationTypePoll,
|
|
||||||
notificationStatus: NotificationTypeStatus,
|
|
||||||
}
|
|
||||||
|
|
||||||
output, ok := mapped[value]
|
|
||||||
if !ok {
|
|
||||||
return NotificationTypeUnknown, UnknownNotificationError{}
|
|
||||||
}
|
|
||||||
|
|
||||||
return output, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *NotificationType) UnmarshalJSON(data []byte) error {
|
|
||||||
var (
|
|
||||||
value string
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
|
|
||||||
if err = json.Unmarshal(data, &value); err != nil {
|
|
||||||
return fmt.Errorf("unable to unmarshal the data: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ignore the error if the notification type from another Fediverse service
|
|
||||||
// is not known by enbas. It will be seen as an unknown notification to the user.
|
|
||||||
*n, _ = ParseNotificationType(value)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type UnknownNotificationError struct{}
|
|
||||||
|
|
||||||
func (e UnknownNotificationError) Error() string {
|
|
||||||
return "unknown notification type"
|
|
||||||
}
|
|
||||||
|
|
||||||
type Notification struct {
|
|
||||||
Account *Account `json:"account"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
ID string `json:"id"`
|
|
||||||
Status *Status `json:"status"`
|
|
||||||
Type NotificationType `json:"type"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n Notification) Display(noColor bool) string {
|
|
||||||
var builder strings.Builder
|
|
||||||
|
|
||||||
separator := "────────────────────────────────────────────────────────────────────────────────"
|
|
||||||
|
|
||||||
fmt.Fprintf(&builder,
|
|
||||||
n.Type.MessageFormat(),
|
|
||||||
utilities.DisplayNameFormat(noColor, n.Account.DisplayName)+" (@"+n.Account.Acct+")",
|
|
||||||
)
|
|
||||||
|
|
||||||
if n.Status != nil {
|
|
||||||
builder.WriteString("\n\n" + utilities.DisplayNameFormat(noColor, n.Status.Account.DisplayName) + " (@" + n.Status.Account.Acct + ")\n")
|
|
||||||
builder.WriteString(utilities.WrapLines(utilities.ConvertHTMLToText(n.Status.Content), "\n", 80) + "\n\n")
|
|
||||||
builder.WriteString(utilities.FieldFormat(noColor, "ID:") + " " + n.Status.ID + "\t" + utilities.FieldFormat(noColor, "Created at:") + " " + utilities.FormatTime(n.Status.CreatedAt) + "\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.WriteString(separator + "\n")
|
|
||||||
|
|
||||||
return builder.String()
|
|
||||||
}
|
|
23
internal/model/poll.go
Normal file
23
internal/model/poll.go
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
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 int `json:"votes_count"`
|
||||||
|
}
|
|
@ -1,15 +1,5 @@
|
||||||
// SPDX-FileCopyrightText: 2024 Dan Anglin <d.n.i.anglin@gmail.com>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
package model
|
package model
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"codeflow.dananglin.me.uk/apollo/enbas/internal/utilities"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Preferences struct {
|
type Preferences struct {
|
||||||
PostingDefaultVisibility string `json:"posting:default:visibility"`
|
PostingDefaultVisibility string `json:"posting:default:visibility"`
|
||||||
PostingDefaultSensitive bool `json:"posting:default:sensitive"`
|
PostingDefaultSensitive bool `json:"posting:default:sensitive"`
|
||||||
|
@ -18,19 +8,3 @@ type Preferences struct {
|
||||||
ReadingExpandSpoilers bool `json:"reading:expand:spoilers"`
|
ReadingExpandSpoilers bool `json:"reading:expand:spoilers"`
|
||||||
ReadingAutoplayGifs bool `json:"reading:autoplay:gifs"`
|
ReadingAutoplayGifs bool `json:"reading:autoplay:gifs"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p Preferences) Display(noColor bool) string {
|
|
||||||
format := `
|
|
||||||
%s
|
|
||||||
%s: %s
|
|
||||||
%s: %s
|
|
||||||
%s: %t`
|
|
||||||
|
|
||||||
return fmt.Sprintf(
|
|
||||||
format,
|
|
||||||
utilities.HeaderFormat(noColor, "YOUR PREFERENCES:"),
|
|
||||||
utilities.FieldFormat(noColor, "Default post language"), p.PostingDefaultLanguage,
|
|
||||||
utilities.FieldFormat(noColor, "Default post visibility"), p.PostingDefaultVisibility,
|
|
||||||
utilities.FieldFormat(noColor, "Mark posts as sensitive by default"), p.PostingDefaultSensitive,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,15 +1,7 @@
|
||||||
// SPDX-FileCopyrightText: 2024 Dan Anglin <d.n.i.anglin@gmail.com>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
package model
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"codeflow.dananglin.me.uk/apollo/enbas/internal/utilities"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Status struct {
|
type Status struct {
|
||||||
|
@ -30,13 +22,13 @@ type Status struct {
|
||||||
Mentions []Mention `json:"mentions"`
|
Mentions []Mention `json:"mentions"`
|
||||||
Muted bool `json:"muted"`
|
Muted bool `json:"muted"`
|
||||||
Pinned bool `json:"pinned"`
|
Pinned bool `json:"pinned"`
|
||||||
Poll Poll `json:"poll"`
|
Poll *Poll `json:"poll"`
|
||||||
Reblog *StatusReblogged `json:"reblog"`
|
Reblog *StatusReblogged `json:"reblog"`
|
||||||
Reblogged bool `json:"reblogged"`
|
Reblogged bool `json:"reblogged"`
|
||||||
ReblogsCount int `json:"reblogs_count"`
|
ReblogsCount int `json:"reblogs_count"`
|
||||||
RepliesCount int `json:"replies_count"`
|
RepliesCount int `json:"replies_count"`
|
||||||
Sensitive bool `json:"sensitive"`
|
Sensitive bool `json:"sensitive"`
|
||||||
SpolierText string `json:"spoiler_text"`
|
SpoilerText string `json:"spoiler_text"`
|
||||||
Tags []Tag `json:"tags"`
|
Tags []Tag `json:"tags"`
|
||||||
Text string `json:"text"`
|
Text string `json:"text"`
|
||||||
URI string `json:"uri"`
|
URI string `json:"uri"`
|
||||||
|
@ -68,24 +60,6 @@ type Mention struct {
|
||||||
Username string `json:"username"`
|
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 int `json:"votes_count"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type StatusReblogged struct {
|
type StatusReblogged struct {
|
||||||
Account Account `json:"account"`
|
Account Account `json:"account"`
|
||||||
Application Application `json:"application"`
|
Application Application `json:"application"`
|
||||||
|
@ -104,12 +78,12 @@ type StatusReblogged struct {
|
||||||
Mentions []Mention `json:"mentions"`
|
Mentions []Mention `json:"mentions"`
|
||||||
Muted bool `json:"muted"`
|
Muted bool `json:"muted"`
|
||||||
Pinned bool `json:"pinned"`
|
Pinned bool `json:"pinned"`
|
||||||
Poll Poll `json:"poll"`
|
Poll *Poll `json:"poll"`
|
||||||
Reblogged bool `json:"reblogged"`
|
Reblogged bool `json:"reblogged"`
|
||||||
RebloggsCount int `json:"reblogs_count"`
|
RebloggsCount int `json:"reblogs_count"`
|
||||||
RepliesCount int `json:"replies_count"`
|
RepliesCount int `json:"replies_count"`
|
||||||
Sensitive bool `json:"sensitive"`
|
Sensitive bool `json:"sensitive"`
|
||||||
SpolierText string `json:"spoiler_text"`
|
SpoilerText string `json:"spoiler_text"`
|
||||||
Tags []Tag `json:"tags"`
|
Tags []Tag `json:"tags"`
|
||||||
Text string `json:"text"`
|
Text string `json:"text"`
|
||||||
URI string `json:"uri"`
|
URI string `json:"uri"`
|
||||||
|
@ -157,93 +131,7 @@ type MediaDimensions struct {
|
||||||
Width int `json:"width"`
|
Width int `json:"width"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s Status) Display(noColor bool) string {
|
|
||||||
format := `
|
|
||||||
%s (@%s)
|
|
||||||
|
|
||||||
%s
|
|
||||||
%s
|
|
||||||
%s
|
|
||||||
%s
|
|
||||||
|
|
||||||
%s
|
|
||||||
%s
|
|
||||||
|
|
||||||
%s
|
|
||||||
Boosts: %d
|
|
||||||
Likes: %d
|
|
||||||
Replies: %d
|
|
||||||
|
|
||||||
%s
|
|
||||||
%s
|
|
||||||
|
|
||||||
%s
|
|
||||||
%s
|
|
||||||
`
|
|
||||||
|
|
||||||
return fmt.Sprintf(
|
|
||||||
format,
|
|
||||||
utilities.DisplayNameFormat(noColor, s.Account.DisplayName), s.Account.Username,
|
|
||||||
utilities.HeaderFormat(noColor, "CONTENT:"),
|
|
||||||
utilities.WrapLines(utilities.ConvertHTMLToText(s.Content), "\n ", 80),
|
|
||||||
utilities.HeaderFormat(noColor, "STATUS ID:"),
|
|
||||||
s.ID,
|
|
||||||
utilities.HeaderFormat(noColor, "CREATED AT:"),
|
|
||||||
utilities.FormatTime(s.CreatedAt),
|
|
||||||
utilities.HeaderFormat(noColor, "STATS:"),
|
|
||||||
s.ReblogsCount,
|
|
||||||
s.FavouritesCount,
|
|
||||||
s.RepliesCount,
|
|
||||||
utilities.HeaderFormat(noColor, "VISIBILITY:"),
|
|
||||||
s.Visibility,
|
|
||||||
utilities.HeaderFormat(noColor, "URL:"),
|
|
||||||
s.URL,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
type StatusListType int
|
|
||||||
|
|
||||||
const (
|
|
||||||
StatusListTimeline StatusListType = iota
|
|
||||||
StatusListBookMarks
|
|
||||||
)
|
|
||||||
|
|
||||||
type StatusList struct {
|
type StatusList struct {
|
||||||
Type StatusListType
|
|
||||||
Name string
|
Name string
|
||||||
Statuses []Status
|
Statuses []Status
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s StatusList) Display(noColor bool) string {
|
|
||||||
var builder strings.Builder
|
|
||||||
var name string
|
|
||||||
|
|
||||||
separator := "────────────────────────────────────────────────────────────────────────────────"
|
|
||||||
|
|
||||||
if s.Type == StatusListTimeline {
|
|
||||||
name = "TIMELINE: " + s.Name
|
|
||||||
} else {
|
|
||||||
name = s.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.WriteString(utilities.HeaderFormat(noColor, name) + "\n")
|
|
||||||
|
|
||||||
for _, status := range s.Statuses {
|
|
||||||
builder.WriteString("\n" + utilities.DisplayNameFormat(noColor, status.Account.DisplayName) + " (@" + status.Account.Acct + ")\n")
|
|
||||||
|
|
||||||
statusID := status.ID
|
|
||||||
createdAt := status.CreatedAt
|
|
||||||
|
|
||||||
if status.Reblog != nil {
|
|
||||||
builder.WriteString("reposted this status from " + utilities.DisplayNameFormat(noColor, status.Reblog.Account.DisplayName) + " (@" + status.Reblog.Account.Acct + ")\n")
|
|
||||||
statusID = status.Reblog.ID
|
|
||||||
createdAt = status.Reblog.CreatedAt
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.WriteString(utilities.WrapLines(utilities.ConvertHTMLToText(status.Content), "\n", 80) + "\n\n")
|
|
||||||
builder.WriteString(utilities.FieldFormat(noColor, "ID:") + " " + statusID + "\t" + utilities.FieldFormat(noColor, "Created at:") + " " + utilities.FormatTime(createdAt) + "\n")
|
|
||||||
builder.WriteString(separator + "\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
return builder.String()
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,7 +1,3 @@
|
||||||
// SPDX-FileCopyrightText: 2024 Dan Anglin <d.n.i.anglin@gmail.com>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
package model
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
|
@ -1,7 +1,3 @@
|
||||||
// SPDX-FileCopyrightText: 2024 Dan Anglin <d.n.i.anglin@gmail.com>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
package model
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
|
@ -1,7 +1,3 @@
|
||||||
// SPDX-FileCopyrightText: 2024 Dan Anglin <d.n.i.anglin@gmail.com>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
package model
|
package model
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
144
internal/printer/account.go
Normal file
144
internal/printer/account.go
Normal file
|
@ -0,0 +1,144 @@
|
||||||
|
package printer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"codeflow.dananglin.me.uk/apollo/enbas/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p Printer) PrintAccount(
|
||||||
|
account model.Account,
|
||||||
|
relationship *model.AccountRelationship,
|
||||||
|
preferences *model.Preferences,
|
||||||
|
statuses *model.StatusList,
|
||||||
|
userAccountID string,
|
||||||
|
) {
|
||||||
|
var builder strings.Builder
|
||||||
|
|
||||||
|
builder.WriteString("\n" + p.fullDisplayNameFormat(account.DisplayName, account.Acct))
|
||||||
|
builder.WriteString("\n\n" + p.headerFormat("ACCOUNT ID:"))
|
||||||
|
builder.WriteString("\n" + account.ID)
|
||||||
|
builder.WriteString("\n\n" + p.headerFormat("JOINED ON:"))
|
||||||
|
builder.WriteString("\n" + p.formatDate(account.CreatedAt))
|
||||||
|
builder.WriteString("\n\n" + p.headerFormat("STATS:"))
|
||||||
|
builder.WriteString("\n" + p.fieldFormat("Followers:"))
|
||||||
|
builder.WriteString(" " + strconv.Itoa(account.FollowersCount))
|
||||||
|
builder.WriteString("\n" + p.fieldFormat("Following:"))
|
||||||
|
builder.WriteString(" " + strconv.Itoa(account.FollowingCount))
|
||||||
|
builder.WriteString("\n" + p.fieldFormat("Statuses:"))
|
||||||
|
builder.WriteString(" " + strconv.Itoa(account.StatusCount))
|
||||||
|
builder.WriteString("\n\n" + p.headerFormat("BIOGRAPHY:"))
|
||||||
|
builder.WriteString(p.convertHTMLToText(account.Note, true))
|
||||||
|
builder.WriteString("\n\n" + p.headerFormat("METADATA:"))
|
||||||
|
|
||||||
|
for _, field := range account.Fields {
|
||||||
|
builder.WriteString("\n" + p.fieldFormat(field.Name) + ": " + p.convertHTMLToText(field.Value, false))
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.WriteString("\n\n" + p.headerFormat("ACCOUNT URL:"))
|
||||||
|
builder.WriteString("\n" + account.URL)
|
||||||
|
|
||||||
|
if relationship != nil {
|
||||||
|
builder.WriteString(p.accountRelationship(relationship))
|
||||||
|
}
|
||||||
|
|
||||||
|
if preferences != nil {
|
||||||
|
builder.WriteString(p.userPreferences(preferences))
|
||||||
|
}
|
||||||
|
|
||||||
|
if statuses != nil {
|
||||||
|
builder.WriteString("\n\n" + p.statusList(*statuses, userAccountID))
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.WriteString("\n\n")
|
||||||
|
|
||||||
|
p.print(builder.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Printer) accountRelationship(relationship *model.AccountRelationship) string {
|
||||||
|
var builder strings.Builder
|
||||||
|
|
||||||
|
builder.WriteString("\n\n" + p.headerFormat("YOUR RELATIONSHIP WITH THIS ACCOUNT:"))
|
||||||
|
builder.WriteString("\n" + p.fieldFormat("Following:"))
|
||||||
|
builder.WriteString(" " + strconv.FormatBool(relationship.Following))
|
||||||
|
builder.WriteString("\n" + p.fieldFormat("Is following you:"))
|
||||||
|
builder.WriteString(" " + strconv.FormatBool(relationship.FollowedBy))
|
||||||
|
builder.WriteString("\n" + p.fieldFormat("A follow request was sent and is pending:"))
|
||||||
|
builder.WriteString(" " + strconv.FormatBool(relationship.FollowRequested))
|
||||||
|
builder.WriteString("\n" + p.fieldFormat("Received a pending follow request:"))
|
||||||
|
builder.WriteString(" " + strconv.FormatBool(relationship.FollowRequestedBy))
|
||||||
|
builder.WriteString("\n" + p.fieldFormat("Endorsed:"))
|
||||||
|
builder.WriteString(" " + strconv.FormatBool(relationship.Endorsed))
|
||||||
|
builder.WriteString("\n" + p.fieldFormat("Showing Reposts (boosts):"))
|
||||||
|
builder.WriteString(" " + strconv.FormatBool(relationship.ShowingReblogs))
|
||||||
|
builder.WriteString("\n" + p.fieldFormat("Muted:"))
|
||||||
|
builder.WriteString(" " + strconv.FormatBool(relationship.Muting))
|
||||||
|
builder.WriteString("\n" + p.fieldFormat("Notifications muted:"))
|
||||||
|
builder.WriteString(" " + strconv.FormatBool(relationship.MutingNotifications))
|
||||||
|
builder.WriteString("\n" + p.fieldFormat("Blocking:"))
|
||||||
|
builder.WriteString(" " + strconv.FormatBool(relationship.Blocking))
|
||||||
|
builder.WriteString("\n" + p.fieldFormat("Is blocking you:"))
|
||||||
|
builder.WriteString(" " + strconv.FormatBool(relationship.BlockedBy))
|
||||||
|
builder.WriteString("\n" + p.fieldFormat("Blocking account's domain:"))
|
||||||
|
builder.WriteString(" " + strconv.FormatBool(relationship.DomainBlocking))
|
||||||
|
|
||||||
|
if relationship.PrivateNote != "" {
|
||||||
|
builder.WriteString("\n\n" + p.headerFormat("YOUR PRIVATE NOTE ABOUT THIS ACCOUNT:"))
|
||||||
|
builder.WriteString("\n" + p.wrapLines(relationship.PrivateNote, 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Printer) userPreferences(preferences *model.Preferences) string {
|
||||||
|
var builder strings.Builder
|
||||||
|
|
||||||
|
builder.WriteString("\n\n" + p.headerFormat("YOUR PREFERENCES:"))
|
||||||
|
|
||||||
|
builder.WriteString("\n" + p.fieldFormat("Default post language:"))
|
||||||
|
builder.WriteString(" " + preferences.PostingDefaultLanguage)
|
||||||
|
|
||||||
|
builder.WriteString("\n" + p.fieldFormat("Default post visibility:"))
|
||||||
|
builder.WriteString(" " + preferences.PostingDefaultVisibility)
|
||||||
|
|
||||||
|
builder.WriteString("\n" + p.fieldFormat("Mark posts as sensitive by default:"))
|
||||||
|
builder.WriteString(" " + strconv.FormatBool(preferences.PostingDefaultSensitive))
|
||||||
|
|
||||||
|
return builder.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Printer) PrintAccountList(list model.AccountList) {
|
||||||
|
var builder strings.Builder
|
||||||
|
|
||||||
|
builder.WriteString("\n")
|
||||||
|
|
||||||
|
switch list.Type {
|
||||||
|
case model.AccountListFollowers:
|
||||||
|
builder.WriteString(p.headerFormat("Followed by:"))
|
||||||
|
case model.AccountListFollowing:
|
||||||
|
builder.WriteString(p.headerFormat("Following:"))
|
||||||
|
case model.AccountListBlockedAccount:
|
||||||
|
builder.WriteString(p.headerFormat("Blocked accounts:"))
|
||||||
|
case model.AccountListFollowRequests:
|
||||||
|
builder.WriteString(p.headerFormat("Accounts that have requested to follow you:"))
|
||||||
|
case model.AccountListMuted:
|
||||||
|
builder.WriteString(p.headerFormat("Muted accounts:"))
|
||||||
|
default:
|
||||||
|
builder.WriteString(p.headerFormat("Accounts:"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if list.Type == model.AccountListBlockedAccount {
|
||||||
|
for ind := range list.Accounts {
|
||||||
|
builder.WriteString("\n" + symbolBullet + " " + list.Accounts[ind].Acct + " (" + list.Accounts[ind].ID + ")")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for ind := range list.Accounts {
|
||||||
|
builder.WriteString("\n" + symbolBullet + " " + p.fullDisplayNameFormat(list.Accounts[ind].DisplayName, list.Accounts[ind].Acct))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.WriteString("\n")
|
||||||
|
|
||||||
|
p.print(builder.String())
|
||||||
|
}
|
|
@ -1,8 +1,4 @@
|
||||||
// SPDX-FileCopyrightText: 2024 Dan Anglin <d.n.i.anglin@gmail.com>
|
package printer
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
package utilities
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
|
@ -23,7 +19,7 @@ type htmlConvertState struct {
|
||||||
orderedListIndex int
|
orderedListIndex int
|
||||||
}
|
}
|
||||||
|
|
||||||
func ConvertHTMLToText(text string) string {
|
func (p Printer) convertHTMLToText(text string, wrapLines bool) string {
|
||||||
var builder strings.Builder
|
var builder strings.Builder
|
||||||
|
|
||||||
state := htmlConvertState{
|
state := htmlConvertState{
|
||||||
|
@ -37,6 +33,10 @@ func ConvertHTMLToText(text string) string {
|
||||||
tt := token.Next()
|
tt := token.Next()
|
||||||
switch tt {
|
switch tt {
|
||||||
case html.ErrorToken:
|
case html.ErrorToken:
|
||||||
|
if wrapLines {
|
||||||
|
return p.wrapLines(builder.String(), 0)
|
||||||
|
}
|
||||||
|
|
||||||
return builder.String()
|
return builder.String()
|
||||||
case html.TextToken:
|
case html.TextToken:
|
||||||
text := token.Token().Data
|
text := token.Token().Data
|
||||||
|
@ -66,7 +66,7 @@ func processTagToken(state *htmlConvertState, writer io.StringWriter, tag string
|
||||||
case "<li>":
|
case "<li>":
|
||||||
switch state.htmlListType {
|
switch state.htmlListType {
|
||||||
case htmlUnorderedList:
|
case htmlUnorderedList:
|
||||||
_, _ = writer.WriteString("• ")
|
_, _ = writer.WriteString(symbolBullet + " ")
|
||||||
case htmlOrderedList:
|
case htmlOrderedList:
|
||||||
_, _ = writer.WriteString(strconv.Itoa(state.orderedListIndex) + ". ")
|
_, _ = writer.WriteString(strconv.Itoa(state.orderedListIndex) + ". ")
|
||||||
state.orderedListIndex++
|
state.orderedListIndex++
|
38
internal/printer/instance.go
Normal file
38
internal/printer/instance.go
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
package printer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"codeflow.dananglin.me.uk/apollo/enbas/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p Printer) PrintInstance(instance model.InstanceV2) {
|
||||||
|
var builder strings.Builder
|
||||||
|
|
||||||
|
builder.WriteString("\n" + p.headerFormat("INSTANCE TITLE:"))
|
||||||
|
builder.WriteString("\n" + instance.Title)
|
||||||
|
|
||||||
|
builder.WriteString("\n\n" + p.headerFormat("INSTANCE DESCRIPTION:"))
|
||||||
|
builder.WriteString("\n" + p.wrapLines(instance.DescriptionText, 0))
|
||||||
|
|
||||||
|
builder.WriteString("\n\n" + p.headerFormat("DOMAIN:"))
|
||||||
|
builder.WriteString("\n" + instance.Domain)
|
||||||
|
|
||||||
|
builder.WriteString("\n\n" + p.headerFormat("TERMS AND CONDITIONS:"))
|
||||||
|
builder.WriteString("\n" + p.wrapLines(instance.TermsText, 2))
|
||||||
|
|
||||||
|
builder.WriteString("\n\n" + p.headerFormat("VERSION:"))
|
||||||
|
builder.WriteString("\nRunning GoToSocial " + instance.Version)
|
||||||
|
|
||||||
|
builder.WriteString("\n\n" + p.headerFormat("CONTACT:"))
|
||||||
|
builder.WriteString("\n" + p.fieldFormat("Name:"))
|
||||||
|
builder.WriteString(" " + instance.Contact.Account.DisplayName)
|
||||||
|
builder.WriteString("\n" + p.fieldFormat("Username:"))
|
||||||
|
builder.WriteString(" " + instance.Contact.Account.Acct)
|
||||||
|
builder.WriteString("\n" + p.fieldFormat("Email:"))
|
||||||
|
builder.WriteString(" " + instance.Contact.Email)
|
||||||
|
|
||||||
|
builder.WriteString("\n\n")
|
||||||
|
|
||||||
|
p.print(builder.String())
|
||||||
|
}
|
45
internal/printer/list.go
Normal file
45
internal/printer/list.go
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
package printer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"codeflow.dananglin.me.uk/apollo/enbas/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p Printer) PrintList(list model.List) {
|
||||||
|
var builder strings.Builder
|
||||||
|
|
||||||
|
builder.WriteString("\n" + p.headerFormat("LIST TITLE:") + "\n")
|
||||||
|
builder.WriteString(list.Title + "\n\n")
|
||||||
|
builder.WriteString(p.headerFormat("LIST ID:") + "\n")
|
||||||
|
builder.WriteString(list.ID + "\n\n")
|
||||||
|
builder.WriteString(p.headerFormat("REPLIES POLICY:") + "\n")
|
||||||
|
builder.WriteString(list.RepliesPolicy.String() + "\n\n")
|
||||||
|
builder.WriteString(p.headerFormat("ADDED ACCOUNTS:"))
|
||||||
|
|
||||||
|
if len(list.Accounts) > 0 {
|
||||||
|
for acct, name := range list.Accounts {
|
||||||
|
builder.WriteString("\n" + symbolBullet + " " + p.fullDisplayNameFormat(name, acct))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
builder.WriteString("\n" + "None")
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.WriteString("\n")
|
||||||
|
|
||||||
|
printToStdout(builder.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Printer) PrintLists(lists []model.List) {
|
||||||
|
var builder strings.Builder
|
||||||
|
|
||||||
|
builder.WriteString("\n" + p.headerFormat("LISTS"))
|
||||||
|
|
||||||
|
for i := range lists {
|
||||||
|
builder.WriteString("\n" + symbolBullet + " " + lists[i].Title + " (" + lists[i].ID + ")")
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.WriteString("\n")
|
||||||
|
|
||||||
|
printToStdout(builder.String())
|
||||||
|
}
|
45
internal/printer/media_attachment.go
Normal file
45
internal/printer/media_attachment.go
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
package printer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"codeflow.dananglin.me.uk/apollo/enbas/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p Printer) PrintMediaAttachment(attachement model.Attachment) {
|
||||||
|
var builder strings.Builder
|
||||||
|
|
||||||
|
// The ID of the media attachment
|
||||||
|
builder.WriteString("\n" + p.headerFormat("MEDIA ATTACHMENT ID:"))
|
||||||
|
builder.WriteString("\n" + attachement.ID)
|
||||||
|
|
||||||
|
// The media attachment type
|
||||||
|
builder.WriteString("\n\n" + p.headerFormat("TYPE:"))
|
||||||
|
builder.WriteString("\n" + attachement.Type)
|
||||||
|
|
||||||
|
// The description that came with the media attachment (if any)
|
||||||
|
description := attachement.Description
|
||||||
|
if description == "" {
|
||||||
|
description = noMediaDescription
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.WriteString("\n\n" + p.headerFormat("DESCRIPTION:"))
|
||||||
|
builder.WriteString("\n" + description)
|
||||||
|
|
||||||
|
// The original size of the media attachment
|
||||||
|
builder.WriteString("\n\n" + p.headerFormat("ORIGINAL SIZE:"))
|
||||||
|
builder.WriteString("\n" + attachement.Meta.Original.Size)
|
||||||
|
|
||||||
|
// The media attachment's focus
|
||||||
|
builder.WriteString("\n\n" + p.headerFormat("FOCUS:"))
|
||||||
|
builder.WriteString("\n" + p.fieldFormat("x:") + " " + strconv.FormatFloat(attachement.Meta.Focus.X, 'f', 1, 64))
|
||||||
|
builder.WriteString("\n" + p.fieldFormat("y:") + " " + strconv.FormatFloat(attachement.Meta.Focus.Y, 'f', 1, 64))
|
||||||
|
|
||||||
|
// The URL to the source of the media attachment
|
||||||
|
builder.WriteString("\n\n" + p.headerFormat("URL:"))
|
||||||
|
builder.WriteString("\n" + attachement.URL)
|
||||||
|
builder.WriteString("\n\n")
|
||||||
|
|
||||||
|
p.print(builder.String())
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue