Compare commits

...

24 Commits

Author SHA1 Message Date
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
fiatjaf
455ec79e58 NIP-50 search filter on req. 2023-10-08 15:49:11 -03:00
fiatjaf
8d111e556e count uses all relays again, now correctly. 2023-10-08 15:31:20 -03:00
fiatjaf
e4a9b3ccc7 fix count again, it was sending REQs instead of COUNTs to relays. only use the first relay. 2023-10-08 15:05:51 -03:00
fiatjaf
3896ef323b update go-nostr dependency. 2023-10-08 14:47:45 -03:00
fiatjaf
c214513304 improve count. 2023-10-08 14:40:46 -03:00
Yasuhiro Matsumoto
6ccca357e2 support NIP-45 COUNT 2023-08-23 12:50:23 -03:00
fiatjaf
f9cf01b48b do target validation on a case-by-case basis and don't validate empty -author on nevent. 2023-07-08 20:52:50 -03:00
fiatjaf
fb7c49bb5c add nak encode to readme. 2023-07-05 15:05:09 -03:00
fiatjaf
4ad0769a62 add encode command with support for all the nip19 things. 2023-07-05 15:03:26 -03:00
fiatjaf
3ace11d7b2 support --nson flag on event. 2023-07-05 14:11:15 -03:00
fiatjaf
194e94ec9a update go-nostr for OK-related security fix. 2023-06-26 21:05:01 -03:00
fiatjaf
2b2018b742 allow extra tag elements on event creation, separated by ";" 2023-06-26 20:52:12 -03:00
fiatjaf
30c8eb83b2 rename editable to selectable. 2023-06-20 15:33:29 -03:00
fiatjaf
015cfd857c print naddr if parsed event is replaceable. 2023-06-20 15:31:53 -03:00
fiatjaf
4e5f7e6d21 print naddr when given an "a" tag. 2023-06-20 15:21:25 -03:00
fiatjaf
fb9faf24ae mention that the "d" tag is the identifier. 2023-06-20 14:52:03 -03:00
fiatjaf
76ca99a73b clicking on edit button to fill in the input. 2023-06-20 14:49:56 -03:00
fiatjaf
7890466783 removing relay hints. 2023-06-20 11:57:01 -03:00
fiatjaf
ba2d86ca33 each relay hint in a separate component. 2023-06-20 11:48:50 -03:00
fiatjaf
dff57c207e accept tags with keys of any length. 2023-06-07 07:02:38 -03:00
fiatjaf
c3777abd81 fix "if this is a private key" section. 2023-06-02 09:04:25 -03:00
14 changed files with 802 additions and 49 deletions

View File

