From 78932833df423d9d14493a6398ed58c333e76da9 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 7 Nov 2023 17:57:43 -0300 Subject: [PATCH] support running nak with multiple lines of stdin sequentially. --- event.go | 191 +++++++++++++++++++++++++++-------------------------- helpers.go | 52 ++++++++++++--- req.go | 154 +++++++++++++++++++++--------------------- verify.go | 31 +++++---- 4 files changed, 237 insertions(+), 191 deletions(-) diff --git a/event.go b/event.go index 57a59fa..a060025 100644 --- a/event.go +++ b/event.go @@ -90,115 +90,118 @@ example: }, ArgsUsage: "[relay...]", Action: func(c *cli.Context) error { - evt := nostr.Event{ - Tags: make(nostr.Tags, 0, 3), - } - - mustRehashAndResign := false - - if stdinEvent := getStdin(); stdinEvent != "" { - if err := json.Unmarshal([]byte(stdinEvent), &evt); err != nil { - return fmt.Errorf("invalid event received from stdin: %w", err) + for stdinEvent := range getStdinLinesOrBlank() { + evt := nostr.Event{ + Tags: make(nostr.Tags, 0, 3), } - } - if kind := c.Int("kind"); kind != 0 { - evt.Kind = kind - mustRehashAndResign = true - } else if evt.Kind == 0 { - evt.Kind = 1 - mustRehashAndResign = true - } - - if content := c.String("content"); content != "" { - evt.Content = content - mustRehashAndResign = true - } else if evt.Content == "" && evt.Kind == 1 { - evt.Content = "hello from the nostr army knife" - mustRehashAndResign = true - } - - tags := make(nostr.Tags, 0, 5) - for _, tagFlag := range c.StringSlice("tag") { - // tags are in the format key=value - spl := strings.Split(tagFlag, "=") - if len(spl) == 2 && len(spl[0]) > 0 { - tag := nostr.Tag{spl[0]} - // tags may also contain extra elements separated with a ";" - spl2 := strings.Split(spl[1], ";") - tag = append(tag, spl2...) - // ~ - tags = append(tags, tag) - } - } - for _, etag := range c.StringSlice("e") { - tags = append(tags, []string{"e", etag}) - mustRehashAndResign = true - } - for _, ptag := range c.StringSlice("p") { - tags = append(tags, []string{"p", ptag}) - mustRehashAndResign = true - } - if len(tags) > 0 { - for _, tag := range tags { - evt.Tags = append(evt.Tags, tag) - } - mustRehashAndResign = true - } - - if createdAt := c.String("created-at"); createdAt != "" { - ts := time.Now() - if createdAt != "now" { - if v, err := strconv.ParseInt(createdAt, 10, 64); err != nil { - return fmt.Errorf("failed to parse timestamp '%s': %w", createdAt, err) - } else { - ts = time.Unix(v, 0) + mustRehashAndResign := false + if stdinEvent != "" { + if err := json.Unmarshal([]byte(stdinEvent), &evt); err != nil { + lineProcessingError(c, "invalid event received from stdin: %s", err) + continue } } - evt.CreatedAt = nostr.Timestamp(ts.Unix()) - mustRehashAndResign = true - } else if evt.CreatedAt == 0 { - evt.CreatedAt = nostr.Now() - mustRehashAndResign = true - } - if evt.Sig == "" || mustRehashAndResign { - if err := evt.Sign(c.String("sec")); err != nil { - return fmt.Errorf("error signing with provided key: %w", err) + if kind := c.Int("kind"); kind != 0 { + evt.Kind = kind + mustRehashAndResign = true + } else if evt.Kind == 0 { + evt.Kind = 1 + mustRehashAndResign = true } - } - relays := c.Args().Slice() - if len(relays) > 0 { - fmt.Println(evt.String()) - for _, url := range relays { - fmt.Fprintf(os.Stderr, "publishing to %s... ", url) - if relay, err := nostr.RelayConnect(c.Context, url); err != nil { - fmt.Fprintf(os.Stderr, "failed to connect: %s\n", err) - } else { - ctx, cancel := context.WithTimeout(c.Context, 10*time.Second) - defer cancel() - if status, err := relay.Publish(ctx, evt); err != nil { - fmt.Fprintf(os.Stderr, "failed: %s\n", err) + if content := c.String("content"); content != "" { + evt.Content = content + mustRehashAndResign = true + } else if evt.Content == "" && evt.Kind == 1 { + evt.Content = "hello from the nostr army knife" + mustRehashAndResign = true + } + + tags := make(nostr.Tags, 0, 5) + for _, tagFlag := range c.StringSlice("tag") { + // tags are in the format key=value + spl := strings.Split(tagFlag, "=") + if len(spl) == 2 && len(spl[0]) > 0 { + tag := nostr.Tag{spl[0]} + // tags may also contain extra elements separated with a ";" + spl2 := strings.Split(spl[1], ";") + tag = append(tag, spl2...) + // ~ + tags = append(tags, tag) + } + } + for _, etag := range c.StringSlice("e") { + tags = append(tags, []string{"e", etag}) + mustRehashAndResign = true + } + for _, ptag := range c.StringSlice("p") { + tags = append(tags, []string{"p", ptag}) + mustRehashAndResign = true + } + if len(tags) > 0 { + for _, tag := range tags { + evt.Tags = append(evt.Tags, tag) + } + mustRehashAndResign = true + } + + if createdAt := c.String("created-at"); createdAt != "" { + ts := time.Now() + if createdAt != "now" { + if v, err := strconv.ParseInt(createdAt, 10, 64); err != nil { + return fmt.Errorf("failed to parse timestamp '%s': %w", createdAt, err) } else { - fmt.Fprintf(os.Stderr, "%s.\n", status) + ts = time.Unix(v, 0) } } + evt.CreatedAt = nostr.Timestamp(ts.Unix()) + mustRehashAndResign = true + } else if evt.CreatedAt == 0 { + evt.CreatedAt = nostr.Now() + mustRehashAndResign = true } - } else { - var result string - if c.Bool("envelope") { - j, _ := json.Marshal([]any{"EVENT", evt}) - result = string(j) - } else if c.Bool("nson") { - result, _ = nson.Marshal(&evt) + + if evt.Sig == "" || mustRehashAndResign { + if err := evt.Sign(c.String("sec")); err != nil { + return fmt.Errorf("error signing with provided key: %w", err) + } + } + + relays := c.Args().Slice() + if len(relays) > 0 { + fmt.Println(evt.String()) + for _, url := range relays { + fmt.Fprintf(os.Stderr, "publishing to %s... ", url) + if relay, err := nostr.RelayConnect(c.Context, url); err != nil { + fmt.Fprintf(os.Stderr, "failed to connect: %s\n", err) + } else { + ctx, cancel := context.WithTimeout(c.Context, 10*time.Second) + defer cancel() + if status, err := relay.Publish(ctx, evt); err != nil { + fmt.Fprintf(os.Stderr, "failed: %s\n", err) + } else { + fmt.Fprintf(os.Stderr, "%s.\n", status) + } + } + } } else { - j, _ := easyjson.Marshal(&evt) - result = string(j) + var result string + if c.Bool("envelope") { + j, _ := json.Marshal([]any{"EVENT", evt}) + result = string(j) + } else if c.Bool("nson") { + result, _ = nson.Marshal(&evt) + } else { + j, _ := easyjson.Marshal(&evt) + result = string(j) + } + fmt.Println(result) } - fmt.Println(result) } + exitIfLineProcessingError(c) return nil }, } diff --git a/helpers.go b/helpers.go index 67805bb..80d9371 100644 --- a/helpers.go +++ b/helpers.go @@ -1,7 +1,9 @@ package main import ( + "bufio" "bytes" + "context" "fmt" "io" "net/url" @@ -11,7 +13,36 @@ import ( "github.com/urfave/cli/v2" ) -func getStdin() string { +const ( + LINE_PROCESSING_ERROR = iota +) + +func getStdinLinesOrBlank() chan string { + ch := make(chan string) + go func() { + r := bufio.NewReader(os.Stdin) + if _, err := r.Peek(1); err != nil { + ch <- "" + close(ch) + } else { + scanner := bufio.NewScanner(r) + for scanner.Scan() { + ch <- scanner.Text() + } + close(ch) + } + }() + return ch +} + +func getStdinOrFirstArgument(c *cli.Context) string { + // try the first argument + target := c.Args().First() + if target != "" { + return target + } + + // try the stdin stat, _ := os.Stdin.Stat() if (stat.Mode() & os.ModeCharDevice) == 0 { read := bytes.NewBuffer(make([]byte, 0, 1000)) @@ -23,14 +54,6 @@ func getStdin() string { return "" } -func getStdinOrFirstArgument(c *cli.Context) string { - target := c.Args().First() - if target != "" { - return target - } - return getStdin() -} - func validateRelayURLs(wsurls []string) error { for _, wsurl := range wsurls { u, err := url.Parse(wsurl) @@ -49,3 +72,14 @@ func validateRelayURLs(wsurls []string) error { return nil } + +func lineProcessingError(c *cli.Context, msg string, args ...any) { + c.Context = context.WithValue(c.Context, LINE_PROCESSING_ERROR, true) + fmt.Fprintf(os.Stderr, msg+"\n", args...) +} + +func exitIfLineProcessingError(c *cli.Context) { + if val := c.Context.Value(LINE_PROCESSING_ERROR); val != nil && val.(bool) { + os.Exit(123) + } +} diff --git a/req.go b/req.go index c8f3985..499b411 100644 --- a/req.go +++ b/req.go @@ -95,87 +95,91 @@ example: }, ArgsUsage: "[relay...]", Action: func(c *cli.Context) error { - filter := nostr.Filter{} - if stdinFilter := getStdin(); stdinFilter != "" { - if err := json.Unmarshal([]byte(stdinFilter), &filter); err != nil { - return fmt.Errorf("invalid filter received from stdin: %w", err) + for stdinFilter := range getStdinLinesOrBlank() { + filter := nostr.Filter{} + if stdinFilter != "" { + if err := json.Unmarshal([]byte(stdinFilter), &filter); err != nil { + lineProcessingError(c, "invalid filter received from stdin: %s", err) + continue + } } - } - if authors := c.StringSlice("author"); len(authors) > 0 { - filter.Authors = append(filter.Authors, authors...) - } - if ids := c.StringSlice("id"); len(ids) > 0 { - filter.IDs = append(filter.IDs, ids...) - } - if kinds := c.IntSlice("kind"); len(kinds) > 0 { - filter.Kinds = append(filter.Kinds, kinds...) - } - if search := c.String("search"); search != "" { - filter.Search = search - } - tags := make([][]string, 0, 5) - for _, tagFlag := range c.StringSlice("tag") { - spl := strings.Split(tagFlag, "=") - if len(spl) == 2 && len(spl[0]) == 1 { - tags = append(tags, spl) + if authors := c.StringSlice("author"); len(authors) > 0 { + filter.Authors = append(filter.Authors, authors...) + } + if ids := c.StringSlice("id"); len(ids) > 0 { + filter.IDs = append(filter.IDs, ids...) + } + if kinds := c.IntSlice("kind"); len(kinds) > 0 { + filter.Kinds = append(filter.Kinds, kinds...) + } + if search := c.String("search"); search != "" { + filter.Search = search + } + tags := make([][]string, 0, 5) + for _, tagFlag := range c.StringSlice("tag") { + spl := strings.Split(tagFlag, "=") + if len(spl) == 2 && len(spl[0]) == 1 { + tags = append(tags, spl) + } else { + return fmt.Errorf("invalid --tag '%s'", tagFlag) + } + } + for _, etag := range c.StringSlice("e") { + tags = append(tags, []string{"e", etag}) + } + for _, ptag := range c.StringSlice("p") { + tags = append(tags, []string{"p", ptag}) + } + + if len(tags) > 0 && filter.Tags == nil { + filter.Tags = make(nostr.TagMap) + } + + for _, tag := range tags { + if _, ok := filter.Tags[tag[0]]; !ok { + filter.Tags[tag[0]] = make([]string, 0, 3) + } + filter.Tags[tag[0]] = append(filter.Tags[tag[0]], tag[1]) + } + + if since := c.Int("since"); since != 0 { + ts := nostr.Timestamp(since) + filter.Since = &ts + } + if until := c.Int("until"); until != 0 { + ts := nostr.Timestamp(until) + filter.Until = &ts + } + if limit := c.Int("limit"); limit != 0 { + filter.Limit = limit + } + + relays := c.Args().Slice() + if len(relays) > 0 { + pool := nostr.NewSimplePool(c.Context) + fn := pool.SubManyEose + if c.Bool("stream") { + fn = pool.SubMany + } + for ie := range fn(c.Context, relays, nostr.Filters{filter}) { + fmt.Println(ie.Event) + } } else { - return fmt.Errorf("invalid --tag '%s'", tagFlag) + // no relays given, will just print the filter + var result string + if c.Bool("bare") { + result = filter.String() + } else { + j, _ := json.Marshal([]any{"REQ", "nak", filter}) + result = string(j) + } + + fmt.Println(result) } } - for _, etag := range c.StringSlice("e") { - tags = append(tags, []string{"e", etag}) - } - for _, ptag := range c.StringSlice("p") { - tags = append(tags, []string{"p", ptag}) - } - - if len(tags) > 0 && filter.Tags == nil { - filter.Tags = make(nostr.TagMap) - } - - for _, tag := range tags { - if _, ok := filter.Tags[tag[0]]; !ok { - filter.Tags[tag[0]] = make([]string, 0, 3) - } - filter.Tags[tag[0]] = append(filter.Tags[tag[0]], tag[1]) - } - - if since := c.Int("since"); since != 0 { - ts := nostr.Timestamp(since) - filter.Since = &ts - } - if until := c.Int("until"); until != 0 { - ts := nostr.Timestamp(until) - filter.Until = &ts - } - if limit := c.Int("limit"); limit != 0 { - filter.Limit = limit - } - - relays := c.Args().Slice() - if len(relays) > 0 { - pool := nostr.NewSimplePool(c.Context) - fn := pool.SubManyEose - if c.Bool("stream") { - fn = pool.SubMany - } - for ie := range fn(c.Context, relays, nostr.Filters{filter}) { - fmt.Println(ie.Event) - } - } else { - // no relays given, will just print the filter - var result string - if c.Bool("bare") { - result = filter.String() - } else { - j, _ := json.Marshal([]any{"REQ", "nak", filter}) - result = string(j) - } - - fmt.Println(result) - } + exitIfLineProcessingError(c) return nil }, } diff --git a/verify.go b/verify.go index b428b18..f31aa19 100644 --- a/verify.go +++ b/verify.go @@ -2,7 +2,6 @@ package main import ( "encoding/json" - "fmt" "github.com/nbd-wtf/go-nostr" "github.com/urfave/cli/v2" @@ -17,21 +16,27 @@ var verify = &cli.Command{ it outputs nothing if the verification is successful. `, Action: func(c *cli.Context) error { - evt := nostr.Event{} - if stdinEvent := getStdin(); stdinEvent != "" { - if err := json.Unmarshal([]byte(stdinEvent), &evt); err != nil { - return fmt.Errorf("invalid JSON: %w", err) + for stdinEvent := range getStdinLinesOrBlank() { + evt := nostr.Event{} + if stdinEvent != "" { + if err := json.Unmarshal([]byte(stdinEvent), &evt); err != nil { + lineProcessingError(c, "invalid event: %s", err) + continue + } + } + + if evt.GetID() != evt.ID { + lineProcessingError(c, "invalid .id, expected %s, got %s", evt.GetID(), evt.ID) + continue + } + + if ok, err := evt.CheckSignature(); !ok { + lineProcessingError(c, "invalid signature: %s", err) + continue } } - if evt.GetID() != evt.ID { - return fmt.Errorf("invalid .id, expected %s, got %s", evt.GetID(), evt.ID) - } - - if ok, err := evt.CheckSignature(); !ok { - return fmt.Errorf("invalid signature: %w", err) - } - + exitIfLineProcessingError(c) return nil }, }