Compare commits

...

11 Commits

Author SHA1 Message Date
fiatjaf
85d658bdd4 github action to publish the cli binaries. 2023-10-29 23:15:04 -03:00
fiatjaf
bf966b3e2c nak event can take (and optionally modify) events from stdin. 2023-10-29 21:48:18 -03:00
fiatjaf
0615a8b577 nak req can take (and optionally modify) filters from stdin. 2023-10-29 19:11:35 -03:00
fiatjaf
c6e9fdd053 trim spaces from stdin. 2023-10-23 08:04:28 -03:00
fiatjaf
50dde2117c don't fail encode when reading from stdin because of the number of arguments. 2023-10-23 08:04:21 -03:00
fiatjaf
ffa41046fd fetch with optional --relay flags. 2023-10-20 21:01:11 -03:00
fiatjaf
757a6eb313 support fetch npub 2023-10-20 20:57:41 -03:00
fiatjaf
208d909727 support reading from stdin. 2023-10-20 20:57:29 -03:00
fiatjaf
459b127988 fetch: use relay hints from author pubkeys. 2023-10-15 09:22:45 -03:00
fiatjaf
db157e6181 fetch method to fetch events from nip19 codes and relay hints. 2023-10-15 09:18:23 -03:00
fiatjaf
ada76f281a add encode note. 2023-10-10 11:29:06 -03:00
8 changed files with 289 additions and 59 deletions

39
.github/workflows/release-cli.yml vendored Normal file
View File

@@ -0,0 +1,39 @@
name: build cli for all platforms
on:
push:
tags:
- '*'
permissions:
contents: write
jobs:
make-release:
runs-on: ubuntu-latest
steps:
- uses: actions/create-release@latest
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: ${{ github.ref }}
build-all-for-all:
runs-on: ubuntu-latest
needs:
- make-release
strategy:
matrix:
goos: [linux, freebsd, darwin, windows]
goarch: [amd64, arm64]
exclude:
- goarch: arm64
goos: windows
steps:
- uses: actions/checkout@v3
- uses: wangyoucao577/go-release-action@v1.40
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
goos: ${{ matrix.goos }}
goarch: ${{ matrix.goarch }}
overwrite: true

View File

@@ -3,7 +3,6 @@ package main
import (
"encoding/hex"
"fmt"
"net/url"
"strings"
"github.com/nbd-wtf/go-nostr/nip19"
@@ -21,8 +20,8 @@ var encode = &cli.Command{
nak encode nevent --author <pubkey-hex> --relay <relay-url> --relay <other-relay> <event-id>
nak encode nsec <privkey-hex>`,
Before: func(c *cli.Context) error {
if c.Args().Len() < 2 {
return fmt.Errorf("expected more than 2 arguments.")
if c.Args().Len() < 1 {
return fmt.Errorf("expected more than 1 argument.")
}
return nil
},
@@ -31,7 +30,7 @@ var encode = &cli.Command{
Name: "npub",
Usage: "encode a hex private key into bech32 'npub' format",
Action: func(c *cli.Context) error {
target := c.Args().First()
target := getStdinOrFirstArgument(c)
if err := validate32BytesHex(target); err != nil {
return err
}
@@ -48,7 +47,7 @@ var encode = &cli.Command{
Name: "nsec",
Usage: "encode a hex private key into bech32 'nsec' format",
Action: func(c *cli.Context) error {
target := c.Args().First()
target := getStdinOrFirstArgument(c)
if err := validate32BytesHex(target); err != nil {
return err
}
@@ -72,7 +71,7 @@ var encode = &cli.Command{
},
},
Action: func(c *cli.Context) error {
target := c.Args().First()
target := getStdinOrFirstArgument(c)
if err := validate32BytesHex(target); err != nil {
return err
}
@@ -105,7 +104,7 @@ var encode = &cli.Command{
},
},
Action: func(c *cli.Context) error {
target := c.Args().First()
target := getStdinOrFirstArgument(c)
if err := validate32BytesHex(target); err != nil {
return err
}
@@ -187,6 +186,23 @@ var encode = &cli.Command{
}
},
},
{
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
}
if npub, err := nip19.EncodeNote(target); err == nil {
fmt.Println(npub)
return nil
} else {
return err
}
},
},
},
}
@@ -203,22 +219,3 @@ func validate32BytesHex(target string) error {
return nil
}
func validateRelayURLs(wsurls []string) error {
for _, wsurl := range wsurls {
u, err := url.Parse(wsurl)
if err != nil {
return fmt.Errorf("invalid relay url '%s': %s", wsurl, err)
}
if u.Scheme != "ws" && u.Scheme != "wss" {
return fmt.Errorf("relay url must use wss:// or ws:// schemes, got '%s'", wsurl)
}
if u.Host == "" {
return fmt.Errorf("relay url '%s' is missing the hostname", wsurl)
}
}
return nil
}

View File

@@ -20,10 +20,18 @@ const CATEGORY_EVENT_FIELDS = "EVENT FIELDS"
var event = &cli.Command{
Name: "event",
Usage: "generates an encoded event and either prints it or sends it to a set of relays",
Description: `example usage (for sending directly to a relay with 'nostcat'):
nak event -k 1 -c hello --envelope | nostcat wss://nos.lol
standalone:
nak event -k 1 -c hello wss://nos.lol`,
Description: `outputs an event built with the flags. if one or more relays are given as arguments, an attempt is also made to publish the event to these relays.
example:
nak event -c hello wss://nos.lol
nak event -k 3 -p 3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d
if an event -- or a partial event -- is given on stdin, the flags can be used to optionally modify it. if it is modified it is rehashed and resigned, otherwise it is just returned as given, but that can be used to just publish to relays.
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 '{"tags": [["t", "spam"]]}' | nak event -c 'this is spam'
`,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "sec",
@@ -44,7 +52,7 @@ standalone:
Aliases: []string{"k"},
Usage: "event kind",
DefaultText: "1",
Value: 1,
Value: 0,
Category: CATEGORY_EVENT_FIELDS,
},
&cli.StringFlag{
@@ -52,7 +60,7 @@ standalone:
Aliases: []string{"c"},
Usage: "event content",
DefaultText: "hello from the nostr army knife",
Value: "hello from the nostr army knife",
Value: "",
Category: CATEGORY_EVENT_FIELDS,
},
&cli.StringSliceFlag{
@@ -76,16 +84,38 @@ standalone:
Aliases: []string{"time", "ts"},
Usage: "unix timestamp value for the created_at field",
DefaultText: "now",
Value: "now",
Value: "",
Category: CATEGORY_EVENT_FIELDS,
},
},
ArgsUsage: "[relay...]",
Action: func(c *cli.Context) error {
evt := nostr.Event{
Kind: c.Int("kind"),
Content: c.String("content"),
Tags: make(nostr.Tags, 0, 3),
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 {
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)
@@ -103,29 +133,39 @@ standalone:
}
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
}
createdAt := c.String("created-at")
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)
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
}
evt.CreatedAt = nostr.Timestamp(ts.Unix())
if err := evt.Sign(c.String("sec")); err != nil {
return fmt.Errorf("error signing with provided key: %w", err)
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()

90
fetch.go Normal file
View File

@@ -0,0 +1,90 @@
package main
import (
"fmt"
"github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nip19"
"github.com/nbd-wtf/go-nostr/sdk"
"github.com/urfave/cli/v2"
)
var fetch = &cli.Command{
Name: "fetch",
Usage: "fetches events related to the given nip19 code from the included relay hints",
Description: `example usage:
nak fetch nevent1qqsxrwm0hd3s3fddh4jc2574z3xzufq6qwuyz2rvv3n087zvym3dpaqprpmhxue69uhhqatzd35kxtnjv4kxz7tfdenju6t0xpnej4
echo npub1h8spmtw9m2huyv6v2j2qd5zv956z2zdugl6mgx02f2upffwpm3nqv0j4ps | nak fetch --relay wss://relay.nostr.band`,
Flags: []cli.Flag{
&cli.StringSliceFlag{
Name: "relay",
Aliases: []string{"r"},
Usage: "also use these relays to fetch from",
},
},
ArgsUsage: "[nip19code]",
Action: func(c *cli.Context) error {
filter := nostr.Filter{}
code := getStdinOrFirstArgument(c)
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
}
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 {
return fmt.Errorf("no relay hints found")
}
for ie := range pool.SubManyEose(c.Context, relays, nostr.Filters{filter}) {
fmt.Println(ie.Event)
}
return nil
},
}