@@ -54,8 +54,10 @@ USAGE:
COMMANDS:
req generates encoded REQ messages and optionally use them to talk to relays
count generates encoded COUNT messages and optionally use them to talk to relays
event generates an encoded event and either prints it or sends it to a set of relays
decode decodes nip19, nip21, nip05 or hex entities
encode encodes notes and other stuff to nip19 entities
help, h Shows a list of commands or help for one command
GLOBAL OPTIONS:
@@ -135,6 +137,36 @@ OPTIONS:
-e value [ -e value ] shortcut for --tag e=<value>
-p value [ -p value ] shortcut for --tag p=<value>
~> nak count --help
NAME:
nak count - generates encoded COUNT messages and optionally use them to talk to relays
USAGE:
nak count [command options] [relay...]
DESCRIPTION:
outputs a NIP-45 request. Mostly same as req.
example usage (with 'nostcat'):
nak count -k 1 -a 3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d | nostcat wss://nos.lol
standalone:
nak count -k 1 wss://nos.lol
OPTIONS:
--bare when printing the filter, print just the filter, not enveloped in a ["COUNT", ...] array (default: false)
--stream keep the subscription open, printing all events as they are returned (default: false, will close on EOSE)
FILTER ATTRIBUTES
--author value, -a value [ --author value, -a value ] only accept events from these authors (pubkey as hex)
--id value, -i value [ --id value, -i value ] only accept events with these ids (hex)
--kind value, -k value [ --kind value, -k value ] only accept events with these kind numbers
--limit value, -l value only accept up to this number of events (default: 0)
--since value, -s value only accept events newer than this (unix timestamp) (default: 0)
--tag value, -t value [ --tag value, -t value ] takes a tag like -t e=<id>, only accept events with these tags
--until value, -u value only accept events older than this (unix timestamp) (default: 0)
-e value [ -e value ] shortcut for --tag e=<value>
-p value [ -p value ] shortcut for --tag p=<value>
~> nak decode --help
NAME:
@@ -154,6 +186,34 @@ OPTIONS:
--id, -e return just the event id, if applicable (default: false)
--pubkey, -p return just the pubkey, if applicable (default: false)
--help, -h show help
~> nak encode --help
NAME:
nak encode - encodes notes and other stuff to nip19 entities
USAGE:
nak encode command [command options] [arguments...]
DESCRIPTION:
example usage:
nak encode npub <pubkey-hex>
nak encode nprofile <pubkey-hex>
nak encode nprofile --relay <relay-url> <pubkey-hex>
nak encode nevent <event-id>
nak encode nevent --author <pubkey-hex> --relay <relay-url> --relay <other-relay> <event-id>
nak encode nsec <privkey-hex>
COMMANDS:
npub encode a hex private key into bech32 'npub' format
nsec encode a hex private key into bech32 'nsec' format
nprofile generate profile codes with attached relay information
nevent generate event codes with optionally attached relay information
naddr generate codes for NIP-33 parameterized replaceable events
help, h Shows a list of commands or help for one command
OPTIONS:
--help, -h show help
```
written in go using [go-nostr](https://github.com/nbd-wtf/go-nostr), heavily inspired by [nostril](http://git.jb55.com/nostril/).

147
count.go Normal file
View File

@@ -0,0 +1,147 @@
package main
import (
"encoding/json"
"errors"
"fmt"
"strings"
"github.com/nbd-wtf/go-nostr"
"github.com/urfave/cli/v2"
)
var count = &cli.Command{
Name: "count",
Usage: "generates encoded COUNT messages and optionally use them to talk to relays",
Description: `outputs a NIP-45 request (the flags are mostly the same as 'nak req').`,
Flags: []cli.Flag{
&cli.StringSliceFlag{
Name: "author",
Aliases: []string{"a"},
Usage: "only accept events from these authors (pubkey as hex)",
Category: CATEGORY_FILTER_ATTRIBUTES,
},
&cli.IntSliceFlag{
Name: "kind",
Aliases: []string{"k"},
Usage: "only accept events with these kind numbers",
Category: CATEGORY_FILTER_ATTRIBUTES,
},
&cli.StringSliceFlag{
Name: "tag",
Aliases: []string{"t"},
Usage: "takes a tag like -t e=<id>, only accept events with these tags",
Category: CATEGORY_FILTER_ATTRIBUTES,
},
&cli.StringSliceFlag{
Name: "e",
Usage: "shortcut for --tag e=<value>",
Category: CATEGORY_FILTER_ATTRIBUTES,
},
&cli.StringSliceFlag{
Name: "p",
Usage: "shortcut for --tag p=<value>",
Category: CATEGORY_FILTER_ATTRIBUTES,
},
&cli.IntFlag{
Name: "since",
Aliases: []string{"s"},
Usage: "only accept events newer than this (unix timestamp)",
Category: CATEGORY_FILTER_ATTRIBUTES,
},
&cli.IntFlag{
Name: "until",
Aliases: []string{"u"},
Usage: "only accept events older than this (unix timestamp)",
Category: CATEGORY_FILTER_ATTRIBUTES,
},
&cli.IntFlag{
Name: "limit",
Aliases: []string{"l"},
Usage: "only accept up to this number of events",
Category: CATEGORY_FILTER_ATTRIBUTES,
},
},
ArgsUsage: "[relay...]",
Action: func(c *cli.Context) error {
filter := nostr.Filter{}
if authors := c.StringSlice("author"); len(authors) > 0 {
filter.Authors = authors
}
if ids := c.StringSlice("id"); len(ids) > 0 {
filter.IDs = ids
}
if kinds := c.IntSlice("kind"); len(kinds) > 0 {
filter.Kinds = kinds
}
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 = 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()
successes := 0
failures := make([]error, 0, len(relays))
if len(relays) > 0 {
for _, relayUrl := range relays {
relay, err := nostr.RelayConnect(c.Context, relayUrl)
if err != nil {
failures = append(failures, err)
continue
}
count, err := relay.Count(c.Context, nostr.Filters{filter})
if err != nil {
failures = append(failures, err)
continue
}
fmt.Printf("%s: %d\n", relay.URL, count)
successes++
}
if successes == 0 {
return errors.Join(failures...)
}
} else {
// no relays given, will just print the filter
var result string
j, _ := json.Marshal([]any{"COUNT", "nak", filter})
result = string(j)
fmt.Println(result)
}
return nil
},
}

7
edit.svg Normal file
View File

@@ -0,0 +1,7 @@
<?xml version="1.0" ?>
<svg width="800px" height="800px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<g>
<path d="M20,16v4a2,2,0,0,1-2,2H4a2,2,0,0,1-2-2V6A2,2,0,0,1,4,4H8" fill="none" stroke="#f9cc9d" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<polygon fill="none" points="12.5 15.8 22 6.2 17.8 2 8.3 11.5 8 16 12.5 15.8" stroke="#f9cc9d" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 463 B

241
encode.go Normal file
View File

@@ -0,0 +1,241 @@
package main
import (
"encoding/hex"
"fmt"
"net/url"
"strings"
"github.com/nbd-wtf/go-nostr/nip19"
"github.com/urfave/cli/v2"
)
var encode = &cli.Command{
Name: "encode",
Usage: "encodes notes and other stuff to nip19 entities",
Description: `example usage:
nak encode npub <pubkey-hex>
nak encode nprofile <pubkey-hex>
nak encode nprofile --relay <relay-url> <pubkey-hex>
nak encode nevent <event-id>
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.")
}
return nil
},
Subcommands: []*cli.Command{
{
Name: "npub",
Usage: "encode a hex private key into bech32 'npub' format",
Action: func(c *cli.Context) error {
target := c.Args().First()
if err := validate32BytesHex(target); err != nil {
return err
}
if npub, err := nip19.EncodePublicKey(target); err == nil {
fmt.Println(npub)
return nil
} else {
return err
}
},
},
{
Name: "nsec",
Usage: "encode a hex private key into bech32 'nsec' format",
Action: func(c *cli.Context) error {
target := c.Args().First()
if err := validate32BytesHex(target); err != nil {
return err
}
if npub, err := nip19.EncodePrivateKey(target); err == nil {
fmt.Println(npub)
return nil
} else {
return err
}
},
},
{
Name: "nprofile",
Usage: "generate profile codes with attached relay information",
Flags: []cli.Flag{
&cli.StringSliceFlag{
Name: "relay",
Aliases: []string{"r"},
Usage: "attach relay hints to nprofile code",
},
},
Action: func(c *cli.Context) error {
target := c.Args().First()
if err := validate32BytesHex(target); err != nil {
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
}
},
},
{
Name: "nevent",
Usage: "generate event codes with optionally attached relay information",
Flags: []cli.Flag{
&cli.StringSliceFlag{
Name: "relay",
Aliases: []string{"r"},
Usage: "attach relay hints to nevent code",
},
&cli.StringFlag{
Name: "author",
Usage: "attach an author pubkey as a hint to the nevent code",
},
},
Action: func(c *cli.Context) error {
target := c.Args().First()
if err := validate32BytesHex(target); err != nil {
return err
}
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)
return nil
} else {
return err
}
},
},
{
Name: "naddr",
Usage: "generate codes for NIP-33 parameterized replaceable events",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "identifier",
Aliases: []string{"d"},
Usage: "the \"d\" tag identifier of this replaceable event",
Required: true,
},
&cli.StringFlag{
Name: "pubkey",
Usage: "pubkey of the naddr author",
Aliases: []string{"p"},
Required: true,
},
&cli.Int64Flag{
Name: "kind",
Aliases: []string{"k"},
Usage: "kind of referred replaceable event",
Required: true,
},
&cli.StringSliceFlag{
Name: "relay",
Aliases: []string{"r"},
Usage: "attach relay hints to naddr code",
},
},
Action: func(c *cli.Context) error {
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)
}
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",
Usage: "generate note1 event codes (not recommended)",
Action: func(c *cli.Context) error {
target := c.Args().First()
if err := validate32BytesHex(target); err != nil {
return err
}
if npub, err := nip19.EncodeNote(target); err == nil {
fmt.Println(npub)
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
}
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

@@ -9,7 +9,9 @@ import (
"strings"
"time"
"github.com/mailru/easyjson"
"github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nson"
"github.com/urfave/cli/v2"
)
@@ -33,6 +35,10 @@ standalone:
Name: "envelope",
Usage: "print the event enveloped in a [\"EVENT\", ...] message ready to be sent to a relay",
},
&cli.BoolFlag{
Name: "nson",
Usage: "encode the event using NSON",
},
&cli.IntFlag{
Name: "kind",
Aliases: []string{"k"},
@@ -82,11 +88,17 @@ standalone:
Tags: make(nostr.Tags, 0, 3),
}
tags := make([][]string, 0, 5)
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]) == 1 {
tags = append(tags, spl)
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") {
@@ -138,8 +150,11 @@ standalone:
if c.Bool("envelope") {
j, _ := json.Marshal([]any{"EVENT", evt})
result = string(j)
} else if c.Bool("nson") {
result, _ = nson.Marshal(&evt)
} else {
result = evt.String()
j, _ := easyjson.Marshal(&evt)
result = string(j)
}
fmt.Println(result)
}

