Compare commits

...

46 Commits

Author SHA1 Message Date
Yasuhiro Matsumoto
242b028656 -until now 2023-12-12 13:34:30 -03:00
Yasuhiro Matsumoto
f0d90b567c -since now 2023-12-12 13:34:30 -03:00
fiatjaf
2d1e27f766 fix for nil error case on publish. 2023-12-10 20:49:08 -03:00
fiatjaf
bfa72640cd bunker: a better prompt. 2023-12-09 17:42:01 -03:00
fiatjaf
e5b0b15908 bunker: improve error message. 2023-12-09 17:11:55 -03:00
fiatjaf
0860cfcf6d rename nsecbunker->bunker. 2023-12-09 16:37:59 -03:00
fiatjaf
b7b61c0723 support --auth/--sec/--prompt-sec on req. 2023-12-09 16:32:04 -03:00
fiatjaf
ed3156ae10 fix event publishing flow: no need to reconnect and AUTH messages make sense. 2023-12-09 09:14:45 -03:00
fiatjaf
30dbe2c1c0 fix handling multiple lines in event (broken in previous commit). 2023-12-07 18:14:26 -03:00
fiatjaf
4d75605c20 print event more consistently and auth when required and allowed. 2023-12-07 18:12:18 -03:00
fiatjaf
26b1aa359a nsecbunker/nip46 is working now. 2023-12-02 15:33:37 -03:00
fiatjaf
bc7cd0939c nsecbunker work-in-progress. 2023-12-02 12:20:15 -03:00
fiatjaf
5657fdc6a7 update go-nostr. 2023-12-01 13:22:04 -03:00
fiatjaf
d9d36e7619 use easyjson and envelopes. 2023-11-28 15:18:43 -03:00
fiatjaf
f2f9dda33a actually this is the fix. 2023-11-24 21:19:10 -03:00
fiatjaf
53cb2c0490 event: fix handling of -k and kind in stdin event, and default to 1. 2023-11-24 21:08:13 -03:00
fiatjaf
4a3c7dc825 remove extra whitespace on usage strings. 2023-11-20 15:01:53 -03:00
fiatjaf
05f2275c9e nak relay 2023-11-20 15:00:50 -03:00
fiatjaf
082be94614 update go-nostr. 2023-11-19 07:20:52 -03:00
fiatjaf
15217f2466 fetch: fix handling of --relay tags. 2023-11-15 09:48:57 -03:00
fiatjaf
8fbfdc65c8 add --silent global option to remove the stderr logs. 2023-11-13 15:03:27 -03:00
fiatjaf
11fe6b5809 connect to relays once per call instead of in each iteration and fail early if no connection works. 2023-11-13 14:57:35 -03:00
fiatjaf
6a7a5eb26e fix bug with kind being set to zero and replaced silently. 2023-11-13 10:34:09 -03:00
fiatjaf
795e98bc2e close channel in getStdinLinesOrFirstArgument() 2023-11-08 22:54:52 -03:00
fiatjaf
4fdd80670a encode npub and nprofile tests. 2023-11-08 22:54:34 -03:00
fiatjaf
e507d90766 beginnings of some humble tests. 2023-11-08 22:26:41 -03:00
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
32 changed files with 1166 additions and 1437 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

10
.gitignore vendored
View File

@@ -1,9 +1 @@
target
.bsp
globals.bundle.js
yarn.lock
node_modules
project/project
.metals
.bloop
project/metals.sbt
nak

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>

270
README.md
View File

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

140
bunker.go Normal file
View File

