package main import ( "bytes" "context" "fmt" "log" "os" _ "github.com/joho/godotenv/autoload" agnostic "github.com/bluesky-social/indigo/api/agnostic" comatproto "github.com/bluesky-social/indigo/api/atproto" "github.com/bluesky-social/indigo/atproto/client" "github.com/bluesky-social/indigo/atproto/identity" "github.com/bluesky-social/indigo/atproto/syntax" "github.com/carlmjohnson/versioninfo" "github.com/urfave/cli/v3" ) func main() { cmd := &cli.Command{ Name: "halide", Usage: "CLI tool for for grain.social (atproto)", Version: versioninfo.Short(), Commands: []*cli.Command{ { Name: "login", Usage: "create auth session", Action: runLogin, Flags: []cli.Flag{ &cli.StringFlag{ Name: "username", Aliases: []string{"u"}, Required: true, Sources: cli.EnvVars("ATP_USERNAME"), }, &cli.StringFlag{ Name: "password", Aliases: []string{"p"}, Required: true, Sources: cli.EnvVars("ATP_PASSWORD"), }, }, }, { Name: "photo", Commands: []*cli.Command{ { Name: "list", Aliases: []string{"ls"}, Usage: "enumerate photos", Action: runPhotoList, }, { Name: "upload", Usage: "upload a photo", ArgsUsage: ``, Flags: []cli.Flag{ &cli.StringFlag{ Name: "alt", Usage: "image alt text (for accessibility)", }, }, Action: runPhotoUpload, }, }, }, { Name: "gallery", Commands: []*cli.Command{ { Name: "list", Aliases: []string{"ls"}, Usage: "enumerate galleries", Action: runGalleryList, }, }, }, }, } if err := cmd.Run(context.Background(), os.Args); err != nil { log.Fatal(err) } } func runLogin(ctx context.Context, cmd *cli.Command) error { atid, err := syntax.ParseAtIdentifier(cmd.String("username")) if err != nil { return err } dir := identity.DefaultDirectory() c, err := client.LoginWithPassword(ctx, dir, *atid, cmd.String("password"), "", nil) if err != nil { return err } pa := c.Auth.(*client.PasswordAuth) return persistAuthSession(pa.Session) } func runPhotoUpload(ctx context.Context, cmd *cli.Command) error { photoPath := cmd.Args().First() if photoPath == "" { return fmt.Errorf("need to provide file path as an argument") } c, err := loadClient() if err != nil { return err } // TODO: refactor to do upload with c.Do(), streaming from file fileBytes, err := os.ReadFile(photoPath) if err != nil { return err } resp, err := comatproto.RepoUploadBlob(ctx, c, bytes.NewReader(fileBytes)) if err != nil { return err } photo := make(map[string]any) photo["$type"] = "social.grain.photo" photo["createdAt"] = syntax.DatetimeNow() photo["photo"] = resp.Blob // TODO: if alt was not required, this would be behind an 'if' photo["alt"] = cmd.String("alt") res, err := agnostic.RepoCreateRecord(ctx, c, &agnostic.RepoCreateRecord_Input{ Collection: "social.grain.photo", Record: photo, Repo: c.AccountDID.String(), }) if err != nil { return err } fmt.Printf("%s\t%s\n", res.Uri, res.Cid) return nil } func runPhotoList(ctx context.Context, cmd *cli.Command) error { c, err := loadClient() if err != nil { return err } out, err := agnostic.RepoListRecords(ctx, c, "social.grain.photo", "", 100, c.AccountDID.String(), false) if err != nil { return err } for _, rec := range out.Records { // TODO: parse rec.Value to lexgen struct type fmt.Printf("%s\n", rec.Uri) } return nil } func runGalleryList(ctx context.Context, cmd *cli.Command) error { c, err := loadClient() if err != nil { return err } out, err := agnostic.RepoListRecords(ctx, c, "social.grain.gallery", "", 100, c.AccountDID.String(), false) if err != nil { return err } for _, rec := range out.Records { // TODO: parse rec.Value to lexgen struct type fmt.Printf("%s\n", rec.Uri) } return nil }