feat: init
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
decentral1se 2024-08-01 09:55:07 +02:00
commit a9363705e8
Signed by: decentral1se
GPG Key ID: 03789458B3D0C410
9 changed files with 580 additions and 0 deletions

8
.drone.yml Normal file
View File

@ -0,0 +1,8 @@
---
kind: pipeline
name: blurp
steps:
- name: build
image: golang:1.21
commands:
- go build -v ./...

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
archive

15
LICENSE Normal file
View File

@ -0,0 +1,15 @@
blurp: A GoToSocial status deletion tool.
Copyright (C) 2024 decentral1se
This program is free software: you can redistribute it and/or modify it under
the terms of the GNU Affero General Public License as published by the Free
Software Foundation, either version 3 of the License, or (at your option) any
later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE. See the GNU Affero General Public License for more
details.
You should have received a copy of the GNU Affero General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>.

37
Makefile Normal file
View File

@ -0,0 +1,37 @@
PROG := blurp
COMMIT := $(shell git rev-list -1 HEAD)
GCFLAGS := -gcflags="all=-l -B"
LDFLAGS := -ldflags="-X 'main.Commit=$(COMMIT)' -s -w"
ALLFLAGS := -v $(GCFLAGS) $(LDFLAGS)
default: build
check:
@test -z $$(gofmt -l .) || \
(echo "gofmt: formatting issue - run 'make format' to resolve" && exit 1)
format:
@gofmt -s -w .
build: check
@go build $(ALLFLAGS) .
install:
@go install $(ALLFLAGS) .
test:
@go test ./... -cover -v
clean:
@rm ./$(PROG)
upx:
@upx --lzma ./$(PROG)
release:
@goreleaser release --snapshot --clean
deps:
@go get -t -u ./...
.PHONY: check format build install test clean upx release deps

60
README.md Normal file
View File

