bluesky viewer in the terminal
at main 3.8 kB view raw
1package main 2 3import ( 4 "context" 5 "fmt" 6 7 "github.com/stormlightlabs/skypanel/cli/internal/imports" 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 15func LoginCommand() *cli.Command { 16 return &cli.Command{ 17 Name: "login", 18 Usage: "Authenticate with Bluesky", 19 Description: `Authenticate with Bluesky using one of two methods: 20 21 1. Direct credentials via flags: 22 skycli login --handle @user.bsky.social --password your-app-password 23 24 2. Credentials from an env file: 25 skycli login --file /path/to/.env 26 27 The env file should contain: 28 BLUESKY_HANDLE=your.handle.bsky.social 29 BLUESKY_PASSWORD=your-app-password 30 31 File paths can be relative or absolute.`, 32 Flags: []cli.Flag{ 33 &cli.StringFlag{ 34 Name: "file", 35 Aliases: []string{"f"}, 36 Usage: "Path to env file containing BLUESKY_HANDLE and BLUESKY_PASSWORD", 37 }, 38 &cli.StringFlag{ 39 Name: "handle", 40 Aliases: []string{"u"}, 41 Usage: "Your Bluesky handle (e.g., @user.bsky.social)", 42 }, 43 &cli.StringFlag{ 44 Name: "password", 45 Aliases: []string{"p"}, 46 Usage: "Your app password", 47 }, 48 }, 49 Action: LoginAction, 50 } 51} 52 53func LoginAction(ctx context.Context, cmd *cli.Command) error { 54 if err := setup.EnsurePersistenceReady(ctx); err != nil { 55 return fmt.Errorf("persistence layer not ready: %w", err) 56 } 57 58 reg := registry.Get() 59 60 var handle, password string 61 filePath := cmd.String("file") 62 63 if filePath != "" { 64 env, err := imports.ParseEnvFile(filePath) 65 if err != nil { 66 return fmt.Errorf("failed to parse env file: %w", err) 67 } 68 69 handle = env["BLUESKY_HANDLE"] 70 password = env["BLUESKY_PASSWORD"] 71 72 if handle == "" { 73 return fmt.Errorf("BLUESKY_HANDLE not found in env file") 74 } 75 if password == "" { 76 return fmt.Errorf("BLUESKY_PASSWORD not found in env file") 77 } 78 } else { 79 handle = cmd.String("handle") 80 password = cmd.String("password") 81 82 if handle == "" || password == "" { 83 return fmt.Errorf("either --file or both --handle and --password are required") 84 } 85 } 86 87 logger.Info("Authenticating with Bluesky", "handle", handle) 88 89 service, err := reg.GetService() 90 if err != nil { 91 return fmt.Errorf("failed to get service: %w", err) 92 } 93 94 credentials := map[string]string{ 95 "identifier": handle, 96 "password": password, 97 } 98 99 if err := service.Authenticate(ctx, credentials); err != nil { 100 logger.Error("Authentication failed", "error", err) 101 return err 102 } 103 104 sessionRepo, err := reg.GetSessionRepo() 105 if err != nil { 106 return fmt.Errorf("failed to get session repository: %w", err) 107 } 108 109 session, err := createSessionFromService(service, handle) 110 if err != nil { 111 return fmt.Errorf("failed to create session: %w", err) 112 } 113 114 if err := sessionRepo.Save(ctx, session); err != nil { 115 logger.Error("Failed to save session", "error", err) 116 return fmt.Errorf("authentication succeeded but failed to save session: %w", err) 117 } 118 119 logger.Debug("Session saved successfully", "did", session.ID(), "handle", handle) 120 ui.Successln("Successfully authenticated as %s", handle) 121 return nil 122} 123 124// createSessionFromService creates a SessionModel from an authenticated service 125func createSessionFromService(service *store.BlueskyService, handle string) (*store.SessionModel, error) { 126 did := service.GetDid() 127 if did == "" { 128 return nil, fmt.Errorf("no DID available from authenticated service") 129 } 130 131 accessToken := service.GetAccessToken() 132 refreshToken := service.GetRefreshToken() 133 134 session := &store.SessionModel{ 135 Handle: handle, 136 Token: accessToken + "|" + refreshToken, 137 ServiceURL: service.BaseURL(), 138 IsValid: true, 139 } 140 session.SetID(did) 141 142 return session, nil 143}