1package main
2
3import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "strings"
8 "time"
9
10 comatproto "github.com/bluesky-social/indigo/api/atproto"
11 appbsky "github.com/bluesky-social/indigo/api/bsky"
12 lexutil "github.com/bluesky-social/indigo/lex/util"
13 "github.com/bluesky-social/indigo/util"
14 "github.com/bluesky-social/indigo/util/cliutil"
15
16 cli "github.com/urfave/cli/v2"
17)
18
19var bskyCmd = &cli.Command{
20 Name: "bsky",
21 Usage: "sub-commands for bsky-specific endpoints",
22 Subcommands: []*cli.Command{
23 bskyFollowCmd,
24 bskyListFollowsCmd,
25 bskyPostCmd,
26 bskyGetFeedCmd,
27 bskyLikeCmd,
28 bskyDeletePostCmd,
29 bskyActorGetSuggestionsCmd,
30 bskyNotificationsCmd,
31 },
32}
33
34var bskyFollowCmd = &cli.Command{
35 Name: "follow",
36 Usage: "create a follow relationship (auth required)",
37 Flags: []cli.Flag{},
38 ArgsUsage: `<user>`,
39 Action: func(cctx *cli.Context) error {
40 xrpcc, err := cliutil.GetXrpcClient(cctx, true)
41 if err != nil {
42 return err
43 }
44
45 user := cctx.Args().First()
46
47 follow := appbsky.GraphFollow{
48 LexiconTypeID: "app.bsky.graph.follow",
49 CreatedAt: time.Now().Format(time.RFC3339),
50 Subject: user,
51 }
52
53 resp, err := comatproto.RepoCreateRecord(context.TODO(), xrpcc, &comatproto.RepoCreateRecord_Input{
54 Collection: "app.bsky.graph.follow",
55 Repo: xrpcc.Auth.Did,
56 Record: &lexutil.LexiconTypeDecoder{Val: &follow},
57 })
58 if err != nil {
59 return err
60 }
61
62 fmt.Println(resp.Uri)
63
64 return nil
65 },
66}
67
68var bskyListFollowsCmd = &cli.Command{
69 Name: "list-follows",
70 Usage: "print list of follows for account",
71 ArgsUsage: `[actor]`,
72 Action: func(cctx *cli.Context) error {
73 xrpcc, err := cliutil.GetXrpcClient(cctx, false)
74 if err != nil {
75 return err
76 }
77
78 user := cctx.Args().First()
79 if user == "" {
80 user = xrpcc.Auth.Did
81 }
82
83 ctx := context.TODO()
84 resp, err := appbsky.GraphGetFollows(ctx, xrpcc, user, "", 100)
85 if err != nil {
86 return err
87 }
88
89 for _, f := range resp.Follows {
90 fmt.Println(f.Did, f.Handle)
91 }
92
93 return nil
94 },
95}
96
97var bskyPostCmd = &cli.Command{
98 Name: "post",
99 Usage: "create a post record",
100 ArgsUsage: `<text>`,
101 Action: func(cctx *cli.Context) error {
102 xrpcc, err := cliutil.GetXrpcClient(cctx, true)
103 if err != nil {
104 return err
105 }
106
107 auth := xrpcc.Auth
108
109 text := strings.Join(cctx.Args().Slice(), " ")
110
111 resp, err := comatproto.RepoCreateRecord(context.TODO(), xrpcc, &comatproto.RepoCreateRecord_Input{
112 Collection: "app.bsky.feed.post",
113 Repo: auth.Did,
114 Record: &lexutil.LexiconTypeDecoder{Val: &appbsky.FeedPost{
115 Text: text,
116 CreatedAt: time.Now().Format(util.ISO8601),
117 }},
118 })
119 if err != nil {
120 return fmt.Errorf("failed to create post: %w", err)
121 }
122
123 fmt.Println(resp.Cid)
124 fmt.Println(resp.Uri)
125
126 return nil
127 },
128}
129
130func prettyPrintPost(p *appbsky.FeedDefs_FeedViewPost, uris bool) {
131 fmt.Println(strings.Repeat("-", 60))
132 rec := p.Post.Record.Val.(*appbsky.FeedPost)
133 fmt.Printf("%s (%s)", p.Post.Author.Handle, rec.CreatedAt)
134 if uris {
135 fmt.Println(" -- ", p.Post.Uri)
136 } else {
137 fmt.Println(":")
138 }
139 fmt.Println(rec.Text)
140}
141
142var bskyGetFeedCmd = &cli.Command{
143 Name: "get-feed",
144 Usage: "fetch bsky feed",
145 Flags: []cli.Flag{
146 &cli.IntFlag{
147 Name: "count",
148 Value: 100,
149 },
150 &cli.StringFlag{
151 Name: "author",
152 Usage: "specify handle of user to list their authored feed",
153 },
154 &cli.BoolFlag{
155 Name: "raw",
156 Usage: "print out feed in raw json",
157 },
158 &cli.BoolFlag{
159 Name: "uris",
160 Usage: "include URIs in pretty print output",
161 },
162 },
163 Action: func(cctx *cli.Context) error {
164 xrpcc, err := cliutil.GetXrpcClient(cctx, true)
165 if err != nil {
166 return err
167 }
168
169 ctx := context.TODO()
170
171 raw := cctx.Bool("raw")
172
173 uris := cctx.Bool("uris")
174
175 author := cctx.String("author")
176 if author != "" {
177 if author == "self" {
178 author = xrpcc.Auth.Did
179 }
180
181 tl, err := appbsky.FeedGetAuthorFeed(ctx, xrpcc, author, "", "", false, 99)
182 if err != nil {
183 return err
184 }
185
186 for i := len(tl.Feed) - 1; i >= 0; i-- {
187 it := tl.Feed[i]
188 if raw {
189 jsonPrint(it)
190 } else {
191 prettyPrintPost(it, uris)
192 }
193 }
194 } else {
195 algo := "reverse-chronological"
196 tl, err := appbsky.FeedGetTimeline(ctx, xrpcc, algo, "", int64(cctx.Int("count")))
197 if err != nil {
198 return err
199 }
200
201 for i := len(tl.Feed) - 1; i >= 0; i-- {
202 it := tl.Feed[i]
203 if raw {
204 jsonPrint(it)
205 } else {
206 prettyPrintPost(it, uris)
207 }
208 }
209 }
210
211 return nil
212
213 },
214}
215
216var bskyActorGetSuggestionsCmd = &cli.Command{
217 Name: "actor-get-suggestions",
218 ArgsUsage: "[author]",
219 Action: func(cctx *cli.Context) error {
220 xrpcc, err := cliutil.GetXrpcClient(cctx, true)
221 if err != nil {
222 return err
223 }
224
225 ctx := context.TODO()
226
227 author := cctx.Args().First()
228 if author == "" {
229 author = xrpcc.Auth.Did
230 }
231
232 resp, err := appbsky.ActorGetSuggestions(ctx, xrpcc, "", 100)
233 if err != nil {
234 return err
235 }
236
237 b, err := json.MarshalIndent(resp.Actors, "", " ")
238 if err != nil {
239 return err
240 }
241
242 fmt.Println(string(b))
243
244 return nil
245
246 },
247}
248
249var bskyLikeCmd = &cli.Command{
250 Name: "like",
251 Usage: "create bsky 'like' record",
252 ArgsUsage: "<post>",
253 Action: func(cctx *cli.Context) error {
254 xrpcc, err := cliutil.GetXrpcClient(cctx, true)
255 if err != nil {
256 return err
257 }
258
259 arg := cctx.Args().First()
260
261 parts := strings.Split(arg, "/")
262 if len(parts) < 3 {
263 return fmt.Errorf("invalid post uri: %q", arg)
264 }
265 rkey := parts[len(parts)-1]
266 collection := parts[len(parts)-2]
267 did := parts[2]
268
269 fmt.Println(did, collection, rkey)
270 ctx := context.TODO()
271 resp, err := comatproto.RepoGetRecord(ctx, xrpcc, "", collection, did, rkey)
272 if err != nil {
273 return fmt.Errorf("getting record: %w", err)
274 }
275
276 out, err := comatproto.RepoCreateRecord(ctx, xrpcc, &comatproto.RepoCreateRecord_Input{
277 Collection: "app.bsky.feed.like",
278 Repo: xrpcc.Auth.Did,
279 Record: &lexutil.LexiconTypeDecoder{
280 Val: &appbsky.FeedLike{
281 CreatedAt: time.Now().Format(util.ISO8601),
282 Subject: &comatproto.RepoStrongRef{Uri: resp.Uri, Cid: *resp.Cid},
283 },
284 },
285 })
286 if err != nil {
287 return fmt.Errorf("creating like failed: %w", err)
288 }
289 _ = out
290 return nil
291
292 },
293}
294
295var bskyDeletePostCmd = &cli.Command{
296 Name: "delete-post",
297 ArgsUsage: `<rkey>`,
298 Action: func(cctx *cli.Context) error {
299 xrpcc, err := cliutil.GetXrpcClient(cctx, true)
300 if err != nil {
301 return err
302 }
303
304 rkey := cctx.Args().First()
305
306 if rkey == "" {
307 return fmt.Errorf("must specify rkey of post to delete")
308 }
309
310 schema := "app.bsky.feed.post"
311 if strings.Contains(rkey, "/") {
312 parts := strings.Split(rkey, "/")
313 schema = parts[0]
314 rkey = parts[1]
315 }
316
317 _, err = comatproto.RepoDeleteRecord(context.TODO(), xrpcc, &comatproto.RepoDeleteRecord_Input{
318 Repo: xrpcc.Auth.Did,
319 Collection: schema,
320 Rkey: rkey,
321 })
322 return err
323 },
324}
325
326var bskyNotificationsCmd = &cli.Command{
327 Name: "notifs",
328 Usage: "fetch bsky notifications (requires auth)",
329 Flags: []cli.Flag{},
330 Action: func(cctx *cli.Context) error {
331 ctx := context.TODO()
332
333 xrpcc, err := cliutil.GetXrpcClient(cctx, true)
334 if err != nil {
335 return err
336 }
337
338 notifs, err := appbsky.NotificationListNotifications(ctx, xrpcc, "", 50, false, nil, "")
339 if err != nil {
340 return err
341 }
342
343 for _, n := range notifs.Notifications {
344 b, err := json.Marshal(n)
345 if err != nil {
346 return err
347 }
348
349 fmt.Println(string(b))
350 }
351
352 return nil
353 },
354}