Compare commits

...

2 commits

Author SHA1 Message Date
402f371c09
checkpoint: created code for deleting statuses (need testing of course) 2024-08-16 14:51:11 +01:00
3037af60ed
feat: mute and unmute statuses
This commit adds support for muting and unmuting statuses. When viewing
a status the user can now see whether they've muted the status or not.
A status can only be muted by the user if they own it or are mentioned
in it.

PR: apollo/enbas#47
Resolves: apollo/enbas#46
2024-08-16 14:42:57 +01:00
10 changed files with 277 additions and 6 deletions

View file

@ -604,11 +604,29 @@ enbas show --type liked
### Mute a status ### Mute a status
_Not yet supported_ 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
_Not yet supported_ 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 ### Vote in a poll within a status

16
example/config.json Normal file
View file

@ -0,0 +1,16 @@
{
"credentialsFile": "~/.config/enbas/credentials.json",
"cacheDirectory": "~/.cache/enbas",
"lineWrapMaxWidth": 80,
"http": {
"timeout": 5,
"mediaTimeout": 30
},
"integrations": {
"browser": "firefox",
"editor": "vim",
"pager": "less -FIRX",
"imageViewer": "feh --scale-down",
"videoPlayer": "mpv"
}
}

View file

@ -241,7 +241,7 @@ func (g *Client) ReblogStatus(statusID string) error {
if err := g.sendRequest(params); err != nil { if err := g.sendRequest(params); err != nil {
return fmt.Errorf( return fmt.Errorf(
"received an error after sending the request to reblog the status; %w", "received an error after sending the request to reblog the status: %w",
err, err,
) )
} }
@ -262,10 +262,75 @@ func (g *Client) UnreblogStatus(statusID string) error {
if err := g.sendRequest(params); err != nil { if err := g.sendRequest(params); err != nil {
return fmt.Errorf( return fmt.Errorf(
"received an error after sending the request to un-reblog the status; %w", "received an error after sending the request to un-reblog the status: %w",
err, err,
) )
} }
return nil 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
}

View file

@ -1,9 +1,12 @@
package executor package executor
import ( import (
"errors"
"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"
) )
func (d *DeleteExecutor) Execute() error { func (d *DeleteExecutor) Execute() error {
@ -13,6 +16,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]
@ -41,3 +45,54 @@ func (d *DeleteExecutor) deleteList(gtsClient *client.Client) error {
return nil return nil
} }
func (d *DeleteExecutor) deleteStatus(gtsClient *client.Client) error {
if d.statusID == "" {
return FlagNotSetError{flagText: flagStatusID}
}
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 errors.New("unable to delete the status because you are not the owner")
}
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 := filepath.Join(
utilities.CalculateCacheDir(
d.config.CacheDirectory,
utilities.GetFQDN(gtsClient.Authentication.Instance),
),
"statuses",
)
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 status' text was successfully written to '" + path + "'.")
}
return nil
}

View file