51
helpers.go Normal file
View File

@@ -0,0 +1,51 @@
package main
import (
"bytes"
"fmt"
"io"
"net/url"
"os"
"strings"
"github.com/urfave/cli/v2"
)
func getStdin() string {
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())
}
}
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)
if err != nil {
return fmt.Errorf("invalid relay url '%s': %s", wsurl, err)
}
if u.Scheme != "ws" && u.Scheme != "wss" {
return fmt.Errorf("relay url must use wss:// or ws:// schemes, got '%s'", wsurl)
}
if u.Host == "" {
return fmt.Errorf("relay url '%s' is missing the hostname", wsurl)
}
}
return nil
}

View File

@@ -14,6 +14,7 @@ func main() {
Commands: []*cli.Command{
req,
count,
fetch,
event,
decode,
encode,

38
req.go
View File

@@ -16,10 +16,15 @@ var req = &cli.Command{
Usage: "generates encoded REQ messages and optionally use them to talk to relays",
Description: `outputs a NIP-01 Nostr filter. when a relay is not given, will print the filter, otherwise will connect to the given relay and send the filter.
example usage (with 'nostcat'):
nak req -k 1 -a 3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d | nostcat wss://nos.lol
standalone:
nak req -k 1 wss://nos.lol`,
example:
nak req -k 1 -l 15 wss://nostr.wine wss://nostr-pub.wellorder.net
nak req -k 0 -a 3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d wss://nos.lol | jq '.content | fromjson | .name'
it can also take a filter from stdin, optionally modify it with flags and send it to specific relays (or just print it).
example:
echo '{"kinds": [1], "#t": ["test"]}' | nak req -l 5 -k 4549 --tag t=spam wss://nostr-pub.wellorder.net
`,
Flags: []cli.Flag{
&cli.StringSliceFlag{
Name: "author",
@@ -91,15 +96,20 @@ standalone:
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)
}
}
if authors := c.StringSlice("author"); len(authors) > 0 {
filter.Authors = authors
filter.Authors = append(filter.Authors, authors...)
}
if ids := c.StringSlice("id"); len(ids) > 0 {
filter.IDs = ids
filter.IDs = append(filter.IDs, ids...)
}
if kinds := c.IntSlice("kind"); len(kinds) > 0 {
filter.Kinds = kinds
filter.Kinds = append(filter.Kinds, kinds...)
}
if search := c.String("search"); search != "" {
filter.Search = search
@@ -119,14 +129,16 @@ standalone:
for _, ptag := range c.StringSlice("p") {
tags = append(tags, []string{"p", ptag})
}
if len(tags) > 0 {
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])
}
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 {