Something went wrong. Try again.
bluesky viewer in the terminal
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257package main
import ( "context" "encoding/json" "fmt" "time"
"github.com/google/uuid" "github.com/stormlightlabs/skypanel/cli/internal/registry" "github.com/stormlightlabs/skypanel/cli/internal/setup" "github.com/stormlightlabs/skypanel/cli/internal/store" "github.com/stormlightlabs/skypanel/cli/internal/ui" "github.com/urfave/cli/v3")
// FetchTimelineAction fetches and displays the authenticated user's home timelinefunc FetchTimelineAction(ctx context.Context, cmd *cli.Command) error { if err := setup.EnsurePersistenceReady(ctx); err != nil { return fmt.Errorf("persistence layer not ready: %w", err) }
reg := registry.Get()
service, err := reg.GetService() if err != nil { return fmt.Errorf("failed to get service: %w", err) }
if !service.Authenticated() { return fmt.Errorf("not authenticated: run 'skycli login' first") }
limit := cmd.Int("limit") cursor := cmd.String("cursor") asJSON := cmd.Bool("json")
logger.Debug("Fetching timeline", "limit", limit, "cursor", cursor)
response, err := service.GetTimeline(ctx, limit, cursor) if err != nil { return fmt.Errorf("failed to fetch timeline: %w", err) }
if asJSON { return ui.DisplayJSON(response) }
ui.DisplayFeed(response.Feed, response.Cursor) return nil}
// FetchFeedAction fetches and displays posts from a specific feedfunc FetchFeedAction(ctx context.Context, cmd *cli.Command) error { if err := setup.EnsurePersistenceReady(ctx); err != nil { return fmt.Errorf("persistence layer not ready: %w", err) }
reg := registry.Get()
if cmd.Args().Len() == 0 { return fmt.Errorf("feed URI or local feed ID required") }
feedIdentifier := cmd.Args().First() limit := cmd.Int("limit") cursor := cmd.String("cursor") asJSON := cmd.Bool("json")
service, err := reg.GetService() if err != nil { return fmt.Errorf("failed to get service: %w", err) }
if !service.Authenticated() { return fmt.Errorf("not authenticated: run 'skycli login' first") }
feedRepo, err := reg.GetFeedRepo() if err != nil { return fmt.Errorf("failed to get feed repository: %w", err) }
var feedURI string
if _, err := uuid.Parse(feedIdentifier); err == nil { feed, err := feedRepo.Get(ctx, feedIdentifier) if err != nil { return fmt.Errorf("failed to get local feed: %w", err) } if feedModel, ok := feed.(*store.FeedModel); ok { feedURI = feedModel.Source logger.Debug("Resolved local feed ID to URI", "id", feedIdentifier, "uri", feedURI) } } else { feedURI = feedIdentifier }
logger.Debug("Fetching feed", "uri", feedURI, "limit", limit, "cursor", cursor)
response, err := service.GetAuthorFeed(ctx, feedURI, limit, cursor) if err != nil { return fmt.Errorf("failed to fetch feed: %w", err) }
if asJSON { return ui.DisplayJSON(response) }
ui.Titleln("Feed: %s", feedURI) ui.DisplayFeed(response.Feed, response.Cursor) return nil}
// FetchAuthorAction fetches and displays posts from a specific author with profile cachingfunc FetchAuthorAction(ctx context.Context, cmd *cli.Command) error { if err := setup.EnsurePersistenceReady(ctx); err != nil { return fmt.Errorf("persistence layer not ready: %w", err) }
reg := registry.Get()
if cmd.Args().Len() == 0 { return fmt.Errorf("actor handle or DID required") }
actor := cmd.Args().First() limit := cmd.Int("limit") cursor := cmd.String("cursor") asJSON := cmd.Bool("json")
service, err := reg.GetService() if err != nil { return fmt.Errorf("failed to get service: %w", err) }
if !service.Authenticated() { return fmt.Errorf("not authenticated: run 'skycli login' first") }
profileRepo, err := reg.GetProfileRepo() if err != nil { return fmt.Errorf("failed to get profile repository: %w", err) }
cachedProfile, err := profileRepo.GetByDid(ctx, actor) if err != nil { logger.Warn("Failed to check profile cache", "error", err) }
var profile *store.ActorProfile if cachedProfile != nil && cachedProfile.IsFresh(time.Hour) { logger.Debug("Using cached profile", "did", actor) if err := json.Unmarshal([]byte(cachedProfile.DataJSON), &profile); err != nil { logger.Warn("Failed to unmarshal cached profile", "error", err) cachedProfile = nil } }
if cachedProfile == nil || !cachedProfile.IsFresh(time.Hour) { logger.Debug("Fetching profile from API", "actor", actor) profile, err = service.GetProfile(ctx, actor) if err != nil { return fmt.Errorf("failed to fetch profile: %w", err) }
profileJSON, err := json.Marshal(profile) if err != nil { logger.Warn("Failed to marshal profile for caching", "error", err) } else { profileModel := &store.ProfileModel{ Did: profile.Did, Handle: profile.Handle, DataJSON: string(profileJSON), FetchedAt: time.Now(), } if err := profileRepo.Save(ctx, profileModel); err != nil { logger.Warn("Failed to cache profile", "error", err) } else { logger.Debug("Cached profile", "did", profile.Did) } } }
logger.Debug("Fetching author feed", "actor", actor, "limit", limit, "cursor", cursor)
response, err := service.GetAuthorFeed(ctx, actor, limit, cursor) if err != nil { return fmt.Errorf("failed to fetch author feed: %w", err) }
if asJSON { return ui.DisplayJSON(response) }
if profile != nil { ui.DisplayProfileHeader(profile) }
ui.DisplayFeed(response.Feed, response.Cursor) return nil}
// FetchCommand returns the fetch command with subcommands for timeline, feed, and authorfunc FetchCommand() *cli.Command { commonFlags := []cli.Flag{ &cli.IntFlag{ Name: "limit", Aliases: []string{"l"}, Usage: "Maximum number of posts to fetch", Value: 25, }, &cli.StringFlag{ Name: "cursor", Aliases: []string{"c"}, Usage: "Pagination cursor for fetching additional results", }, &cli.BoolFlag{ Name: "json", Aliases: []string{"j"}, Usage: "Output raw JSON response", }, }
return &cli.Command{ Name: "fetch", Usage: "Fetch posts from timeline, feeds, or authors", Commands: []*cli.Command{ { Name: "timeline", Usage: "Fetch authenticated user's home timeline", ArgsUsage: " ", Flags: commonFlags, Action: FetchTimelineAction, }, { Name: "feed", Usage: "Fetch posts from a specific feed by URI or local feed ID", ArgsUsage: "<feed-uri-or-id>", Flags: commonFlags, Action: FetchFeedAction, }, { Name: "author", Usage: "Fetch posts from a specific author (with profile caching)", ArgsUsage: "<actor-handle-or-did>", Flags: commonFlags, Action: FetchAuthorAction, }, }, Action: func(ctx context.Context, cmd *cli.Command) error { return FetchTimelineAction(ctx, cmd) }, Flags: commonFlags, }}