@ -0,0 +1,60 @@
# blurp
[![Build Status](https://build.coopcloud.tech/api/badges/decentral1se/blurp/status.svg?ref=refs/heads/main)](https://build.coopcloud.tech/decentral1se/blurp)
> 🚧 Status: **ALPHA SOFTWARE** 🚧
A [GoToSocial](https://docs.gotosocial.org) status deletion tool.
## Install
It's ~ 3 MB in size. Only `linux/amd64` is supported atm.
```
curl https://git.coopcloud.tech/decentral1se/blurp/raw/branch/main/blurp -o blurp
chmod +x blurp
```
## Preparation
* `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` will remove statuses but not media attachments *immediately* for **local media**. Remote media is removed immediately. Unattached local media will be remove the next time the media cleaning job runs.
## Delete stuff 🔥
You'll nee to log in first:
```
blurp login -u foo@bar.zone
```
You might want to archive stuff offline before wiping it:
```
blurp archive
```
See `./archive` for every status (favourited, pinned, boosted, bookmarked etc.)
connected with your account. All media attachments are downloaded alongside the
status. Status content is dumped as a JSON file.
> 🔴 **DANGER ZONE** 🔴
To delete all statuses older than *2 weeks*:
```
blurp delete
```
> 🔴 **DANGER ZONE** 🔴
## ACK
Made possible by the good work of [slurp](https://github.com/VyrCossont/slurp).
# License
<a href="https://git.coopcloud.tech/decentral1se/blurp/src/branch/main/LICENSE">
<img src="https://www.gnu.org/graphics/agplv3-with-text-162x68.png" />
</a>

BIN
blurp Executable file

Binary file not shown.

298
blurp.go Normal file
View File

@ -0,0 +1,298 @@
package main
import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"log/slog"
"net/http"
"net/url"
"os"
"path/filepath"
"time"
auth "git.coopcloud.tech/decentral1se/gtslib-auth-keyring"
"git.coopcloud.tech/decentral1se/gtslib/client/accounts"
"git.coopcloud.tech/decentral1se/gtslib/client/statuses"
"git.coopcloud.tech/decentral1se/gtslib/models"
"github.com/peterhellberg/link"
"github.com/pkg/errors"
"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
}
resp, err := authClient.Client.Accounts.AccountVerify(nil, authClient.Auth)
if err != nil {
return nil, errors.WithStack(err)
}
return resp.GetPayload(), nil
}
func main() {
if err := rootCmd.Execute(); err != nil {
slog.Error("woops, something went wrong", "error", err)
}
}
var rootCmd = &cobra.Command{
Use: "blurp",
Short: "A GoToSocial status deletion tool",
}
var authLoginCmd = &cobra.Command{
Use: "login",
Short: "Log in",
RunE: func(cmd *cobra.Command, args []string) error {
return auth.Login(user)
},
}
var archiveCmd = &cobra.Command{
Use: "archive",
Short: "Archive statuses",
RunE: func(cmd *cobra.Command, args []string) error {
authClient, err := auth.NewAuthClient(user)
if err != nil {
slog.Error("unable to create auth client", "error", err)
}
acc, err := getAccount(authClient)
if err != nil {
slog.Error("unable to retrieve account", "error", err)
}
pagedRequester := &statusPagedRequester{accID: acc.ID}
statuses, err := ReadAllPaged(authClient, pagedRequester)
if err != nil {
slog.Error("unable to download paged response", "error", err)
}
basePath := filepath.Join(".", "archive")
if err := os.MkdirAll(basePath, 0755); err != nil {
slog.Error("unable to create status directory", "error", err)
}
for _, status := range statuses {
basePath = filepath.Join(".", "archive")
if len(status.MediaAttachments) > 0 {
basePath = filepath.Join(basePath, status.ID)
if err := os.MkdirAll(basePath, 0755); err != nil {
slog.Error("unable to create status directory", "error", err)
}
for _, media := range status.MediaAttachments {
parsed, err := url.Parse(media.URL)
if err != nil {
slog.Error("unable to parse media URL", "error", err)
}
imagePath := filepath.Join(basePath, filepath.Base(parsed.Path))
if _, err := os.Stat(imagePath); errors.Is(err, os.ErrNotExist) {
if err := httpGetFile(imagePath, media.URL); err != nil {
slog.Error("unable to download file", "error", err)
}
slog.Info(fmt.Sprintf("archived %s", imagePath))
}
}
}
payload, err := json.MarshalIndent(status, "", " ")
if err != nil {
slog.Error("unable to marshal", "error", err)
}
jsonPath := filepath.Join(basePath, fmt.Sprintf("%s.json", status.ID))
if _, err := os.Stat(jsonPath); errors.Is(err, os.ErrNotExist) {
if err = ioutil.WriteFile(jsonPath, payload, 0644); err != nil {
slog.Error("unable to write JSON file", "error", err)
}
slog.Info(fmt.Sprintf("archived %s", jsonPath))
}
}
return nil
},
}
// httpGetFile downloads a file from the internet.
func httpGetFile(filepath, url string) error {
out, err := os.Create(filepath)
if err != nil {
return fmt.Errorf("httpGetFile: unable to create '%s': %s", filepath, err)
}
defer out.Close()
resp, err := http.Get(url)
if err != nil {
return fmt.Errorf("httpGetFile: unable to HTTP GET '%s'", url)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("httpGetFile: HTTP GET response code %v for '%s'", resp.StatusCode, url)
}
_, err = io.Copy(out, resp.Body)
if err != nil {
return fmt.Errorf("httpGetFile: unable to copy HTTP GET response to disk: %s", err)
}
return nil
}
type PagedRequester[Response PagedResponse[Element], Element any] interface {
Request(authClient *auth.Client, maxID *string) (Response, error)
}
type PagedResponse[Element any] interface {
Link() string
Elements() []Element
}
// 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.
return nil, nil
}
nextUrl, err := url.Parse(next.URI)
if err != nil {
return nil, errors.Wrap(err, "couldn't parse next page URL")
}
nextMaxID := nextUrl.Query().Get("max_id")
if nextMaxID == "" {
return nil, errors.New("couldn't find next page max ID")
}
return &nextMaxID, err
}
func ReadAllPaged[
Requester PagedRequester[Response, Element],
Response PagedResponse[Element], Element any](
authClient *auth.Client, pagedRequester Requester) ([]Element, error) {
var all []Element
var maxID *string
for {
err := authClient.Wait()
if err != nil {
return all, errors.WithStack(err)
}
pagedResponse, err := pagedRequester.Request(authClient, maxID)
if err != nil {
slog.Error("error fetching page", "error", err)
return all, errors.WithStack(err)
}
maxID, err = ParseLinkMaxID(pagedResponse.Link())
if err != nil {
slog.Error("error parsing Link header", "error", err)
return all, errors.WithStack(err)
}
if maxID == nil {
// End of pages.
break
}
all = append(all, pagedResponse.Elements()...)
}
return all, nil
}
type statusPagedRequester struct {
accID string
}
type statusPagedResponse struct {
resp *accounts.AccountStatusesOK
}
func (pagedResponse *statusPagedResponse) Link() string {
return pagedResponse.resp.Link
}
func (pagedResponse *statusPagedResponse) Elements() []*models.Status {
return pagedResponse.resp.GetPayload()
}
func (pagedRequester *statusPagedRequester) Request(
authClient *auth.Client, maxID *string) (*statusPagedResponse, error) {
resp, err := authClient.Client.Accounts.AccountStatuses(&accounts.AccountStatusesParams{
ID: pagedRequester.accID,
MaxID: maxID,
}, authClient.Auth)
if err != nil {
return nil, err
}
return &statusPagedResponse{resp}, 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)
}
pagedRequester := &statusPagedRequester{accID: acc.ID}
allStatuses, err := ReadAllPaged(authClient, pagedRequester)
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(authLoginCmd)
rootCmd.AddCommand(archiveCmd)
rootCmd.AddCommand(deleteCmd)
}

