Compare commits

...

46 Commits

Author SHA1 Message Date
fiatjaf
d95b6f50ff --prompt-sec for getting a secret key from a prompt. 2023-11-08 14:26:25 -03:00
fiatjaf
200e4e61f7 add a more complex example of fetching subnotes to readme. 2023-11-08 12:56:38 -03:00
fiatjaf
714d65312c support multiline stdin on decode, encode and fetch, and improve the helpers. 2023-11-08 12:50:36 -03:00
fiatjaf
5722061bf3 update go-nostr and sdk to standalone module. 2023-11-08 00:13:28 -03:00
fiatjaf
6f72d3c133 fix pipe check. 2023-11-07 23:51:07 -03:00
fiatjaf
78932833df support running nak with multiple lines of stdin sequentially. 2023-11-07 23:43:37 -03:00
fiatjaf_
31b42c3499 public domain license. 2023-11-04 08:02:34 -03:00
fiatjaf
7bce92f56d nak verify 2023-11-02 08:10:29 -03:00
fiatjaf
be8e3dfb39 tweak gh actions build settings. 2023-11-02 08:10:29 -03:00
Bitkarrot
e681ad87cd fix install instructions (#5) 2023-10-30 22:53:08 -03:00
fiatjaf
9e5e736395 update readme with some helpful examples instead of a giant wall of text. 2023-10-29 23:39:52 -03:00
fiatjaf
c5573410df move nak-web into a separate repository. 2023-10-29 23:39:52 -03:00
fiatjaf
85d658bdd4 github action to publish the cli binaries. 2023-10-29 23:15:04 -03:00
fiatjaf
bf966b3e2c nak event can take (and optionally modify) events from stdin. 2023-10-29 21:48:18 -03:00
fiatjaf
0615a8b577 nak req can take (and optionally modify) filters from stdin. 2023-10-29 19:11:35 -03:00
fiatjaf
c6e9fdd053 trim spaces from stdin. 2023-10-23 08:04:28 -03:00
fiatjaf
50dde2117c don't fail encode when reading from stdin because of the number of arguments. 2023-10-23 08:04:21 -03:00
fiatjaf
ffa41046fd fetch with optional --relay flags. 2023-10-20 21:01:11 -03:00
fiatjaf
757a6eb313 support fetch npub 2023-10-20 20:57:41 -03:00
fiatjaf
208d909727 support reading from stdin. 2023-10-20 20:57:29 -03:00
fiatjaf
459b127988 fetch: use relay hints from author pubkeys. 2023-10-15 09:22:45 -03:00
fiatjaf
db157e6181 fetch method to fetch events from nip19 codes and relay hints. 2023-10-15 09:18:23 -03:00
fiatjaf
ada76f281a add encode note. 2023-10-10 11:29:06 -03:00
fiatjaf
455ec79e58 NIP-50 search filter on req. 2023-10-08 15:49:11 -03:00
fiatjaf
8d111e556e count uses all relays again, now correctly. 2023-10-08 15:31:20 -03:00
fiatjaf
e4a9b3ccc7 fix count again, it was sending REQs instead of COUNTs to relays. only use the first relay. 2023-10-08 15:05:51 -03:00
fiatjaf
3896ef323b update go-nostr dependency. 2023-10-08 14:47:45 -03:00
fiatjaf
c214513304 improve count. 2023-10-08 14:40:46 -03:00
Yasuhiro Matsumoto
6ccca357e2 support NIP-45 COUNT 2023-08-23 12:50:23 -03:00
fiatjaf
f9cf01b48b do target validation on a case-by-case basis and don't validate empty -author on nevent. 2023-07-08 20:52:50 -03:00
fiatjaf
fb7c49bb5c add nak encode to readme. 2023-07-05 15:05:09 -03:00
fiatjaf
4ad0769a62 add encode command with support for all the nip19 things. 2023-07-05 15:03:26 -03:00
fiatjaf
3ace11d7b2 support --nson flag on event. 2023-07-05 14:11:15 -03:00
fiatjaf
194e94ec9a update go-nostr for OK-related security fix. 2023-06-26 21:05:01 -03:00
fiatjaf
2b2018b742 allow extra tag elements on event creation, separated by ";" 2023-06-26 20:52:12 -03:00
fiatjaf
30c8eb83b2 rename editable to selectable. 2023-06-20 15:33:29 -03:00
fiatjaf
015cfd857c print naddr if parsed event is replaceable. 2023-06-20 15:31:53 -03:00
fiatjaf
4e5f7e6d21 print naddr when given an "a" tag. 2023-06-20 15:21:25 -03:00
fiatjaf
fb9faf24ae mention that the "d" tag is the identifier. 2023-06-20 14:52:03 -03:00
fiatjaf
76ca99a73b clicking on edit button to fill in the input. 2023-06-20 14:49:56 -03:00
fiatjaf
7890466783 removing relay hints. 2023-06-20 11:57:01 -03:00
fiatjaf
ba2d86ca33 each relay hint in a separate component. 2023-06-20 11:48:50 -03:00
fiatjaf
dff57c207e accept tags with keys of any length. 2023-06-07 07:02:38 -03:00
fiatjaf
c3777abd81 fix "if this is a private key" section. 2023-06-02 09:04:25 -03:00
fiatjaf
746a13861d update go-nostr so subscriptions can end. 2023-05-30 17:51:40 -03:00
fiatjaf
bd7b22c4ff cancel publish context after 10 seconds. 2023-05-30 13:43:57 -03:00
28 changed files with 1105 additions and 971 deletions

View File

@@ -1,20 +0,0 @@
name: build page and publish to cloudflare
on:
push:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 1
- uses: olafurpg/setup-scala@v11
- name: build page / compile scalajs
run: sbt fullLinkJS/esBuild
- name: publish to cloudflare
uses: cloudflare/pages-action@v1
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: 60325047cc7d0811c6b337717918cbc1
projectName: nostr-army-knife
directory: .

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

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

24
LICENSE Normal file
View File

@@ -0,0 +1,24 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <https://unlicense.org>

210
README.md
View File

@@ -1,165 +1,83 @@
# nostr army knife
# nak, the nostr army knife
this repository contains two things:
install with `go install github.com/fiatjaf/nak@latest` or
[download a binary](https://github.com/fiatjaf/nak/releases).
## a command-line tool for decoding and encoding nostr entities and talking to relays
## what can you do with it?
Install with `go install github.com/fiatjaf/nak`.
take a look at the help text that comes in it to learn all possibilities, but here are some:
It pairs nicely with https://github.com/blakejakopovic/nostcat using unix pipes.
### examples
### make a nostr event signed with the default key (`01`)
```shell
~> nak event
{"id":"53443506e7d09e55b922a2369b80f926007a8a8a8ea5f09df1db59fe1993335e","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1698632644,"kind":1,"tags":[],"content":"hello from the nostr army knife","sig":"4bdb609c975b2b61338c2ff4c7ce91d4afe74bea4ed1601a62e1fd125bd4c0ae6e0166cca96e5cfb7e0f50583eb6a0dd0b66072566299b6007742db56278010c"}
```
~> nak decode nsec1aqc5q5l8da0l7u6gra4p5xhleclngezlpsgd7z5dx07cpu8sxf2shqgn6y
{
"pubkey": "5b36b874b2b983197ba4be80553b2e4b6db2895a04567cea0aa47585b2e0c620",
"private_key": "e8314053e76f5fff73481f6a1a1affce3f34645f0c10df0a8d33fd80f0f03255"
}
~> nak event -c hello --sec e8314053e76f5fff73481f6a1a1affce3f34645f0c10df0a8d33fd80f0f03255
{"id":"ed840ef37a40cce4f4b8c361e5df13457ad664209cf4a297fd7df7e84fdd32e0","pubkey":"5b36b874b2b983197ba4be80553b2e4b6db2895a04567cea0aa47585b2e0c620","created_at":1683201092,"kind":1,"tags":[],"content":"hello","sig":"304a87dbbdf986a187eb9417316cfe3d6f8f31793ba20c9c6d7e4ebeeefe950d6ecba6098c201b7170c04e27c2f920d607a90f5c8763c35ac806dce37df1d05d"}
~> nak event -c hello --sec e8314053e76f5fff73481f6a1a1affce3f34645f0c10df0a8d33fd80f0f03255 wss://relay.stoner.com wss://nos.lol wss://nostr.wine wss://atlas.nostr.land wss://relay.damus.io
{"id":"54a534647bdcd2751d743fea4fc9eee5dba613887d69425f0891d9c2f82772a5","pubkey":"5b36b874b2b983197ba4be80553b2e4b6db2895a04567cea0aa47585b2e0c620","created_at":1684895417,"kind":1,"tags":[],"content":"hello","sig":"81a14cfe628fab6cd6135bb66f6e8b3bb4bfce4f666462a1303fdfbc9038fd141e73db3fe7e774a8f023fc70622c50a67d4fa41d3d09806c78f051985c11e0bd"}
publishing to wss://relay.stoner.com... failed: msg: blocked: pubkey is not allowed to publish to this relay
publishing to wss://nos.lol... success.
publishing to wss://nostr.wine... failed: msg: blocked: not an active paid member
publishing to wss://atlas.nostr.land... failed: msg: blocked: pubkey not admitted
### make a nostr event with custom content and tags, sign it with a different key and publish it to two relays
```shell
~> nak event --sec 02 -c 'good morning' --tag t=gm wss://nostr-pub.wellorder.net wss://relay.damus.io
{"id":"e20978737ab7cd36eca300a65f11738176123f2e0c23054544b18fe493e2aa1a","pubkey":"c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5","created_at":1698632753,"kind":1,"tags":[["t","gm"]],"content":"good morning","sig":"5687c1a97066c349cb3bde0c0719fd1652a13403ba6aca7557b646307ee6718528cd86989db08bf6a7fd04bea0b0b87c1dd1b78c2d21b80b80eebab7f40b8916"}
publishing to wss://nostr-pub.wellorder.net... success.
publishing to wss://relay.damus.io... success.
```
~> nak decode nevent1qqs29yet5tp0qq5xu5qgkeehkzqh5qu46739axzezcxpj4tjlkx9j7gpr4mhxue69uhkummnw3ez6ur4vgh8wetvd3hhyer9wghxuet5sh59ud
### query a bunch of relays for a tag with a limit of 2 for each, print their content
```shell
~> nak req -k 1 -t t=gm -l 2 wss://nostr.mom wss://nostr.wine wss://nostr-pub.wellorder.net | jq .content
"#GM, you sovereign savage #freeple of the #nostrverse. Let's cause some #nostroversy. "
"ITM slaves!\n#gm https://image.nostr.build/cbbcdf80bfc302a6678ecf9387c87d87deca3e0e288a12e262926c34feb3f6aa.jpg "
"good morning"
"The problem is to start, but along the way it's fixed #GM ☀️"
"Activando modo zen…\n\n#GM #Nostr #Hispano"
```
### decode a nip19 note1 code, add a relay hint, encode it back to nevent1
```shell
~> nak decode note1ttnnrw78wy0hs5fa59yj03yvcu2r4y0xetg9vh7uf4em39n604vsyp37f2 | jq -r .id | nak encode nevent -r wss://nostr.zbd.gg
nevent1qqs94ee3h0rhz8mc2y76zjf8cjxvw9p6j8nv45zktlwy6uacjea86kgpzfmhxue69uhkummnw3ezu7nzvshxwec8zw8h7
~> nak decode nevent1qqs94ee3h0rhz8mc2y76zjf8cjxvw9p6j8nv45zktlwy6uacjea86kgpzfmhxue69uhkummnw3ezu7nzvshxwec8zw8h7
{
"id": "a2932ba2c2f00286e5008b6737b0817a0395d7a25e9859160c195572fd8c5979",
"id": "5ae731bbc7711f78513da14927c48cc7143a91e6cad0565fdc4d73b8967a7d59",
"relays": [
"wss://nostr-pub.wellorder.net"
"wss://nostr.zbd.gg"
]
}
~> nak req -a a2932ba2c2f00286e5008b6737b0817a0395d7a25e9859160c195572fd8c5979 -k 1 -a e8b487c079b0f67c695ae6c4c2552a47f38adfa2533cc5926bd2c102942fdcb7
["REQ","nak",{"kinds":[1],"authors":["a2932ba2c2f00286e5008b6737b0817a0395d7a25e9859160c195572fd8c5979","e8b487c079b0f67c695ae6c4c2552a47f38adfa2533cc5926bd2c102942fdcb7"]}]
~> nak req -k 1 -l 1 --stream wss://relay.stoner.com
{"id":"1d73832917bf5a72276c53e9246c28b97225b51cd5735843434f7756fc0ddead","pubkey":"3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d","created_at":1684894689,"kind":1,"tags":[["p","bcbeb5a2e6b547f6d0c3d8c16145f7bb94f3639ec7ecbcfe50045dbb2eede70b","wss://nos.lol","artk42"],["e","b5af6815c8d89a7d5b6201b9e624fbd5389fca3337ba2dc05c6187234a7c1bd5","wss://nos.lol","root"],["e","5795e27aff0a459a30c64a61a32c43d968cd19c8f1926cf01fc02e9da7c56f2b","wss://nos.lol","reply"],["client","coracle"]],"content":"Because that makes no sense.","sig":"3ee5b2b26ec6b116ef1a6b1c10bc7e56674a3c36841814f68b57f63259f3d78e23629d4599afe67e72c220e27b4b0966cc51adc1da808c8c6111dedb531ac0c3"}
```
### documentation
```
~> nak --help
NAME:
nak - the nostr army knife command-line tool
USAGE:
nak [global options] command [command options] [arguments...]
COMMANDS:
req generates encoded REQ messages and optionally use them to talk to relays
event generates an encoded event and either prints it or sends it to a set of relays
decode decodes nip19, nip21, nip05 or hex entities
help, h Shows a list of commands or help for one command
GLOBAL OPTIONS:
--help, -h show help
~> nak event --help
NAME:
nak event - generates an encoded event and either prints it or sends it to a set of relays
USAGE:
nak event [command options] [arguments...]
DESCRIPTION:
example usage (for sending directly to a relay with 'nostcat'):
nak event -k 1 -c hello --envelope | nostcat wss://nos.lol
standalone:
nak event -k 1 -c hello wss://nos.lol`,
OPTIONS:
--envelope print the event enveloped in a ["EVENT", ...] message ready to be sent to a relay (default: false)
--sec value secret key to sign the event (default: the key '1')
EVENT FIELDS
--content value, -c value event content (default: hello from the nostr army knife)
--created-at value, --time value, --ts value unix timestamp value for the created_at field (default: now)
--kind value, -k value event kind (default: 1)
--tag value, -t value [ --tag value, -t value ] sets a tag field on the event, takes a value like -t e=<id>
-e value [ -e value ] shortcut for --tag e=<value>
-p value [ -p value ] shortcut for --tag p=<value>
~> nak req --help
NAME:
nak req - generates encoded REQ messages and optionally use them to talk to relays
USAGE:
nak req [command options] [relay...]
DESCRIPTION:
outputs a NIP-01 Nostr filter. when a relay is not given, will print the filter, otherwise will connect to the given relay and send the filter.
example usage (with 'nostcat'):
nak req -k 1 -a 3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d | nostcat wss://nos.lol
standalone:
nak req -k 1 wss://nos.lol
OPTIONS:
--bare when printing the filter, print just the filter, not enveloped in a ["REQ", ...] array (default: false)
--stream keep the subscription open, printing all events as they are returned (default: false, will close on EOSE)
FILTER ATTRIBUTES
--author value, -a value [ --author value, -a value ] only accept events from these authors (pubkey as hex)
--id value, -i value [ --id value, -i value ] only accept events with these ids (hex)
--kind value, -k value [ --kind value, -k value ] only accept events with these kind numbers
--limit value, -l value only accept up to this number of events (default: 0)
--since value, -s value only accept events newer than this (unix timestamp) (default: 0)
--tag value, -t value [ --tag value, -t value ] takes a tag like -t e=<id>, only accept events with these tags
--until value, -u value only accept events older than this (unix timestamp) (default: 0)
-e value [ -e value ] shortcut for --tag e=<value>
-p value [ -p value ] shortcut for --tag p=<value>
OPTIONS:
--bare when printing the filter, print just the filter, not enveloped in a ["REQ", ...] array (default: false)
--stream keep the subscription open, printing all events as they are returned (default: false, will close on EOSE)
FILTER ATTRIBUTES
--author value, -a value [ --author value, -a value ] only accept events from these authors (pubkey as hex)
--id value, -i value [ --id value, -i value ] only accept events with these ids (hex)
--kind value, -k value [ --kind value, -k value ] only accept events with these kind numbers
--limit value, -l value only accept up to this number of events (default: 0)
--since value, -s value only accept events newer than this (unix timestamp) (default: 0)
--tag value, -t value [ --tag value, -t value ] takes a tag like -t e=<id>, only accept events with these tags
--until value, -u value only accept events older than this (unix timestamp) (default: 0)
-e value [ -e value ] shortcut for --tag e=<value>
-p value [ -p value ] shortcut for --tag p=<value>
~> nak decode --help
NAME:
nak decode - decodes nip19, nip21, nip05 or hex entities
USAGE:
nak decode [command options] <npub | nprofile | nip05 | nevent | naddr | nsec>
DESCRIPTION:
example usage:
nak decode npub1uescmd5krhrmj9rcura833xpke5eqzvcz5nxjw74ufeewf2sscxq4g7chm
nak decode nevent1qqs29yet5tp0qq5xu5qgkeehkzqh5qu46739axzezcxpj4tjlkx9j7gpr4mhxue69uhkummnw3ez6ur4vgh8wetvd3hhyer9wghxuet5sh59ud
nak decode nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpz4mhxue69uhk2er9dchxummnw3ezumrpdejqz8thwden5te0dehhxarj94c82c3wwajkcmr0wfjx2u3wdejhgqgcwaehxw309aex2mrp0yhxummnw3exzarf9e3k7mgnp0sh5
nak decode nsec1jrmyhtjhgd9yqalps8hf9mayvd58852gtz66m7tqpacjedkp6kxq4dyxsr
OPTIONS:
--id, -e return just the event id, if applicable (default: false)
--pubkey, -p return just the pubkey, if applicable (default: false)
--help, -h show help
### fetch an event using relay and author hints automatically from a nevent1 code, pretty-print it
```shell
nak fetch nevent1qqs2e3k48vtrkzjm8vvyzcmsmkf58unrxtq2k4h5yspay6vhcqm4wqcpz9mhxue69uhkummnw3ezuamfdejj7q3ql2vyh47mk2p0qlsku7hg0vn29faehy9hy34ygaclpn66ukqp3afqxpqqqqqqz7ttjyq | jq
{
"id": "acc6d53b163b0a5b3b18416370dd9343f26332c0ab56f42403d26997c0375703",
"pubkey": "fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52",
"created_at": 1697370933,
"kind": 1,
"tags": [],
"content": "`q` tags = a kind 1 that wanted to be a kind:6 but fell short\n\n🥁",
"sig": "b5b63d7c8491a4a0517df2c58151665c583abc6cd31fd50b957bf8fefc8e55c87c922cbdcb50888cb9f1c03c26ab5c02c1dccc14b46b78e1e16c60094f2358da"
}
```
written in go using [go-nostr](https://github.com/nbd-wtf/go-nostr), heavily inspired by [nostril](http://git.jb55.com/nostril/).
### republish an event from one relay to multiple others
```shell
~> nak req -i e20978737ab7cd36eca300a65f11738176123f2e0c23054544b18fe493e2aa1a wss://nostr.wine/ wss://nostr-pub.wellorder.net | nak event wss://nostr.wine wss://offchain.pub wss://public.relaying.io wss://eden.nostr.land wss://atlas.nostr.land wss://relayable.org
{"id":"e20978737ab7cd36eca300a65f11738176123f2e0c23054544b18fe493e2aa1a","pubkey":"c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5","created_at":1698632753,"kind":1,"tags":[["t","gm"]],"content":"good morning","sig":"5687c1a97066c349cb3bde0c0719fd1652a13403ba6aca7557b646307ee6718528cd86989db08bf6a7fd04bea0b0b87c1dd1b78c2d21b80b80eebab7f40b8916"}
publishing to wss://nostr.wine... failed: msg: blocked: not an active paid member
publishing to wss://offchain.pub... success.
publishing to wss://public.relaying.io... success.
publishing to wss://eden.nostr.land... failed: msg: blocked: not on white-list
publishing to wss://atlas.nostr.land... failed: msg: blocked: not on white-list
publishing to wss://relayable.org... success.
```
## a toolkit for debugging all things nostr as a webpage:
### verify if an event is good
```shell
~> echo '{"content":"hello world","created_at":1698923350,"id":"05bd99d54cb835f327e0092c4275ee44c7ff51219eff417c19f70c9e2c53ad5a","kind":1,"pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","sig":"0a04a296321ed933858577f36fb2fb9a0933e966f9ee32b539493f5a4d00120891b1ca9152ebfbc04fb403bdaa7c73f415e7c4954e55726b4b4fa8cebf008cd6","tags":[]}' | nak verify
invalid .id, expected 05bd99d54cb835f427e0092c4275ee44c7ff51219eff417c19f70c9e2c53ad5a, got 05bd99d54cb835f327e0092c4275ee44c7ff51219eff417c19f70c9e2c53ad5a
```
![](https://user-images.githubusercontent.com/1653275/227681805-0cd20b39-de0d-4fcb-abb4-de3283404e8f.png)
written in [scala](https://scala-lang.org/) with [calico](https://www.armanbilge.com/calico/) and [snow](https://github.com/fiatjaf/snow)
### fetch all quoted events by a given pubkey in their last 100 notes
```shell
nak req -l 100 -k 1 -a 2edbcea694d164629854a52583458fd6d965b161e3c48b57d3aff01940558884 wss://relay.damus.io | jq -r '.content | match("nostr:((note1|nevent1)[a-z0-9]+)";"g") | .captures[0].string' | nak decode | jq -cr '{ids: [.id]}' | nak req wss://relay.damus.io
```

View File

@@ -1,14 +0,0 @@
enablePlugins(ScalaJSPlugin, EsbuildPlugin)
name := "nostr-army-knife"
scalaVersion := "3.3.0-RC4"
lazy val root = (project in file("."))
.settings(
libraryDependencies ++= Seq(
"com.armanbilge" %%% "calico" % "0.2.0-RC2",
"com.fiatjaf" %%% "snow" % "0.0.1"
),
scalaJSUseMainModuleInitializer := true,
scalaJSLinkerConfig ~= { _.withModuleKind(ModuleKind.CommonJSModule) }
)

147
count.go Normal file
View File

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

View File

@@ -8,7 +8,7 @@ import (
"github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nip19"
"github.com/nbd-wtf/go-nostr/sdk"
sdk "github.com/nbd-wtf/nostr-sdk"
"github.com/urfave/cli/v2"
)
@@ -34,43 +34,45 @@ var decode = &cli.Command{
},
ArgsUsage: "<npub | nprofile | nip05 | nevent | naddr | nsec>",
Action: func(c *cli.Context) error {
args := c.Args()
if args.Len() != 1 {
return fmt.Errorf("invalid number of arguments, need just one")
}
input := args.First()
if strings.HasPrefix(input, "nostr:") {
input = input[6:]
}
var decodeResult DecodeResult
if b, err := hex.DecodeString(input); err == nil {
if len(b) == 64 {
decodeResult.HexResult.PossibleTypes = []string{"sig"}
decodeResult.HexResult.Signature = hex.EncodeToString(b)
} else if len(b) == 32 {
decodeResult.HexResult.PossibleTypes = []string{"pubkey", "private_key", "event_id"}
decodeResult.HexResult.ID = hex.EncodeToString(b)
decodeResult.HexResult.PrivateKey = hex.EncodeToString(b)
decodeResult.HexResult.PublicKey = hex.EncodeToString(b)
} else {
return fmt.Errorf("hex string with invalid number of bytes: %d", len(b))
for input := range getStdinLinesOrFirstArgument(c) {
if strings.HasPrefix(input, "nostr:") {
input = input[6:]
}
} else if evp := sdk.InputToEventPointer(input); evp != nil {
decodeResult = DecodeResult{EventPointer: evp}
} else if pp := sdk.InputToProfile(c.Context, input); pp != nil {
decodeResult = DecodeResult{ProfilePointer: pp}
} else if prefix, value, err := nip19.Decode(input); err == nil && prefix == "naddr" {
ep := value.(nostr.EntityPointer)
decodeResult = DecodeResult{EntityPointer: &ep}
} else if prefix, value, err := nip19.Decode(input); err == nil && prefix == "nsec" {
decodeResult.PrivateKey.PrivateKey = value.(string)
decodeResult.PrivateKey.PublicKey, _ = nostr.GetPublicKey(value.(string))
} else {
return fmt.Errorf("couldn't decode input")
var decodeResult DecodeResult
if b, err := hex.DecodeString(input); err == nil {
if len(b) == 64 {
decodeResult.HexResult.PossibleTypes = []string{"sig"}
decodeResult.HexResult.Signature = hex.EncodeToString(b)
} else if len(b) == 32 {
decodeResult.HexResult.PossibleTypes = []string{"pubkey", "private_key", "event_id"}
decodeResult.HexResult.ID = hex.EncodeToString(b)
decodeResult.HexResult.PrivateKey = hex.EncodeToString(b)
decodeResult.HexResult.PublicKey = hex.EncodeToString(b)
} else {
lineProcessingError(c, "hex string with invalid number of bytes: %d", len(b))
continue
}
} else if evp := sdk.InputToEventPointer(input); evp != nil {
decodeResult = DecodeResult{EventPointer: evp}
} else if pp := sdk.InputToProfile(c.Context, input); pp != nil {
decodeResult = DecodeResult{ProfilePointer: pp}
} else if prefix, value, err := nip19.Decode(input); err == nil && prefix == "naddr" {
ep := value.(nostr.EntityPointer)
decodeResult = DecodeResult{EntityPointer: &ep}
} else if prefix, value, err := nip19.Decode(input); err == nil && prefix == "nsec" {
decodeResult.PrivateKey.PrivateKey = value.(string)
decodeResult.PrivateKey.PublicKey, _ = nostr.GetPublicKey(value.(string))
} else {
lineProcessingError(c, "couldn't decode input '%s': %s", input, err)
continue
}
fmt.Println(decodeResult.JSON())
}
fmt.Println(decodeResult.JSON())
exitIfLineProcessingError(c)
return nil
},
}

232
encode.go Normal file
View File

@@ -0,0 +1,232 @@
package main
import (
"fmt"
"github.com/nbd-wtf/go-nostr/nip19"
"github.com/urfave/cli/v2"
)
var encode = &cli.Command{
Name: "encode",
Usage: "encodes notes and other stuff to nip19 entities",
Description: `example usage:
nak encode npub <pubkey-hex>
nak encode nprofile <pubkey-hex>
nak encode nprofile --relay <relay-url> <pubkey-hex>
nak encode nevent <event-id>
nak encode nevent --author <pubkey-hex> --relay <relay-url> --relay <other-relay> <event-id>
nak encode nsec <privkey-hex>`,
Before: func(c *cli.Context) error {
if c.Args().Len() < 1 {
return fmt.Errorf("expected more than 1 argument.")
}
return nil
},
Subcommands: []*cli.Command{
{
Name: "npub",
Usage: "encode a hex public key into bech32 'npub' format",
Action: func(c *cli.Context) error {
for target := range getStdinLinesOrFirstArgument(c) {
if err := validate32BytesHex(target); err != nil {
lineProcessingError(c, "invalid public key: %s", target, err)
continue
}
if npub, err := nip19.EncodePublicKey(target); err == nil {
fmt.Println(npub)
} else {
return err
}
}
exitIfLineProcessingError(c)
return nil
},
},
{
Name: "nsec",
Usage: "encode a hex private key into bech32 'nsec' format",
Action: func(c *cli.Context) error {
for target := range getStdinLinesOrFirstArgument(c) {
if err := validate32BytesHex(target); err != nil {
lineProcessingError(c, "invalid private key: %s", target, err)
continue
}
if npub, err := nip19.EncodePrivateKey(target); err == nil {
fmt.Println(npub)
} else {
return err
}
}
exitIfLineProcessingError(c)
return nil
},
},
{
Name: "nprofile",
Usage: "generate profile codes with attached relay information",
Flags: []cli.Flag{
&cli.StringSliceFlag{
Name: "relay",
Aliases: []string{"r"},
Usage: "attach relay hints to nprofile code",
},
},
Action: func(c *cli.Context) error {
for target := range getStdinLinesOrFirstArgument(c) {
if err := validate32BytesHex(target); err != nil {
lineProcessingError(c, "invalid public key: %s", target, err)
continue
}
relays := c.StringSlice("relay")
if err := validateRelayURLs(relays); err != nil {
return err
}
if npub, err := nip19.EncodeProfile(target, relays); err == nil {
fmt.Println(npub)
} else {
return err
}
}
exitIfLineProcessingError(c)
return nil
},
},
{
Name: "nevent",
Usage: "generate event codes with optionally attached relay information",
Flags: []cli.Flag{
&cli.StringSliceFlag{
Name: "relay",
Aliases: []string{"r"},
Usage: "attach relay hints to nevent code",
},
&cli.StringFlag{
Name: "author",
Usage: "attach an author pubkey as a hint to the nevent code",
},
},
Action: func(c *cli.Context) error {
for target := range getStdinLinesOrFirstArgument(c) {
if err := validate32BytesHex(target); err != nil {
lineProcessingError(c, "invalid event id: %s", target, err)
continue
}
author := c.String("author")
if author != "" {
if err := validate32BytesHex(author); err != nil {
return err
}
}
relays := c.StringSlice("relay")
if err := validateRelayURLs(relays); err != nil {
return err
}
if npub, err := nip19.EncodeEvent(target, relays, author); err == nil {
fmt.Println(npub)
} else {
return err
}
}
exitIfLineProcessingError(c)
return nil
},
},
{
Name: "naddr",
Usage: "generate codes for NIP-33 parameterized replaceable events",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "identifier",
Aliases: []string{"d"},
Usage: "the \"d\" tag identifier of this replaceable event -- can also be read from stdin",
Required: true,
},
&cli.StringFlag{
Name: "pubkey",
Usage: "pubkey of the naddr author",
Aliases: []string{"p"},
Required: true,
},
&cli.Int64Flag{
Name: "kind",
Aliases: []string{"k"},
Usage: "kind of referred replaceable event",
Required: true,
},
&cli.StringSliceFlag{
Name: "relay",
Aliases: []string{"r"},
Usage: "attach relay hints to naddr code",
},
},
Action: func(c *cli.Context) error {
for d := range getStdinLinesOrBlank() {
pubkey := c.String("pubkey")
if err := validate32BytesHex(pubkey); err != nil {
return err
}
kind := c.Int("kind")
if kind < 30000 || kind >= 40000 {
return fmt.Errorf("kind must be between 30000 and 39999, as per NIP-16, got %d", kind)
}
if d == "" {
d = c.String("identifier")
if d == "" {
lineProcessingError(c, "\"d\" tag identifier can't be empty")
continue
}
}
relays := c.StringSlice("relay")
if err := validateRelayURLs(relays); err != nil {
return err
}
if npub, err := nip19.EncodeEntity(pubkey, kind, d, relays); err == nil {
fmt.Println(npub)
} else {
return err
}
}
exitIfLineProcessingError(c)
return nil
},
},
{
Name: "note",
Usage: "generate note1 event codes (not recommended)",
Action: func(c *cli.Context) error {
for target := range getStdinLinesOrFirstArgument(c) {
if err := validate32BytesHex(target); err != nil {
lineProcessingError(c, "invalid event id: %s", target, err)
continue
}
if note, err := nip19.EncodeNote(target); err == nil {
fmt.Println(note)
} else {
return err
}
}
exitIfLineProcessingError(c)
return nil
},
},
},
}

219
event.go
View File

@@ -1,6 +1,7 @@
package main
import (
"context"
"encoding/json"
"fmt"
"os"
@@ -8,7 +9,11 @@ import (
"strings"
"time"
"github.com/bgentry/speakeasy"
"github.com/mailru/easyjson"
"github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nip19"
"github.com/nbd-wtf/go-nostr/nson"
"github.com/urfave/cli/v2"
)
@@ -17,27 +22,43 @@ const CATEGORY_EVENT_FIELDS = "EVENT FIELDS"
var event = &cli.Command{
Name: "event",
Usage: "generates an encoded event and either prints it or sends it to a set of relays",
Description: `example usage (for sending directly to a relay with 'nostcat'):
nak event -k 1 -c hello --envelope | nostcat wss://nos.lol
standalone:
nak event -k 1 -c hello wss://nos.lol`,
Description: `outputs an event built with the flags. if one or more relays are given as arguments, an attempt is also made to publish the event to these relays.
example:
nak event -c hello wss://nos.lol
nak event -k 3 -p 3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d
if an event -- or a partial event -- is given on stdin, the flags can be used to optionally modify it. if it is modified it is rehashed and resigned, otherwise it is just returned as given, but that can be used to just publish to relays.
example:
echo '{"id":"a889df6a387419ff204305f4c2d296ee328c3cd4f8b62f205648a541b4554dfb","pubkey":"c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5","created_at":1698623783,"kind":1,"tags":[],"content":"hello from the nostr army knife","sig":"84876e1ee3e726da84e5d195eb79358b2b3eaa4d9bd38456fde3e8a2af3f1cd4cda23f23fda454869975b3688797d4c66e12f4c51c1b43c6d2997c5e61865661"}' | nak event wss://offchain.pub
echo '{"tags": [["t", "spam"]]}' | nak event -c 'this is spam'
`,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "sec",
Usage: "secret key to sign the event",
Usage: "secret key to sign the event, 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 event",
},
&cli.BoolFlag{
Name: "envelope",
Usage: "print the event enveloped in a [\"EVENT\", ...] message ready to be sent to a relay",
},
&cli.BoolFlag{
Name: "nson",
Usage: "encode the event using NSON",
},
&cli.IntFlag{
Name: "kind",
Aliases: []string{"k"},
Usage: "event kind",
DefaultText: "1",
Value: 1,
Value: 0,
Category: CATEGORY_EVENT_FIELDS,
},
&cli.StringFlag{
@@ -45,7 +66,7 @@ standalone:
Aliases: []string{"c"},
Usage: "event content",
DefaultText: "hello from the nostr army knife",
Value: "hello from the nostr army knife",
Value: "",
Category: CATEGORY_EVENT_FIELDS,
},
&cli.StringSliceFlag{
@@ -69,78 +90,152 @@ standalone:
Aliases: []string{"time", "ts"},
Usage: "unix timestamp value for the created_at field",
DefaultText: "now",
Value: "now",
Value: "",
Category: CATEGORY_EVENT_FIELDS,
},
},
ArgsUsage: "[relay...]",
Action: func(c *cli.Context) error {
evt := nostr.Event{
Kind: c.Int("kind"),
Content: c.String("content"),
Tags: make(nostr.Tags, 0, 3),
}
tags := make([][]string, 0, 5)
for _, tagFlag := range c.StringSlice("tag") {
spl := strings.Split(tagFlag, "=")
if len(spl) == 2 && len(spl[0]) == 1 {
tags = append(tags, spl)
// gather the secret key first
sec := c.String("sec")
if c.Bool("prompt-sec") {
if isPiped() {
return fmt.Errorf("can't prompt for a secret key when processing data from a pipe, try again without --prompt-sec")
}
var err error
sec, err = speakeasy.FAsk(os.Stderr, "type your secret key as nsec or hex: ")
if err != nil {
return fmt.Errorf("failed to get secret key: %w", err)
}
}
for _, etag := range c.StringSlice("e") {
tags = append(tags, []string{"e", etag})
}
for _, ptag := range c.StringSlice("p") {
tags = append(tags, []string{"p", ptag})
}
if len(tags) > 0 {
for _, tag := range tags {
evt.Tags = append(evt.Tags, tag)
if strings.HasPrefix(sec, "nsec1") {
_, hex, err := nip19.Decode(sec)
if err != nil {
return fmt.Errorf("invalid nsec: %w", err)
}
sec = hex.(string)
}
if len(sec) > 64 {
return fmt.Errorf("invalid secret key: too large")
}
sec = strings.Repeat("0", 64-len(sec)) + sec // left-pad
if err := validate32BytesHex(sec); err != nil {
return fmt.Errorf("invalid secret key")
}
createdAt := c.String("created-at")
ts := time.Now()
if createdAt != "now" {
if v, err := strconv.ParseInt(createdAt, 10, 64); err != nil {
return fmt.Errorf("failed to parse timestamp '%s': %w", createdAt, err)
} else {
ts = time.Unix(v, 0)
// then process input and generate events
for stdinEvent := range getStdinLinesOrBlank() {
evt := nostr.Event{
Tags: make(nostr.Tags, 0, 3),
}
}
evt.CreatedAt = nostr.Timestamp(ts.Unix())
if err := evt.Sign(c.String("sec")); err != nil {
return fmt.Errorf("error signing with provided key: %w", err)
}
relays := c.Args().Slice()
if len(relays) > 0 {
fmt.Println(evt.String())
for _, url := range relays {
fmt.Fprintf(os.Stderr, "publishing to %s... ", url)
if relay, err := nostr.RelayConnect(c.Context, url); err != nil {
fmt.Fprintf(os.Stderr, "failed to connect: %s\n", err)
} else {
if status, err := relay.Publish(c.Context, evt); err != nil {
fmt.Fprintf(os.Stderr, "failed: %s\n", err)
} else {
fmt.Fprintf(os.Stderr, "%s.\n", status)
}
mustRehashAndResign := false
if stdinEvent != "" {
if err := json.Unmarshal([]byte(stdinEvent), &evt); err != nil {
lineProcessingError(c, "invalid event received from stdin: %s", err)
continue
}
}
} else {
var result string
if c.Bool("envelope") {
j, _ := json.Marshal([]any{"EVENT", evt})
result = string(j)
} else {
result = evt.String()
if kind := c.Int("kind"); kind != 0 {
evt.Kind = kind
mustRehashAndResign = true
} else if evt.Kind == 0 {
evt.Kind = 1
mustRehashAndResign = true
}
if content := c.String("content"); content != "" {
evt.Content = content
mustRehashAndResign = true
} else if evt.Content == "" && evt.Kind == 1 {
evt.Content = "hello from the nostr army knife"
mustRehashAndResign = true
}
tags := make(nostr.Tags, 0, 5)
for _, tagFlag := range c.StringSlice("tag") {
// tags are in the format key=value
spl := strings.Split(tagFlag, "=")
if len(spl) == 2 && len(spl[0]) > 0 {
tag := nostr.Tag{spl[0]}
// tags may also contain extra elements separated with a ";"
spl2 := strings.Split(spl[1], ";")
tag = append(tag, spl2...)
// ~
tags = append(tags, tag)
}
}
for _, etag := range c.StringSlice("e") {
tags = append(tags, []string{"e", etag})
mustRehashAndResign = true
}
for _, ptag := range c.StringSlice("p") {
tags = append(tags, []string{"p", ptag})
mustRehashAndResign = true
}
if len(tags) > 0 {
for _, tag := range tags {
evt.Tags = append(evt.Tags, tag)
}
mustRehashAndResign = true
}
if createdAt := c.String("created-at"); createdAt != "" {
ts := time.Now()
if createdAt != "now" {
if v, err := strconv.ParseInt(createdAt, 10, 64); err != nil {
return fmt.Errorf("failed to parse timestamp '%s': %w", createdAt, err)
} else {
ts = time.Unix(v, 0)
}
}
evt.CreatedAt = nostr.Timestamp(ts.Unix())
mustRehashAndResign = true
} else if evt.CreatedAt == 0 {
evt.CreatedAt = nostr.Now()
mustRehashAndResign = true
}
if evt.Sig == "" || mustRehashAndResign {
if err := evt.Sign(sec); err != nil {
return fmt.Errorf("error signing with provided key: %w", err)
}
}
relays := c.Args().Slice()
if len(relays) > 0 {
fmt.Println(evt.String())
for _, url := range relays {
fmt.Fprintf(os.Stderr, "publishing to %s... ", url)
if relay, err := nostr.RelayConnect(c.Context, url); err != nil {
fmt.Fprintf(os.Stderr, "failed to connect: %s\n", err)
} else {
ctx, cancel := context.WithTimeout(c.Context, 10*time.Second)
defer cancel()
if status, err := relay.Publish(ctx, evt); err != nil {
fmt.Fprintf(os.Stderr, "failed: %s\n", err)
} else {
fmt.Fprintf(os.Stderr, "%s.\n", status)
}
}
}
} else {
var result string
if c.Bool("envelope") {
j, _ := json.Marshal([]any{"EVENT", evt})
result = string(j)
} else if c.Bool("nson") {
result, _ = nson.Marshal(&evt)
} else {
j, _ := easyjson.Marshal(&evt)
result = string(j)
}
fmt.Println(result)
}
fmt.Println(result)
}
exitIfLineProcessingError(c)
return nil
},
}

View File

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

Before

Width:  |  Height:  |  Size: 278 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

94
fetch.go Normal file
View File

@@ -0,0 +1,94 @@
package main
import (
"fmt"
"github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nip19"
sdk "github.com/nbd-wtf/nostr-sdk"
"github.com/urfave/cli/v2"
)
var fetch = &cli.Command{
Name: "fetch",
Usage: "fetches events related to the given nip19 code from the included relay hints",
Description: `example usage:
nak fetch nevent1qqsxrwm0hd3s3fddh4jc2574z3xzufq6qwuyz2rvv3n087zvym3dpaqprpmhxue69uhhqatzd35kxtnjv4kxz7tfdenju6t0xpnej4
echo npub1h8spmtw9m2huyv6v2j2qd5zv956z2zdugl6mgx02f2upffwpm3nqv0j4ps | nak fetch --relay wss://relay.nostr.band`,
Flags: []cli.Flag{
&cli.StringSliceFlag{
Name: "relay",
Aliases: []string{"r"},
Usage: "also use these relays to fetch from",
},
},
ArgsUsage: "[nip19code]",
Action: func(c *cli.Context) error {
for code := range getStdinLinesOrFirstArgument(c) {
filter := nostr.Filter{}
prefix, value, err := nip19.Decode(code)
if err != nil {
lineProcessingError(c, "failed to decode: %s", err)
continue
}
relays := c.StringSlice("relay")
if err := validateRelayURLs(relays); err != nil {
return err
}
var authorHint string
switch prefix {
case "nevent":
v := value.(nostr.EventPointer)
filter.IDs = append(filter.IDs, v.ID)
if v.Author != "" {
authorHint = v.Author
}
relays = v.Relays
case "naddr":
v := value.(nostr.EntityPointer)
filter.Tags = nostr.TagMap{"d": []string{v.Identifier}}
filter.Kinds = append(filter.Kinds, v.Kind)
filter.Authors = append(filter.Authors, v.PublicKey)
authorHint = v.PublicKey
relays = v.Relays
case "nprofile":
v := value.(nostr.ProfilePointer)
filter.Authors = append(filter.Authors, v.PublicKey)
filter.Kinds = append(filter.Kinds, 0)
authorHint = v.PublicKey
relays = v.Relays
case "npub":
v := value.(string)
filter.Authors = append(filter.Authors, v)
filter.Kinds = append(filter.Kinds, 0)
authorHint = v
}
pool := nostr.NewSimplePool(c.Context)
if authorHint != "" {
relayList := sdk.FetchRelaysForPubkey(c.Context, pool, authorHint,
"wss://purplepag.es", "wss://offchain.pub", "wss://public.relaying.io")
for _, relayListItem := range relayList {
if relayListItem.Outbox {
relays = append(relays, relayListItem.URL)
}
}
}
if len(relays) == 0 {
lineProcessingError(c, "no relay hints found")
continue
}
for ie := range pool.SubManyEose(c.Context, relays, nostr.Filters{filter}) {
fmt.Println(ie.Event)
}
}
exitIfLineProcessingError(c)
return nil
},
}

30
go.mod
View File

@@ -1,30 +1,40 @@
module github.com/fiatjaf/nak
go 1.20
go 1.21
toolchain go1.21.0
require (
github.com/nbd-wtf/go-nostr v0.18.3
github.com/bgentry/speakeasy v0.1.0
github.com/mailru/easyjson v0.7.7
github.com/nbd-wtf/go-nostr v0.25.3
github.com/nbd-wtf/nostr-sdk v0.0.2
github.com/urfave/cli/v2 v2.25.3
)
require (
github.com/SaveTheRbtz/generic-sync-map-go v0.0.0-20220414055132-a37292614db8 // indirect
github.com/btcsuite/btcd/btcec/v2 v2.2.0 // indirect
github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect
github.com/btcsuite/btcd/btcutil v1.1.3 // indirect
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 // indirect
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/decred/dcrd/crypto/blake256 v1.0.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
github.com/dgraph-io/ristretto v0.1.1 // indirect
github.com/dustin/go-humanize v1.0.0 // indirect
github.com/fiatjaf/eventstore v0.1.0 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.2.0 // indirect
github.com/golang/glog v1.0.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/tidwall/gjson v1.14.4 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
golang.org/x/exp v0.0.0-20221106115401-f9659909a136 // indirect
golang.org/x/sys v0.6.0 // indirect
golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 // indirect
golang.org/x/sys v0.8.0 // indirect
)

59
go.sum
View File

@@ -1,20 +1,21 @@
github.com/SaveTheRbtz/generic-sync-map-go v0.0.0-20220414055132-a37292614db8 h1:Xa6tp8DPDhdV+k23uiTC/GrAYOe4IdyJVKtob4KW3GA=
github.com/SaveTheRbtz/generic-sync-map-go v0.0.0-20220414055132-a37292614db8/go.mod h1:ihkm1viTbO/LOsgdGoFPBSvzqvx7ibvkMzYp3CgtHik=
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
github.com/btcsuite/btcd v0.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/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.2.0 h1:fzn1qaOt32TuLjFlkzYSsBC35Q3KUjT1SwPxiMSCF5k=
github.com/btcsuite/btcd/btcec/v2 v2.2.0/go.mod h1:U7MHm051Al6XmscBQ0BoNydpOTsFAn707034b5nY8zU=
github.com/btcsuite/btcd/btcec/v2 v2.3.2 h1:5n0X6hX0Zk+6omWcihdYvdAlGf2DfasC0GMf7DClJ3U=
github.com/btcsuite/btcd/btcec/v2 v2.3.2/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04=
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.3 h1:xfbtw8lwpp0G6NwSHb+UE67ryTFHJAiNuipusjXSohQ=
github.com/btcsuite/btcd/btcutil v1.1.3/go.mod h1:UR7dsSJzJUfMmFiiLlIrMq1lS9jh9EdCV7FStZSnpi0=
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U=
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2 h1:KdUfX2zKommPRa+PD0sWZUyXe9w277ABlgELO7H04IM=
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg=
@@ -24,17 +25,30 @@ github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku
github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0=
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc=
github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y=
github.com/decred/dcrd/crypto/blake256 v1.0.1/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.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218=
github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8=
github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/fiatjaf/eventstore v0.1.0 h1:/g7VTw6dsXmjICD3rBuHNIvAammHJ5unrKJ71Dz+VTs=
github.com/fiatjaf/eventstore v0.1.0/go.mod h1:juMei5HL3HJi6t7vZjj7VdEItDPu31+GLROepdUK4tw=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
@@ -43,6 +57,9 @@ github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.2.0 h1:u0p9s3xLYpZCA1z5JgCkMeB34CKCMMQbM+G8Ii7YD0I=
github.com/gobwas/ws v1.2.0/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ=
github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
@@ -63,8 +80,10 @@ github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlT
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/nbd-wtf/go-nostr v0.18.3 h1:ofMYxlFAptyoErlOGOCUk7zGHQNJ8/ZkIXXOsveFZ+c=
github.com/nbd-wtf/go-nostr v0.18.3/go.mod h1:GPJOOK8US38kz+bfb9nWe873Xu0e6bXlThejOs1LTkc=
github.com/nbd-wtf/go-nostr v0.25.3 h1:RPPh4cOosw0OZi5KG627pZ3GlKxiKsjARluzen/mB9g=
github.com/nbd-wtf/go-nostr v0.25.3/go.mod h1:bkffJI+x914sPQWum9ZRUn66D7NpDnAoWo1yICvj3/0=
github.com/nbd-wtf/nostr-sdk v0.0.2 h1:mZIeti+DOF0D1179q+NLL/h0LVMMOPRQAYpOuUrn5Zk=
github.com/nbd-wtf/nostr-sdk v0.0.2/go.mod h1:KQZOtzcrXBlVhpZYG1tw83ADIONNMMPjUU3ZAH5U2RY=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
@@ -74,11 +93,19 @@ github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/puzpuzpuz/xsync/v2 v2.5.1 h1:mVGYAvzDSu52+zaGyNjC+24Xw2bQi3kTr4QJ6N9pIIU=
github.com/puzpuzpuz/xsync/v2 v2.5.1/go.mod h1:gD2H2krq/w52MfPLE+Uy64TzJDVY7lP2znR9qmR35kU=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
@@ -93,14 +120,15 @@ github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsr
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-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20221106115401-f9659909a136 h1:Fq7F/w7MAa1KJ5bt2aJ62ihqp9HDcRuyILskkpIAurw=
golang.org/x/exp v0.0.0-20221106115401-f9659909a136/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 h1:5llv2sWeaMSnA3w2kS57ouQQ4pudlXrR0dCgw51QK9o=
golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
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-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-20200813134508-3edf25e44fcc h1:zK/HqS5bZxDptfPJNq8v7vJfXtkU7r9TLIoSr1bXaP4=
golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -111,8 +139,10 @@ golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -129,6 +159,9 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

110
helpers.go Normal file
View File

@@ -0,0 +1,110 @@
package main
import (
"bufio"
"context"
"encoding/hex"
"fmt"
"net/url"
"os"
"strings"
"github.com/urfave/cli/v2"
)
const (
LINE_PROCESSING_ERROR = iota
)
func isPiped() bool {
stat, _ := os.Stdin.Stat()
return stat.Mode()&os.ModeCharDevice == 0
}
func getStdinLinesOrBlank() chan string {
multi := make(chan string)
if hasStdinLines := writeStdinLinesOrNothing(multi); !hasStdinLines {
single := make(chan string, 1)
single <- ""
close(single)
return single
} else {
return multi
}
}
func getStdinLinesOrFirstArgument(c *cli.Context) chan string {
// try the first argument
target := c.Args().First()
if target != "" {
single := make(chan string, 1)
single <- target
return single
}
// try the stdin
multi := make(chan string)
writeStdinLinesOrNothing(multi)
return multi
}
func writeStdinLinesOrNothing(ch chan string) (hasStdinLines bool) {
if isPiped() {
// piped
go func() {
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
ch <- strings.TrimSpace(scanner.Text())
}
close(ch)
}()
return true
} else {
// not piped
return false
}
}
func validateRelayURLs(wsurls []string) error {
for _, wsurl := range wsurls {
u, err := url.Parse(wsurl)
if err != nil {
return fmt.Errorf("invalid relay url '%s': %s", wsurl, err)
}
if u.Scheme != "ws" && u.Scheme != "wss" {
return fmt.Errorf("relay url must use wss:// or ws:// schemes, got '%s'", wsurl)
}
if u.Host == "" {
return fmt.Errorf("relay url '%s' is missing the hostname", wsurl)
}
}
return nil
}
func validate32BytesHex(target string) error {
if _, err := hex.DecodeString(target); err != nil {
return fmt.Errorf("target '%s' is not valid hex: %s", target, err)
}
if len(target) != 64 {
return fmt.Errorf("expected '%s' to be 64 characters (32 bytes), got %d", target, len(target))
}
if strings.ToLower(target) != target {
return fmt.Errorf("expected target to be all lowercase hex. try again with '%s'", strings.ToLower(target))
}
return nil
}
func lineProcessingError(c *cli.Context, msg string, args ...any) {
c.Context = context.WithValue(c.Context, LINE_PROCESSING_ERROR, true)
fmt.Fprintf(os.Stderr, msg+"\n", args...)
}
func exitIfLineProcessingError(c *cli.Context) {
if val := c.Context.Value(LINE_PROCESSING_ERROR); val != nil && val.(bool) {
os.Exit(123)
}
}

View File

@@ -1,7 +0,0 @@
<meta charset=utf-8>
<title>nostr army knife</title>
<script src="https://cdn.tailwindcss.com"></script>
<body class="bg-emerald-200 text-black m-0 w-full h-full">
<div id="app" class="w-full h-full"></div>
<script type="module" src="/target/esbuild/bundle.js"></script>
</body>

View File

@@ -1,13 +0,0 @@
build-prod:
sbt fullLinkJS/esBuild
cloudflare:
rm -fr cf
mkdir -p cf/target/esbuild
cp index.html cf/
cp favicon.ico cf/
cp target/esbuild/bundle.js cf/target/esbuild
wrangler pages publish cf --project-name nostr-army-knife --branch master
rm -fr cf
build-and-deploy: build-prod cloudflare

View File

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

View File

@@ -1 +0,0 @@
sbt.version=1.7.1

View File

@@ -1,2 +0,0 @@
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.13.0")
addSbtPlugin("com.fiatjaf" % "sbt-esbuild" % "0.1.1")

149
req.go
View File

@@ -16,10 +16,15 @@ var req = &cli.Command{
Usage: "generates encoded REQ messages and optionally use them to talk to relays",
Description: `outputs a NIP-01 Nostr filter. when a relay is not given, will print the filter, otherwise will connect to the given relay and send the filter.
example usage (with 'nostcat'):
nak req -k 1 -a 3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d | nostcat wss://nos.lol
standalone:
nak req -k 1 wss://nos.lol`,
example:
nak req -k 1 -l 15 wss://nostr.wine wss://nostr-pub.wellorder.net
nak req -k 0 -a 3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d wss://nos.lol | jq '.content | fromjson | .name'
it can also take a filter from stdin, optionally modify it with flags and send it to specific relays (or just print it).
example:
echo '{"kinds": [1], "#t": ["test"]}' | nak req -l 5 -k 4549 --tag t=spam wss://nostr-pub.wellorder.net
`,
Flags: []cli.Flag{
&cli.StringSliceFlag{
Name: "author",
@@ -73,6 +78,11 @@ standalone:
Usage: "only accept up to this number of events",
Category: CATEGORY_FILTER_ATTRIBUTES,
},
&cli.StringFlag{
Name: "search",
Usage: "a NIP-50 search query, use it only with relays that explicitly support it",
Category: CATEGORY_FILTER_ATTRIBUTES,
},
&cli.BoolFlag{
Name: "bare",
Usage: "when printing the filter, print just the filter, not enveloped in a [\"REQ\", ...] array",
@@ -85,78 +95,91 @@ standalone:
},
ArgsUsage: "[relay...]",
Action: func(c *cli.Context) error {
filter := nostr.Filter{}
if authors := c.StringSlice("author"); len(authors) > 0 {
filter.Authors = authors
}
if ids := c.StringSlice("id"); len(ids) > 0 {
filter.IDs = ids
}
if kinds := c.IntSlice("kind"); len(kinds) > 0 {
filter.Kinds = kinds
}
tags := make([][]string, 0, 5)
for _, tagFlag := range c.StringSlice("tag") {
spl := strings.Split(tagFlag, "=")
if len(spl) == 2 && len(spl[0]) == 1 {
tags = append(tags, spl)
} else {
return fmt.Errorf("invalid --tag '%s'", tagFlag)
for stdinFilter := range getStdinLinesOrBlank() {
filter := nostr.Filter{}
if stdinFilter != "" {
if err := json.Unmarshal([]byte(stdinFilter), &filter); err != nil {
lineProcessingError(c, "invalid filter '%s' received from stdin: %s", stdinFilter, err)
continue
}
}
}
for _, etag := range c.StringSlice("e") {
tags = append(tags, []string{"e", etag})
}
for _, ptag := range c.StringSlice("p") {
tags = append(tags, []string{"p", ptag})
}
if len(tags) > 0 {
filter.Tags = make(nostr.TagMap)
if authors := c.StringSlice("author"); len(authors) > 0 {
filter.Authors = append(filter.Authors, authors...)
}
if ids := c.StringSlice("id"); len(ids) > 0 {
filter.IDs = append(filter.IDs, ids...)
}
if kinds := c.IntSlice("kind"); len(kinds) > 0 {
filter.Kinds = append(filter.Kinds, kinds...)
}
if search := c.String("search"); search != "" {
filter.Search = search
}
tags := make([][]string, 0, 5)
for _, tagFlag := range c.StringSlice("tag") {
spl := strings.Split(tagFlag, "=")
if len(spl) == 2 && len(spl[0]) == 1 {
tags = append(tags, spl)
} else {
return fmt.Errorf("invalid --tag '%s'", tagFlag)
}
}
for _, etag := range c.StringSlice("e") {
tags = append(tags, []string{"e", etag})
}
for _, ptag := range c.StringSlice("p") {
tags = append(tags, []string{"p", ptag})
}
if len(tags) > 0 && filter.Tags == nil {
filter.Tags = make(nostr.TagMap)
}
for _, tag := range tags {
if _, ok := filter.Tags[tag[0]]; !ok {
filter.Tags[tag[0]] = make([]string, 0, 3)
}
filter.Tags[tag[0]] = append(filter.Tags[tag[0]], tag[1])
}
}
if since := c.Int("since"); since != 0 {
ts := nostr.Timestamp(since)
filter.Since = &ts
}
if until := c.Int("until"); until != 0 {
ts := nostr.Timestamp(until)
filter.Until = &ts
}
if limit := c.Int("limit"); limit != 0 {
filter.Limit = limit
}
if since := c.Int("since"); since != 0 {
ts := nostr.Timestamp(since)
filter.Since = &ts
}
if until := c.Int("until"); until != 0 {
ts := nostr.Timestamp(until)
filter.Until = &ts
}
if limit := c.Int("limit"); limit != 0 {
filter.Limit = limit
}
relays := c.Args().Slice()
if len(relays) > 0 {
pool := nostr.NewSimplePool(c.Context)
fn := pool.SubManyEose
if c.Bool("stream") {
fn = pool.SubMany
}
for evt := range fn(c.Context, relays, nostr.Filters{filter}) {
fmt.Println(evt)
}
} else {
// no relays given, will just print the filter
var result string
if c.Bool("bare") {
result = filter.String()
relays := c.Args().Slice()
if len(relays) > 0 {
pool := nostr.NewSimplePool(c.Context)
fn := pool.SubManyEose
if c.Bool("stream") {
fn = pool.SubMany
}
for ie := range fn(c.Context, relays, nostr.Filters{filter}) {
fmt.Println(ie.Event)
}
} else {
j, _ := json.Marshal([]any{"REQ", "nak", filter})
result = string(j)
}
// no relays given, will just print the filter
var result string
if c.Bool("bare") {
result = filter.String()
} else {
j, _ := json.Marshal([]any{"REQ", "nak", filter})
result = string(j)
}
fmt.Println(result)
fmt.Println(result)
}
}
exitIfLineProcessingError(c)
return nil
},
}

View File

@@ -1,315 +0,0 @@
import cats.data.{Store => *, *}
import cats.effect.*
import cats.effect.syntax.all.*
import cats.syntax.all.*
import fs2.concurrent.*
import fs2.dom.{Event => _, *}
import io.circe.parser.*
import io.circe.syntax.*
import calico.*
import calico.html.io.{*, given}
import calico.syntax.*
import scodec.bits.ByteVector
import scoin.*
import snow.*
import Utils.*
object Components {
def render32Bytes(bytes32: ByteVector32): Resource[IO, HtmlDivElement[IO]] =
div(
cls := "text-md",
entry("canonical hex", bytes32.toHex),
"if this is a public key:",
div(
cls := "mt-2 pl-2 mb-2",
entry(
"npub",
NIP19.encode(XOnlyPublicKey(bytes32))
),
nip19_21(
"nprofile",
NIP19.encode(ProfilePointer(XOnlyPublicKey(bytes32)))
)
),
"if this is a private key:",
div(
cls := "pl-2 mb-2",
entry(
"nsec",
NIP19.encode(PrivateKey(bytes32))
),
entry(
"npub",
NIP19.encode(XOnlyPublicKey(bytes32))
),
nip19_21(
"nprofile",
NIP19.encode(ProfilePointer(XOnlyPublicKey(bytes32)))
)
),
"if this is an event id:",
div(
cls := "pl-2 mb-2",
nip19_21(
"nevent",
NIP19.encode(EventPointer(bytes32.toHex))
)
),
div(
cls := "pl-2 mb-2",
entry(
"note",
NIP19.encode(bytes32)
)
)
)
def renderEventPointer(
store: Store,
evp: snow.EventPointer
): Resource[IO, HtmlDivElement[IO]] =
div(
cls := "text-md",
entry("event id (hex)", evp.id),
relayHints(store, evp.relays),
evp.author.map { pk =>
entry("author hint (pubkey hex)", pk.value.toHex)
},
nip19_21("nevent", NIP19.encode(evp)),
entry("note", NIP19.encode(ByteVector32.fromValidHex(evp.id)))
)
def renderProfilePointer(
store: Store,
pp: snow.ProfilePointer,
sk: Option[PrivateKey] = None
): Resource[IO, HtmlDivElement[IO]] =
div(
cls := "text-md",
sk.map { k => entry("private key (hex)", k.value.toHex) },
sk.map { k => entry("nsec", NIP19.encode(k)) },
entry("public key (hex)", pp.pubkey.value.toHex),
relayHints(
store,
pp.relays,
dynamic = if sk.isDefined then false else true
),
entry("npub", NIP19.encode(pp.pubkey)),
nip19_21("nprofile", NIP19.encode(pp))
)
def renderAddressPointer(
store: Store,
addr: snow.AddressPointer
): Resource[IO, HtmlDivElement[IO]] =
div(
cls := "text-md",
entry("author (pubkey hex)", addr.author.value.toHex),
entry("identifier", addr.d),
entry("kind", addr.kind.toString),
relayHints(store, addr.relays),
nip19_21("naddr", NIP19.encode(addr))
)
def renderEvent(
store: Store,
event: Event
): Resource[IO, HtmlDivElement[IO]] =
div(
cls := "text-md",
if event.pubkey.isEmpty then
Some(
div(
cls := "flex items-center",
entry("missing", "pubkey"),
button(
Styles.buttonSmall,
"fill with a debugging key",
onClick --> (_.foreach { _ =>
store.input.set(
event
.copy(pubkey = Some(keyOne.publicKey.xonly))
.asJson
.printWith(jsonPrinter)
)
})
)
)
)
else None,
if event.id.isEmpty then
Some(
div(
cls := "flex items-center",
entry("missing", "id"),
if event.pubkey.isDefined then
Some(
button(
Styles.buttonSmall,
"fill id",
onClick --> (_.foreach(_ =>
store.input.set(
event
.copy(id = Some(event.hash.toHex))
.asJson
.printWith(jsonPrinter)
)
))
)
)
else None
)
)
else None,
if event.sig.isEmpty then
Some(
div(
cls := "flex items-center",
entry("missing", "sig"),
if event.id.isDefined && event.pubkey == Some(
keyOne.publicKey.xonly
)
then
Some(
button(
Styles.buttonSmall,
"sign",
onClick --> (_.foreach(_ =>
store.input.set(
event
.sign(keyOne)
.asJson
.printWith(jsonPrinter)
)
))
)
)
else None
)
)
else None,
entry("serialized event", event.serialized),
entry("implied event id", event.hash.toHex),
entry(
"does the implied event id match the given event id?",
event.id == Some(event.hash.toHex) match {
case true => "yes"; case false => "no"
}
),
entry(
"is signature valid?",
event.isValid match {
case true => "yes"; case false => "no"
}
),
event.id.map(id =>
nip19_21(
"nevent",
NIP19.encode(EventPointer(id, author = event.pubkey))
)
),
event.id.map(id =>
entry(
"note",
NIP19.encode(ByteVector32.fromValidHex(id))
)
)
)
private def entry(
key: String,
value: String
): Resource[IO, HtmlDivElement[IO]] =
div(
cls := "flex items-center space-x-3",
span(cls := "font-bold", key + " "),
span(Styles.mono, cls := "max-w-xl", value)
)
private def nip19_21(
key: String,
code: String
): Resource[IO, HtmlDivElement[IO]] =
div(
span(cls := "font-bold", key + " "),
span(Styles.mono, cls := "break-all", code),
a(
href := "nostr:" + code,
external
)
)
private def relayHints(
store: Store,
relays: List[String],
dynamic: Boolean = true
): Resource[IO, HtmlDivElement[IO]] =
if !dynamic && relays.isEmpty then div("")
else
SignallingRef[IO].of(false).toResource.flatMap { active =>
val value =
if relays.size > 0 then relays.reduce((a, b) => s"$a, $b") else ""
div(
cls := "flex items-center space-x-3",
span(cls := "font-bold", "relay hints "),
span(Styles.mono, cls := "max-w-xl", value),
active.map {
case true =>
div(
input.withSelf { self =>
(
onKeyPress --> (_.foreach(evt =>
evt.key match {
case "Enter" =>
self.value.get.flatMap(url =>
if url.startsWith("wss://") || url
.startsWith("ws://")
then {
store.result.get.flatMap(result =>
store.input.set(
result
.map {
case a: AddressPointer =>
NIP19
.encode(
a.copy(relays = url :: a.relays)
)
case p: ProfilePointer =>
NIP19
.encode(
p.copy(relays = url :: p.relays)
)
case e: EventPointer =>
NIP19
.encode(
e.copy(relays = url :: e.relays)
)
case r => ""
}
.getOrElse("")
)
)
>> active.set(false)
} else IO.unit
)
case _ => IO.unit
}
))
)
}
)
case false if dynamic =>
button(
Styles.buttonSmall,
"add relay hint",
onClick --> (_.foreach(_ => active.set(true)))
)
case false => div("")
}
)
}
private val external = img(cls := "inline w-4 ml-2", src := "ext.svg")
}

View File

@@ -1,147 +0,0 @@
import cats.effect.*
import cats.effect.syntax.all.*
import cats.syntax.all.*
import fs2.concurrent.*
import fs2.dom.{Event => _, *}
import io.circe.parser.*
import io.circe.syntax.*
import calico.*
import calico.html.io.{*, given}
import calico.syntax.*
import scoin.*
import snow.*
import Utils.*
import Components.*
object Main extends IOWebApp {
def render: Resource[IO, HtmlDivElement[IO]] = Store(window).flatMap {
store =>
div(
cls := "flex w-full h-full flex-col items-center justify-center",
div(
cls := "w-4/5",
h1(
cls := "px-1 py-2 text-center text-xl",
img(
cls := "inline-block w-8 mr-2",
src := "/favicon.ico"
),
a(
href := "/",
"nostr army knife"
)
),
div(
cls := "flex my-3",
input(store),
actions(store)
),
result(store)
),
div(
cls := "flex justify-end mr-5 mt-10 text-xs w-4/5",
a(
href := "https://github.com/fiatjaf/nak",
"source code"
),
a(
cls := "ml-4",
href := "https://github.com/fiatjaf/nak",
"get the command-line tool"
)
)
)
}
def actions(store: Store): Resource[IO, HtmlDivElement[IO]] =
div(
cls := "flex flex-col space-y-1 my-3",
store.input.map {
case "" => div("")
case _ =>
button(
Styles.button,
"clear",
onClick --> (_.foreach(_ => store.input.set("")))
)
},
store.result.map {
case Right(_: Event) =>
button(
Styles.button,
"format",
onClick --> (_.foreach(_ =>
store.input.update(original =>
parse(original).toOption
.map(_.printWith(jsonPrinter))
.getOrElse(original)
)
))
)
case _ => div("")
},
button(
Styles.button,
"generate event",
onClick --> (_.foreach(_ =>
store.input.set(
Event(
kind = 1,
content = "hello world"
).sign(keyOne)
.asJson
.printWith(jsonPrinter)
)
))
),
button(
Styles.button,
"generate keypair",
onClick --> (_.foreach(_ =>
store.input.set(
NIP19.encode(PrivateKey(randomBytes32()))
)
))
)
)
def input(store: Store): Resource[IO, HtmlDivElement[IO]] =
div(
cls := "w-full grow",
div(
cls := "w-full flex justify-center",
textArea.withSelf { self =>
(
cls := "w-full max-h-96 p-3 rounded",
styleAttr := "min-height: 280px; font-family: monospace",
spellCheck := false,
placeholder := "paste something nostric (event JSON, nprofile, npub, nevent etc or hex key or id)",
onInput --> (_.foreach(_ =>
self.value.get.flatMap(store.input.set)
)),
value <-- store.input
)
}
)
)
def result(store: Store): Resource[IO, HtmlDivElement[IO]] =
div(
cls := "w-full flex my-5",
store.result.map {
case Left(msg) => div(msg)
case Right(bytes: ByteVector32) => render32Bytes(bytes)
case Right(event: Event) => renderEvent(store, event)
case Right(pp: ProfilePointer) => renderProfilePointer(store, pp)
case Right(evp: EventPointer) => renderEventPointer(store, evp)
case Right(sk: PrivateKey) =>
renderProfilePointer(
store,
ProfilePointer(pubkey = sk.publicKey.xonly),
Some(sk)
)
case Right(addr: AddressPointer) => renderAddressPointer(store, addr)
}
)
}

View File

@@ -1,45 +0,0 @@
import scala.util.Try
import io.circe.parser.*
import cats.syntax.all.*
import scodec.bits.ByteVector
import scoin.*
import snow.*
type Result = Either[
String,
Event | PrivateKey | AddressPointer | EventPointer | ProfilePointer |
ByteVector32
]
object Parser {
val additions = raw" *\+ *".r
def parseInput(input: String): Result =
if input == "" then Left("")
else
ByteVector
.fromHex(input)
.flatMap(b => Try(Right(ByteVector32(b))).toOption)
.getOrElse(
NIP19.decode(input) match {
case Right(pp: ProfilePointer) => Right(pp)
case Right(evp: EventPointer) => Right(evp)
case Right(sk: PrivateKey) => Right(sk)
case Right(addr: AddressPointer) => Right(addr)
case Left(_) =>
parse(input) match {
case Left(err: io.circe.ParsingFailure) =>
Left("not valid JSON or NIP-19 code")
case Right(json) =>
json
.as[Event]
.leftMap { err =>
err.pathToRootString match {
case None => s"decoding ${err.pathToRootString}"
case Some(path) => s"field $path is missing or wrong"
}
}
}
}
)
}

View File

@@ -1,46 +0,0 @@
import cats.data.*
import cats.effect.*
import cats.effect.syntax.all.*
import cats.syntax.all.*
import fs2.concurrent.*
import fs2.dom.{Event => _, *}
import scoin.PrivateKey
case class Store(
input: SignallingRef[IO, String],
result: SignallingRef[IO, Result]
)
object Store {
def apply(window: Window[IO]): Resource[IO, Store] = {
val key = "nak-input"
for {
input <- SignallingRef[IO].of("").toResource
result <- SignallingRef[IO, Result](Left("")).toResource
_ <- Resource.eval {
OptionT(window.localStorage.getItem(key))
.foreachF(input.set(_))
}
_ <- window.localStorage
.events(window)
.foreach {
case Storage.Event.Updated(`key`, _, value, _) =>
input.set(value)
case _ => IO.unit
}
.compile
.drain
.background
_ <- input.discrete
.evalTap(input => IO.cede *> window.localStorage.setItem(key, input))
.evalTap(input => result.set(Parser.parseInput(input.trim())))
.compile
.drain
.background
} yield Store(input, result)
}
}

View File

@@ -1,9 +0,0 @@
import calico.html.io.*
object Styles {
val button = cls :=
"shrink bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 mx-2 px-4 rounded "
val buttonSmall = cls :=
"shrink text-sm bg-blue-500 hover:bg-blue-700 text-white font-bold mx-2 px-2 rounded "
val mono = styleAttr := "font-family: monospace"
}

View File

@@ -1,22 +0,0 @@
import io.circe.Printer
import scodec.bits.ByteVector
import scoin.*
object Utils {
val keyOne = PrivateKey(ByteVector32(ByteVector(0x01).padLeft(32)))
val jsonPrinter = Printer(
dropNullValues = false,
indent = " ",
lbraceRight = "\n",
rbraceLeft = "\n",
lbracketRight = "\n",
rbracketLeft = "\n",
lrbracketsEmpty = "",
arrayCommaRight = "\n",
objectCommaRight = "\n",
colonLeft = "",
colonRight = " ",
sortKeys = true
)
}

42
verify.go Normal file
View File

@@ -0,0 +1,42 @@
package main
import (
"encoding/json"
"github.com/nbd-wtf/go-nostr"
"github.com/urfave/cli/v2"
)
var verify = &cli.Command{
Name: "verify",
Usage: "checks the hash and signature of an event given through stdin",
Description: `example:
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.
`,
Action: func(c *cli.Context) error {
for stdinEvent := range getStdinLinesOrBlank() {
evt := nostr.Event{}
if stdinEvent != "" {
if err := json.Unmarshal([]byte(stdinEvent), &evt); err != nil {
lineProcessingError(c, "invalid event: %s", err)
continue
}
}
if evt.GetID() != evt.ID {
lineProcessingError(c, "invalid .id, expected %s, got %s", evt.GetID(), evt.ID)
continue
}
if ok, err := evt.CheckSignature(); !ok {
lineProcessingError(c, "invalid signature: %s", err)
continue
}
}
exitIfLineProcessingError(c)
return nil
},
}