Something went wrong. Try again.
A tool for managing custom bluesky feeds
Something went wrong. Try again.
6.0 kB · 203 lines
Go
at setup-ci
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204package publish
import ( "bytes" "errors" "fmt" "log/slog" "time"
"github.com/bluesky-social/indigo/xrpc" atprotoutil "tangled.org/dhawos.dev/bskyfeedctl/atproto" "tangled.org/dhawos.dev/bskyfeedctl/config" "tangled.org/dhawos.dev/bskyfeedctl/feeds"
"github.com/bluesky-social/indigo/api/atproto" "github.com/bluesky-social/indigo/api/bsky" "github.com/bluesky-social/indigo/lex/util" "github.com/spf13/cobra")
func init() { UnpublishCmd.Flags().StringVarP(&feedID, "feed", "f", "", "The feed to delete") err := UnpublishCmd.MarkFlagRequired("feed") if err != nil { panic(err) }}
const FeedGeneratorCollection = "app.bsky.feed.generator"
type AuthConfig struct { Handle string AppPassword string}
type PublisherConfig struct { Feed feeds.FeedConfig Auth AuthConfig}
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 != 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 { 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 != 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") registeredFeeds := feeds.RegisterFeeds(nil) for recordID, feed := range registeredFeeds { slog.Info("Attempting to retrieve previous record", slog.String("feed", feed.ID)) getRecordOutput, err := atproto.RepoGetRecord( ctx, &client, "", FeedGeneratorCollection, client.Auth.Did, recordID, ) recordFound := false if err != nil { 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 } 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.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.String("feed", feed.ID)) } var swapRecord *string if recordFound { swapRecord = getRecordOutput.Cid } else { swapRecord = nil } putInput := atproto.RepoPutRecord_Input{ Collection: FeedGeneratorCollection, Record: &util.LexiconTypeDecoder{Val: &bsky.FeedGenerator{ Avatar: avatar, CreatedAt: time.Now().Format(time.RFC3339), Description: &feed.Description, Did: publisherConfig.Feed.FeedDID, DisplayName: feed.DisplayName, }}, Repo: client.Auth.Did, Rkey: feed.ID, SwapRecord: swapRecord, } 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.String("feed", feed.ID)) } return nil },}