demo CLI tool for grain.social
1package main
2
3import (
4 "bytes"
5 "context"
6 "fmt"
7 "log"
8 "os"
9
10 _ "github.com/joho/godotenv/autoload"
11
12 agnostic "github.com/bluesky-social/indigo/api/agnostic"
13 comatproto "github.com/bluesky-social/indigo/api/atproto"
14 "github.com/bluesky-social/indigo/atproto/client"
15 "github.com/bluesky-social/indigo/atproto/identity"
16 "github.com/bluesky-social/indigo/atproto/syntax"
17 "github.com/carlmjohnson/versioninfo"
18 "github.com/urfave/cli/v3"
19)
20
21func main() {
22
23 cmd := &cli.Command{
24 Name: "halide",
25 Usage: "CLI tool for for grain.social (atproto)",
26 Version: versioninfo.Short(),
27 Commands: []*cli.Command{
28 {
29 Name: "login",
30 Usage: "create auth session",
31 Action: runLogin,
32 Flags: []cli.Flag{
33 &cli.StringFlag{
34 Name: "username",
35 Aliases: []string{"u"},
36 Required: true,
37 Sources: cli.EnvVars("ATP_USERNAME"),
38 },
39 &cli.StringFlag{
40 Name: "password",
41 Aliases: []string{"p"},
42 Required: true,
43 Sources: cli.EnvVars("ATP_PASSWORD"),
44 },
45 },
46 },
47 {
48 Name: "photo",
49 Commands: []*cli.Command{
50 {
51 Name: "list",
52 Aliases: []string{"ls"},
53 Usage: "enumerate photos",
54 Action: runPhotoList,
55 },
56 {
57 Name: "upload",
58 Usage: "upload a photo",
59 ArgsUsage: `<file>`,
60 Flags: []cli.Flag{
61 &cli.StringFlag{
62 Name: "alt",
63 Usage: "image alt text (for accessibility)",
64 },
65 },
66 Action: runPhotoUpload,
67 },
68 },
69 },
70 {
71 Name: "gallery",
72 Commands: []*cli.Command{
73 {
74 Name: "list",
75 Aliases: []string{"ls"},
76 Usage: "enumerate galleries",
77 Action: runGalleryList,
78 },
79 },
80 },
81 },
82 }
83
84 if err := cmd.Run(context.Background(), os.Args); err != nil {
85 log.Fatal(err)
86 }
87}
88
89func runLogin(ctx context.Context, cmd *cli.Command) error {
90 atid, err := syntax.ParseAtIdentifier(cmd.String("username"))
91 if err != nil {
92 return err
93 }
94
95 dir := identity.DefaultDirectory()
96 c, err := client.LoginWithPassword(ctx, dir, *atid, cmd.String("password"), "", nil)
97 if err != nil {
98 return err
99 }
100
101 pa := c.Auth.(*client.PasswordAuth)
102 return persistAuthSession(pa.Session)
103}
104
105func runPhotoUpload(ctx context.Context, cmd *cli.Command) error {
106 photoPath := cmd.Args().First()
107 if photoPath == "" {
108 return fmt.Errorf("need to provide file path as an argument")
109 }
110
111 c, err := loadClient()
112 if err != nil {
113 return err
114 }
115
116 // TODO: refactor to do upload with c.Do(), streaming from file
117 fileBytes, err := os.ReadFile(photoPath)
118 if err != nil {
119 return err
120 }
121
122 resp, err := comatproto.RepoUploadBlob(ctx, c, bytes.NewReader(fileBytes))
123 if err != nil {
124 return err
125 }
126
127 photo := make(map[string]any)
128 photo["$type"] = "social.grain.photo"
129 photo["createdAt"] = syntax.DatetimeNow()
130 photo["photo"] = resp.Blob
131 // TODO: if alt was not required, this would be behind an 'if'
132 photo["alt"] = cmd.String("alt")
133
134 res, err := agnostic.RepoCreateRecord(ctx, c, &agnostic.RepoCreateRecord_Input{
135 Collection: "social.grain.photo",
136 Record: photo,
137 Repo: c.AccountDID.String(),
138 })
139 if err != nil {
140 return err
141 }
142
143 fmt.Printf("%s\t%s\n", res.Uri, res.Cid)
144 return nil
145}
146
147func runPhotoList(ctx context.Context, cmd *cli.Command) error {
148 c, err := loadClient()
149 if err != nil {
150 return err
151 }
152
153 out, err := agnostic.RepoListRecords(ctx, c, "social.grain.photo", "", 100, c.AccountDID.String(), false)
154 if err != nil {
155 return err
156 }
157
158 for _, rec := range out.Records {
159 // TODO: parse rec.Value to lexgen struct type
160 fmt.Printf("%s\n", rec.Uri)
161 }
162
163 return nil
164}
165
166func runGalleryList(ctx context.Context, cmd *cli.Command) error {
167 c, err := loadClient()
168 if err != nil {
169 return err
170 }
171
172 out, err := agnostic.RepoListRecords(ctx, c, "social.grain.gallery", "", 100, c.AccountDID.String(), false)
173 if err != nil {
174 return err
175 }
176
177 for _, rec := range out.Records {
178 // TODO: parse rec.Value to lexgen struct type
179 fmt.Printf("%s\n", rec.Uri)
180 }
181
182 return nil
183}