50
go.mod Normal file
View File

@ -0,0 +1,50 @@
module git.coopcloud.tech/decentral1se/blurp
go 1.21.1
require (
git.coopcloud.tech/decentral1se/gtslib v0.0.0-20240731163334-5a7b6157993c
git.coopcloud.tech/decentral1se/gtslib-auth-keyring v0.0.0-20240731165536-91d7360746a5
github.com/peterhellberg/link v1.2.0
github.com/pkg/errors v0.9.1
github.com/spf13/cobra v1.8.1
)
require (
github.com/adrg/xdg v0.5.0 // indirect
github.com/alessio/shellescape v1.4.2 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/danieljoos/wincred v1.2.2 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/analysis v0.23.0 // indirect
github.com/go-openapi/errors v0.22.0 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/loads v0.22.0 // indirect
github.com/go-openapi/runtime v0.28.0 // indirect
github.com/go-openapi/spec v0.21.0 // indirect
github.com/go-openapi/strfmt v0.23.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/go-openapi/validate v0.24.0 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/oklog/ulid v1.3.1 // indirect
github.com/opentracing/opentracing-go v1.2.0 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/zalando/go-keyring v0.2.5 // indirect
go.mongodb.org/mongo-driver v1.16.0 // indirect
go.opentelemetry.io/otel v1.28.0 // indirect
go.opentelemetry.io/otel/metric v1.28.0 // indirect
go.opentelemetry.io/otel/trace v1.28.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.22.0 // indirect
golang.org/x/time v0.5.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
webfinger.net/go/webfinger v0.1.0 // indirect
)

111
go.sum Normal file
View File

