mirror of
https://github.com/fiatjaf/nak.git
synced 2025-12-08 16:48:51 +00:00
Compare commits
88 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e353680bc | ||
|
|
ff8701a3b0 | ||
|
|
ad6b8c4ba5 | ||
|
|
dba3f648ad | ||
|
|
12a1f1563e | ||
|
|
e2dd3ca544 | ||
|
|
df5ebd3f56 | ||
|
|
81571c6952 | ||
|
|
6e43a6b733 | ||
|
|
943e8835f9 | ||
|
|
6b659c1552 | ||
|
|
aa53f2cd60 | ||
|
|
5509095277 | ||
|
|
a3ef9b45de | ||
|
|
df20a3241a | ||
|
|
53a2451303 | ||
|
|
2d992f235e | ||
|
|
7675929056 | ||
|
|
7f608588a2 | ||
|
|
fd5cd55f6f | ||
|
|
932361fe8f | ||
|
|
11ae7bc4d3 | ||
|
|
7033bfee19 | ||
|
|
f425097c5a | ||
|
|
dd0ef2ca64 | ||
|
|
491a094e07 | ||
|
|
9d619ddf00 | ||
|
|
5d32739573 | ||
|
|
a187e448f2 | ||
|
|
9a9e96a829 | ||
|
|
4c6181d649 | ||
|
|
71b106fd45 | ||
|
|
40892c1228 | ||
|
|
847f8aaa69 | ||
|
|
134d1225d6 | ||
|
|
464766a836 | ||
|
|
ea53eca74f | ||
|
|
38ed370c59 | ||
|
|
5b04bc4859 | ||
|
|
2988c71ccb | ||
|
|
d7c0ff2bb7 | ||
|
|
43fe41df5d | ||
|
|
3215726417 | ||
|
|
a4886dc445 | ||
|
|
dae7eba8ca | ||
|
|
2b5f3355bc | ||
|
|
bd5ca27661 | ||
|
|
9d02301b2d | ||
|
|
9bbc87b27a | ||
|
|
88a07a3504 | ||
|
|
8a934cc76b | ||
|
|
e0c967efa9 | ||
|
|
36c32ae308 | ||
|
|
6d23509d8c | ||
|
|
29b6ecbafe | ||
|
|
11f37afa5b | ||
|
|
cf1694704e | ||
|
|
b3ef2c1289 | ||
|
|
cfdea699bc | ||
|
|
014c6bc11d | ||
|
|
0240866fa1 | ||
|
|
a4d9ceecfa | ||
|
|
56657d8aa9 | ||
|
|
ea7b88cfd7 | ||
|
|
2042b14578 | ||
|
|
9d43e66fac | ||
|
|
85e9610265 | ||
|
|
2edfa5cbea | ||
|
|
9690dc70cb | ||
|
|
c90e61dbec | ||
|
|
d226cd6ce4 | ||
|
|
3d78e91f62 | ||
|
|
84965f2253 | ||
|
|
928c73513c | ||
|
|
a36142604d | ||
|
|
220fe84f1b | ||
|
|
48c0e342e3 | ||
|
|
ec2e214c02 | ||
|
|
9f62d4679f | ||
|
|
809865ca0c | ||
|
|
813ab3b6ac | ||
|
|
09ed2a040a | ||
|
|
7846960c4e | ||
|
|
ce6bb0aa22 | ||
|
|
49ce12ffc2 | ||
|
|
a5013c513d | ||
|
|
8f51fe757b | ||
|
|
30ca5776c5 |
13
.github/workflows/release-cli.yml
vendored
13
.github/workflows/release-cli.yml
vendored
@@ -25,10 +25,20 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
goos: [linux, freebsd, darwin, windows]
|
goos: [linux, freebsd, darwin, windows]
|
||||||
goarch: [amd64, arm64]
|
goarch: [arm, amd64, arm64, riscv64]
|
||||||
exclude:
|
exclude:
|
||||||
- goarch: arm64
|
- goarch: arm64
|
||||||
goos: windows
|
goos: windows
|
||||||
|
- goarch: riscv64
|
||||||
|
goos: windows
|
||||||
|
- goarch: riscv64
|
||||||
|
goos: darwin
|
||||||
|
- goarch: arm
|
||||||
|
goos: windows
|
||||||
|
- goarch: arm
|
||||||
|
goos: darwin
|
||||||
|
- goarch: arm
|
||||||
|
goos: freebsd
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: wangyoucao577/go-release-action@v1.40
|
- uses: wangyoucao577/go-release-action@v1.40
|
||||||
@@ -36,6 +46,7 @@ jobs:
|
|||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
goos: ${{ matrix.goos }}
|
goos: ${{ matrix.goos }}
|
||||||
goarch: ${{ matrix.goarch }}
|
goarch: ${{ matrix.goarch }}
|
||||||
|
ldflags: -X main.version=${{ github.ref_name }}
|
||||||
overwrite: true
|
overwrite: true
|
||||||
md5sum: false
|
md5sum: false
|
||||||
sha256sum: false
|
sha256sum: false
|
||||||
|
|||||||
73
README.md
73
README.md
@@ -77,15 +77,14 @@ publishing to wss://relayable.org... success.
|
|||||||
invalid .id, expected 05bd99d54cb835f427e0092c4275ee44c7ff51219eff417c19f70c9e2c53ad5a, got 05bd99d54cb835f327e0092c4275ee44c7ff51219eff417c19f70c9e2c53ad5a
|
invalid .id, expected 05bd99d54cb835f427e0092c4275ee44c7ff51219eff417c19f70c9e2c53ad5a, got 05bd99d54cb835f327e0092c4275ee44c7ff51219eff417c19f70c9e2c53ad5a
|
||||||
```
|
```
|
||||||
|
|
||||||
### fetch all quoted events by a given pubkey in their last 100 notes
|
### fetch all quoted events by a given pubkey in their last 10 notes of 2023
|
||||||
```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 10 -k 1 --until 'December 31 2023' -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...
|
connecting to relay.damus.io...
|
||||||
ok.
|
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":"0000000a5109c9747e3847282fcaef3d221d1be5e864ced7b2099d416a18d15a","pubkey":"7bdef7be22dd8e59f4600e044aa53a1cf975a9dc7d27df5833bc77db784a5805","created_at":1703869609,"tags":[["nonce","12912720851599460299","25"]],"content":"https://image.nostr.build/5eb40d3cae799bc572763b8f8bee95643344fa392d280efcb0fd28a935879e2a.png\n\nNostr is not dying.\nIt is just a physiological and healthy slowdown on the part of all those who have made this possible in such a short time, sharing extraordinary enthusiasm. This is necessary to regain a little energy, it will allow some things to be cleaned up and more focused goals to be set.\n\nIt is like the caterpillar that is about to become a butterfly, it has to stop moving, acting, doing all the time; it has to do one last silent work and rest, letting time go by. And then a new phase of life can begin.\n\nWe have an amazing 2024 ahead.\nThank you all, who have given so much and believe in Nostr.\n\nPS: an interesting cue suggested by this image, you cannot have both silk and butterfly, you have to choose: a precious and sophisticated ornament, or the living, colorful beauty of freedom.","sig":"16fe157fb13dba2474d510db5253edc409b465515371015a91b26b8f39e5aa873453bc366947c37463c49466f5fceb7dea0485432f979a03471c8f76b73e553c"}
|
||||||
{"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":"ac0cc72dfee39f41d94568f574e7b613d3979facbd7b477a16b52eb763db4b6e","pubkey":"2250f69694c2a43929e77e5de0f6a61ae5e37a1ee6d6a3baef1706ed9901248b","created_at":1703873865,"tags":[["r","https://zine.wavlake.com/2023-year-in-review/"]],"content":"It's been an incredible year for us here at Wavlake and we wanted to take a moment to look back and see how far we've come since launch. Read more.. https://zine.wavlake.com/2023-year-in-review/","sig":"189e354f67f48f3046fd762c83f9bf3a776d502d514e2839a1b459c30107a02453304ef695cdc7d254724041feec3800806b21eb76259df87144aaef821ace5b"}
|
||||||
{"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":"6215766c5aadfaf51488134682f7d28f237218b5405da2fc11d1fefe1ebf8154","pubkey":"4ce6abbd68dab6e9fdf6e8e9912a8e12f9b539e078c634c55a9bff2994a514dd","created_at":1703879775,"tags":[["imeta","url https://video.nostr.build/7b4e7c326fa4fcba58a40914ce9db4f060bd917878f2194f6d139948b085ebb9.mp4","blurhash eHD,QG_4ogMu_3to%O-:MwM_IWRjx^-pIUoe-;t7%Nt7%gV?M{WBxu","dim 480x268"],["t","zaps"],["t","powakili23"],["p","4f82bced42584a6acfced2a657b5acabc4f90d75a95ed3ff888f3b04b8928630"],["p","ce75bae2349804caa5f4de8ae8f775bb558135f412441d9e32f88e4226c5d165"],["p","94bd495b78f8f6e5aff8ebc90e052d3a409d1f9d82e43ab56ca2cafb81b18ddf"],["p","50ff5b7ebeac1cc0d03dc878be8a59f1b63d45a7d5e60ade4b6f6f31eca25954"],["p","f300cf2bdf9808ed229dfa468260753a0b179935bdb87612b6d4f5b9fe3fc7cf"],["r","https://geyser.fund/entry/2636"],["r","https://video.nostr.build/7b4e7c326fa4fcba58a40914ce9db4f060bd917878f2194f6d139948b085ebb9.mp4"]],"content":"POWA - HQ UPDATE - DEC 2023\nTLDR: plan to open January 2024, 1 million Sats to go to reach milestone. #zaps go to fund this project. ⚡️powa@geyser.fund\n\nHello,\n\nFirst and foremost, I’d like to thank you for the incredible support shown for this project. It’s been an absolute honor to oversee this Proof of Work initiative.\n\nI am thrilled to announce that we are right on track for the grand opening in January 2024.\n\nCurrently, we're just over 1 million Sats away from reaching our target for this phase.\n\nPlease take a moment to enjoy the video and stay tuned for further updates about POWA. \n\nMan Like Who?\nMan Like Kweks!\n🇹🇿⚡️💜🏔️\n#powakili23\nnostr:npub1f7ptem2ztp9x4n7w62n90ddv40z0jrt4490d8lug3uasfwyjsccqkknerm nostr:npub1ee6m4c35nqzv4f05m69w3am4hd2czd05zfzpm83jlz8yyfk969js78tfcv nostr:npub1jj75jkmclrmwttlca0ysupfd8fqf68uastjr4dtv5t90hqd33h0s4gcksp nostr:npub12rl4kl474swvp5paeputazje7xmr63d86hnq4hjtdahnrm9zt92qgq500s nostr:npub17vqv727lnqyw6g5alfrgycr48g930xf4hku8vy4k6n6mnl3lcl8sglecc5 \n\nhttps://geyser.fund/entry/2636 https://video.nostr.build/7b4e7c326fa4fcba58a40914ce9db4f060bd917878f2194f6d139948b085ebb9.mp4 ","sig":"97d13c17d91c319f343cc770222d6d4a0a714d0e7e4ef43373adaf215a4c077f0bdf12bac488c74dbd4d55718d46c17a617b93c8660736b70bcd61a8820ece67"}
|
||||||
{"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"}
|
|
||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -168,6 +167,68 @@ listening at [wss://relay.damus.io wss://nos.lol wss://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
|
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
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### generate a NIP-70 protected event with a date set to two weeks ago and some multi-value tags
|
||||||
|
```shell
|
||||||
|
~> nak event --ts 'two weeks ago' -t '-' -t 'e=f59911b561c37c90b01e9e5c2557307380835c83399756f4d62d8167227e420a;wss://relay.whatever.com;root;a9e0f110f636f3191644110c19a33448daf09d7cda9708a769e91b7e91340208' -t 'p=a9e0f110f636f3191644110c19a33448daf09d7cda9708a769e91b7e91340208;wss://p-relay.com' -c 'I know the future'
|
||||||
|
{"kind":1,"id":"f030fccd90c783858dfcee204af94826cf0f1c85d6fc85a0087e9e5172419393","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1719677535,"tags":[["-"],["e","f59911b561c37c90b01e9e5c2557307380835c83399756f4d62d8167227e420a","wss://relay.whatever.com","root","a9e0f110f636f3191644110c19a33448daf09d7cda9708a769e91b7e91340208"],["p","a9e0f110f636f3191644110c19a33448daf09d7cda9708a769e91b7e91340208","wss://p-relay.com"]],"content":"I know the future","sig":"8b36a74e29df8bc12bed66896820da6940d4d9409721b3ed2e910c838833a178cb45fd5bb1c6eb6adc66ab2808bfac9f6644a2c55a6570bb2ad90f221c9c7551"}
|
||||||
|
```
|
||||||
|
|
||||||
|
### download the latest 50000 notes from a relay, regardless of their natural query limits, by paginating requests
|
||||||
|
```shell
|
||||||
|
~> nak req -k 1 --limit 50000 --paginate --paginate-interval 2s nos.lol > events.jsonl
|
||||||
|
~> wc -l events.jsonl
|
||||||
|
50000 events.jsonl
|
||||||
|
```
|
||||||
|
|
||||||
|
### run a somewhat verbose local relay for test purposes
|
||||||
|
```shell
|
||||||
|
~> nak serve
|
||||||
|
> relay running at ws://localhost:10547
|
||||||
|
got request {"kinds":[1],"authors":["79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"],"since":1724082362}
|
||||||
|
got event {"kind":1,"id":"e3c6bf630d6deea74c0ee2f7f7ba6da55a627498a32f1e72029229bb1810bce3","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1724082366,"tags":[],"content":"two","sig":"34261cf226c3fee2df24e55a89f43f5349c98a64bce46bdc46807b0329f334cea93e9e8bc285c1259a5684cf23f5e507c8e6dad47a31a6615d706b1130d09e69"}
|
||||||
|
got event {"kind":1,"id":"0bbb397c8f87ae557650b9d6ee847292df8e530c458ffea1b24bdcb7bed0ec5e","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1724082369,"tags":[],"content":"three","sig":"aa1cb7d5f0f03f358fc4c0a4351a4f1c66e3a7627021b618601c56ba598b825b6d95d9c8720a4c60666a7eb21e17018cf326222f9f574a9396f2f2da7f007546"}
|
||||||
|
• events stored: 2, subscriptions opened: 1
|
||||||
|
got event {"kind":1,"id":"029ebff759dd54dbd01b929f879fea5802de297e1c3768ca16d9b97cc8bca38f","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1724082371,"tags":[],"content":"four","sig":"9816de517d87d4c3ede57c1c50e3c237486794241afadcd891e1acbba2c5e672286090e6ad3402b047d69bae8095bc4e20e57ac70d92386dfa26db216379330f"}
|
||||||
|
got event {"kind":1,"id":"fe6489fa6fbb925be839377b9b7049d73be755dc2bdad97ff6dd9eecbf8b3a32","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1724082383,"tags":[],"content":"five","sig":"865ce5e32eead5bdb950ac1fbc55bc92dde26818ee3136634538ec42914de179a51e672c2d4269d4362176e5e8cd5e08e69b35b91c6c2af867e129b93d607635"}
|
||||||
|
got request {"kinds":[30818]}
|
||||||
|
• events stored: 4, subscriptions opened: 1
|
||||||
|
```
|
||||||
|
|
||||||
|
### make an event with a PoW target
|
||||||
|
```shell
|
||||||
|
~> nak event -c 'hello getwired.app and labour.fiatjaf.com' --pow 24
|
||||||
|
{"kind":1,"id":"0000009dcc7c62056eafdb41fac817379ec2becf0ce27c5fbe98d0735d968147","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1724160828,"tags":[["nonce","515504","24"]],"content":"hello getwired.app and labour.fiatjaf.com","sig":"7edb988065ccc12779fe99270945b212f3723838f315d76d5e90e9ffa27198f13fa556614295f518d968d55bab81878167d4162b3a7cf81a6b423c6761bd504c"}
|
||||||
|
```
|
||||||
|
|
||||||
|
### make a nostr event signed with a key given as an environment variable
|
||||||
|
|
||||||
|
```shell
|
||||||
|
~> export NOSTR_SECRET_KEY=ncryptsec1qggyy9vw0nclmw8ly9caz6aa7f85a4ufhsct64uva337pulsdw00n6twa2lzhzk2znzsyu60urx9s08lx00ke6ual3lszyn5an9zarm6s70lw5lj6dv3mj3f9p4tvp0we6qyz4gp420mapfmvqheuttv
|
||||||
|
~> nak event -c 'it supports keys as hex, nsec or ncryptsec'
|
||||||
|
type the password to decrypt your secret key: ********
|
||||||
|
{"kind":1,"id":"5cbf3feb9a7d99c3ee2a88693a591caca1a8348fea427b3652c27f7a8a76af48","pubkey":"b00bcab55375d8c7b731dd9841f6d805ff1cf6fdc945e7326786deb5ddac6ce4","created_at":1724247924,"tags":[],"content":"it supports keys as hex, nsec or ncryptsec","sig":"fb3fd170bc10e5042322c7a05dd4bbd8ac9947b39026b8a7afd1ee02524e8e3aa1d9554e9c7b6181ca1b45cab01cd06643bdffa5ce678b475e6b185e1c14b085"}
|
||||||
|
```
|
||||||
|
|
||||||
|
### download some helpful `jq` functions for dealing with nostr events
|
||||||
|
```shell
|
||||||
|
~> nak req -i 412f2d3e73acc312942c055ac2a695dc60bf58ff97e06689a8a79e97796c4cdb relay.westernbtc.com | jq -r .content > ~/.jq
|
||||||
|
```
|
||||||
|
|
||||||
|
### watch a NIP-53 livestream (zap.stream etc)
|
||||||
|
```shell
|
||||||
|
~> # this requires the jq utils from the step above
|
||||||
|
~> mpv $(nak fetch naddr1qqjxvvm9xscnsdtx95cxvcfk956rsvtx943rje3k95mx2dp389jnwwrp8ymxgqg4waehxw309aex2mrp0yhxgctdw4eju6t09upzpn6956apxcad0mfp8grcuugdysg44eepex68h50t73zcathmfs49qvzqqqrkvu7ed38k | jq -r 'tag_value("streaming")')
|
||||||
|
~>
|
||||||
|
~> # or without the utils
|
||||||
|
~> mpv $(nak fetch naddr1qqjxvvm9xscnsdtx95cxvcfk956rsvtx943rje3k95mx2dp389jnwwrp8ymxgqg4waehxw309aex2mrp0yhxgctdw4eju6t09upzpn6956apxcad0mfp8grcuugdysg44eepex68h50t73zcathmfs49qvzqqqrkvu7ed38k | jq -r '.tags | map(select(.[0] == "streaming") | .[1])[0]')
|
||||||
|
```
|
||||||
|
|
||||||
|
### download a NIP-35 torrent from an `nevent`
|
||||||
|
```shell
|
||||||
|
~> # this requires the jq utils from two steps above
|
||||||
|
~> aria2c $(nak fetch nevent1qqsdsg6x7uujekac4ga7k7qa9q9sx8gqj7xzjf5w9us0dm0ghvf4ugspp4mhxue69uhkummn9ekx7mq6dw9y4 | jq -r '"magnet:?xt=urn:btih:\(tag_value("x"))&dn=\(tag_value("title"))&tr=http%3A%2F%2Ftracker.loadpeers.org%3A8080%2FxvRKfvAlnfuf5EfxTT5T0KIVPtbqAHnX%2Fannounce&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A6969%2Fannounce&tr=udp%3A%2F%2Fopen.stealth.si%3A80%2Fannounce&tr=udp%3A%2F%2Ftracker.torrent.eu.org%3A451%2Fannounce&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337&tr=\(tags("tracker") | map(.[1] | @uri) | join("&tr="))"')
|
||||||
|
```
|
||||||
|
|
||||||
## contributing to this repository
|
## contributing to this repository
|
||||||
|
|
||||||
Use NIP-34 to send your patches to `naddr1qqpkucttqy28wumn8ghj7un9d3shjtnwdaehgu3wvfnsz9nhwden5te0wfjkccte9ehx7um5wghxyctwvsq3gamnwvaz7tmjv4kxz7fwv3sk6atn9e5k7q3q80cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsxpqqqpmej2wctpn`.
|
Use NIP-34 to send your patches to `naddr1qqpkucttqy28wumn8ghj7un9d3shjtnwdaehgu3wvfnsz9nhwden5te0wfjkccte9ehx7um5wghxyctwvsq3gamnwvaz7tmjv4kxz7fwv3sk6atn9e5k7q3q80cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsxpqqqpmej2wctpn`.
|
||||||
|
|||||||
36
bunker.go
36
bunker.go
@@ -2,33 +2,32 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
|
"github.com/fiatjaf/cli/v3"
|
||||||
"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/v3"
|
|
||||||
"golang.org/x/exp/slices"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var bunker = &cli.Command{
|
var bunker = &cli.Command{
|
||||||
Name: "bunker",
|
Name: "bunker",
|
||||||
Usage: "starts a NIP-46 signer daemon with the given --sec key",
|
Usage: "starts a nip46 signer daemon with the given --sec key",
|
||||||
ArgsUsage: "[relay...]",
|
ArgsUsage: "[relay...]",
|
||||||
Description: ``,
|
Description: ``,
|
||||||
|
DisableSliceFlagSeparator: true,
|
||||||
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 hex or nsec",
|
||||||
DefaultText: "the key '1'",
|
DefaultText: "the key '1'",
|
||||||
Value: "0000000000000000000000000000000000000000000000000000000000000001",
|
|
||||||
},
|
},
|
||||||
&cli.BoolFlag{
|
&cli.BoolFlag{
|
||||||
Name: "prompt-sec",
|
Name: "prompt-sec",
|
||||||
@@ -50,7 +49,7 @@ var bunker = &cli.Command{
|
|||||||
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(ctx, relayUrls, false)
|
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)
|
||||||
@@ -84,8 +83,6 @@ var bunker = &cli.Command{
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
npub, _ := nip19.EncodePublicKey(pubkey)
|
npub, _ := nip19.EncodePublicKey(pubkey)
|
||||||
bold := color.New(color.Bold).Sprint
|
|
||||||
italic := color.New(color.Italic).Sprint
|
|
||||||
|
|
||||||
// this function will be called every now and then
|
// this function will be called every now and then
|
||||||
printBunkerInfo := func() {
|
printBunkerInfo := func() {
|
||||||
@@ -94,12 +91,12 @@ var bunker = &cli.Command{
|
|||||||
|
|
||||||
authorizedKeysStr := ""
|
authorizedKeysStr := ""
|
||||||
if len(authorizedKeys) != 0 {
|
if len(authorizedKeys) != 0 {
|
||||||
authorizedKeysStr = "\n authorized keys:\n - " + italic(strings.Join(authorizedKeys, "\n - "))
|
authorizedKeysStr = "\n authorized keys:\n - " + colors.italic(strings.Join(authorizedKeys, "\n - "))
|
||||||
}
|
}
|
||||||
|
|
||||||
authorizedSecretsStr := ""
|
authorizedSecretsStr := ""
|
||||||
if len(authorizedSecrets) != 0 {
|
if len(authorizedSecrets) != 0 {
|
||||||
authorizedSecretsStr = "\n authorized secrets:\n - " + italic(strings.Join(authorizedSecrets, "\n - "))
|
authorizedSecretsStr = "\n authorized secrets:\n - " + colors.italic(strings.Join(authorizedSecrets, "\n - "))
|
||||||
}
|
}
|
||||||
|
|
||||||
preauthorizedFlags := ""
|
preauthorizedFlags := ""
|
||||||
@@ -131,21 +128,20 @@ var bunker = &cli.Command{
|
|||||||
)
|
)
|
||||||
|
|
||||||
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",
|
||||||
bold(relayURLs),
|
colors.bold(relayURLs),
|
||||||
bold(pubkey),
|
colors.bold(pubkey),
|
||||||
bold(npub),
|
colors.bold(npub),
|
||||||
authorizedKeysStr,
|
authorizedKeysStr,
|
||||||
authorizedSecretsStr,
|
authorizedSecretsStr,
|
||||||
color.CyanString(restartCommand),
|
color.CyanString(restartCommand),
|
||||||
bold(bunkerURI),
|
colors.bold(bunkerURI),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
printBunkerInfo()
|
printBunkerInfo()
|
||||||
|
|
||||||
// subscribe to relays
|
// subscribe to relays
|
||||||
pool := nostr.NewSimplePool(ctx)
|
|
||||||
now := nostr.Now()
|
now := nostr.Now()
|
||||||
events := pool.SubMany(ctx, relayURLs, nostr.Filters{
|
events := sys.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}},
|
||||||
@@ -177,14 +173,14 @@ var bunker = &cli.Command{
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
return harmless || slices.Contains(authorizedKeys, from) || slices.Contains(authorizedSecrets, secret)
|
return slices.Contains(authorizedKeys, from) || slices.Contains(authorizedSecrets, secret)
|
||||||
}
|
}
|
||||||
|
|
||||||
for ie := range events {
|
for ie := range events {
|
||||||
cancelPreviousBunkerInfoPrint() // this prevents us from printing a million bunker info blocks
|
cancelPreviousBunkerInfoPrint() // this prevents us from printing a million bunker info blocks
|
||||||
|
|
||||||
// handle the NIP-46 request event
|
// handle the NIP-46 request event
|
||||||
req, resp, eventResponse, err := signer.HandleRequest(ie.Event)
|
req, resp, eventResponse, err := signer.HandleRequest(ctx, ie.Event)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log("< failed to handle request from %s: %s\n", ie.Event.PubKey, err.Error())
|
log("< failed to handle request from %s: %s\n", ie.Event.PubKey, err.Error())
|
||||||
continue
|
continue
|
||||||
@@ -198,7 +194,7 @@ var bunker = &cli.Command{
|
|||||||
handlerWg.Add(len(relayURLs))
|
handlerWg.Add(len(relayURLs))
|
||||||
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, _ := sys.Pool.EnsureRelay(relayURL); relay != nil {
|
||||||
err := relay.Publish(ctx, eventResponse)
|
err := relay.Publish(ctx, eventResponse)
|
||||||
printLock.Lock()
|
printLock.Lock()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -223,7 +219,7 @@ var bunker = &cli.Command{
|
|||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
case <-time.After(time.Minute * 5):
|
case <-time.After(time.Minute * 5):
|
||||||
fmt.Fprintf(os.Stderr, "\n")
|
log("\n")
|
||||||
printBunkerInfo()
|
printBunkerInfo()
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|||||||
71
count.go
71
count.go
@@ -2,19 +2,21 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/fiatjaf/cli/v3"
|
||||||
"github.com/nbd-wtf/go-nostr"
|
"github.com/nbd-wtf/go-nostr"
|
||||||
"github.com/urfave/cli/v3"
|
"github.com/nbd-wtf/go-nostr/nip45"
|
||||||
|
"github.com/nbd-wtf/go-nostr/nip45/hyperloglog"
|
||||||
)
|
)
|
||||||
|
|
||||||
var count = &cli.Command{
|
var count = &cli.Command{
|
||||||
Name: "count",
|
Name: "count",
|
||||||
Usage: "generates encoded COUNT messages and optionally use them to talk to relays",
|
Usage: "generates encoded COUNT messages and optionally use them to talk to relays",
|
||||||
Description: `outputs a NIP-45 request (the flags are mostly the same as 'nak req').`,
|
Description: `outputs a nip45 request (the flags are mostly the same as 'nak req').`,
|
||||||
|
DisableSliceFlagSeparator: true,
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
&cli.StringSliceFlag{
|
&cli.StringSliceFlag{
|
||||||
Name: "author",
|
Name: "author",
|
||||||
@@ -65,6 +67,32 @@ var count = &cli.Command{
|
|||||||
},
|
},
|
||||||
ArgsUsage: "[relay...]",
|
ArgsUsage: "[relay...]",
|
||||||
Action: func(ctx context.Context, c *cli.Command) error {
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
|
biggerUrlSize := 0
|
||||||
|
relayUrls := c.Args().Slice()
|
||||||
|
if len(relayUrls) > 0 {
|
||||||
|
relays := connectToAllRelays(ctx,
|
||||||
|
relayUrls,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
if len(relays) == 0 {
|
||||||
|
log("failed to connect to any of the given relays.\n")
|
||||||
|
os.Exit(3)
|
||||||
|
}
|
||||||
|
relayUrls = make([]string, len(relays))
|
||||||
|
for i, relay := range relays {
|
||||||
|
relayUrls[i] = relay.URL
|
||||||
|
if len(relay.URL) > biggerUrlSize {
|
||||||
|
biggerUrlSize = len(relay.URL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
for _, relay := range relays {
|
||||||
|
relay.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
filter := nostr.Filter{}
|
filter := nostr.Filter{}
|
||||||
|
|
||||||
if authors := c.StringSlice("author"); len(authors) > 0 {
|
if authors := c.StringSlice("author"); len(authors) > 0 {
|
||||||
@@ -84,7 +112,7 @@ var count = &cli.Command{
|
|||||||
tags := make([][]string, 0, 5)
|
tags := make([][]string, 0, 5)
|
||||||
for _, tagFlag := range c.StringSlice("tag") {
|
for _, tagFlag := range c.StringSlice("tag") {
|
||||||
spl := strings.SplitN(tagFlag, "=", 2)
|
spl := strings.SplitN(tagFlag, "=", 2)
|
||||||
if len(spl) == 2 && len(spl[0]) == 1 {
|
if len(spl) == 2 {
|
||||||
tags = append(tags, spl)
|
tags = append(tags, spl)
|
||||||
} else {
|
} else {
|
||||||
return fmt.Errorf("invalid --tag '%s'", tagFlag)
|
return fmt.Errorf("invalid --tag '%s'", tagFlag)
|
||||||
@@ -118,26 +146,35 @@ var count = &cli.Command{
|
|||||||
filter.Limit = int(limit)
|
filter.Limit = int(limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
relays := c.Args().Slice()
|
|
||||||
successes := 0
|
successes := 0
|
||||||
failures := make([]error, 0, len(relays))
|
if len(relayUrls) > 0 {
|
||||||
if len(relays) > 0 {
|
var hll *hyperloglog.HyperLogLog
|
||||||
for _, relayUrl := range relays {
|
if offset := nip45.HyperLogLogEventPubkeyOffsetForFilter(filter); offset != -1 && len(relayUrls) > 1 {
|
||||||
relay, err := nostr.RelayConnect(ctx, relayUrl)
|
hll = hyperloglog.New(offset)
|
||||||
|
}
|
||||||
|
for _, relayUrl := range relayUrls {
|
||||||
|
relay, _ := sys.Pool.EnsureRelay(relayUrl)
|
||||||
|
count, hllRegisters, err := relay.Count(ctx, nostr.Filters{filter})
|
||||||
|
fmt.Fprintf(os.Stderr, "%s%s: ", strings.Repeat(" ", biggerUrlSize-len(relayUrl)), relayUrl)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
failures = append(failures, err)
|
fmt.Fprintf(os.Stderr, "❌ %s\n", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
count, err := relay.Count(ctx, nostr.Filters{filter})
|
|
||||||
if err != nil {
|
var hasHLLStr string
|
||||||
failures = append(failures, err)
|
if hll != nil && len(hllRegisters) == 256 {
|
||||||
continue
|
hll.MergeRegisters(hllRegisters)
|
||||||
|
hasHLLStr = " 📋"
|
||||||
}
|
}
|
||||||
fmt.Printf("%s: %d\n", relay.URL, count)
|
|
||||||
|
fmt.Fprintf(os.Stderr, "%d%s\n", count, hasHLLStr)
|
||||||
successes++
|
successes++
|
||||||
}
|
}
|
||||||
if successes == 0 {
|
if successes == 0 {
|
||||||
return errors.Join(failures...)
|
return fmt.Errorf("all relays have failed")
|
||||||
|
} else if hll != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "📋 HyperLogLog sum: %d\n", hll.Count())
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// no relays given, will just print the filter
|
// no relays given, will just print the filter
|
||||||
|
|||||||
23
decode.go
23
decode.go
@@ -3,13 +3,12 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/fiatjaf/cli/v3"
|
||||||
"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"
|
"github.com/nbd-wtf/go-nostr/sdk"
|
||||||
"github.com/urfave/cli/v3"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var decode = &cli.Command{
|
var decode = &cli.Command{
|
||||||
@@ -20,6 +19,7 @@ var decode = &cli.Command{
|
|||||||
nak decode nevent1qqs29yet5tp0qq5xu5qgkeehkzqh5qu46739axzezcxpj4tjlkx9j7gpr4mhxue69uhkummnw3ez6ur4vgh8wetvd3hhyer9wghxuet5sh59ud
|
nak decode nevent1qqs29yet5tp0qq5xu5qgkeehkzqh5qu46739axzezcxpj4tjlkx9j7gpr4mhxue69uhkummnw3ez6ur4vgh8wetvd3hhyer9wghxuet5sh59ud
|
||||||
nak decode nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpz4mhxue69uhk2er9dchxummnw3ezumrpdejqz8thwden5te0dehhxarj94c82c3wwajkcmr0wfjx2u3wdejhgqgcwaehxw309aex2mrp0yhxummnw3exzarf9e3k7mgnp0sh5
|
nak decode nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpz4mhxue69uhk2er9dchxummnw3ezumrpdejqz8thwden5te0dehhxarj94c82c3wwajkcmr0wfjx2u3wdejhgqgcwaehxw309aex2mrp0yhxummnw3exzarf9e3k7mgnp0sh5
|
||||||
nak decode nsec1jrmyhtjhgd9yqalps8hf9mayvd58852gtz66m7tqpacjedkp6kxq4dyxsr`,
|
nak decode nsec1jrmyhtjhgd9yqalps8hf9mayvd58852gtz66m7tqpacjedkp6kxq4dyxsr`,
|
||||||
|
DisableSliceFlagSeparator: true,
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
&cli.BoolFlag{
|
&cli.BoolFlag{
|
||||||
Name: "id",
|
Name: "id",
|
||||||
@@ -55,11 +55,22 @@ var decode = &cli.Command{
|
|||||||
}
|
}
|
||||||
} else if evp := sdk.InputToEventPointer(input); evp != nil {
|
} else if evp := sdk.InputToEventPointer(input); evp != nil {
|
||||||
decodeResult = DecodeResult{EventPointer: evp}
|
decodeResult = DecodeResult{EventPointer: evp}
|
||||||
|
if c.Bool("id") {
|
||||||
|
stdout(evp.ID)
|
||||||
|
continue
|
||||||
|
}
|
||||||
} else if pp := sdk.InputToProfile(ctx, input); pp != nil {
|
} else if pp := sdk.InputToProfile(ctx, input); pp != nil {
|
||||||
decodeResult = DecodeResult{ProfilePointer: pp}
|
decodeResult = DecodeResult{ProfilePointer: pp}
|
||||||
|
if c.Bool("pubkey") {
|
||||||
|
stdout(pp.PublicKey)
|
||||||
|
continue
|
||||||
|
}
|
||||||
} 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)
|
if ep, ok := value.(nostr.EntityPointer); ok {
|
||||||
decodeResult = DecodeResult{EntityPointer: &ep}
|
decodeResult = DecodeResult{EntityPointer: &ep}
|
||||||
|
} else {
|
||||||
|
ctx = lineProcessingError(ctx, "couldn't decode naddr: %s", err)
|
||||||
|
}
|
||||||
} else if prefix, value, err := nip19.Decode(input); err == nil && prefix == "nsec" {
|
} else if prefix, value, err := nip19.Decode(input); err == nil && prefix == "nsec" {
|
||||||
decodeResult.PrivateKey.PrivateKey = value.(string)
|
decodeResult.PrivateKey.PrivateKey = value.(string)
|
||||||
decodeResult.PrivateKey.PublicKey, _ = nostr.GetPublicKey(value.(string))
|
decodeResult.PrivateKey.PublicKey, _ = nostr.GetPublicKey(value.(string))
|
||||||
@@ -68,6 +79,10 @@ var decode = &cli.Command{
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if c.Bool("pubkey") || c.Bool("id") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
stdout(decodeResult.JSON())
|
stdout(decodeResult.JSON())
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
33
encode.go
33
encode.go
@@ -4,9 +4,9 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/fiatjaf/cli/v3"
|
||||||
"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/v3"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var encode = &cli.Command{
|
var encode = &cli.Command{
|
||||||
@@ -25,10 +25,12 @@ var encode = &cli.Command{
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
|
DisableSliceFlagSeparator: true,
|
||||||
Commands: []*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",
|
||||||
|
DisableSliceFlagSeparator: true,
|
||||||
Action: func(ctx context.Context, c *cli.Command) 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 {
|
||||||
@@ -50,6 +52,7 @@ var encode = &cli.Command{
|
|||||||
{
|
{
|
||||||
Name: "nsec",
|
Name: "nsec",
|
||||||
Usage: "encode a hex private key into bech32 'nsec' format",
|
Usage: "encode a hex private key into bech32 'nsec' format",
|
||||||
|
DisableSliceFlagSeparator: true,
|
||||||
Action: func(ctx context.Context, c *cli.Command) 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 {
|
||||||
@@ -78,6 +81,7 @@ var encode = &cli.Command{
|
|||||||
Usage: "attach relay hints to nprofile code",
|
Usage: "attach relay hints to nprofile code",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
DisableSliceFlagSeparator: true,
|
||||||
Action: func(ctx context.Context, c *cli.Command) 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 {
|
||||||
@@ -116,6 +120,7 @@ var encode = &cli.Command{
|
|||||||
Usage: "attach an author pubkey as a hint to the nevent code",
|
Usage: "attach an author pubkey as a hint to the nevent code",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
DisableSliceFlagSeparator: true,
|
||||||
Action: func(ctx context.Context, c *cli.Command) 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 {
|
||||||
@@ -148,7 +153,7 @@ var encode = &cli.Command{
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "naddr",
|
Name: "naddr",
|
||||||
Usage: "generate codes for NIP-33 parameterized replaceable events",
|
Usage: "generate codes for addressable events",
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: "identifier",
|
Name: "identifier",
|
||||||
@@ -174,6 +179,7 @@ var encode = &cli.Command{
|
|||||||
Usage: "attach relay hints to naddr code",
|
Usage: "attach relay hints to naddr code",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
DisableSliceFlagSeparator: true,
|
||||||
Action: func(ctx context.Context, c *cli.Command) 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")
|
||||||
@@ -183,7 +189,7 @@ var encode = &cli.Command{
|
|||||||
|
|
||||||
kind := c.Int("kind")
|
kind := c.Int("kind")
|
||||||
if kind < 30000 || kind >= 40000 {
|
if kind < 30000 || kind >= 40000 {
|
||||||
return fmt.Errorf("kind must be between 30000 and 39999, as per NIP-16, got %d", kind)
|
return fmt.Errorf("kind must be between 30000 and 39999, got %d", kind)
|
||||||
}
|
}
|
||||||
|
|
||||||
if d == "" {
|
if d == "" {
|
||||||
@@ -206,27 +212,6 @@ var encode = &cli.Command{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exitIfLineProcessingError(ctx)
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "note",
|
|
||||||
Usage: "generate note1 event codes (not recommended)",
|
|
||||||
Action: func(ctx context.Context, c *cli.Command) error {
|
|
||||||
for target := range getStdinLinesOrArguments(c.Args()) {
|
|
||||||
if ok := nostr.IsValid32ByteHex(target); !ok {
|
|
||||||
ctx = lineProcessingError(ctx, "invalid event id: %s", target)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if note, err := nip19.EncodeNote(target); err == nil {
|
|
||||||
stdout(note)
|
|
||||||
} else {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exitIfLineProcessingError(ctx)
|
exitIfLineProcessingError(ctx)
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
|
|||||||
140
encrypt_decrypt.go
Normal file
140
encrypt_decrypt.go
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/fiatjaf/cli/v3"
|
||||||
|
"github.com/nbd-wtf/go-nostr"
|
||||||
|
"github.com/nbd-wtf/go-nostr/nip04"
|
||||||
|
)
|
||||||
|
|
||||||
|
var encrypt = &cli.Command{
|
||||||
|
Name: "encrypt",
|
||||||
|
Usage: "encrypts a string with nip44 (or nip04 if specified using a flag) and returns the resulting ciphertext as base64",
|
||||||
|
ArgsUsage: "[plaintext string]",
|
||||||
|
DisableSliceFlagSeparator: true,
|
||||||
|
Flags: append(
|
||||||
|
defaultKeyFlags,
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "recipient-pubkey",
|
||||||
|
Aliases: []string{"p", "tgt", "target", "pubkey"},
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "nip04",
|
||||||
|
Usage: "use nip04 encryption instead of nip44",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
|
target := c.String("recipient-pubkey")
|
||||||
|
if !nostr.IsValidPublicKey(target) {
|
||||||
|
return fmt.Errorf("target %s is not a valid public key", target)
|
||||||
|
}
|
||||||
|
|
||||||
|
plaintext := c.Args().First()
|
||||||
|
|
||||||
|
if c.Bool("nip04") {
|
||||||
|
sec, bunker, err := gatherSecretKeyOrBunkerFromArguments(ctx, c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if bunker != nil {
|
||||||
|
ciphertext, err := bunker.NIP04Encrypt(ctx, target, plaintext)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
stdout(ciphertext)
|
||||||
|
} else {
|
||||||
|
ss, err := nip04.ComputeSharedSecret(target, sec)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to compute nip04 shared secret: %w", err)
|
||||||
|
}
|
||||||
|
ciphertext, err := nip04.Encrypt(plaintext, ss)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to encrypt as nip04: %w", err)
|
||||||
|
}
|
||||||
|
stdout(ciphertext)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
kr, _, err := gatherKeyerFromArguments(ctx, c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := kr.Encrypt(ctx, plaintext, target)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to encrypt: %w", err)
|
||||||
|
}
|
||||||
|
stdout(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var decrypt = &cli.Command{
|
||||||
|
Name: "decrypt",
|
||||||
|
Usage: "decrypts a base64 nip44 ciphertext (or nip04 if specified using a flag) and returns the resulting plaintext",
|
||||||
|
ArgsUsage: "[ciphertext base64]",
|
||||||
|
DisableSliceFlagSeparator: true,
|
||||||
|
Flags: append(
|
||||||
|
defaultKeyFlags,
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "sender-pubkey",
|
||||||
|
Aliases: []string{"p", "src", "source", "pubkey"},
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "nip04",
|
||||||
|
Usage: "use nip04 encryption instead of nip44",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
|
source := c.String("sender-pubkey")
|
||||||
|
if !nostr.IsValidPublicKey(source) {
|
||||||
|
return fmt.Errorf("source %s is not a valid public key", source)
|
||||||
|
}
|
||||||
|
|
||||||
|
ciphertext := c.Args().First()
|
||||||
|
|
||||||
|
if c.Bool("nip04") {
|
||||||
|
sec, bunker, err := gatherSecretKeyOrBunkerFromArguments(ctx, c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if bunker != nil {
|
||||||
|
plaintext, err := bunker.NIP04Decrypt(ctx, source, ciphertext)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
stdout(plaintext)
|
||||||
|
} else {
|
||||||
|
ss, err := nip04.ComputeSharedSecret(source, sec)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to compute nip04 shared secret: %w", err)
|
||||||
|
}
|
||||||
|
plaintext, err := nip04.Decrypt(ciphertext, ss)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to encrypt as nip04: %w", err)
|
||||||
|
}
|
||||||
|
stdout(plaintext)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
kr, _, err := gatherKeyerFromArguments(ctx, c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := kr.Decrypt(ctx, ciphertext, source)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to encrypt: %w", err)
|
||||||
|
}
|
||||||
|
stdout(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
149
event.go
149
event.go
@@ -2,22 +2,24 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/fiatjaf/cli/v3"
|
||||||
"github.com/mailru/easyjson"
|
"github.com/mailru/easyjson"
|
||||||
"github.com/nbd-wtf/go-nostr"
|
"github.com/nbd-wtf/go-nostr"
|
||||||
|
"github.com/nbd-wtf/go-nostr/nip13"
|
||||||
"github.com/nbd-wtf/go-nostr/nip19"
|
"github.com/nbd-wtf/go-nostr/nip19"
|
||||||
"github.com/nbd-wtf/go-nostr/nson"
|
|
||||||
"github.com/urfave/cli/v3"
|
|
||||||
"golang.org/x/exp/slices"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const CATEGORY_EVENT_FIELDS = "EVENT FIELDS"
|
const (
|
||||||
|
CATEGORY_EVENT_FIELDS = "EVENT FIELDS"
|
||||||
|
CATEGORY_SIGNER = "SIGNER OPTIONS"
|
||||||
|
CATEGORY_EXTRAS = "EXTRAS"
|
||||||
|
)
|
||||||
|
|
||||||
var event = &cli.Command{
|
var event = &cli.Command{
|
||||||
Name: "event",
|
Name: "event",
|
||||||
@@ -33,26 +35,8 @@ if an event -- or a partial event -- is given on stdin, the flags can be used to
|
|||||||
example:
|
example:
|
||||||
echo '{"id":"a889df6a387419ff204305f4c2d296ee328c3cd4f8b62f205648a541b4554dfb","pubkey":"c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5","created_at":1698623783,"kind":1,"tags":[],"content":"hello from the nostr army knife","sig":"84876e1ee3e726da84e5d195eb79358b2b3eaa4d9bd38456fde3e8a2af3f1cd4cda23f23fda454869975b3688797d4c66e12f4c51c1b43c6d2997c5e61865661"}' | nak event wss://offchain.pub
|
echo '{"id":"a889df6a387419ff204305f4c2d296ee328c3cd4f8b62f205648a541b4554dfb","pubkey":"c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5","created_at":1698623783,"kind":1,"tags":[],"content":"hello from the nostr army knife","sig":"84876e1ee3e726da84e5d195eb79358b2b3eaa4d9bd38456fde3e8a2af3f1cd4cda23f23fda454869975b3688797d4c66e12f4c51c1b43c6d2997c5e61865661"}' | nak event wss://offchain.pub
|
||||||
echo '{"tags": [["t", "spam"]]}' | nak event -c 'this is spam'`,
|
echo '{"tags": [["t", "spam"]]}' | nak event -c 'this is spam'`,
|
||||||
Flags: []cli.Flag{
|
DisableSliceFlagSeparator: true,
|
||||||
&cli.StringFlag{
|
Flags: append(defaultKeyFlags,
|
||||||
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",
|
|
||||||
},
|
|
||||||
// ~ these args are only for the convoluted musig2 signing process
|
// ~ these args are only for the convoluted musig2 signing process
|
||||||
// they will be generally copy-shared-pasted across some manual coordination method between participants
|
// they will be generally copy-shared-pasted across some manual coordination method between participants
|
||||||
&cli.UintFlag{
|
&cli.UintFlag{
|
||||||
@@ -60,6 +44,7 @@ example:
|
|||||||
Usage: "number of signers to use for musig2",
|
Usage: "number of signers to use for musig2",
|
||||||
Value: 1,
|
Value: 1,
|
||||||
DefaultText: "1 -- i.e. do not use musig2 at all",
|
DefaultText: "1 -- i.e. do not use musig2 at all",
|
||||||
|
Category: CATEGORY_SIGNER,
|
||||||
},
|
},
|
||||||
&cli.StringSliceFlag{
|
&cli.StringSliceFlag{
|
||||||
Name: "musig-pubkey",
|
Name: "musig-pubkey",
|
||||||
@@ -78,23 +63,27 @@ example:
|
|||||||
Hidden: true,
|
Hidden: true,
|
||||||
},
|
},
|
||||||
// ~~~
|
// ~~~
|
||||||
|
&cli.UintFlag{
|
||||||
|
Name: "pow",
|
||||||
|
Usage: "nip13 difficulty to target when doing hash work on the event id",
|
||||||
|
Category: CATEGORY_EXTRAS,
|
||||||
|
},
|
||||||
&cli.BoolFlag{
|
&cli.BoolFlag{
|
||||||
Name: "envelope",
|
Name: "envelope",
|
||||||
Usage: "print the event enveloped in a [\"EVENT\", ...] message ready to be sent to a relay",
|
Usage: "print the event enveloped in a [\"EVENT\", ...] message ready to be sent to a relay",
|
||||||
|
Category: CATEGORY_EXTRAS,
|
||||||
},
|
},
|
||||||
&cli.BoolFlag{
|
&cli.BoolFlag{
|
||||||
Name: "auth",
|
Name: "auth",
|
||||||
Usage: "always perform NIP-42 \"AUTH\" when facing an \"auth-required: \" rejection and try again",
|
Usage: "always perform nip42 \"AUTH\" when facing an \"auth-required: \" rejection and try again",
|
||||||
|
Category: CATEGORY_EXTRAS,
|
||||||
},
|
},
|
||||||
&cli.BoolFlag{
|
&cli.BoolFlag{
|
||||||
Name: "nevent",
|
Name: "nevent",
|
||||||
Usage: "print the nevent code (to stderr) after the event is published",
|
Usage: "print the nevent code (to stderr) after the event is published",
|
||||||
|
Category: CATEGORY_EXTRAS,
|
||||||
},
|
},
|
||||||
&cli.BoolFlag{
|
&cli.UintFlag{
|
||||||
Name: "nson",
|
|
||||||
Usage: "encode the event using NSON",
|
|
||||||
},
|
|
||||||
&cli.IntFlag{
|
|
||||||
Name: "kind",
|
Name: "kind",
|
||||||
Aliases: []string{"k"},
|
Aliases: []string{"k"},
|
||||||
Usage: "event kind",
|
Usage: "event kind",
|
||||||
@@ -113,7 +102,7 @@ example:
|
|||||||
&cli.StringSliceFlag{
|
&cli.StringSliceFlag{
|
||||||
Name: "tag",
|
Name: "tag",
|
||||||
Aliases: []string{"t"},
|
Aliases: []string{"t"},
|
||||||
Usage: "sets a tag field on the event, takes a value like -t e=<id>",
|
Usage: "sets a tag field on the event, takes a value like -t e=<id> or -t sometag=\"value one;value two;value three\"",
|
||||||
Category: CATEGORY_EVENT_FIELDS,
|
Category: CATEGORY_EVENT_FIELDS,
|
||||||
},
|
},
|
||||||
&cli.StringSliceFlag{
|
&cli.StringSliceFlag{
|
||||||
@@ -131,21 +120,21 @@ example:
|
|||||||
Usage: "shortcut for --tag d=<value>",
|
Usage: "shortcut for --tag d=<value>",
|
||||||
Category: CATEGORY_EVENT_FIELDS,
|
Category: CATEGORY_EVENT_FIELDS,
|
||||||
},
|
},
|
||||||
&cli.StringFlag{
|
&NaturalTimeFlag{
|
||||||
Name: "created-at",
|
Name: "created-at",
|
||||||
Aliases: []string{"time", "ts"},
|
Aliases: []string{"time", "ts"},
|
||||||
Usage: "unix timestamp value for the created_at field",
|
Usage: "unix timestamp value for the created_at field",
|
||||||
DefaultText: "now",
|
DefaultText: "now",
|
||||||
Value: "",
|
Value: nostr.Now(),
|
||||||
Category: CATEGORY_EVENT_FIELDS,
|
Category: CATEGORY_EVENT_FIELDS,
|
||||||
},
|
},
|
||||||
},
|
),
|
||||||
ArgsUsage: "[relay...]",
|
ArgsUsage: "[relay...]",
|
||||||
Action: func(ctx context.Context, c *cli.Command) 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(ctx, relayUrls, false)
|
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 +147,7 @@ example:
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
sec, bunker, err := gatherSecretKeyOrBunkerFromArguments(ctx, c)
|
kr, sec, err := gatherKeyerFromArguments(ctx, c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -182,7 +171,7 @@ example:
|
|||||||
kindWasSupplied = strings.Contains(stdinEvent, `"kind"`)
|
kindWasSupplied = strings.Contains(stdinEvent, `"kind"`)
|
||||||
}
|
}
|
||||||
|
|
||||||
if kind := c.Int("kind"); slices.Contains(c.FlagNames(), "kind") {
|
if kind := c.Uint("kind"); slices.Contains(c.FlagNames(), "kind") {
|
||||||
evt.Kind = int(kind)
|
evt.Kind = int(kind)
|
||||||
mustRehashAndResign = true
|
mustRehashAndResign = true
|
||||||
} else if !kindWasSupplied {
|
} else if !kindWasSupplied {
|
||||||
@@ -190,16 +179,17 @@ example:
|
|||||||
mustRehashAndResign = true
|
mustRehashAndResign = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if content := c.String("content"); content != "" {
|
if c.IsSet("content") {
|
||||||
evt.Content = content
|
evt.Content = c.String("content")
|
||||||
mustRehashAndResign = true
|
mustRehashAndResign = true
|
||||||
} else if evt.Content == "" && evt.Kind == 1 {
|
} else if evt.Content == "" && evt.Kind == 1 {
|
||||||
evt.Content = "hello from the nostr army knife"
|
evt.Content = "hello from the nostr army knife"
|
||||||
mustRehashAndResign = true
|
mustRehashAndResign = true
|
||||||
}
|
}
|
||||||
|
|
||||||
tags := make(nostr.Tags, 0, 5)
|
tagFlags := c.StringSlice("tag")
|
||||||
for _, tagFlag := range c.StringSlice("tag") {
|
tags := make(nostr.Tags, 0, len(tagFlags)+2)
|
||||||
|
for _, tagFlag := range tagFlags {
|
||||||
// tags are in the format key=value
|
// tags are in the format key=value
|
||||||
tagName, tagValue, found := strings.Cut(tagFlag, "=")
|
tagName, tagValue, found := strings.Cut(tagFlag, "=")
|
||||||
tag := []string{tagName}
|
tag := []string{tagName}
|
||||||
@@ -208,20 +198,17 @@ example:
|
|||||||
tagValues := strings.Split(tagValue, ";")
|
tagValues := strings.Split(tagValue, ";")
|
||||||
tag = append(tag, tagValues...)
|
tag = append(tag, tagValues...)
|
||||||
}
|
}
|
||||||
tags = tags.AppendUnique(tag)
|
tags = append(tags, tag)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, etag := range c.StringSlice("e") {
|
for _, etag := range c.StringSlice("e") {
|
||||||
tags = tags.AppendUnique([]string{"e", etag})
|
tags = tags.AppendUnique([]string{"e", etag})
|
||||||
mustRehashAndResign = true
|
|
||||||
}
|
}
|
||||||
for _, ptag := range c.StringSlice("p") {
|
for _, ptag := range c.StringSlice("p") {
|
||||||
tags = tags.AppendUnique([]string{"p", ptag})
|
tags = tags.AppendUnique([]string{"p", ptag})
|
||||||
mustRehashAndResign = true
|
|
||||||
}
|
}
|
||||||
for _, dtag := range c.StringSlice("d") {
|
for _, dtag := range c.StringSlice("d") {
|
||||||
tags = tags.AppendUnique([]string{"d", dtag})
|
tags = tags.AppendUnique([]string{"d", dtag})
|
||||||
mustRehashAndResign = true
|
|
||||||
}
|
}
|
||||||
if len(tags) > 0 {
|
if len(tags) > 0 {
|
||||||
for _, tag := range tags {
|
for _, tag := range tags {
|
||||||
@@ -230,28 +217,43 @@ example:
|
|||||||
mustRehashAndResign = true
|
mustRehashAndResign = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if createdAt := c.String("created-at"); createdAt != "" {
|
if c.IsSet("created-at") {
|
||||||
ts := time.Now()
|
evt.CreatedAt = getNaturalDate(c, "created-at")
|
||||||
if createdAt != "now" {
|
|
||||||
if v, err := strconv.ParseInt(createdAt, 10, 64); err != nil {
|
|
||||||
return fmt.Errorf("failed to parse timestamp '%s': %w", createdAt, err)
|
|
||||||
} else {
|
|
||||||
ts = time.Unix(v, 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
evt.CreatedAt = nostr.Timestamp(ts.Unix())
|
|
||||||
mustRehashAndResign = true
|
mustRehashAndResign = true
|
||||||
} else if evt.CreatedAt == 0 {
|
} else if evt.CreatedAt == 0 {
|
||||||
evt.CreatedAt = nostr.Now()
|
evt.CreatedAt = nostr.Now()
|
||||||
mustRehashAndResign = true
|
mustRehashAndResign = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if evt.Sig == "" || mustRehashAndResign {
|
if c.IsSet("musig") || c.IsSet("sec") || c.IsSet("prompt-sec") {
|
||||||
if bunker != nil {
|
mustRehashAndResign = true
|
||||||
if err := bunker.SignEvent(ctx, &evt); err != nil {
|
|
||||||
return fmt.Errorf("failed to sign with bunker: %w", err)
|
|
||||||
}
|
}
|
||||||
} else if numSigners := c.Uint("musig"); numSigners > 1 && sec != "" {
|
|
||||||
|
if difficulty := c.Uint("pow"); difficulty > 0 {
|
||||||
|
// before doing pow we need the pubkey
|
||||||
|
if numSigners := c.Uint("musig"); numSigners > 1 {
|
||||||
|
pubkeys := c.StringSlice("musig-pubkey")
|
||||||
|
if int(numSigners) != len(pubkeys) {
|
||||||
|
return fmt.Errorf("when doing a pow with musig we must know all signer pubkeys upfront")
|
||||||
|
}
|
||||||
|
evt.PubKey, err = getMusigAggregatedKey(ctx, pubkeys)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
evt.PubKey, _ = kr.GetPublicKey(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// try to generate work with this difficulty -- runs forever
|
||||||
|
nonceTag, _ := nip13.DoWork(ctx, evt, int(difficulty))
|
||||||
|
evt.Tags = append(evt.Tags, nonceTag)
|
||||||
|
|
||||||
|
mustRehashAndResign = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if evt.Sig == "" || mustRehashAndResign {
|
||||||
|
if numSigners := c.Uint("musig"); numSigners > 1 {
|
||||||
|
// must do musig
|
||||||
pubkeys := c.StringSlice("musig-pubkey")
|
pubkeys := c.StringSlice("musig-pubkey")
|
||||||
secNonce := c.String("musig-nonce-secret")
|
secNonce := c.String("musig-nonce-secret")
|
||||||
pubNonces := c.StringSlice("musig-nonce")
|
pubNonces := c.StringSlice("musig-nonce")
|
||||||
@@ -266,7 +268,7 @@ example:
|
|||||||
// instructions for what to do should have been printed by the performMusig() function
|
// instructions for what to do should have been printed by the performMusig() function
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
} else if err := evt.Sign(sec); err != nil {
|
} else if err := kr.SignEvent(ctx, &evt); err != nil {
|
||||||
return fmt.Errorf("error signing with provided key: %w", err)
|
return fmt.Errorf("error signing with provided key: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -276,8 +278,6 @@ example:
|
|||||||
if c.Bool("envelope") {
|
if c.Bool("envelope") {
|
||||||
j, _ := json.Marshal(nostr.EventEnvelope{Event: evt})
|
j, _ := json.Marshal(nostr.EventEnvelope{Event: evt})
|
||||||
result = string(j)
|
result = string(j)
|
||||||
} else if c.Bool("nson") {
|
|
||||||
result, _ = nson.Marshal(&evt)
|
|
||||||
} else {
|
} else {
|
||||||
j, _ := easyjson.Marshal(&evt)
|
j, _ := easyjson.Marshal(&evt)
|
||||||
result = string(j)
|
result = string(j)
|
||||||
@@ -303,23 +303,12 @@ example:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// error publishing
|
// error publishing
|
||||||
if strings.HasPrefix(err.Error(), "msg: auth-required:") && (sec != "" || bunker != nil) && doAuth {
|
if strings.HasPrefix(err.Error(), "msg: auth-required:") && kr != nil && doAuth {
|
||||||
// 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
|
pk, _ := kr.GetPublicKey(ctx)
|
||||||
if bunker != nil {
|
|
||||||
pk, err = bunker.GetPublicKey(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get public key from bunker: %w", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
pk, _ = nostr.GetPublicKey(sec)
|
|
||||||
}
|
|
||||||
log("performing auth as %s... ", pk)
|
log("performing auth as %s... ", pk)
|
||||||
if err := relay.Auth(ctx, func(evt *nostr.Event) error {
|
if err := relay.Auth(ctx, func(authEvent *nostr.Event) error {
|
||||||
if bunker != nil {
|
return kr.SignEvent(ctx, authEvent)
|
||||||
return bunker.SignEvent(ctx, evt)
|
|
||||||
}
|
|
||||||
return evt.Sign(sec)
|
|
||||||
}); err == nil {
|
}); err == nil {
|
||||||
// try to publish again, but this time don't try to auth again
|
// try to publish again, but this time don't try to auth again
|
||||||
doAuth = false
|
doAuth = false
|
||||||
|
|||||||
124
example_test.go
124
example_test.go
@@ -2,9 +2,12 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"os"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// these tests are tricky because commands and flags are declared as globals and values set in one call may persist
|
||||||
|
// to the next. for example, if in the first test we set --limit 2 then doesn't specify --limit in the second then
|
||||||
|
// it will still return true for cmd.IsSet("limit") and then we will set .LimitZero = true
|
||||||
|
|
||||||
var ctx = context.Background()
|
var ctx = context.Background()
|
||||||
|
|
||||||
func ExampleEventBasic() {
|
func ExampleEventBasic() {
|
||||||
@@ -14,22 +17,22 @@ func ExampleEventBasic() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// (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)
|
||||||
func ExampleEventParsingFromStdin() {
|
// func ExampleEventParsingFromStdin() {
|
||||||
prevStdin := os.Stdin
|
// prevStdin := os.Stdin
|
||||||
defer func() { os.Stdin = prevStdin }()
|
// defer func() { os.Stdin = prevStdin }()
|
||||||
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(ctx, []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(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"})
|
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"}
|
// {"kind":11,"id":"19aba166dcf354bf5ef64f4afe69ada1eb851495001ee05e07d393ee8c8ea179","pubkey":"2fa2104d6b38d11b0230010559879124e42ab8dfeff5ff29dc9cdadd4ecacc3f","created_at":1699485669,"tags":[["t","spam"],["r","https://abc.def?name=foobar","nothing"],["e","36d88cf5fcc449f2390a424907023eda7a74278120eebab8d02797cd92e7e29c"]],"content":"skjdbaskd","sig":"cf452def4a68341c897c3fc96fa34dc6895a5b8cc266d4c041bcdf758ec992ec5adb8b0179e98552aaaf9450526a26d7e62e413b15b1c57e0cfc8db6b29215d7"}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ExampleEncode() {
|
func ExampleEncode() {
|
||||||
@@ -64,23 +67,31 @@ func ExampleDecode() {
|
|||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ExampleDecodePubkey() {
|
||||||
|
app.Run(ctx, []string{"nak", "decode", "-p", "npub10xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqpkge6d", "npub1ccz8l9zpa47k6vz9gphftsrumpw80rjt3nhnefat4symjhrsnmjs38mnyd"})
|
||||||
|
// Output:
|
||||||
|
// 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798
|
||||||
|
// c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleDecodeEventId() {
|
||||||
|
app.Run(ctx, []string{"nak", "decode", "-e", "nevent1qyd8wumn8ghj7urewfsk66ty9enxjct5dfskvtnrdakj7qgmwaehxw309aex2mrp0yh8wetnw3jhymnzw33jucm0d5hszxthwden5te0wfjkccte9eekummjwsh8xmmrd9skctcpzamhxue69uhkzarvv9ejumn0wd68ytnvv9hxgtcqyqllp5v5j0nxr74fptqxkhvfv0h3uj870qpk3ln8a58agyxl3fka296ewr8", "nevent1qqswh48lurxs8u0pll9qj2rzctvjncwhstpzlstq59rdtzlty79awns5hl5uf"})
|
||||||
|
// Output:
|
||||||
|
// 3ff0d19493e661faa90ac06b5d8963ef1e48fe780368fe67ed0fd410df8a6dd5
|
||||||
|
// ebd4ffe0cd03f1e1ffca092862c2d929e1d782c22fc160a146d58beb278bd74e
|
||||||
|
}
|
||||||
|
|
||||||
func ExampleReq() {
|
func ExampleReq() {
|
||||||
app.Run(ctx, []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() {
|
|
||||||
app.Run(ctx, []string{"nak", "req", "-i", "3ff0d19493e661faa90ac06b5d8963ef1e48fe780368fe67ed0fd410df8a6dd5", "wss://nostr.wine"})
|
|
||||||
// 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"}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExampleMultipleFetch() {
|
func ExampleMultipleFetch() {
|
||||||
app.Run(ctx, []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"}
|
// {"kind":31923,"id":"9ae5014573fc75ced00b343868d2cd9343ebcbbae50591c6fa8ae1cd99568f05","pubkey":"5b0183ab6c3e322bf4d41c6b3aef98562a144847b7499543727c5539a114563e","created_at":1707764605,"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"}
|
// {"kind":1,"id":"3ff0d19493e661faa90ac06b5d8963ef1e48fe780368fe67ed0fd410df8a6dd5","pubkey":"3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d","created_at":1710759386,"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() {
|
||||||
@@ -91,50 +102,37 @@ func ExampleKeyPublic() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ExampleKeyDecrypt() {
|
func ExampleKeyDecrypt() {
|
||||||
app.Run(ctx, []string{"nak", "key", "decrypt", "ncryptsec1qggfep0m5ythsegkmwfrhhx2zx5gazyhdygvlngcds4wsgdpzfy6nr0exy0pdk0ydwrqyhndt2trtwcgwwag0ja3aqclzptfxxqvprdyaz3qfrmazpecx2ff6dph5mfdjnh5sw8sgecul32eru6xet34", "banana"})
|
app.Run(ctx, []string{"nak", "key", "decrypt", "ncryptsec1qgg2gx2a7hxpsse2zulrv7m8qwccvl3mh8e9k8vtz3wpyrwuuclaq73gz7ddt5kpa93qyfhfjakguuf8uhw90jn6mszh7kqeh9mxzlyw8hy75fluzx4h75frwmu2yngsq7hx7w32d0vdyxyns5g6rqft", "banana"})
|
||||||
// Output:
|
// Output:
|
||||||
// nsec180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsgyumg0
|
// 718d756f60cf5179ef35b39dc6db3ff58f04c0734f81f6d4410f0b047ddf9029
|
||||||
}
|
}
|
||||||
|
|
||||||
func ExampleRelay() {
|
func ExampleReqIdFromRelay() {
|
||||||
app.Run(ctx, []string{"nak", "relay", "relay.nos.social", "pyramid.fiatjaf.com"})
|
app.Run(ctx, []string{"nak", "req", "-i", "20a6606ed548fe7107533cf3416ce1aa5e957c315c2a40249e12bd9873dca7da", "--limit", "1", "nos.lol"})
|
||||||
// Output:
|
// Output:
|
||||||
// {
|
// {"kind":1,"id":"20a6606ed548fe7107533cf3416ce1aa5e957c315c2a40249e12bd9873dca7da","pubkey":"dd664d5e4016433a8cd69f005ae1480804351789b59de5af06276de65633d319","created_at":1720972243,"tags":[["e","bdb2210fe6d9c4b141f08b5d9d1147cd8e1dc1d82f552a889ab171894249d21d","","root"],["e","c2e45f09e7d62ed12afe2b8b1bcf6be823b560a53ef06905365a78979a1b9ee3","","reply"],["p","036533caa872376946d4e4fdea4c1a0441eda38ca2d9d9417bb36006cbaabf58","","mention"]],"content":"Yeah, so bizarre, but I guess most people are meant to be serfs.","sig":"9ea7488415c250d0ac8fcb2219f211cb369dddf2a75c0f63d2db773c6dc1ef9dd9679b8941c0e7551744ea386afebad2024be8ce3ac418d4f47c95e7491af38e"}
|
||||||
// "name": "nos.social strfry relay",
|
}
|
||||||
// "description": "This is a strfry instance handled by nos.social",
|
|
||||||
// "pubkey": "89ef92b9ebe6dc1e4ea398f6477f227e95429627b0a33dc89b640e137b256be5",
|
func ExampleReqWithFlagsAfter1() {
|
||||||
// "contact": "https://nos.social",
|
app.Run(ctx, []string{"nak", "req", "nos.lol", "-i", "20a6606ed548fe7107533cf3416ce1aa5e957c315c2a40249e12bd9873dca7da", "--limit", "1"})
|
||||||
// "supported_nips": [
|
// Output:
|
||||||
// 1,
|
// {"kind":1,"id":"20a6606ed548fe7107533cf3416ce1aa5e957c315c2a40249e12bd9873dca7da","pubkey":"dd664d5e4016433a8cd69f005ae1480804351789b59de5af06276de65633d319","created_at":1720972243,"tags":[["e","bdb2210fe6d9c4b141f08b5d9d1147cd8e1dc1d82f552a889ab171894249d21d","","root"],["e","c2e45f09e7d62ed12afe2b8b1bcf6be823b560a53ef06905365a78979a1b9ee3","","reply"],["p","036533caa872376946d4e4fdea4c1a0441eda38ca2d9d9417bb36006cbaabf58","","mention"]],"content":"Yeah, so bizarre, but I guess most people are meant to be serfs.","sig":"9ea7488415c250d0ac8fcb2219f211cb369dddf2a75c0f63d2db773c6dc1ef9dd9679b8941c0e7551744ea386afebad2024be8ce3ac418d4f47c95e7491af38e"}
|
||||||
// 2,
|
}
|
||||||
// 4,
|
|
||||||
// 9,
|
func ExampleReqWithFlagsAfter2() {
|
||||||
// 11,
|
app.Run(ctx, []string{"nak", "req", "-e", "893d4c10f1c230240812c6bdf9ad877eed1e29e87029d153820c24680bb183b1", "nostr.mom", "--author", "2a7dcf382bcc96a393ada5c975f500393b3f7be6e466bff220aa161ad6b15eb6", "--limit", "1", "-k", "7"})
|
||||||
// 12,
|
// Output:
|
||||||
// 16,
|
// {"kind":7,"id":"9b4868b068ea34ae51092807586c4541b3569d9efc23862aea48ef13de275857","pubkey":"2a7dcf382bcc96a393ada5c975f500393b3f7be6e466bff220aa161ad6b15eb6","created_at":1720987327,"tags":[["e","893d4c10f1c230240812c6bdf9ad877eed1e29e87029d153820c24680bb183b1"],["p","1e978baae414eee990dba992871549ad4a099b9d6f7e71c8059b254ea024dddc"],["k","1"]],"content":"❤️","sig":"7eddd112c642ecdb031330dadc021790642b3c10ecc64158ba3ae63edd798b26afb9b5a3bba72835ce171719a724de1472f65c9b3339b6bead0ce2846f93dfc9"}
|
||||||
// 20,
|
}
|
||||||
// 22,
|
|
||||||
// 28,
|
func ExampleReqWithFlagsAfter3() {
|
||||||
// 33,
|
app.Run(ctx, []string{"nak", "req", "--limit", "1", "pyramid.fiatjaf.com", "-a", "3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24", "-qp", "3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24", "-e", "9f3c1121c96edf17d84b9194f74d66d012b28c4e25b3ef190582c76b8546a188"})
|
||||||
// 40
|
// Output:
|
||||||
// ],
|
// {"kind":1,"id":"101572c80ebdc963dab8440f6307387a3023b6d90f7e495d6c5ee1ef77045a67","pubkey":"3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24","created_at":1720987305,"tags":[["e","ceacdc29fa7a0b51640b30d2424e188215460617db5ba5bb52d3fbf0094eebb3","","root"],["e","9f3c1121c96edf17d84b9194f74d66d012b28c4e25b3ef190582c76b8546a188","","reply"],["p","3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24"],["p","6b96c3eb36c6cd457d906bbaafe7b36cacfb8bcc4ab235be6eab3b71c6669251"]],"content":"Nope. I grew up playing in the woods. Never once saw a bear in the woods. If I did, I'd probably shiy my pants, then scream at it like I was a crazy person with my arms above my head to make me seem huge.","sig":"b098820b4a5635865cada9f9a5813be2bc6dd7180e16e590cf30e07916d8ed6ed98ab38b64f3bfba12d88d37335f229f7ef8c084bc48132e936c664a54d3e650"}
|
||||||
// "software": "git+https://github.com/hoytech/strfry.git",
|
}
|
||||||
// "version": "0.9.4",
|
|
||||||
// "icon": ""
|
func ExampleNaturalTimestamps() {
|
||||||
// }
|
app.Run(ctx, []string{"nak", "event", "-t", "plu=pla", "-e", "3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24", "--ts", "May 19 2018 03:37:19", "-c", "nn"})
|
||||||
// {
|
// Output:
|
||||||
// "name": "the fiatjaf pyramid",
|
// {"kind":0,"id":"b10da0095f96aa2accd99fa3d93bf29a76f51d2594cf5a0a52f8e961aecd0b67","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1526711839,"tags":[["plu","pla"],["e","3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24"]],"content":"nn","sig":"988442c97064a041ba5e2bfbd64e84d3f819b2169e865511d9d53e74667949ff165325942acaa2ca233c8b529adedf12cf44088cf04081b56d098c5f4d52dd8f"}
|
||||||
// "description": "a relay just for the coolest of the coolest",
|
|
||||||
// "pubkey": "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d",
|
|
||||||
// "contact": "",
|
|
||||||
// "supported_nips": [],
|
|
||||||
// "software": "https://github.com/fiatjaf/khatru",
|
|
||||||
// "version": "n/a",
|
|
||||||
// "limitation": {
|
|
||||||
// "auth_required": false,
|
|
||||||
// "payment_required": false,
|
|
||||||
// "restricted_writes": true
|
|
||||||
// },
|
|
||||||
// "icon": "https://clipart-library.com/images_k/pyramid-transparent/pyramid-transparent-19.png"
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|||||||
63
fetch.go
63
fetch.go
@@ -2,32 +2,33 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/fiatjaf/cli/v3"
|
||||||
"github.com/nbd-wtf/go-nostr"
|
"github.com/nbd-wtf/go-nostr"
|
||||||
|
"github.com/nbd-wtf/go-nostr/nip05"
|
||||||
"github.com/nbd-wtf/go-nostr/nip19"
|
"github.com/nbd-wtf/go-nostr/nip19"
|
||||||
sdk "github.com/nbd-wtf/nostr-sdk"
|
"github.com/nbd-wtf/go-nostr/sdk/hints"
|
||||||
"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 or the author's NIP-65 relays.",
|
Usage: "fetches events related to the given nip19 or nip05 code from the included relay hints or the author's outbox 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`,
|
||||||
Flags: []cli.Flag{
|
DisableSliceFlagSeparator: true,
|
||||||
|
Flags: append(reqFilterFlags,
|
||||||
&cli.StringSliceFlag{
|
&cli.StringSliceFlag{
|
||||||
Name: "relay",
|
Name: "relay",
|
||||||
Aliases: []string{"r"},
|
Aliases: []string{"r"},
|
||||||
Usage: "also use these relays to fetch from",
|
Usage: "also use these relays to fetch from",
|
||||||
},
|
},
|
||||||
},
|
),
|
||||||
ArgsUsage: "[nip19code]",
|
ArgsUsage: "[nip05_or_nip19_code]",
|
||||||
Action: func(ctx context.Context, c *cli.Command) error {
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
pool := nostr.NewSimplePool(ctx)
|
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
pool.Relays.Range(func(_ string, relay *nostr.Relay) bool {
|
sys.Pool.Relays.Range(func(_ string, relay *nostr.Relay) bool {
|
||||||
relay.Close()
|
relay.Close()
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
@@ -35,18 +36,28 @@ var fetch = &cli.Command{
|
|||||||
|
|
||||||
for code := range getStdinLinesOrArguments(c.Args()) {
|
for code := range getStdinLinesOrArguments(c.Args()) {
|
||||||
filter := nostr.Filter{}
|
filter := nostr.Filter{}
|
||||||
|
var authorHint string
|
||||||
|
relays := c.StringSlice("relay")
|
||||||
|
|
||||||
|
if nip05.IsValidIdentifier(code) {
|
||||||
|
pp, err := nip05.QueryIdentifier(ctx, code)
|
||||||
|
if err != nil {
|
||||||
|
ctx = lineProcessingError(ctx, "failed to fetch nip05: %s", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
authorHint = pp.PublicKey
|
||||||
|
relays = append(relays, pp.Relays...)
|
||||||
|
filter.Authors = append(filter.Authors, pp.PublicKey)
|
||||||
|
} else {
|
||||||
prefix, value, err := nip19.Decode(code)
|
prefix, value, err := nip19.Decode(code)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx = lineProcessingError(ctx, "failed to decode: %s", err)
|
ctx = lineProcessingError(ctx, "failed to decode: %s", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
relays := c.StringSlice("relay")
|
|
||||||
if err := normalizeAndValidateRelayURLs(relays); err != nil {
|
if err := normalizeAndValidateRelayURLs(relays); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
var authorHint string
|
|
||||||
|
|
||||||
switch prefix {
|
switch prefix {
|
||||||
case "nevent":
|
case "nevent":
|
||||||
@@ -56,35 +67,45 @@ var fetch = &cli.Command{
|
|||||||
authorHint = v.Author
|
authorHint = v.Author
|
||||||
}
|
}
|
||||||
relays = append(relays, v.Relays...)
|
relays = append(relays, v.Relays...)
|
||||||
|
case "note":
|
||||||
|
filter.IDs = append(filter.IDs, value.(string))
|
||||||
case "naddr":
|
case "naddr":
|
||||||
v := value.(nostr.EntityPointer)
|
v := value.(nostr.EntityPointer)
|
||||||
|
filter.Kinds = []int{v.Kind}
|
||||||
filter.Tags = nostr.TagMap{"d": []string{v.Identifier}}
|
filter.Tags = nostr.TagMap{"d": []string{v.Identifier}}
|
||||||
filter.Kinds = append(filter.Kinds, v.Kind)
|
|
||||||
filter.Authors = append(filter.Authors, v.PublicKey)
|
filter.Authors = append(filter.Authors, v.PublicKey)
|
||||||
authorHint = v.PublicKey
|
authorHint = v.PublicKey
|
||||||
relays = append(relays, v.Relays...)
|
relays = append(relays, v.Relays...)
|
||||||
case "nprofile":
|
case "nprofile":
|
||||||
v := value.(nostr.ProfilePointer)
|
v := value.(nostr.ProfilePointer)
|
||||||
filter.Authors = append(filter.Authors, v.PublicKey)
|
filter.Authors = append(filter.Authors, v.PublicKey)
|
||||||
filter.Kinds = append(filter.Kinds, 0)
|
|
||||||
authorHint = v.PublicKey
|
authorHint = v.PublicKey
|
||||||
relays = append(relays, v.Relays...)
|
relays = append(relays, v.Relays...)
|
||||||
case "npub":
|
case "npub":
|
||||||
v := value.(string)
|
v := value.(string)
|
||||||
filter.Authors = append(filter.Authors, v)
|
filter.Authors = append(filter.Authors, v)
|
||||||
filter.Kinds = append(filter.Kinds, 0)
|
|
||||||
authorHint = v
|
authorHint = v
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unexpected prefix %s", prefix)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if authorHint != "" {
|
if authorHint != "" {
|
||||||
relayList := sdk.FetchRelaysForPubkey(ctx, pool, authorHint,
|
for _, url := range relays {
|
||||||
"wss://purplepag.es", "wss://relay.damus.io", "wss://relay.noswhere.com",
|
sys.Hints.Save(authorHint, nostr.NormalizeURL(url), hints.LastInHint, nostr.Now())
|
||||||
"wss://nos.lol", "wss://public.relaying.io", "wss://relay.nostr.band")
|
}
|
||||||
for _, relayListItem := range relayList {
|
|
||||||
if relayListItem.Outbox {
|
for _, url := range sys.FetchOutboxRelays(ctx, authorHint, 3) {
|
||||||
relays = append(relays, relayListItem.URL)
|
relays = append(relays, url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := applyFlagsToFilter(c, &filter); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(filter.Authors) > 0 && len(filter.Kinds) == 0 {
|
||||||
|
filter.Kinds = append(filter.Kinds, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(relays) == 0 {
|
if len(relays) == 0 {
|
||||||
@@ -92,7 +113,7 @@ var fetch = &cli.Command{
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
for ie := range pool.SubManyEose(ctx, relays, nostr.Filters{filter}) {
|
for ie := range sys.Pool.SubManyEose(ctx, relays, nostr.Filters{filter}) {
|
||||||
stdout(ie.Event)
|
stdout(ie.Event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
95
flags.go
Normal file
95
flags.go
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/fiatjaf/cli/v3"
|
||||||
|
"github.com/markusmobius/go-dateparser"
|
||||||
|
"github.com/nbd-wtf/go-nostr"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NaturalTimeFlag = cli.FlagBase[nostr.Timestamp, struct{}, naturalTimeValue]
|
||||||
|
|
||||||
|
// wrap to satisfy golang's flag interface.
|
||||||
|
type naturalTimeValue struct {
|
||||||
|
timestamp *nostr.Timestamp
|
||||||
|
hasBeenSet bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ cli.ValueCreator[nostr.Timestamp, struct{}] = naturalTimeValue{}
|
||||||
|
|
||||||
|
// Below functions are to satisfy the ValueCreator interface
|
||||||
|
|
||||||
|
func (t naturalTimeValue) Create(val nostr.Timestamp, p *nostr.Timestamp, c struct{}) cli.Value {
|
||||||
|
*p = val
|
||||||
|
return &naturalTimeValue{
|
||||||
|
timestamp: p,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t naturalTimeValue) ToString(b nostr.Timestamp) string {
|
||||||
|
ts := b.Time()
|
||||||
|
|
||||||
|
if ts.IsZero() {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%v", ts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Timestamp constructor(for internal testing only)
|
||||||
|
func newTimestamp(timestamp nostr.Timestamp) *naturalTimeValue {
|
||||||
|
return &naturalTimeValue{timestamp: ×tamp}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Below functions are to satisfy the flag.Value interface
|
||||||
|
|
||||||
|
// Parses the string value to timestamp
|
||||||
|
func (t *naturalTimeValue) Set(value string) error {
|
||||||
|
var ts time.Time
|
||||||
|
if n, err := strconv.ParseInt(value, 10, 64); err == nil {
|
||||||
|
// when the input is a raw number, treat it as an exact timestamp
|
||||||
|
ts = time.Unix(n, 0)
|
||||||
|
} else if errors.Is(err, strconv.ErrRange) {
|
||||||
|
// this means a huge number, so we should fail
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
// otherwise try to parse it as a human date string in natural language
|
||||||
|
date, err := dateparser.Parse(&dateparser.Configuration{
|
||||||
|
DefaultTimezone: time.Local,
|
||||||
|
CurrentTime: time.Now(),
|
||||||
|
}, value)
|
||||||
|
ts = date.Time
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.timestamp != nil {
|
||||||
|
*t.timestamp = nostr.Timestamp(ts.Unix())
|
||||||
|
}
|
||||||
|
|
||||||
|
t.hasBeenSet = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a readable representation of this value (for usage defaults)
|
||||||
|
func (t *naturalTimeValue) String() string {
|
||||||
|
return fmt.Sprintf("%#v", t.timestamp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value returns the timestamp value stored in the flag
|
||||||
|
func (t *naturalTimeValue) Value() *nostr.Timestamp {
|
||||||
|
return t.timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get returns the flag structure
|
||||||
|
func (t *naturalTimeValue) Get() any {
|
||||||
|
return *t.timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
func getNaturalDate(cmd *cli.Command, name string) nostr.Timestamp {
|
||||||
|
return cmd.Value(name).(nostr.Timestamp)
|
||||||
|
}
|
||||||
70
go.mod
70
go.mod
@@ -1,41 +1,69 @@
|
|||||||
module github.com/fiatjaf/nak
|
module github.com/fiatjaf/nak
|
||||||
|
|
||||||
go 1.21
|
go 1.23.3
|
||||||
|
|
||||||
toolchain go1.21.0
|
toolchain go1.23.4
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/btcsuite/btcd/btcec/v2 v2.3.3
|
github.com/bep/debounce v1.2.1
|
||||||
|
github.com/btcsuite/btcd/btcec/v2 v2.3.4
|
||||||
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/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/fiatjaf/cli/v3 v3.0.0-20240723181502-e7dd498b16ae
|
||||||
github.com/nbd-wtf/go-nostr v0.34.2
|
github.com/fiatjaf/eventstore v0.15.0
|
||||||
github.com/nbd-wtf/nostr-sdk v0.0.5
|
github.com/fiatjaf/khatru v0.15.0
|
||||||
github.com/urfave/cli/v3 v3.0.0-alpha9
|
github.com/json-iterator/go v1.1.12
|
||||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842
|
github.com/mailru/easyjson v0.9.0
|
||||||
|
github.com/mark3labs/mcp-go v0.8.3
|
||||||
|
github.com/markusmobius/go-dateparser v1.2.3
|
||||||
|
github.com/nbd-wtf/go-nostr v0.49.3
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/btcsuite/btcd/btcutil v1.1.3 // indirect
|
fiatjaf.com/lib v0.2.0 // indirect
|
||||||
|
github.com/andybalholm/brotli v1.0.5 // indirect
|
||||||
|
github.com/btcsuite/btcd v0.24.2 // indirect
|
||||||
|
github.com/btcsuite/btcd/btcutil v1.1.5 // indirect
|
||||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect
|
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.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/decred/dcrd/crypto/blake256 v1.0.1 // indirect
|
github.com/coder/websocket v1.8.12 // indirect
|
||||||
github.com/fiatjaf/eventstore v0.2.16 // indirect
|
github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect
|
||||||
github.com/gobwas/httphead v0.1.0 // indirect
|
github.com/dgraph-io/ristretto v1.0.0 // indirect
|
||||||
github.com/gobwas/pool v0.2.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/gobwas/ws v1.4.0 // indirect
|
github.com/elliotchance/pie/v2 v2.7.0 // indirect
|
||||||
|
github.com/elnosh/gonuts v0.3.1-0.20250123162555-7c0381a585e3 // indirect
|
||||||
|
github.com/fasthttp/websocket v1.5.7 // indirect
|
||||||
|
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/graph-gophers/dataloader/v7 v7.1.0 // indirect
|
||||||
|
github.com/hablullah/go-hijri v1.0.2 // indirect
|
||||||
|
github.com/hablullah/go-juliandays v1.0.0 // indirect
|
||||||
|
github.com/jalaali/go-jalaali v0.0.0-20210801064154-80525e88d958 // indirect
|
||||||
github.com/josharian/intern v1.0.0 // indirect
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
|
github.com/klauspost/compress v1.17.11 // indirect
|
||||||
|
github.com/magefile/mage v1.14.0 // indirect
|
||||||
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/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/tidwall/gjson v1.17.1 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
|
github.com/puzpuzpuz/xsync/v3 v3.4.0 // indirect
|
||||||
|
github.com/rs/cors v1.11.1 // indirect
|
||||||
|
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect
|
||||||
|
github.com/tetratelabs/wazero v1.8.0 // indirect
|
||||||
|
github.com/tidwall/gjson v1.18.0 // 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
|
||||||
golang.org/x/crypto v0.7.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
golang.org/x/sys v0.20.0 // indirect
|
github.com/valyala/fasthttp v1.51.0 // indirect
|
||||||
golang.org/x/text v0.8.0 // indirect
|
github.com/wasilibs/go-re2 v1.3.0 // indirect
|
||||||
|
github.com/x448/float16 v0.8.4 // indirect
|
||||||
|
golang.org/x/crypto v0.32.0 // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect
|
||||||
|
golang.org/x/net v0.34.0 // indirect
|
||||||
|
golang.org/x/sys v0.29.0 // indirect
|
||||||
|
golang.org/x/text v0.21.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
replace github.com/urfave/cli/v3 => github.com/fiatjaf/cli/v3 v3.0.0-20240712212113-3a8b0280e2c5
|
|
||||||
|
|||||||
144
go.sum
144
go.sum
@@ -1,15 +1,23 @@
|
|||||||
|
fiatjaf.com/lib v0.2.0 h1:TgIJESbbND6GjOgGHxF5jsO6EMjuAxIzZHPo5DXYexs=
|
||||||
|
fiatjaf.com/lib v0.2.0/go.mod h1:Ycqq3+mJ9jAWu7XjbQI1cVr+OFgnHn79dQR5oTII47g=
|
||||||
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
|
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
|
||||||
|
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
|
||||||
|
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||||
|
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
|
||||||
|
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
|
||||||
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
|
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
|
||||||
github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M=
|
github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M=
|
||||||
github.com/btcsuite/btcd v0.23.0/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY=
|
github.com/btcsuite/btcd v0.23.5-0.20231215221805-96c9fd8078fd/go.mod h1:nm3Bko6zh6bWP60UxwoT5LzdGJsQJaPo6HjduXq9p6A=
|
||||||
|
github.com/btcsuite/btcd v0.24.2 h1:aLmxPguqxza+4ag8R1I2nnJjSu2iFn/kqtHTIImswcY=
|
||||||
|
github.com/btcsuite/btcd v0.24.2/go.mod h1:5C8ChTkl5ejr3WHj8tkQSCmydiMEPB0ZhQhehpq7Dgg=
|
||||||
github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA=
|
github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA=
|
||||||
github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE=
|
github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE=
|
||||||
github.com/btcsuite/btcd/btcec/v2 v2.3.3 h1:6+iXlDKE8RMtKsvK0gshlXIuPbyWM/h84Ensb7o3sC0=
|
github.com/btcsuite/btcd/btcec/v2 v2.3.4 h1:3EJjcN70HCu/mwqlUsGK8GcNVyLVxFDlWurTXGPFfiQ=
|
||||||
github.com/btcsuite/btcd/btcec/v2 v2.3.3/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04=
|
github.com/btcsuite/btcd/btcec/v2 v2.3.4/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04=
|
||||||
github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A=
|
github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A=
|
||||||
github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE=
|
github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE=
|
||||||
github.com/btcsuite/btcd/btcutil v1.1.3 h1:xfbtw8lwpp0G6NwSHb+UE67ryTFHJAiNuipusjXSohQ=
|
github.com/btcsuite/btcd/btcutil v1.1.5 h1:+wER79R5670vs/ZusMTF1yTcRYE5GUsFbdjdisflzM8=
|
||||||
github.com/btcsuite/btcd/btcutil v1.1.3/go.mod h1:UR7dsSJzJUfMmFiiLlIrMq1lS9jh9EdCV7FStZSnpi0=
|
github.com/btcsuite/btcd/btcutil v1.1.5/go.mod h1:PSZZ4UitpLBWzxGd5VGOrLnmOjtPP/a6HaFo12zMs00=
|
||||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
|
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
|
||||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
|
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
|
||||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 h1:59Kx4K6lzOW5w6nFlA0v5+lk/6sjybR934QNHSJZPTQ=
|
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 h1:59Kx4K6lzOW5w6nFlA0v5+lk/6sjybR934QNHSJZPTQ=
|
||||||
@@ -23,37 +31,51 @@ github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku
|
|||||||
github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
|
github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
|
||||||
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
|
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
|
||||||
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
|
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
|
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
|
||||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
|
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
|
||||||
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/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
|
||||||
|
github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
||||||
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=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
|
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
|
||||||
github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y=
|
github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8=
|
||||||
github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
|
github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg=
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg=
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
|
||||||
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/dgraph-io/ristretto v1.0.0 h1:SYG07bONKMlFDUYu5pEu3DGAh8c2OFNzKm6G9J4Si84=
|
||||||
|
github.com/dgraph-io/ristretto v1.0.0/go.mod h1:jTi2FiYEhQ1NsMmA7DeBykizjOuY88NhKBkepyu1jPc=
|
||||||
|
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y=
|
||||||
|
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||||
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
|
github.com/elliotchance/pie/v2 v2.7.0 h1:FqoIKg4uj0G/CrLGuMS9ejnFKa92lxE1dEgBD3pShXg=
|
||||||
|
github.com/elliotchance/pie/v2 v2.7.0/go.mod h1:18t0dgGFH006g4eVdDtWfgFZPQEgl10IoEO8YWEq3Og=
|
||||||
|
github.com/elnosh/gonuts v0.3.1-0.20250123162555-7c0381a585e3 h1:k7evIqJ2BtFn191DgY/b03N2bMYA/iQwzr4f/uHYn20=
|
||||||
|
github.com/elnosh/gonuts v0.3.1-0.20250123162555-7c0381a585e3/go.mod h1:vgZomh4YQk7R3w4ltZc0sHwCmndfHkuX6V4sga/8oNs=
|
||||||
|
github.com/fasthttp/websocket v1.5.7 h1:0a6o2OfeATvtGgoMKleURhLT6JqWPg7fYfWnH4KHau4=
|
||||||
|
github.com/fasthttp/websocket v1.5.7/go.mod h1:bC4fxSono9czeXHQUVKxsC0sNjbm7lPJR04GDFqClfU=
|
||||||
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-20240723181502-e7dd498b16ae h1:0B/1dU3YECIbPoBIRTQ4c0scZCNz9TVHtQpiODGrTTo=
|
||||||
github.com/fiatjaf/cli/v3 v3.0.0-20240712212113-3a8b0280e2c5/go.mod h1:Z1ItyMma7t6I7zHG9OpbExhHQOSkFf/96n+mAZ9MtVI=
|
github.com/fiatjaf/cli/v3 v3.0.0-20240723181502-e7dd498b16ae/go.mod h1:aAWPO4bixZZxPtOnH6K3q4GbQ0jftUNDW9Oa861IRew=
|
||||||
github.com/fiatjaf/eventstore v0.2.16 h1:NR64mnyUT5nJR8Sj2AwJTd1Hqs5kKJcCFO21ggUkvWg=
|
github.com/fiatjaf/eventstore v0.15.0 h1:5UXe0+vIb30/cYcOWipks8nR3g+X8W224TFy5yPzivk=
|
||||||
github.com/fiatjaf/eventstore v0.2.16/go.mod h1:rUc1KhVufVmC+HUOiuPweGAcvG6lEOQCkRCn2Xn5VRA=
|
github.com/fiatjaf/eventstore v0.15.0/go.mod h1:KAsld5BhkmSck48aF11Txu8X+OGNmoabw4TlYVWqInc=
|
||||||
|
github.com/fiatjaf/khatru v0.15.0 h1:0aLWiTrdzoKD4WmW35GWL/Jsn4dACCUw325JKZg/AmI=
|
||||||
|
github.com/fiatjaf/khatru v0.15.0/go.mod h1:GBQJXZpitDatXF9RookRXcWB5zCJclCE4ufDK3jk80g=
|
||||||
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=
|
||||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||||
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
|
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
|
||||||
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
|
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
|
||||||
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
|
||||||
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
|
||||||
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
|
|
||||||
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
|
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||||
@@ -65,24 +87,49 @@ github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW
|
|||||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
|
github.com/graph-gophers/dataloader/v7 v7.1.0 h1:Wn8HGF/q7MNXcvfaBnLEPEFJttVHR8zuEqP1obys/oc=
|
||||||
|
github.com/graph-gophers/dataloader/v7 v7.1.0/go.mod h1:1bKE0Dm6OUcTB/OAuYVOZctgIz7Q3d0XrYtlIzTgg6Q=
|
||||||
|
github.com/hablullah/go-hijri v1.0.2 h1:drT/MZpSZJQXo7jftf5fthArShcaMtsal0Zf/dnmp6k=
|
||||||
|
github.com/hablullah/go-hijri v1.0.2/go.mod h1:OS5qyYLDjORXzK4O1adFw9Q5WfhOcMdAKglDkcTxgWQ=
|
||||||
|
github.com/hablullah/go-juliandays v1.0.0 h1:A8YM7wIj16SzlKT0SRJc9CD29iiaUzpBLzh5hr0/5p0=
|
||||||
|
github.com/hablullah/go-juliandays v1.0.0/go.mod h1:0JOYq4oFOuDja+oospuc61YoX+uNEn7Z6uHYTbBzdGc=
|
||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
|
github.com/jalaali/go-jalaali v0.0.0-20210801064154-80525e88d958 h1:qxLoi6CAcXVzjfvu+KXIXJOAsQB62LXjsfbOaErsVzE=
|
||||||
|
github.com/jalaali/go-jalaali v0.0.0-20210801064154-80525e88d958/go.mod h1:Wqfu7mjUHj9WDzSSPI5KfBclTTEnLveRUFr/ujWnTgE=
|
||||||
github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||||
github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
|
github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
|
||||||
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
|
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
|
||||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
||||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
||||||
|
github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo=
|
||||||
|
github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||||
|
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
|
||||||
|
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
||||||
|
github.com/mark3labs/mcp-go v0.8.3 h1:IzlyN8BaP4YwUMUDqxOGJhGdZXEDQiAPX43dNPgnzrg=
|
||||||
|
github.com/mark3labs/mcp-go v0.8.3/go.mod h1:cjMlBU0cv/cj9kjlgmRhoJ5JREdS7YX83xeIG9Ko/jE=
|
||||||
|
github.com/markusmobius/go-dateparser v1.2.3 h1:TvrsIvr5uk+3v6poDjaicnAFJ5IgtFHgLiuMY2Eb7Nw=
|
||||||
|
github.com/markusmobius/go-dateparser v1.2.3/go.mod h1:cMwQRrBUQlK1UI5TIFHEcvpsMbkWrQLXuaPNMFzuYLk=
|
||||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
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.34.2 h1:9b4qZ29DhQf9xEWN8/7zfDD868r1jFbpjrR3c+BHc+E=
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/nbd-wtf/go-nostr v0.34.2/go.mod h1:NZQkxl96ggbO8rvDpVjcsojJqKTPwqhP4i82O7K5DJs=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
github.com/nbd-wtf/nostr-sdk v0.0.5 h1:rec+FcDizDVO0W25PX0lgYMXvP7zNNOgI3Fu9UCm4BY=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/nbd-wtf/nostr-sdk v0.0.5/go.mod h1:iJJsikesCGLNFZ9dLqhLPDzdt924EagUmdQxT3w2Lmk=
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
|
github.com/nbd-wtf/go-nostr v0.49.3 h1:7tsEdMZOtJ764JuMLffkbhVUi4yyf688dbqArLvItPs=
|
||||||
|
github.com/nbd-wtf/go-nostr v0.49.3/go.mod h1:M50QnhkraC5Ol93v3jqxSMm1aGxUQm5mlmkYw5DJzh8=
|
||||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
@@ -92,36 +139,55 @@ github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5
|
|||||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
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.4.0 h1:DuVBAdXuGFHv8adVXjWWZ63pJq+NRXOWVXlKDBZ+mJ4=
|
||||||
github.com/puzpuzpuz/xsync/v3 v3.1.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
github.com/puzpuzpuz/xsync/v3 v3.4.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||||
|
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
|
||||||
|
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
|
||||||
|
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk=
|
||||||
|
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g=
|
||||||
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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
|
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
|
||||||
github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U=
|
github.com/tetratelabs/wazero v1.8.0 h1:iEKu0d4c2Pd+QSRieYbnQC9yiFlMS9D+Jr0LsRmcF4g=
|
||||||
github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
github.com/tetratelabs/wazero v1.8.0/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs=
|
||||||
|
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||||
|
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||||
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/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
|
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
|
||||||
|
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
|
||||||
|
github.com/wasilibs/go-re2 v1.3.0 h1:LFhBNzoStM3wMie6rN2slD1cuYH2CGiHpvNL3UtcsMw=
|
||||||
|
github.com/wasilibs/go-re2 v1.3.0/go.mod h1:AafrCXVvGRJJOImMajgJ2M7rVmWyisVK7sFshbxnVrg=
|
||||||
|
github.com/wasilibs/nottinygc v0.4.0 h1:h1TJMihMC4neN6Zq+WKpLxgd9xCFMw7O9ETLwY2exJQ=
|
||||||
|
github.com/wasilibs/nottinygc v0.4.0/go.mod h1:oDcIotskuYNMpqMF23l7Z8uzD4TC0WXHK8jetlB3HIo=
|
||||||
|
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||||
|
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||||
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=
|
||||||
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
|
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
|
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA=
|
||||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
|
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
|
||||||
golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||||
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
@@ -134,13 +200,13 @@ golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||||||
golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
|
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
|||||||
164
helpers.go
164
helpers.go
@@ -3,32 +3,40 @@ package main
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
|
"net/http"
|
||||||
|
"net/textproto"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/chzyer/readline"
|
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
|
"github.com/fiatjaf/cli/v3"
|
||||||
|
jsoniter "github.com/json-iterator/go"
|
||||||
"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/sdk"
|
||||||
"github.com/nbd-wtf/go-nostr/nip46"
|
|
||||||
"github.com/nbd-wtf/go-nostr/nip49"
|
|
||||||
"github.com/urfave/cli/v3"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var sys *sdk.System
|
||||||
|
|
||||||
|
var (
|
||||||
|
hintsFilePath string
|
||||||
|
hintsFileExists bool
|
||||||
|
)
|
||||||
|
|
||||||
|
var json = jsoniter.ConfigFastest
|
||||||
|
|
||||||
const (
|
const (
|
||||||
LINE_PROCESSING_ERROR = iota
|
LINE_PROCESSING_ERROR = iota
|
||||||
)
|
)
|
||||||
|
|
||||||
var log = func(msg string, args ...any) {
|
var (
|
||||||
fmt.Fprintf(color.Error, msg, args...)
|
log = func(msg string, args ...any) { fmt.Fprintf(color.Error, msg, args...) }
|
||||||
}
|
logverbose = func(msg string, args ...any) {} // by default do nothing
|
||||||
|
stdout = fmt.Println
|
||||||
var stdout = fmt.Println
|
)
|
||||||
|
|
||||||
func isPiped() bool {
|
func isPiped() bool {
|
||||||
stat, _ := os.Stdin.Stat()
|
stat, _ := os.Stdin.Stat()
|
||||||
@@ -66,7 +74,9 @@ func getStdinLinesOrArgumentsFromSlice(args []string) chan string {
|
|||||||
|
|
||||||
// try the stdin
|
// try the stdin
|
||||||
multi := make(chan string)
|
multi := make(chan string)
|
||||||
writeStdinLinesOrNothing(multi)
|
if !writeStdinLinesOrNothing(multi) {
|
||||||
|
close(multi)
|
||||||
|
}
|
||||||
return multi
|
return multi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,7 +85,7 @@ func writeStdinLinesOrNothing(ch chan string) (hasStdinLines bool) {
|
|||||||
// piped
|
// piped
|
||||||
go func() {
|
go func() {
|
||||||
scanner := bufio.NewScanner(os.Stdin)
|
scanner := bufio.NewScanner(os.Stdin)
|
||||||
scanner.Buffer(make([]byte, 16*1024), 256*1024)
|
scanner.Buffer(make([]byte, 16*1024*1024), 256*1024*1024)
|
||||||
hasEmittedAtLeastOne := false
|
hasEmittedAtLeastOne := false
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
ch <- strings.TrimSpace(scanner.Text())
|
ch <- strings.TrimSpace(scanner.Text())
|
||||||
@@ -120,13 +130,22 @@ func connectToAllRelays(
|
|||||||
relayUrls []string,
|
relayUrls []string,
|
||||||
forcePreAuth bool,
|
forcePreAuth bool,
|
||||||
opts ...nostr.PoolOption,
|
opts ...nostr.PoolOption,
|
||||||
) (*nostr.SimplePool, []*nostr.Relay) {
|
) []*nostr.Relay {
|
||||||
|
sys.Pool = nostr.NewSimplePool(context.Background(),
|
||||||
|
append(opts,
|
||||||
|
nostr.WithEventMiddleware(sys.TrackEventHints),
|
||||||
|
nostr.WithPenaltyBox(),
|
||||||
|
nostr.WithRelayOptions(
|
||||||
|
nostr.WithRequestHeader(http.Header{textproto.CanonicalMIMEHeaderKey("user-agent"): {"nak/s"}}),
|
||||||
|
),
|
||||||
|
)...,
|
||||||
|
)
|
||||||
|
|
||||||
relays := make([]*nostr.Relay, 0, len(relayUrls))
|
relays := make([]*nostr.Relay, 0, len(relayUrls))
|
||||||
pool := nostr.NewSimplePool(ctx, opts...)
|
|
||||||
relayLoop:
|
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 := sys.Pool.EnsureRelay(url); err == nil {
|
||||||
if forcePreAuth {
|
if forcePreAuth {
|
||||||
log("waiting for auth challenge... ")
|
log("waiting for auth challenge... ")
|
||||||
signer := opts[0].(nostr.WithAuthHandler)
|
signer := opts[0].(nostr.WithAuthHandler)
|
||||||
@@ -140,7 +159,7 @@ relayLoop:
|
|||||||
if (*challengeTag)[1] == "" {
|
if (*challengeTag)[1] == "" {
|
||||||
return fmt.Errorf("auth not received yet *****")
|
return fmt.Errorf("auth not received yet *****")
|
||||||
}
|
}
|
||||||
return signer(authEvent)
|
return signer(ctx, nostr.RelayEvent{Event: authEvent, Relay: relay})
|
||||||
}); err == nil {
|
}); err == nil {
|
||||||
// auth succeeded
|
// auth succeeded
|
||||||
break challengeWaitLoop
|
break challengeWaitLoop
|
||||||
@@ -166,7 +185,7 @@ relayLoop:
|
|||||||
log(err.Error() + "\n")
|
log(err.Error() + "\n")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return pool, relays
|
return relays
|
||||||
}
|
}
|
||||||
|
|
||||||
func lineProcessingError(ctx context.Context, msg string, args ...any) context.Context {
|
func lineProcessingError(ctx context.Context, msg string, args ...any) context.Context {
|
||||||
@@ -180,101 +199,6 @@ func exitIfLineProcessingError(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func gatherSecretKeyOrBunkerFromArguments(ctx context.Context, c *cli.Command) (string, *nip46.BunkerClient, error) {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
if bunkerURL := c.String("connect"); bunkerURL != "" {
|
|
||||||
clientKey := c.String("connect-as")
|
|
||||||
if clientKey != "" {
|
|
||||||
clientKey = strings.Repeat("0", 64-len(clientKey)) + clientKey
|
|
||||||
} else {
|
|
||||||
clientKey = nostr.GeneratePrivateKey()
|
|
||||||
}
|
|
||||||
bunker, err := nip46.ConnectBunker(ctx, clientKey, bunkerURL, nil, func(s string) {
|
|
||||||
fmt.Fprintf(color.Error, color.CyanString("[nip46]: open the following URL: %s"), s)
|
|
||||||
})
|
|
||||||
return "", bunker, err
|
|
||||||
}
|
|
||||||
sec := c.String("sec")
|
|
||||||
if c.Bool("prompt-sec") {
|
|
||||||
if isPiped() {
|
|
||||||
return "", nil, fmt.Errorf("can't prompt for a secret key when processing data from a pipe, try again without --prompt-sec")
|
|
||||||
}
|
|
||||||
sec, err = askPassword("type your secret key as ncryptsec, nsec or hex: ", nil)
|
|
||||||
if err != nil {
|
|
||||||
return "", nil, fmt.Errorf("failed to get secret key: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(sec, "ncryptsec1") {
|
|
||||||
sec, err = promptDecrypt(sec)
|
|
||||||
if err != nil {
|
|
||||||
return "", nil, fmt.Errorf("failed to decrypt: %w", err)
|
|
||||||
}
|
|
||||||
} else if bsec, err := hex.DecodeString(leftPadKey(sec)); err == nil {
|
|
||||||
sec = hex.EncodeToString(bsec)
|
|
||||||
} else if prefix, hexvalue, err := nip19.Decode(sec); err != nil {
|
|
||||||
return "", nil, fmt.Errorf("invalid nsec: %w", err)
|
|
||||||
} else if prefix == "nsec" {
|
|
||||||
sec = hexvalue.(string)
|
|
||||||
}
|
|
||||||
|
|
||||||
if ok := nostr.IsValid32ByteHex(sec); !ok {
|
|
||||||
return "", nil, fmt.Errorf("invalid secret key")
|
|
||||||
}
|
|
||||||
return sec, nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func promptDecrypt(ncryptsec string) (string, error) {
|
|
||||||
for i := 1; i < 4; i++ {
|
|
||||||
var attemptStr string
|
|
||||||
if i > 1 {
|
|
||||||
attemptStr = fmt.Sprintf(" [%d/3]", i)
|
|
||||||
}
|
|
||||||
password, err := askPassword("type the password to decrypt your secret key"+attemptStr+": ", nil)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
sec, err := nip49.Decrypt(ncryptsec, password)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return sec, nil
|
|
||||||
}
|
|
||||||
return "", fmt.Errorf("couldn't decrypt private key")
|
|
||||||
}
|
|
||||||
|
|
||||||
func askPassword(msg string, shouldAskAgain func(answer string) bool) (string, error) {
|
|
||||||
config := &readline.Config{
|
|
||||||
Stdout: color.Error,
|
|
||||||
Prompt: color.YellowString(msg),
|
|
||||||
InterruptPrompt: "^C",
|
|
||||||
DisableAutoSaveHistory: true,
|
|
||||||
EnableMask: true,
|
|
||||||
MaskRune: '*',
|
|
||||||
}
|
|
||||||
return _ask(config, msg, "", shouldAskAgain)
|
|
||||||
}
|
|
||||||
|
|
||||||
func _ask(config *readline.Config, msg string, defaultValue string, shouldAskAgain func(answer string) bool) (string, error) {
|
|
||||||
rl, err := readline.NewEx(config)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
rl.WriteStdin([]byte(defaultValue))
|
|
||||||
for {
|
|
||||||
answer, err := rl.Readline()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
answer = strings.TrimSpace(strings.ToLower(answer))
|
|
||||||
if shouldAskAgain != nil && shouldAskAgain(answer) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return answer, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||||
|
|
||||||
func randString(n int) string {
|
func randString(n int) string {
|
||||||
@@ -288,3 +212,17 @@ func randString(n int) string {
|
|||||||
func leftPadKey(k string) string {
|
func leftPadKey(k string) string {
|
||||||
return strings.Repeat("0", 64-len(k)) + k
|
return strings.Repeat("0", 64-len(k)) + k
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var colors = struct {
|
||||||
|
reset func(...any) (int, error)
|
||||||
|
italic func(...any) string
|
||||||
|
italicf func(string, ...any) string
|
||||||
|
bold func(...any) string
|
||||||
|
boldf func(string, ...any) string
|
||||||
|
}{
|
||||||
|
color.New(color.Reset).Print,
|
||||||
|
color.New(color.Italic).Sprint,
|
||||||
|
color.New(color.Italic).Sprintf,
|
||||||
|
color.New(color.Bold).Sprint,
|
||||||
|
color.New(color.Bold).Sprintf,
|
||||||
|
}
|
||||||
|
|||||||
161
helpers_key.go
Normal file
161
helpers_key.go
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/chzyer/readline"
|
||||||
|
"github.com/fatih/color"
|
||||||
|
"github.com/fiatjaf/cli/v3"
|
||||||
|
"github.com/nbd-wtf/go-nostr"
|
||||||
|
"github.com/nbd-wtf/go-nostr/keyer"
|
||||||
|
"github.com/nbd-wtf/go-nostr/nip19"
|
||||||
|
"github.com/nbd-wtf/go-nostr/nip46"
|
||||||
|
"github.com/nbd-wtf/go-nostr/nip49"
|
||||||
|
)
|
||||||
|
|
||||||
|
var defaultKeyFlags = []cli.Flag{
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "sec",
|
||||||
|
Usage: "secret key to sign the event, as nsec, ncryptsec or hex, or a bunker URL, it is more secure to use the environment variable NOSTR_SECRET_KEY than this flag",
|
||||||
|
DefaultText: "the key '1'",
|
||||||
|
Aliases: []string{"connect"},
|
||||||
|
Category: CATEGORY_SIGNER,
|
||||||
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "prompt-sec",
|
||||||
|
Usage: "prompt the user to paste a hex or nsec with which to sign the event",
|
||||||
|
Category: CATEGORY_SIGNER,
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "connect-as",
|
||||||
|
Usage: "private key to use when communicating with nip46 bunkers",
|
||||||
|
DefaultText: "a random key",
|
||||||
|
Category: CATEGORY_SIGNER,
|
||||||
|
Sources: cli.EnvVars("NOSTR_CLIENT_KEY"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func gatherKeyerFromArguments(ctx context.Context, c *cli.Command) (nostr.Keyer, string, error) {
|
||||||
|
key, bunker, err := gatherSecretKeyOrBunkerFromArguments(ctx, c)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
var kr nostr.Keyer
|
||||||
|
if bunker != nil {
|
||||||
|
kr = keyer.NewBunkerSignerFromBunkerClient(bunker)
|
||||||
|
} else {
|
||||||
|
kr, err = keyer.NewPlainKeySigner(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
return kr, key, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func gatherSecretKeyOrBunkerFromArguments(ctx context.Context, c *cli.Command) (string, *nip46.BunkerClient, error) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
sec := c.String("sec")
|
||||||
|
if strings.HasPrefix(sec, "bunker://") {
|
||||||
|
// it's a bunker
|
||||||
|
bunkerURL := sec
|
||||||
|
clientKey := c.String("connect-as")
|
||||||
|
if clientKey != "" {
|
||||||
|
clientKey = strings.Repeat("0", 64-len(clientKey)) + clientKey
|
||||||
|
} else {
|
||||||
|
clientKey = nostr.GeneratePrivateKey()
|
||||||
|
}
|
||||||
|
bunker, err := nip46.ConnectBunker(ctx, clientKey, bunkerURL, nil, func(s string) {
|
||||||
|
log(color.CyanString("[nip46]: open the following URL: %s"), s)
|
||||||
|
})
|
||||||
|
return "", bunker, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// take private from flags, environment variable or default to 1
|
||||||
|
if sec == "" {
|
||||||
|
if key, ok := os.LookupEnv("NOSTR_SECRET_KEY"); ok {
|
||||||
|
sec = key
|
||||||
|
} else {
|
||||||
|
sec = "0000000000000000000000000000000000000000000000000000000000000001"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Bool("prompt-sec") {
|
||||||
|
if isPiped() {
|
||||||
|
return "", nil, fmt.Errorf("can't prompt for a secret key when processing data from a pipe, try again without --prompt-sec")
|
||||||
|
}
|
||||||
|
sec, err = askPassword("type your secret key as ncryptsec, nsec or hex: ", nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, fmt.Errorf("failed to get secret key: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(sec, "ncryptsec1") {
|
||||||
|
sec, err = promptDecrypt(sec)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, fmt.Errorf("failed to decrypt: %w", err)
|
||||||
|
}
|
||||||
|
} else if bsec, err := hex.DecodeString(leftPadKey(sec)); err == nil {
|
||||||
|
sec = hex.EncodeToString(bsec)
|
||||||
|
} else if prefix, hexvalue, err := nip19.Decode(sec); err != nil {
|
||||||
|
return "", nil, fmt.Errorf("invalid nsec: %w", err)
|
||||||
|
} else if prefix == "nsec" {
|
||||||
|
sec = hexvalue.(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok := nostr.IsValid32ByteHex(sec); !ok {
|
||||||
|
return "", nil, fmt.Errorf("invalid secret key")
|
||||||
|
}
|
||||||
|
|
||||||
|
return sec, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func promptDecrypt(ncryptsec string) (string, error) {
|
||||||
|
for i := 1; i < 4; i++ {
|
||||||
|
var attemptStr string
|
||||||
|
if i > 1 {
|
||||||
|
attemptStr = fmt.Sprintf(" [%d/3]", i)
|
||||||
|
}
|
||||||
|
password, err := askPassword("type the password to decrypt your secret key"+attemptStr+": ", nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
sec, err := nip49.Decrypt(ncryptsec, password)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return sec, nil
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("couldn't decrypt private key")
|
||||||
|
}
|
||||||
|
|
||||||
|
func askPassword(msg string, shouldAskAgain func(answer string) bool) (string, error) {
|
||||||
|
config := &readline.Config{
|
||||||
|
Stdout: color.Error,
|
||||||
|
Prompt: color.YellowString(msg),
|
||||||
|
InterruptPrompt: "^C",
|
||||||
|
DisableAutoSaveHistory: true,
|
||||||
|
EnableMask: true,
|
||||||
|
MaskRune: '*',
|
||||||
|
}
|
||||||
|
|
||||||
|
rl, err := readline.NewEx(config)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
answer, err := rl.Readline()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
answer = strings.TrimSpace(answer)
|
||||||
|
if shouldAskAgain != nil && shouldAskAgain(answer) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return answer, err
|
||||||
|
}
|
||||||
|
}
|
||||||
50
key.go
50
key.go
@@ -3,29 +3,28 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"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/decred/dcrd/dcrec/secp256k1/v4"
|
||||||
|
"github.com/fiatjaf/cli/v3"
|
||||||
"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/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: ``,
|
||||||
|
DisableSliceFlagSeparator: true,
|
||||||
Commands: []*cli.Command{
|
Commands: []*cli.Command{
|
||||||
generate,
|
generate,
|
||||||
public,
|
public,
|
||||||
encrypt,
|
encryptKey,
|
||||||
decrypt,
|
decryptKey,
|
||||||
combine,
|
combine,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -34,6 +33,7 @@ var generate = &cli.Command{
|
|||||||
Name: "generate",
|
Name: "generate",
|
||||||
Usage: "generates a secret key",
|
Usage: "generates a secret key",
|
||||||
Description: ``,
|
Description: ``,
|
||||||
|
DisableSliceFlagSeparator: true,
|
||||||
Action: func(ctx context.Context, c *cli.Command) error {
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
sec := nostr.GeneratePrivateKey()
|
sec := nostr.GeneratePrivateKey()
|
||||||
stdout(sec)
|
stdout(sec)
|
||||||
@@ -46,24 +46,34 @@ 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]",
|
||||||
|
DisableSliceFlagSeparator: true,
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "with-parity",
|
||||||
|
Usage: "output 33 bytes instead of 32, the first one being either '02' or '03', a prefix indicating whether this pubkey is even or odd.",
|
||||||
|
},
|
||||||
|
},
|
||||||
Action: func(ctx context.Context, c *cli.Command) error {
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
for sec := range getSecretKeysFromStdinLinesOrSlice(ctx, c, c.Args().Slice()) {
|
for sec := range getSecretKeysFromStdinLinesOrSlice(ctx, c, c.Args().Slice()) {
|
||||||
pubkey, err := nostr.GetPublicKey(sec)
|
b, _ := hex.DecodeString(sec)
|
||||||
if err != nil {
|
_, pk := btcec.PrivKeyFromBytes(b)
|
||||||
ctx = lineProcessingError(ctx, "failed to derive public key: %s", err)
|
|
||||||
continue
|
if c.Bool("with-parity") {
|
||||||
|
stdout(hex.EncodeToString(pk.SerializeCompressed()))
|
||||||
|
} else {
|
||||||
|
stdout(hex.EncodeToString(pk.SerializeCompressed()[1:]))
|
||||||
}
|
}
|
||||||
stdout(pubkey)
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
var encrypt = &cli.Command{
|
var encryptKey = &cli.Command{
|
||||||
Name: "encrypt",
|
Name: "encrypt",
|
||||||
Usage: "encrypts a secret key and prints an ncryptsec code",
|
Usage: "encrypts a secret key and prints an ncryptsec code",
|
||||||
Description: `uses the NIP-49 standard.`,
|
Description: `uses the nip49 standard.`,
|
||||||
ArgsUsage: "<secret> <password>",
|
ArgsUsage: "<secret> <password>",
|
||||||
|
DisableSliceFlagSeparator: true,
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
&cli.IntFlag{
|
&cli.IntFlag{
|
||||||
Name: "logn",
|
Name: "logn",
|
||||||
@@ -97,11 +107,12 @@ var encrypt = &cli.Command{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
var decrypt = &cli.Command{
|
var decryptKey = &cli.Command{
|
||||||
Name: "decrypt",
|
Name: "decrypt",
|
||||||
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 nip49 standard.`,
|
||||||
ArgsUsage: "<ncryptsec-code> <password>",
|
ArgsUsage: "<ncryptsec-code> <password>",
|
||||||
|
DisableSliceFlagSeparator: true,
|
||||||
Action: func(ctx context.Context, c *cli.Command) error {
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
var ncryptsec string
|
var ncryptsec string
|
||||||
var password string
|
var password string
|
||||||
@@ -152,6 +163,7 @@ var combine = &cli.Command{
|
|||||||
|
|
||||||
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.`,
|
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...]",
|
||||||
|
DisableSliceFlagSeparator: true,
|
||||||
Action: func(ctx context.Context, c *cli.Command) error {
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
type Combination struct {
|
type Combination struct {
|
||||||
Variants []string `json:"input_variants"`
|
Variants []string `json:"input_variants"`
|
||||||
@@ -182,7 +194,7 @@ However, if the intent is to check if two existing Nostr pubkeys match a given c
|
|||||||
for i, prefix := range []byte{0x02, 0x03} {
|
for i, prefix := range []byte{0x02, 0x03} {
|
||||||
pubk, err := btcec.ParsePubKey(append([]byte{prefix}, keyb...))
|
pubk, err := btcec.ParsePubKey(append([]byte{prefix}, keyb...))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "error parsing key %s: %s", keyhex, err)
|
log("error parsing key %s: %s", keyhex, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
group[i] = pubk
|
group[i] = pubk
|
||||||
@@ -223,7 +235,7 @@ However, if the intent is to check if two existing Nostr pubkeys match a given c
|
|||||||
|
|
||||||
agg, _, _, err := musig2.AggregateKeys(combining, true)
|
agg, _, _, err := musig2.AggregateKeys(combining, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "error aggregating: %s", err)
|
log("error aggregating: %s", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,13 +258,13 @@ However, if the intent is to check if two existing Nostr pubkeys match a given c
|
|||||||
}
|
}
|
||||||
|
|
||||||
res, _ := json.MarshalIndent(result, "", " ")
|
res, _ := json.MarshalIndent(result, "", " ")
|
||||||
fmt.Println(string(res))
|
stdout(string(res))
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func getSecretKeysFromStdinLinesOrSlice(ctx context.Context, c *cli.Command, keys []string) chan string {
|
func getSecretKeysFromStdinLinesOrSlice(ctx context.Context, _ *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) {
|
||||||
|
|||||||
100
main.go
100
main.go
@@ -2,17 +2,26 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"net/textproto"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/urfave/cli/v3"
|
"github.com/fiatjaf/cli/v3"
|
||||||
|
"github.com/nbd-wtf/go-nostr"
|
||||||
|
"github.com/nbd-wtf/go-nostr/sdk"
|
||||||
|
"github.com/nbd-wtf/go-nostr/sdk/hints/memoryh"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var version string = "debug"
|
||||||
|
|
||||||
var app = &cli.Command{
|
var app = &cli.Command{
|
||||||
Name: "nak",
|
Name: "nak",
|
||||||
Suggest: true,
|
Suggest: true,
|
||||||
UseShortOptionHandling: true,
|
UseShortOptionHandling: true,
|
||||||
AllowFlagsAfterArguments: true,
|
AllowFlagsAfterArguments: true,
|
||||||
Usage: "the nostr army knife command-line tool",
|
Usage: "the nostr army knife command-line tool",
|
||||||
|
DisableSliceFlagSeparator: true,
|
||||||
Commands: []*cli.Command{
|
Commands: []*cli.Command{
|
||||||
req,
|
req,
|
||||||
count,
|
count,
|
||||||
@@ -24,29 +33,116 @@ var app = &cli.Command{
|
|||||||
verify,
|
verify,
|
||||||
relay,
|
relay,
|
||||||
bunker,
|
bunker,
|
||||||
|
serve,
|
||||||
|
encrypt,
|
||||||
|
decrypt,
|
||||||
|
outbox,
|
||||||
|
wallet,
|
||||||
|
mcpServer,
|
||||||
},
|
},
|
||||||
|
Version: version,
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "config-path",
|
||||||
|
Hidden: true,
|
||||||
|
Persistent: true,
|
||||||
|
},
|
||||||
&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",
|
||||||
Aliases: []string{"q"},
|
Aliases: []string{"q"},
|
||||||
|
Persistent: true,
|
||||||
Action: func(ctx context.Context, c *cli.Command, b bool) error {
|
Action: func(ctx context.Context, c *cli.Command, b bool) error {
|
||||||
q := c.Count("quiet")
|
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 {
|
||||||
stdout = func(a ...any) (int, error) { return 0, nil }
|
stdout = func(_ ...any) (int, error) { return 0, nil }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "verbose",
|
||||||
|
Usage: "print more stuff than normally",
|
||||||
|
Aliases: []string{"v"},
|
||||||
|
Persistent: true,
|
||||||
|
Action: func(ctx context.Context, c *cli.Command, b bool) error {
|
||||||
|
v := c.Count("verbose")
|
||||||
|
if v >= 1 {
|
||||||
|
logverbose = log
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Before: func(ctx context.Context, c *cli.Command) error {
|
||||||
|
configPath := c.String("config-path")
|
||||||
|
if configPath == "" {
|
||||||
|
if home, err := os.UserHomeDir(); err == nil {
|
||||||
|
configPath = filepath.Join(home, ".config/nak")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if configPath != "" {
|
||||||
|
hintsFilePath = filepath.Join(configPath, "outbox/hints.db")
|
||||||
|
}
|
||||||
|
if hintsFilePath != "" {
|
||||||
|
if _, err := os.Stat(hintsFilePath); !os.IsNotExist(err) {
|
||||||
|
hintsFileExists = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if hintsFilePath != "" {
|
||||||
|
if data, err := os.ReadFile(hintsFilePath); err == nil {
|
||||||
|
hintsdb := memoryh.NewHintDB()
|
||||||
|
if err := json.Unmarshal(data, &hintsdb); err == nil {
|
||||||
|
sys = sdk.NewSystem(
|
||||||
|
sdk.WithHintsDB(hintsdb),
|
||||||
|
)
|
||||||
|
goto systemOperational
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sys = sdk.NewSystem()
|
||||||
|
|
||||||
|
systemOperational:
|
||||||
|
sys.Pool = nostr.NewSimplePool(context.Background(),
|
||||||
|
nostr.WithAuthorKindQueryMiddleware(sys.TrackQueryAttempts),
|
||||||
|
nostr.WithEventMiddleware(sys.TrackEventHints),
|
||||||
|
nostr.WithRelayOptions(
|
||||||
|
nostr.WithRequestHeader(http.Header{textproto.CanonicalMIMEHeaderKey("user-agent"): {"nak/b"}}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
After: func(ctx context.Context, c *cli.Command) error {
|
||||||
|
// save hints database on exit
|
||||||
|
if hintsFileExists {
|
||||||
|
data, err := json.Marshal(sys.Hints)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.WriteFile(hintsFilePath, data, 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
defer colors.reset()
|
||||||
|
|
||||||
|
cli.VersionFlag = &cli.BoolFlag{
|
||||||
|
Name: "version",
|
||||||
|
Usage: "prints the version",
|
||||||
|
}
|
||||||
|
|
||||||
if err := app.Run(context.Background(), os.Args); err != nil {
|
if err := app.Run(context.Background(), os.Args); err != nil {
|
||||||
stdout(err)
|
stdout(err)
|
||||||
|
colors.reset()
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
217
mcp.go
Normal file
217
mcp.go
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/fiatjaf/cli/v3"
|
||||||
|
"github.com/mark3labs/mcp-go/mcp"
|
||||||
|
"github.com/mark3labs/mcp-go/server"
|
||||||
|
"github.com/nbd-wtf/go-nostr"
|
||||||
|
"github.com/nbd-wtf/go-nostr/nip19"
|
||||||
|
)
|
||||||
|
|
||||||
|
var mcpServer = &cli.Command{
|
||||||
|
Name: "mcp",
|
||||||
|
Usage: "pander to the AI gods",
|
||||||
|
Description: ``,
|
||||||
|
DisableSliceFlagSeparator: true,
|
||||||
|
Flags: []cli.Flag{},
|
||||||
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
|
s := server.NewMCPServer(
|
||||||
|
"nak",
|
||||||
|
version,
|
||||||
|
)
|
||||||
|
|
||||||
|
s.AddTool(mcp.NewTool("publish_note",
|
||||||
|
mcp.WithDescription("Publish a short note event to Nostr with the given text content"),
|
||||||
|
mcp.WithString("content",
|
||||||
|
mcp.Required(),
|
||||||
|
mcp.Description("Arbitrary string to be published"),
|
||||||
|
),
|
||||||
|
mcp.WithString("mention",
|
||||||
|
mcp.Required(),
|
||||||
|
mcp.Description("Nostr user's public key to be mentioned"),
|
||||||
|
),
|
||||||
|
), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
content, _ := request.Params.Arguments["content"].(string)
|
||||||
|
mention, _ := request.Params.Arguments["mention"].(string)
|
||||||
|
|
||||||
|
if mention != "" && !nostr.IsValidPublicKey(mention) {
|
||||||
|
return mcp.NewToolResultError("the given mention isn't a valid public key, it must be 32 bytes hex, like the ones returned by search_profile"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
sk := os.Getenv("NOSTR_SECRET_KEY")
|
||||||
|
if sk == "" {
|
||||||
|
sk = "0000000000000000000000000000000000000000000000000000000000000001"
|
||||||
|
}
|
||||||
|
var relays []string
|
||||||
|
|
||||||
|
evt := nostr.Event{
|
||||||
|
Kind: 1,
|
||||||
|
Tags: nostr.Tags{{"client", "goose/nak"}},
|
||||||
|
Content: content,
|
||||||
|
CreatedAt: nostr.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if mention != "" {
|
||||||
|
evt.Tags = append(evt.Tags, nostr.Tag{"p", mention})
|
||||||
|
// their inbox relays
|
||||||
|
relays = sys.FetchInboxRelays(ctx, mention, 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
evt.Sign(sk)
|
||||||
|
|
||||||
|
// our write relays
|
||||||
|
relays = append(relays, sys.FetchOutboxRelays(ctx, evt.PubKey, 3)...)
|
||||||
|
|
||||||
|
if len(relays) == 0 {
|
||||||
|
relays = []string{"nos.lol", "relay.damus.io"}
|
||||||
|
}
|
||||||
|
|
||||||
|
for res := range sys.Pool.PublishMany(ctx, []string{"nos.lol"}, evt) {
|
||||||
|
if res.Error != nil {
|
||||||
|
return mcp.NewToolResultError(
|
||||||
|
fmt.Sprintf("there was an error publishing the event to the relay %s",
|
||||||
|
res.RelayURL),
|
||||||
|
), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mcp.NewToolResultText("event was successfully published with id " + evt.ID), nil
|
||||||
|
})
|
||||||
|
|
||||||
|
s.AddTool(mcp.NewTool("resolve_nostr_uri",
|
||||||
|
mcp.WithDescription("Resolve URIs prefixed with nostr:, including nostr:nevent1..., nostr:npub1..., nostr:nprofile1... and nostr:naddr1..."),
|
||||||
|
mcp.WithString("uri",
|
||||||
|
mcp.Required(),
|
||||||
|
mcp.Description("URI to be resolved"),
|
||||||
|
),
|
||||||
|
), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
uri, _ := request.Params.Arguments["uri"].(string)
|
||||||
|
if strings.HasPrefix(uri, "nostr:") {
|
||||||
|
uri = uri[6:]
|
||||||
|
}
|
||||||
|
|
||||||
|
prefix, data, err := nip19.Decode(uri)
|
||||||
|
if err != nil {
|
||||||
|
return mcp.NewToolResultError("this Nostr uri is invalid"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch prefix {
|
||||||
|
case "npub":
|
||||||
|
pm := sys.FetchProfileMetadata(ctx, data.(string))
|
||||||
|
return mcp.NewToolResultText(
|
||||||
|
fmt.Sprintf("this is a Nostr profile named '%s', their public key is '%s'",
|
||||||
|
pm.ShortName(), pm.PubKey),
|
||||||
|
), nil
|
||||||
|
case "nprofile":
|
||||||
|
pm, _ := sys.FetchProfileFromInput(ctx, uri)
|
||||||
|
return mcp.NewToolResultText(
|
||||||
|
fmt.Sprintf("this is a Nostr profile named '%s', their public key is '%s'",
|
||||||
|
pm.ShortName(), pm.PubKey),
|
||||||
|
), nil
|
||||||
|
case "nevent":
|
||||||
|
event, _, err := sys.FetchSpecificEventFromInput(ctx, uri, false)
|
||||||
|
if err != nil {
|
||||||
|
return mcp.NewToolResultError("Couldn't find this event anywhere"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return mcp.NewToolResultText(
|
||||||
|
fmt.Sprintf("this is a Nostr event: %s", event),
|
||||||
|
), nil
|
||||||
|
case "naddr":
|
||||||
|
return mcp.NewToolResultError("For now we can't handle this kind of Nostr uri"), nil
|
||||||
|
default:
|
||||||
|
return mcp.NewToolResultError("We don't know how to handle this Nostr uri"), nil
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
s.AddTool(mcp.NewTool("search_profile",
|
||||||
|
mcp.WithDescription("Search for the public key of a Nostr user given their name"),
|
||||||
|
mcp.WithString("name",
|
||||||
|
mcp.Required(),
|
||||||
|
mcp.Description("Name to be searched"),
|
||||||
|
),
|
||||||
|
), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
name, _ := request.Params.Arguments["name"].(string)
|
||||||
|
re := sys.Pool.QuerySingle(ctx, []string{"relay.nostr.band", "nostr.wine"}, nostr.Filter{Search: name, Kinds: []int{0}})
|
||||||
|
if re == nil {
|
||||||
|
return mcp.NewToolResultError("couldn't find anyone with that name"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return mcp.NewToolResultText(re.PubKey), nil
|
||||||
|
})
|
||||||
|
|
||||||
|
s.AddTool(mcp.NewTool("get_outbox_relay_for_pubkey",
|
||||||
|
mcp.WithDescription("Get the best relay from where to read notes from a specific Nostr user"),
|
||||||
|
mcp.WithString("pubkey",
|
||||||
|
mcp.Required(),
|
||||||
|
mcp.Description("Public key of Nostr user we want to know the relay from where to read"),
|
||||||
|
),
|
||||||
|
), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
name, _ := request.Params.Arguments["name"].(string)
|
||||||
|
re := sys.Pool.QuerySingle(ctx, []string{"relay.nostr.band", "nostr.wine"}, nostr.Filter{Search: name, Kinds: []int{0}})
|
||||||
|
if re == nil {
|
||||||
|
return mcp.NewToolResultError("couldn't find anyone with that name"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return mcp.NewToolResultText(re.PubKey), nil
|
||||||
|
})
|
||||||
|
|
||||||
|
s.AddTool(mcp.NewTool("read_events_from_relay",
|
||||||
|
mcp.WithDescription("Makes a REQ query to one relay using the specified parameters, this can be used to fetch notes from a profile"),
|
||||||
|
mcp.WithNumber("kind",
|
||||||
|
mcp.Required(),
|
||||||
|
mcp.Description("event kind number to include in the 'kinds' field"),
|
||||||
|
),
|
||||||
|
mcp.WithString("pubkey",
|
||||||
|
mcp.Description("pubkey to include in the 'authors' field"),
|
||||||
|
),
|
||||||
|
mcp.WithNumber("limit",
|
||||||
|
mcp.Required(),
|
||||||
|
mcp.Description("maximum number of events to query"),
|
||||||
|
),
|
||||||
|
mcp.WithString("relay",
|
||||||
|
mcp.Required(),
|
||||||
|
mcp.Description("relay URL to send the query to"),
|
||||||
|
),
|
||||||
|
), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
relay, _ := request.Params.Arguments["relay"].(string)
|
||||||
|
limit, _ := request.Params.Arguments["limit"].(int)
|
||||||
|
kind, _ := request.Params.Arguments["kind"].(int)
|
||||||
|
pubkey, _ := request.Params.Arguments["pubkey"].(string)
|
||||||
|
|
||||||
|
if pubkey != "" && !nostr.IsValidPublicKey(pubkey) {
|
||||||
|
return mcp.NewToolResultError("the given pubkey isn't a valid public key, it must be 32 bytes hex, like the ones returned by search_profile"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
filter := nostr.Filter{
|
||||||
|
Limit: limit,
|
||||||
|
Kinds: []int{kind},
|
||||||
|
}
|
||||||
|
if pubkey != "" {
|
||||||
|
filter.Authors = []string{pubkey}
|
||||||
|
}
|
||||||
|
|
||||||
|
events := sys.Pool.SubManyEose(ctx, []string{relay}, nostr.Filters{filter})
|
||||||
|
|
||||||
|
|
||||||
|
result := strings.Builder{}
|
||||||
|
for ie := range events {
|
||||||
|
result.WriteString("author public key: ")
|
||||||
|
result.WriteString(ie.PubKey)
|
||||||
|
result.WriteString("content: '")
|
||||||
|
result.WriteString(ie.Content)
|
||||||
|
result.WriteString("'")
|
||||||
|
result.WriteString("\n---\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
return mcp.NewToolResultText(result.String()), nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return server.ServeStdio(s)
|
||||||
|
},
|
||||||
|
}
|
||||||
36
musig2.go
36
musig2.go
@@ -6,7 +6,6 @@ import (
|
|||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -15,8 +14,33 @@ import (
|
|||||||
"github.com/nbd-wtf/go-nostr"
|
"github.com/nbd-wtf/go-nostr"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func getMusigAggregatedKey(_ context.Context, keys []string) (string, error) {
|
||||||
|
knownSigners := make([]*btcec.PublicKey, len(keys))
|
||||||
|
for i, spk := range keys {
|
||||||
|
bpk, err := hex.DecodeString(spk)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("'%s' is invalid hex: %w", spk, err)
|
||||||
|
}
|
||||||
|
if len(bpk) == 32 {
|
||||||
|
return "", fmt.Errorf("'%s' is missing the leading parity byte", spk)
|
||||||
|
}
|
||||||
|
pk, err := btcec.ParsePubKey(bpk)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("'%s' is not a valid pubkey: %w", spk, err)
|
||||||
|
}
|
||||||
|
knownSigners[i] = pk
|
||||||
|
}
|
||||||
|
|
||||||
|
aggpk, _, _, err := musig2.AggregateKeys(knownSigners, true)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("aggregation failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return hex.EncodeToString(aggpk.FinalKey.SerializeCompressed()[1:]), nil
|
||||||
|
}
|
||||||
|
|
||||||
func performMusig(
|
func performMusig(
|
||||||
ctx context.Context,
|
_ context.Context,
|
||||||
sec string,
|
sec string,
|
||||||
evt *nostr.Event,
|
evt *nostr.Event,
|
||||||
numSigners int,
|
numSigners int,
|
||||||
@@ -110,8 +134,8 @@ func performMusig(
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
fmt.Fprintf(os.Stderr, "the following code should be saved secretly until the next step an included with --musig-nonce-secret:\n")
|
log("the following code should be saved secretly until the next step an included with --musig-nonce-secret:\n")
|
||||||
fmt.Fprintf(os.Stderr, "%s\n\n", base64.StdEncoding.EncodeToString(nonce.SecNonce[:]))
|
log("%s\n\n", base64.StdEncoding.EncodeToString(nonce.SecNonce[:]))
|
||||||
|
|
||||||
knownNonces = append(knownNonces, nonce.PubNonce)
|
knownNonces = append(knownNonces, nonce.PubNonce)
|
||||||
printPublicCommandForNextPeer(evt, numSigners, knownSigners, knownNonces, nil, false)
|
printPublicCommandForNextPeer(evt, numSigners, knownSigners, knownNonces, nil, false)
|
||||||
@@ -124,7 +148,7 @@ func performMusig(
|
|||||||
} else {
|
} else {
|
||||||
evt.PubKey = hex.EncodeToString(comb.SerializeCompressed()[1:])
|
evt.PubKey = hex.EncodeToString(comb.SerializeCompressed()[1:])
|
||||||
evt.ID = evt.GetID()
|
evt.ID = evt.GetID()
|
||||||
fmt.Fprintf(os.Stderr, "combined key: %x\n\n", comb.SerializeCompressed())
|
log("combined key: %x\n\n", comb.SerializeCompressed())
|
||||||
}
|
}
|
||||||
|
|
||||||
// we have all the signers, which means we must also have all the nonces
|
// we have all the signers, which means we must also have all the nonces
|
||||||
@@ -219,7 +243,7 @@ func printPublicCommandForNextPeer(
|
|||||||
maybeNonceSecret = " --musig-nonce-secret '<insert-nonce-secret>'"
|
maybeNonceSecret = " --musig-nonce-secret '<insert-nonce-secret>'"
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Fprintf(os.Stderr, "the next signer and they should call this on their side:\nnak event --sec <insert-secret-key> --musig %d %s%s%s%s%s\n",
|
log("the next signer and they should call this on their side:\nnak event --sec <insert-secret-key> --musig %d %s%s%s%s%s\n",
|
||||||
numSigners,
|
numSigners,
|
||||||
eventToCliArgs(evt),
|
eventToCliArgs(evt),
|
||||||
signersToCliArgs(knownSigners),
|
signersToCliArgs(knownSigners),
|
||||||
|
|||||||
68
outbox.go
Normal file
68
outbox.go
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/fiatjaf/cli/v3"
|
||||||
|
"github.com/nbd-wtf/go-nostr"
|
||||||
|
)
|
||||||
|
|
||||||
|
var outbox = &cli.Command{
|
||||||
|
Name: "outbox",
|
||||||
|
Usage: "manage outbox relay hints database",
|
||||||
|
DisableSliceFlagSeparator: true,
|
||||||
|
Commands: []*cli.Command{
|
||||||
|
{
|
||||||
|
Name: "init",
|
||||||
|
Usage: "initialize the outbox hints database",
|
||||||
|
DisableSliceFlagSeparator: true,
|
||||||
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
|
if hintsFileExists {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if hintsFilePath == "" {
|
||||||
|
return fmt.Errorf("couldn't find a place to store the hints, pass --config-path to fix.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll(filepath.Dir(hintsFilePath), 0777); err == nil {
|
||||||
|
if err := os.WriteFile(hintsFilePath, []byte("{}"), 0644); err != nil {
|
||||||
|
return fmt.Errorf("failed to create hints database: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log("initialized hints database at %s\n", hintsFilePath)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "list",
|
||||||
|
Usage: "list outbox relays for a given pubkey",
|
||||||
|
ArgsUsage: "<pubkey>",
|
||||||
|
DisableSliceFlagSeparator: true,
|
||||||
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
|
if !hintsFileExists {
|
||||||
|
log("running with temporary fragile data.\n")
|
||||||
|
log("call `nak outbox init` to setup persistence.\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Args().Len() != 1 {
|
||||||
|
return fmt.Errorf("expected exactly one argument (pubkey)")
|
||||||
|
}
|
||||||
|
|
||||||
|
pubkey := c.Args().First()
|
||||||
|
if !nostr.IsValidPublicKey(pubkey) {
|
||||||
|
return fmt.Errorf("invalid public key: %s", pubkey)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, relay := range sys.FetchOutboxRelays(ctx, pubkey, 6) {
|
||||||
|
stdout(relay)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
76
paginate.go
Normal file
76
paginate.go
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"math"
|
||||||
|
"slices"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/nbd-wtf/go-nostr"
|
||||||
|
)
|
||||||
|
|
||||||
|
func paginateWithParams(
|
||||||
|
interval time.Duration,
|
||||||
|
globalLimit uint64,
|
||||||
|
) func(ctx context.Context, urls []string, filters nostr.Filters, opts ...nostr.SubscriptionOption) chan nostr.RelayEvent {
|
||||||
|
return func(ctx context.Context, urls []string, filters nostr.Filters, opts ...nostr.SubscriptionOption) chan nostr.RelayEvent {
|
||||||
|
// filters will always be just one
|
||||||
|
filter := filters[0]
|
||||||
|
|
||||||
|
nextUntil := nostr.Now()
|
||||||
|
if filter.Until != nil {
|
||||||
|
nextUntil = *filter.Until
|
||||||
|
}
|
||||||
|
|
||||||
|
if globalLimit == 0 {
|
||||||
|
globalLimit = uint64(filter.Limit)
|
||||||
|
if globalLimit == 0 && !filter.LimitZero {
|
||||||
|
globalLimit = math.MaxUint64
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var globalCount uint64 = 0
|
||||||
|
globalCh := make(chan nostr.RelayEvent)
|
||||||
|
|
||||||
|
repeatedCache := make([]string, 0, 300)
|
||||||
|
nextRepeatedCache := make([]string, 0, 300)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer close(globalCh)
|
||||||
|
|
||||||
|
for {
|
||||||
|
filter.Until = &nextUntil
|
||||||
|
time.Sleep(interval)
|
||||||
|
|
||||||
|
keepGoing := false
|
||||||
|
for evt := range sys.Pool.SubManyEose(ctx, urls, nostr.Filters{filter}, opts...) {
|
||||||
|
if slices.Contains(repeatedCache, evt.ID) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
keepGoing = true // if we get one that isn't repeated, then keep trying to get more
|
||||||
|
nextRepeatedCache = append(nextRepeatedCache, evt.ID)
|
||||||
|
|
||||||
|
globalCh <- evt
|
||||||
|
|
||||||
|
globalCount++
|
||||||
|
if globalCount >= globalLimit {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if evt.CreatedAt < *filter.Until {
|
||||||
|
nextUntil = evt.CreatedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !keepGoing {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
repeatedCache = nextRepeatedCache
|
||||||
|
nextRepeatedCache = nextRepeatedCache[:0]
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return globalCh
|
||||||
|
}
|
||||||
|
}
|
||||||
55
relay.go
55
relay.go
@@ -6,24 +6,27 @@ import (
|
|||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
|
||||||
|
|
||||||
|
"github.com/fiatjaf/cli/v3"
|
||||||
"github.com/nbd-wtf/go-nostr"
|
"github.com/nbd-wtf/go-nostr"
|
||||||
"github.com/nbd-wtf/go-nostr/nip11"
|
"github.com/nbd-wtf/go-nostr/nip11"
|
||||||
"github.com/nbd-wtf/go-nostr/nip86"
|
"github.com/nbd-wtf/go-nostr/nip86"
|
||||||
"github.com/urfave/cli/v3"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var relay = &cli.Command{
|
var relay = &cli.Command{
|
||||||
Name: "relay",
|
Name: "relay",
|
||||||
Usage: "gets the relay information document for the given relay, as JSON",
|
Usage: "gets the relay information document for the given relay, as JSON -- or allows usage of the relay management API.",
|
||||||
Description: `example:
|
Description: `examples:
|
||||||
nak relay nostr.wine`,
|
fetching relay information:
|
||||||
|
nak relay nostr.wine
|
||||||
|
|
||||||
|
managing a relay
|
||||||
|
nak relay nostr.wine banevent --sec 1234 --id 037eb3751073770ff17483b1b1ff125866cd5147668271975ef0a8a8e7ee184a --reason "I don't like it"`,
|
||||||
ArgsUsage: "<relay-url>",
|
ArgsUsage: "<relay-url>",
|
||||||
|
DisableSliceFlagSeparator: true,
|
||||||
Action: func(ctx context.Context, c *cli.Command) 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 == "" {
|
||||||
@@ -74,27 +77,7 @@ var relay = &cli.Command{
|
|||||||
flags[i] = declareFlag(argName)
|
flags[i] = declareFlag(argName)
|
||||||
}
|
}
|
||||||
|
|
||||||
flags = append(flags,
|
flags = append(flags, defaultKeyFlags...)
|
||||||
&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{
|
cmd := &cli.Command{
|
||||||
Name: def.method,
|
Name: def.method,
|
||||||
@@ -115,7 +98,7 @@ var relay = &cli.Command{
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
sec, bunker, err := gatherSecretKeyOrBunkerFromArguments(ctx, c)
|
kr, _, err := gatherKeyerFromArguments(ctx, c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -131,24 +114,20 @@ var relay = &cli.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Authorization
|
// Authorization
|
||||||
hostname := strings.Split(strings.Split(httpUrl, "://")[1], "/")[0]
|
|
||||||
payloadHash := sha256.Sum256(reqj)
|
payloadHash := sha256.Sum256(reqj)
|
||||||
authEvent := nostr.Event{
|
tokenEvent := nostr.Event{
|
||||||
Kind: 27235,
|
Kind: 27235,
|
||||||
CreatedAt: nostr.Now(),
|
CreatedAt: nostr.Now(),
|
||||||
Tags: nostr.Tags{
|
Tags: nostr.Tags{
|
||||||
{"host", hostname},
|
{"u", httpUrl},
|
||||||
|
{"method", "POST"},
|
||||||
{"payload", hex.EncodeToString(payloadHash[:])},
|
{"payload", hex.EncodeToString(payloadHash[:])},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if bunker != nil {
|
if err := kr.SignEvent(ctx, &tokenEvent); err != nil {
|
||||||
if err := bunker.SignEvent(ctx, &authEvent); err != nil {
|
return fmt.Errorf("failed to sign token event: %w", err)
|
||||||
return fmt.Errorf("failed to sign with bunker: %w", err)
|
|
||||||
}
|
}
|
||||||
} else if err := authEvent.Sign(sec); err != nil {
|
evtj, _ := json.Marshal(tokenEvent)
|
||||||
return fmt.Errorf("error signing with provided key: %w", err)
|
|
||||||
}
|
|
||||||
evtj, _ := json.Marshal(authEvent)
|
|
||||||
req.Header.Set("Authorization", "Nostr "+base64.StdEncoding.EncodeToString(evtj))
|
req.Header.Set("Authorization", "Nostr "+base64.StdEncoding.EncodeToString(evtj))
|
||||||
|
|
||||||
// Content-Type
|
// Content-Type
|
||||||
|
|||||||
297
req.go
297
req.go
@@ -2,23 +2,24 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/fiatjaf/cli/v3"
|
||||||
"github.com/mailru/easyjson"
|
"github.com/mailru/easyjson"
|
||||||
"github.com/nbd-wtf/go-nostr"
|
"github.com/nbd-wtf/go-nostr"
|
||||||
"github.com/urfave/cli/v3"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const CATEGORY_FILTER_ATTRIBUTES = "FILTER ATTRIBUTES"
|
const (
|
||||||
|
CATEGORY_FILTER_ATTRIBUTES = "FILTER ATTRIBUTES"
|
||||||
|
// CATEGORY_SIGNER = "SIGNER OPTIONS" -- defined at event.go as the same (yes, I know)
|
||||||
|
)
|
||||||
|
|
||||||
var req = &cli.Command{
|
var req = &cli.Command{
|
||||||
Name: "req",
|
Name: "req",
|
||||||
Usage: "generates encoded REQ messages and optionally use them to talk to relays",
|
Usage: "generates encoded REQ messages and optionally use them to talk to relays",
|
||||||
Description: `outputs a NIP-01 Nostr filter. when a relay is not given, will print the filter, otherwise will connect to the given relay and send the filter.
|
Description: `outputs a nip01 Nostr filter. when a relay is not given, will print the filter, otherwise will connect to the given relay and send the filter.
|
||||||
|
|
||||||
example:
|
example:
|
||||||
nak req -k 1 -l 15 wss://nostr.wine wss://nostr-pub.wellorder.net
|
nak req -k 1 -l 15 wss://nostr.wine wss://nostr-pub.wellorder.net
|
||||||
@@ -28,7 +29,128 @@ it can also take a filter from stdin, optionally modify it with flags and send i
|
|||||||
|
|
||||||
example:
|
example:
|
||||||
echo '{"kinds": [1], "#t": ["test"]}' | nak req -l 5 -k 4549 --tag t=spam wss://nostr-pub.wellorder.net`,
|
echo '{"kinds": [1], "#t": ["test"]}' | nak req -l 5 -k 4549 --tag t=spam wss://nostr-pub.wellorder.net`,
|
||||||
Flags: []cli.Flag{
|
DisableSliceFlagSeparator: true,
|
||||||
|
Flags: append(defaultKeyFlags,
|
||||||
|
append(reqFilterFlags,
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "stream",
|
||||||
|
Usage: "keep the subscription open, printing all events as they are returned",
|
||||||
|
DefaultText: "false, will close on EOSE",
|
||||||
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "paginate",
|
||||||
|
Usage: "make multiple REQs to the relay decreasing the value of 'until' until 'limit' or 'since' conditions are met",
|
||||||
|
DefaultText: "false",
|
||||||
|
},
|
||||||
|
&cli.DurationFlag{
|
||||||
|
Name: "paginate-interval",
|
||||||
|
Usage: "time between queries when using --paginate",
|
||||||
|
},
|
||||||
|
&cli.UintFlag{
|
||||||
|
Name: "paginate-global-limit",
|
||||||
|
Usage: "global limit at which --paginate should stop",
|
||||||
|
DefaultText: "uses the value given by --limit/-l or infinite",
|
||||||
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "bare",
|
||||||
|
Usage: "when printing the filter, print just the filter, not enveloped in a [\"REQ\", ...] array",
|
||||||
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "auth",
|
||||||
|
Usage: "always perform nip42 \"AUTH\" when facing an \"auth-required: \" rejection and try again",
|
||||||
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "force-pre-auth",
|
||||||
|
Aliases: []string{"fpa"},
|
||||||
|
Usage: "after connecting, for a nip42 \"AUTH\" message to be received, act on it and only then send the \"REQ\"",
|
||||||
|
Category: CATEGORY_SIGNER,
|
||||||
|
},
|
||||||
|
)...,
|
||||||
|
),
|
||||||
|
ArgsUsage: "[relay...]",
|
||||||
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
|
relayUrls := c.Args().Slice()
|
||||||
|
if len(relayUrls) > 0 {
|
||||||
|
relays := connectToAllRelays(ctx,
|
||||||
|
relayUrls,
|
||||||
|
c.Bool("force-pre-auth"),
|
||||||
|
nostr.WithAuthHandler(
|
||||||
|
func(ctx context.Context, authEvent nostr.RelayEvent) error {
|
||||||
|
if !c.Bool("auth") && !c.Bool("force-pre-auth") {
|
||||||
|
return fmt.Errorf("auth not authorized")
|
||||||
|
}
|
||||||
|
kr, _, err := gatherKeyerFromArguments(ctx, c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
pk, _ := kr.GetPublicKey(ctx)
|
||||||
|
log("performing auth as %s... ", pk)
|
||||||
|
|
||||||
|
return kr.SignEvent(ctx, authEvent.Event)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if len(relays) == 0 {
|
||||||
|
log("failed to connect to any of the given relays.\n")
|
||||||
|
os.Exit(3)
|
||||||
|
}
|
||||||
|
relayUrls = make([]string, len(relays))
|
||||||
|
for i, relay := range relays {
|
||||||
|
relayUrls[i] = relay.URL
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
for _, relay := range relays {
|
||||||
|
relay.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
for stdinFilter := range getStdinLinesOrBlank() {
|
||||||
|
filter := nostr.Filter{}
|
||||||
|
if stdinFilter != "" {
|
||||||
|
if err := easyjson.Unmarshal([]byte(stdinFilter), &filter); err != nil {
|
||||||
|
ctx = lineProcessingError(ctx, "invalid filter '%s' received from stdin: %s", stdinFilter, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := applyFlagsToFilter(c, &filter); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(relayUrls) > 0 {
|
||||||
|
fn := sys.Pool.SubManyEose
|
||||||
|
if c.Bool("paginate") {
|
||||||
|
fn = paginateWithParams(c.Duration("paginate-interval"), c.Uint("paginate-global-limit"))
|
||||||
|
} else if c.Bool("stream") {
|
||||||
|
fn = sys.Pool.SubMany
|
||||||
|
}
|
||||||
|
|
||||||
|
for ie := range fn(ctx, relayUrls, nostr.Filters{filter}) {
|
||||||
|
stdout(ie.Event)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// no relays given, will just print the filter
|
||||||
|
var result string
|
||||||
|
if c.Bool("bare") {
|
||||||
|
result = filter.String()
|
||||||
|
} else {
|
||||||
|
j, _ := json.Marshal(nostr.ReqEnvelope{SubscriptionID: "nak", Filters: nostr.Filters{filter}})
|
||||||
|
result = string(j)
|
||||||
|
}
|
||||||
|
|
||||||
|
stdout(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exitIfLineProcessingError(ctx)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var reqFilterFlags = []cli.Flag{
|
||||||
&cli.StringSliceFlag{
|
&cli.StringSliceFlag{
|
||||||
Name: "author",
|
Name: "author",
|
||||||
Aliases: []string{"a"},
|
Aliases: []string{"a"},
|
||||||
@@ -68,19 +190,19 @@ example:
|
|||||||
Usage: "shortcut for --tag d=<value>",
|
Usage: "shortcut for --tag d=<value>",
|
||||||
Category: CATEGORY_FILTER_ATTRIBUTES,
|
Category: CATEGORY_FILTER_ATTRIBUTES,
|
||||||
},
|
},
|
||||||
&cli.StringFlag{
|
&NaturalTimeFlag{
|
||||||
Name: "since",
|
Name: "since",
|
||||||
Aliases: []string{"s"},
|
Aliases: []string{"s"},
|
||||||
Usage: "only accept events newer than this (unix timestamp)",
|
Usage: "only accept events newer than this (unix timestamp)",
|
||||||
Category: CATEGORY_FILTER_ATTRIBUTES,
|
Category: CATEGORY_FILTER_ATTRIBUTES,
|
||||||
},
|
},
|
||||||
&cli.StringFlag{
|
&NaturalTimeFlag{
|
||||||
Name: "until",
|
Name: "until",
|
||||||
Aliases: []string{"u"},
|
Aliases: []string{"u"},
|
||||||
Usage: "only accept events older than this (unix timestamp)",
|
Usage: "only accept events older than this (unix timestamp)",
|
||||||
Category: CATEGORY_FILTER_ATTRIBUTES,
|
Category: CATEGORY_FILTER_ATTRIBUTES,
|
||||||
},
|
},
|
||||||
&cli.IntFlag{
|
&cli.UintFlag{
|
||||||
Name: "limit",
|
Name: "limit",
|
||||||
Aliases: []string{"l"},
|
Aliases: []string{"l"},
|
||||||
Usage: "only accept up to this number of events",
|
Usage: "only accept up to this number of events",
|
||||||
@@ -88,105 +210,12 @@ example:
|
|||||||
},
|
},
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: "search",
|
Name: "search",
|
||||||
Usage: "a NIP-50 search query, use it only with relays that explicitly support it",
|
Usage: "a nip50 search query, use it only with relays that explicitly support it",
|
||||||
Category: CATEGORY_FILTER_ATTRIBUTES,
|
Category: CATEGORY_FILTER_ATTRIBUTES,
|
||||||
},
|
},
|
||||||
&cli.BoolFlag{
|
|
||||||
Name: "stream",
|
|
||||||
Usage: "keep the subscription open, printing all events as they are returned",
|
|
||||||
DefaultText: "false, will close on EOSE",
|
|
||||||
},
|
|
||||||
&cli.BoolFlag{
|
|
||||||
Name: "bare",
|
|
||||||
Usage: "when printing the filter, print just the filter, not enveloped in a [\"REQ\", ...] array",
|
|
||||||
},
|
|
||||||
&cli.BoolFlag{
|
|
||||||
Name: "auth",
|
|
||||||
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{
|
|
||||||
Name: "sec",
|
|
||||||
Usage: "secret key to sign the AUTH challenge, as hex or nsec",
|
|
||||||
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 AUTH challenge",
|
|
||||||
},
|
|
||||||
&cli.StringFlag{
|
|
||||||
Name: "connect",
|
|
||||||
Usage: "sign AUTH 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",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
ArgsUsage: "[relay...]",
|
|
||||||
Action: func(ctx context.Context, c *cli.Command) error {
|
|
||||||
var pool *nostr.SimplePool
|
|
||||||
|
|
||||||
relayUrls := c.Args().Slice()
|
|
||||||
if len(relayUrls) > 0 {
|
|
||||||
var relays []*nostr.Relay
|
|
||||||
pool, relays = connectToAllRelays(ctx, relayUrls, c.Bool("force-pre-auth"), nostr.WithAuthHandler(func(evt *nostr.Event) error {
|
|
||||||
if !c.Bool("auth") && !c.Bool("force-pre-auth") {
|
|
||||||
return fmt.Errorf("auth not authorized")
|
|
||||||
}
|
|
||||||
sec, bunker, err := gatherSecretKeyOrBunkerFromArguments(ctx, c)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var pk string
|
|
||||||
if bunker != nil {
|
|
||||||
pk, err = bunker.GetPublicKey(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get public key from bunker: %w", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
pk, _ = nostr.GetPublicKey(sec)
|
|
||||||
}
|
|
||||||
log("performing auth as %s... ", pk)
|
|
||||||
|
|
||||||
if bunker != nil {
|
|
||||||
return bunker.SignEvent(ctx, evt)
|
|
||||||
} else {
|
|
||||||
return evt.Sign(sec)
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
if len(relays) == 0 {
|
|
||||||
log("failed to connect to any of the given relays.\n")
|
|
||||||
os.Exit(3)
|
|
||||||
}
|
|
||||||
relayUrls = make([]string, len(relays))
|
|
||||||
for i, relay := range relays {
|
|
||||||
relayUrls[i] = relay.URL
|
|
||||||
}
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
for _, relay := range relays {
|
|
||||||
relay.Close()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
for stdinFilter := range getStdinLinesOrBlank() {
|
|
||||||
filter := nostr.Filter{}
|
|
||||||
if stdinFilter != "" {
|
|
||||||
if err := easyjson.Unmarshal([]byte(stdinFilter), &filter); err != nil {
|
|
||||||
ctx = lineProcessingError(ctx, "invalid filter '%s' received from stdin: %s", stdinFilter, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func applyFlagsToFilter(c *cli.Command, filter *nostr.Filter) error {
|
||||||
if authors := c.StringSlice("author"); len(authors) > 0 {
|
if authors := c.StringSlice("author"); len(authors) > 0 {
|
||||||
filter.Authors = append(filter.Authors, authors...)
|
filter.Authors = append(filter.Authors, authors...)
|
||||||
}
|
}
|
||||||
@@ -201,8 +230,8 @@ example:
|
|||||||
}
|
}
|
||||||
tags := make([][]string, 0, 5)
|
tags := make([][]string, 0, 5)
|
||||||
for _, tagFlag := range c.StringSlice("tag") {
|
for _, tagFlag := range c.StringSlice("tag") {
|
||||||
spl := strings.Split(tagFlag, "=")
|
spl := strings.SplitN(tagFlag, "=", 2)
|
||||||
if len(spl) == 2 && len(spl[0]) == 1 {
|
if len(spl) == 2 {
|
||||||
tags = append(tags, spl)
|
tags = append(tags, spl)
|
||||||
} else {
|
} else {
|
||||||
return fmt.Errorf("invalid --tag '%s'", tagFlag)
|
return fmt.Errorf("invalid --tag '%s'", tagFlag)
|
||||||
@@ -229,57 +258,21 @@ example:
|
|||||||
filter.Tags[tag[0]] = append(filter.Tags[tag[0]], tag[1])
|
filter.Tags[tag[0]] = append(filter.Tags[tag[0]], tag[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
if since := c.String("since"); since != "" {
|
if c.IsSet("since") {
|
||||||
if since == "now" {
|
nts := getNaturalDate(c, "since")
|
||||||
ts := nostr.Now()
|
filter.Since = &nts
|
||||||
filter.Since = &ts
|
|
||||||
} else if i, err := strconv.Atoi(since); err == nil {
|
|
||||||
ts := nostr.Timestamp(i)
|
|
||||||
filter.Since = &ts
|
|
||||||
} else {
|
|
||||||
return fmt.Errorf("parse error: Invalid numeric literal %q", since)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if c.IsSet("until") {
|
||||||
|
nts := getNaturalDate(c, "until")
|
||||||
|
filter.Until = &nts
|
||||||
}
|
}
|
||||||
if until := c.String("until"); until != "" {
|
|
||||||
if until == "now" {
|
if limit := c.Uint("limit"); limit != 0 {
|
||||||
ts := nostr.Now()
|
|
||||||
filter.Until = &ts
|
|
||||||
} else if i, err := strconv.Atoi(until); err == nil {
|
|
||||||
ts := nostr.Timestamp(i)
|
|
||||||
filter.Until = &ts
|
|
||||||
} else {
|
|
||||||
return fmt.Errorf("parse error: Invalid numeric literal %q", until)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if limit := c.Int("limit"); limit != 0 {
|
|
||||||
filter.Limit = int(limit)
|
filter.Limit = int(limit)
|
||||||
} else if c.IsSet("limit") || c.Bool("stream") {
|
} else if c.IsSet("limit") {
|
||||||
filter.LimitZero = true
|
filter.LimitZero = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(relayUrls) > 0 {
|
|
||||||
fn := pool.SubManyEose
|
|
||||||
if c.Bool("stream") {
|
|
||||||
fn = pool.SubMany
|
|
||||||
}
|
|
||||||
for ie := range fn(ctx, relayUrls, nostr.Filters{filter}) {
|
|
||||||
stdout(ie.Event)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// no relays given, will just print the filter
|
|
||||||
var result string
|
|
||||||
if c.Bool("bare") {
|
|
||||||
result = filter.String()
|
|
||||||
} else {
|
|
||||||
j, _ := json.Marshal(nostr.ReqEnvelope{SubscriptionID: "nak", Filters: nostr.Filters{filter}})
|
|
||||||
result = string(j)
|
|
||||||
}
|
|
||||||
|
|
||||||
stdout(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exitIfLineProcessingError(ctx)
|
|
||||||
return nil
|
return nil
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|||||||
122
serve.go
Normal file
122
serve.go
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/bep/debounce"
|
||||||
|
"github.com/fatih/color"
|
||||||
|
"github.com/fiatjaf/cli/v3"
|
||||||
|
"github.com/fiatjaf/eventstore/slicestore"
|
||||||
|
"github.com/fiatjaf/khatru"
|
||||||
|
"github.com/nbd-wtf/go-nostr"
|
||||||
|
)
|
||||||
|
|
||||||
|
var serve = &cli.Command{
|
||||||
|
Name: "serve",
|
||||||
|
Usage: "starts an in-memory relay for testing purposes",
|
||||||
|
DisableSliceFlagSeparator: true,
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "hostname",
|
||||||
|
Usage: "hostname where to listen for connections",
|
||||||
|
Value: "localhost",
|
||||||
|
},
|
||||||
|
&cli.UintFlag{
|
||||||
|
Name: "port",
|
||||||
|
Usage: "port where to listen for connections",
|
||||||
|
Value: 10547,
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "events",
|
||||||
|
Usage: "file containing the initial batch of events that will be served by the relay as newline-separated JSON (jsonl)",
|
||||||
|
DefaultText: "the relay will start empty",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
|
db := slicestore.SliceStore{MaxLimit: math.MaxInt}
|
||||||
|
|
||||||
|
var scanner *bufio.Scanner
|
||||||
|
if path := c.String("events"); path != "" {
|
||||||
|
f, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to file at '%s': %w", path, err)
|
||||||
|
}
|
||||||
|
scanner = bufio.NewScanner(f)
|
||||||
|
} else if isPiped() {
|
||||||
|
scanner = bufio.NewScanner(os.Stdin)
|
||||||
|
}
|
||||||
|
|
||||||
|
if scanner != nil {
|
||||||
|
scanner.Buffer(make([]byte, 16*1024*1024), 256*1024*1024)
|
||||||
|
i := 0
|
||||||
|
for scanner.Scan() {
|
||||||
|
var evt nostr.Event
|
||||||
|
if err := json.Unmarshal(scanner.Bytes(), &evt); err != nil {
|
||||||
|
return fmt.Errorf("invalid event received at line %d: %s (`%s`)", i, err, scanner.Text())
|
||||||
|
}
|
||||||
|
db.SaveEvent(ctx, &evt)
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rl := khatru.NewRelay()
|
||||||
|
rl.QueryEvents = append(rl.QueryEvents, db.QueryEvents)
|
||||||
|
rl.CountEvents = append(rl.CountEvents, db.CountEvents)
|
||||||
|
rl.DeleteEvent = append(rl.DeleteEvent, db.DeleteEvent)
|
||||||
|
rl.StoreEvent = append(rl.StoreEvent, db.SaveEvent)
|
||||||
|
|
||||||
|
started := make(chan bool)
|
||||||
|
exited := make(chan error)
|
||||||
|
|
||||||
|
hostname := c.String("hostname")
|
||||||
|
port := int(c.Uint("port"))
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
err := rl.Start(hostname, port, started)
|
||||||
|
exited <- err
|
||||||
|
}()
|
||||||
|
|
||||||
|
var printStatus func()
|
||||||
|
|
||||||
|
// relay logging
|
||||||
|
rl.RejectFilter = append(rl.RejectFilter, func(ctx context.Context, filter nostr.Filter) (reject bool, msg string) {
|
||||||
|
log(" got %s %v\n", color.HiYellowString("request"), colors.italic(filter))
|
||||||
|
printStatus()
|
||||||
|
return false, ""
|
||||||
|
})
|
||||||
|
rl.RejectCountFilter = append(rl.RejectCountFilter, func(ctx context.Context, filter nostr.Filter) (reject bool, msg string) {
|
||||||
|
log(" got %s %v\n", color.HiCyanString("count request"), colors.italic(filter))
|
||||||
|
printStatus()
|
||||||
|
return false, ""
|
||||||
|
})
|
||||||
|
rl.RejectEvent = append(rl.RejectEvent, func(ctx context.Context, event *nostr.Event) (reject bool, msg string) {
|
||||||
|
log(" got %s %v\n", color.BlueString("event"), colors.italic(event))
|
||||||
|
printStatus()
|
||||||
|
return false, ""
|
||||||
|
})
|
||||||
|
|
||||||
|
d := debounce.New(time.Second * 2)
|
||||||
|
printStatus = func() {
|
||||||
|
d(func() {
|
||||||
|
totalEvents := 0
|
||||||
|
ch, _ := db.QueryEvents(ctx, nostr.Filter{})
|
||||||
|
for range ch {
|
||||||
|
totalEvents++
|
||||||
|
}
|
||||||
|
subs := rl.GetListeningFilters()
|
||||||
|
|
||||||
|
log(" %s events stored: %s, subscriptions opened: %s\n", color.HiMagentaString("•"), color.HiMagentaString("%d", totalEvents), color.HiMagentaString("%d", len(subs)))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
<-started
|
||||||
|
log("%s relay running at %s\n", color.HiRedString(">"), colors.boldf("ws://%s:%d", hostname, port))
|
||||||
|
|
||||||
|
return <-exited
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -2,10 +2,9 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
|
|
||||||
|
"github.com/fiatjaf/cli/v3"
|
||||||
"github.com/nbd-wtf/go-nostr"
|
"github.com/nbd-wtf/go-nostr"
|
||||||
"github.com/urfave/cli/v3"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var verify = &cli.Command{
|
var verify = &cli.Command{
|
||||||
@@ -15,6 +14,7 @@ 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.`,
|
||||||
|
DisableSliceFlagSeparator: true,
|
||||||
Action: func(ctx context.Context, c *cli.Command) 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{}
|
||||||
@@ -31,7 +31,7 @@ it outputs nothing if the verification is successful.`,
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ok, err := evt.CheckSignature(); !ok {
|
if ok, err := evt.CheckSignature(); !ok {
|
||||||
ctx = lineProcessingError(ctx, "invalid signature: %s", err)
|
ctx = lineProcessingError(ctx, "invalid signature: %v", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
487
wallet.go
Normal file
487
wallet.go
Normal file
@@ -0,0 +1,487 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/fatih/color"
|
||||||
|
"github.com/fiatjaf/cli/v3"
|
||||||
|
"github.com/nbd-wtf/go-nostr"
|
||||||
|
"github.com/nbd-wtf/go-nostr/nip60"
|
||||||
|
"github.com/nbd-wtf/go-nostr/nip61"
|
||||||
|
)
|
||||||
|
|
||||||
|
func prepareWallet(ctx context.Context, c *cli.Command) (*nip60.Wallet, func(), error) {
|
||||||
|
kr, _, err := gatherKeyerFromArguments(ctx, c)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pk, err := kr.GetPublicKey(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
relays := sys.FetchOutboxRelays(ctx, pk, 3)
|
||||||
|
w := nip60.LoadWallet(ctx, kr, sys.Pool, relays)
|
||||||
|
if w == nil {
|
||||||
|
return nil, nil, fmt.Errorf("error loading walle")
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Processed = func(evt *nostr.Event, err error) {
|
||||||
|
if err == nil {
|
||||||
|
logverbose("processed event %s\n", evt)
|
||||||
|
} else {
|
||||||
|
log("error processing event %s: %s\n", evt, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
w.PublishUpdate = func(event nostr.Event, deleted, received, change *nip60.Token, isHistory bool) {
|
||||||
|
desc := "wallet"
|
||||||
|
if received != nil {
|
||||||
|
mint, _ := strings.CutPrefix(received.Mint, "https://")
|
||||||
|
desc = fmt.Sprintf("received from %s with %d proofs totalling %d",
|
||||||
|
mint, len(received.Proofs), received.Proofs.Amount())
|
||||||
|
} else if change != nil {
|
||||||
|
mint, _ := strings.CutPrefix(change.Mint, "https://")
|
||||||
|
desc = fmt.Sprintf("change from %s with %d proofs totalling %d",
|
||||||
|
mint, len(change.Proofs), change.Proofs.Amount())
|
||||||
|
} else if deleted != nil {
|
||||||
|
mint, _ := strings.CutPrefix(deleted.Mint, "https://")
|
||||||
|
desc = fmt.Sprintf("deleting a used token from %s with %d proofs totalling %d",
|
||||||
|
mint, len(deleted.Proofs), deleted.Proofs.Amount())
|
||||||
|
} else if isHistory {
|
||||||
|
desc = "history entry"
|
||||||
|
}
|
||||||
|
|
||||||
|
log("- saving kind:%d event (%s)... ", event.Kind, desc)
|
||||||
|
first := true
|
||||||
|
for res := range sys.Pool.PublishMany(ctx, relays, event) {
|
||||||
|
cleanUrl, ok := strings.CutPrefix(res.RelayURL, "wss://")
|
||||||
|
if !ok {
|
||||||
|
cleanUrl = res.RelayURL
|
||||||
|
}
|
||||||
|
|
||||||
|
if !first {
|
||||||
|
log(", ")
|
||||||
|
}
|
||||||
|
first = false
|
||||||
|
|
||||||
|
if res.Error != nil {
|
||||||
|
log("%s: %s", color.RedString(cleanUrl), res.Error)
|
||||||
|
} else {
|
||||||
|
log("%s: ok", color.GreenString(cleanUrl))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
<-w.Stable
|
||||||
|
|
||||||
|
return w, func() {
|
||||||
|
w.Close()
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var wallet = &cli.Command{
|
||||||
|
Name: "wallet",
|
||||||
|
Usage: "displays the current wallet balance",
|
||||||
|
Description: "all wallet data is stored on Nostr relays, signed and encrypted with the given key, and reloaded again from relays on every call.\n\nthe same data can be accessed by other compatible nip60 clients.",
|
||||||
|
DisableSliceFlagSeparator: true,
|
||||||
|
Flags: defaultKeyFlags,
|
||||||
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
|
w, closew, err := prepareWallet(ctx, c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
stdout(w.Balance())
|
||||||
|
|
||||||
|
closew()
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
Commands: []*cli.Command{
|
||||||
|
{
|
||||||
|
Name: "mints",
|
||||||
|
Usage: "lists, adds or remove default mints from the wallet",
|
||||||
|
DisableSliceFlagSeparator: true,
|
||||||
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
|
w, closew, err := prepareWallet(ctx, c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, url := range w.Mints {
|
||||||
|
stdout(strings.Split(url, "://")[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
closew()
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
Commands: []*cli.Command{
|
||||||
|
{
|
||||||
|
Name: "add",
|
||||||
|
DisableSliceFlagSeparator: true,
|
||||||
|
ArgsUsage: "<mint>...",
|
||||||
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
|
w, closew, err := prepareWallet(ctx, c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := w.AddMint(ctx, c.Args().Slice()...); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
closew()
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "remove",
|
||||||
|
DisableSliceFlagSeparator: true,
|
||||||
|
ArgsUsage: "<mint>...",
|
||||||
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
|
w, closew, err := prepareWallet(ctx, c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := w.RemoveMint(ctx, c.Args().Slice()...); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
closew()
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "tokens",
|
||||||
|
Usage: "lists existing tokens with their mints and aggregated amounts",
|
||||||
|
DisableSliceFlagSeparator: true,
|
||||||
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
|
w, closew, err := prepareWallet(ctx, c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, token := range w.Tokens {
|
||||||
|
stdout(token.ID(), token.Proofs.Amount(), strings.Split(token.Mint, "://")[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
closew()
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "receive",
|
||||||
|
Usage: "takes a cashu token string as an argument and adds it to the wallet",
|
||||||
|
ArgsUsage: "<token>",
|
||||||
|
DisableSliceFlagSeparator: true,
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.StringSliceFlag{
|
||||||
|
Name: "mint",
|
||||||
|
Usage: "mint to swap the token into",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
|
args := c.Args().Slice()
|
||||||
|
if len(args) != 1 {
|
||||||
|
return fmt.Errorf("must be called as `nak wallet receive <token>")
|
||||||
|
}
|
||||||
|
|
||||||
|
w, closew, err := prepareWallet(ctx, c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
proofs, mint, err := nip60.GetProofsAndMint(args[0])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := make([]nip60.ReceiveOption, 0, 1)
|
||||||
|
for _, url := range c.StringSlice("mint") {
|
||||||
|
opts = append(opts, nip60.WithMintDestination(url))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := w.Receive(ctx, proofs, mint, opts...); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
closew()
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "send",
|
||||||
|
Usage: "prints a cashu token with the given amount for sending to someone else",
|
||||||
|
ArgsUsage: "<amount>",
|
||||||
|
DisableSliceFlagSeparator: true,
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "mint",
|
||||||
|
Usage: "send from a specific mint",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
|
args := c.Args().Slice()
|
||||||
|
if len(args) != 1 {
|
||||||
|
return fmt.Errorf("must be called as `nak wallet send <amount>")
|
||||||
|
}
|
||||||
|
amount, err := strconv.ParseUint(args[0], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("amount '%s' is invalid", args[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
w, closew, err := prepareWallet(ctx, c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := make([]nip60.SendOption, 0, 1)
|
||||||
|
if mint := c.String("mint"); mint != "" {
|
||||||
|
mint = "http" + nostr.NormalizeURL(mint)[2:]
|
||||||
|
opts = append(opts, nip60.WithMint(mint))
|
||||||
|
}
|
||||||
|
proofs, mint, err := w.Send(ctx, amount, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
stdout(nip60.MakeTokenString(proofs, mint))
|
||||||
|
|
||||||
|
closew()
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "pay",
|
||||||
|
Usage: "pays a bolt11 lightning invoice and outputs the preimage",
|
||||||
|
ArgsUsage: "<invoice>",
|
||||||
|
DisableSliceFlagSeparator: true,
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "mint",
|
||||||
|
Usage: "pay from a specific mint",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
|
args := c.Args().Slice()
|
||||||
|
if len(args) != 1 {
|
||||||
|
return fmt.Errorf("must be called as `nak wallet pay <invoice>")
|
||||||
|
}
|
||||||
|
|
||||||
|
w, closew, err := prepareWallet(ctx, c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := make([]nip60.SendOption, 0, 1)
|
||||||
|
if mint := c.String("mint"); mint != "" {
|
||||||
|
mint = "http" + nostr.NormalizeURL(mint)[2:]
|
||||||
|
opts = append(opts, nip60.WithMint(mint))
|
||||||
|
}
|
||||||
|
|
||||||
|
preimage, err := w.PayBolt11(ctx, args[0], opts...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
stdout(preimage)
|
||||||
|
|
||||||
|
closew()
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "nutzap",
|
||||||
|
Usage: "sends a nip61 nutzap to one or more Nostr profiles and/or events",
|
||||||
|
ArgsUsage: "<amount> <target>",
|
||||||
|
Description: "<amount> is in satoshis, <target> can be an npub, nprofile, nevent or hex pubkey.",
|
||||||
|
DisableSliceFlagSeparator: true,
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "mint",
|
||||||
|
Usage: "send from a specific mint",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "message",
|
||||||
|
Usage: "attach a message to the nutzap",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
|
args := c.Args().Slice()
|
||||||
|
if len(args) >= 2 {
|
||||||
|
return fmt.Errorf("must be called as `nak wallet send <amount> <target>...")
|
||||||
|
}
|
||||||
|
|
||||||
|
w, closew, err := prepareWallet(ctx, c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
amount := c.Uint("amount")
|
||||||
|
target := c.String("target")
|
||||||
|
|
||||||
|
var evt *nostr.Event
|
||||||
|
var eventId string
|
||||||
|
|
||||||
|
if strings.HasPrefix(target, "nevent1") {
|
||||||
|
evt, _, err = sys.FetchSpecificEventFromInput(ctx, target, false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
eventId = evt.ID
|
||||||
|
target = evt.PubKey
|
||||||
|
}
|
||||||
|
|
||||||
|
pm, err := sys.FetchProfileFromInput(ctx, target)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log("sending %d sat to '%s' (%s)", amount, pm.ShortName(), pm.Npub())
|
||||||
|
|
||||||
|
opts := make([]nip60.SendOption, 0, 1)
|
||||||
|
if mint := c.String("mint"); mint != "" {
|
||||||
|
mint = "http" + nostr.NormalizeURL(mint)[2:]
|
||||||
|
opts = append(opts, nip60.WithMint(mint))
|
||||||
|
}
|
||||||
|
|
||||||
|
kr, _, _ := gatherKeyerFromArguments(ctx, c)
|
||||||
|
results, err := nip61.SendNutzap(
|
||||||
|
ctx,
|
||||||
|
kr,
|
||||||
|
w,
|
||||||
|
sys.Pool,
|
||||||
|
pm.PubKey,
|
||||||
|
sys.FetchInboxRelays,
|
||||||
|
sys.FetchOutboxRelays(ctx, pm.PubKey, 3),
|
||||||
|
eventId,
|
||||||
|
amount,
|
||||||
|
c.String("message"),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log("- publishing nutzap... ")
|
||||||
|
first := true
|
||||||
|
for res := range results {
|
||||||
|
cleanUrl, ok := strings.CutPrefix(res.RelayURL, "wss://")
|
||||||
|
if !ok {
|
||||||
|
cleanUrl = res.RelayURL
|
||||||
|
}
|
||||||
|
|
||||||
|
if !first {
|
||||||
|
log(", ")
|
||||||
|
}
|
||||||
|
first = false
|
||||||
|
if res.Error != nil {
|
||||||
|
log("%s: %s", color.RedString(cleanUrl), res.Error)
|
||||||
|
} else {
|
||||||
|
log("%s: ok", color.GreenString(cleanUrl))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
closew()
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
Commands: []*cli.Command{
|
||||||
|
{
|
||||||
|
Name: "setup",
|
||||||
|
Usage: "setup your wallet private key and kind:10019 event for receiving nutzaps",
|
||||||
|
DisableSliceFlagSeparator: true,
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.StringSliceFlag{
|
||||||
|
Name: "mint",
|
||||||
|
Usage: "mints to receive nutzaps in",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "private-key",
|
||||||
|
Usage: "private key used for receiving nutzaps",
|
||||||
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "force",
|
||||||
|
Aliases: []string{"f"},
|
||||||
|
Usage: "forces replacement of private-key",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
|
w, closew, err := prepareWallet(ctx, c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if w.PrivateKey == nil {
|
||||||
|
if sk := c.String("private-key"); sk != "" {
|
||||||
|
if err := w.SetPrivateKey(ctx, sk); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("missing --private-key")
|
||||||
|
}
|
||||||
|
} else if sk := c.String("private-key"); sk != "" && !c.Bool("force") {
|
||||||
|
return fmt.Errorf("refusing to replace existing private key, use the --force flag")
|
||||||
|
}
|
||||||
|
|
||||||
|
kr, _, _ := gatherKeyerFromArguments(ctx, c)
|
||||||
|
pk, _ := kr.GetPublicKey(ctx)
|
||||||
|
relays := sys.FetchWriteRelays(ctx, pk, 6)
|
||||||
|
|
||||||
|
info := nip61.Info{}
|
||||||
|
ie := sys.Pool.QuerySingle(ctx, relays, nostr.Filter{
|
||||||
|
Kinds: []int{10019},
|
||||||
|
Authors: []string{pk},
|
||||||
|
Limit: 1,
|
||||||
|
})
|
||||||
|
if ie != nil {
|
||||||
|
info.ParseEvent(ie.Event)
|
||||||
|
}
|
||||||
|
|
||||||
|
if mints := c.StringSlice("mints"); len(mints) == 0 && len(info.Mints) == 0 {
|
||||||
|
info.Mints = w.Mints
|
||||||
|
}
|
||||||
|
if len(info.Mints) == 0 {
|
||||||
|
return fmt.Errorf("missing --mint")
|
||||||
|
}
|
||||||
|
|
||||||
|
evt := nostr.Event{}
|
||||||
|
if err := info.ToEvent(ctx, kr, &evt); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
stdout(evt)
|
||||||
|
log("- saving kind:10019 event... ")
|
||||||
|
first := true
|
||||||
|
for res := range sys.Pool.PublishMany(ctx, relays, evt) {
|
||||||
|
cleanUrl, ok := strings.CutPrefix(res.RelayURL, "wss://")
|
||||||
|
if !ok {
|
||||||
|
cleanUrl = res.RelayURL
|
||||||
|
}
|
||||||
|
|
||||||
|
if !first {
|
||||||
|
log(", ")
|
||||||
|
}
|
||||||
|
first = false
|
||||||
|
|
||||||
|
if res.Error != nil {
|
||||||
|
log("%s: %s", color.RedString(cleanUrl), res.Error)
|
||||||
|
} else {
|
||||||
|
log("%s: ok", color.GreenString(cleanUrl))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
closew()
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
14
zapstore.yaml
Normal file
14
zapstore.yaml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
nak:
|
||||||
|
cli:
|
||||||
|
name: nak
|
||||||
|
summary: a command line tool for doing all things nostr
|
||||||
|
repository: https://github.com/fiatjaf/nak
|
||||||
|
artifacts:
|
||||||
|
nak-v%v-darwin-arm64:
|
||||||
|
platforms: [darwin-arm64]
|
||||||
|
nak-v%v-darwin-amd64:
|
||||||
|
platforms: [darwin-x86_64]
|
||||||
|
nak-v%v-linux-arm64:
|
||||||
|
platforms: [linux-aarch64]
|
||||||
|
nak-v%v-linux-amd64:
|
||||||
|
platforms: [linux-x86_64]
|
||||||
Reference in New Issue
Block a user