porting all github actions from bluesky-social/indigo to tangled CI
at main 7.7 kB view raw
1package main 2 3import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "os" 8 9 "github.com/bluesky-social/indigo/api/agnostic" 10 comatproto "github.com/bluesky-social/indigo/api/atproto" 11 "github.com/bluesky-social/indigo/atproto/data" 12 "github.com/bluesky-social/indigo/atproto/identity" 13 "github.com/bluesky-social/indigo/atproto/syntax" 14 "github.com/bluesky-social/indigo/xrpc" 15 16 "github.com/urfave/cli/v2" 17) 18 19var cmdRecord = &cli.Command{ 20 Name: "record", 21 Usage: "sub-commands for repo records", 22 Flags: []cli.Flag{}, 23 Subcommands: []*cli.Command{ 24 cmdRecordGet, 25 cmdRecordList, 26 &cli.Command{ 27 Name: "create", 28 Usage: "create record from JSON", 29 ArgsUsage: `<file>`, 30 Flags: []cli.Flag{ 31 &cli.StringFlag{ 32 Name: "rkey", 33 Aliases: []string{"r"}, 34 Usage: "record key", 35 }, 36 &cli.BoolFlag{ 37 Name: "no-validate", 38 Aliases: []string{"n"}, 39 Usage: "tells PDS not to validate record Lexicon schema", 40 }, 41 }, 42 Action: runRecordCreate, 43 }, 44 &cli.Command{ 45 Name: "update", 46 Usage: "replace existing record from JSON", 47 ArgsUsage: `<file>`, 48 Flags: []cli.Flag{ 49 &cli.StringFlag{ 50 Name: "rkey", 51 Aliases: []string{"r"}, 52 Required: true, 53 Usage: "record key", 54 }, 55 &cli.BoolFlag{ 56 Name: "no-validate", 57 Aliases: []string{"n"}, 58 Usage: "tells PDS not to validate record Lexicon schema", 59 }, 60 }, 61 Action: runRecordUpdate, 62 }, 63 &cli.Command{ 64 Name: "delete", 65 Usage: "delete an existing record", 66 Flags: []cli.Flag{ 67 &cli.StringFlag{ 68 Name: "collection", 69 Aliases: []string{"c"}, 70 Required: true, 71 Usage: "collection (NSID)", 72 }, 73 &cli.StringFlag{ 74 Name: "rkey", 75 Aliases: []string{"r"}, 76 Required: true, 77 Usage: "record key", 78 }, 79 }, 80 Action: runRecordDelete, 81 }, 82 }, 83} 84 85var cmdRecordGet = &cli.Command{ 86 Name: "get", 87 Usage: "fetch record from the network", 88 ArgsUsage: `<at-uri>`, 89 Flags: []cli.Flag{}, 90 Action: runRecordGet, 91} 92 93var cmdRecordList = &cli.Command{ 94 Name: "ls", 95 Aliases: []string{"list"}, 96 Usage: "list all records for an account", 97 ArgsUsage: `<at-identifier>`, 98 Flags: []cli.Flag{ 99 &cli.StringFlag{ 100 Name: "collection", 101 Usage: "only list records from a specific collection", 102 }, 103 &cli.BoolFlag{ 104 Name: "collections", 105 Aliases: []string{"c"}, 106 Usage: "list collections, not individual record paths", 107 }, 108 }, 109 Action: runRecordList, 110} 111 112func runRecordGet(cctx *cli.Context) error { 113 ctx := context.Background() 114 dir := identity.DefaultDirectory() 115 116 uriArg := cctx.Args().First() 117 if uriArg == "" { 118 return fmt.Errorf("expected a single AT-URI argument") 119 } 120 121 aturi, err := syntax.ParseATURI(uriArg) 122 if err != nil { 123 return fmt.Errorf("not a valid AT-URI: %v", err) 124 } 125 ident, err := dir.Lookup(ctx, aturi.Authority()) 126 if err != nil { 127 return err 128 } 129 130 record, err := fetchRecord(ctx, *ident, aturi) 131 if err != nil { 132 return err 133 } 134 135 b, err := json.MarshalIndent(record, "", " ") 136 if err != nil { 137 return err 138 } 139 140 fmt.Println(string(b)) 141 return nil 142} 143 144func runRecordList(cctx *cli.Context) error { 145 ctx := context.Background() 146 username := cctx.Args().First() 147 if username == "" { 148 return fmt.Errorf("need to provide username as an argument") 149 } 150 ident, err := resolveIdent(ctx, username) 151 if err != nil { 152 return err 153 } 154 155 // create a new API client to connect to the account's PDS 156 xrpcc := xrpc.Client{ 157 Host: ident.PDSEndpoint(), 158 UserAgent: userAgent(), 159 } 160 if xrpcc.Host == "" { 161 return fmt.Errorf("no PDS endpoint for identity") 162 } 163 164 desc, err := comatproto.RepoDescribeRepo(ctx, &xrpcc, ident.DID.String()) 165 if err != nil { 166 return err 167 } 168 if cctx.Bool("collections") { 169 for _, nsid := range desc.Collections { 170 fmt.Printf("%s\n", nsid) 171 } 172 return nil 173 } 174 collections := desc.Collections 175 filter := cctx.String("collection") 176 if filter != "" { 177 collections = []string{filter} 178 } 179 180 for _, nsid := range collections { 181 cursor := "" 182 for { 183 // collection string, cursor string, limit int64, repo string, reverse bool 184 resp, err := agnostic.RepoListRecords(ctx, &xrpcc, nsid, cursor, 100, ident.DID.String(), false) 185 if err != nil { 186 return err 187 } 188 for _, rec := range resp.Records { 189 aturi, err := syntax.ParseATURI(rec.Uri) 190 if err != nil { 191 return err 192 } 193 fmt.Printf("%s\t%s\t%s\n", aturi.Collection(), aturi.RecordKey(), rec.Cid) 194 } 195 if resp.Cursor != nil && *resp.Cursor != "" { 196 cursor = *resp.Cursor 197 } else { 198 break 199 } 200 } 201 } 202 203 return nil 204} 205 206func runRecordCreate(cctx *cli.Context) error { 207 ctx := context.Background() 208 recordPath := cctx.Args().First() 209 if recordPath == "" { 210 return fmt.Errorf("need to provide file path as an argument") 211 } 212 213 xrpcc, err := loadAuthClient(ctx) 214 if err == ErrNoAuthSession { 215 return fmt.Errorf("auth required, but not logged in") 216 } else if err != nil { 217 return err 218 } 219 220 recordBytes, err := os.ReadFile(recordPath) 221 if err != nil { 222 return err 223 } 224 225 recordVal, err := data.UnmarshalJSON(recordBytes) 226 if err != nil { 227 return err 228 } 229 230 nsid, err := data.ExtractTypeJSON(recordBytes) 231 if err != nil { 232 return err 233 } 234 235 var rkey *string 236 if cctx.String("rkey") != "" { 237 rk, err := syntax.ParseRecordKey(cctx.String("rkey")) 238 if err != nil { 239 return err 240 } 241 s := rk.String() 242 rkey = &s 243 } 244 validate := !cctx.Bool("no-validate") 245 246 resp, err := agnostic.RepoCreateRecord(ctx, xrpcc, &agnostic.RepoCreateRecord_Input{ 247 Collection: nsid, 248 Repo: xrpcc.Auth.Did, 249 Record: recordVal, 250 Rkey: rkey, 251 Validate: &validate, 252 }) 253 if err != nil { 254 return err 255 } 256 257 fmt.Printf("%s\t%s\n", resp.Uri, resp.Cid) 258 return nil 259} 260 261func runRecordUpdate(cctx *cli.Context) error { 262 ctx := context.Background() 263 recordPath := cctx.Args().First() 264 if recordPath == "" { 265 return fmt.Errorf("need to provide file path as an argument") 266 } 267 268 xrpcc, err := loadAuthClient(ctx) 269 if err == ErrNoAuthSession { 270 return fmt.Errorf("auth required, but not logged in") 271 } else if err != nil { 272 return err 273 } 274 275 recordBytes, err := os.ReadFile(recordPath) 276 if err != nil { 277 return err 278 } 279 280 recordVal, err := data.UnmarshalJSON(recordBytes) 281 if err != nil { 282 return err 283 } 284 285 nsid, err := data.ExtractTypeJSON(recordBytes) 286 if err != nil { 287 return err 288 } 289 290 rkey := cctx.String("rkey") 291 292 // NOTE: need to fetch existing record CID to perform swap. this is optional in theory, but golang can't deal with "optional" and "nullable", so we always need to set this (?) 293 existing, err := agnostic.RepoGetRecord(ctx, xrpcc, "", nsid, xrpcc.Auth.Did, rkey) 294 if err != nil { 295 return err 296 } 297 298 validate := !cctx.Bool("no-validate") 299 300 resp, err := agnostic.RepoPutRecord(ctx, xrpcc, &agnostic.RepoPutRecord_Input{ 301 Collection: nsid, 302 Repo: xrpcc.Auth.Did, 303 Record: recordVal, 304 Rkey: rkey, 305 Validate: &validate, 306 SwapRecord: existing.Cid, 307 }) 308 if err != nil { 309 return err 310 } 311 312 fmt.Printf("%s\t%s\n", resp.Uri, resp.Cid) 313 return nil 314} 315 316func runRecordDelete(cctx *cli.Context) error { 317 ctx := context.Background() 318 319 xrpcc, err := loadAuthClient(ctx) 320 if err == ErrNoAuthSession { 321 return fmt.Errorf("auth required, but not logged in") 322 } else if err != nil { 323 return err 324 } 325 326 rkey, err := syntax.ParseRecordKey(cctx.String("rkey")) 327 if err != nil { 328 return err 329 } 330 collection, err := syntax.ParseNSID(cctx.String("collection")) 331 if err != nil { 332 return err 333 } 334 335 _, err = comatproto.RepoDeleteRecord(ctx, xrpcc, &comatproto.RepoDeleteRecord_Input{ 336 Collection: collection.String(), 337 Repo: xrpcc.Auth.Did, 338 Rkey: rkey.String(), 339 }) 340 if err != nil { 341 return err 342 } 343 return nil 344}