commit a9363705e8bdf089ac98d8f5cbf887405efd4afd Author: decentral1se Date: Thu Aug 1 09:55:07 2024 +0200 feat: init diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..a57beb8 --- /dev/null +++ b/.drone.yml @@ -0,0 +1,8 @@ +--- +kind: pipeline +name: blurp +steps: + - name: build + image: golang:1.21 + commands: + - go build -v ./... diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f1d43c6 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +archive diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..42dfd5e --- /dev/null +++ b/LICENSE @@ -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 . diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..53e1566 --- /dev/null +++ b/Makefile @@ -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..a851960 --- /dev/null +++ b/README.md @@ -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 + + + + diff --git a/blurp b/blurp new file mode 100755 index 0000000..3618533 Binary files /dev/null and b/blurp differ diff --git a/blurp.go b/blurp.go new file mode 100644 index 0000000..475718d --- /dev/null +++ b/blurp.go @@ -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) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..bfd6f7c --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..4561228 --- /dev/null +++ b/go.sum @@ -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=