support running nak with multiple lines of stdin sequentially.

This commit is contained in:
fiatjaf 2023-11-07 17:57:43 -03:00
parent 31b42c3499
commit 78932833df
No known key found for this signature in database
GPG Key ID: BAD43C4BE5C1A3A1
4 changed files with 237 additions and 191 deletions

191
event.go
View File

@ -90,115 +90,118 @@ example:
}, },
ArgsUsage: "[relay...]", ArgsUsage: "[relay...]",
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
evt := nostr.Event{ for stdinEvent := range getStdinLinesOrBlank() {
Tags: make(nostr.Tags, 0, 3), 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)
} }
}
if kind := c.Int("kind"); kind != 0 { mustRehashAndResign := false
evt.Kind = kind if stdinEvent != "" {
mustRehashAndResign = true if err := json.Unmarshal([]byte(stdinEvent), &evt); err != nil {
} else if evt.Kind == 0 { lineProcessingError(c, "invalid event received from stdin: %s", err)
evt.Kind = 1 continue
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)
} }
} }
evt.CreatedAt = nostr.Timestamp(ts.Unix())
mustRehashAndResign = true
} else if evt.CreatedAt == 0 {
evt.CreatedAt = nostr.Now()
mustRehashAndResign = true
}
if evt.Sig == "" || mustRehashAndResign { if kind := c.Int("kind"); kind != 0 {
if err := evt.Sign(c.String("sec")); err != nil { evt.Kind = kind
return fmt.Errorf("error signing with provided key: %w", err) mustRehashAndResign = true
} else if evt.Kind == 0 {
evt.Kind = 1
mustRehashAndResign = true
} }
}
relays := c.Args().Slice() if content := c.String("content"); content != "" {
if len(relays) > 0 { evt.Content = content
fmt.Println(evt.String()) mustRehashAndResign = true
for _, url := range relays { } else if evt.Content == "" && evt.Kind == 1 {
fmt.Fprintf(os.Stderr, "publishing to %s... ", url) evt.Content = "hello from the nostr army knife"
if relay, err := nostr.RelayConnect(c.Context, url); err != nil { mustRehashAndResign = true
fmt.Fprintf(os.Stderr, "failed to connect: %s\n", err) }
} else {
ctx, cancel := context.WithTimeout(c.Context, 10*time.Second) tags := make(nostr.Tags, 0, 5)
defer cancel() for _, tagFlag := range c.StringSlice("tag") {
if status, err := relay.Publish(ctx, evt); err != nil { // tags are in the format key=value
fmt.Fprintf(os.Stderr, "failed: %s\n", err) 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 { } 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 evt.Sig == "" || mustRehashAndResign {
if c.Bool("envelope") { if err := evt.Sign(c.String("sec")); err != nil {
j, _ := json.Marshal([]any{"EVENT", evt}) return fmt.Errorf("error signing with provided key: %w", err)
result = string(j) }
} else if c.Bool("nson") { }
result, _ = nson.Marshal(&evt)
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 { } else {
j, _ := easyjson.Marshal(&evt) var result string
result = string(j) 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 return nil
}, },
} }

View File

@ -1,7 +1,9 @@
package main package main
import ( import (
"bufio"
"bytes" "bytes"
"context"
"fmt" "fmt"
"io" "io"
"net/url" "net/url"
@ -11,7 +13,36 @@ import (
"github.com/urfave/cli/v2" "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() stat, _ := os.Stdin.Stat()
if (stat.Mode() & os.ModeCharDevice) == 0 { if (stat.Mode() & os.ModeCharDevice) == 0 {
read := bytes.NewBuffer(make([]byte, 0, 1000)) read := bytes.NewBuffer(make([]byte, 0, 1000))
@ -23,14 +54,6 @@ func getStdin() string {
return "" return ""
} }
func getStdinOrFirstArgument(c *cli.Context) string {
target := c.Args().First()
if target != "" {
return target
}
return getStdin()
}
func validateRelayURLs(wsurls []string) error { func validateRelayURLs(wsurls []string) error {
for _, wsurl := range wsurls { for _, wsurl := range wsurls {
u, err := url.Parse(wsurl) u, err := url.Parse(wsurl)
@ -49,3 +72,14 @@ func validateRelayURLs(wsurls []string) error {
return nil 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)
}
}

154
req.go
View File

@ -95,87 +95,91 @@ example:
}, },
ArgsUsage: "[relay...]", ArgsUsage: "[relay...]",
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
filter := nostr.Filter{} for stdinFilter := range getStdinLinesOrBlank() {
if stdinFilter := getStdin(); stdinFilter != "" { filter := nostr.Filter{}
if err := json.Unmarshal([]byte(stdinFilter), &filter); err != nil { if stdinFilter != "" {
return fmt.Errorf("invalid filter received from stdin: %w", err) 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 { if authors := c.StringSlice("author"); len(authors) > 0 {
filter.Authors = append(filter.Authors, authors...) filter.Authors = append(filter.Authors, authors...)
} }
if ids := c.StringSlice("id"); len(ids) > 0 { if ids := c.StringSlice("id"); len(ids) > 0 {
filter.IDs = append(filter.IDs, ids...) filter.IDs = append(filter.IDs, ids...)
} }
if kinds := c.IntSlice("kind"); len(kinds) > 0 { if kinds := c.IntSlice("kind"); len(kinds) > 0 {
filter.Kinds = append(filter.Kinds, kinds...) filter.Kinds = append(filter.Kinds, kinds...)
} }
if search := c.String("search"); search != "" { if search := c.String("search"); search != "" {
filter.Search = search filter.Search = search
} }
tags := make([][]string, 0, 5) tags := make([][]string, 0, 5)
for _, tagFlag := range c.StringSlice("tag") { for _, tagFlag := range c.StringSlice("tag") {
spl := strings.Split(tagFlag, "=") spl := strings.Split(tagFlag, "=")
if len(spl) == 2 && len(spl[0]) == 1 { if len(spl) == 2 && len(spl[0]) == 1 {
tags = append(tags, spl) 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 { } 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 return nil
}, },
} }

View File

@ -2,7 +2,6 @@ package main
import ( import (
"encoding/json" "encoding/json"
"fmt"
"github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
@ -17,21 +16,27 @@ var verify = &cli.Command{
it outputs nothing if the verification is successful. it outputs nothing if the verification is successful.
`, `,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
evt := nostr.Event{} for stdinEvent := range getStdinLinesOrBlank() {
if stdinEvent := getStdin(); stdinEvent != "" { evt := nostr.Event{}
if err := json.Unmarshal([]byte(stdinEvent), &evt); err != nil { if stdinEvent != "" {
return fmt.Errorf("invalid JSON: %w", err) 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 { exitIfLineProcessingError(c)
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)
}
return nil return nil
}, },
} }