From 9c54d6d5f9b5ad6ba3c9044a97d18348fdd561ac Mon Sep 17 00:00:00 2001 From: Blake Leonard Date: Mon, 16 Dec 2024 22:37:39 -0500 Subject: [PATCH 1/4] feat(cmd/goat): XRPC subcommand Signed-off-by: Blake Leonard --- cmd/goat/main.go | 1 + cmd/goat/xrpc.go | 129 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+) create mode 100644 cmd/goat/xrpc.go diff --git a/cmd/goat/main.go b/cmd/goat/main.go index c211b698a..7f470279e 100644 --- a/cmd/goat/main.go +++ b/cmd/goat/main.go @@ -38,6 +38,7 @@ func run(args []string) error { cmdSyntax, cmdCrypto, cmdPds, + cmdXRPC, } return app.Run(args) } diff --git a/cmd/goat/xrpc.go b/cmd/goat/xrpc.go new file mode 100644 index 000000000..599407a75 --- /dev/null +++ b/cmd/goat/xrpc.go @@ -0,0 +1,129 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "io" + "os" + "reflect" + "strings" + + "github.com/bluesky-social/indigo/xrpc" + "github.com/urfave/cli/v2" +) + +var cmdXRPC = &cli.Command{ + Name: "xrpc", + Usage: "use an XRPC endpoint", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "proxy", + Usage: "the service to proxy to, referred to by its DID and service ID", + }, + &cli.StringFlag{ + Name: "type", + Usage: "the MIME type of the request body for a procedure", + Value: "application/json", + }, + // --post and --get are flags instead of subcommands so that, + // when and if lexicon resolution is added, that can be used + // to infer the request type + &cli.BoolFlag{ + Name: "procedure", + Aliases: []string{"post", "p"}, + Usage: "execute an XRPC procedure (POST request)", + }, + &cli.BoolFlag{ + Name: "query", + Aliases: []string{"q", "get", "g"}, + Usage: "execute an XRPC query (GET request)", + }, + }, + ArgsUsage: " [paramKey=paramValue...]", + Action: runXRPC, +} + +func runXRPC(cctx *cli.Context) error { + ctx := context.Background() + nsid := cctx.Args().First() + if nsid == "" { + return fmt.Errorf("need to provide NSID as argument") + } + + paramList := cctx.Args().Tail() + paramMap := make(map[string]interface{}) + for _, param := range paramList { + split := strings.SplitN(param, "=", 2) + if len(split) != 2 { + return fmt.Errorf("parameters must be split with an equals sign") + } + if strings.Index(split[1], "\"") == 0 { + value := strings.Trim(split[1], "\"'") + paramMap[split[0]] = value + } else { + paramMap[split[0]] = split[1] + } + } + + procedureFlag := cctx.Bool("procedure") + queryFlag := cctx.Bool("query") + if !procedureFlag && !queryFlag { + // TODO: resolve the lexicon for the provided NSID + return fmt.Errorf("need to provide exactly one of --procedure or --query") + } else if procedureFlag && queryFlag { + return fmt.Errorf("need to provide exactly one of --procedure or --query") + } + + inpenc := cctx.String("type") + if inpenc == "" { + inpenc = "application/json" + } + + proxy := cctx.String("proxy") + + xrpcc, err := loadAuthClient(ctx) + if err == ErrNoAuthSession { + return fmt.Errorf("auth required, but not logged in") + } else if err != nil { + return err + } + if proxy != "" { + if xrpcc.Headers == nil { + xrpcc.Headers = make(map[string]string) + } + xrpcc.Headers["atproto-proxy"] = proxy + } + + var input []byte + if procedureFlag { + input, err = io.ReadAll(os.Stdin) + if err != nil { + return fmt.Errorf("could not read input: %w", err) + } + } + + var rpcType xrpc.XRPCRequestType + if procedureFlag { + rpcType = xrpc.Procedure + } else if queryFlag { + rpcType = xrpc.Query + } + + var output any + err = xrpcc.Do(ctx, rpcType, inpenc, nsid, paramMap, input, &output) + if err != nil { + return err + } + + if reflect.TypeOf(output).Kind().String() == "map" || reflect.TypeOf(output).Kind().String() == "slice" { + data, err := json.MarshalIndent(output, "", " ") + if err != nil { + return err + } + fmt.Println(string(data[:])) + } else { + fmt.Println(output) + } + return nil +} From 22f93a30dd82f16516d2f3893b74bca4de85c0d8 Mon Sep 17 00:00:00 2001 From: Blake Leonard Date: Mon, 16 Dec 2024 23:05:27 -0500 Subject: [PATCH 2/4] fix(goat/xrpc): apparent hang (waiting for EOF) on procedure calls Signed-off-by: Blake Leonard --- cmd/goat/xrpc.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/cmd/goat/xrpc.go b/cmd/goat/xrpc.go index 599407a75..819daa81c 100644 --- a/cmd/goat/xrpc.go +++ b/cmd/goat/xrpc.go @@ -1,6 +1,7 @@ package main import ( + "bytes" "context" "encoding/json" "fmt" @@ -10,6 +11,7 @@ import ( "strings" "github.com/bluesky-social/indigo/xrpc" + "github.com/mattn/go-isatty" "github.com/urfave/cli/v2" ) @@ -96,12 +98,13 @@ func runXRPC(cctx *cli.Context) error { } var input []byte - if procedureFlag { + if !isatty.IsTerminal(os.Stdin.Fd()) { input, err = io.ReadAll(os.Stdin) if err != nil { return fmt.Errorf("could not read input: %w", err) } } + inputReader := bytes.NewBuffer(input) var rpcType xrpc.XRPCRequestType if procedureFlag { @@ -111,7 +114,7 @@ func runXRPC(cctx *cli.Context) error { } var output any - err = xrpcc.Do(ctx, rpcType, inpenc, nsid, paramMap, input, &output) + err = xrpcc.Do(ctx, rpcType, inpenc, nsid, paramMap, inputReader, &output) if err != nil { return err } From f64b73a1e8407ff287e6091394833e5f494d51b8 Mon Sep 17 00:00:00 2001 From: Blake Leonard Date: Mon, 16 Dec 2024 23:06:09 -0500 Subject: [PATCH 3/4] refactor(goat/xrpc): don't indent when unmarshalling I think we prefer to handle that by piping to jq? Signed-off-by: Blake Leonard --- cmd/goat/xrpc.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/goat/xrpc.go b/cmd/goat/xrpc.go index 819daa81c..58b7582f6 100644 --- a/cmd/goat/xrpc.go +++ b/cmd/goat/xrpc.go @@ -120,7 +120,7 @@ func runXRPC(cctx *cli.Context) error { } if reflect.TypeOf(output).Kind().String() == "map" || reflect.TypeOf(output).Kind().String() == "slice" { - data, err := json.MarshalIndent(output, "", " ") + data, err := json.Marshal(output) if err != nil { return err } From af9b17e75c0b385dab3255fc30017d928e026b21 Mon Sep 17 00:00:00 2001 From: Blake Leonard Date: Mon, 16 Dec 2024 23:15:57 -0500 Subject: [PATCH 4/4] chore(goat/xrpc): mention standard input in the command description Signed-off-by: Blake Leonard --- cmd/goat/xrpc.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/goat/xrpc.go b/cmd/goat/xrpc.go index 58b7582f6..a9cb9f88e 100644 --- a/cmd/goat/xrpc.go +++ b/cmd/goat/xrpc.go @@ -18,6 +18,7 @@ import ( var cmdXRPC = &cli.Command{ Name: "xrpc", Usage: "use an XRPC endpoint", + Description: "Execute an XRPC query or procedure via your PDS. Procedure inputs should be provided via standard input (i.e. a pipe, or redirection with <).", Flags: []cli.Flag{ &cli.StringFlag{ Name: "proxy",