bluesky viewer in the terminal
at main 6.3 kB view raw
1package main 2 3import ( 4 "context" 5 "fmt" 6 "strings" 7 8 "github.com/stormlightlabs/skypanel/cli/internal/registry" 9 "github.com/stormlightlabs/skypanel/cli/internal/setup" 10 "github.com/stormlightlabs/skypanel/cli/internal/store" 11 "github.com/stormlightlabs/skypanel/cli/internal/ui" 12 "github.com/urfave/cli/v3" 13) 14 15// SearchUsersAction searches for users (actors) by query string 16func SearchUsersAction(ctx context.Context, cmd *cli.Command) error { 17 if err := setup.EnsurePersistenceReady(ctx); err != nil { 18 return fmt.Errorf("persistence layer not ready: %w", err) 19 } 20 21 reg := registry.Get() 22 23 if cmd.Args().Len() == 0 { 24 return fmt.Errorf("search query required") 25 } 26 27 query := cmd.Args().First() 28 limit := cmd.Int("limit") 29 cursor := cmd.String("cursor") 30 asJSON := cmd.Bool("json") 31 32 service, err := reg.GetService() 33 if err != nil { 34 return fmt.Errorf("failed to get service: %w", err) 35 } 36 37 if !service.Authenticated() { 38 return fmt.Errorf("not authenticated: run 'skycli login' first") 39 } 40 41 logger.Debug("Searching users", "query", query, "limit", limit, "cursor", cursor) 42 43 result, err := service.SearchActors(ctx, query, limit, cursor) 44 if err != nil { 45 return fmt.Errorf("failed to search users: %w", err) 46 } 47 48 if asJSON { 49 return ui.DisplayJSON(result) 50 } 51 52 if len(result.Actors) == 0 { 53 ui.Infoln("No users found matching query: %s", query) 54 return nil 55 } 56 57 ui.Titleln("Search Results: %s", query) 58 fmt.Println() 59 60 for i, actor := range result.Actors { 61 ui.Subtitleln("[%d] @%s", i+1, actor.Handle) 62 if actor.DisplayName != "" { 63 ui.Infoln(" Name: %s", actor.DisplayName) 64 } 65 ui.Infoln(" DID: %s", actor.Did) 66 if actor.Description != "" { 67 desc := actor.Description 68 if len(desc) > 100 { 69 desc = desc[:100] + "..." 70 } 71 ui.Infoln(" Bio: %s", desc) 72 } 73 ui.Infoln(" Followers: %d | Following: %d | Posts: %d", 74 actor.FollowersCount, actor.FollowsCount, actor.PostsCount) 75 fmt.Println() 76 } 77 78 ui.Successln("Found %d user(s)", len(result.Actors)) 79 if result.Cursor != "" { 80 ui.Infoln("Next cursor: %s", result.Cursor) 81 } 82 83 return nil 84} 85 86// SearchPostsAction searches for posts by query string 87func SearchPostsAction(ctx context.Context, cmd *cli.Command) error { 88 if err := setup.EnsurePersistenceReady(ctx); err != nil { 89 return fmt.Errorf("persistence layer not ready: %w", err) 90 } 91 92 reg := registry.Get() 93 94 if cmd.Args().Len() == 0 { 95 return fmt.Errorf("search query required") 96 } 97 98 query := cmd.Args().First() 99 limit := cmd.Int("limit") 100 cursor := cmd.String("cursor") 101 asJSON := cmd.Bool("json") 102 103 service, err := reg.GetService() 104 if err != nil { 105 return fmt.Errorf("failed to get service: %w", err) 106 } 107 108 if !service.Authenticated() { 109 return fmt.Errorf("not authenticated: run 'skycli login' first") 110 } 111 112 logger.Debug("Searching posts", "query", query, "limit", limit, "cursor", cursor) 113 114 result, err := service.SearchPosts(ctx, query, limit, cursor) 115 if err != nil { 116 return fmt.Errorf("failed to search posts: %w", err) 117 } 118 119 if asJSON { 120 return ui.DisplayJSON(result) 121 } 122 123 if len(result.Posts) == 0 { 124 ui.Infoln("No posts found matching query: %s", query) 125 return nil 126 } 127 128 ui.Titleln("Search Results: %s", query) 129 ui.DisplayFeed(result.Posts, result.Cursor) 130 131 return nil 132} 133 134// SearchFeedsAction searches for feeds in the local database by name or source 135func SearchFeedsAction(ctx context.Context, cmd *cli.Command) error { 136 if err := setup.EnsurePersistenceReady(ctx); err != nil { 137 return fmt.Errorf("persistence layer not ready: %w", err) 138 } 139 140 reg := registry.Get() 141 142 if cmd.Args().Len() == 0 { 143 return fmt.Errorf("search query required") 144 } 145 146 query := cmd.Args().First() 147 asJSON := cmd.Bool("json") 148 149 feedRepo, err := reg.GetFeedRepo() 150 if err != nil { 151 return fmt.Errorf("failed to get feed repository: %w", err) 152 } 153 154 logger.Debug("Searching local feeds", "query", query) 155 156 allFeeds, err := feedRepo.List(ctx) 157 if err != nil { 158 logger.Error("Failed to list feeds", "error", err) 159 return err 160 } 161 162 var matchingFeeds []store.Model 163 queryLower := strings.ToLower(query) 164 165 for _, model := range allFeeds { 166 if feed, ok := model.(*store.FeedModel); ok { 167 nameLower := strings.ToLower(feed.Name) 168 sourceLower := strings.ToLower(feed.Source) 169 if strings.Contains(nameLower, queryLower) || strings.Contains(sourceLower, queryLower) { 170 matchingFeeds = append(matchingFeeds, feed) 171 } 172 } 173 } 174 175 if len(matchingFeeds) == 0 { 176 ui.Infoln("No feeds found matching query: %s", query) 177 return nil 178 } 179 180 if asJSON { 181 return ui.DisplayJSON(matchingFeeds) 182 } 183 184 ui.Titleln("Search Results: %s", query) 185 fmt.Println() 186 187 for i, model := range matchingFeeds { 188 if feed, ok := model.(*store.FeedModel); ok { 189 ui.Subtitleln("[%d] %s", i+1, feed.Name) 190 ui.Infoln(" ID: %s", feed.ID()) 191 ui.Infoln(" Source: %s", feed.Source) 192 ui.Infoln(" Local: %t", feed.IsLocal) 193 fmt.Println() 194 } 195 } 196 197 ui.Successln("Found %d feed(s)", len(matchingFeeds)) 198 return nil 199} 200 201// SearchCommand returns the search command with subcommands for users, posts, and feeds 202func SearchCommand() *cli.Command { 203 commonFlags := []cli.Flag{ 204 &cli.IntFlag{ 205 Name: "limit", 206 Aliases: []string{"l"}, 207 Usage: "Maximum number of results to return", 208 Value: 25, 209 }, 210 &cli.StringFlag{ 211 Name: "cursor", 212 Aliases: []string{"c"}, 213 Usage: "Pagination cursor for fetching additional results", 214 }, 215 &cli.BoolFlag{ 216 Name: "json", 217 Aliases: []string{"j"}, 218 Usage: "Output raw JSON response", 219 }, 220 } 221 222 feedFlags := []cli.Flag{ 223 &cli.BoolFlag{ 224 Name: "json", 225 Aliases: []string{"j"}, 226 Usage: "Output raw JSON response", 227 }, 228 } 229 230 return &cli.Command{ 231 Name: "search", 232 Usage: "Search for users, posts, or feeds", 233 Commands: []*cli.Command{ 234 { 235 Name: "users", 236 Usage: "Search for users by handle or name", 237 ArgsUsage: "<query>", 238 Flags: commonFlags, 239 Action: SearchUsersAction, 240 }, 241 { 242 Name: "posts", 243 Usage: "Search for posts by text content", 244 ArgsUsage: "<query>", 245 Flags: commonFlags, 246 Action: SearchPostsAction, 247 }, 248 { 249 Name: "feeds", 250 Usage: "Search local feeds by name or source (local search only)", 251 ArgsUsage: "<query>", 252 Flags: feedFlags, 253 Action: SearchFeedsAction, 254 }, 255 }, 256 } 257}