diff --git a/cmd/publishv2/main.go b/cmd/publishv2/main.go index 9840ab3..5e4b37d 100644 --- a/cmd/publishv2/main.go +++ b/cmd/publishv2/main.go @@ -2,6 +2,7 @@ package publishv2 import ( "bytes" + "errors" "fmt" "log/slog" "time" @@ -17,6 +18,11 @@ import ( "github.com/spf13/cobra" ) +func init() { + UnpublishCmd.Flags().StringVarP(&feedID, "feed", "f", "", "The feed to delete") + UnpublishCmd.MarkFlagRequired("feed") +} + const FeedGeneratorCollection = "app.bsky.feed.generator" type AuthConfig struct { @@ -29,7 +35,63 @@ type PublisherConfig struct { Auth AuthConfig } -var Cmd = &cobra.Command{ +var feedID string + +var UnpublishCmd = &cobra.Command{ + Use: "unpublish", + Short: "Unpublishes the feed from your PDS", + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + var publisherConfig PublisherConfig + + if err := config.Loader.Unmarshal(&publisherConfig); err != nil { + return fmt.Errorf("decoding config into struct: %w", err) + } + + ownerPDS, err := atprotoutil.ResolveUserPDS(ctx, publisherConfig.Feed.OwnerDID) + if err := config.Loader.Unmarshal(&publisherConfig); err != nil { + return fmt.Errorf("resolving user PDS: %w", err) + } + slog.Info("Found user PDS", slog.String("pds", ownerPDS.Host)) + + client := xrpc.Client{ + Host: fmt.Sprintf("https://%s", ownerPDS.Host), + } + + if _, err := atproto.ServerDescribeServer(ctx, &client); err != nil { + return fmt.Errorf("getting info from PDS: %w", err) + } + authInput := atproto.ServerCreateSession_Input{ + Identifier: publisherConfig.Auth.Handle, + Password: publisherConfig.Auth.AppPassword, + } + authOutput, err := atproto.ServerCreateSession(ctx, &client, &authInput) + if err != nil { + return fmt.Errorf("creating session with PDS: %w", err) + } + client.Auth = &xrpc.AuthInfo{ + AccessJwt: authOutput.AccessJwt, + RefreshJwt: authOutput.RefreshJwt, + Handle: authOutput.Handle, + Did: authOutput.Did, + } + slog.Info("Session established with PDS") + deleteInput := &atproto.RepoDeleteRecord_Input{ + Collection: FeedGeneratorCollection, + Repo: client.Auth.Did, + Rkey: feedID, + } + slog.Info("Deleting record", slog.String("record", feedID)) + _, err = atproto.RepoDeleteRecord(ctx, &client, deleteInput) + if err != nil { + return fmt.Errorf("deleting record for feed generator: %w", err) + } + slog.Info("Record deleted", slog.String("record", feedID)) + return nil + }, +} + +var PublishCmd = &cobra.Command{ Use: "publish", Short: "Commands to publish the feed to your PDS", RunE: func(cmd *cobra.Command, args []string) error { @@ -70,7 +132,7 @@ var Cmd = &cobra.Command{ slog.Info("Session established with PDS") registeredFeeds := feeds.RegisterFeeds(nil) for recordID, feed := range registeredFeeds { - slog.Info("Attempting to retrieve previous record") + slog.Info("Attempting to retrieve previous record", slog.String("feed", feed.ID)) getRecordOutput, err := atproto.RepoGetRecord( ctx, &client, @@ -79,22 +141,39 @@ var Cmd = &cobra.Command{ client.Auth.Did, recordID, ) + recordFound := false if err != nil { - return fmt.Errorf("getting existing record: %w", err) + xrpcErr, ok := errors.AsType[*xrpc.XRPCError](err) + if !ok { + return fmt.Errorf("getting existing record: %w", err) + } + if xrpcErr.ErrStr != "RecordNotFound" { + return fmt.Errorf("getting existing record: %w", err) + } + } else { + recordFound = true } - _, ok := getRecordOutput.Value.Val.(*bsky.FeedGenerator) - if !ok { - return fmt.Errorf("parsing existing record as feed generator: %w", err) + if recordFound { + _, ok := getRecordOutput.Value.Val.(*bsky.FeedGenerator) + if !ok { + return fmt.Errorf("parsing existing record as feed generator: %w", err) + } } var avatar *util.LexBlob - slog.Info("Uploading feed avatar") + slog.Info("Uploading feed avatar", slog.String("feed", feed.ID)) if feed.Avatar != nil { uploadOutput, err := atproto.RepoUploadBlob(ctx, &client, bytes.NewReader(feed.Avatar)) if err != nil { return fmt.Errorf("uploading avatar blob: %w", err) } avatar = uploadOutput.Blob - slog.Info("Avatar uploaded") + slog.Info("Avatar uploaded", slog.String("feed", feed.ID)) + } + var swapRecord *string + if recordFound { + swapRecord = getRecordOutput.Cid + } else { + swapRecord = nil } putInput := atproto.RepoPutRecord_Input{ Collection: FeedGeneratorCollection, @@ -107,14 +186,14 @@ var Cmd = &cobra.Command{ }}, Repo: client.Auth.Did, Rkey: feed.ID, - SwapRecord: getRecordOutput.Cid, + SwapRecord: swapRecord, } - slog.Info("Updating feed record") + slog.Info("Updating feed record", slog.String("feed", feed.ID)) _, err = atproto.RepoPutRecord(ctx, &client, &putInput) if err != nil { return fmt.Errorf("putting record for feed generator: %w", err) } - slog.Info("Feed published") + slog.Info("Feed published", slog.String("feed", feed.ID)) } return nil }, diff --git a/cmd/root.go b/cmd/root.go index ee7a7c8..64e372a 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -12,7 +12,8 @@ func init() { RootCmd.AddCommand(indexerv2.Cmd) RootCmd.AddCommand(feedv2.Cmd) RootCmd.AddCommand(seed.Cmd) - RootCmd.AddCommand(publishv2.Cmd) + RootCmd.AddCommand(publishv2.PublishCmd) + RootCmd.AddCommand(publishv2.UnpublishCmd) } var RootCmd = &cobra.Command{