mirror of
https://github.com/fiatjaf/nak.git
synced 2025-12-08 16:48:51 +00:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a3c7dc825 | ||
|
|
05f2275c9e | ||
|
|
082be94614 | ||
|
|
15217f2466 | ||
|
|
8fbfdc65c8 | ||
|
|
11fe6b5809 | ||
|
|
6a7a5eb26e | ||
|
|
795e98bc2e | ||
|
|
4fdd80670a | ||
|
|
e507d90766 | ||
|
|
d95b6f50ff | ||
|
|
200e4e61f7 | ||
|
|
714d65312c |
@@ -76,3 +76,8 @@ publishing to wss://relayable.org... success.
|
|||||||
~> echo '{"content":"hello world","created_at":1698923350,"id":"05bd99d54cb835f327e0092c4275ee44c7ff51219eff417c19f70c9e2c53ad5a","kind":1,"pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","sig":"0a04a296321ed933858577f36fb2fb9a0933e966f9ee32b539493f5a4d00120891b1ca9152ebfbc04fb403bdaa7c73f415e7c4954e55726b4b4fa8cebf008cd6","tags":[]}' | nak verify
|
~> echo '{"content":"hello world","created_at":1698923350,"id":"05bd99d54cb835f327e0092c4275ee44c7ff51219eff417c19f70c9e2c53ad5a","kind":1,"pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","sig":"0a04a296321ed933858577f36fb2fb9a0933e966f9ee32b539493f5a4d00120891b1ca9152ebfbc04fb403bdaa7c73f415e7c4954e55726b4b4fa8cebf008cd6","tags":[]}' | nak verify
|
||||||
invalid .id, expected 05bd99d54cb835f427e0092c4275ee44c7ff51219eff417c19f70c9e2c53ad5a, got 05bd99d54cb835f327e0092c4275ee44c7ff51219eff417c19f70c9e2c53ad5a
|
invalid .id, expected 05bd99d54cb835f427e0092c4275ee44c7ff51219eff417c19f70c9e2c53ad5a, got 05bd99d54cb835f327e0092c4275ee44c7ff51219eff417c19f70c9e2c53ad5a
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### fetch all quoted events by a given pubkey in their last 100 notes
|
||||||
|
```shell
|
||||||
|
nak req -l 100 -k 1 -a 2edbcea694d164629854a52583458fd6d965b161e3c48b57d3aff01940558884 wss://relay.damus.io | jq -r '.content | match("nostr:((note1|nevent1)[a-z0-9]+)";"g") | .captures[0].string' | nak decode | jq -cr '{ids: [.id]}' | nak req wss://relay.damus.io
|
||||||
|
```
|
||||||
|
|||||||
70
decode.go
70
decode.go
@@ -34,43 +34,45 @@ var decode = &cli.Command{
|
|||||||
},
|
},
|
||||||
ArgsUsage: "<npub | nprofile | nip05 | nevent | naddr | nsec>",
|
ArgsUsage: "<npub | nprofile | nip05 | nevent | naddr | nsec>",
|
||||||
Action: func(c *cli.Context) error {
|
Action: func(c *cli.Context) error {
|
||||||
args := c.Args()
|
for input := range getStdinLinesOrFirstArgument(c) {
|
||||||
if args.Len() != 1 {
|
if strings.HasPrefix(input, "nostr:") {
|
||||||
return fmt.Errorf("invalid number of arguments, need just one")
|
input = input[6:]
|
||||||
}
|
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
} else if evp := sdk.InputToEventPointer(input); evp != nil {
|
|
||||||
decodeResult = DecodeResult{EventPointer: evp}
|
var decodeResult DecodeResult
|
||||||
} else if pp := sdk.InputToProfile(c.Context, input); pp != nil {
|
if b, err := hex.DecodeString(input); err == nil {
|
||||||
decodeResult = DecodeResult{ProfilePointer: pp}
|
if len(b) == 64 {
|
||||||
} else if prefix, value, err := nip19.Decode(input); err == nil && prefix == "naddr" {
|
decodeResult.HexResult.PossibleTypes = []string{"sig"}
|
||||||
ep := value.(nostr.EntityPointer)
|
decodeResult.HexResult.Signature = hex.EncodeToString(b)
|
||||||
decodeResult = DecodeResult{EntityPointer: &ep}
|
} else if len(b) == 32 {
|
||||||
} else if prefix, value, err := nip19.Decode(input); err == nil && prefix == "nsec" {
|
decodeResult.HexResult.PossibleTypes = []string{"pubkey", "private_key", "event_id"}
|
||||||
decodeResult.PrivateKey.PrivateKey = value.(string)
|
decodeResult.HexResult.ID = hex.EncodeToString(b)
|
||||||
decodeResult.PrivateKey.PublicKey, _ = nostr.GetPublicKey(value.(string))
|
decodeResult.HexResult.PrivateKey = hex.EncodeToString(b)
|
||||||
} else {
|
decodeResult.HexResult.PublicKey = hex.EncodeToString(b)
|
||||||
return fmt.Errorf("couldn't decode input")
|
} 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
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
213
encode.go
213
encode.go
@@ -1,9 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/nbd-wtf/go-nostr/nip19"
|
"github.com/nbd-wtf/go-nostr/nip19"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
@@ -28,36 +26,44 @@ var encode = &cli.Command{
|
|||||||
Subcommands: []*cli.Command{
|
Subcommands: []*cli.Command{
|
||||||
{
|
{
|
||||||
Name: "npub",
|
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 {
|
Action: func(c *cli.Context) error {
|
||||||
target := getStdinOrFirstArgument(c)
|
for target := range getStdinLinesOrFirstArgument(c) {
|
||||||
if err := validate32BytesHex(target); err != nil {
|
if err := validate32BytesHex(target); err != nil {
|
||||||
return err
|
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 {
|
exitIfLineProcessingError(c)
|
||||||
fmt.Println(npub)
|
return nil
|
||||||
return nil
|
|
||||||
} else {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "nsec",
|
Name: "nsec",
|
||||||
Usage: "encode a hex private key into bech32 'nsec' format",
|
Usage: "encode a hex private key into bech32 'nsec' format",
|
||||||
Action: func(c *cli.Context) error {
|
Action: func(c *cli.Context) error {
|
||||||
target := getStdinOrFirstArgument(c)
|
for target := range getStdinLinesOrFirstArgument(c) {
|
||||||
if err := validate32BytesHex(target); err != nil {
|
if err := validate32BytesHex(target); err != nil {
|
||||||
return err
|
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 {
|
exitIfLineProcessingError(c)
|
||||||
fmt.Println(npub)
|
return nil
|
||||||
return nil
|
|
||||||
} else {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -71,22 +77,26 @@ var encode = &cli.Command{
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Action: func(c *cli.Context) error {
|
Action: func(c *cli.Context) error {
|
||||||
target := getStdinOrFirstArgument(c)
|
for target := range getStdinLinesOrFirstArgument(c) {
|
||||||
if err := validate32BytesHex(target); err != nil {
|
if err := validate32BytesHex(target); err != nil {
|
||||||
return err
|
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")
|
exitIfLineProcessingError(c)
|
||||||
if err := validateRelayURLs(relays); err != nil {
|
return nil
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if npub, err := nip19.EncodeProfile(target, relays); err == nil {
|
|
||||||
fmt.Println(npub)
|
|
||||||
return nil
|
|
||||||
} else {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -104,29 +114,33 @@ var encode = &cli.Command{
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Action: func(c *cli.Context) error {
|
Action: func(c *cli.Context) error {
|
||||||
target := getStdinOrFirstArgument(c)
|
for target := range getStdinLinesOrFirstArgument(c) {
|
||||||
if err := validate32BytesHex(target); err != nil {
|
if err := validate32BytesHex(target); err != nil {
|
||||||
return err
|
lineProcessingError(c, "invalid event id: %s", target, err)
|
||||||
}
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
author := c.String("author")
|
author := c.String("author")
|
||||||
if author != "" {
|
if author != "" {
|
||||||
if err := validate32BytesHex(author); err != nil {
|
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
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
relays := c.StringSlice("relay")
|
exitIfLineProcessingError(c)
|
||||||
if err := validateRelayURLs(relays); err != nil {
|
return nil
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if npub, err := nip19.EncodeEvent(target, relays, author); err == nil {
|
|
||||||
fmt.Println(npub)
|
|
||||||
return nil
|
|
||||||
} else {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -136,7 +150,7 @@ var encode = &cli.Command{
|
|||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: "identifier",
|
Name: "identifier",
|
||||||
Aliases: []string{"d"},
|
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,
|
Required: true,
|
||||||
},
|
},
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
@@ -158,64 +172,61 @@ var encode = &cli.Command{
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Action: func(c *cli.Context) error {
|
Action: func(c *cli.Context) error {
|
||||||
pubkey := c.String("pubkey")
|
for d := range getStdinLinesOrBlank() {
|
||||||
if err := validate32BytesHex(pubkey); err != nil {
|
pubkey := c.String("pubkey")
|
||||||
return err
|
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")
|
exitIfLineProcessingError(c)
|
||||||
if kind < 30000 || kind >= 40000 {
|
return nil
|
||||||
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
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "note",
|
Name: "note",
|
||||||
Usage: "generate note1 event codes (not recommended)",
|
Usage: "generate note1 event codes (not recommended)",
|
||||||
Action: func(c *cli.Context) error {
|
Action: func(c *cli.Context) error {
|
||||||
target := getStdinOrFirstArgument(c)
|
for target := range getStdinLinesOrFirstArgument(c) {
|
||||||
if err := validate32BytesHex(target); err != nil {
|
if err := validate32BytesHex(target); err != nil {
|
||||||
return err
|
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 {
|
exitIfLineProcessingError(c)
|
||||||
fmt.Println(npub)
|
return nil
|
||||||
return nil
|
|
||||||
} else {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func validate32BytesHex(target string) error {
|
|
||||||
if _, err := hex.DecodeString(target); err != nil {
|
|
||||||
return fmt.Errorf("target '%s' is not valid hex: %s", target, err)
|
|
||||||
}
|
|
||||||
if len(target) != 64 {
|
|
||||||
return fmt.Errorf("expected '%s' to be 64 characters (32 bytes), got %d", target, len(target))
|
|
||||||
}
|
|
||||||
if strings.ToLower(target) != target {
|
|
||||||
return fmt.Errorf("expected target to be all lowercase hex. try again with '%s'", strings.ToLower(target))
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
74
event.go
74
event.go
@@ -9,10 +9,13 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/bgentry/speakeasy"
|
||||||
"github.com/mailru/easyjson"
|
"github.com/mailru/easyjson"
|
||||||
"github.com/nbd-wtf/go-nostr"
|
"github.com/nbd-wtf/go-nostr"
|
||||||
|
"github.com/nbd-wtf/go-nostr/nip19"
|
||||||
"github.com/nbd-wtf/go-nostr/nson"
|
"github.com/nbd-wtf/go-nostr/nson"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
)
|
)
|
||||||
|
|
||||||
const CATEGORY_EVENT_FIELDS = "EVENT FIELDS"
|
const CATEGORY_EVENT_FIELDS = "EVENT FIELDS"
|
||||||
@@ -30,15 +33,18 @@ if an event -- or a partial event -- is given on stdin, the flags can be used to
|
|||||||
|
|
||||||
example:
|
example:
|
||||||
echo '{"id":"a889df6a387419ff204305f4c2d296ee328c3cd4f8b62f205648a541b4554dfb","pubkey":"c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5","created_at":1698623783,"kind":1,"tags":[],"content":"hello from the nostr army knife","sig":"84876e1ee3e726da84e5d195eb79358b2b3eaa4d9bd38456fde3e8a2af3f1cd4cda23f23fda454869975b3688797d4c66e12f4c51c1b43c6d2997c5e61865661"}' | nak event wss://offchain.pub
|
echo '{"id":"a889df6a387419ff204305f4c2d296ee328c3cd4f8b62f205648a541b4554dfb","pubkey":"c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5","created_at":1698623783,"kind":1,"tags":[],"content":"hello from the nostr army knife","sig":"84876e1ee3e726da84e5d195eb79358b2b3eaa4d9bd38456fde3e8a2af3f1cd4cda23f23fda454869975b3688797d4c66e12f4c51c1b43c6d2997c5e61865661"}' | nak event wss://offchain.pub
|
||||||
echo '{"tags": [["t", "spam"]]}' | nak event -c 'this is spam'
|
echo '{"tags": [["t", "spam"]]}' | nak event -c 'this is spam'`,
|
||||||
`,
|
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: "sec",
|
Name: "sec",
|
||||||
Usage: "secret key to sign the event",
|
Usage: "secret key to sign the event, as hex or nsec",
|
||||||
DefaultText: "the key '1'",
|
DefaultText: "the key '1'",
|
||||||
Value: "0000000000000000000000000000000000000000000000000000000000000001",
|
Value: "0000000000000000000000000000000000000000000000000000000000000001",
|
||||||
},
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "prompt-sec",
|
||||||
|
Usage: "prompt the user to paste a hex or nsec with which to sign the event",
|
||||||
|
},
|
||||||
&cli.BoolFlag{
|
&cli.BoolFlag{
|
||||||
Name: "envelope",
|
Name: "envelope",
|
||||||
Usage: "print the event enveloped in a [\"EVENT\", ...] message ready to be sent to a relay",
|
Usage: "print the event enveloped in a [\"EVENT\", ...] message ready to be sent to a relay",
|
||||||
@@ -90,25 +96,63 @@ example:
|
|||||||
},
|
},
|
||||||
ArgsUsage: "[relay...]",
|
ArgsUsage: "[relay...]",
|
||||||
Action: func(c *cli.Context) error {
|
Action: func(c *cli.Context) error {
|
||||||
|
// try to connect to the relays here
|
||||||
|
var relays []*nostr.Relay
|
||||||
|
if relayUrls := c.Args().Slice(); len(relayUrls) > 0 {
|
||||||
|
_, relays = connectToAllRelays(c.Context, relayUrls)
|
||||||
|
if len(relays) == 0 {
|
||||||
|
log("failed to connect to any of the given relays.\n")
|
||||||
|
os.Exit(3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// gather the secret key first
|
||||||
|
sec := c.String("sec")
|
||||||
|
if c.Bool("prompt-sec") {
|
||||||
|
if isPiped() {
|
||||||
|
return fmt.Errorf("can't prompt for a secret key when processing data from a pipe, try again without --prompt-sec")
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
sec, err = speakeasy.FAsk(os.Stderr, "type your secret key as nsec or hex: ")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get secret key: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(sec, "nsec1") {
|
||||||
|
_, hex, err := nip19.Decode(sec)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid nsec: %w", err)
|
||||||
|
}
|
||||||
|
sec = hex.(string)
|
||||||
|
}
|
||||||
|
if len(sec) > 64 {
|
||||||
|
return fmt.Errorf("invalid secret key: too large")
|
||||||
|
}
|
||||||
|
sec = strings.Repeat("0", 64-len(sec)) + sec // left-pad
|
||||||
|
if err := validate32BytesHex(sec); err != nil {
|
||||||
|
return fmt.Errorf("invalid secret key")
|
||||||
|
}
|
||||||
|
|
||||||
|
// then process input and generate events
|
||||||
for stdinEvent := range getStdinLinesOrBlank() {
|
for stdinEvent := range getStdinLinesOrBlank() {
|
||||||
evt := nostr.Event{
|
evt := nostr.Event{
|
||||||
Tags: make(nostr.Tags, 0, 3),
|
Tags: make(nostr.Tags, 0, 3),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kindWasSupplied := true
|
||||||
mustRehashAndResign := false
|
mustRehashAndResign := false
|
||||||
if stdinEvent != "" {
|
if stdinEvent != "" {
|
||||||
if err := json.Unmarshal([]byte(stdinEvent), &evt); err != nil {
|
if err := json.Unmarshal([]byte(stdinEvent), &evt); err != nil {
|
||||||
lineProcessingError(c, "invalid event received from stdin: %s", err)
|
lineProcessingError(c, "invalid event received from stdin: %s", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
kindWasSupplied = strings.Contains(stdinEvent, `"kind"`)
|
||||||
}
|
}
|
||||||
|
kindWasSupplied = slices.Contains(c.FlagNames(), "kind")
|
||||||
|
|
||||||
if kind := c.Int("kind"); kind != 0 {
|
if kind := c.Int("kind"); kindWasSupplied {
|
||||||
evt.Kind = kind
|
evt.Kind = kind
|
||||||
mustRehashAndResign = true
|
mustRehashAndResign = true
|
||||||
} else if evt.Kind == 0 {
|
|
||||||
evt.Kind = 1
|
|
||||||
mustRehashAndResign = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if content := c.String("content"); content != "" {
|
if content := c.String("content"); content != "" {
|
||||||
@@ -164,25 +208,25 @@ example:
|
|||||||
}
|
}
|
||||||
|
|
||||||
if evt.Sig == "" || mustRehashAndResign {
|
if evt.Sig == "" || mustRehashAndResign {
|
||||||
if err := evt.Sign(c.String("sec")); err != nil {
|
if err := evt.Sign(sec); err != nil {
|
||||||
return fmt.Errorf("error signing with provided key: %w", err)
|
return fmt.Errorf("error signing with provided key: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
relays := c.Args().Slice()
|
|
||||||
if len(relays) > 0 {
|
if len(relays) > 0 {
|
||||||
fmt.Println(evt.String())
|
fmt.Println(evt.String())
|
||||||
for _, url := range relays {
|
os.Stdout.Sync()
|
||||||
fmt.Fprintf(os.Stderr, "publishing to %s... ", url)
|
for _, relay := range relays {
|
||||||
if relay, err := nostr.RelayConnect(c.Context, url); err != nil {
|
log("publishing to %s... ", relay.URL)
|
||||||
fmt.Fprintf(os.Stderr, "failed to connect: %s\n", err)
|
if relay, err := nostr.RelayConnect(c.Context, relay.URL); err != nil {
|
||||||
|
log("failed to connect: %s\n", err)
|
||||||
} else {
|
} else {
|
||||||
ctx, cancel := context.WithTimeout(c.Context, 10*time.Second)
|
ctx, cancel := context.WithTimeout(c.Context, 10*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
if status, err := relay.Publish(ctx, evt); err != nil {
|
if status, err := relay.Publish(ctx, evt); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "failed: %s\n", err)
|
log("failed: %s\n", err)
|
||||||
} else {
|
} else {
|
||||||
fmt.Fprintf(os.Stderr, "%s.\n", status)
|
log("%s.\n", status)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
31
example_test.go
Normal file
31
example_test.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
func ExampleEventBasic() {
|
||||||
|
app.Run([]string{"nak", "event", "--ts", "1699485669"})
|
||||||
|
// Output:
|
||||||
|
// {"id":"36d88cf5fcc449f2390a424907023eda7a74278120eebab8d02797cd92e7e29c","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1699485669,"kind":1,"tags":[],"content":"hello from the nostr army knife","sig":"68e71a192e8abcf8582a222434ac823ecc50607450ebe8cc4c145eb047794cc382dc3f888ce879d2f404f5ba6085a47601360a0fa2dd4b50d317bd0c6197c2c2"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleEventComplex() {
|
||||||
|
app.Run([]string{"nak", "event", "--ts", "1699485669", "-k", "11", "-c", "skjdbaskd", "--sec", "17", "-t", "t=spam", "-e", "36d88cf5fcc449f2390a424907023eda7a74278120eebab8d02797cd92e7e29c", "-t", "r=https://abc.def;nothing"})
|
||||||
|
// Output:
|
||||||
|
// {"id":"aec4de6d051a7c2b6ca2d087903d42051a31e07fb742f1240970084822de10a6","pubkey":"2fa2104d6b38d11b0230010559879124e42ab8dfeff5ff29dc9cdadd4ecacc3f","created_at":1699485669,"kind":11,"tags":[["t","spam"],["r","https://abc.def","nothing"],["e","36d88cf5fcc449f2390a424907023eda7a74278120eebab8d02797cd92e7e29c"]],"content":"skjdbaskd","sig":"1165ac7a27d774d351ef19c8e918fb22f4005fcba193976c3d7edba6ef87ead7f14467f376a9e199f8371835368d86a8506f591e382528d00287fb168a7b8f38"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleReq() {
|
||||||
|
app.Run([]string{"nak", "req", "-k", "1", "-l", "18", "-a", "2fa2104d6b38d11b0230010559879124e42ab8dfeff5ff29dc9cdadd4ecacc3f", "-e", "aec4de6d051a7c2b6ca2d087903d42051a31e07fb742f1240970084822de10a6"})
|
||||||
|
// Output:
|
||||||
|
// ["REQ","nak",{"kinds":[1],"authors":["2fa2104d6b38d11b0230010559879124e42ab8dfeff5ff29dc9cdadd4ecacc3f"],"limit":18,"#e":["aec4de6d051a7c2b6ca2d087903d42051a31e07fb742f1240970084822de10a6"]}]
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleEncodeNpub() {
|
||||||
|
app.Run([]string{"nak", "encode", "npub", "a6a67ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179822"})
|
||||||
|
// Output:
|
||||||
|
// npub156n8a7wuhwk9tgrzjh8gwzc8q2dlekedec5djk0js9d3d7qhnq3qjpdq28
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleEncodeNprofile() {
|
||||||
|
app.Run([]string{"nak", "encode", "nprofile", "-r", "wss://example.com", "a6a67ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179822"})
|
||||||
|
// Output:
|
||||||
|
// nprofile1qqs2dfn7l8wthtz45p3ftn58pvrs9xlumvkuu2xet8egzkcklqtesgspz9mhxue69uhk27rpd4cxcefwvdhk6fl5jug
|
||||||
|
}
|
||||||
112
fetch.go
112
fetch.go
@@ -24,67 +24,71 @@ var fetch = &cli.Command{
|
|||||||
},
|
},
|
||||||
ArgsUsage: "[nip19code]",
|
ArgsUsage: "[nip19code]",
|
||||||
Action: func(c *cli.Context) error {
|
Action: func(c *cli.Context) error {
|
||||||
filter := nostr.Filter{}
|
for code := range getStdinLinesOrFirstArgument(c) {
|
||||||
code := getStdinOrFirstArgument(c)
|
filter := nostr.Filter{}
|
||||||
|
|
||||||
prefix, value, err := nip19.Decode(code)
|
prefix, value, err := nip19.Decode(code)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
lineProcessingError(c, "failed to decode: %s", err)
|
||||||
}
|
continue
|
||||||
|
|
||||||
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)
|
relays := c.StringSlice("relay")
|
||||||
if authorHint != "" {
|
if err := validateRelayURLs(relays); err != nil {
|
||||||
relayList := sdk.FetchRelaysForPubkey(c.Context, pool, authorHint,
|
return err
|
||||||
"wss://purplepag.es", "wss://offchain.pub", "wss://public.relaying.io")
|
}
|
||||||
for _, relayListItem := range relayList {
|
var authorHint string
|
||||||
if relayListItem.Outbox {
|
|
||||||
relays = append(relays, relayListItem.URL)
|
switch prefix {
|
||||||
|
case "nevent":
|
||||||
|
v := value.(nostr.EventPointer)
|
||||||
|
filter.IDs = append(filter.IDs, v.ID)
|
||||||
|
if v.Author != "" {
|
||||||
|
authorHint = v.Author
|
||||||
|
}
|
||||||
|
relays = append(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 = append(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 = append(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 {
|
exitIfLineProcessingError(c)
|
||||||
return fmt.Errorf("no relay hints found")
|
|
||||||
}
|
|
||||||
|
|
||||||
for ie := range pool.SubManyEose(c.Context, relays, nostr.Filters{filter}) {
|
|
||||||
fmt.Println(ie.Event)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
3
go.mod
3
go.mod
@@ -5,8 +5,9 @@ go 1.21
|
|||||||
toolchain go1.21.0
|
toolchain go1.21.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/bgentry/speakeasy v0.1.0
|
||||||
github.com/mailru/easyjson v0.7.7
|
github.com/mailru/easyjson v0.7.7
|
||||||
github.com/nbd-wtf/go-nostr v0.25.3
|
github.com/nbd-wtf/go-nostr v0.25.7
|
||||||
github.com/nbd-wtf/nostr-sdk v0.0.2
|
github.com/nbd-wtf/nostr-sdk v0.0.2
|
||||||
github.com/urfave/cli/v2 v2.25.3
|
github.com/urfave/cli/v2 v2.25.3
|
||||||
)
|
)
|
||||||
|
|||||||
4
go.sum
4
go.sum
@@ -1,4 +1,6 @@
|
|||||||
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
|
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
|
||||||
|
github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY=
|
||||||
|
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||||
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
|
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
|
||||||
github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M=
|
github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M=
|
||||||
github.com/btcsuite/btcd v0.23.0/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY=
|
github.com/btcsuite/btcd v0.23.0/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY=
|
||||||
@@ -80,6 +82,8 @@ github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0
|
|||||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||||
github.com/nbd-wtf/go-nostr v0.25.3 h1:RPPh4cOosw0OZi5KG627pZ3GlKxiKsjARluzen/mB9g=
|
github.com/nbd-wtf/go-nostr v0.25.3 h1:RPPh4cOosw0OZi5KG627pZ3GlKxiKsjARluzen/mB9g=
|
||||||
github.com/nbd-wtf/go-nostr v0.25.3/go.mod h1:bkffJI+x914sPQWum9ZRUn66D7NpDnAoWo1yICvj3/0=
|
github.com/nbd-wtf/go-nostr v0.25.3/go.mod h1:bkffJI+x914sPQWum9ZRUn66D7NpDnAoWo1yICvj3/0=
|
||||||
|
github.com/nbd-wtf/go-nostr v0.25.7 h1:DcGOSgKVr/L6w62tRtKeV2t46sRyFcq9pWcyIFkh0eM=
|
||||||
|
github.com/nbd-wtf/go-nostr v0.25.7/go.mod h1:bkffJI+x914sPQWum9ZRUn66D7NpDnAoWo1yICvj3/0=
|
||||||
github.com/nbd-wtf/nostr-sdk v0.0.2 h1:mZIeti+DOF0D1179q+NLL/h0LVMMOPRQAYpOuUrn5Zk=
|
github.com/nbd-wtf/nostr-sdk v0.0.2 h1:mZIeti+DOF0D1179q+NLL/h0LVMMOPRQAYpOuUrn5Zk=
|
||||||
github.com/nbd-wtf/nostr-sdk v0.0.2/go.mod h1:KQZOtzcrXBlVhpZYG1tw83ADIONNMMPjUU3ZAH5U2RY=
|
github.com/nbd-wtf/nostr-sdk v0.0.2/go.mod h1:KQZOtzcrXBlVhpZYG1tw83ADIONNMMPjUU3ZAH5U2RY=
|
||||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||||
|
|||||||
104
helpers.go
104
helpers.go
@@ -2,14 +2,14 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/nbd-wtf/go-nostr"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -17,41 +17,58 @@ const (
|
|||||||
LINE_PROCESSING_ERROR = iota
|
LINE_PROCESSING_ERROR = iota
|
||||||
)
|
)
|
||||||
|
|
||||||
func getStdinLinesOrBlank() chan string {
|
var log = func(msg string, args ...any) {
|
||||||
ch := make(chan string)
|
fmt.Fprintf(os.Stderr, msg, args...)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getStdinOrFirstArgument(c *cli.Context) string {
|
func isPiped() bool {
|
||||||
|
stat, _ := os.Stdin.Stat()
|
||||||
|
return stat.Mode()&os.ModeCharDevice == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func getStdinLinesOrBlank() chan string {
|
||||||
|
multi := make(chan string)
|
||||||
|
if hasStdinLines := writeStdinLinesOrNothing(multi); !hasStdinLines {
|
||||||
|
single := make(chan string, 1)
|
||||||
|
single <- ""
|
||||||
|
close(single)
|
||||||
|
return single
|
||||||
|
} else {
|
||||||
|
return multi
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getStdinLinesOrFirstArgument(c *cli.Context) chan string {
|
||||||
// try the first argument
|
// try the first argument
|
||||||
target := c.Args().First()
|
target := c.Args().First()
|
||||||
if target != "" {
|
if target != "" {
|
||||||
return target
|
single := make(chan string, 1)
|
||||||
|
single <- target
|
||||||
|
close(single)
|
||||||
|
return single
|
||||||
}
|
}
|
||||||
|
|
||||||
// try the stdin
|
// try the stdin
|
||||||
stat, _ := os.Stdin.Stat()
|
multi := make(chan string)
|
||||||
if (stat.Mode() & os.ModeCharDevice) == 0 {
|
writeStdinLinesOrNothing(multi)
|
||||||
read := bytes.NewBuffer(make([]byte, 0, 1000))
|
return multi
|
||||||
_, err := io.Copy(read, os.Stdin)
|
}
|
||||||
if err == nil {
|
|
||||||
return strings.TrimSpace(read.String())
|
func writeStdinLinesOrNothing(ch chan string) (hasStdinLines bool) {
|
||||||
}
|
if isPiped() {
|
||||||
|
// 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 {
|
func validateRelayURLs(wsurls []string) error {
|
||||||
@@ -73,9 +90,38 @@ func validateRelayURLs(wsurls []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validate32BytesHex(target string) error {
|
||||||
|
if _, err := hex.DecodeString(target); err != nil {
|
||||||
|
return fmt.Errorf("target '%s' is not valid hex: %s", target, err)
|
||||||
|
}
|
||||||
|
if len(target) != 64 {
|
||||||
|
return fmt.Errorf("expected '%s' to be 64 characters (32 bytes), got %d", target, len(target))
|
||||||
|
}
|
||||||
|
if strings.ToLower(target) != target {
|
||||||
|
return fmt.Errorf("expected target to be all lowercase hex. try again with '%s'", strings.ToLower(target))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func connectToAllRelays(ctx context.Context, relayUrls []string) (*nostr.SimplePool, []*nostr.Relay) {
|
||||||
|
relays := make([]*nostr.Relay, 0, len(relayUrls))
|
||||||
|
pool := nostr.NewSimplePool(ctx)
|
||||||
|
for _, url := range relayUrls {
|
||||||
|
log("connecting to %s... ", url)
|
||||||
|
if relay, err := pool.EnsureRelay(url); err == nil {
|
||||||
|
relays = append(relays, relay)
|
||||||
|
log("ok.\n")
|
||||||
|
} else {
|
||||||
|
log(err.Error() + "\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pool, relays
|
||||||
|
}
|
||||||
|
|
||||||
func lineProcessingError(c *cli.Context, msg string, args ...any) {
|
func lineProcessingError(c *cli.Context, msg string, args ...any) {
|
||||||
c.Context = context.WithValue(c.Context, LINE_PROCESSING_ERROR, true)
|
c.Context = context.WithValue(c.Context, LINE_PROCESSING_ERROR, true)
|
||||||
fmt.Fprintf(os.Stderr, msg+"\n", args...)
|
log(msg+"\n", args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func exitIfLineProcessingError(c *cli.Context) {
|
func exitIfLineProcessingError(c *cli.Context) {
|
||||||
|
|||||||
40
main.go
40
main.go
@@ -7,21 +7,35 @@ import (
|
|||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
var app = &cli.App{
|
||||||
app := &cli.App{
|
Name: "nak",
|
||||||
Name: "nak",
|
Usage: "the nostr army knife command-line tool",
|
||||||
Usage: "the nostr army knife command-line tool",
|
Commands: []*cli.Command{
|
||||||
Commands: []*cli.Command{
|
req,
|
||||||
req,
|
count,
|
||||||
count,
|
fetch,
|
||||||
fetch,
|
event,
|
||||||
event,
|
decode,
|
||||||
decode,
|
encode,
|
||||||
encode,
|
verify,
|
||||||
verify,
|
relay,
|
||||||
|
},
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "silent",
|
||||||
|
Usage: "do not print logs and info messages to stderr",
|
||||||
|
Aliases: []string{"s"},
|
||||||
|
Action: func(ctx *cli.Context, b bool) error {
|
||||||
|
if b {
|
||||||
|
log = func(msg string, args ...any) {}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
if err := app.Run(os.Args); err != nil {
|
if err := app.Run(os.Args); err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|||||||
37
relay.go
Normal file
37
relay.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/nbd-wtf/go-nostr/nip11"
|
||||||
|
"github.com/urfave/cli/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
var relay = &cli.Command{
|
||||||
|
Name: "relay",
|
||||||
|
Usage: "gets the relay information document for the given relay, as JSON",
|
||||||
|
Description: `example:
|
||||||
|
nak relay nostr.wine`,
|
||||||
|
ArgsUsage: "<relay-url>",
|
||||||
|
Action: func(c *cli.Context) error {
|
||||||
|
url := c.Args().First()
|
||||||
|
if url == "" {
|
||||||
|
return fmt.Errorf("specify the <relay-url>")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasPrefix(url, "wss://") && !strings.HasPrefix(url, "ws://") {
|
||||||
|
url = "wss://" + url
|
||||||
|
}
|
||||||
|
|
||||||
|
info, err := nip11.Fetch(c.Context, url)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to fetch '%s' information document: %w", url, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pretty, _ := json.MarshalIndent(info, "", " ")
|
||||||
|
fmt.Println(string(pretty))
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
27
req.go
27
req.go
@@ -3,6 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/nbd-wtf/go-nostr"
|
"github.com/nbd-wtf/go-nostr"
|
||||||
@@ -23,8 +24,7 @@ example:
|
|||||||
it can also take a filter from stdin, optionally modify it with flags and send it to specific relays (or just print it).
|
it can also take a filter from stdin, optionally modify it with flags and send it to specific relays (or just print it).
|
||||||
|
|
||||||
example:
|
example:
|
||||||
echo '{"kinds": [1], "#t": ["test"]}' | nak req -l 5 -k 4549 --tag t=spam wss://nostr-pub.wellorder.net
|
echo '{"kinds": [1], "#t": ["test"]}' | nak req -l 5 -k 4549 --tag t=spam wss://nostr-pub.wellorder.net`,
|
||||||
`,
|
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
&cli.StringSliceFlag{
|
&cli.StringSliceFlag{
|
||||||
Name: "author",
|
Name: "author",
|
||||||
@@ -95,11 +95,26 @@ example:
|
|||||||
},
|
},
|
||||||
ArgsUsage: "[relay...]",
|
ArgsUsage: "[relay...]",
|
||||||
Action: func(c *cli.Context) error {
|
Action: func(c *cli.Context) error {
|
||||||
|
var pool *nostr.SimplePool
|
||||||
|
relayUrls := c.Args().Slice()
|
||||||
|
if len(relayUrls) > 0 {
|
||||||
|
var relays []*nostr.Relay
|
||||||
|
pool, relays = connectToAllRelays(c.Context, relayUrls)
|
||||||
|
if len(relays) == 0 {
|
||||||
|
log("failed to connect to any of the given relays.\n")
|
||||||
|
os.Exit(3)
|
||||||
|
}
|
||||||
|
relayUrls = make([]string, len(relays))
|
||||||
|
for i, relay := range relays {
|
||||||
|
relayUrls[i] = relay.URL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for stdinFilter := range getStdinLinesOrBlank() {
|
for stdinFilter := range getStdinLinesOrBlank() {
|
||||||
filter := nostr.Filter{}
|
filter := nostr.Filter{}
|
||||||
if stdinFilter != "" {
|
if stdinFilter != "" {
|
||||||
if err := json.Unmarshal([]byte(stdinFilter), &filter); err != nil {
|
if err := json.Unmarshal([]byte(stdinFilter), &filter); err != nil {
|
||||||
lineProcessingError(c, "invalid filter received from stdin: %s", err)
|
lineProcessingError(c, "invalid filter '%s' received from stdin: %s", stdinFilter, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -155,14 +170,12 @@ example:
|
|||||||
filter.Limit = limit
|
filter.Limit = limit
|
||||||
}
|
}
|
||||||
|
|
||||||
relays := c.Args().Slice()
|
if len(relayUrls) > 0 {
|
||||||
if len(relays) > 0 {
|
|
||||||
pool := nostr.NewSimplePool(c.Context)
|
|
||||||
fn := pool.SubManyEose
|
fn := pool.SubManyEose
|
||||||
if c.Bool("stream") {
|
if c.Bool("stream") {
|
||||||
fn = pool.SubMany
|
fn = pool.SubMany
|
||||||
}
|
}
|
||||||
for ie := range fn(c.Context, relays, nostr.Filters{filter}) {
|
for ie := range fn(c.Context, relayUrls, nostr.Filters{filter}) {
|
||||||
fmt.Println(ie.Event)
|
fmt.Println(ie.Event)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -13,8 +13,7 @@ var verify = &cli.Command{
|
|||||||
Description: `example:
|
Description: `example:
|
||||||
echo '{"id":"a889df6a387419ff204305f4c2d296ee328c3cd4f8b62f205648a541b4554dfb","pubkey":"c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5","created_at":1698623783,"kind":1,"tags":[],"content":"hello from the nostr army knife","sig":"84876e1ee3e726da84e5d195eb79358b2b3eaa4d9bd38456fde3e8a2af3f1cd4cda23f23fda454869975b3688797d4c66e12f4c51c1b43c6d2997c5e61865661"}' | nak verify
|
echo '{"id":"a889df6a387419ff204305f4c2d296ee328c3cd4f8b62f205648a541b4554dfb","pubkey":"c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5","created_at":1698623783,"kind":1,"tags":[],"content":"hello from the nostr army knife","sig":"84876e1ee3e726da84e5d195eb79358b2b3eaa4d9bd38456fde3e8a2af3f1cd4cda23f23fda454869975b3688797d4c66e12f4c51c1b43c6d2997c5e61865661"}' | nak verify
|
||||||
|
|
||||||
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 {
|
||||||
for stdinEvent := range getStdinLinesOrBlank() {
|
for stdinEvent := range getStdinLinesOrBlank() {
|
||||||
evt := nostr.Event{}
|
evt := nostr.Event{}
|
||||||
|
|||||||
Reference in New Issue
Block a user