diff --git a/decode.go b/decode.go index c1929c9..d8b0fd9 100644 --- a/decode.go +++ b/decode.go @@ -34,43 +34,45 @@ var decode = &cli.Command{ }, ArgsUsage: "", Action: func(c *cli.Context) error { - args := c.Args() - if args.Len() != 1 { - return fmt.Errorf("invalid number of arguments, need just one") - } - input := args.First() - if strings.HasPrefix(input, "nostr:") { - input = input[6:] - } - - var decodeResult DecodeResult - if b, err := hex.DecodeString(input); err == nil { - if len(b) == 64 { - decodeResult.HexResult.PossibleTypes = []string{"sig"} - decodeResult.HexResult.Signature = hex.EncodeToString(b) - } else if len(b) == 32 { - decodeResult.HexResult.PossibleTypes = []string{"pubkey", "private_key", "event_id"} - decodeResult.HexResult.ID = hex.EncodeToString(b) - decodeResult.HexResult.PrivateKey = hex.EncodeToString(b) - decodeResult.HexResult.PublicKey = hex.EncodeToString(b) - } else { - return fmt.Errorf("hex string with invalid number of bytes: %d", len(b)) + for input := range getStdinLinesOrFirstArgument(c) { + if strings.HasPrefix(input, "nostr:") { + input = input[6:] } - } else if evp := sdk.InputToEventPointer(input); evp != nil { - decodeResult = DecodeResult{EventPointer: evp} - } else if pp := sdk.InputToProfile(c.Context, input); pp != nil { - decodeResult = DecodeResult{ProfilePointer: pp} - } else if prefix, value, err := nip19.Decode(input); err == nil && prefix == "naddr" { - ep := value.(nostr.EntityPointer) - decodeResult = DecodeResult{EntityPointer: &ep} - } else if prefix, value, err := nip19.Decode(input); err == nil && prefix == "nsec" { - decodeResult.PrivateKey.PrivateKey = value.(string) - decodeResult.PrivateKey.PublicKey, _ = nostr.GetPublicKey(value.(string)) - } else { - return fmt.Errorf("couldn't decode input") + + var decodeResult DecodeResult + if b, err := hex.DecodeString(input); err == nil { + if len(b) == 64 { + decodeResult.HexResult.PossibleTypes = []string{"sig"} + decodeResult.HexResult.Signature = hex.EncodeToString(b) + } else if len(b) == 32 { + decodeResult.HexResult.PossibleTypes = []string{"pubkey", "private_key", "event_id"} + decodeResult.HexResult.ID = hex.EncodeToString(b) + decodeResult.HexResult.PrivateKey = hex.EncodeToString(b) + decodeResult.HexResult.PublicKey = hex.EncodeToString(b) + } else { + lineProcessingError(c, "hex string with invalid number of bytes: %d", len(b)) + continue + } + } else if evp := sdk.InputToEventPointer(input); evp != nil { + decodeResult = DecodeResult{EventPointer: evp} + } else if pp := sdk.InputToProfile(c.Context, input); pp != nil { + decodeResult = DecodeResult{ProfilePointer: pp} + } else if prefix, value, err := nip19.Decode(input); err == nil && prefix == "naddr" { + ep := value.(nostr.EntityPointer) + decodeResult = DecodeResult{EntityPointer: &ep} + } else if prefix, value, err := nip19.Decode(input); err == nil && prefix == "nsec" { + decodeResult.PrivateKey.PrivateKey = value.(string) + decodeResult.PrivateKey.PublicKey, _ = nostr.GetPublicKey(value.(string)) + } else { + lineProcessingError(c, "couldn't decode input '%s': %s", input, err) + continue + } + + fmt.Println(decodeResult.JSON()) + } - fmt.Println(decodeResult.JSON()) + exitIfLineProcessingError(c) return nil }, } diff --git a/encode.go b/encode.go index 3d6773a..2c337d0 100644 --- a/encode.go +++ b/encode.go @@ -28,36 +28,44 @@ var encode = &cli.Command{ Subcommands: []*cli.Command{ { Name: "npub", - Usage: "encode a hex private key into bech32 'npub' format", + Usage: "encode a hex public key into bech32 'npub' format", Action: func(c *cli.Context) error { - target := getStdinOrFirstArgument(c) - if err := validate32BytesHex(target); err != nil { - return err + for target := range getStdinLinesOrFirstArgument(c) { + if err := validate32BytesHex(target); err != nil { + lineProcessingError(c, "invalid public key: %s", target, err) + continue + } + + if npub, err := nip19.EncodePublicKey(target); err == nil { + fmt.Println(npub) + } else { + return err + } } - if npub, err := nip19.EncodePublicKey(target); err == nil { - fmt.Println(npub) - return nil - } else { - return err - } + exitIfLineProcessingError(c) + return nil }, }, { Name: "nsec", Usage: "encode a hex private key into bech32 'nsec' format", Action: func(c *cli.Context) error { - target := getStdinOrFirstArgument(c) - if err := validate32BytesHex(target); err != nil { - return err + for target := range getStdinLinesOrFirstArgument(c) { + if err := validate32BytesHex(target); err != nil { + lineProcessingError(c, "invalid private key: %s", target, err) + continue + } + + if npub, err := nip19.EncodePrivateKey(target); err == nil { + fmt.Println(npub) + } else { + return err + } } - if npub, err := nip19.EncodePrivateKey(target); err == nil { - fmt.Println(npub) - return nil - } else { - return err - } + exitIfLineProcessingError(c) + return nil }, }, { @@ -71,22 +79,26 @@ var encode = &cli.Command{ }, }, Action: func(c *cli.Context) error { - target := getStdinOrFirstArgument(c) - if err := validate32BytesHex(target); err != nil { - return err + for target := range getStdinLinesOrFirstArgument(c) { + if err := validate32BytesHex(target); err != nil { + lineProcessingError(c, "invalid public key: %s", target, err) + continue + } + + relays := c.StringSlice("relay") + if err := validateRelayURLs(relays); err != nil { + return err + } + + if npub, err := nip19.EncodeProfile(target, relays); err == nil { + fmt.Println(npub) + } else { + return err + } } - relays := c.StringSlice("relay") - if err := validateRelayURLs(relays); err != nil { - return err - } - - if npub, err := nip19.EncodeProfile(target, relays); err == nil { - fmt.Println(npub) - return nil - } else { - return err - } + exitIfLineProcessingError(c) + return nil }, }, { @@ -104,29 +116,33 @@ var encode = &cli.Command{ }, }, Action: func(c *cli.Context) error { - target := getStdinOrFirstArgument(c) - if err := validate32BytesHex(target); err != nil { - return err - } + for target := range getStdinLinesOrFirstArgument(c) { + if err := validate32BytesHex(target); err != nil { + lineProcessingError(c, "invalid event id: %s", target, err) + continue + } - author := c.String("author") - if author != "" { - if err := validate32BytesHex(author); err != nil { + author := c.String("author") + if author != "" { + if err := validate32BytesHex(author); err != nil { + return err + } + } + + relays := c.StringSlice("relay") + if err := validateRelayURLs(relays); err != nil { + return err + } + + if npub, err := nip19.EncodeEvent(target, relays, author); err == nil { + fmt.Println(npub) + } else { return err } } - relays := c.StringSlice("relay") - if err := validateRelayURLs(relays); err != nil { - return err - } - - if npub, err := nip19.EncodeEvent(target, relays, author); err == nil { - fmt.Println(npub) - return nil - } else { - return err - } + exitIfLineProcessingError(c) + return nil }, }, { @@ -136,7 +152,7 @@ var encode = &cli.Command{ &cli.StringFlag{ Name: "identifier", Aliases: []string{"d"}, - Usage: "the \"d\" tag identifier of this replaceable event", + Usage: "the \"d\" tag identifier of this replaceable event -- can also be read from stdin", Required: true, }, &cli.StringFlag{ @@ -158,49 +174,60 @@ var encode = &cli.Command{ }, }, Action: func(c *cli.Context) error { - pubkey := c.String("pubkey") - if err := validate32BytesHex(pubkey); err != nil { - return err + for d := range getStdinLinesOrBlank() { + pubkey := c.String("pubkey") + if err := validate32BytesHex(pubkey); err != nil { + return err + } + + kind := c.Int("kind") + if kind < 30000 || kind >= 40000 { + return fmt.Errorf("kind must be between 30000 and 39999, as per NIP-16, got %d", kind) + } + + if d == "" { + d = c.String("identifier") + if d == "" { + lineProcessingError(c, "\"d\" tag identifier can't be empty") + continue + } + } + + relays := c.StringSlice("relay") + if err := validateRelayURLs(relays); err != nil { + return err + } + + if npub, err := nip19.EncodeEntity(pubkey, kind, d, relays); err == nil { + fmt.Println(npub) + } else { + return err + } } - kind := c.Int("kind") - if kind < 30000 || kind >= 40000 { - return fmt.Errorf("kind must be between 30000 and 39999, as per NIP-16, got %d", kind) - } - - d := c.String("identifier") - if d == "" { - return fmt.Errorf("\"d\" tag identifier can't be empty") - } - - relays := c.StringSlice("relay") - if err := validateRelayURLs(relays); err != nil { - return err - } - - if npub, err := nip19.EncodeEntity(pubkey, kind, d, relays); err == nil { - fmt.Println(npub) - return nil - } else { - return err - } + exitIfLineProcessingError(c) + return nil }, }, { Name: "note", Usage: "generate note1 event codes (not recommended)", Action: func(c *cli.Context) error { - target := getStdinOrFirstArgument(c) - if err := validate32BytesHex(target); err != nil { - return err + for target := range getStdinLinesOrFirstArgument(c) { + if err := validate32BytesHex(target); err != nil { + lineProcessingError(c, "invalid event id: %s", target, err) + continue + } + + if note, err := nip19.EncodeNote(target); err == nil { + fmt.Println(note) + } else { + return err + } } - if npub, err := nip19.EncodeNote(target); err == nil { - fmt.Println(npub) - return nil - } else { - return err - } + exitIfLineProcessingError(c) + return nil }, }, }, diff --git a/fetch.go b/fetch.go index d532d3b..43c24ed 100644 --- a/fetch.go +++ b/fetch.go @@ -24,67 +24,71 @@ var fetch = &cli.Command{ }, ArgsUsage: "[nip19code]", Action: func(c *cli.Context) error { - filter := nostr.Filter{} - code := getStdinOrFirstArgument(c) + for code := range getStdinLinesOrFirstArgument(c) { + filter := nostr.Filter{} - prefix, value, err := nip19.Decode(code) - if err != nil { - return err - } - - relays := c.StringSlice("relay") - if err := validateRelayURLs(relays); err != nil { - return err - } - var authorHint string - - switch prefix { - case "nevent": - v := value.(nostr.EventPointer) - filter.IDs = append(filter.IDs, v.ID) - if v.Author != "" { - authorHint = v.Author + prefix, value, err := nip19.Decode(code) + if err != nil { + lineProcessingError(c, "failed to decode: %s", err) + continue } - relays = v.Relays - case "naddr": - v := value.(nostr.EntityPointer) - filter.Tags = nostr.TagMap{"d": []string{v.Identifier}} - filter.Kinds = append(filter.Kinds, v.Kind) - filter.Authors = append(filter.Authors, v.PublicKey) - authorHint = v.PublicKey - relays = v.Relays - case "nprofile": - v := value.(nostr.ProfilePointer) - filter.Authors = append(filter.Authors, v.PublicKey) - filter.Kinds = append(filter.Kinds, 0) - authorHint = v.PublicKey - relays = v.Relays - case "npub": - v := value.(string) - filter.Authors = append(filter.Authors, v) - filter.Kinds = append(filter.Kinds, 0) - authorHint = v - } - pool := nostr.NewSimplePool(c.Context) - if authorHint != "" { - relayList := sdk.FetchRelaysForPubkey(c.Context, pool, authorHint, - "wss://purplepag.es", "wss://offchain.pub", "wss://public.relaying.io") - for _, relayListItem := range relayList { - if relayListItem.Outbox { - relays = append(relays, relayListItem.URL) + relays := c.StringSlice("relay") + if err := validateRelayURLs(relays); err != nil { + return err + } + var authorHint string + + switch prefix { + case "nevent": + v := value.(nostr.EventPointer) + filter.IDs = append(filter.IDs, v.ID) + if v.Author != "" { + authorHint = v.Author + } + relays = v.Relays + case "naddr": + v := value.(nostr.EntityPointer) + filter.Tags = nostr.TagMap{"d": []string{v.Identifier}} + filter.Kinds = append(filter.Kinds, v.Kind) + filter.Authors = append(filter.Authors, v.PublicKey) + authorHint = v.PublicKey + relays = v.Relays + case "nprofile": + v := value.(nostr.ProfilePointer) + filter.Authors = append(filter.Authors, v.PublicKey) + filter.Kinds = append(filter.Kinds, 0) + authorHint = v.PublicKey + relays = v.Relays + case "npub": + v := value.(string) + filter.Authors = append(filter.Authors, v) + filter.Kinds = append(filter.Kinds, 0) + authorHint = v + } + + pool := nostr.NewSimplePool(c.Context) + if authorHint != "" { + relayList := sdk.FetchRelaysForPubkey(c.Context, pool, authorHint, + "wss://purplepag.es", "wss://offchain.pub", "wss://public.relaying.io") + for _, relayListItem := range relayList { + if relayListItem.Outbox { + relays = append(relays, relayListItem.URL) + } } } + + if len(relays) == 0 { + lineProcessingError(c, "no relay hints found") + continue + } + + for ie := range pool.SubManyEose(c.Context, relays, nostr.Filters{filter}) { + fmt.Println(ie.Event) + } } - if len(relays) == 0 { - return fmt.Errorf("no relay hints found") - } - - for ie := range pool.SubManyEose(c.Context, relays, nostr.Filters{filter}) { - fmt.Println(ie.Event) - } - + exitIfLineProcessingError(c) return nil }, } diff --git a/helpers.go b/helpers.go index e172f26..6f05666 100644 --- a/helpers.go +++ b/helpers.go @@ -2,10 +2,8 @@ package main import ( "bufio" - "bytes" "context" "fmt" - "io" "net/url" "os" "strings" @@ -18,40 +16,47 @@ const ( ) func getStdinLinesOrBlank() chan string { - ch := make(chan string) - go func() { - if stat, _ := os.Stdin.Stat(); stat.Mode()&os.ModeCharDevice == 0 { - // piped - scanner := bufio.NewScanner(os.Stdin) - for scanner.Scan() { - ch <- scanner.Text() - } - } else { - // not piped - ch <- "" - } - close(ch) - }() - return ch + multi := make(chan string) + if hasStdinLines := writeStdinLinesOrNothing(multi); !hasStdinLines { + single := make(chan string, 1) + single <- "" + close(single) + return single + } else { + return multi + } } -func getStdinOrFirstArgument(c *cli.Context) string { +func getStdinLinesOrFirstArgument(c *cli.Context) chan string { // try the first argument target := c.Args().First() if target != "" { - return target + single := make(chan string, 1) + single <- target + return single } // try the stdin - stat, _ := os.Stdin.Stat() - if (stat.Mode() & os.ModeCharDevice) == 0 { - read := bytes.NewBuffer(make([]byte, 0, 1000)) - _, err := io.Copy(read, os.Stdin) - if err == nil { - return strings.TrimSpace(read.String()) - } + multi := make(chan string) + writeStdinLinesOrNothing(multi) + return multi +} + +func writeStdinLinesOrNothing(ch chan string) (hasStdinLines bool) { + if stat, _ := os.Stdin.Stat(); stat.Mode()&os.ModeCharDevice == 0 { + // piped + go func() { + scanner := bufio.NewScanner(os.Stdin) + for scanner.Scan() { + ch <- strings.TrimSpace(scanner.Text()) + } + close(ch) + }() + return true + } else { + // not piped + return false } - return "" } func validateRelayURLs(wsurls []string) error {