@ -199,6 +199,8 @@ type DeleteExecutor struct {
printer *printer.Printer printer *printer.Printer
config *config.Config config *config.Config
listID string listID string
saveText bool
statusID string
resourceType string resourceType string
} }
@ -215,6 +217,8 @@ func NewDeleteExecutor(
exe.Usage = usage.ExecutorUsageFunc("delete", "Deletes a specific resource", exe.FlagSet) exe.Usage = usage.ExecutorUsageFunc("delete", "Deletes a specific resource", exe.FlagSet)
exe.StringVar(&exe.listID, "list-id", "", "The ID of the list in question") exe.StringVar(&exe.listID, "list-id", "", "The ID of the list in question")
exe.BoolVar(&exe.saveText, "save-text", false, "Set to true to save the text of the deleted status")
exe.StringVar(&exe.statusID, "status-id", "", "The ID of the status")
exe.StringVar(&exe.resourceType, "type", "", "The type of resource you want to action on (e.g. account, status)") exe.StringVar(&exe.resourceType, "type", "", "The type of resource you want to action on (e.g. account, status)")
return &exe return &exe
@ -347,6 +351,7 @@ type MuteExecutor struct {
accountName internalFlag.StringSliceValue accountName internalFlag.StringSliceValue
muteDuration internalFlag.TimeDurationValue muteDuration internalFlag.TimeDurationValue
muteNotifications bool muteNotifications bool
statusID string
resourceType string resourceType string
} }
@ -367,6 +372,7 @@ func NewMuteExecutor(
exe.Var(&exe.accountName, "account-name", "The name of the account") exe.Var(&exe.accountName, "account-name", "The name of the account")
exe.Var(&exe.muteDuration, "mute-duration", "Specify how long the mute should last for. To mute indefinitely, set this to 0s") exe.Var(&exe.muteDuration, "mute-duration", "Specify how long the mute should last for. To mute indefinitely, set this to 0s")
exe.BoolVar(&exe.muteNotifications, "mute-notifications", false, "Set to true to mute notifications as well as posts") exe.BoolVar(&exe.muteNotifications, "mute-notifications", false, "Set to true to mute notifications as well as posts")
exe.StringVar(&exe.statusID, "status-id", "", "The ID of the status")
exe.StringVar(&exe.resourceType, "type", "", "The type of resource you want to action on (e.g. account, status)") exe.StringVar(&exe.resourceType, "type", "", "The type of resource you want to action on (e.g. account, status)")
return &exe return &exe
@ -591,6 +597,7 @@ type UnmuteExecutor struct {
printer *printer.Printer printer *printer.Printer
config *config.Config config *config.Config
accountName internalFlag.StringSliceValue accountName internalFlag.StringSliceValue
statusID string
resourceType string resourceType string
} }
@ -608,6 +615,7 @@ func NewUnmuteExecutor(
exe.Usage = usage.ExecutorUsageFunc("unmute", "Umutes a specific resource (e.g. an account)", exe.FlagSet) exe.Usage = usage.ExecutorUsageFunc("unmute", "Umutes a specific resource (e.g. an account)", exe.FlagSet)
exe.Var(&exe.accountName, "account-name", "The name of the account") exe.Var(&exe.accountName, "account-name", "The name of the account")
exe.StringVar(&exe.statusID, "status-id", "", "The ID of the status")
exe.StringVar(&exe.resourceType, "type", "", "The type of resource you want to action on (e.g. account, status)") exe.StringVar(&exe.resourceType, "type", "", "The type of resource you want to action on (e.g. account, status)")
return &exe return &exe

View file

@ -1,6 +1,7 @@
package executor package executor
import ( import (
"errors"
"fmt" "fmt"
"codeflow.dananglin.me.uk/apollo/enbas/internal/client" "codeflow.dananglin.me.uk/apollo/enbas/internal/client"
@ -9,6 +10,7 @@ import (
func (m *MuteExecutor) Execute() error { func (m *MuteExecutor) Execute() error {
funcMap := map[string]func(*client.Client) error{ funcMap := map[string]func(*client.Client) error{
resourceAccount: m.muteAccount, resourceAccount: m.muteAccount,
resourceStatus: m.muteStatus,
} }
doFunc, ok := funcMap[m.resourceType] doFunc, ok := funcMap[m.resourceType]
@ -43,3 +45,44 @@ func (m *MuteExecutor) muteAccount(gtsClient *client.Client) error {
return nil return nil
} }
func (m *MuteExecutor) muteStatus(gtsClient *client.Client) error {
if m.statusID == "" {
return FlagNotSetError{flagText: flagStatusID}
}
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 errors.New("unable to mute the status because you are not the owner and you are not 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
}

View file

@ -1,6 +1,7 @@
package executor package executor
import ( import (
"errors"
"fmt" "fmt"
"codeflow.dananglin.me.uk/apollo/enbas/internal/client" "codeflow.dananglin.me.uk/apollo/enbas/internal/client"
@ -9,6 +10,7 @@ import (
func (m *UnmuteExecutor) Execute() error { func (m *UnmuteExecutor) Execute() error {
funcMap := map[string]func(*client.Client) error{ funcMap := map[string]func(*client.Client) error{
resourceAccount: m.unmuteAccount, resourceAccount: m.unmuteAccount,
resourceStatus: m.unmuteStatus,
} }
doFunc, ok := funcMap[m.resourceType] doFunc, ok := funcMap[m.resourceType]
@ -38,3 +40,44 @@ func (m *UnmuteExecutor) unmuteAccount(gtsClient *client.Client) error {
return nil return nil
} }
func (m *UnmuteExecutor) unmuteStatus(gtsClient *client.Client) error {
if m.statusID == "" {
return FlagNotSetError{flagText: flagStatusID}
}
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 errors.New("unable to unmute the status because you are not the owner and you are not 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
}

View file

@ -66,6 +66,7 @@ func (p Printer) PrintStatus(status model.Status, userAccountID string) {
builder.WriteString("\n" + p.fieldFormat("Boosted: ") + strconv.FormatBool(status.Reblogged)) builder.WriteString("\n" + p.fieldFormat("Boosted: ") + strconv.FormatBool(status.Reblogged))
builder.WriteString("\n" + p.fieldFormat("Liked: ") + strconv.FormatBool(status.Favourited)) builder.WriteString("\n" + p.fieldFormat("Liked: ") + strconv.FormatBool(status.Favourited))
builder.WriteString("\n" + p.fieldFormat("Bookmarked: ") + strconv.FormatBool(status.Bookmarked)) builder.WriteString("\n" + p.fieldFormat("Bookmarked: ") + strconv.FormatBool(status.Bookmarked))
builder.WriteString("\n" + p.fieldFormat("Muted: ") + strconv.FormatBool(status.Muted))
// Status visibility // Status visibility
builder.WriteString("\n\n" + p.headerFormat("VISIBILITY:")) builder.WriteString("\n\n" + p.headerFormat("VISIBILITY:"))

View file

@ -55,3 +55,17 @@ func FileExists(path string) (bool, error) {
return true, nil return true, nil
} }
func SaveTextToFile(path, text string) error {
file, err := os.Create(path)
if err != nil {
return fmt.Errorf("unable to open %q: %w", path, err)
}
defer file.Close()
if _, err := fmt.Fprint(file, text); err != nil {
return fmt.Errorf("received an error writing the text to the file: %w", err)
}
return nil
}

View file

@ -152,6 +152,10 @@
"type": "StringSliceValue", "type": "StringSliceValue",
"description": "A poll option. Use this multiple times to set multiple options" "description": "A poll option. Use this multiple times to set multiple options"
}, },
"save-text": {
"type": "bool",
"description": "Set to true to save the text of the deleted status"
},
"sensitive": { "sensitive": {
"type": "BoolPtrValue", "type": "BoolPtrValue",
"description": "Set to true if the status should be marked as sensitive" "description": "Set to true if the status should be marked as sensitive"
@ -278,6 +282,8 @@
"additionalFields": [], "additionalFields": [],
"flags": [ "flags": [
{ "flag": "list-id", "fieldName": "listID", "default": ""}, { "flag": "list-id", "fieldName": "listID", "default": ""},
{ "flag": "save-text", "default": "false" },
{ "flag": "status-id", "fieldName": "statusID", "default": "" },
{ "flag": "type", "fieldName": "resourceType", "default": "" } { "flag": "type", "fieldName": "resourceType", "default": "" }
], ],
"summary": "Deletes a specific resource", "summary": "Deletes a specific resource",
@ -335,6 +341,7 @@
{ "flag": "account-name" }, { "flag": "account-name" },
{ "flag": "mute-duration" }, { "flag": "mute-duration" },
{ "flag": "mute-notifications", "default": "false" }, { "flag": "mute-notifications", "default": "false" },
{ "flag": "status-id", "fieldName": "statusID", "default": "" },
{ "flag": "type", "fieldName": "resourceType", "default": "" } { "flag": "type", "fieldName": "resourceType", "default": "" }
], ],
"summary": "Mutes a specific resource (e.g. an account)", "summary": "Mutes a specific resource (e.g. an account)",
@ -427,6 +434,7 @@
"additionalFields": [], "additionalFields": [],
"flags": [ "flags": [
{ "flag": "account-name" }, { "flag": "account-name" },
{ "flag": "status-id", "fieldName": "statusID", "default": "" },
{ "flag": "type", "fieldName": "resourceType", "default": "" } { "flag": "type", "fieldName": "resourceType", "default": "" }
], ],
"summary": "Umutes a specific resource (e.g. an account)", "summary": "Umutes a specific resource (e.g. an account)",