bluesky viewer in the terminal
at main 154 lines 4.1 kB view raw
1package main 2 3import ( 4 "context" 5 "fmt" 6 7 "github.com/stormlightlabs/skypanel/cli/internal/registry" 8 "github.com/stormlightlabs/skypanel/cli/internal/setup" 9 "github.com/stormlightlabs/skypanel/cli/internal/store" 10 "github.com/urfave/cli/v3" 11) 12 13// ListFollowingAction fetches and displays accounts the user follows 14func ListFollowingAction(ctx context.Context, cmd *cli.Command) error { 15 if err := setup.EnsurePersistenceReady(ctx); err != nil { 16 return fmt.Errorf("persistence layer not ready: %w", err) 17 } 18 19 reg := registry.Get() 20 21 service, err := reg.GetService() 22 if err != nil { 23 return fmt.Errorf("failed to get service: %w", err) 24 } 25 26 if !service.Authenticated() { 27 return fmt.Errorf("not authenticated: run 'skycli login' first") 28 } 29 30 cacheRepo, err := reg.GetCacheRepo() 31 if err != nil { 32 return fmt.Errorf("failed to get cache repository: %w", err) 33 } 34 35 actor := cmd.String("user") 36 if actor == "" { 37 actor = service.GetDid() 38 } 39 inactiveDays := cmd.Int("inactive") 40 mutual := cmd.Bool("mutual") 41 quietPosters := cmd.Bool("quiet") 42 quietThreshold := cmd.Float("threshold") 43 outputFormat := cmd.String("output") 44 refresh := cmd.Bool("refresh") 45 46 logger.Debugf("Fetching following for actor %v", actor) 47 48 var allFollowing []store.ActorProfile 49 cursor := "" 50 page := 0 51 for { 52 page++ 53 response, err := service.GetFollows(ctx, actor, 100, cursor) 54 if err != nil { 55 return fmt.Errorf("failed to fetch following: %w", err) 56 } 57 58 allFollowing = append(allFollowing, response.Follows...) 59 60 if response.Cursor != "" { 61 logger.Infof("Fetched page %d (%d following so far)...", page, len(allFollowing)) 62 } 63 64 if response.Cursor == "" { 65 break 66 } 67 cursor = response.Cursor 68 } 69 70 logger.Infof("Fetched %d total following", len(allFollowing)) 71 72 if mutual { 73 var mutualFollows []store.ActorProfile 74 for _, follow := range allFollowing { 75 if follow.Viewer != nil && follow.Viewer.FollowedBy != "" { 76 mutualFollows = append(mutualFollows, follow) 77 } 78 } 79 allFollowing = mutualFollows 80 } 81 82 followerInfos, actors := enrichFollowerProfiles(ctx, service, allFollowing, logger) 83 84 if inactiveDays > 0 { 85 followerInfos = filterInactive(ctx, service, cacheRepo, followerInfos, actors, inactiveDays, refresh, logger) 86 } 87 88 if quietPosters { 89 followerInfos = filterQuiet(ctx, service, cacheRepo, followerInfos, actors, quietThreshold, refresh, logger) 90 } 91 92 switch outputFormat { 93 case "json": 94 return outputFollowersJSON(followerInfos) 95 case "csv": 96 return outputFollowersCSV(followerInfos, inactiveDays > 0 || quietPosters) 97 default: 98 displayFollowersTable(followerInfos, inactiveDays > 0 || quietPosters) 99 } 100 101 return nil 102} 103 104// FollowingCommand returns the following command 105func FollowingCommand() *cli.Command { 106 return &cli.Command{ 107 Name: "following", 108 Usage: "Manage and analyze accounts you follow", 109 Commands: []*cli.Command{ 110 { 111 Name: "list", 112 Usage: "List accounts you follow", 113 UsageText: "Fetch all accounts you follow with optional filters for inactive accounts and mutual follows.", 114 ArgsUsage: " ", 115 Flags: []cli.Flag{ 116 &cli.StringFlag{ 117 Name: "user", 118 Aliases: []string{"u"}, 119 Usage: "User handle or DID (defaults to authenticated user)", 120 }, 121 &cli.IntFlag{ 122 Name: "inactive", 123 Usage: "Show only accounts with no posts in N days", 124 Value: 0, 125 }, 126 &cli.BoolFlag{ 127 Name: "mutual", 128 Usage: "Show only mutual follows", 129 }, 130 &cli.BoolFlag{ 131 Name: "quiet", 132 Usage: "Show only quiet posters (low posting frequency)", 133 }, 134 &cli.FloatFlag{ 135 Name: "threshold", 136 Usage: "Posts per day threshold for quiet posters (used with --quiet)", 137 Value: 1.0, 138 }, 139 &cli.StringFlag{ 140 Name: "output", 141 Aliases: []string{"o"}, 142 Usage: "Output format: table, json, csv", 143 Value: "table", 144 }, 145 &cli.BoolFlag{ 146 Name: "refresh", 147 Usage: "Force refresh cached data (bypasses 24-hour cache)", 148 }, 149 }, 150 Action: ListFollowingAction, 151 }, 152 }, 153 } 154}