View File

@@ -1 +1 @@
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M17 13.5v6H5v-12h6m3-3h6v6m0-6-9 9" class="icon_svg-stroke" stroke="#666" stroke-width="1.5" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round"></path></svg>
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M17 13.5v6H5v-12h6m3-3h6v6m0-6-9 9" class="icon_svg-stroke" stroke="#3b82f6" stroke-width="1.5" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round"></path></svg>

Before

Width:  |  Height:  |  Size: 278 B

After

Width:  |  Height:  |  Size: 281 B

73
fetch.go Normal file
View File

@@ -0,0 +1,73 @@
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: ``,
Flags: []cli.Flag{},
ArgsUsage: "[nip19code]",
Action: func(c *cli.Context) error {
filter := nostr.Filter{}
code := c.Args().First()
prefix, value, err := nip19.Decode(code)
if err != nil {
return err
}
var relays []string
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)
authorHint = v.PublicKey
relays = v.Relays
}
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
},
}

11
go.mod
View File

@@ -3,7 +3,8 @@ module github.com/fiatjaf/nak
go 1.20
require (
github.com/nbd-wtf/go-nostr v0.18.7
github.com/mailru/easyjson v0.7.7
github.com/nbd-wtf/go-nostr v0.24.2
github.com/urfave/cli/v2 v2.25.3
)
@@ -11,15 +12,19 @@ require (
github.com/btcsuite/btcd/btcec/v2 v2.2.0 // indirect
github.com/btcsuite/btcd/btcutil v1.1.3 // indirect
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.1.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/decred/dcrd/crypto/blake256 v1.0.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
github.com/dgraph-io/ristretto v0.1.1 // indirect
github.com/dustin/go-humanize v1.0.0 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.2.0 // indirect
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/puzpuzpuz/xsync v1.5.2 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/puzpuzpuz/xsync/v2 v2.5.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/tidwall/gjson v1.14.4 // indirect
github.com/tidwall/match v1.1.1 // indirect

26
go.sum
View File

@@ -22,6 +22,8 @@ github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku
github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -33,6 +35,12 @@ github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218=
github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8=
github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
@@ -41,6 +49,8 @@ github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.2.0 h1:u0p9s3xLYpZCA1z5JgCkMeB34CKCMMQbM+G8Ii7YD0I=
github.com/gobwas/ws v1.2.0/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
@@ -61,8 +71,8 @@ github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlT
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/nbd-wtf/go-nostr v0.18.7 h1:UZBc5ewiosuTVEWi9rXRDEu83oH1W6Gf8n0QzyTP/UQ=
github.com/nbd-wtf/go-nostr v0.18.7/go.mod h1:F9y6+M8askJCjilLgMC3rD0moA6UtG1MCnyClNYXeys=
github.com/nbd-wtf/go-nostr v0.24.2 h1:1PdFED7uHh3BlXfDVD96npBc0YAgj9hPT+l6NWog4kc=
github.com/nbd-wtf/go-nostr v0.24.2/go.mod h1:eE8Qf8QszZbCd9arBQyotXqATNUElWsTEEx+LLORhyQ=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
@@ -72,12 +82,17 @@ github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/puzpuzpuz/xsync v1.5.2 h1:yRAP4wqSOZG+/4pxJ08fPTwrfL0IzE/LKQ/cw509qGY=
github.com/puzpuzpuz/xsync v1.5.2/go.mod h1:K98BYhX3k1dQ2M63t1YNVDanbwUPmBCAhNmVrrxfiGg=
github.com/puzpuzpuz/xsync/v2 v2.5.0 h1:2k4qrO/orvmEXZ3hmtHqIy9XaQtPTwzMZk1+iErpE8c=
github.com/puzpuzpuz/xsync/v2 v2.5.0/go.mod h1:gD2H2krq/w52MfPLE+Uy64TzJDVY7lP2znR9qmR35kU=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
@@ -111,6 +126,7 @@ golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -129,6 +145,8 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View File

@@ -13,8 +13,11 @@ func main() {
Usage: "the nostr army knife command-line tool",
Commands: []*cli.Command{
req,
count,
fetch,
event,
decode,
encode,
},
}

13
req.go
View File

@@ -73,6 +73,11 @@ standalone:
Usage: "only accept up to this number of events",
Category: CATEGORY_FILTER_ATTRIBUTES,
},
&cli.StringFlag{
Name: "search",
Usage: "a NIP-50 search query, use it only with relays that explicitly support it",
Category: CATEGORY_FILTER_ATTRIBUTES,
},
&cli.BoolFlag{
Name: "bare",
Usage: "when printing the filter, print just the filter, not enveloped in a [\"REQ\", ...] array",
@@ -96,7 +101,9 @@ standalone:
if kinds := c.IntSlice("kind"); len(kinds) > 0 {
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, "=")
@@ -141,8 +148,8 @@ standalone:
if c.Bool("stream") {
fn = pool.SubMany
}
for evt := range fn(c.Context, relays, nostr.Filters{filter}) {
fmt.Println(evt)
for ie := range fn(c.Context, relays, nostr.Filters{filter}) {
fmt.Println(ie.Event)
}
} else {
// no relays given, will just print the filter

View File

@@ -16,7 +16,10 @@ import snow.*
import Utils.*
object Components {
def render32Bytes(bytes32: ByteVector32): Resource[IO, HtmlDivElement[IO]] =
def render32Bytes(
store: Store,
bytes32: ByteVector32
): Resource[IO, HtmlDivElement[IO]] =
div(
cls := "text-md",
entry("canonical hex", bytes32.toHex),
@@ -25,9 +28,16 @@ object Components {
cls := "mt-2 pl-2 mb-2",
entry(
"npub",
NIP19.encode(XOnlyPublicKey(bytes32)),
Some(
selectable(
store,
NIP19.encode(XOnlyPublicKey(bytes32))
)
)
),
nip19_21(
store,
"nprofile",
NIP19.encode(ProfilePointer(XOnlyPublicKey(bytes32)))
)
@@ -37,21 +47,35 @@ object Components {
cls := "pl-2 mb-2",
entry(
"nsec",
NIP19.encode(PrivateKey(bytes32)),
Some(
selectable(
store,
NIP19.encode(PrivateKey(bytes32))
)
)
),
entry(
"npub",
NIP19.encode(XOnlyPublicKey(bytes32))
NIP19.encode(PrivateKey(bytes32).publicKey.xonly),
Some(
selectable(
store,
NIP19.encode(PrivateKey(bytes32).publicKey.xonly)
)
)
),
nip19_21(
store,
"nprofile",
NIP19.encode(ProfilePointer(XOnlyPublicKey(bytes32)))
NIP19.encode(ProfilePointer(PrivateKey(bytes32).publicKey.xonly))
)
),
"if this is an event id:",
div(
cls := "pl-2 mb-2",
nip19_21(
store,
"nevent",
NIP19.encode(EventPointer(bytes32.toHex))
)
@@ -60,10 +84,16 @@ object Components {
cls := "pl-2 mb-2",
entry(
"note",
NIP19.encode(bytes32),
Some(
selectable(
store,
NIP19.encode(bytes32)
)
)
)
)
)
def renderEventPointer(
store: Store,
@@ -71,13 +101,21 @@ object Components {
): Resource[IO, HtmlDivElement[IO]] =
div(
cls := "text-md",
entry("event id (hex)", evp.id),
entry(
"event id (hex)",
evp.id,
Some(selectable(store, evp.id))
),
relayHints(store, evp.relays),
evp.author.map { pk =>
entry("author hint (pubkey hex)", pk.value.toHex)
},
nip19_21("nevent", NIP19.encode(evp)),
entry("note", NIP19.encode(ByteVector32.fromValidHex(evp.id)))
nip19_21(store, "nevent", NIP19.encode(evp)),
entry(
"note",
NIP19.encode(ByteVector32.fromValidHex(evp.id)),
Some(selectable(store, NIP19.encode(ByteVector32.fromValidHex(evp.id))))
)
)
def renderProfilePointer(
@@ -87,30 +125,55 @@ object Components {
): Resource[IO, HtmlDivElement[IO]] =
div(
cls := "text-md",
sk.map { k => entry("private key (hex)", k.value.toHex) },
sk.map { k => entry("nsec", NIP19.encode(k)) },
entry("public key (hex)", pp.pubkey.value.toHex),
sk.map { k =>
entry(
"private key (hex)",
k.value.toHex,
Some(selectable(store, k.value.toHex))
)
},
sk.map { k =>
entry(
"nsec",
NIP19.encode(k),
Some(selectable(store, NIP19.encode(k)))
)
},
entry(
"public key (hex)",
pp.pubkey.value.toHex,
Some(selectable(store, pp.pubkey.value.toHex))
),
relayHints(
store,
pp.relays,
dynamic = if sk.isDefined then false else true
),
entry("npub", NIP19.encode(pp.pubkey)),
nip19_21("nprofile", NIP19.encode(pp))
entry(
"npub",
NIP19.encode(pp.pubkey),
Some(selectable(store, NIP19.encode(pp.pubkey)))
),
nip19_21(store, "nprofile", NIP19.encode(pp))
)
def renderAddressPointer(
store: Store,
addr: snow.AddressPointer
): Resource[IO, HtmlDivElement[IO]] =
): Resource[IO, HtmlDivElement[IO]] = {
val nip33atag =
s"${addr.kind}:${addr.author.value.toHex}:${addr.d}"
div(
cls := "text-md",
entry("author (pubkey hex)", addr.author.value.toHex),
entry("identifier", addr.d),
entry("identifier (d tag)", addr.d),
entry("kind", addr.kind.toString),
relayHints(store, addr.relays),
nip19_21("naddr", NIP19.encode(addr))
nip19_21(store, "naddr", NIP19.encode(addr)),
entry("nip33 'a' tag", nip33atag, Some(selectable(store, nip33atag)))
)
}
def renderEvent(
store: Store,
@@ -205,35 +268,60 @@ object Components {
),
event.id.map(id =>
nip19_21(
store,
"nevent",
NIP19.encode(EventPointer(id, author = event.pubkey))
)
),
if event.kind >= 30000 && event.kind < 40000 then
event.pubkey
.map(author =>
nip19_21(
store,
"naddr",
NIP19.encode(
AddressPointer(
d = event.tags
.collectFirst { case "d" :: v :: _ => v }
.getOrElse(""),
kind = event.kind,
author = author,
relays = List.empty
)
)
)
)
else
event.id.map(id =>
entry(
"note",
NIP19.encode(ByteVector32.fromValidHex(id))
NIP19.encode(ByteVector32.fromValidHex(id)),
Some(selectable(store, NIP19.encode(ByteVector32.fromValidHex(id))))
)
)
)
private def entry(
key: String,
value: String
value: String,
selectLink: Option[Resource[IO, HtmlSpanElement[IO]]] = None
): Resource[IO, HtmlDivElement[IO]] =
div(
cls := "flex items-center space-x-3",
span(cls := "font-bold", key + " "),
span(Styles.mono, cls := "max-w-xl", value)
span(Styles.mono, cls := "max-w-xl break-all", value),
selectLink
)
private def nip19_21(
store: Store,
key: String,
code: String
): Resource[IO, HtmlDivElement[IO]] =
div(
span(cls := "font-bold", key + " "),
span(Styles.mono, cls := "break-all", code),
selectable(store, code),
a(
href := "nostr:" + code,
external
@@ -254,13 +342,65 @@ object Components {
div(
cls := "flex items-center space-x-3",
span(cls := "font-bold", "relay hints "),
span(Styles.mono, cls := "max-w-xl", value),
if relays.size == 0 then div("")
else
// displaying each relay hint
div(
cls := "flex flex-wrap max-w-xl",
relays
.map(url =>
div(
Styles.mono,
cls := "flex items-center rounded py-0.5 px-1 mr-1 mb-1 bg-orange-100",
url,
// removing a relay hint by clicking on the x
div(
cls := "cursor-pointer ml-1 text-rose-600 hover:text-rose-300",
onClick --> (_.foreach(_ => {
store.result.get.flatMap(result =>
store.input.set(
result
.map {
case a: AddressPointer =>
NIP19
.encode(
a.copy(relays =
relays.filterNot(_ == url)
)
)
case p: ProfilePointer =>
NIP19
.encode(
p.copy(relays =
relays.filterNot(_ == url)
)
)
case e: EventPointer =>
NIP19
.encode(
e.copy(relays =
relays.filterNot(_ == url)
)
)
case r => ""
}
.getOrElse("")
)
)
})),
"×"
)
)
)
)
,
active.map {
case true =>
div(
input.withSelf { self =>
(
onKeyPress --> (_.foreach(evt =>
// confirm adding a relay hint
evt.key match {
case "Enter" =>
self.value.get.flatMap(url =>
@@ -274,17 +414,17 @@ object Components {
case a: AddressPointer =>
NIP19
.encode(
a.copy(relays = url :: a.relays)
a.copy(relays = a.relays :+ url)
)
case p: ProfilePointer =>
NIP19
.encode(
p.copy(relays = url :: p.relays)
p.copy(relays = p.relays :+ url)
)
case e: EventPointer =>
NIP19
.encode(
e.copy(relays = url :: e.relays)
e.copy(relays = e.relays :+ url)
)
case r => ""
}
@@ -301,6 +441,7 @@ object Components {
}
)
case false if dynamic =>
// button to add a new relay hint
button(
Styles.buttonSmall,
"add relay hint",
@@ -311,5 +452,25 @@ object Components {
)
}
private def selectable(
store: Store,
code: String
): Resource[IO, HtmlSpanElement[IO]] =
span(
store.input.map(current =>
if current == code then a("")
else
a(
href := "#/" + code,
onClick --> (_.foreach(evt =>
evt.preventDefault >>
store.input.set(code)
)),
edit
)
)
)
private val edit = img(cls := "inline w-4 ml-2", src := "edit.svg")
private val external = img(cls := "inline w-4 ml-2", src := "ext.svg")
}

View File

@@ -18,7 +18,7 @@ object Main extends IOWebApp {
def render: Resource[IO, HtmlDivElement[IO]] = Store(window).flatMap {
store =>
div(
cls := "flex w-full h-full flex-col items-center justify-center",
cls := "flex w-full flex-col items-center justify-center",
div(
cls := "w-4/5",
h1(
@@ -131,7 +131,7 @@ object Main extends IOWebApp {
cls := "w-full flex my-5",
store.result.map {
case Left(msg) => div(msg)
case Right(bytes: ByteVector32) => render32Bytes(bytes)
case Right(bytes: ByteVector32) => render32Bytes(store, bytes)
case Right(event: Event) => renderEvent(store, event)
case Right(pp: ProfilePointer) => renderProfilePointer(store, pp)
case Right(evp: EventPointer) => renderEventPointer(store, evp)

View File

@@ -26,7 +26,23 @@ object Parser {
case Right(evp: EventPointer) => Right(evp)
case Right(sk: PrivateKey) => Right(sk)
case Right(addr: AddressPointer) => Right(addr)
case Left(_) if input.split(":").size == 3 =>
// parse "a" tag format, nip 33
val spl = input.split(":")
(
spl(0).toIntOption,
ByteVector.fromHex(spl(1)),
Some(spl(2))
).mapN((kind, author, identifier) =>
AddressPointer(
identifier,
kind,
scoin.XOnlyPublicKey(ByteVector32(author)),
relays = List.empty
)
).toRight("couldn't parse as a nip33 'a' tag")
case Left(_) =>
// parse event json
parse(input) match {
case Left(err: io.circe.ParsingFailure) =>
Left("not valid JSON or NIP-19 code")