@ -0,0 +1,111 @@
git.coopcloud.tech/decentral1se/gtslib v0.0.0-20240731163334-5a7b6157993c h1:hi7jaEF5J5zEFurV2N5P1JTIPMtDgMTeQVis8b1J6Uk=
git.coopcloud.tech/decentral1se/gtslib v0.0.0-20240731163334-5a7b6157993c/go.mod h1:isFkvbxjltfWUlzdVETbupmKLZvDf5XviBkgd2/+A5M=
git.coopcloud.tech/decentral1se/gtslib-auth-keyring v0.0.0-20240731165536-91d7360746a5 h1:/enn25rJ8hfI15YHd/ATJpOGTsb62uVnQWK//KYFz6c=
git.coopcloud.tech/decentral1se/gtslib-auth-keyring v0.0.0-20240731165536-91d7360746a5/go.mod h1:fWMf/Wlc6Qjosg4xoaJg0UOb9GHoP7GrlIXz38wZAws=
github.com/adrg/xdg v0.5.0 h1:dDaZvhMXatArP1NPHhnfaQUqWBLBsmx1h1HXQdMoFCY=
github.com/adrg/xdg v0.5.0/go.mod h1:dDdY4M4DF9Rjy4kHPeNL+ilVF+p2lK8IdM9/rTSGcI4=
github.com/alessio/shellescape v1.4.2 h1:MHPfaU+ddJ0/bYWpgIeUnQUqKrlJ1S7BfEYPM4uEoM0=
github.com/alessio/shellescape v1.4.2/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0=
github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7nd/ogr0Uh8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/analysis v0.23.0 h1:aGday7OWupfMs+LbmLZG4k0MYXIANxcuBTYUC03zFCU=
github.com/go-openapi/analysis v0.23.0/go.mod h1:9mz9ZWaSlV8TvjQHLl2mUW2PbZtemkE8yA5v22ohupo=
github.com/go-openapi/errors v0.22.0 h1:c4xY/OLxUBSTiepAg3j/MHuAv5mJhnf53LLMWFB+u/w=
github.com/go-openapi/errors v0.22.0/go.mod h1:J3DmZScxCDufmIMsdOuDHxJbdOGC0xtUynjIx092vXE=
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
github.com/go-openapi/loads v0.22.0 h1:ECPGd4jX1U6NApCGG1We+uEozOAvXvJSF4nnwHZ8Aco=
github.com/go-openapi/loads v0.22.0/go.mod h1:yLsaTCS92mnSAZX5WWoxszLj0u+Ojl+Zs5Stn1oF+rs=
github.com/go-openapi/runtime v0.28.0 h1:gpPPmWSNGo214l6n8hzdXYhPuJcGtziTOgUpvsFWGIQ=
github.com/go-openapi/runtime v0.28.0/go.mod h1:QN7OzcS+XuYmkQLw05akXk0jRH/eZ3kb18+1KwW9gyc=
github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY=
github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=
github.com/go-openapi/strfmt v0.23.0 h1:nlUS6BCqcnAk0pyhi9Y+kdDVZdZMHfEKQiS4HaMgO/c=
github.com/go-openapi/strfmt v0.23.0/go.mod h1:NrtIpfKtWIygRkKVsxh7XQMDQW5HKQl6S5ik2elW+K4=
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58=
github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
github.com/peterhellberg/link v1.2.0 h1:UA5pg3Gp/E0F2WdX7GERiNrPQrM1K6CVJUUWfHa4t6c=
github.com/peterhellberg/link v1.2.0/go.mod h1:gYfAh+oJgQu2SrZHg5hROVRQe1ICoK0/HHJTcE0edxc=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/zalando/go-keyring v0.2.5 h1:Bc2HHpjALryKD62ppdEzaFG6VxL6Bc+5v0LYpN8Lba8=
github.com/zalando/go-keyring v0.2.5/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk=
go.mongodb.org/mongo-driver v1.16.0 h1:tpRsfBJMROVHKpdGyc1BBEzzjDUWjItxbVSZ8Ls4BQ4=
go.mongodb.org/mongo-driver v1.16.0/go.mod h1:oB6AhJQvFQL4LEHyXi6aJzQJtBiTQHiAd83l0GdFaiw=
go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo=
go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4=
go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q=
go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s=
go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw=
go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg=
go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g=
go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
webfinger.net/go/webfinger v0.1.0 h1:e/J18UgjFE8+ZbKxzKm4+gv4ehidNnF6hcbHwS3K63U=
webfinger.net/go/webfinger v0.1.0/go.mod h1:+najbdnIKfnKo68tU2TF+AXm8/MOqLYXqx22j8Xw7FM=