mirror of
https://github.com/fiatjaf/nak.git
synced 2026-08-01 20:26:14 +00:00
Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
08015ed306 | ||
|
|
93359bc1e8 | ||
|
|
7debc6fd2d | ||
|
|
143c7fe000 | ||
|
|
1d26118430 | ||
|
|
e7027c169a | ||
|
|
78b5cccfc2 | ||
|
|
20ea54a282 | ||
|
|
76c8caa2de | ||
|
|
8312d9ece7 |
+147
@@ -0,0 +1,147 @@
|
||||
{
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2020,
|
||||
"ecmaFeatures": {
|
||||
"jsx": true
|
||||
},
|
||||
"sourceType": "module",
|
||||
"allowImportExportEverywhere": false
|
||||
},
|
||||
|
||||
"env": {
|
||||
"es6": true,
|
||||
"node": true
|
||||
},
|
||||
|
||||
"plugins": [
|
||||
"react",
|
||||
"babel"
|
||||
],
|
||||
|
||||
"globals": {
|
||||
"document": false,
|
||||
"navigator": false,
|
||||
"window": false,
|
||||
"location": false,
|
||||
"URL": false,
|
||||
"URLSearchParams": false,
|
||||
"fetch": false,
|
||||
"EventSource": false,
|
||||
"localStorage": false,
|
||||
"sessionStorage": false
|
||||
},
|
||||
|
||||
"rules": {
|
||||
"react/jsx-uses-vars": 2,
|
||||
"react/jsx-no-undef": 2,
|
||||
"react/jsx-uses-react": 2,
|
||||
"accessor-pairs": 2,
|
||||
"arrow-spacing": [2, { "before": true, "after": true }],
|
||||
"block-spacing": [2, "always"],
|
||||
"brace-style": [2, "1tbs", { "allowSingleLine": true }],
|
||||
"comma-dangle": 0,
|
||||
"comma-spacing": [2, { "before": false, "after": true }],
|
||||
"comma-style": [2, "last"],
|
||||
"constructor-super": 2,
|
||||
"curly": [0, "multi-line"],
|
||||
"dot-location": [2, "property"],
|
||||
"eol-last": 2,
|
||||
"eqeqeq": [2, "allow-null"],
|
||||
"generator-star-spacing": [2, { "before": true, "after": true }],
|
||||
"handle-callback-err": [2, "^(err|error)$" ],
|
||||
"indent": 0,
|
||||
"jsx-quotes": [2, "prefer-double"],
|
||||
"key-spacing": [2, { "beforeColon": false, "afterColon": true }],
|
||||
"keyword-spacing": [2, { "before": true, "after": true }],
|
||||
"new-cap": 0,
|
||||
"new-parens": 0,
|
||||
"no-array-constructor": 2,
|
||||
"no-caller": 2,
|
||||
"no-class-assign": 2,
|
||||
"no-cond-assign": 2,
|
||||
"no-const-assign": 2,
|
||||
"no-control-regex": 0,
|
||||
"no-debugger": 0,
|
||||
"no-delete-var": 2,
|
||||
"no-dupe-args": 2,
|
||||
"no-dupe-class-members": 2,
|
||||
"no-dupe-keys": 2,
|
||||
"no-duplicate-case": 2,
|
||||
"no-empty-character-class": 2,
|
||||
"no-empty-pattern": 2,
|
||||
"no-eval": 0,
|
||||
"no-ex-assign": 2,
|
||||
"no-extend-native": 2,
|
||||
"no-extra-bind": 2,
|
||||
"no-extra-boolean-cast": 2,
|
||||
"no-extra-parens": [2, "functions"],
|
||||
"no-fallthrough": 2,
|
||||
"no-floating-decimal": 2,
|
||||
"no-func-assign": 2,
|
||||
"no-implied-eval": 2,
|
||||
"no-inner-declarations": [0, "functions"],
|
||||
"no-invalid-regexp": 2,
|
||||
"no-irregular-whitespace": 2,
|
||||
"no-iterator": 2,
|
||||
"no-label-var": 2,
|
||||
"no-labels": [2, { "allowLoop": false, "allowSwitch": false }],
|
||||
"no-lone-blocks": 2,
|
||||
"no-mixed-spaces-and-tabs": 2,
|
||||
"no-multi-spaces": 2,
|
||||
"no-multi-str": 2,
|
||||
"no-multiple-empty-lines": [2, { "max": 2 }],
|
||||
"no-native-reassign": 2,
|
||||
"no-negated-in-lhs": 2,
|
||||
"no-new": 0,
|
||||
"no-new-func": 2,
|
||||
"no-new-object": 2,
|
||||
"no-new-require": 2,
|
||||
"no-new-symbol": 2,
|
||||
"no-new-wrappers": 2,
|
||||
"no-obj-calls": 2,
|
||||
"no-octal": 2,
|
||||
"no-octal-escape": 2,
|
||||
"no-path-concat": 0,
|
||||
"no-proto": 2,
|
||||
"no-redeclare": 2,
|
||||
"no-regex-spaces": 2,
|
||||
"no-return-assign": 0,
|
||||
"no-self-assign": 2,
|
||||
"no-self-compare": 2,
|
||||
"no-sequences": 2,
|
||||
"no-shadow-restricted-names": 2,
|
||||
"no-spaced-func": 2,
|
||||
"no-sparse-arrays": 2,
|
||||
"no-this-before-super": 2,
|
||||
"no-throw-literal": 2,
|
||||
"no-trailing-spaces": 2,
|
||||
"no-undef": 2,
|
||||
"no-undef-init": 2,
|
||||
"no-unexpected-multiline": 2,
|
||||
"no-unneeded-ternary": [2, { "defaultAssignment": false }],
|
||||
"no-unreachable": 2,
|
||||
"no-unused-vars": [2, { "vars": "local", "args": "none", "varsIgnorePattern": "^_"}],
|
||||
"no-useless-call": 2,
|
||||
"no-useless-constructor": 2,
|
||||
"no-with": 2,
|
||||
"one-var": [0, { "initialized": "never" }],
|
||||
"operator-linebreak": [2, "after", { "overrides": { "?": "before", ":": "before" } }],
|
||||
"padded-blocks": [2, "never"],
|
||||
"quotes": [2, "single", { "avoidEscape": true, "allowTemplateLiterals": true }],
|
||||
"semi": [2, "never"],
|
||||
"semi-spacing": [2, { "before": false, "after": true }],
|
||||
"space-before-blocks": [2, "always"],
|
||||
"space-before-function-paren": 0,
|
||||
"space-in-parens": [2, "never"],
|
||||
"space-infix-ops": 2,
|
||||
"space-unary-ops": [2, { "words": true, "nonwords": false }],
|
||||
"spaced-comment": 0,
|
||||
"template-curly-spacing": [2, "never"],
|
||||
"use-isnan": 2,
|
||||
"valid-typeof": 2,
|
||||
"wrap-iife": [2, "any"],
|
||||
"yield-star-spacing": [2, "both"],
|
||||
"yoda": 0,
|
||||
"no-unsafe-optional-chaning": 0
|
||||
}
|
||||
}
|
||||
@@ -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: .
|
||||
@@ -1,39 +0,0 @@
|
||||
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
|
||||
+2
-8
@@ -1,9 +1,3 @@
|
||||
target
|
||||
.bsp
|
||||
globals.bundle.js
|
||||
yarn.lock
|
||||
node_modules
|
||||
project/project
|
||||
.metals
|
||||
.bloop
|
||||
project/metals.sbt
|
||||
public/app.js
|
||||
yarn.lock
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
arrowParens: avoid
|
||||
bracketSpacing: false
|
||||
jsxBracketSameLine: false
|
||||
printWidth: 80
|
||||
proseWrap: preserve
|
||||
semi: false
|
||||
singleQuote: true
|
||||
trailingComma: none
|
||||
useTabs: false
|
||||
@@ -1,2 +0,0 @@
|
||||
version = 3.5.8
|
||||
runner.dialect = scala3
|
||||
@@ -1,225 +1 @@
|
||||
# nostr army knife
|
||||
|
||||
this repository contains two things:
|
||||
|
||||
## a command-line tool for decoding and encoding nostr entities and talking to relays
|
||||
|
||||
Install with `go install github.com/fiatjaf/nak`.
|
||||
|
||||
It pairs nicely with https://github.com/blakejakopovic/nostcat using unix pipes.
|
||||
|
||||
### examples
|
||||
|
||||
```
|
||||
~> 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
|
||||
publishing to wss://relay.damus.io... success.
|
||||
|
||||
~> nak decode nevent1qqs29yet5tp0qq5xu5qgkeehkzqh5qu46739axzezcxpj4tjlkx9j7gpr4mhxue69uhkummnw3ez6ur4vgh8wetvd3hhyer9wghxuet5sh59ud
|
||||
{
|
||||
"id": "a2932ba2c2f00286e5008b6737b0817a0395d7a25e9859160c195572fd8c5979",
|
||||
"relays": [
|
||||
"wss://nostr-pub.wellorder.net"
|
||||
]
|
||||
}
|
||||
|
||||
~> 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
|
||||
```
|
||||
|
||||
written in go using [go-nostr](https://github.com/nbd-wtf/go-nostr), heavily inspired by [nostril](http://git.jb55.com/nostril/).
|
||||
|
||||
## a toolkit for debugging all things nostr as a webpage:
|
||||
|
||||

|
||||
|
||||
written in [scala](https://scala-lang.org/) with [calico](https://www.armanbilge.com/calico/) and [snow](https://github.com/fiatjaf/snow)
|
||||
[](https://app.netlify.com/sites/nostr-army-knife/deploys)
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import React, {useState, useEffect} from 'react'
|
||||
import {render} from 'react-dom'
|
||||
|
||||
import Nothing from './handlers/Nothing'
|
||||
import EventSigning from './handlers/EventSigning'
|
||||
import KeyHandling from './handlers/KeyHandling'
|
||||
|
||||
const handlers = [EventSigning, KeyHandling, Nothing]
|
||||
|
||||
function App() {
|
||||
let [value, setValue] = useState(localStorage.getItem('value'))
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem('value', value)
|
||||
}, [value])
|
||||
|
||||
let Result
|
||||
for (let i = 0; i < handlers.length; i++) {
|
||||
let alt = handlers[i]
|
||||
if (alt.match(value)) {
|
||||
Result = alt
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<main style={{fontFamily: 'monospace'}}>
|
||||
<div
|
||||
style={{
|
||||
width: '90%',
|
||||
margin: 'auto'
|
||||
}}
|
||||
>
|
||||
<h1>nostr army knife</h1>
|
||||
paste something nostric
|
||||
<textarea
|
||||
value={value}
|
||||
onChange={e => setValue(e.target.value)}
|
||||
style={{
|
||||
padding: '7px',
|
||||
width: '100%',
|
||||
minHeight: '200px'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<hr style={{margin: '18px 0'}} />
|
||||
<div
|
||||
style={{
|
||||
width: '90%',
|
||||
margin: 'auto'
|
||||
}}
|
||||
>
|
||||
<Result value={value} />
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
render(<App />, document.getElementById('app'))
|
||||
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const esbuild = require('esbuild')
|
||||
const alias = require('esbuild-plugin-alias')
|
||||
const nodeGlobals = require('@esbuild-plugins/node-globals-polyfill').default
|
||||
|
||||
const prod = process.argv.indexOf('prod') !== -1
|
||||
|
||||
esbuild
|
||||
.build({
|
||||
bundle: true,
|
||||
entryPoints: ['app.jsx'],
|
||||
outdir: 'public',
|
||||
plugins: [
|
||||
alias({
|
||||
stream: require.resolve('readable-stream')
|
||||
}),
|
||||
nodeGlobals({buffer: true})
|
||||
],
|
||||
sourcemap: prod ? false : 'inline',
|
||||
define: {
|
||||
window: 'self',
|
||||
global: 'self'
|
||||
}
|
||||
})
|
||||
.then(() => console.log('build success.'))
|
||||
@@ -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) }
|
||||
)
|
||||
@@ -0,0 +1,19 @@
|
||||
import React from 'react'
|
||||
|
||||
export default function Item({label, hint, children}) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
marginBottom: '9px',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordWrap: 'break-word',
|
||||
wordBreak: 'break-all'
|
||||
}}
|
||||
>
|
||||
<b data-wenk={hint} data-wenk-pos="right">
|
||||
{label}:{' '}
|
||||
</b>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
var count = &cli.Command{
|
||||
Name: "count",
|
||||
Usage: "generates encoded COUNT messages and optionally use them to talk to relays",
|
||||
Description: `outputs a NIP-45 request (the flags are mostly the same as 'nak req').`,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringSliceFlag{
|
||||
Name: "author",
|
||||
Aliases: []string{"a"},
|
||||
Usage: "only accept events from these authors (pubkey as hex)",
|
||||
Category: CATEGORY_FILTER_ATTRIBUTES,
|
||||
},
|
||||
&cli.IntSliceFlag{
|
||||
Name: "kind",
|
||||
Aliases: []string{"k"},
|
||||
Usage: "only accept events with these kind numbers",
|
||||
Category: CATEGORY_FILTER_ATTRIBUTES,
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "tag",
|
||||
Aliases: []string{"t"},
|
||||
Usage: "takes a tag like -t e=<id>, only accept events with these tags",
|
||||
Category: CATEGORY_FILTER_ATTRIBUTES,
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "e",
|
||||
Usage: "shortcut for --tag e=<value>",
|
||||
Category: CATEGORY_FILTER_ATTRIBUTES,
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "p",
|
||||
Usage: "shortcut for --tag p=<value>",
|
||||
Category: CATEGORY_FILTER_ATTRIBUTES,
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: "since",
|
||||
Aliases: []string{"s"},
|
||||
Usage: "only accept events newer than this (unix timestamp)",
|
||||
Category: CATEGORY_FILTER_ATTRIBUTES,
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: "until",
|
||||
Aliases: []string{"u"},
|
||||
Usage: "only accept events older than this (unix timestamp)",
|
||||
Category: CATEGORY_FILTER_ATTRIBUTES,
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: "limit",
|
||||
Aliases: []string{"l"},
|
||||
Usage: "only accept up to this number of events",
|
||||
Category: CATEGORY_FILTER_ATTRIBUTES,
|
||||
},
|
||||
},
|
||||
ArgsUsage: "[relay...]",
|
||||
Action: func(c *cli.Context) error {
|
||||
filter := nostr.Filter{}
|
||||
|
||||
if authors := c.StringSlice("author"); len(authors) > 0 {
|
||||
filter.Authors = authors
|
||||
}
|
||||
if ids := c.StringSlice("id"); len(ids) > 0 {
|
||||
filter.IDs = ids
|
||||
}
|
||||
if kinds := c.IntSlice("kind"); len(kinds) > 0 {
|
||||
filter.Kinds = kinds
|
||||
}
|
||||
|
||||
tags := make([][]string, 0, 5)
|
||||
for _, tagFlag := range c.StringSlice("tag") {
|
||||
spl := strings.Split(tagFlag, "=")
|
||||
if len(spl) == 2 && len(spl[0]) == 1 {
|
||||
tags = append(tags, spl)
|
||||
} else {
|
||||
return fmt.Errorf("invalid --tag '%s'", tagFlag)
|
||||
}
|
||||
}
|
||||
for _, etag := range c.StringSlice("e") {
|
||||
tags = append(tags, []string{"e", etag})
|
||||
}
|
||||
for _, ptag := range c.StringSlice("p") {
|
||||
tags = append(tags, []string{"p", ptag})
|
||||
}
|
||||
if len(tags) > 0 {
|
||||
filter.Tags = make(nostr.TagMap)
|
||||
for _, tag := range tags {
|
||||
if _, ok := filter.Tags[tag[0]]; !ok {
|
||||
filter.Tags[tag[0]] = make([]string, 0, 3)
|
||||
}
|
||||
filter.Tags[tag[0]] = append(filter.Tags[tag[0]], tag[1])
|
||||
}
|
||||
}
|
||||
|
||||
if since := c.Int("since"); since != 0 {
|
||||
ts := nostr.Timestamp(since)
|
||||
filter.Since = &ts
|
||||
}
|
||||
if until := c.Int("until"); until != 0 {
|
||||
ts := nostr.Timestamp(until)
|
||||
filter.Until = &ts
|
||||
}
|
||||
if limit := c.Int("limit"); limit != 0 {
|
||||
filter.Limit = limit
|
||||
}
|
||||
|
||||
relays := c.Args().Slice()
|
||||
successes := 0
|
||||
failures := make([]error, 0, len(relays))
|
||||
if len(relays) > 0 {
|
||||
for _, relayUrl := range relays {
|
||||
relay, err := nostr.RelayConnect(c.Context, relayUrl)
|
||||
if err != nil {
|
||||
failures = append(failures, err)
|
||||
continue
|
||||
}
|
||||
count, err := relay.Count(c.Context, nostr.Filters{filter})
|
||||
if err != nil {
|
||||
failures = append(failures, err)
|
||||
continue
|
||||
}
|
||||
fmt.Printf("%s: %d\n", relay.URL, count)
|
||||
successes++
|
||||
}
|
||||
if successes == 0 {
|
||||
return errors.Join(failures...)
|
||||
}
|
||||
} else {
|
||||
// no relays given, will just print the filter
|
||||
var result string
|
||||
j, _ := json.Marshal([]any{"COUNT", "nak", filter})
|
||||
result = string(j)
|
||||
fmt.Println(result)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/nbd-wtf/go-nostr/nip19"
|
||||
"github.com/nbd-wtf/go-nostr/sdk"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
var decode = &cli.Command{
|
||||
Name: "decode",
|
||||
Usage: "decodes nip19, nip21, nip05 or hex entities",
|
||||
Description: `example usage:
|
||||
nak decode npub1uescmd5krhrmj9rcura833xpke5eqzvcz5nxjw74ufeewf2sscxq4g7chm
|
||||
nak decode nevent1qqs29yet5tp0qq5xu5qgkeehkzqh5qu46739axzezcxpj4tjlkx9j7gpr4mhxue69uhkummnw3ez6ur4vgh8wetvd3hhyer9wghxuet5sh59ud
|
||||
nak decode nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpz4mhxue69uhk2er9dchxummnw3ezumrpdejqz8thwden5te0dehhxarj94c82c3wwajkcmr0wfjx2u3wdejhgqgcwaehxw309aex2mrp0yhxummnw3exzarf9e3k7mgnp0sh5
|
||||
nak decode nsec1jrmyhtjhgd9yqalps8hf9mayvd58852gtz66m7tqpacjedkp6kxq4dyxsr`,
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "id",
|
||||
Aliases: []string{"e"},
|
||||
Usage: "return just the event id, if applicable",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "pubkey",
|
||||
Aliases: []string{"p"},
|
||||
Usage: "return just the pubkey, if applicable",
|
||||
},
|
||||
},
|
||||
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))
|
||||
}
|
||||
} 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")
|
||||
}
|
||||
|
||||
fmt.Println(decodeResult.JSON())
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
type DecodeResult struct {
|
||||
*nostr.EventPointer
|
||||
*nostr.ProfilePointer
|
||||
*nostr.EntityPointer
|
||||
HexResult struct {
|
||||
PossibleTypes []string `json:"possible_types"`
|
||||
PublicKey string `json:"pubkey,omitempty"`
|
||||
ID string `json:"event_id,omitempty"`
|
||||
PrivateKey string `json:"private_key,omitempty"`
|
||||
Signature string `json:"sig,omitempty"`
|
||||
}
|
||||
PrivateKey struct {
|
||||
nostr.ProfilePointer
|
||||
PrivateKey string `json:"private_key"`
|
||||
}
|
||||
}
|
||||
|
||||
func (d DecodeResult) JSON() string {
|
||||
var j []byte
|
||||
if d.EventPointer != nil {
|
||||
j, _ = json.MarshalIndent(d.EventPointer, "", " ")
|
||||
} else if d.ProfilePointer != nil {
|
||||
j, _ = json.MarshalIndent(d.ProfilePointer, "", " ")
|
||||
} else if d.EntityPointer != nil {
|
||||
j, _ = json.MarshalIndent(d.EntityPointer, "", " ")
|
||||
} else if len(d.HexResult.PossibleTypes) > 0 {
|
||||
j, _ = json.MarshalIndent(d.HexResult, "", " ")
|
||||
} else if d.PrivateKey.PrivateKey != "" {
|
||||
j, _ = json.MarshalIndent(d.PrivateKey, "", " ")
|
||||
}
|
||||
return string(j)
|
||||
}
|
||||
@@ -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 |
@@ -1,221 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/nbd-wtf/go-nostr/nip19"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
var encode = &cli.Command{
|
||||
Name: "encode",
|
||||
Usage: "encodes notes and other stuff to nip19 entities",
|
||||
Description: `example usage:
|
||||
nak encode npub <pubkey-hex>
|
||||
nak encode nprofile <pubkey-hex>
|
||||
nak encode nprofile --relay <relay-url> <pubkey-hex>
|
||||
nak encode nevent <event-id>
|
||||
nak encode nevent --author <pubkey-hex> --relay <relay-url> --relay <other-relay> <event-id>
|
||||
nak encode nsec <privkey-hex>`,
|
||||
Before: func(c *cli.Context) error {
|
||||
if c.Args().Len() < 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",
|
||||
Action: func(c *cli.Context) error {
|
||||
target := getStdinOrFirstArgument(c)
|
||||
if err := validate32BytesHex(target); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if npub, err := nip19.EncodePublicKey(target); err == nil {
|
||||
fmt.Println(npub)
|
||||
return nil
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "nsec",
|
||||
Usage: "encode a hex private key into bech32 'nsec' format",
|
||||
Action: func(c *cli.Context) error {
|
||||
target := getStdinOrFirstArgument(c)
|
||||
if err := validate32BytesHex(target); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if npub, err := nip19.EncodePrivateKey(target); err == nil {
|
||||
fmt.Println(npub)
|
||||
return nil
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "nprofile",
|
||||
Usage: "generate profile codes with attached relay information",
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringSliceFlag{
|
||||
Name: "relay",
|
||||
Aliases: []string{"r"},
|
||||
Usage: "attach relay hints to nprofile code",
|
||||
},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
target := getStdinOrFirstArgument(c)
|
||||
if err := validate32BytesHex(target); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
relays := c.StringSlice("relay")
|
||||
if err := validateRelayURLs(relays); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if npub, err := nip19.EncodeProfile(target, relays); err == nil {
|
||||
fmt.Println(npub)
|
||||
return nil
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "nevent",
|
||||
Usage: "generate event codes with optionally attached relay information",
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringSliceFlag{
|
||||
Name: "relay",
|
||||
Aliases: []string{"r"},
|
||||
Usage: "attach relay hints to nevent code",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "author",
|
||||
Usage: "attach an author pubkey as a hint to the nevent code",
|
||||
},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
target := getStdinOrFirstArgument(c)
|
||||
if err := validate32BytesHex(target); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
author := c.String("author")
|
||||
if author != "" {
|
||||
if err := validate32BytesHex(author); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
relays := c.StringSlice("relay")
|
||||
if err := validateRelayURLs(relays); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if npub, err := nip19.EncodeEvent(target, relays, author); err == nil {
|
||||
fmt.Println(npub)
|
||||
return nil
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "naddr",
|
||||
Usage: "generate codes for NIP-33 parameterized replaceable events",
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "identifier",
|
||||
Aliases: []string{"d"},
|
||||
Usage: "the \"d\" tag identifier of this replaceable event",
|
||||
Required: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "pubkey",
|
||||
Usage: "pubkey of the naddr author",
|
||||
Aliases: []string{"p"},
|
||||
Required: true,
|
||||
},
|
||||
&cli.Int64Flag{
|
||||
Name: "kind",
|
||||
Aliases: []string{"k"},
|
||||
Usage: "kind of referred replaceable event",
|
||||
Required: true,
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "relay",
|
||||
Aliases: []string{"r"},
|
||||
Usage: "attach relay hints to naddr code",
|
||||
},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
pubkey := c.String("pubkey")
|
||||
if err := validate32BytesHex(pubkey); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
kind := c.Int("kind")
|
||||
if kind < 30000 || kind >= 40000 {
|
||||
return fmt.Errorf("kind must be between 30000 and 39999, as per NIP-16, got %d", kind)
|
||||
}
|
||||
|
||||
d := c.String("identifier")
|
||||
if d == "" {
|
||||
return fmt.Errorf("\"d\" tag identifier can't be empty")
|
||||
}
|
||||
|
||||
relays := c.StringSlice("relay")
|
||||
if err := validateRelayURLs(relays); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if npub, err := nip19.EncodeEntity(pubkey, kind, d, relays); err == nil {
|
||||
fmt.Println(npub)
|
||||
return nil
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "note",
|
||||
Usage: "generate note1 event codes (not recommended)",
|
||||
Action: func(c *cli.Context) error {
|
||||
target := getStdinOrFirstArgument(c)
|
||||
if err := validate32BytesHex(target); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if npub, err := nip19.EncodeNote(target); err == nil {
|
||||
fmt.Println(npub)
|
||||
return nil
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func validate32BytesHex(target string) error {
|
||||
if _, err := hex.DecodeString(target); err != nil {
|
||||
return fmt.Errorf("target '%s' is not valid hex: %s", target, err)
|
||||
}
|
||||
if len(target) != 64 {
|
||||
return fmt.Errorf("expected '%s' to be 64 characters (32 bytes), got %d", target, len(target))
|
||||
}
|
||||
if strings.ToLower(target) != target {
|
||||
return fmt.Errorf("expected target to be all lowercase hex. try again with '%s'", strings.ToLower(target))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,204 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mailru/easyjson"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/nbd-wtf/go-nostr/nson"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
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: `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",
|
||||
DefaultText: "the key '1'",
|
||||
Value: "0000000000000000000000000000000000000000000000000000000000000001",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "envelope",
|
||||
Usage: "print the event enveloped in a [\"EVENT\", ...] message ready to be sent to a relay",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "nson",
|
||||
Usage: "encode the event using NSON",
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: "kind",
|
||||
Aliases: []string{"k"},
|
||||
Usage: "event kind",
|
||||
DefaultText: "1",
|
||||
Value: 0,
|
||||
Category: CATEGORY_EVENT_FIELDS,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "content",
|
||||
Aliases: []string{"c"},
|
||||
Usage: "event content",
|
||||
DefaultText: "hello from the nostr army knife",
|
||||
Value: "",
|
||||
Category: CATEGORY_EVENT_FIELDS,
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "tag",
|
||||
Aliases: []string{"t"},
|
||||
Usage: "sets a tag field on the event, takes a value like -t e=<id>",
|
||||
Category: CATEGORY_EVENT_FIELDS,
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "e",
|
||||
Usage: "shortcut for --tag e=<value>",
|
||||
Category: CATEGORY_EVENT_FIELDS,
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "p",
|
||||
Usage: "shortcut for --tag p=<value>",
|
||||
Category: CATEGORY_EVENT_FIELDS,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "created-at",
|
||||
Aliases: []string{"time", "ts"},
|
||||
Usage: "unix timestamp value for the created_at field",
|
||||
DefaultText: "now",
|
||||
Value: "",
|
||||
Category: CATEGORY_EVENT_FIELDS,
|
||||
},
|
||||
},
|
||||
ArgsUsage: "[relay...]",
|
||||
Action: func(c *cli.Context) error {
|
||||
evt := nostr.Event{
|
||||
Tags: make(nostr.Tags, 0, 3),
|
||||
}
|
||||
|
||||
mustRehashAndResign := false
|
||||
|
||||
if stdinEvent := getStdin(); stdinEvent != "" {
|
||||
if err := json.Unmarshal([]byte(stdinEvent), &evt); err != nil {
|
||||
return fmt.Errorf("invalid event received from stdin: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if kind := c.Int("kind"); kind != 0 {
|
||||
evt.Kind = kind
|
||||
mustRehashAndResign = true
|
||||
} else if evt.Kind == 0 {
|
||||
evt.Kind = 1
|
||||
mustRehashAndResign = true
|
||||
}
|
||||
|
||||
if content := c.String("content"); content != "" {
|
||||
evt.Content = content
|
||||
mustRehashAndResign = true
|
||||
} else if evt.Content == "" && evt.Kind == 1 {
|
||||
evt.Content = "hello from the nostr army knife"
|
||||
mustRehashAndResign = true
|
||||
}
|
||||
|
||||
tags := make(nostr.Tags, 0, 5)
|
||||
for _, tagFlag := range c.StringSlice("tag") {
|
||||
// tags are in the format key=value
|
||||
spl := strings.Split(tagFlag, "=")
|
||||
if len(spl) == 2 && len(spl[0]) > 0 {
|
||||
tag := nostr.Tag{spl[0]}
|
||||
// tags may also contain extra elements separated with a ";"
|
||||
spl2 := strings.Split(spl[1], ";")
|
||||
tag = append(tag, spl2...)
|
||||
// ~
|
||||
tags = append(tags, tag)
|
||||
}
|
||||
}
|
||||
for _, etag := range c.StringSlice("e") {
|
||||
tags = append(tags, []string{"e", etag})
|
||||
mustRehashAndResign = true
|
||||
}
|
||||
for _, ptag := range c.StringSlice("p") {
|
||||
tags = append(tags, []string{"p", ptag})
|
||||
mustRehashAndResign = true
|
||||
}
|
||||
if len(tags) > 0 {
|
||||
for _, tag := range tags {
|
||||
evt.Tags = append(evt.Tags, tag)
|
||||
}
|
||||
mustRehashAndResign = true
|
||||
}
|
||||
|
||||
if createdAt := c.String("created-at"); createdAt != "" {
|
||||
ts := time.Now()
|
||||
if createdAt != "now" {
|
||||
if v, err := strconv.ParseInt(createdAt, 10, 64); err != nil {
|
||||
return fmt.Errorf("failed to parse timestamp '%s': %w", createdAt, err)
|
||||
} else {
|
||||
ts = time.Unix(v, 0)
|
||||
}
|
||||
}
|
||||
evt.CreatedAt = nostr.Timestamp(ts.Unix())
|
||||
mustRehashAndResign = true
|
||||
} else if evt.CreatedAt == 0 {
|
||||
evt.CreatedAt = nostr.Now()
|
||||
mustRehashAndResign = true
|
||||
}
|
||||
|
||||
if evt.Sig == "" || mustRehashAndResign {
|
||||
if err := evt.Sign(c.String("sec")); err != nil {
|
||||
return fmt.Errorf("error signing with provided key: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
relays := c.Args().Slice()
|
||||
if len(relays) > 0 {
|
||||
fmt.Println(evt.String())
|
||||
for _, url := range relays {
|
||||
fmt.Fprintf(os.Stderr, "publishing to %s... ", url)
|
||||
if relay, err := nostr.RelayConnect(c.Context, url); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to connect: %s\n", err)
|
||||
} else {
|
||||
ctx, cancel := context.WithTimeout(c.Context, 10*time.Second)
|
||||
defer cancel()
|
||||
if status, err := relay.Publish(ctx, evt); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed: %s\n", err)
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "%s.\n", status)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
var result string
|
||||
if c.Bool("envelope") {
|
||||
j, _ := json.Marshal([]any{"EVENT", evt})
|
||||
result = string(j)
|
||||
} else if c.Bool("nson") {
|
||||
result, _ = nson.Marshal(&evt)
|
||||
} else {
|
||||
j, _ := easyjson.Marshal(&evt)
|
||||
result = string(j)
|
||||
}
|
||||
fmt.Println(result)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
@@ -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 |
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 37 KiB |
@@ -1,90 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/nbd-wtf/go-nostr/nip19"
|
||||
"github.com/nbd-wtf/go-nostr/sdk"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
var fetch = &cli.Command{
|
||||
Name: "fetch",
|
||||
Usage: "fetches events related to the given nip19 code from the included relay hints",
|
||||
Description: `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 := getStdinOrFirstArgument(c)
|
||||
|
||||
prefix, value, err := nip19.Decode(code)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
relays := c.StringSlice("relay")
|
||||
if err := validateRelayURLs(relays); err != nil {
|
||||
return err
|
||||
}
|
||||
var authorHint string
|
||||
|
||||
switch prefix {
|
||||
case "nevent":
|
||||
v := value.(nostr.EventPointer)
|
||||
filter.IDs = append(filter.IDs, v.ID)
|
||||
if v.Author != "" {
|
||||
authorHint = v.Author
|
||||
}
|
||||
relays = v.Relays
|
||||
case "naddr":
|
||||
v := value.(nostr.EntityPointer)
|
||||
filter.Tags = nostr.TagMap{"d": []string{v.Identifier}}
|
||||
filter.Kinds = append(filter.Kinds, v.Kind)
|
||||
filter.Authors = append(filter.Authors, v.PublicKey)
|
||||
authorHint = v.PublicKey
|
||||
relays = v.Relays
|
||||
case "nprofile":
|
||||
v := value.(nostr.ProfilePointer)
|
||||
filter.Authors = append(filter.Authors, v.PublicKey)
|
||||
filter.Kinds = append(filter.Kinds, 0)
|
||||
authorHint = v.PublicKey
|
||||
relays = v.Relays
|
||||
case "npub":
|
||||
v := value.(string)
|
||||
filter.Authors = append(filter.Authors, v)
|
||||
filter.Kinds = append(filter.Kinds, 0)
|
||||
authorHint = v
|
||||
}
|
||||
|
||||
pool := nostr.NewSimplePool(c.Context)
|
||||
if authorHint != "" {
|
||||
relayList := sdk.FetchRelaysForPubkey(c.Context, pool, authorHint,
|
||||
"wss://purplepag.es", "wss://offchain.pub", "wss://public.relaying.io")
|
||||
for _, relayListItem := range relayList {
|
||||
if relayListItem.Outbox {
|
||||
relays = append(relays, relayListItem.URL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(relays) == 0 {
|
||||
return fmt.Errorf("no relay hints found")
|
||||
}
|
||||
|
||||
for ie := range pool.SubManyEose(c.Context, relays, nostr.Filters{filter}) {
|
||||
fmt.Println(ie.Event)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
module github.com/fiatjaf/nak
|
||||
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/mailru/easyjson v0.7.7
|
||||
github.com/nbd-wtf/go-nostr v0.24.2
|
||||
github.com/urfave/cli/v2 v2.25.3
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.2.0 // indirect
|
||||
github.com/btcsuite/btcd/btcutil v1.1.3 // indirect
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.1 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||
github.com/decred/dcrd/crypto/blake256 v1.0.0 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
|
||||
github.com/dgraph-io/ristretto v0.1.1 // indirect
|
||||
github.com/dustin/go-humanize v1.0.0 // indirect
|
||||
github.com/gobwas/httphead v0.1.0 // indirect
|
||||
github.com/gobwas/pool v0.2.1 // indirect
|
||||
github.com/gobwas/ws v1.2.0 // indirect
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/puzpuzpuz/xsync/v2 v2.5.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/tidwall/gjson v1.14.4 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
||||
golang.org/x/exp v0.0.0-20221106115401-f9659909a136 // indirect
|
||||
golang.org/x/sys v0.6.0 // indirect
|
||||
)
|
||||
@@ -1,152 +0,0 @@
|
||||
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
|
||||
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/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/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=
|
||||
github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY=
|
||||
github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I=
|
||||
github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
|
||||
github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
|
||||
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
|
||||
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
|
||||
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
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/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
|
||||
github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218=
|
||||
github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8=
|
||||
github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA=
|
||||
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA=
|
||||
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
|
||||
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/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
|
||||
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/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=
|
||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||
github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
|
||||
github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
|
||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/puzpuzpuz/xsync/v2 v2.5.0 h1:2k4qrO/orvmEXZ3hmtHqIy9XaQtPTwzMZk1+iErpE8c=
|
||||
github.com/puzpuzpuz/xsync/v2 v2.5.0/go.mod h1:gD2H2krq/w52MfPLE+Uy64TzJDVY7lP2znR9qmR35kU=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
|
||||
github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
|
||||
github.com/tidwall/gjson v1.14.4/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/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/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/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
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=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
@@ -0,0 +1,103 @@
|
||||
import React, {useState} from 'react'
|
||||
import useComputedState from 'use-computed-state'
|
||||
import {
|
||||
getPublicKey,
|
||||
getEventHash,
|
||||
serializeEvent,
|
||||
verifySignature,
|
||||
signEvent
|
||||
} from 'nostr-tools'
|
||||
|
||||
import Item from '../components/item'
|
||||
|
||||
export default function EventSigning({value}) {
|
||||
let evt = JSON.parse(value)
|
||||
let [privateKey, setPrivateKey] = useState(
|
||||
'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
|
||||
)
|
||||
let privateKeyIsValid = useComputedState(
|
||||
() => privateKey.match(/^[a-f0-9]{64}$/),
|
||||
[privateKey]
|
||||
)
|
||||
let publicKey = useComputedState(
|
||||
() => (privateKeyIsValid ? getPublicKey(privateKey) : null),
|
||||
[privateKeyIsValid]
|
||||
)
|
||||
let signature = useComputedState(async () => {
|
||||
if (evt.sig) return null
|
||||
|
||||
try {
|
||||
evt.pubkey = publicKey
|
||||
return await signEvent(evt, privateKey)
|
||||
} catch (err) {
|
||||
return null
|
||||
}
|
||||
}, [value, privateKey])
|
||||
let eventHash = useComputedState(() => getEventHash(evt), [value])
|
||||
let isValidHash = useComputedState(
|
||||
() => evt.id === eventHash,
|
||||
[value, eventHash]
|
||||
)
|
||||
let isValidSignature = useComputedState(async () => {
|
||||
if (evt.id && evt.sig) {
|
||||
try {
|
||||
return await verifySignature(evt)
|
||||
} catch (err) {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}, [value])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Item
|
||||
label="serialized event"
|
||||
hint="according to nip-01 signature algorithm"
|
||||
>
|
||||
{serializeEvent(evt)}
|
||||
</Item>
|
||||
<Item label="event id" hint="sha256 hash of serialized event">
|
||||
{eventHash}
|
||||
</Item>
|
||||
<Item
|
||||
label="event id matches hash"
|
||||
hint="the calculated hash from event data matches the 'id' in the body?"
|
||||
>
|
||||
{isValidHash?.toString()}
|
||||
</Item>
|
||||
{isValidHash &&
|
||||
(evt.sig ? (
|
||||
<Item label="signature valid">
|
||||
{isValidSignature?.toString() || ''}
|
||||
</Item>
|
||||
) : (
|
||||
<>
|
||||
<Item
|
||||
label="private key"
|
||||
hint="paste any private key here (32 bytes hex-encoded)"
|
||||
>
|
||||
<input
|
||||
value={privateKey}
|
||||
onChange={e => setPrivateKey(e.target.value.toLowerCase())}
|
||||
/>{' '}
|
||||
{privateKeyIsValid ? 'valid' : 'invalid'}
|
||||
</Item>
|
||||
<Item label="public key">{publicKey}</Item>
|
||||
<Item label="signature">{privateKeyIsValid ? signature : ''}</Item>
|
||||
</>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
EventSigning.match = value => {
|
||||
try {
|
||||
let evt = JSON.parse(value)
|
||||
return evt.kind && evt.content && evt.tags
|
||||
} catch (err) {
|
||||
/**/
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import React from 'react'
|
||||
import useBooleanState from 'use-boolean-state'
|
||||
|
||||
import {getPublicKey} from 'nostr-tools'
|
||||
|
||||
import Item from '../components/item'
|
||||
|
||||
export default function KeyHandling({value}) {
|
||||
let privateKey = value
|
||||
let publicKey = getPublicKey(privateKey)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Item label="private key">{privateKey}</Item>
|
||||
<Item label="public key">{publicKey}</Item>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
KeyHandling.match = value => {
|
||||
try {
|
||||
if (value.toLowerCase().match(/^[a-f0-9]{64}$/)) return true
|
||||
} catch (err) {
|
||||
/**/
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import React from 'react'
|
||||
|
||||
export default function Nothing() {
|
||||
return (
|
||||
<>
|
||||
you can paste
|
||||
<ul>
|
||||
<li>an unsigned event to be hashed and signed</li>
|
||||
<li>a signed event to have its signature checked</li>
|
||||
<li>a nostr relay URL to be inspected</li>
|
||||
<li>a nostr event id we'll try to fetch</li>
|
||||
<li>a nip05 identifier to be checked</li>
|
||||
<li>
|
||||
contribute a new function:{' '}
|
||||
<a
|
||||
target="_blank"
|
||||
style={{color: 'inherit'}}
|
||||
href="https://github.com/fiatjaf/nostr-army-knife"
|
||||
>
|
||||
_______
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Nothing.match = () => true
|
||||
-51
@@ -1,51 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func getStdin() string {
|
||||
stat, _ := os.Stdin.Stat()
|
||||
if (stat.Mode() & os.ModeCharDevice) == 0 {
|
||||
read := bytes.NewBuffer(make([]byte, 0, 1000))
|
||||
_, err := io.Copy(read, os.Stdin)
|
||||
if err == nil {
|
||||
return strings.TrimSpace(read.String())
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func getStdinOrFirstArgument(c *cli.Context) string {
|
||||
target := c.Args().First()
|
||||
if target != "" {
|
||||
return target
|
||||
}
|
||||
return getStdin()
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -1,28 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"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,
|
||||
},
|
||||
}
|
||||
|
||||
if err := app.Run(os.Args); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@esbuild-plugins/node-globals-polyfill": "^0.1.1",
|
||||
"esbuild": "^0.14.21",
|
||||
"esbuild-plugin-alias": "^0.2.1",
|
||||
"events": "^3.3.0",
|
||||
"nostr-tools": "^0.22.1",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"use-boolean-state": "^1.0.2",
|
||||
"use-computed-state": "1.1.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "./build.js prod",
|
||||
"watch": "ag -l --js | entr ./build.js"
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
sbt.version=1.7.1
|
||||
@@ -1,2 +0,0 @@
|
||||
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.13.0")
|
||||
addSbtPlugin("com.fiatjaf" % "sbt-esbuild" % "0.1.1")
|
||||
@@ -0,0 +1,8 @@
|
||||
<meta charset="utf-8" />
|
||||
<title>nostr army knife</title>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/wenk/1.0.8/wenk.min.css"
|
||||
/>
|
||||
<div id="app" />
|
||||
<script src="app.js"></script>
|
||||
@@ -1,181 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
const CATEGORY_FILTER_ATTRIBUTES = "FILTER ATTRIBUTES"
|
||||
|
||||
var req = &cli.Command{
|
||||
Name: "req",
|
||||
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:
|
||||
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",
|
||||
Aliases: []string{"a"},
|
||||
Usage: "only accept events from these authors (pubkey as hex)",
|
||||
Category: CATEGORY_FILTER_ATTRIBUTES,
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "id",
|
||||
Aliases: []string{"i"},
|
||||
Usage: "only accept events with these ids (hex)",
|
||||
Category: CATEGORY_FILTER_ATTRIBUTES,
|
||||
},
|
||||
&cli.IntSliceFlag{
|
||||
Name: "kind",
|
||||
Aliases: []string{"k"},
|
||||
Usage: "only accept events with these kind numbers",
|
||||
Category: CATEGORY_FILTER_ATTRIBUTES,
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "tag",
|
||||
Aliases: []string{"t"},
|
||||
Usage: "takes a tag like -t e=<id>, only accept events with these tags",
|
||||
Category: CATEGORY_FILTER_ATTRIBUTES,
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "e",
|
||||
Usage: "shortcut for --tag e=<value>",
|
||||
Category: CATEGORY_FILTER_ATTRIBUTES,
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "p",
|
||||
Usage: "shortcut for --tag p=<value>",
|
||||
Category: CATEGORY_FILTER_ATTRIBUTES,
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: "since",
|
||||
Aliases: []string{"s"},
|
||||
Usage: "only accept events newer than this (unix timestamp)",
|
||||
Category: CATEGORY_FILTER_ATTRIBUTES,
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: "until",
|
||||
Aliases: []string{"u"},
|
||||
Usage: "only accept events older than this (unix timestamp)",
|
||||
Category: CATEGORY_FILTER_ATTRIBUTES,
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: "limit",
|
||||
Aliases: []string{"l"},
|
||||
Usage: "only accept up to this number of events",
|
||||
Category: CATEGORY_FILTER_ATTRIBUTES,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "search",
|
||||
Usage: "a NIP-50 search query, use it only with relays that explicitly support it",
|
||||
Category: CATEGORY_FILTER_ATTRIBUTES,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "bare",
|
||||
Usage: "when printing the filter, print just the filter, not enveloped in a [\"REQ\", ...] array",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "stream",
|
||||
Usage: "keep the subscription open, printing all events as they are returned",
|
||||
DefaultText: "false, will close on EOSE",
|
||||
},
|
||||
},
|
||||
ArgsUsage: "[relay...]",
|
||||
Action: func(c *cli.Context) error {
|
||||
filter := nostr.Filter{}
|
||||
if stdinFilter := getStdin(); stdinFilter != "" {
|
||||
if err := json.Unmarshal([]byte(stdinFilter), &filter); err != nil {
|
||||
return fmt.Errorf("invalid filter received from stdin: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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()
|
||||
} else {
|
||||
j, _ := json.Marshal([]any{"REQ", "nak", filter})
|
||||
result = string(j)
|
||||
}
|
||||
|
||||
fmt.Println(result)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user