@@ -0,0 +1,140 @@
package main
import (
"encoding/json"
"fmt"
"net/url"
"os"
"github.com/manifoldco/promptui"
"github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nip19"
"github.com/nbd-wtf/go-nostr/nip46"
"github.com/urfave/cli/v2"
"golang.org/x/exp/slices"
)
var bunker = &cli.Command{
Name: "bunker",
Usage: "starts a NIP-46 signer daemon with the given --sec key",
ArgsUsage: "[relay...]",
Description: ``,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "sec",
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: "yes",
Aliases: []string{"y"},
Usage: "always respond to any NIP-46 requests from anyone",
},
},
Action: func(c *cli.Context) error {
// try to connect to the relays here
qs := url.Values{}
relayURLs := make([]string, 0, c.Args().Len())
if relayUrls := c.Args().Slice(); len(relayUrls) > 0 {
_, relays := connectToAllRelays(c.Context, relayUrls)
if len(relays) == 0 {
log("failed to connect to any of the given relays.\n")
os.Exit(3)
}
for _, relay := range relays {
relayURLs = append(relayURLs, relay.URL)
qs.Add("relay", relay.URL)
}
}
if len(relayURLs) == 0 {
return fmt.Errorf("not connected to any relays: please specify at least one")
}
// gather the secret key
sec, err := gatherSecretKeyFromArguments(c)
if err != nil {
return err
}
pubkey, err := nostr.GetPublicKey(sec)
if err != nil {
return err
}
npub, _ := nip19.EncodePublicKey(pubkey)
log("listening at %s%v%s:\n %spubkey:%s %s\n %snpub:%s %s\n %sconnection code:%s %s\n %sbunker:%s %s\n\n",
BOLD_ON, relayURLs, BOLD_OFF,
BOLD_ON, BOLD_OFF, pubkey,
BOLD_ON, BOLD_OFF, npub,
BOLD_ON, BOLD_OFF, fmt.Sprintf("%s#secret?%s", npub, qs.Encode()),
BOLD_ON, BOLD_OFF, fmt.Sprintf("bunker://%s?%s", pubkey, qs.Encode()),
)
alwaysYes := c.Bool("yes")
// subscribe to relays
pool := nostr.NewSimplePool(c.Context)
events := pool.SubMany(c.Context, relayURLs, nostr.Filters{
{
Kinds: []int{24133},
Tags: nostr.TagMap{"p": []string{pubkey}},
},
})
signer := nip46.NewSigner(sec)
for ie := range events {
req, resp, eventResponse, harmless, err := signer.HandleRequest(ie.Event)
if err != nil {
log("< failed to handle request from %s: %s", ie.Event.PubKey, err.Error())
continue
}
jreq, _ := json.MarshalIndent(req, " ", " ")
log("- got request from '%s': %s\n", ie.Event.PubKey, string(jreq))
jresp, _ := json.MarshalIndent(resp, " ", " ")
log("~ responding with %s\n", string(jresp))
if alwaysYes || harmless || askProceed(ie.Event.PubKey) {
if err := ie.Relay.Publish(c.Context, eventResponse); err == nil {
log("* sent response!\n")
} else {
log("* failed to send response: %s\n", err)
}
}
}
return nil
},
}
var allowedSources = make([]string, 0, 2)
func askProceed(source string) bool {
if slices.Contains(allowedSources, source) {
return true
}
prompt := promptui.Select{
Label: "proceed?",
Items: []string{
"no",
"yes",
"always from this source",
},
}
n, _, _ := prompt.Run()
switch n {
case 0:
return false
case 1:
return true
case 2:
allowedSources = append(allowedSources, source)
return true
}
return false
}

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
},
}

View File

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

Before

Width:  |  Height:  |  Size: 463 B

237
encode.go
View File

