Compare commits

...

19 Commits

Author SHA1 Message Date
fiatjaf
e18e8c00e7 add 9 new examples to readme. 2024-07-12 19:17:22 -03:00
fiatjaf
bca4362ca5 fix inconsistency in nak key decrypt output: print hex always. 2024-07-12 19:15:13 -03:00
fiatjaf
54c4be10bd fix and improve flag reordering for subcommands. 2024-07-12 18:51:07 -03:00
fiatjaf
27f925c05e left pad keys on nak key too so nak key public 02 works, for example. 2024-07-12 14:04:14 -03:00
fiatjaf
79cb63a1b4 add all other relay management rpc methods. 2024-07-12 13:57:58 -03:00
fiatjaf
5ee0036128 implement nip86 client: making management RPC calls to relays. 2024-07-11 15:34:15 -03:00
fiatjaf
316d94166e fix lineProcessingError() -- it wasn't returning a ctx so it was a noop. 2024-07-11 15:33:44 -03:00
fiatjaf
2ca6bb0940 update tests so they can run again (but they're not working). 2024-07-10 14:48:18 -03:00
fiatjaf
ac00c5065f nak req: --force-pre-auth flag. 2024-07-10 14:48:02 -03:00
fiatjaf
441ee9a5ed update go-nostr so just "localhost[:port]" works as a relay url. 2024-07-05 00:11:59 -03:00
fiatjaf
9a41450209 use modified cli library that accepts flags after arguments. 2024-06-25 23:23:51 -03:00
fiatjaf
dba2ed0b5f update to cli v3. 2024-06-25 22:18:26 -03:00
fiatjaf
2079ddf818 support prompting for a password on nak decrypt. 2024-06-25 13:46:15 -03:00
fiatjaf
2135b68106 more aliases in nak encode flags. 2024-06-13 12:05:27 -03:00
fiatjaf
9f98a0aea3 fix nak key encrypt reading from stdin. 2024-06-12 08:54:00 -03:00
fiatjaf
1ba39ca7d7 print bunker restart command without schemes in relay urls when possible. 2024-06-07 06:30:58 -03:00
fiatjaf
262c0c892a fix simple test event ordering. 2024-06-06 15:43:42 -03:00
fiatjaf
363bd66a8a accept relay URLs without scheme everywhere. 2024-06-06 15:38:40 -03:00
fiatjaf
eccce6dc4a nak key combine now returns all possible combinations. 2024-05-16 19:50:52 -03:00
16 changed files with 648 additions and 211 deletions

View File

@@ -79,9 +79,95 @@ invalid .id, expected 05bd99d54cb835f427e0092c4275ee44c7ff51219eff417c19f70c9e2c
### fetch all quoted events by a given pubkey in their last 100 notes ### fetch all quoted events by a given pubkey in their last 100 notes
```shell ```shell
nak req -l 100 -k 1 -a 2edbcea694d164629854a52583458fd6d965b161e3c48b57d3aff01940558884 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 relay.damus.io ~> nak req -l 100 -k 1 -a 2edbcea694d164629854a52583458fd6d965b161e3c48b57d3aff01940558884 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 relay.damus.io
connecting to relay.damus.io...
ok.
{"kind":1,"id":"dad32411fea62fda6ae057e97c73402f2031913388a721e059728a0efee5f0dd","pubkey":"5b0183ab6c3e322bf4d41c6b3aef98562a144847b7499543727c5539a114563e","created_at":1709057416,"tags":[["p","ad9d42203fd2480ea2e5c4c64593a027708aebe2b02aa60bd7b1d666daa5b08d"],["p","5ea721dd7828229a39a372477090208db30a6c2d357951b8ae504d2ecf86c06c"]],"content":"Fridays Edition of nostr:npub14kw5ygpl6fyqagh9cnrytyaqyacg46lzkq42vz7hk8txdk49kzxs04j7y0 will feature nostr:npub1t6njrhtc9q3f5wdrwfrhpypq3kes5mpdx4u4rw9w2pxjanuxcpkqveagv3 \n\nWe will be diving into Bitcoin, Content Creating, Music and the future of V4V. \n\nNostr Nest 2.0 🤘🏻\nSet your BlockClocks for 4:30EST","sig":"69cc403982e6c5fe996d545d6057c581a46be97ab79d818c1bc01e84e9f11a64a275d8834a4063a59fa135f7f116e38c51125173d5ce88671a4ddc2f656e01e4"}
{"kind":1,"id":"a681e9ca594dc455018be0a1c895576a8264956aee3e4fc01b872aa6df580632","pubkey":"5b0183ab6c3e322bf4d41c6b3aef98562a144847b7499543727c5539a114563e","created_at":1708456729,"tags":[["p","ad9d42203fd2480ea2e5c4c64593a027708aebe2b02aa60bd7b1d666daa5b08d"],["t","plebchain"],["p","4ce6abbd68dab6e9fdf6e8e9912a8e12f9b539e078c634c55a9bff2994a514dd"],["p","b83a28b7e4e5d20bd960c5faeb6625f95529166b8bdb045d42634a2f35919450"],["p","5a9c48c8f4782351135dd89c5d8930feb59cb70652ffd37d9167bf922f2d1069"],["p","f8e6c64342f1e052480630e27e1016dce35fc3a614e60434fef4aa2503328ca9"],["p","2edbcea694d164629854a52583458fd6d965b161e3c48b57d3aff01940558884"],["r","https://nostrnests.com/plebchainradio"]],"content":"This weeks edition of nostr:npub14kw5ygpl6fyqagh9cnrytyaqyacg46lzkq42vz7hk8txdk49kzxs04j7y0 will feature the wildly talented #Plebchain Legend nostr:npub1fnn2h0tgm2mwnl0kar5ez25wztum2w0q0rrrf326n0ljn999znwsqf4xnx \n\nWe will be discussing building his local community in Tanzania, his worldly success in V4V music and all things Nostr and Bitcoin. \n\nFilling in for nostr:npub1hqaz3dlyuhfqhktqchawke39l92jj9nt30dsgh2zvd9z7dv3j3gqpkt56s this week will be nostr:npub1t2wy3j850q34zy6amzw9mzfsl66eedcx2tlaxlv3v7leytedzp5szs8c2u \u0026 nostr:npub1lrnvvs6z78s9yjqxxr38uyqkmn34lsaxznnqgd877j4z2qej3j5s09qnw5 from nostr:npub19mduaf5569jx9xz555jcx3v06mvktvtpu0zgk47n4lcpjsz43zzqhj6vzk \n\nSet your Blockclocks for this Friday 4PM EST! \nhttps://nostrnests.com/plebchainradio","sig":"b4528c7a248bf04ab9fcd0ce8033fdc9656b0e92dccf5f3a6b8cd7ad66cf074619100c7d192ae9a87745bc5445f6fe36221c1fd5820d5038bbcae2aedb5090d8"}
{"kind":1,"id":"cc396bbc9e01910e56ef169916c39197d468b65e80c42aaa0a874a32500039c4","pubkey":"b9d02cb8fddeb191701ec0648e37ed1f6afba263e0060fc06099a62851d25e04","created_at":1708210291,"tags":[["imeta","url https://image.nostr.build/185e4c10dc2e46dacf6ad38fbc4319f52860fc3f96efb433be55bdb91fc225b8.jpg","blurhash eUGR9vR*4pt6OqK0WVspWWoL0fso%eWBwM+~aySxs:S2tQW;VtWVW;","dim 4032x3024"],["p","f133b246f07633fde1a894133ac270ab8750502b64a9779c0bac3c9228198dda"],["p","5ea721dd7828229a39a372477090208db30a6c2d357951b8ae504d2ecf86c06c"],["t","cultureshock"],["r","https://image.nostr.build/185e4c10dc2e46dacf6ad38fbc4319f52860fc3f96efb433be55bdb91fc225b8.jpg"]],"content":"10 minutes.\n\nFireside chat between nostr:npub17yemy3hswcelmcdgjsfn4sns4wr4q5ptvj5h08qt4s7fy2qe3hdqsczs99 and nostr:npub1t6njrhtc9q3f5wdrwfrhpypq3kes5mpdx4u4rw9w2pxjanuxcpkqveagv3 followed by live set to close down #CultureShock.\n\nStreaming V4V at tunestr.io https://image.nostr.build/185e4c10dc2e46dacf6ad38fbc4319f52860fc3f96efb433be55bdb91fc225b8.jpg ","sig":"b0464529b2d2de2b4df911d47dbfe4aa31ac8f2db285b1a5930941cf1877425c14d6901d1be1bfaa13cd3d981d5e9a722debfc47c4f087d95da628a7035437ec"}
{"kind":1,"id":"f24aed86b493f266952ed35d4724582946cdf73985581f9986641f81fad6b73d","pubkey":"fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52","created_at":1707912005,"tags":[["p","fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52","wss://140.f7z.io/","mention"]],"content":"Releasing: Whynotstr 0.0.0 😂\n\nCollaborative editing, the left-side-of-the-curve approach\n\nTry it out: https://collab-lemon.vercel.app/\n\nObviously open source: https://github.com/pablof7z/collab\n\nnostr:nevent1qvzqqqqqqypzp75cf0tahv5z7plpdeaws7ex52nmnwgtwfr2g3m37r844evqrr6jqy88wumn8ghj7mn0wvhxcmmv9uq35amnwvaz7tms09exzmtfvshxv6tpw34xze3wvdhk6tcqyznqm6guz9k38dpqtc84s7jflec7m7wpzvtx2xjkjkm2xm89ucy2jhdcnur","sig":"6f562d733e50f5934dcf359a4f16dece1734302c0cc3a793ee2f08007ccb4ade3591373a633538617611f327feb7534ad4d11a8475163c7f734a01c63e52b79f"}
...
``` ```
## Contributing to this repository ### sign an event collaboratively with multiple parties using musig2
```shell
~> nak event --sec 1234 -k 1 -c 'hello from a combined key' --musig 2
the following code should be saved secretly until the next step an included with --musig-nonce-secret:
QebOT03ERmV7km22CqEqBPFmzAkgxQzGGbR7Si8yIZCBrd1N9A3LKwGLO71kbgXZ9EYFKpjiwun4u0mj5Tq6vwM3pK7x+EI8oHbkt9majKv/QN24Ix8qnwEIHxXX+mXBug==
the next signer and they should call this on their side:
nak event --sec <insert-secret-key> --musig 2 -k 1 -ts 1720821287 -c 'hello from a combined key' --musig-pubkey 0337a4aef1f8423ca076e4b7d99a8cabff40ddb8231f2a9f01081f15d7fa65c1ba --musig-nonce 0285af37c6c43638cda2c773098e867c749ddf1e9d096b78686c5d000603935ad3025c4a1e042eb6b0dcfd864d1e072d2ce8da06f2c0dcf13fd7d1fcef0dd26dbc92
```
demo videos with [2](https://njump.me/nevent1qqs8pmmae89agph80928l6gjm0wymechqazv80jwqrqy4cgk08epjaczyqalp33lewf5vdq847t6te0wvnags0gs0mu72kz8938tn24wlfze674zkzz), [3](https://njump.me/nevent1qqsrp320drqcnmnam6jvmdd4lgdvh2ay0xrdesrvy6q9qqdfsk7r55qzyqalp33lewf5vdq847t6te0wvnags0gs0mu72kz8938tn24wlfze6c32d4m) and [4](https://njump.me/nevent1qqsre84xe6qpagf2w2xjtjwc95j4dd5ccue68gxl8grkd6t6hjhaj5qzyqalp33lewf5vdq847t6te0wvnags0gs0mu72kz8938tn24wlfze6t8t7ak) parties.
### generate a private key
```shell
~> nak key generate 18:59
7b94e287b1fafa694ded1619b27de7effd3646104a158e187ff4edc56bc6148d
```
### encrypt key with NIP-49
```shell
~> nak key encrypt 7b94e287b1fafa694ded1619b27de7effd3646104a158e187ff4edc56bc6148d mypassword
ncryptsec1qggx54cg270zy9y8krwmfz29jyypsuxken2fkk99gr52qhje968n6mwkrfstqaqhq9eq94pnzl4nff437l4lp4ur2cs4f9um8738s35l2esx2tas48thtfhrk5kq94pf9j2tpk54yuermra0xu6hl5ls
```
### decrypt key with NIP-49
```shell
~> nak key decrypt ncryptsec1qggx54cg270zy9y8krwmfz29jyypsuxken2fkk99gr52qhje968n6mwkrfstqaqhq9eq94pnzl4nff437l4lp4ur2cs4f9um8738s35l2esx2tas48thtfhrk5kq94pf9j2tpk54yuermra0xu6hl5ls mypassword
7b94e287b1fafa694ded1619b27de7effd3646104a158e187ff4edc56bc6148d
~>
~> nak key decrypt ncryptsec1qggx54cg270zy9y8krwmfz29jyypsuxken2fkk99gr52qhje968n6mwkrfstqaqhq9eq94pnzl4nff437l4lp4ur2cs4f9um8738s35l2esx2tas48thtfhrk5kq94pf9j2tpk54yuermra0xu6hl5ls
type the password to decrypt your secret key: **********
7b94e287b1fafa694ded1619b27de7effd3646104a158e187ff4edc56bc6148d
```
### get a public key from a private key
```shell
~> nak key public 7b94e287b1fafa694ded1619b27de7effd3646104a158e187ff4edc56bc6148d
985d66d2644dfa7676e26046914470d66ebc7fa783a3f57f139fde32d0d631d7
```
### sign an event using a remote NIP-46 bunker
```shell
~> nak event --connect 'bunker://a9e0f110f636f3191644110c19a33448daf09d7cda9708a769e91b7e91340208?relay=wss%3A%2F%2Frelay.damus.io&relay=wss%3A%2F%2Frelay.nsecbunker.com&relay=wss%3A%2F%2Fnos.lol&secret=TWfGbjQCLxUf' -c 'hello from bunker'
```
### sign an event using a NIP-49 encrypted key
```shell
~> nak event --sec ncryptsec1qggx54cg270zy9y8krwmfz29jyypsuxken2fkk99gr52qhje968n6mwkrfstqaqhq9eq94pnzl4nff437l4lp4ur2cs4f9um8738s35l2esx2tas48thtfhrk5kq94pf9j2tpk54yuermra0xu6hl5ls -c 'hello from encrypted key'
type the password to decrypt your secret key: **********
{"kind":1,"id":"8aa5c931fb1da507f14801de6a1814b7f0baae984dc502b9889f347f5aa3cc4e","pubkey":"985d66d2644dfa7676e26046914470d66ebc7fa783a3f57f139fde32d0d631d7","created_at":1720822280,"tags":[],"content":"hello from encrypted key","sig":"9d1c9e56e87f787cc5b6191ec47690ce59fa4bef105b56297484253953e18fb930f6683f007e84a9ce9dc9a25b20c191c510629156dcd24bd16e15d302d20944"}
```
### talk to a relay's NIP-86 management API
```shell
nak relay allowpubkey --sec ncryptsec1qggx54cg270zy9y8krwmfz29jyypsuxken2fkk99gr52qhje968n6mwkrfstqaqhq9eq94pnzl4nff437l4lp4ur2cs4f9um8738s35l2esx2tas48thtfhrk5kq94pf9j2tpk54yuermra0xu6hl5ls --pubkey a9e0f110f636f3191644110c19a33448daf09d7cda9708a769e91b7e91340208 pyramid.fiatjaf.com
type the password to decrypt your secret key: **********
calling 'allowpubkey' on https://pyramid.fiatjaf.com...
{
"result": null,
"error": "failed to add to whitelist: pubkey 985d66d2644dfa7676e26046914470d66ebc7fa783a3f57f139fde32d0d631d7 doesn't have permission to invite"
}
```
### start a bunker locally
```shell
~> nak bunker --sec ncryptsec1qggrp80ptf0s7kyl0r38ktzg60fem85m89uz7um6rjn4pnep2nnvcgqm8h7q36c76z9sypatdh4fmw6etfxu99mv5cxkw4ymcsryw0zz7evyuplsgvnj5yysf449lq94klzvnahsw2lzxflvcq4qpf5q -k 3fbf7fbb2a2111e205f74aca0166e29e421729c9a07bc45aa85d39535b47c9ed relay.damus.io nos.lol relay.nsecbunker.com
connecting to relay.damus.io... ok.
connecting to nos.lol... ok.
connecting to relay.nsecbunker.com... ok.
type the password to decrypt your secret key: ***
listening at [wss://relay.damus.io wss://nos.lol wss://relay.nsecbunker.com]:
pubkey: f59911b561c37c90b01e9e5c2557307380835c83399756f4d62d8167227e420a
npub: npub17kv3rdtpcd7fpvq7newz24eswwqgxhyr8xt4daxk9kqkwgn7gg9q4gy8vf
authorized keys:
- 3fbf7fbb2a2111e205f74aca0166e29e421729c9a07bc45aa85d39535b47c9ed
to restart: nak bunker --sec ncryptsec1qggrp80ptf0s7kyl0r38ktzg60fem85m89uz7um6rjn4pnep2nnvcgqm8h7q36c76z9sypatdh4fmw6etfxu99mv5cxkw4ymcsryw0zz7evyuplsgvnj5yysf449lq94klzvnahsw2lzxflvcq4qpf5q -k 3fbf7fbb2a2111e205f74aca0166e29e421729c9a07bc45aa85d39535b47c9ed relay.damus.io nos.lol relay.nsecbunker.com
bunker: bunker://f59911b561c37c90b01e9e5c2557307380835c83399756f4d62d8167227e420a?relay=wss%3A%2F%2Frelay.damus.io&relay=wss%3A%2F%2Fnos.lol&relay=wss%3A%2F%2Frelay.nsecbunker.com&secret=XuuiMbcLwuwL
```
## contributing to this repository
Use NIP-34 to send your patches to `naddr1qqpkucttqy28wumn8ghj7un9d3shjtnwdaehgu3wvfnsz9nhwden5te0wfjkccte9ehx7um5wghxyctwvsq3gamnwvaz7tmjv4kxz7fwv3sk6atn9e5k7q3q80cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsxpqqqpmej2wctpn`. Use NIP-34 to send your patches to `naddr1qqpkucttqy28wumn8ghj7un9d3shjtnwdaehgu3wvfnsz9nhwden5te0wfjkccte9ehx7um5wghxyctwvsq3gamnwvaz7tmjv4kxz7fwv3sk6atn9e5k7q3q80cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsxpqqqpmej2wctpn`.

View File

@@ -14,7 +14,7 @@ import (
"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/nip19"
"github.com/nbd-wtf/go-nostr/nip46" "github.com/nbd-wtf/go-nostr/nip46"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
"golang.org/x/exp/slices" "golang.org/x/exp/slices"
) )
@@ -45,12 +45,12 @@ var bunker = &cli.Command{
Usage: "pubkeys for which we will always respond", Usage: "pubkeys for which we will always respond",
}, },
}, },
Action: func(c *cli.Context) error { Action: func(ctx context.Context, c *cli.Command) error {
// try to connect to the relays here // try to connect to the relays here
qs := url.Values{} qs := url.Values{}
relayURLs := make([]string, 0, c.Args().Len()) relayURLs := make([]string, 0, c.Args().Len())
if relayUrls := c.Args().Slice(); len(relayUrls) > 0 { if relayUrls := c.Args().Slice(); len(relayUrls) > 0 {
_, relays := connectToAllRelays(c.Context, relayUrls) _, relays := connectToAllRelays(ctx, relayUrls, false)
if len(relays) == 0 { if len(relays) == 0 {
log("failed to connect to any of the given relays.\n") log("failed to connect to any of the given relays.\n")
os.Exit(3) os.Exit(3)
@@ -65,7 +65,7 @@ var bunker = &cli.Command{
} }
// gather the secret key // gather the secret key
sec, _, err := gatherSecretKeyOrBunkerFromArguments(c) sec, _, err := gatherSecretKeyOrBunkerFromArguments(ctx, c)
if err != nil { if err != nil {
return err return err
} }
@@ -115,10 +115,19 @@ var bunker = &cli.Command{
secretKeyFlag = "--sec " + sec secretKeyFlag = "--sec " + sec
} }
relayURLsPossiblyWithoutSchema := make([]string, len(relayURLs))
for i, url := range relayURLs {
if strings.HasPrefix(url, "wss://") {
relayURLsPossiblyWithoutSchema[i] = url[6:]
} else {
relayURLsPossiblyWithoutSchema[i] = url
}
}
restartCommand := fmt.Sprintf("nak bunker %s%s %s", restartCommand := fmt.Sprintf("nak bunker %s%s %s",
secretKeyFlag, secretKeyFlag,
preauthorizedFlags, preauthorizedFlags,
strings.Join(relayURLs, " "), strings.Join(relayURLsPossiblyWithoutSchema, " "),
) )
log("listening at %v:\n pubkey: %s \n npub: %s%s%s\n to restart: %s\n bunker: %s\n\n", log("listening at %v:\n pubkey: %s \n npub: %s%s%s\n to restart: %s\n bunker: %s\n\n",
@@ -134,9 +143,9 @@ var bunker = &cli.Command{
printBunkerInfo() printBunkerInfo()
// subscribe to relays // subscribe to relays
pool := nostr.NewSimplePool(c.Context) pool := nostr.NewSimplePool(ctx)
now := nostr.Now() now := nostr.Now()
events := pool.SubMany(c.Context, relayURLs, nostr.Filters{ events := pool.SubMany(ctx, relayURLs, nostr.Filters{
{ {
Kinds: []int{nostr.KindNostrConnect}, Kinds: []int{nostr.KindNostrConnect},
Tags: nostr.TagMap{"p": []string{pubkey}}, Tags: nostr.TagMap{"p": []string{pubkey}},
@@ -151,7 +160,7 @@ var bunker = &cli.Command{
// just a gimmick // just a gimmick
var cancelPreviousBunkerInfoPrint context.CancelFunc var cancelPreviousBunkerInfoPrint context.CancelFunc
_, cancel := context.WithCancel(c.Context) _, cancel := context.WithCancel(ctx)
cancelPreviousBunkerInfoPrint = cancel cancelPreviousBunkerInfoPrint = cancel
// asking user for authorization // asking user for authorization
@@ -190,7 +199,7 @@ var bunker = &cli.Command{
for _, relayURL := range relayURLs { for _, relayURL := range relayURLs {
go func(relayURL string) { go func(relayURL string) {
if relay, _ := pool.EnsureRelay(relayURL); relay != nil { if relay, _ := pool.EnsureRelay(relayURL); relay != nil {
err := relay.Publish(c.Context, eventResponse) err := relay.Publish(ctx, eventResponse)
printLock.Lock() printLock.Lock()
if err == nil { if err == nil {
log("* sent response through %s\n", relay.URL) log("* sent response through %s\n", relay.URL)
@@ -206,7 +215,7 @@ var bunker = &cli.Command{
// just after handling one request we trigger this // just after handling one request we trigger this
go func() { go func() {
ctx, cancel := context.WithCancel(c.Context) ctx, cancel := context.WithCancel(ctx)
defer cancel() defer cancel()
cancelPreviousBunkerInfoPrint = cancel cancelPreviousBunkerInfoPrint = cancel
// the idea is that we will print the bunker URL again so it is easier to copy-paste by users // the idea is that we will print the bunker URL again so it is easier to copy-paste by users

View File

@@ -1,13 +1,14 @@
package main package main
import ( import (
"context"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"strings" "strings"
"github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
) )
var count = &cli.Command{ var count = &cli.Command{
@@ -63,7 +64,7 @@ var count = &cli.Command{
}, },
}, },
ArgsUsage: "[relay...]", ArgsUsage: "[relay...]",
Action: func(c *cli.Context) error { Action: func(ctx context.Context, c *cli.Command) error {
filter := nostr.Filter{} filter := nostr.Filter{}
if authors := c.StringSlice("author"); len(authors) > 0 { if authors := c.StringSlice("author"); len(authors) > 0 {
@@ -72,7 +73,11 @@ var count = &cli.Command{
if ids := c.StringSlice("id"); len(ids) > 0 { if ids := c.StringSlice("id"); len(ids) > 0 {
filter.IDs = ids filter.IDs = ids
} }
if kinds := c.IntSlice("kind"); len(kinds) > 0 { if kinds64 := c.IntSlice("kind"); len(kinds64) > 0 {
kinds := make([]int, len(kinds64))
for i, v := range kinds64 {
kinds[i] = int(v)
}
filter.Kinds = kinds filter.Kinds = kinds
} }
@@ -110,7 +115,7 @@ var count = &cli.Command{
filter.Until = &ts filter.Until = &ts
} }
if limit := c.Int("limit"); limit != 0 { if limit := c.Int("limit"); limit != 0 {
filter.Limit = limit filter.Limit = int(limit)
} }
relays := c.Args().Slice() relays := c.Args().Slice()
@@ -118,12 +123,12 @@ var count = &cli.Command{
failures := make([]error, 0, len(relays)) failures := make([]error, 0, len(relays))
if len(relays) > 0 { if len(relays) > 0 {
for _, relayUrl := range relays { for _, relayUrl := range relays {
relay, err := nostr.RelayConnect(c.Context, relayUrl) relay, err := nostr.RelayConnect(ctx, relayUrl)
if err != nil { if err != nil {
failures = append(failures, err) failures = append(failures, err)
continue continue
} }
count, err := relay.Count(c.Context, nostr.Filters{filter}) count, err := relay.Count(ctx, nostr.Filters{filter})
if err != nil { if err != nil {
failures = append(failures, err) failures = append(failures, err)
continue continue

View File

@@ -1,6 +1,7 @@
package main package main
import ( import (
"context"
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"strings" "strings"
@@ -8,7 +9,7 @@ import (
"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/nip19"
sdk "github.com/nbd-wtf/nostr-sdk" sdk "github.com/nbd-wtf/nostr-sdk"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
) )
var decode = &cli.Command{ var decode = &cli.Command{
@@ -32,7 +33,7 @@ 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(ctx context.Context, c *cli.Command) error {
for input := range getStdinLinesOrArguments(c.Args()) { for input := range getStdinLinesOrArguments(c.Args()) {
if strings.HasPrefix(input, "nostr:") { if strings.HasPrefix(input, "nostr:") {
input = input[6:] input = input[6:]
@@ -49,12 +50,12 @@ var decode = &cli.Command{
decodeResult.HexResult.PrivateKey = hex.EncodeToString(b) decodeResult.HexResult.PrivateKey = hex.EncodeToString(b)
decodeResult.HexResult.PublicKey = hex.EncodeToString(b) decodeResult.HexResult.PublicKey = hex.EncodeToString(b)
} else { } else {
lineProcessingError(c, "hex string with invalid number of bytes: %d", len(b)) ctx = lineProcessingError(ctx, "hex string with invalid number of bytes: %d", len(b))
continue continue
} }
} else if evp := sdk.InputToEventPointer(input); evp != nil { } else if evp := sdk.InputToEventPointer(input); evp != nil {
decodeResult = DecodeResult{EventPointer: evp} decodeResult = DecodeResult{EventPointer: evp}
} else if pp := sdk.InputToProfile(c.Context, input); pp != nil { } else if pp := sdk.InputToProfile(ctx, input); pp != nil {
decodeResult = DecodeResult{ProfilePointer: pp} decodeResult = DecodeResult{ProfilePointer: pp}
} else if prefix, value, err := nip19.Decode(input); err == nil && prefix == "naddr" { } else if prefix, value, err := nip19.Decode(input); err == nil && prefix == "naddr" {
ep := value.(nostr.EntityPointer) ep := value.(nostr.EntityPointer)
@@ -63,7 +64,7 @@ var decode = &cli.Command{
decodeResult.PrivateKey.PrivateKey = value.(string) decodeResult.PrivateKey.PrivateKey = value.(string)
decodeResult.PrivateKey.PublicKey, _ = nostr.GetPublicKey(value.(string)) decodeResult.PrivateKey.PublicKey, _ = nostr.GetPublicKey(value.(string))
} else { } else {
lineProcessingError(c, "couldn't decode input '%s': %s", input, err) ctx = lineProcessingError(ctx, "couldn't decode input '%s': %s", input, err)
continue continue
} }
@@ -71,7 +72,7 @@ var decode = &cli.Command{
} }
exitIfLineProcessingError(c) exitIfLineProcessingError(ctx)
return nil return nil
}, },
} }

View File

@@ -1,11 +1,12 @@
package main package main
import ( import (
"context"
"fmt" "fmt"
"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/nip19"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
) )
var encode = &cli.Command{ var encode = &cli.Command{
@@ -18,20 +19,20 @@ var encode = &cli.Command{
nak encode nevent <event-id> nak encode nevent <event-id>
nak encode nevent --author <pubkey-hex> --relay <relay-url> --relay <other-relay> <event-id> nak encode nevent --author <pubkey-hex> --relay <relay-url> --relay <other-relay> <event-id>
nak encode nsec <privkey-hex>`, nak encode nsec <privkey-hex>`,
Before: func(c *cli.Context) error { Before: func(ctx context.Context, c *cli.Command) error {
if c.Args().Len() < 1 { if c.Args().Len() < 1 {
return fmt.Errorf("expected more than 1 argument.") return fmt.Errorf("expected more than 1 argument.")
} }
return nil return nil
}, },
Subcommands: []*cli.Command{ Commands: []*cli.Command{
{ {
Name: "npub", Name: "npub",
Usage: "encode a hex public key into bech32 'npub' format", Usage: "encode a hex public key into bech32 'npub' format",
Action: func(c *cli.Context) error { Action: func(ctx context.Context, c *cli.Command) error {
for target := range getStdinLinesOrArguments(c.Args()) { for target := range getStdinLinesOrArguments(c.Args()) {
if ok := nostr.IsValidPublicKey(target); !ok { if ok := nostr.IsValidPublicKey(target); !ok {
lineProcessingError(c, "invalid public key: %s", target) ctx = lineProcessingError(ctx, "invalid public key: %s", target)
continue continue
} }
@@ -42,17 +43,17 @@ var encode = &cli.Command{
} }
} }
exitIfLineProcessingError(c) exitIfLineProcessingError(ctx)
return nil return nil
}, },
}, },
{ {
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(ctx context.Context, c *cli.Command) error {
for target := range getStdinLinesOrArguments(c.Args()) { for target := range getStdinLinesOrArguments(c.Args()) {
if ok := nostr.IsValid32ByteHex(target); !ok { if ok := nostr.IsValid32ByteHex(target); !ok {
lineProcessingError(c, "invalid private key: %s", target) ctx = lineProcessingError(ctx, "invalid private key: %s", target)
continue continue
} }
@@ -63,7 +64,7 @@ var encode = &cli.Command{
} }
} }
exitIfLineProcessingError(c) exitIfLineProcessingError(ctx)
return nil return nil
}, },
}, },
@@ -77,15 +78,15 @@ var encode = &cli.Command{
Usage: "attach relay hints to nprofile code", Usage: "attach relay hints to nprofile code",
}, },
}, },
Action: func(c *cli.Context) error { Action: func(ctx context.Context, c *cli.Command) error {
for target := range getStdinLinesOrArguments(c.Args()) { for target := range getStdinLinesOrArguments(c.Args()) {
if ok := nostr.IsValid32ByteHex(target); !ok { if ok := nostr.IsValid32ByteHex(target); !ok {
lineProcessingError(c, "invalid public key: %s", target) ctx = lineProcessingError(ctx, "invalid public key: %s", target)
continue continue
} }
relays := c.StringSlice("relay") relays := c.StringSlice("relay")
if err := validateRelayURLs(relays); err != nil { if err := normalizeAndValidateRelayURLs(relays); err != nil {
return err return err
} }
@@ -96,7 +97,7 @@ var encode = &cli.Command{
} }
} }
exitIfLineProcessingError(c) exitIfLineProcessingError(ctx)
return nil return nil
}, },
}, },
@@ -111,13 +112,14 @@ var encode = &cli.Command{
}, },
&cli.StringFlag{ &cli.StringFlag{
Name: "author", Name: "author",
Aliases: []string{"a"},
Usage: "attach an author pubkey as a hint to the nevent code", Usage: "attach an author pubkey as a hint to the nevent code",
}, },
}, },
Action: func(c *cli.Context) error { Action: func(ctx context.Context, c *cli.Command) error {
for target := range getStdinLinesOrArguments(c.Args()) { for target := range getStdinLinesOrArguments(c.Args()) {
if ok := nostr.IsValid32ByteHex(target); !ok { if ok := nostr.IsValid32ByteHex(target); !ok {
lineProcessingError(c, "invalid event id: %s", target) ctx = lineProcessingError(ctx, "invalid event id: %s", target)
continue continue
} }
@@ -129,7 +131,7 @@ var encode = &cli.Command{
} }
relays := c.StringSlice("relay") relays := c.StringSlice("relay")
if err := validateRelayURLs(relays); err != nil { if err := normalizeAndValidateRelayURLs(relays); err != nil {
return err return err
} }
@@ -140,7 +142,7 @@ var encode = &cli.Command{
} }
} }
exitIfLineProcessingError(c) exitIfLineProcessingError(ctx)
return nil return nil
}, },
}, },
@@ -157,10 +159,10 @@ var encode = &cli.Command{
&cli.StringFlag{ &cli.StringFlag{
Name: "pubkey", Name: "pubkey",
Usage: "pubkey of the naddr author", Usage: "pubkey of the naddr author",
Aliases: []string{"p"}, Aliases: []string{"author", "a", "p"},
Required: true, Required: true,
}, },
&cli.Int64Flag{ &cli.IntFlag{
Name: "kind", Name: "kind",
Aliases: []string{"k"}, Aliases: []string{"k"},
Usage: "kind of referred replaceable event", Usage: "kind of referred replaceable event",
@@ -172,7 +174,7 @@ var encode = &cli.Command{
Usage: "attach relay hints to naddr code", Usage: "attach relay hints to naddr code",
}, },
}, },
Action: func(c *cli.Context) error { Action: func(ctx context.Context, c *cli.Command) error {
for d := range getStdinLinesOrBlank() { for d := range getStdinLinesOrBlank() {
pubkey := c.String("pubkey") pubkey := c.String("pubkey")
if ok := nostr.IsValidPublicKey(pubkey); !ok { if ok := nostr.IsValidPublicKey(pubkey); !ok {
@@ -187,34 +189,34 @@ var encode = &cli.Command{
if d == "" { if d == "" {
d = c.String("identifier") d = c.String("identifier")
if d == "" { if d == "" {
lineProcessingError(c, "\"d\" tag identifier can't be empty") ctx = lineProcessingError(ctx, "\"d\" tag identifier can't be empty")
continue continue
} }
} }
relays := c.StringSlice("relay") relays := c.StringSlice("relay")
if err := validateRelayURLs(relays); err != nil { if err := normalizeAndValidateRelayURLs(relays); err != nil {
return err return err
} }
if npub, err := nip19.EncodeEntity(pubkey, kind, d, relays); err == nil { if npub, err := nip19.EncodeEntity(pubkey, int(kind), d, relays); err == nil {
stdout(npub) stdout(npub)
} else { } else {
return err return err
} }
} }
exitIfLineProcessingError(c) exitIfLineProcessingError(ctx)
return nil return nil
}, },
}, },
{ {
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(ctx context.Context, c *cli.Command) error {
for target := range getStdinLinesOrArguments(c.Args()) { for target := range getStdinLinesOrArguments(c.Args()) {
if ok := nostr.IsValid32ByteHex(target); !ok { if ok := nostr.IsValid32ByteHex(target); !ok {
lineProcessingError(c, "invalid event id: %s", target) ctx = lineProcessingError(ctx, "invalid event id: %s", target)
continue continue
} }
@@ -225,7 +227,7 @@ var encode = &cli.Command{
} }
} }
exitIfLineProcessingError(c) exitIfLineProcessingError(ctx)
return nil return nil
}, },
}, },

View File

@@ -13,7 +13,7 @@ import (
"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/nip19"
"github.com/nbd-wtf/go-nostr/nson" "github.com/nbd-wtf/go-nostr/nson"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
"golang.org/x/exp/slices" "golang.org/x/exp/slices"
) )
@@ -36,7 +36,7 @@ example:
Flags: []cli.Flag{ Flags: []cli.Flag{
&cli.StringFlag{ &cli.StringFlag{
Name: "sec", Name: "sec",
Usage: "secret key to sign the event, as hex or nsec", Usage: "secret key to sign the event, as nsec, ncryptsec or hex",
DefaultText: "the key '1'", DefaultText: "the key '1'",
Value: "0000000000000000000000000000000000000000000000000000000000000001", Value: "0000000000000000000000000000000000000000000000000000000000000001",
}, },
@@ -141,11 +141,11 @@ example:
}, },
}, },
ArgsUsage: "[relay...]", ArgsUsage: "[relay...]",
Action: func(c *cli.Context) error { Action: func(ctx context.Context, c *cli.Command) error {
// try to connect to the relays here // try to connect to the relays here
var relays []*nostr.Relay var relays []*nostr.Relay
if relayUrls := c.Args().Slice(); len(relayUrls) > 0 { if relayUrls := c.Args().Slice(); len(relayUrls) > 0 {
_, relays = connectToAllRelays(c.Context, relayUrls) _, relays = connectToAllRelays(ctx, relayUrls, false)
if len(relays) == 0 { if len(relays) == 0 {
log("failed to connect to any of the given relays.\n") log("failed to connect to any of the given relays.\n")
os.Exit(3) os.Exit(3)
@@ -158,7 +158,7 @@ example:
} }
}() }()
sec, bunker, err := gatherSecretKeyOrBunkerFromArguments(c) sec, bunker, err := gatherSecretKeyOrBunkerFromArguments(ctx, c)
if err != nil { if err != nil {
return err return err
} }
@@ -176,14 +176,14 @@ example:
if stdinEvent != "" { if stdinEvent != "" {
if err := easyjson.Unmarshal([]byte(stdinEvent), &evt); err != nil { if err := easyjson.Unmarshal([]byte(stdinEvent), &evt); err != nil {
lineProcessingError(c, "invalid event received from stdin: %s", err) ctx = lineProcessingError(ctx, "invalid event received from stdin: %s", err)
continue continue
} }
kindWasSupplied = strings.Contains(stdinEvent, `"kind"`) kindWasSupplied = strings.Contains(stdinEvent, `"kind"`)
} }
if kind := c.Int("kind"); slices.Contains(c.FlagNames(), "kind") { if kind := c.Int("kind"); slices.Contains(c.FlagNames(), "kind") {
evt.Kind = kind evt.Kind = int(kind)
mustRehashAndResign = true mustRehashAndResign = true
} else if !kindWasSupplied { } else if !kindWasSupplied {
evt.Kind = 1 evt.Kind = 1
@@ -248,7 +248,7 @@ example:
if evt.Sig == "" || mustRehashAndResign { if evt.Sig == "" || mustRehashAndResign {
if bunker != nil { if bunker != nil {
if err := bunker.SignEvent(c.Context, &evt); err != nil { if err := bunker.SignEvent(ctx, &evt); err != nil {
return fmt.Errorf("failed to sign with bunker: %w", err) return fmt.Errorf("failed to sign with bunker: %w", err)
} }
} else if numSigners := c.Uint("musig"); numSigners > 1 && sec != "" { } else if numSigners := c.Uint("musig"); numSigners > 1 && sec != "" {
@@ -256,7 +256,7 @@ example:
secNonce := c.String("musig-nonce-secret") secNonce := c.String("musig-nonce-secret")
pubNonces := c.StringSlice("musig-nonce") pubNonces := c.StringSlice("musig-nonce")
partialSigs := c.StringSlice("musig-partial") partialSigs := c.StringSlice("musig-partial")
signed, err := performMusig(c.Context, signed, err := performMusig(ctx,
sec, &evt, int(numSigners), pubkeys, pubNonces, secNonce, partialSigs) sec, &evt, int(numSigners), pubkeys, pubNonces, secNonce, partialSigs)
if err != nil { if err != nil {
return fmt.Errorf("musig error: %w", err) return fmt.Errorf("musig error: %w", err)
@@ -291,7 +291,7 @@ example:
for _, relay := range relays { for _, relay := range relays {
publish: publish:
log("publishing to %s... ", relay.URL) log("publishing to %s... ", relay.URL)
ctx, cancel := context.WithTimeout(c.Context, 10*time.Second) ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel() defer cancel()
err := relay.Publish(ctx, evt) err := relay.Publish(ctx, evt)
@@ -307,7 +307,7 @@ example:
// if the relay is requesting auth and we can auth, let's do it // if the relay is requesting auth and we can auth, let's do it
var pk string var pk string
if bunker != nil { if bunker != nil {
pk, err = bunker.GetPublicKey(c.Context) pk, err = bunker.GetPublicKey(ctx)
if err != nil { if err != nil {
return fmt.Errorf("failed to get public key from bunker: %w", err) return fmt.Errorf("failed to get public key from bunker: %w", err)
} }
@@ -315,9 +315,9 @@ example:
pk, _ = nostr.GetPublicKey(sec) pk, _ = nostr.GetPublicKey(sec)
} }
log("performing auth as %s... ", pk) log("performing auth as %s... ", pk)
if err := relay.Auth(c.Context, func(evt *nostr.Event) error { if err := relay.Auth(ctx, func(evt *nostr.Event) error {
if bunker != nil { if bunker != nil {
return bunker.SignEvent(c.Context, evt) return bunker.SignEvent(ctx, evt)
} }
return evt.Sign(sec) return evt.Sign(sec)
}); err == nil { }); err == nil {
@@ -337,7 +337,7 @@ example:
} }
} }
exitIfLineProcessingError(c) exitIfLineProcessingError(ctx)
return nil return nil
}, },
} }

View File

@@ -1,11 +1,16 @@
package main package main
import "os" import (
"context"
"os"
)
var ctx = context.Background()
func ExampleEventBasic() { func ExampleEventBasic() {
app.Run([]string{"nak", "event", "--ts", "1699485669"}) app.Run(ctx, []string{"nak", "event", "--ts", "1699485669"})
// Output: // Output:
// {"id":"36d88cf5fcc449f2390a424907023eda7a74278120eebab8d02797cd92e7e29c","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1699485669,"kind":1,"tags":[],"content":"hello from the nostr army knife","sig":"68e71a192e8abcf8582a222434ac823ecc50607450ebe8cc4c145eb047794cc382dc3f888ce879d2f404f5ba6085a47601360a0fa2dd4b50d317bd0c6197c2c2"} // {"kind":1,"id":"36d88cf5fcc449f2390a424907023eda7a74278120eebab8d02797cd92e7e29c","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1699485669,"tags":[],"content":"hello from the nostr army knife","sig":"68e71a192e8abcf8582a222434ac823ecc50607450ebe8cc4c145eb047794cc382dc3f888ce879d2f404f5ba6085a47601360a0fa2dd4b50d317bd0c6197c2c2"}
} }
// (for some reason there can only be one test dealing with stdin in the suite otherwise it halts) // (for some reason there can only be one test dealing with stdin in the suite otherwise it halts)
@@ -15,22 +20,22 @@ func ExampleEventParsingFromStdin() {
r, w, _ := os.Pipe() r, w, _ := os.Pipe()
os.Stdin = r os.Stdin = r
w.WriteString("{\"content\":\"hello world\"}\n{\"content\":\"hello sun\"}\n") w.WriteString("{\"content\":\"hello world\"}\n{\"content\":\"hello sun\"}\n")
app.Run([]string{"nak", "event", "-t", "t=spam", "--ts", "1699485669"}) app.Run(ctx, []string{"nak", "event", "-t", "t=spam", "--ts", "1699485669"})
// Output: // Output:
// {"id":"bda134f9077c11973afe6aa5a1cc6f5bcea01c40d318b8f91dcb8e50507cfa52","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1699485669,"kind":1,"tags":[["t","spam"]],"content":"hello world","sig":"7552454bb8e7944230142634e3e34ac7468bad9b21ed6909da572c611018dff1d14d0792e98b5806f6330edc51e09efa6d0b66a9694dc34606c70f4e580e7493"} // {"id":"bda134f9077c11973afe6aa5a1cc6f5bcea01c40d318b8f91dcb8e50507cfa52","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1699485669,"kind":1,"tags":[["t","spam"]],"content":"hello world","sig":"7552454bb8e7944230142634e3e34ac7468bad9b21ed6909da572c611018dff1d14d0792e98b5806f6330edc51e09efa6d0b66a9694dc34606c70f4e580e7493"}
// {"id":"879c36ec73acca288825b53585389581d3836e7f0fe4d46e5eba237ca56d6af5","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1699485669,"kind":1,"tags":[["t","spam"]],"content":"hello sun","sig":"6c7e6b13ebdf931d26acfdd00bec2ec1140ddaf8d1ed61453543a14e729a460fe36c40c488ccb194a0e1ab9511cb6c36741485f501bdb93c39ca4c51bc59cbd4"} // {"id":"879c36ec73acca288825b53585389581d3836e7f0fe4d46e5eba237ca56d6af5","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1699485669,"kind":1,"tags":[["t","spam"]],"content":"hello sun","sig":"6c7e6b13ebdf931d26acfdd00bec2ec1140ddaf8d1ed61453543a14e729a460fe36c40c488ccb194a0e1ab9511cb6c36741485f501bdb93c39ca4c51bc59cbd4"}
} }
func ExampleEventComplex() { 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?name=foobar;nothing"}) app.Run(ctx, []string{"nak", "event", "--ts", "1699485669", "-k", "11", "-c", "skjdbaskd", "--sec", "17", "-t", "t=spam", "-e", "36d88cf5fcc449f2390a424907023eda7a74278120eebab8d02797cd92e7e29c", "-t", "r=https://abc.def?name=foobar;nothing"})
// Output: // Output:
// {"id":"19aba166dcf354bf5ef64f4afe69ada1eb851495001ee05e07d393ee8c8ea179","pubkey":"2fa2104d6b38d11b0230010559879124e42ab8dfeff5ff29dc9cdadd4ecacc3f","created_at":1699485669,"kind":11,"tags":[["t","spam"],["r","https://abc.def?name=foobar","nothing"],["e","36d88cf5fcc449f2390a424907023eda7a74278120eebab8d02797cd92e7e29c"]],"content":"skjdbaskd","sig":"cf452def4a68341c897c3fc96fa34dc6895a5b8cc266d4c041bcdf758ec992ec5adb8b0179e98552aaaf9450526a26d7e62e413b15b1c57e0cfc8db6b29215d7"} // {"id":"19aba166dcf354bf5ef64f4afe69ada1eb851495001ee05e07d393ee8c8ea179","pubkey":"2fa2104d6b38d11b0230010559879124e42ab8dfeff5ff29dc9cdadd4ecacc3f","created_at":1699485669,"kind":11,"tags":[["t","spam"],["r","https://abc.def?name=foobar","nothing"],["e","36d88cf5fcc449f2390a424907023eda7a74278120eebab8d02797cd92e7e29c"]],"content":"skjdbaskd","sig":"cf452def4a68341c897c3fc96fa34dc6895a5b8cc266d4c041bcdf758ec992ec5adb8b0179e98552aaaf9450526a26d7e62e413b15b1c57e0cfc8db6b29215d7"}
} }
func ExampleEncode() { func ExampleEncode() {
app.Run([]string{"nak", "encode", "npub", "a6a67ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179822"}) app.Run(ctx, []string{"nak", "encode", "npub", "a6a67ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179822"})
app.Run([]string{"nak", "encode", "nprofile", "-r", "wss://example.com", "a6a67ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179822"}) app.Run(ctx, []string{"nak", "encode", "nprofile", "-r", "wss://example.com", "a6a67ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179822"})
app.Run([]string{"nak", "encode", "nprofile", "-r", "wss://example.com", "a6a67ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179822", "a5592173975ded9f836a9572ea8b11a7e16ceb66464d66d50b27163f7f039d2c"}) app.Run(ctx, []string{"nak", "encode", "nprofile", "-r", "wss://example.com", "a6a67ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179822", "a5592173975ded9f836a9572ea8b11a7e16ceb66464d66d50b27163f7f039d2c"})
// npub156n8a7wuhwk9tgrzjh8gwzc8q2dlekedec5djk0js9d3d7qhnq3qjpdq28 // npub156n8a7wuhwk9tgrzjh8gwzc8q2dlekedec5djk0js9d3d7qhnq3qjpdq28
// nprofile1qqs2dfn7l8wthtz45p3ftn58pvrs9xlumvkuu2xet8egzkcklqtesgspz9mhxue69uhk27rpd4cxcefwvdhk6fl5jug // nprofile1qqs2dfn7l8wthtz45p3ftn58pvrs9xlumvkuu2xet8egzkcklqtesgspz9mhxue69uhk27rpd4cxcefwvdhk6fl5jug
// nprofile1qqs2dfn7l8wthtz45p3ftn58pvrs9xlumvkuu2xet8egzkcklqtesgspz9mhxue69uhk27rpd4cxcefwvdhk6fl5jug // nprofile1qqs2dfn7l8wthtz45p3ftn58pvrs9xlumvkuu2xet8egzkcklqtesgspz9mhxue69uhk27rpd4cxcefwvdhk6fl5jug
@@ -38,7 +43,7 @@ func ExampleEncode() {
} }
func ExampleDecode() { func ExampleDecode() {
app.Run([]string{"nak", "decode", "naddr1qqyrgcmyxe3kvefhqyxhwumn8ghj7mn0wvhxcmmvqgs9kqvr4dkruv3t7n2pc6e6a7v9v2s5fprmwjv4gde8c4fe5y29v0srqsqqql9ngrt6tu", "nevent1qyd8wumn8ghj7urewfsk66ty9enxjct5dfskvtnrdakj7qgmwaehxw309aex2mrp0yh8wetnw3jhymnzw33jucm0d5hszxthwden5te0wfjkccte9eekummjwsh8xmmrd9skctcpzamhxue69uhkzarvv9ejumn0wd68ytnvv9hxgtcqyqllp5v5j0nxr74fptqxkhvfv0h3uj870qpk3ln8a58agyxl3fka296ewr8"}) app.Run(ctx, []string{"nak", "decode", "naddr1qqyrgcmyxe3kvefhqyxhwumn8ghj7mn0wvhxcmmvqgs9kqvr4dkruv3t7n2pc6e6a7v9v2s5fprmwjv4gde8c4fe5y29v0srqsqqql9ngrt6tu", "nevent1qyd8wumn8ghj7urewfsk66ty9enxjct5dfskvtnrdakj7qgmwaehxw309aex2mrp0yh8wetnw3jhymnzw33jucm0d5hszxthwden5te0wfjkccte9eekummjwsh8xmmrd9skctcpzamhxue69uhkzarvv9ejumn0wd68ytnvv9hxgtcqyqllp5v5j0nxr74fptqxkhvfv0h3uj870qpk3ln8a58agyxl3fka296ewr8"})
// Output: // Output:
// { // {
// "pubkey": "5b0183ab6c3e322bf4d41c6b3aef98562a144847b7499543727c5539a114563e", // "pubkey": "5b0183ab6c3e322bf4d41c6b3aef98562a144847b7499543727c5539a114563e",
@@ -60,39 +65,39 @@ func ExampleDecode() {
} }
func ExampleReq() { func ExampleReq() {
app.Run([]string{"nak", "req", "-k", "1", "-l", "18", "-a", "2fa2104d6b38d11b0230010559879124e42ab8dfeff5ff29dc9cdadd4ecacc3f", "-e", "aec4de6d051a7c2b6ca2d087903d42051a31e07fb742f1240970084822de10a6"}) app.Run(ctx, []string{"nak", "req", "-k", "1", "-l", "18", "-a", "2fa2104d6b38d11b0230010559879124e42ab8dfeff5ff29dc9cdadd4ecacc3f", "-e", "aec4de6d051a7c2b6ca2d087903d42051a31e07fb742f1240970084822de10a6"})
// Output: // Output:
// ["REQ","nak",{"kinds":[1],"authors":["2fa2104d6b38d11b0230010559879124e42ab8dfeff5ff29dc9cdadd4ecacc3f"],"limit":18,"#e":["aec4de6d051a7c2b6ca2d087903d42051a31e07fb742f1240970084822de10a6"]}] // ["REQ","nak",{"kinds":[1],"authors":["2fa2104d6b38d11b0230010559879124e42ab8dfeff5ff29dc9cdadd4ecacc3f"],"limit":18,"#e":["aec4de6d051a7c2b6ca2d087903d42051a31e07fb742f1240970084822de10a6"]}]
} }
func ExampleReqIdFromRelay() { func ExampleReqIdFromRelay() {
app.Run([]string{"nak", "req", "-i", "3ff0d19493e661faa90ac06b5d8963ef1e48fe780368fe67ed0fd410df8a6dd5", "wss://nostr.wine"}) app.Run(ctx, []string{"nak", "req", "-i", "3ff0d19493e661faa90ac06b5d8963ef1e48fe780368fe67ed0fd410df8a6dd5", "wss://nostr.wine"})
// Output: // Output:
// {"id":"3ff0d19493e661faa90ac06b5d8963ef1e48fe780368fe67ed0fd410df8a6dd5","pubkey":"3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d","created_at":1710759386,"kind":1,"tags":[],"content":"Nostr was coopted by our the corporate overlords. It is now featured in https://www.iana.org/assignments/well-known-uris/well-known-uris.xhtml.","sig":"faaec167cca4de50b562b7702e8854e2023f0ccd5f36d1b95b6eac20d352206342d6987e9516d283068c768e94dbe8858e2990c3e05405e707fb6fb771ef92f9"} // {"id":"3ff0d19493e661faa90ac06b5d8963ef1e48fe780368fe67ed0fd410df8a6dd5","pubkey":"3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d","created_at":1710759386,"kind":1,"tags":[],"content":"Nostr was coopted by our the corporate overlords. It is now featured in https://www.iana.org/assignments/well-known-uris/well-known-uris.xhtml.","sig":"faaec167cca4de50b562b7702e8854e2023f0ccd5f36d1b95b6eac20d352206342d6987e9516d283068c768e94dbe8858e2990c3e05405e707fb6fb771ef92f9"}
} }
func ExampleMultipleFetch() { func ExampleMultipleFetch() {
app.Run([]string{"nak", "fetch", "naddr1qqyrgcmyxe3kvefhqyxhwumn8ghj7mn0wvhxcmmvqgs9kqvr4dkruv3t7n2pc6e6a7v9v2s5fprmwjv4gde8c4fe5y29v0srqsqqql9ngrt6tu", "nevent1qyd8wumn8ghj7urewfsk66ty9enxjct5dfskvtnrdakj7qgmwaehxw309aex2mrp0yh8wetnw3jhymnzw33jucm0d5hszxthwden5te0wfjkccte9eekummjwsh8xmmrd9skctcpzamhxue69uhkzarvv9ejumn0wd68ytnvv9hxgtcqyqllp5v5j0nxr74fptqxkhvfv0h3uj870qpk3ln8a58agyxl3fka296ewr8"}) app.Run(ctx, []string{"nak", "fetch", "naddr1qqyrgcmyxe3kvefhqyxhwumn8ghj7mn0wvhxcmmvqgs9kqvr4dkruv3t7n2pc6e6a7v9v2s5fprmwjv4gde8c4fe5y29v0srqsqqql9ngrt6tu", "nevent1qyd8wumn8ghj7urewfsk66ty9enxjct5dfskvtnrdakj7qgmwaehxw309aex2mrp0yh8wetnw3jhymnzw33jucm0d5hszxthwden5te0wfjkccte9eekummjwsh8xmmrd9skctcpzamhxue69uhkzarvv9ejumn0wd68ytnvv9hxgtcqyqllp5v5j0nxr74fptqxkhvfv0h3uj870qpk3ln8a58agyxl3fka296ewr8"})
// Output: // Output:
// {"id":"9ae5014573fc75ced00b343868d2cd9343ebcbbae50591c6fa8ae1cd99568f05","pubkey":"5b0183ab6c3e322bf4d41c6b3aef98562a144847b7499543727c5539a114563e","created_at":1707764605,"kind":31923,"tags":[["d","4cd6cfe7"],["name","Nostr PHX Presents Culture Shock"],["description","Nostr PHX presents Culture Shock the first Value 4 Value Cultural Event in Downtown Phoenix. We will showcase the power of Nostr + Bitcoin / Lightning with a full day of education, food, drinks, conversation, vendors and best of all, a live convert which will stream globally for the world to zap. "],["start","1708185600"],["end","1708228800"],["start_tzid","America/Phoenix"],["p","5b0183ab6c3e322bf4d41c6b3aef98562a144847b7499543727c5539a114563e","","host"],["location","Hello Merch, 850 W Lincoln St, Phoenix, AZ 85007, USA","Hello Merch","850 W Lincoln St, Phoenix, AZ 85007, USA"],["address","Hello Merch, 850 W Lincoln St, Phoenix, AZ 85007, USA","Hello Merch","850 W Lincoln St, Phoenix, AZ 85007, USA"],["g","9tbq1rzn"],["image","https://flockstr.s3.amazonaws.com/event/15vSaiscDhVH1KBXhA0i8"],["about","Nostr PHX presents Culture Shock : the first Value 4 Value Cultural Event in Downtown Phoenix. We will showcase the power of Nostr + Bitcoin / Lightning with a full day of education, conversation, food and goods which will be capped off with a live concert streamed globally for the world to boost \u0026 zap. \n\nWe strive to source local vendors, local artists, local partnerships. Please reach out to us if you are interested in participating in this historic event. "],["calendar","31924:5b0183ab6c3e322bf4d41c6b3aef98562a144847b7499543727c5539a114563e:1f238c94"]],"content":"Nostr PHX presents Culture Shock : the first Value 4 Value Cultural Event in Downtown Phoenix. We will showcase the power of Nostr + Bitcoin / Lightning with a full day of education, conversation, food and goods which will be capped off with a live concert streamed globally for the world to boost \u0026 zap. \n\nWe strive to source local vendors, local artists, local partnerships. Please reach out to us if you are interested in participating in this historic event. ","sig":"f676629d1414d96b464644de6babde0c96bd21ef9b41ba69ad886a1d13a942b855b715b22ccf38bc07fead18d3bdeee82d9e3825cf6f003fb5ff1766d95c70a0"} // {"id":"9ae5014573fc75ced00b343868d2cd9343ebcbbae50591c6fa8ae1cd99568f05","pubkey":"5b0183ab6c3e322bf4d41c6b3aef98562a144847b7499543727c5539a114563e","created_at":1707764605,"kind":31923,"tags":[["d","4cd6cfe7"],["name","Nostr PHX Presents Culture Shock"],["description","Nostr PHX presents Culture Shock the first Value 4 Value Cultural Event in Downtown Phoenix. We will showcase the power of Nostr + Bitcoin / Lightning with a full day of education, food, drinks, conversation, vendors and best of all, a live convert which will stream globally for the world to zap. "],["start","1708185600"],["end","1708228800"],["start_tzid","America/Phoenix"],["p","5b0183ab6c3e322bf4d41c6b3aef98562a144847b7499543727c5539a114563e","","host"],["location","Hello Merch, 850 W Lincoln St, Phoenix, AZ 85007, USA","Hello Merch","850 W Lincoln St, Phoenix, AZ 85007, USA"],["address","Hello Merch, 850 W Lincoln St, Phoenix, AZ 85007, USA","Hello Merch","850 W Lincoln St, Phoenix, AZ 85007, USA"],["g","9tbq1rzn"],["image","https://flockstr.s3.amazonaws.com/event/15vSaiscDhVH1KBXhA0i8"],["about","Nostr PHX presents Culture Shock : the first Value 4 Value Cultural Event in Downtown Phoenix. We will showcase the power of Nostr + Bitcoin / Lightning with a full day of education, conversation, food and goods which will be capped off with a live concert streamed globally for the world to boost \u0026 zap. \n\nWe strive to source local vendors, local artists, local partnerships. Please reach out to us if you are interested in participating in this historic event. "],["calendar","31924:5b0183ab6c3e322bf4d41c6b3aef98562a144847b7499543727c5539a114563e:1f238c94"]],"content":"Nostr PHX presents Culture Shock : the first Value 4 Value Cultural Event in Downtown Phoenix. We will showcase the power of Nostr + Bitcoin / Lightning with a full day of education, conversation, food and goods which will be capped off with a live concert streamed globally for the world to boost \u0026 zap. \n\nWe strive to source local vendors, local artists, local partnerships. Please reach out to us if you are interested in participating in this historic event. ","sig":"f676629d1414d96b464644de6babde0c96bd21ef9b41ba69ad886a1d13a942b855b715b22ccf38bc07fead18d3bdeee82d9e3825cf6f003fb5ff1766d95c70a0"}
// {"id":"3ff0d19493e661faa90ac06b5d8963ef1e48fe780368fe67ed0fd410df8a6dd5","pubkey":"3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d","created_at":1710759386,"kind":1,"tags":[],"content":"Nostr was coopted by our the corporate overlords. It is now featured in https://www.iana.org/assignments/well-known-uris/well-known-uris.xhtml.","sig":"faaec167cca4de50b562b7702e8854e2023f0ccd5f36d1b95b6eac20d352206342d6987e9516d283068c768e94dbe8858e2990c3e05405e707fb6fb771ef92f9"} // {"id":"3ff0d19493e661faa90ac06b5d8963ef1e48fe780368fe67ed0fd410df8a6dd5","pubkey":"3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d","created_at":1710759386,"kind":1,"tags":[],"content":"Nostr was coopted by our the corporate overlords. It is now featured in https://www.iana.org/assignments/well-known-uris/well-known-uris.xhtml.","sig":"faaec167cca4de50b562b7702e8854e2023f0ccd5f36d1b95b6eac20d352206342d6987e9516d283068c768e94dbe8858e2990c3e05405e707fb6fb771ef92f9"}
} }
func ExampleKeyPublic() { func ExampleKeyPublic() {
app.Run([]string{"nak", "key", "public", "3ff0d19493e661faa90ac06b5d8963ef1e48fe780368fe67ed0fd410df8a6dd5", "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"}) app.Run(ctx, []string{"nak", "key", "public", "3ff0d19493e661faa90ac06b5d8963ef1e48fe780368fe67ed0fd410df8a6dd5", "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"})
// Output: // Output:
// 70f7120d065870513a6bddb61c8d400ad1e43449b1900ffdb5551e4c421375c8 // 70f7120d065870513a6bddb61c8d400ad1e43449b1900ffdb5551e4c421375c8
// 718d756f60cf5179ef35b39dc6db3ff58f04c0734f81f6d4410f0b047ddf9029 // 718d756f60cf5179ef35b39dc6db3ff58f04c0734f81f6d4410f0b047ddf9029
} }
func ExampleKeyDecrypt() { func ExampleKeyDecrypt() {
app.Run([]string{"nak", "key", "decrypt", "ncryptsec1qggfep0m5ythsegkmwfrhhx2zx5gazyhdygvlngcds4wsgdpzfy6nr0exy0pdk0ydwrqyhndt2trtwcgwwag0ja3aqclzptfxxqvprdyaz3qfrmazpecx2ff6dph5mfdjnh5sw8sgecul32eru6xet34", "banana"}) app.Run(ctx, []string{"nak", "key", "decrypt", "ncryptsec1qggfep0m5ythsegkmwfrhhx2zx5gazyhdygvlngcds4wsgdpzfy6nr0exy0pdk0ydwrqyhndt2trtwcgwwag0ja3aqclzptfxxqvprdyaz3qfrmazpecx2ff6dph5mfdjnh5sw8sgecul32eru6xet34", "banana"})
// Output: // Output:
// nsec180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsgyumg0 // nsec180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsgyumg0
} }
func ExampleRelay() { func ExampleRelay() {
app.Run([]string{"nak", "relay", "relay.nos.social", "pyramid.fiatjaf.com"}) app.Run(ctx, []string{"nak", "relay", "relay.nos.social", "pyramid.fiatjaf.com"})
// Output: // Output:
// { // {
// "name": "nos.social strfry relay", // "name": "nos.social strfry relay",

View File

@@ -1,15 +1,17 @@
package main package main
import ( import (
"context"
"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/nip19"
sdk "github.com/nbd-wtf/nostr-sdk" sdk "github.com/nbd-wtf/nostr-sdk"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
) )
var fetch = &cli.Command{ var fetch = &cli.Command{
Name: "fetch", Name: "fetch",
Usage: "fetches events related to the given nip19 code from the included relay hints", Usage: "fetches events related to the given nip19 code from the included relay hints or the author's NIP-65 relays.",
Description: `example usage: Description: `example usage:
nak fetch nevent1qqsxrwm0hd3s3fddh4jc2574z3xzufq6qwuyz2rvv3n087zvym3dpaqprpmhxue69uhhqatzd35kxtnjv4kxz7tfdenju6t0xpnej4 nak fetch nevent1qqsxrwm0hd3s3fddh4jc2574z3xzufq6qwuyz2rvv3n087zvym3dpaqprpmhxue69uhhqatzd35kxtnjv4kxz7tfdenju6t0xpnej4
echo npub1h8spmtw9m2huyv6v2j2qd5zv956z2zdugl6mgx02f2upffwpm3nqv0j4ps | nak fetch --relay wss://relay.nostr.band`, echo npub1h8spmtw9m2huyv6v2j2qd5zv956z2zdugl6mgx02f2upffwpm3nqv0j4ps | nak fetch --relay wss://relay.nostr.band`,
@@ -21,8 +23,8 @@ var fetch = &cli.Command{
}, },
}, },
ArgsUsage: "[nip19code]", ArgsUsage: "[nip19code]",
Action: func(c *cli.Context) error { Action: func(ctx context.Context, c *cli.Command) error {
pool := nostr.NewSimplePool(c.Context) pool := nostr.NewSimplePool(ctx)
defer func() { defer func() {
pool.Relays.Range(func(_ string, relay *nostr.Relay) bool { pool.Relays.Range(func(_ string, relay *nostr.Relay) bool {
@@ -36,12 +38,12 @@ var fetch = &cli.Command{
prefix, value, err := nip19.Decode(code) prefix, value, err := nip19.Decode(code)
if err != nil { if err != nil {
lineProcessingError(c, "failed to decode: %s", err) ctx = lineProcessingError(ctx, "failed to decode: %s", err)
continue continue
} }
relays := c.StringSlice("relay") relays := c.StringSlice("relay")
if err := validateRelayURLs(relays); err != nil { if err := normalizeAndValidateRelayURLs(relays); err != nil {
return err return err
} }
var authorHint string var authorHint string
@@ -75,7 +77,7 @@ var fetch = &cli.Command{
} }
if authorHint != "" { if authorHint != "" {
relayList := sdk.FetchRelaysForPubkey(c.Context, pool, authorHint, relayList := sdk.FetchRelaysForPubkey(ctx, pool, authorHint,
"wss://purplepag.es", "wss://relay.damus.io", "wss://relay.noswhere.com", "wss://purplepag.es", "wss://relay.damus.io", "wss://relay.noswhere.com",
"wss://nos.lol", "wss://public.relaying.io", "wss://relay.nostr.band") "wss://nos.lol", "wss://public.relaying.io", "wss://relay.nostr.band")
for _, relayListItem := range relayList { for _, relayListItem := range relayList {
@@ -86,16 +88,16 @@ var fetch = &cli.Command{
} }
if len(relays) == 0 { if len(relays) == 0 {
lineProcessingError(c, "no relay hints found") ctx = lineProcessingError(ctx, "no relay hints found")
continue continue
} }
for ie := range pool.SubManyEose(c.Context, relays, nostr.Filters{filter}) { for ie := range pool.SubManyEose(ctx, relays, nostr.Filters{filter}) {
stdout(ie.Event) stdout(ie.Event)
} }
} }
exitIfLineProcessingError(c) exitIfLineProcessingError(ctx)
return nil return nil
}, },
} }

11
go.mod
View File

@@ -7,11 +7,12 @@ toolchain go1.21.0
require ( require (
github.com/btcsuite/btcd/btcec/v2 v2.3.3 github.com/btcsuite/btcd/btcec/v2 v2.3.3
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0
github.com/fatih/color v1.16.0 github.com/fatih/color v1.16.0
github.com/mailru/easyjson v0.7.7 github.com/mailru/easyjson v0.7.7
github.com/nbd-wtf/go-nostr v0.31.2 github.com/nbd-wtf/go-nostr v0.34.2
github.com/nbd-wtf/nostr-sdk v0.0.5 github.com/nbd-wtf/nostr-sdk v0.0.5
github.com/urfave/cli/v2 v2.25.7 github.com/urfave/cli/v3 v3.0.0-alpha9
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842
) )
@@ -20,9 +21,7 @@ require (
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect
github.com/chzyer/logex v1.1.10 // indirect github.com/chzyer/logex v1.1.10 // indirect
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect
github.com/fiatjaf/eventstore v0.2.16 // indirect github.com/fiatjaf/eventstore v0.2.16 // indirect
github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect github.com/gobwas/pool v0.2.1 // indirect
@@ -31,12 +30,12 @@ require (
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/puzpuzpuz/xsync/v3 v3.1.0 // indirect github.com/puzpuzpuz/xsync/v3 v3.1.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/tidwall/gjson v1.17.1 // indirect github.com/tidwall/gjson v1.17.1 // indirect
github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
golang.org/x/crypto v0.7.0 // indirect golang.org/x/crypto v0.7.0 // indirect
golang.org/x/sys v0.20.0 // indirect golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.8.0 // indirect golang.org/x/text v0.8.0 // indirect
) )
replace github.com/urfave/cli/v3 => github.com/fiatjaf/cli/v3 v3.0.0-20240712212113-3a8b0280e2c5

14
go.sum
View File

@@ -29,8 +29,6 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5O
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
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= github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -44,6 +42,8 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3
github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/fiatjaf/cli/v3 v3.0.0-20240712212113-3a8b0280e2c5 h1:yhTRU02Hn1jwq50uUKRxbPZQg0PODe37s73IJNsCJb0=
github.com/fiatjaf/cli/v3 v3.0.0-20240712212113-3a8b0280e2c5/go.mod h1:Z1ItyMma7t6I7zHG9OpbExhHQOSkFf/96n+mAZ9MtVI=
github.com/fiatjaf/eventstore v0.2.16 h1:NR64mnyUT5nJR8Sj2AwJTd1Hqs5kKJcCFO21ggUkvWg= github.com/fiatjaf/eventstore v0.2.16 h1:NR64mnyUT5nJR8Sj2AwJTd1Hqs5kKJcCFO21ggUkvWg=
github.com/fiatjaf/eventstore v0.2.16/go.mod h1:rUc1KhVufVmC+HUOiuPweGAcvG6lEOQCkRCn2Xn5VRA= github.com/fiatjaf/eventstore v0.2.16/go.mod h1:rUc1KhVufVmC+HUOiuPweGAcvG6lEOQCkRCn2Xn5VRA=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
@@ -79,8 +79,8 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/nbd-wtf/go-nostr v0.31.2 h1:PkHCAsSzG0Ce8tfF7LKyvZOjYtCdC+hPh5KfO/Rl1b4= github.com/nbd-wtf/go-nostr v0.34.2 h1:9b4qZ29DhQf9xEWN8/7zfDD868r1jFbpjrR3c+BHc+E=
github.com/nbd-wtf/go-nostr v0.31.2/go.mod h1:vHKtHyLXDXzYBN0fi/9Y/Q5AD0p+hk8TQVKlldAi0gI= github.com/nbd-wtf/go-nostr v0.34.2/go.mod h1:NZQkxl96ggbO8rvDpVjcsojJqKTPwqhP4i82O7K5DJs=
github.com/nbd-wtf/nostr-sdk v0.0.5 h1:rec+FcDizDVO0W25PX0lgYMXvP7zNNOgI3Fu9UCm4BY= github.com/nbd-wtf/nostr-sdk v0.0.5 h1:rec+FcDizDVO0W25PX0lgYMXvP7zNNOgI3Fu9UCm4BY=
github.com/nbd-wtf/nostr-sdk v0.0.5/go.mod h1:iJJsikesCGLNFZ9dLqhLPDzdt924EagUmdQxT3w2Lmk= github.com/nbd-wtf/nostr-sdk v0.0.5/go.mod h1:iJJsikesCGLNFZ9dLqhLPDzdt924EagUmdQxT3w2Lmk=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
@@ -96,8 +96,6 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/puzpuzpuz/xsync/v3 v3.1.0 h1:EewKT7/LNac5SLiEblJeUu8z5eERHrmRLnMQL2d7qX4= github.com/puzpuzpuz/xsync/v3 v3.1.0 h1:EewKT7/LNac5SLiEblJeUu8z5eERHrmRLnMQL2d7qX4=
github.com/puzpuzpuz/xsync/v3 v3.1.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= github.com/puzpuzpuz/xsync/v3 v3.1.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
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/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
@@ -110,10 +108,6 @@ github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JT
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs=
github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=

View File

@@ -9,6 +9,7 @@ import (
"net/url" "net/url"
"os" "os"
"strings" "strings"
"time"
"github.com/chzyer/readline" "github.com/chzyer/readline"
"github.com/fatih/color" "github.com/fatih/color"
@@ -16,7 +17,7 @@ import (
"github.com/nbd-wtf/go-nostr/nip19" "github.com/nbd-wtf/go-nostr/nip19"
"github.com/nbd-wtf/go-nostr/nip46" "github.com/nbd-wtf/go-nostr/nip46"
"github.com/nbd-wtf/go-nostr/nip49" "github.com/nbd-wtf/go-nostr/nip49"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
) )
const ( const (
@@ -92,8 +93,11 @@ func writeStdinLinesOrNothing(ch chan string) (hasStdinLines bool) {
} }
} }
func validateRelayURLs(wsurls []string) error { func normalizeAndValidateRelayURLs(wsurls []string) error {
for _, wsurl := range wsurls { for i, wsurl := range wsurls {
wsurl = nostr.NormalizeURL(wsurl)
wsurls[i] = wsurl
u, err := url.Parse(wsurl) u, err := url.Parse(wsurl)
if err != nil { if err != nil {
return fmt.Errorf("invalid relay url '%s': %s", wsurl, err) return fmt.Errorf("invalid relay url '%s': %s", wsurl, err)
@@ -114,13 +118,48 @@ func validateRelayURLs(wsurls []string) error {
func connectToAllRelays( func connectToAllRelays(
ctx context.Context, ctx context.Context,
relayUrls []string, relayUrls []string,
forcePreAuth bool,
opts ...nostr.PoolOption, opts ...nostr.PoolOption,
) (*nostr.SimplePool, []*nostr.Relay) { ) (*nostr.SimplePool, []*nostr.Relay) {
relays := make([]*nostr.Relay, 0, len(relayUrls)) relays := make([]*nostr.Relay, 0, len(relayUrls))
pool := nostr.NewSimplePool(ctx, opts...) pool := nostr.NewSimplePool(ctx, opts...)
relayLoop:
for _, url := range relayUrls { for _, url := range relayUrls {
log("connecting to %s... ", url) log("connecting to %s... ", url)
if relay, err := pool.EnsureRelay(url); err == nil { if relay, err := pool.EnsureRelay(url); err == nil {
if forcePreAuth {
log("waiting for auth challenge... ")
signer := opts[0].(nostr.WithAuthHandler)
time.Sleep(time.Millisecond * 200)
challengeWaitLoop:
for {
// beginhack
// here starts the biggest and ugliest hack of this codebase
if err := relay.Auth(ctx, func(authEvent *nostr.Event) error {
challengeTag := authEvent.Tags.GetFirst([]string{"challenge", ""})
if (*challengeTag)[1] == "" {
return fmt.Errorf("auth not received yet *****")
}
return signer(authEvent)
}); err == nil {
// auth succeeded
break challengeWaitLoop
} else {
// auth failed
if strings.HasSuffix(err.Error(), "auth not received yet *****") {
// it failed because we didn't receive the challenge yet, so keep waiting
time.Sleep(time.Second)
continue challengeWaitLoop
} else {
// it failed for some other reason, so skip this relay
log(err.Error() + "\n")
continue relayLoop
}
}
// endhack
}
}
relays = append(relays, relay) relays = append(relays, relay)
log("ok.\n") log("ok.\n")
} else { } else {
@@ -130,18 +169,18 @@ func connectToAllRelays(
return pool, relays return pool, relays
} }
func lineProcessingError(c *cli.Context, msg string, args ...any) { func lineProcessingError(ctx context.Context, msg string, args ...any) context.Context {
c.Context = context.WithValue(c.Context, LINE_PROCESSING_ERROR, true)
log(msg+"\n", args...) log(msg+"\n", args...)
return context.WithValue(ctx, LINE_PROCESSING_ERROR, true)
} }
func exitIfLineProcessingError(c *cli.Context) { func exitIfLineProcessingError(ctx context.Context) {
if val := c.Context.Value(LINE_PROCESSING_ERROR); val != nil && val.(bool) { if val := ctx.Value(LINE_PROCESSING_ERROR); val != nil && val.(bool) {
os.Exit(123) os.Exit(123)
} }
} }
func gatherSecretKeyOrBunkerFromArguments(c *cli.Context) (string, *nip46.BunkerClient, error) { func gatherSecretKeyOrBunkerFromArguments(ctx context.Context, c *cli.Command) (string, *nip46.BunkerClient, error) {
var err error var err error
if bunkerURL := c.String("connect"); bunkerURL != "" { if bunkerURL := c.String("connect"); bunkerURL != "" {
@@ -151,7 +190,7 @@ func gatherSecretKeyOrBunkerFromArguments(c *cli.Context) (string, *nip46.Bunker
} else { } else {
clientKey = nostr.GeneratePrivateKey() clientKey = nostr.GeneratePrivateKey()
} }
bunker, err := nip46.ConnectBunker(c.Context, clientKey, bunkerURL, nil, func(s string) { bunker, err := nip46.ConnectBunker(ctx, clientKey, bunkerURL, nil, func(s string) {
fmt.Fprintf(color.Error, color.CyanString("[nip46]: open the following URL: %s"), s) fmt.Fprintf(color.Error, color.CyanString("[nip46]: open the following URL: %s"), s)
}) })
return "", bunker, err return "", bunker, err
@@ -171,7 +210,7 @@ func gatherSecretKeyOrBunkerFromArguments(c *cli.Context) (string, *nip46.Bunker
if err != nil { if err != nil {
return "", nil, fmt.Errorf("failed to decrypt: %w", err) return "", nil, fmt.Errorf("failed to decrypt: %w", err)
} }
} else if bsec, err := hex.DecodeString(strings.Repeat("0", 64-len(sec)) + sec); err == nil { } else if bsec, err := hex.DecodeString(leftPadKey(sec)); err == nil {
sec = hex.EncodeToString(bsec) sec = hex.EncodeToString(bsec)
} else if prefix, hexvalue, err := nip19.Decode(sec); err != nil { } else if prefix, hexvalue, err := nip19.Decode(sec); err != nil {
return "", nil, fmt.Errorf("invalid nsec: %w", err) return "", nil, fmt.Errorf("invalid nsec: %w", err)
@@ -185,7 +224,7 @@ func gatherSecretKeyOrBunkerFromArguments(c *cli.Context) (string, *nip46.Bunker
return sec, nil, nil return sec, nil, nil
} }
func promptDecrypt(ncryptsec1 string) (string, error) { func promptDecrypt(ncryptsec string) (string, error) {
for i := 1; i < 4; i++ { for i := 1; i < 4; i++ {
var attemptStr string var attemptStr string
if i > 1 { if i > 1 {
@@ -195,7 +234,7 @@ func promptDecrypt(ncryptsec1 string) (string, error) {
if err != nil { if err != nil {
return "", err return "", err
} }
sec, err := nip49.Decrypt(ncryptsec1, password) sec, err := nip49.Decrypt(ncryptsec, password)
if err != nil { if err != nil {
continue continue
} }
@@ -245,3 +284,7 @@ func randString(n int) string {
} }
return string(b) return string(b)
} }
func leftPadKey(k string) string {
return strings.Repeat("0", 64-len(k)) + k
}

172
key.go
View File

@@ -1,23 +1,27 @@
package main package main
import ( import (
"context"
"encoding/hex" "encoding/hex"
"encoding/json"
"fmt" "fmt"
"os"
"strings" "strings"
"github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"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/nip19"
"github.com/nbd-wtf/go-nostr/nip49" "github.com/nbd-wtf/go-nostr/nip49"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
) )
var key = &cli.Command{ var key = &cli.Command{
Name: "key", Name: "key",
Usage: "operations on secret keys: generate, derive, encrypt, decrypt.", Usage: "operations on secret keys: generate, derive, encrypt, decrypt.",
Description: ``, Description: ``,
Subcommands: []*cli.Command{ Commands: []*cli.Command{
generate, generate,
public, public,
encrypt, encrypt,
@@ -30,7 +34,7 @@ var generate = &cli.Command{
Name: "generate", Name: "generate",
Usage: "generates a secret key", Usage: "generates a secret key",
Description: ``, Description: ``,
Action: func(c *cli.Context) error { Action: func(ctx context.Context, c *cli.Command) error {
sec := nostr.GeneratePrivateKey() sec := nostr.GeneratePrivateKey()
stdout(sec) stdout(sec)
return nil return nil
@@ -42,11 +46,11 @@ var public = &cli.Command{
Usage: "computes a public key from a secret key", Usage: "computes a public key from a secret key",
Description: ``, Description: ``,
ArgsUsage: "[secret]", ArgsUsage: "[secret]",
Action: func(c *cli.Context) error { Action: func(ctx context.Context, c *cli.Command) error {
for sec := range getSecretKeysFromStdinLinesOrSlice(c, c.Args().Slice()) { for sec := range getSecretKeysFromStdinLinesOrSlice(ctx, c, c.Args().Slice()) {
pubkey, err := nostr.GetPublicKey(sec) pubkey, err := nostr.GetPublicKey(sec)
if err != nil { if err != nil {
lineProcessingError(c, "failed to derive public key: %s", err) ctx = lineProcessingError(ctx, "failed to derive public key: %s", err)
continue continue
} }
stdout(pubkey) stdout(pubkey)
@@ -68,24 +72,23 @@ var encrypt = &cli.Command{
DefaultText: "16", DefaultText: "16",
}, },
}, },
Action: func(c *cli.Context) error { Action: func(ctx context.Context, c *cli.Command) error {
var content string keys := make([]string, 0, 1)
var password string var password string
switch c.Args().Len() { switch c.Args().Len() {
case 1: case 1:
content = ""
password = c.Args().Get(0) password = c.Args().Get(0)
case 2: case 2:
content = c.Args().Get(0) keys = append(keys, c.Args().Get(0))
password = c.Args().Get(1) password = c.Args().Get(1)
} }
if password == "" { if password == "" {
return fmt.Errorf("no password given") return fmt.Errorf("no password given")
} }
for sec := range getSecretKeysFromStdinLinesOrSlice(c, []string{content}) { for sec := range getSecretKeysFromStdinLinesOrSlice(ctx, c, keys) {
ncryptsec, err := nip49.Encrypt(sec, password, uint8(c.Int("logn")), 0x02) ncryptsec, err := nip49.Encrypt(sec, password, uint8(c.Int("logn")), 0x02)
if err != nil { if err != nil {
lineProcessingError(c, "failed to encrypt: %s", err) ctx = lineProcessingError(ctx, "failed to encrypt: %s", err)
continue continue
} }
stdout(ncryptsec) stdout(ncryptsec)
@@ -99,65 +102,157 @@ var decrypt = &cli.Command{
Usage: "takes an ncrypsec and a password and decrypts it into an nsec", Usage: "takes an ncrypsec and a password and decrypts it into an nsec",
Description: `uses the NIP-49 standard.`, Description: `uses the NIP-49 standard.`,
ArgsUsage: "<ncryptsec-code> <password>", ArgsUsage: "<ncryptsec-code> <password>",
Action: func(c *cli.Context) error { Action: func(ctx context.Context, c *cli.Command) error {
var content string var ncryptsec string
var password string var password string
switch c.Args().Len() { switch c.Args().Len() {
case 1:
content = ""
password = c.Args().Get(0)
case 2: case 2:
content = c.Args().Get(0) ncryptsec = c.Args().Get(0)
password = c.Args().Get(1) password = c.Args().Get(1)
}
if password == "" { if password == "" {
return fmt.Errorf("no password given") return fmt.Errorf("no password given")
} }
for ncryptsec := range getStdinLinesOrArgumentsFromSlice([]string{content}) {
sec, err := nip49.Decrypt(ncryptsec, password) sec, err := nip49.Decrypt(ncryptsec, password)
if err != nil { if err != nil {
lineProcessingError(c, "failed to decrypt: %s", err) return fmt.Errorf("failed to decrypt: %s", err)
}
stdout(sec)
return nil
case 1:
if arg := c.Args().Get(0); strings.HasPrefix(arg, "ncryptsec1") {
ncryptsec = arg
if res, err := promptDecrypt(ncryptsec); err != nil {
return err
} else {
stdout(res)
return nil
}
} else {
password = c.Args().Get(0)
for ncryptsec := range getStdinLinesOrArgumentsFromSlice([]string{ncryptsec}) {
sec, err := nip49.Decrypt(ncryptsec, password)
if err != nil {
ctx = lineProcessingError(ctx, "failed to decrypt: %s", err)
continue continue
} }
nsec, _ := nip19.EncodePrivateKey(sec) stdout(sec)
stdout(nsec)
} }
return nil return nil
}
default:
return fmt.Errorf("invalid number of arguments")
}
}, },
} }
var combine = &cli.Command{ var combine = &cli.Command{
Name: "combine", Name: "combine",
Usage: "combines two or more pubkeys using musig2", Usage: "combines two or more pubkeys using musig2",
Description: `The public keys must have 33 bytes (66 characters hex), with the 02 or 03 prefix. It is common in Nostr to drop that first byte, so you'll have to derive the public keys again from the private keys in order to get it back.`, Description: `The public keys must have 33 bytes (66 characters hex), with the 02 or 03 prefix. It is common in Nostr to drop that first byte, so you'll have to derive the public keys again from the private keys in order to get it back.
However, if the intent is to check if two existing Nostr pubkeys match a given combined pubkey, then it might be sufficient to calculate the combined key for all the possible combinations of pubkeys in the input.`,
ArgsUsage: "[pubkey...]", ArgsUsage: "[pubkey...]",
Action: func(c *cli.Context) error { Action: func(ctx context.Context, c *cli.Command) error {
keys := make([]*btcec.PublicKey, 0, 5) type Combination struct {
for _, pub := range c.Args().Slice() { Variants []string `json:"input_variants"`
keyb, err := hex.DecodeString(pub) Output struct {
if err != nil { XOnly string `json:"x_only"`
return fmt.Errorf("error parsing key %s: %w", pub, err) Variant string `json:"variant"`
} `json:"combined_key"`
} }
type Result struct {
Keys []string `json:"keys"`
Combinations []Combination `json:"combinations"`
}
result := Result{}
result.Keys = c.Args().Slice()
keyGroups := make([][]*btcec.PublicKey, 0, len(result.Keys))
for i, keyhex := range result.Keys {
keyb, err := hex.DecodeString(keyhex)
if err != nil {
return fmt.Errorf("error parsing key %s: %w", keyhex, err)
}
if len(keyb) == 32 /* we'll use both the 02 and the 03 prefix versions */ {
group := make([]*btcec.PublicKey, 2)
for i, prefix := range []byte{0x02, 0x03} {
pubk, err := btcec.ParsePubKey(append([]byte{prefix}, keyb...))
if err != nil {
fmt.Fprintf(os.Stderr, "error parsing key %s: %s", keyhex, err)
continue
}
group[i] = pubk
}
keyGroups = append(keyGroups, group)
} else /* assume it's 33 */ {
pubk, err := btcec.ParsePubKey(keyb) pubk, err := btcec.ParsePubKey(keyb)
if err != nil { if err != nil {
return fmt.Errorf("error parsing key %s: %w", pub, err) return fmt.Errorf("error parsing key %s: %w", keyhex, err)
}
keyGroups = append(keyGroups, []*btcec.PublicKey{pubk})
// remove the leading byte from the output just so it is all uniform
result.Keys[i] = result.Keys[i][2:]
}
} }
keys = append(keys, pubk) result.Combinations = make([]Combination, 0, 16)
var fn func(prepend int, curr []int)
fn = func(prepend int, curr []int) {
curr = append([]int{prepend}, curr...)
if len(curr) == len(keyGroups) {
combi := Combination{
Variants: make([]string, len(keyGroups)),
} }
agg, _, _, err := musig2.AggregateKeys(keys, true) combining := make([]*btcec.PublicKey, len(keyGroups))
for g, altKeys := range keyGroups {
altKey := altKeys[curr[g]]
variant := secp256k1.PubKeyFormatCompressedEven
if altKey.Y().Bit(0) == 1 {
variant = secp256k1.PubKeyFormatCompressedOdd
}
combi.Variants[g] = hex.EncodeToString([]byte{variant})
combining[g] = altKey
}
agg, _, _, err := musig2.AggregateKeys(combining, true)
if err != nil { if err != nil {
return err fmt.Fprintf(os.Stderr, "error aggregating: %s", err)
return
} }
fmt.Println(hex.EncodeToString(agg.FinalKey.SerializeCompressed())) serialized := agg.FinalKey.SerializeCompressed()
combi.Output.XOnly = hex.EncodeToString(serialized[1:])
combi.Output.Variant = hex.EncodeToString(serialized[0:1])
result.Combinations = append(result.Combinations, combi)
return
}
fn(0, curr)
if len(keyGroups[len(keyGroups)-len(curr)-1]) > 1 {
fn(1, curr)
}
}
fn(0, nil)
if len(keyGroups[len(keyGroups)-1]) > 1 {
fn(1, nil)
}
res, _ := json.MarshalIndent(result, "", " ")
fmt.Println(string(res))
return nil return nil
}, },
} }
func getSecretKeysFromStdinLinesOrSlice(c *cli.Context, keys []string) chan string { func getSecretKeysFromStdinLinesOrSlice(ctx context.Context, c *cli.Command, keys []string) chan string {
ch := make(chan string) ch := make(chan string)
go func() { go func() {
for sec := range getStdinLinesOrArgumentsFromSlice(keys) { for sec := range getStdinLinesOrArgumentsFromSlice(keys) {
@@ -167,13 +262,14 @@ func getSecretKeysFromStdinLinesOrSlice(c *cli.Context, keys []string) chan stri
if strings.HasPrefix(sec, "nsec1") { if strings.HasPrefix(sec, "nsec1") {
_, data, err := nip19.Decode(sec) _, data, err := nip19.Decode(sec)
if err != nil { if err != nil {
lineProcessingError(c, "invalid nsec code: %s", err) ctx = lineProcessingError(ctx, "invalid nsec code: %s", err)
continue continue
} }
sec = data.(string) sec = data.(string)
} }
sec = leftPadKey(sec)
if !nostr.IsValid32ByteHex(sec) { if !nostr.IsValid32ByteHex(sec) {
lineProcessingError(c, "invalid hex key") ctx = lineProcessingError(ctx, "invalid hex key")
continue continue
} }
ch <- sec ch <- sec

14
main.go
View File

@@ -1,17 +1,17 @@
package main package main
import ( import (
"context"
"os" "os"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
) )
var q int var app = &cli.Command{
var app = &cli.App{
Name: "nak", Name: "nak",
Suggest: true, Suggest: true,
UseShortOptionHandling: true, UseShortOptionHandling: true,
AllowFlagsAfterArguments: true,
Usage: "the nostr army knife command-line tool", Usage: "the nostr army knife command-line tool",
Commands: []*cli.Command{ Commands: []*cli.Command{
req, req,
@@ -29,9 +29,9 @@ var app = &cli.App{
&cli.BoolFlag{ &cli.BoolFlag{
Name: "quiet", Name: "quiet",
Usage: "do not print logs and info messages to stderr, use -qq to also not print anything to stdout", Usage: "do not print logs and info messages to stderr, use -qq to also not print anything to stdout",
Count: &q,
Aliases: []string{"q"}, Aliases: []string{"q"},
Action: func(ctx *cli.Context, b bool) error { Action: func(ctx context.Context, c *cli.Command, b bool) error {
q := c.Count("quiet")
if q >= 1 { if q >= 1 {
log = func(msg string, args ...any) {} log = func(msg string, args ...any) {}
if q >= 2 { if q >= 2 {
@@ -45,7 +45,7 @@ var app = &cli.App{
} }
func main() { func main() {
if err := app.Run(os.Args); err != nil { if err := app.Run(context.Background(), os.Args); err != nil {
stdout(err) stdout(err)
os.Exit(1) os.Exit(1)
} }

203
relay.go
View File

@@ -1,12 +1,21 @@
package main package main
import ( import (
"bytes"
"context"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"net/http"
"strings" "strings"
"github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nip11" "github.com/nbd-wtf/go-nostr/nip11"
"github.com/urfave/cli/v2" "github.com/nbd-wtf/go-nostr/nip86"
"github.com/urfave/cli/v3"
) )
var relay = &cli.Command{ var relay = &cli.Command{
@@ -15,19 +24,15 @@ var relay = &cli.Command{
Description: `example: Description: `example:
nak relay nostr.wine`, nak relay nostr.wine`,
ArgsUsage: "<relay-url>", ArgsUsage: "<relay-url>",
Action: func(c *cli.Context) error { Action: func(ctx context.Context, c *cli.Command) error {
for url := range getStdinLinesOrArguments(c.Args()) { for url := range getStdinLinesOrArguments(c.Args()) {
if url == "" { if url == "" {
return fmt.Errorf("specify the <relay-url>") return fmt.Errorf("specify the <relay-url>")
} }
if !strings.HasPrefix(url, "wss://") && !strings.HasPrefix(url, "ws://") { info, err := nip11.Fetch(ctx, url)
url = "wss://" + url
}
info, err := nip11.Fetch(c.Context, url)
if err != nil { if err != nil {
lineProcessingError(c, "failed to fetch '%s' information document: %w", url, err) ctx = lineProcessingError(ctx, "failed to fetch '%s' information document: %w", url, err)
continue continue
} }
@@ -36,4 +41,186 @@ var relay = &cli.Command{
} }
return nil return nil
}, },
Commands: (func() []*cli.Command {
commands := make([]*cli.Command, 0, 12)
for _, def := range []struct {
method string
args []string
}{
{"allowpubkey", []string{"pubkey", "reason"}},
{"banpubkey", []string{"pubkey", "reason"}},
{"listallowedpubkeys", nil},
{"allowpubkey", []string{"pubkey", "reason"}},
{"listallowedpubkeys", nil},
{"listeventsneedingmoderation", nil},
{"allowevent", []string{"id", "reason"}},
{"banevent", []string{"id", "reason"}},
{"listbannedevents", nil},
{"changerelayname", []string{"name"}},
{"changerelaydescription", []string{"description"}},
{"changerelayicon", []string{"icon"}},
{"allowkind", []string{"kind"}},
{"disallowkind", []string{"kind"}},
{"listallowedkinds", nil},
{"blockip", []string{"ip", "reason"}},
{"unblockip", []string{"ip", "reason"}},
{"listblockedips", nil},
} {
def := def
flags := make([]cli.Flag, len(def.args), len(def.args)+4)
for i, argName := range def.args {
flags[i] = declareFlag(argName)
}
flags = append(flags,
&cli.StringFlag{
Name: "sec",
Usage: "secret key to sign the event, as nsec, ncryptsec or hex",
DefaultText: "the key '1'",
Value: "0000000000000000000000000000000000000000000000000000000000000001",
},
&cli.BoolFlag{
Name: "prompt-sec",
Usage: "prompt the user to paste a hex or nsec with which to sign the event",
},
&cli.StringFlag{
Name: "connect",
Usage: "sign event using NIP-46, expects a bunker://... URL",
},
&cli.StringFlag{
Name: "connect-as",
Usage: "private key to when communicating with the bunker given on --connect",
DefaultText: "a random key",
},
)
cmd := &cli.Command{
Name: def.method,
Usage: fmt.Sprintf(`the "%s" relay management RPC call`, def.method),
Description: fmt.Sprintf(
`the "%s" management RPC call, see https://nips.nostr.com/86 for more information`, def.method),
Action: func(ctx context.Context, c *cli.Command) error {
params := make([]any, len(def.args))
for i, argName := range def.args {
params[i] = getArgument(c, argName)
}
req := nip86.Request{Method: def.method, Params: params}
reqj, _ := json.Marshal(req)
relayUrls := c.Args().Slice()
if len(relayUrls) == 0 {
stdout(string(reqj))
return nil
}
sec, bunker, err := gatherSecretKeyOrBunkerFromArguments(ctx, c)
if err != nil {
return err
}
for _, relayUrl := range relayUrls {
httpUrl := "http" + nostr.NormalizeURL(relayUrl)[2:]
log("calling '%s' on %s... ", def.method, httpUrl)
body := bytes.NewBuffer(nil)
body.Write(reqj)
req, err := http.NewRequestWithContext(ctx, "POST", httpUrl, body)
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
// Authorization
hostname := strings.Split(strings.Split(httpUrl, "://")[1], "/")[0]
payloadHash := sha256.Sum256(reqj)
authEvent := nostr.Event{
Kind: 27235,
CreatedAt: nostr.Now(),
Tags: nostr.Tags{
{"host", hostname},
{"payload", hex.EncodeToString(payloadHash[:])},
},
}
if bunker != nil {
if err := bunker.SignEvent(ctx, &authEvent); err != nil {
return fmt.Errorf("failed to sign with bunker: %w", err)
}
} else if err := authEvent.Sign(sec); err != nil {
return fmt.Errorf("error signing with provided key: %w", err)
}
evtj, _ := json.Marshal(authEvent)
req.Header.Set("Authorization", "Nostr "+base64.StdEncoding.EncodeToString(evtj))
// Content-Type
req.Header.Set("Content-Type", "application/nostr+json+rpc")
// make request to relay
resp, err := http.DefaultClient.Do(req)
if err != nil {
log("failed: %s\n", err)
continue
}
b, err := io.ReadAll(resp.Body)
if err != nil {
log("failed to read response: %s\n", err)
continue
}
if resp.StatusCode >= 300 {
log("failed with status %d\n", resp.StatusCode)
bodyPrintable := string(b)
if len(bodyPrintable) > 300 {
bodyPrintable = bodyPrintable[0:297] + "..."
}
log(bodyPrintable)
continue
}
var response nip86.Response
if err := json.Unmarshal(b, &response); err != nil {
log("bad json response: %s\n", err)
bodyPrintable := string(b)
if len(bodyPrintable) > 300 {
bodyPrintable = bodyPrintable[0:297] + "..."
}
log(bodyPrintable)
continue
}
resp.Body.Close()
// print the result
log("\n")
pretty, _ := json.MarshalIndent(response, "", " ")
stdout(string(pretty))
}
return nil
},
Flags: flags,
}
commands = append(commands, cmd)
}
return commands
})(),
}
func declareFlag(argName string) cli.Flag {
usage := "parameter for this management RPC call, see https://nips.nostr.com/86 for more information."
switch argName {
case "kind":
return &cli.IntFlag{Name: argName, Required: true, Usage: usage}
case "reason":
return &cli.StringFlag{Name: argName, Usage: usage}
default:
return &cli.StringFlag{Name: argName, Required: true, Usage: usage}
}
}
func getArgument(c *cli.Command, argName string) any {
switch argName {
case "kind":
return c.Int(argName)
default:
return c.String(argName)
}
} }

35
req.go
View File

@@ -1,6 +1,7 @@
package main package main
import ( import (
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"os" "os"
@@ -9,7 +10,7 @@ import (
"github.com/mailru/easyjson" "github.com/mailru/easyjson"
"github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
) )
const CATEGORY_FILTER_ATTRIBUTES = "FILTER ATTRIBUTES" const CATEGORY_FILTER_ATTRIBUTES = "FILTER ATTRIBUTES"
@@ -103,6 +104,11 @@ example:
Name: "auth", Name: "auth",
Usage: "always perform NIP-42 \"AUTH\" when facing an \"auth-required: \" rejection and try again", Usage: "always perform NIP-42 \"AUTH\" when facing an \"auth-required: \" rejection and try again",
}, },
&cli.BoolFlag{
Name: "force-pre-auth",
Aliases: []string{"fpa"},
Usage: "after connecting, for a NIP-42 \"AUTH\" message to be received, act on it and only then send the \"REQ\"",
},
&cli.StringFlag{ &cli.StringFlag{
Name: "sec", Name: "sec",
Usage: "secret key to sign the AUTH challenge, as hex or nsec", Usage: "secret key to sign the AUTH challenge, as hex or nsec",
@@ -124,33 +130,34 @@ example:
}, },
}, },
ArgsUsage: "[relay...]", ArgsUsage: "[relay...]",
Action: func(c *cli.Context) error { Action: func(ctx context.Context, c *cli.Command) error {
var pool *nostr.SimplePool var pool *nostr.SimplePool
relayUrls := c.Args().Slice() relayUrls := c.Args().Slice()
if len(relayUrls) > 0 { if len(relayUrls) > 0 {
var relays []*nostr.Relay var relays []*nostr.Relay
pool, relays = connectToAllRelays(c.Context, relayUrls, nostr.WithAuthHandler(func(evt *nostr.Event) error { pool, relays = connectToAllRelays(ctx, relayUrls, c.Bool("force-pre-auth"), nostr.WithAuthHandler(func(evt *nostr.Event) error {
if !c.Bool("auth") { if !c.Bool("auth") && !c.Bool("force-pre-auth") {
return fmt.Errorf("auth not authorized") return fmt.Errorf("auth not authorized")
} }
sec, bunker, err := gatherSecretKeyOrBunkerFromArguments(c) sec, bunker, err := gatherSecretKeyOrBunkerFromArguments(ctx, c)
if err != nil { if err != nil {
return err return err
} }
var pk string var pk string
if bunker != nil { if bunker != nil {
pk, err = bunker.GetPublicKey(c.Context) pk, err = bunker.GetPublicKey(ctx)
if err != nil { if err != nil {
return fmt.Errorf("failed to get public key from bunker: %w", err) return fmt.Errorf("failed to get public key from bunker: %w", err)
} }
} else { } else {
pk, _ = nostr.GetPublicKey(sec) pk, _ = nostr.GetPublicKey(sec)
} }
log("performing auth as %s...\n", pk) log("performing auth as %s... ", pk)
if bunker != nil { if bunker != nil {
return bunker.SignEvent(c.Context, evt) return bunker.SignEvent(ctx, evt)
} else { } else {
return evt.Sign(sec) return evt.Sign(sec)
} }
@@ -175,7 +182,7 @@ example:
filter := nostr.Filter{} filter := nostr.Filter{}
if stdinFilter != "" { if stdinFilter != "" {
if err := easyjson.Unmarshal([]byte(stdinFilter), &filter); err != nil { if err := easyjson.Unmarshal([]byte(stdinFilter), &filter); err != nil {
lineProcessingError(c, "invalid filter '%s' received from stdin: %s", stdinFilter, err) ctx = lineProcessingError(ctx, "invalid filter '%s' received from stdin: %s", stdinFilter, err)
continue continue
} }
} }
@@ -186,8 +193,8 @@ example:
if ids := c.StringSlice("id"); len(ids) > 0 { if ids := c.StringSlice("id"); len(ids) > 0 {
filter.IDs = append(filter.IDs, ids...) filter.IDs = append(filter.IDs, ids...)
} }
if kinds := c.IntSlice("kind"); len(kinds) > 0 { for _, kind64 := range c.IntSlice("kind") {
filter.Kinds = append(filter.Kinds, kinds...) filter.Kinds = append(filter.Kinds, int(kind64))
} }
if search := c.String("search"); search != "" { if search := c.String("search"); search != "" {
filter.Search = search filter.Search = search
@@ -245,7 +252,7 @@ example:
} }
} }
if limit := c.Int("limit"); limit != 0 { if limit := c.Int("limit"); limit != 0 {
filter.Limit = limit filter.Limit = int(limit)
} else if c.IsSet("limit") || c.Bool("stream") { } else if c.IsSet("limit") || c.Bool("stream") {
filter.LimitZero = true filter.LimitZero = true
} }
@@ -255,7 +262,7 @@ example:
if c.Bool("stream") { if c.Bool("stream") {
fn = pool.SubMany fn = pool.SubMany
} }
for ie := range fn(c.Context, relayUrls, nostr.Filters{filter}) { for ie := range fn(ctx, relayUrls, nostr.Filters{filter}) {
stdout(ie.Event) stdout(ie.Event)
} }
} else { } else {
@@ -272,7 +279,7 @@ example:
} }
} }
exitIfLineProcessingError(c) exitIfLineProcessingError(ctx)
return nil return nil
}, },
} }

View File

@@ -1,10 +1,11 @@
package main package main
import ( import (
"context"
"encoding/json" "encoding/json"
"github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
) )
var verify = &cli.Command{ var verify = &cli.Command{
@@ -14,28 +15,28 @@ var verify = &cli.Command{
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(ctx context.Context, c *cli.Command) error {
for stdinEvent := range getStdinLinesOrArguments(c.Args()) { for stdinEvent := range getStdinLinesOrArguments(c.Args()) {
evt := nostr.Event{} evt := nostr.Event{}
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: %s", err) ctx = lineProcessingError(ctx, "invalid event: %s", err)
continue continue
} }
} }
if evt.GetID() != evt.ID { if evt.GetID() != evt.ID {
lineProcessingError(c, "invalid .id, expected %s, got %s", evt.GetID(), evt.ID) ctx = lineProcessingError(ctx, "invalid .id, expected %s, got %s", evt.GetID(), evt.ID)
continue continue
} }
if ok, err := evt.CheckSignature(); !ok { if ok, err := evt.CheckSignature(); !ok {
lineProcessingError(c, "invalid signature: %s", err) ctx = lineProcessingError(ctx, "invalid signature: %s", err)
continue continue
} }
} }
exitIfLineProcessingError(c) exitIfLineProcessingError(ctx)
return nil return nil
}, },
} }