diff --git a/README.md b/README.md index 1aa3db9..6937f83 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,11 @@ chmod +x blurp > also shifting sands). This is a best-effort explanation and might quickly be > out of date. Help welcome! 🟡 -* `blurp delete` will get you rate limited by your own instance. I temporarily solved this by [turning off rate limiting](https://docs.gotosocial.org/en/latest/api/ratelimiting/#can-i-configure-the-rate-limit-can-i-just-turn-it-off), running the command and then turning rate limiting back on again. I could probably implement some backoff in the code but this is just easier. +* `blurp delete` would easily get you [rate + limited](https://docs.gotosocial.org/en/latest/api/ratelimiting/) by your own + instance due to the high volume of delete requests that are sent. To avoid + this, we send 1 request every second which avoids limits in the default + settings. If you have different defaults, pass `--rate/-r`. * `blurp delete` will remove statuses but not media attachments *immediately* for **local media**. Remote media is removed immediately. Unattached local @@ -75,6 +79,10 @@ To delete all statuses older than *2 weeks*: blurp delete ``` +You can use `--weeks/-w` to supply a value for "number of weeks". If you need +to send less requests, say, 1 request every 3 seconds, you can pass `-r 3`. See +`--help` for more. + > 🔴 **DANGER ZONE** 🔴 ## ACK diff --git a/blurp b/blurp index 6ed0504..6dee445 100755 Binary files a/blurp and b/blurp differ diff --git a/blurp.go b/blurp.go index 21d8b06..b63a1ea 100644 --- a/blurp.go +++ b/blurp.go @@ -21,19 +21,22 @@ import ( "github.com/spf13/cobra" ) -// getAccount returns the currently authenticated account. -func getAccount(authClient *auth.Client) (*models.Account, error) { - err := authClient.Wait() - if err != nil { - return nil, err - } +var ( + user string + weeks int + rate int +) - resp, err := authClient.Client.Accounts.AccountVerify(nil, authClient.Auth) - if err != nil { - return nil, errors.WithStack(err) - } +func init() { + loginCmd.Flags().StringVarP(&user, "user", "u", "", "username@domain of account") + rootCmd.AddCommand(loginCmd) - return resp.GetPayload(), nil + archiveCmd.Flags().IntVarP(&rate, "rate", "r", 1, "send a request every 'r' seconds") + rootCmd.AddCommand(archiveCmd) + + deleteCmd.Flags().IntVarP(&weeks, "weeks", "w", 2, "keep statuses NEWER than no. of weeks") + deleteCmd.Flags().IntVarP(&rate, "rate", "r", 1, "send a request every 'r' seconds") + rootCmd.AddCommand(deleteCmd) } // main is the command-line entrypoint. @@ -73,7 +76,7 @@ var archiveCmd = &cobra.Command{ slog.Error("unable to retrieve account", "error", err) } - statuses, err := ReadAllPaged(authClient, acc.ID) + statuses, err := readAllPaged(authClient, acc.ID) if err != nil { slog.Error("unable to download paged response", "error", err) } @@ -127,6 +130,70 @@ var archiveCmd = &cobra.Command{ }, } +var deleteCmd = &cobra.Command{ + Use: "delete", + Short: "Delete statuses like tears in the rain", + RunE: func(cmd *cobra.Command, args []string) error { + authClient, err := auth.NewAuthClient(user) + if err != nil { + slog.Error("unable to create auth client", err) + } + + slog.Info(fmt.Sprintf("keeping statuses NEWER than %d weeks", weeks)) + + acc, err := getAccount(authClient) + if err != nil { + slog.Error("unable to retrieve account", "error", err) + } + + allStatuses, err := readAllPaged(authClient, acc.ID) + if err != nil { + slog.Error("unable to download paged response", "error", err) + } + + ISO8601 := "2006-01-02T15:04:05.000Z" + for _, status := range allStatuses { + t, err := time.Parse(ISO8601, status.CreatedAt) + if err != nil { + slog.Error("unable to parse status 'CreatedAt' value", "error", err) + } + + numHours := time.Duration(168 * weeks) + if t.Before(time.Now().Add(-time.Hour * numHours)) { + _, err := authClient.Client.Statuses.StatusDelete(&statuses.StatusDeleteParams{ + ID: status.ID, + }, authClient.Auth) + if err != nil { + slog.Error("unable to delete status", "error", err) + } + + slog.Info(fmt.Sprintf("deleted %s (created: %s)", status.ID, t.Format(time.DateOnly))) + + time.Sleep(time.Duration(rate)) + } else { + slog.Info(fmt.Sprintf("keeping %s (created: %s)", status.ID, t.Format(time.DateOnly))) + } + } + + return nil + }, +} + +// getAccount returns the currently authenticated account. +func getAccount(authClient *auth.Client) (*models.Account, error) { + err := authClient.Wait() + if err != nil { + return nil, err + } + + resp, err := authClient.Client.Accounts.AccountVerify(nil, authClient.Auth) + if err != nil { + return nil, errors.WithStack(err) + } + + return resp.GetPayload(), nil +} + // httpGetFile downloads a file from the internet. func httpGetFile(filepath, url string) error { out, err := os.Create(filepath) @@ -153,8 +220,8 @@ func httpGetFile(filepath, url string) error { return nil } -// ParseLinkMaxID extracts the `max_id` from the `next` link for paging to older items. -func ParseLinkMaxID(linkHeader string) (*string, error) { +// parseLinkMaxID extracts the `max_id` from the `next` link for paging to older items. +func parseLinkMaxID(linkHeader string) (*string, error) { next := link.Parse(linkHeader)["next"] if next == nil { // No link header in that direction means end of results. @@ -171,7 +238,7 @@ func ParseLinkMaxID(linkHeader string) (*string, error) { return &nextMaxID, err } -func ReadAllPaged(authClient *auth.Client, accID string) ([]*models.Status, error) { +func readAllPaged(authClient *auth.Client, accID string) ([]*models.Status, error) { var all []*models.Status var maxID *string @@ -188,7 +255,7 @@ func ReadAllPaged(authClient *auth.Client, accID string) ([]*models.Status, erro return all, errors.WithStack(err) } - maxID, err = ParseLinkMaxID(resp.Link) + maxID, err = parseLinkMaxID(resp.Link) if err != nil { slog.Error("error parsing Link header", "error", err) return all, errors.WithStack(err) @@ -198,64 +265,10 @@ func ReadAllPaged(authClient *auth.Client, accID string) ([]*models.Status, erro break } + time.Sleep(time.Duration(rate)) + all = append(all, resp.GetPayload()...) } return all, nil } - -var deleteCmd = &cobra.Command{ - Use: "delete", - Short: "Delete statuses like tears in the rain", - RunE: func(cmd *cobra.Command, args []string) error { - authClient, err := auth.NewAuthClient(user) - if err != nil { - slog.Error("unable to create auth client", err) - } - - acc, err := getAccount(authClient) - if err != nil { - slog.Error("unable to retrieve account", err) - } - - allStatuses, err := ReadAllPaged(authClient, acc.ID) - if err != nil { - slog.Error("unable to download paged response", err) - } - - ISO8601 := "2006-01-02T15:04:05.000Z" - for _, status := range allStatuses { - t, err := time.Parse(ISO8601, status.CreatedAt) - if err != nil { - slog.Error("unable to parse status 'CreatedAt' value", err) - } - - // NOTE(d1): 336 hours = 2 weeks - if t.Before(time.Now().Add(-time.Hour * 336)) { - _, err := authClient.Client.Statuses.StatusDelete(&statuses.StatusDeleteParams{ - ID: status.ID, - }, authClient.Auth) - if err != nil { - slog.Error("unable to delete status", err) - } - slog.Info(fmt.Sprintf("deleted %s (created: %s)", status.ID, t.Format(time.DateOnly))) - } else { - slog.Info(fmt.Sprintf("keeping %s (created: %s)", status.ID, t.Format(time.DateOnly))) - } - } - - return nil - }, -} - -var user string - -func init() { - rootCmd.PersistentFlags().StringVarP( - &user, "user", "u", "", "username@domain of account", - ) - - rootCmd.AddCommand(loginCmd) - rootCmd.AddCommand(archiveCmd) - rootCmd.AddCommand(deleteCmd) -}