@@ -1,10 +1,7 @@
package main
import (
"encoding/hex"
"fmt"
"net/url"
"strings"
"github.com/nbd-wtf/go-nostr/nip19"
"github.com/urfave/cli/v2"
@@ -21,44 +18,52 @@ var encode = &cli.Command{
nak encode nevent --author <pubkey-hex> --relay <relay-url> --relay <other-relay> <event-id>
nak encode nsec <privkey-hex>`,
Before: func(c *cli.Context) error {
if c.Args().Len() < 2 {
return fmt.Errorf("expected more than 2 arguments.")
if c.Args().Len() < 1 {
return fmt.Errorf("expected more than 1 argument.")
}
return nil
},
Subcommands: []*cli.Command{
{
Name: "npub",
Usage: "encode a hex private key into bech32 'npub' format",
Usage: "encode a hex public key into bech32 'npub' format",
Action: func(c *cli.Context) error {
target := c.Args().First()
if err := validate32BytesHex(target); err != nil {
return err
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
}
}
if npub, err := nip19.EncodePublicKey(target); err == nil {
fmt.Println(npub)
return nil
} 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 {
target := c.Args().First()
if err := validate32BytesHex(target); err != nil {
return err
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
}
}
if npub, err := nip19.EncodePrivateKey(target); err == nil {
fmt.Println(npub)
return nil
} else {
return err
}
exitIfLineProcessingError(c)
return nil
},
},
{
@@ -72,22 +77,26 @@ var encode = &cli.Command{
},
},
Action: func(c *cli.Context) error {
target := c.Args().First()
if err := validate32BytesHex(target); err != nil {
return err
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
}
}
relays := c.StringSlice("relay")
if err := validateRelayURLs(relays); err != nil {
return err
}
if npub, err := nip19.EncodeProfile(target, relays); err == nil {
fmt.Println(npub)
return nil
} else {
return err
}
exitIfLineProcessingError(c)
return nil
},
},
{
@@ -105,29 +114,33 @@ var encode = &cli.Command{
},
},
Action: func(c *cli.Context) error {
target := c.Args().First()
if err := validate32BytesHex(target); err != nil {
return err
}
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 {
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
}
}
relays := c.StringSlice("relay")
if err := validateRelayURLs(relays); err != nil {
return err
}
if npub, err := nip19.EncodeEvent(target, relays, author); err == nil {
fmt.Println(npub)
return nil
} else {
return err
}
exitIfLineProcessingError(c)
return nil
},
},
{
@@ -137,7 +150,7 @@ var encode = &cli.Command{
&cli.StringFlag{
Name: "identifier",
Aliases: []string{"d"},
Usage: "the \"d\" tag identifier of this replaceable event",
Usage: "the \"d\" tag identifier of this replaceable event -- can also be read from stdin",
Required: true,
},
&cli.StringFlag{
@@ -159,83 +172,61 @@ var encode = &cli.Command{
},
},
Action: func(c *cli.Context) error {
pubkey := c.String("pubkey")
if err := validate32BytesHex(pubkey); err != nil {
return err
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
}
}
kind := c.Int("kind")
if kind < 30000 || kind >= 40000 {
return fmt.Errorf("kind must be between 30000 and 39999, as per NIP-16, got %d", kind)
}
d := c.String("identifier")
if d == "" {
return fmt.Errorf("\"d\" tag identifier can't be empty")
}
relays := c.StringSlice("relay")
if err := validateRelayURLs(relays); err != nil {
return err
}
if npub, err := nip19.EncodeEntity(pubkey, kind, d, relays); err == nil {
fmt.Println(npub)
return nil
} else {
return err
}
exitIfLineProcessingError(c)
return nil
},
},
{
Name: "note",
Usage: "generate note1 event codes (not recommended)",
Action: func(c *cli.Context) error {
target := c.Args().First()
if err := validate32BytesHex(target); err != nil {
return err
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
}
}
if npub, err := nip19.EncodeNote(target); err == nil {
fmt.Println(npub)
return nil
} else {
return err
}
exitIfLineProcessingError(c)
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 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
}

219
event.go
View File

@@ -13,6 +13,7 @@ import (
"github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nson"
"github.com/urfave/cli/v2"
"golang.org/x/exp/slices"
)
const CATEGORY_EVENT_FIELDS = "EVENT FIELDS"
@@ -20,21 +21,36 @@ 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: "auth",
Usage: "always perform NIP-42 \"AUTH\" when facing an \"auth-required: \" rejection and try again",
},
&cli.BoolFlag{
Name: "nson",
Usage: "encode the event using NSON",
@@ -44,7 +60,7 @@ standalone:
Aliases: []string{"k"},
Usage: "event kind",
DefaultText: "1",
Value: 1,
Value: 0,
Category: CATEGORY_EVENT_FIELDS,
},
&cli.StringFlag{
@@ -52,7 +68,7 @@ standalone:
Aliases: []string{"c"},
Usage: "event content",
DefaultText: "hello from the nostr army knife",
Value: "hello from the nostr army knife",
Value: "",
Category: CATEGORY_EVENT_FIELDS,
},
&cli.StringSliceFlag{
@@ -76,79 +92,118 @@ standalone:
Aliases: []string{"time", "ts"},
Usage: "unix timestamp value for the created_at field",
DefaultText: "now",
Value: "now",
Value: "",
Category: CATEGORY_EVENT_FIELDS,
},
},
ArgsUsage: "[relay...]",
Action: func(c *cli.Context) error {
evt := nostr.Event{
Kind: c.Int("kind"),
Content: c.String("content"),
Tags: make(nostr.Tags, 0, 3),
}
tags := make(nostr.Tags, 0, 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})
}
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)
// try to connect to the relays here
var relays []*nostr.Relay
if relayUrls := c.Args().Slice(); len(relayUrls) > 0 {
_, relays = connectToAllRelays(c.Context, relayUrls)
if len(relays) == 0 {
log("failed to connect to any of the given relays.\n")
os.Exit(3)
}
}
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)
// gather the secret key
sec, err := gatherSecretKeyFromArguments(c)
if err != nil {
return err
}
doAuth := c.Bool("auth")
// then process input and generate events
nextline:
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)
}
kindWasSupplied := false
mustRehashAndResign := false
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)
}
if stdinEvent != "" {
if err := easyjson.Unmarshal([]byte(stdinEvent), &evt); err != nil {
lineProcessingError(c, "invalid event received from stdin: %s", err)
continue
}
kindWasSupplied = strings.Contains(stdinEvent, `"kind"`)
}
if kind := c.Int("kind"); slices.Contains(c.FlagNames(), "kind") {
evt.Kind = kind
mustRehashAndResign = true
} else if !kindWasSupplied {
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)
}
}
} else {
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)
}
}
// print event as json
var result string
if c.Bool("envelope") {
j, _ := json.Marshal([]any{"EVENT", evt})
j, _ := json.Marshal(nostr.EventEnvelope{Event: evt})
result = string(j)
} else if c.Bool("nson") {
result, _ = nson.Marshal(&evt)
@@ -157,8 +212,42 @@ standalone:
result = string(j)
}
fmt.Println(result)
// publish to relays
if len(relays) > 0 {
os.Stdout.Sync()
for _, relay := range relays {
publish:
log("publishing to %s... ", relay.URL)
ctx, cancel := context.WithTimeout(c.Context, 10*time.Second)
defer cancel()
err := relay.Publish(ctx, evt)
if err == nil {
// published fine
log("success.\n")
continue nextline
}
// error publishing
if strings.HasPrefix(err.Error(), "msg: auth-required:") && sec != "" && doAuth {
// if the relay is requesting auth and we can auth, let's do it
pk, _ := nostr.GetPublicKey(sec)
log("performing auth as %s... ", pk)
if err := relay.Auth(c.Context, func(evt *nostr.Event) error { return evt.Sign(sec) }); err == nil {
// try to publish again, but this time don't try to auth again
doAuth = false
goto publish
} else {
log("auth error: %s. ", err)
}
}
log("failed: %s\n", err)
}
}
}
exitIfLineProcessingError(c)
return nil
},
}

31
example_test.go Normal file
View File

@@ -0,0 +1,31 @@
package main
func ExampleEventBasic() {
app.Run([]string{"nak", "event", "--ts", "1699485669"})
// Output:
// {"id":"36d88cf5fcc449f2390a424907023eda7a74278120eebab8d02797cd92e7e29c","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1699485669,"kind":1,"tags":[],"content":"hello from the nostr army knife","sig":"68e71a192e8abcf8582a222434ac823ecc50607450ebe8cc4c145eb047794cc382dc3f888ce879d2f404f5ba6085a47601360a0fa2dd4b50d317bd0c6197c2c2"}
}
func ExampleEventComplex() {
app.Run([]string{"nak", "event", "--ts", "1699485669", "-k", "11", "-c", "skjdbaskd", "--sec", "17", "-t", "t=spam", "-e", "36d88cf5fcc449f2390a424907023eda7a74278120eebab8d02797cd92e7e29c", "-t", "r=https://abc.def;nothing"})
// Output:
// {"id":"aec4de6d051a7c2b6ca2d087903d42051a31e07fb742f1240970084822de10a6","pubkey":"2fa2104d6b38d11b0230010559879124e42ab8dfeff5ff29dc9cdadd4ecacc3f","created_at":1699485669,"kind":11,"tags":[["t","spam"],["r","https://abc.def","nothing"],["e","36d88cf5fcc449f2390a424907023eda7a74278120eebab8d02797cd92e7e29c"]],"content":"skjdbaskd","sig":"1165ac7a27d774d351ef19c8e918fb22f4005fcba193976c3d7edba6ef87ead7f14467f376a9e199f8371835368d86a8506f591e382528d00287fb168a7b8f38"}
}
func ExampleReq() {
app.Run([]string{"nak", "req", "-k", "1", "-l", "18", "-a", "2fa2104d6b38d11b0230010559879124e42ab8dfeff5ff29dc9cdadd4ecacc3f", "-e", "aec4de6d051a7c2b6ca2d087903d42051a31e07fb742f1240970084822de10a6"})
// Output:
// ["REQ","nak",{"kinds":[1],"authors":["2fa2104d6b38d11b0230010559879124e42ab8dfeff5ff29dc9cdadd4ecacc3f"],"limit":18,"#e":["aec4de6d051a7c2b6ca2d087903d42051a31e07fb742f1240970084822de10a6"]}]
}
func ExampleEncodeNpub() {
app.Run([]string{"nak", "encode", "npub", "a6a67ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179822"})
// Output:
// npub156n8a7wuhwk9tgrzjh8gwzc8q2dlekedec5djk0js9d3d7qhnq3qjpdq28
}
func ExampleEncodeNprofile() {
app.Run([]string{"nak", "encode", "nprofile", "-r", "wss://example.com", "a6a67ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179822"})
// Output:
// nprofile1qqs2dfn7l8wthtz45p3ftn58pvrs9xlumvkuu2xet8egzkcklqtesgspz9mhxue69uhk27rpd4cxcefwvdhk6fl5jug
}

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="#3b82f6" stroke-width="1.5" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round"></path></svg>

Before

Width:  |  Height:  |  Size: 281 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

123
fetch.go
View File

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

39
go.mod
View File

@@ -1,35 +1,42 @@
module github.com/fiatjaf/nak
go 1.20
go 1.21
toolchain go1.21.0
require (
github.com/bgentry/speakeasy v0.1.0
github.com/mailru/easyjson v0.7.7
github.com/nbd-wtf/go-nostr v0.24.2
github.com/urfave/cli/v2 v2.25.3
github.com/nbd-wtf/go-nostr v0.27.0
github.com/nbd-wtf/nostr-sdk v0.0.5
github.com/urfave/cli/v2 v2.25.7
golang.org/x/exp v0.0.0-20231006140011-7918f672742d
)
require (
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/cespare/xxhash/v2 v2.1.1 // indirect
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // 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/dustin/go-humanize v1.0.1 // indirect
github.com/fiatjaf/eventstore v0.2.16 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.2.0 // indirect
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect
github.com/gobwas/ws v1.3.1 // indirect
github.com/golang/glog v1.1.2 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/manifoldco/promptui v0.9.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/puzpuzpuz/xsync/v2 v2.5.0 // 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/gjson v1.17.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
github.com/tidwall/pretty v1.2.1 // 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/sys v0.14.0 // indirect
)

72
go.sum
View File

@@ -1,18 +1,21 @@
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=
@@ -22,35 +25,46 @@ github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku
github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
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/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew 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/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/fiatjaf/eventstore v0.2.16 h1:NR64mnyUT5nJR8Sj2AwJTd1Hqs5kKJcCFO21ggUkvWg=
github.com/fiatjaf/eventstore v0.2.16/go.mod h1:rUc1KhVufVmC+HUOiuPweGAcvG6lEOQCkRCn2Xn5VRA=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.2.0 h1:u0p9s3xLYpZCA1z5JgCkMeB34CKCMMQbM+G8Ii7YD0I=
github.com/gobwas/ws v1.2.0/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
github.com/gobwas/ws v1.3.1 h1:Qi34dfLMWJbiKaNbDVzM9x27nZBjmkaW6i4+Ku+pGVU=
github.com/gobwas/ws v1.3.1/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.1.2 h1:DVjP2PbBOzHyzA+dn3WhHIq4NdVu3Q+pvivFICf/7fo=
github.com/golang/glog v1.1.2/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ=
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=
@@ -71,8 +85,12 @@ 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.24.2 h1:1PdFED7uHh3BlXfDVD96npBc0YAgj9hPT+l6NWog4kc=
github.com/nbd-wtf/go-nostr v0.24.2/go.mod h1:eE8Qf8QszZbCd9arBQyotXqATNUElWsTEEx+LLORhyQ=
github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
github.com/nbd-wtf/go-nostr v0.27.0 h1:h6JmMMmfNcAORTL2kk/K3+U6Mju6rk/IjcHA/PMeOc8=
github.com/nbd-wtf/go-nostr v0.27.0/go.mod h1:bkffJI+x914sPQWum9ZRUn66D7NpDnAoWo1yICvj3/0=
github.com/nbd-wtf/nostr-sdk v0.0.5 h1:rec+FcDizDVO0W25PX0lgYMXvP7zNNOgI3Fu9UCm4BY=
github.com/nbd-wtf/nostr-sdk v0.0.5/go.mod h1:iJJsikesCGLNFZ9dLqhLPDzdt924EagUmdQxT3w2Lmk=
github.com/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=
@@ -86,38 +104,42 @@ 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.0 h1:2k4qrO/orvmEXZ3hmtHqIy9XaQtPTwzMZk1+iErpE8c=
github.com/puzpuzpuz/xsync/v2 v2.5.0/go.mod h1:gD2H2krq/w52MfPLE+Uy64TzJDVY7lP2znR9qmR35kU=
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 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
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=
github.com/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM=
github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/urfave/cli/v2 v2.25.3 h1:VJkt6wvEBOoSjPFQvOkv6iWIrsJyCrKGtCtxXWwmGeY=
github.com/urfave/cli/v2 v2.25.3/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs=
github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-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-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
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.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
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-20181122145206-62eef0e2fa9b/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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -127,8 +149,9 @@ golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
golang.org/x/sys v0.14.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.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -150,3 +173,4 @@ 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=

170
helpers.go Normal file
View File

@@ -0,0 +1,170 @@
package main
import (
"bufio"
"context"
"encoding/hex"
"fmt"
"net/url"
"os"
"strings"
"github.com/bgentry/speakeasy"
"github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nip19"
"github.com/urfave/cli/v2"
)
const (
LINE_PROCESSING_ERROR = iota
BOLD_ON = "\033[1m"
BOLD_OFF = "\033[21m"
)
var log = func(msg string, args ...any) {
fmt.Fprintf(os.Stderr, msg, args...)
}
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
close(single)
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 connectToAllRelays(
ctx context.Context,
relayUrls []string,
opts ...nostr.PoolOption,
) (*nostr.SimplePool, []*nostr.Relay) {
relays := make([]*nostr.Relay, 0, len(relayUrls))
pool := nostr.NewSimplePool(ctx, opts...)
for _, url := range relayUrls {
log("connecting to %s... ", url)
if relay, err := pool.EnsureRelay(url); err == nil {
relays = append(relays, relay)
log("ok.\n")
} else {
log(err.Error() + "\n")
}
}
return pool, relays
}
func lineProcessingError(c *cli.Context, msg string, args ...any) {
c.Context = context.WithValue(c.Context, LINE_PROCESSING_ERROR, true)
log(msg+"\n", args...)
}
func exitIfLineProcessingError(c *cli.Context) {
if val := c.Context.Value(LINE_PROCESSING_ERROR); val != nil && val.(bool) {
os.Exit(123)
}
}
func gatherSecretKeyFromArguments(c *cli.Context) (string, error) {
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)
}
}
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")
}
return sec, nil
}

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

40
main.go
View File

@@ -7,20 +7,36 @@ import (
"github.com/urfave/cli/v2"
)
func main() {
app := &cli.App{
Name: "nak",
Usage: "the nostr army knife command-line tool",
Commands: []*cli.Command{
req,
count,
fetch,
event,
decode,
encode,
var app = &cli.App{
Name: "nak",
Usage: "the nostr army knife command-line tool",
Commands: []*cli.Command{
req,
count,
fetch,
event,
decode,
encode,
verify,
relay,
bunker,
},
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "silent",
Usage: "do not print logs and info messages to stderr",
Aliases: []string{"s"},
Action: func(ctx *cli.Context, b bool) error {
if b {
log = func(msg string, args ...any) {}
}
return nil
},
},
}
},
}
func main() {
if err := app.Run(os.Args); err != nil {
fmt.Println(err)
os.Exit(1)

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")

37
relay.go Normal file
View File

@@ -0,0 +1,37 @@
package main
import (
"encoding/json"
"fmt"
"strings"
"github.com/nbd-wtf/go-nostr/nip11"
"github.com/urfave/cli/v2"
)
var relay = &cli.Command{
Name: "relay",
Usage: "gets the relay information document for the given relay, as JSON",
Description: `example:
nak relay nostr.wine`,
ArgsUsage: "<relay-url>",
Action: func(c *cli.Context) error {
url := c.Args().First()
if url == "" {
return fmt.Errorf("specify the <relay-url>")
}
if !strings.HasPrefix(url, "wss://") && !strings.HasPrefix(url, "ws://") {
url = "wss://" + url
}
info, err := nip11.Fetch(c.Context, url)
if err != nil {
return fmt.Errorf("failed to fetch '%s' information document: %w", url, err)
}
pretty, _ := json.MarshalIndent(info, "", " ")
fmt.Println(string(pretty))
return nil
},
}

210
req.go
View File

@@ -3,8 +3,11 @@ package main
import (
"encoding/json"
"fmt"
"os"
"strconv"
"strings"
"github.com/mailru/easyjson"
"github.com/nbd-wtf/go-nostr"
"github.com/urfave/cli/v2"
)
@@ -16,10 +19,14 @@ 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",
@@ -55,13 +62,13 @@ standalone:
Usage: "shortcut for --tag p=<value>",
Category: CATEGORY_FILTER_ATTRIBUTES,
},
&cli.IntFlag{
&cli.StringFlag{
Name: "since",
Aliases: []string{"s"},
Usage: "only accept events newer than this (unix timestamp)",
Category: CATEGORY_FILTER_ATTRIBUTES,
},
&cli.IntFlag{
&cli.StringFlag{
Name: "until",
Aliases: []string{"u"},
Usage: "only accept events older than this (unix timestamp)",
@@ -78,92 +85,155 @@ standalone:
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",
},
&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.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",
},
},
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
}
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)
var pool *nostr.SimplePool
relayUrls := c.Args().Slice()
if len(relayUrls) > 0 {
var relays []*nostr.Relay
pool, relays = connectToAllRelays(c.Context, relayUrls, nostr.WithAuthHandler(func(evt *nostr.Event) error {
if !c.Bool("auth") {
return fmt.Errorf("auth not authorized")
}
sec, err := gatherSecretKeyFromArguments(c)
if err != nil {
return err
}
pk, _ := nostr.GetPublicKey(sec)
log("performing auth as %s...\n", pk)
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
}
}
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 stdinFilter := range getStdinLinesOrBlank() {
filter := nostr.Filter{}
if stdinFilter != "" {
if err := easyjson.Unmarshal([]byte(stdinFilter), &filter); err != nil {
lineProcessingError(c, "invalid filter '%s' received from stdin: %s", stdinFilter, err)
continue
}
}
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.String("since"); since != "" {
if since == "now" {
ts := nostr.Now()
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 until := c.String("until"); until != "" {
if until == "now" {
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 = 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 ie := range fn(c.Context, relays, nostr.Filters{filter}) {
fmt.Println(ie.Event)
}
} else {
// no relays given, will just print the filter
var result string
if c.Bool("bare") {
result = filter.String()
if len(relayUrls) > 0 {
fn := pool.SubManyEose
if c.Bool("stream") {
fn = pool.SubMany
}
for ie := range fn(c.Context, relayUrls, 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(nostr.ReqEnvelope{SubscriptionID: "nak", Filters: nostr.Filters{filter}})
result = string(j)
}
fmt.Println(result)
fmt.Println(result)
}
}
exitIfLineProcessingError(c)
return nil
},
}

View File

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

View File

@@ -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 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(store, 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,61 +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(_) if input.split(":").size == 3 =>
// parse "a" tag format, nip 33
val spl = input.split(":")
(
spl(0).toIntOption,
ByteVector.fromHex(spl(1)),
Some(spl(2))
).mapN((kind, author, identifier) =>
AddressPointer(
identifier,
kind,
scoin.XOnlyPublicKey(ByteVector32(author)),
relays = List.empty
)
).toRight("couldn't parse as a nip33 'a' tag")
case Left(_) =>
// parse event json
parse(input) match {
case Left(err: io.circe.ParsingFailure) =>
Left("not valid JSON or NIP-19 code")
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
)
}

41
verify.go Normal file
View File

@@ -0,0 +1,41 @@
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
},
}