mirror of
https://github.com/fiatjaf/nak.git
synced 2025-12-08 16:48:51 +00:00
Compare commits
155 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e4a546212 | ||
|
|
55c9d4ee45 | ||
|
|
550c89d8d7 | ||
|
|
1e9be3ed84 | ||
|
|
79cbc57dde | ||
|
|
1e237b4c42 | ||
|
|
89ec8b9822 | ||
|
|
fba83ea39e | ||
|
|
bd5569955c | ||
|
|
35ea2582d8 | ||
|
|
fa63dbfea3 | ||
|
|
a6509909d0 | ||
|
|
239dd2d42a | ||
|
|
0073c9bdf1 | ||
|
|
b5bd2aecf6 | ||
|
|
f27ac6c0e3 | ||
|
|
6e5441aa18 | ||
|
|
61a68f3dca | ||
|
|
f450e735b6 | ||
|
|
1304a65179 | ||
|
|
aa89093d57 | ||
|
|
4387595437 | ||
|
|
4eb5e929d4 | ||
|
|
150625ee74 | ||
|
|
fc255b5a9a | ||
|
|
5bcf2da794 | ||
|
|
aadcc73906 | ||
|
|
f799c65779 | ||
|
|
c3822225b4 | ||
|
|
67e291e80d | ||
|
|
83195d9a00 | ||
|
|
f9033f778d | ||
|
|
9055f98f66 | ||
|
|
02f22a8c2f | ||
|
|
f98bd7483f | ||
|
|
3005c62566 | ||
|
|
e91a454fc0 | ||
|
|
148f6e8bcb | ||
|
|
024111a8be | ||
|
|
8fba611ad0 | ||
|
|
4d12550d74 | ||
|
|
5d44600f17 | ||
|
|
5a8c7df811 | ||
|
|
01be954ae6 | ||
|
|
d733a31898 | ||
|
|
1b43dbda02 | ||
|
|
e45b54ea62 | ||
|
|
35da063c30 | ||
|
|
15aefe3df4 | ||
|
|
55fd631787 | ||
|
|
6f48c29d0f | ||
|
|
703c186958 | ||
|
|
7ae2e686cb | ||
|
|
9547711e8d | ||
|
|
50119e21e6 | ||
|
|
33f4272dd0 | ||
|
|
7b6f387aad | ||
|
|
b1a03800e6 | ||
|
|
db5dafb58a | ||
|
|
4b15cdf625 | ||
|
|
4b8c067e00 | ||
|
|
931da4b0ae | ||
|
|
c87371208e | ||
|
|
bfe1e6ca94 | ||
|
|
602e03a9a1 | ||
|
|
fe1f50f798 | ||
|
|
d899a92f15 | ||
|
|
1c058f2846 | ||
|
|
4b4d9ec155 | ||
|
|
3031568266 | ||
|
|
a828ee3793 | ||
|
|
186948db9a | ||
|
|
5fe354f642 | ||
|
|
3d961d4bec | ||
|
|
d6a23bd00c | ||
|
|
c1248eb37b | ||
|
|
c60bb82be8 | ||
|
|
f5316a0f35 | ||
|
|
e6448debf2 | ||
|
|
7bb7543ef7 | ||
|
|
43a3e5f40d | ||
|
|
707e5b3918 | ||
|
|
faca2e50f0 | ||
|
|
26930d40bc | ||
|
|
17920d8aef | ||
|
|
95bed5d5a8 | ||
|
|
2e30dfe2eb | ||
|
|
55c6f75b8a | ||
|
|
1f2492c9b1 | ||
|
|
d00976a669 | ||
|
|
4392293ed6 | ||
|
|
60d1292f80 | ||
|
|
6c634d8081 | ||
|
|
1e353680bc | ||
|
|
ff8701a3b0 | ||
|
|
ad6b8c4ba5 | ||
|
|
dba3f648ad | ||
|
|
12a1f1563e | ||
|
|
e2dd3ca544 | ||
|
|
df5ebd3f56 | ||
|
|
81571c6952 | ||
|
|
6e43a6b733 | ||
|
|
943e8835f9 | ||
|
|
6b659c1552 | ||
|
|
aa53f2cd60 | ||
|
|
5509095277 | ||
|
|
a3ef9b45de | ||
|
|
df20a3241a | ||
|
|
53a2451303 | ||
|
|
2d992f235e | ||
|
|
7675929056 | ||
|
|
7f608588a2 | ||
|
|
fd5cd55f6f | ||
|
|
932361fe8f | ||
|
|
11ae7bc4d3 | ||
|
|
7033bfee19 | ||
|
|
f425097c5a | ||
|
|
dd0ef2ca64 | ||
|
|
491a094e07 | ||
|
|
9d619ddf00 | ||
|
|
5d32739573 | ||
|
|
a187e448f2 | ||
|
|
9a9e96a829 | ||
|
|
4c6181d649 | ||
|
|
71b106fd45 | ||
|
|
40892c1228 | ||
|
|
847f8aaa69 | ||
|
|
134d1225d6 | ||
|
|
464766a836 | ||
|
|
ea53eca74f | ||
|
|
38ed370c59 | ||
|
|
5b04bc4859 | ||
|
|
2988c71ccb | ||
|
|
d7c0ff2bb7 | ||
|
|
43fe41df5d | ||
|
|
3215726417 | ||
|
|
a4886dc445 | ||
|
|
dae7eba8ca | ||
|
|
2b5f3355bc | ||
|
|
bd5ca27661 | ||
|
|
9d02301b2d | ||
|
|
9bbc87b27a | ||
|
|
88a07a3504 | ||
|
|
8a934cc76b | ||
|
|
e0c967efa9 | ||
|
|
36c32ae308 | ||
|
|
6d23509d8c | ||
|
|
29b6ecbafe | ||
|
|
11f37afa5b | ||
|
|
cf1694704e | ||
|
|
b3ef2c1289 | ||
|
|
cfdea699bc | ||
|
|
014c6bc11d | ||
|
|
0240866fa1 | ||
|
|
a4d9ceecfa |
3
.github/workflows/release-cli.yml
vendored
3
.github/workflows/release-cli.yml
vendored
@@ -33,6 +33,8 @@ jobs:
|
|||||||
goos: windows
|
goos: windows
|
||||||
- goarch: riscv64
|
- goarch: riscv64
|
||||||
goos: darwin
|
goos: darwin
|
||||||
|
- goarch: arm64
|
||||||
|
goos: freebsd
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: wangyoucao577/go-release-action@v1.40
|
- uses: wangyoucao577/go-release-action@v1.40
|
||||||
@@ -40,6 +42,7 @@ jobs:
|
|||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
goos: ${{ matrix.goos }}
|
goos: ${{ matrix.goos }}
|
||||||
goarch: ${{ matrix.goarch }}
|
goarch: ${{ matrix.goarch }}
|
||||||
|
ldflags: -X main.version=${{ github.ref_name }}
|
||||||
overwrite: true
|
overwrite: true
|
||||||
md5sum: false
|
md5sum: false
|
||||||
sha256sum: false
|
sha256sum: false
|
||||||
|
|||||||
97
.github/workflows/smoke-test-release.yml
vendored
Normal file
97
.github/workflows/smoke-test-release.yml
vendored
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
name: Smoke test the binary
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_run:
|
||||||
|
workflows: ["build cli for all platforms"]
|
||||||
|
types:
|
||||||
|
- completed
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
smoke-test-linux-amd64:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||||
|
steps:
|
||||||
|
- name: Download and smoke test latest binary
|
||||||
|
run: |
|
||||||
|
set -eo pipefail # Exit on error, and on pipe failures
|
||||||
|
|
||||||
|
echo "Downloading nak binary from releases"
|
||||||
|
RELEASE_URL="https://api.github.com/repos/fiatjaf/nak/releases/latest"
|
||||||
|
wget $(wget -q -O - ${RELEASE_URL} | jq -r '.assets[] | select(.name | contains("linux-amd64")) | .browser_download_url') -O nak -nv
|
||||||
|
chmod +x nak
|
||||||
|
|
||||||
|
echo "Running basic tests..."
|
||||||
|
./nak --version
|
||||||
|
|
||||||
|
# Generate and manipulate keys
|
||||||
|
echo "Testing key operations..."
|
||||||
|
SECRET_KEY=$(./nak key generate)
|
||||||
|
PUBLIC_KEY=$(echo $SECRET_KEY | ./nak key public)
|
||||||
|
echo "Generated key pair: $PUBLIC_KEY"
|
||||||
|
|
||||||
|
# Create events
|
||||||
|
echo "Testing event creation..."
|
||||||
|
./nak event -c "hello world"
|
||||||
|
./nak event --ts "2 days ago" -c "event with timestamp"
|
||||||
|
./nak event -k 1 -t "t=test" -c "event with tag"
|
||||||
|
|
||||||
|
# Test NIP-19 encoding/decoding
|
||||||
|
echo "Testing NIP-19 encoding/decoding..."
|
||||||
|
NSEC=$(echo $SECRET_KEY | ./nak encode nsec)
|
||||||
|
echo "Encoded nsec: $NSEC"
|
||||||
|
./nak encode npub 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798
|
||||||
|
NOTE_ID="5ae731bbc7711f78513da14927c48cc7143a91e6cad0565fdc4d73b8967a7d59"
|
||||||
|
NOTE1=$(./nak encode note $NOTE_ID)
|
||||||
|
echo "Encoded note1: $NOTE1"
|
||||||
|
./nak decode $NOTE1
|
||||||
|
./nak decode npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6
|
||||||
|
|
||||||
|
# Test event verification
|
||||||
|
echo "Testing event verification..."
|
||||||
|
# Create an event and verify it
|
||||||
|
VERIFY_EVENT=$(./nak event -c "verify me")
|
||||||
|
echo $VERIFY_EVENT | ./nak verify
|
||||||
|
|
||||||
|
# Test PoW
|
||||||
|
echo "Testing PoW..."
|
||||||
|
./nak event -c "testing pow" --pow 8
|
||||||
|
|
||||||
|
# Test NIP-49 key encryption/decryption
|
||||||
|
echo "Testing NIP-49 key encryption/decryption..."
|
||||||
|
ENCRYPTED_KEY=$(./nak key encrypt $SECRET_KEY "testpassword")
|
||||||
|
echo "Encrypted key: ${ENCRYPTED_KEY:0:20}..."
|
||||||
|
DECRYPTED_KEY=$(./nak key decrypt $ENCRYPTED_KEY "testpassword")
|
||||||
|
if [ "$DECRYPTED_KEY" != "$SECRET_KEY" ]; then
|
||||||
|
echo "NIP-49 encryption/decryption test failed!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test multi-value tags
|
||||||
|
echo "Testing multi-value tags..."
|
||||||
|
./nak event --ts "yesterday" -t "e=f59911b561c37c90b01e9e5c2557307380835c83399756f4d62d8167227e420a;wss://relay.example.com;root" -c "Testing multi-value tags"
|
||||||
|
|
||||||
|
# Test relay operations (with a public relay)
|
||||||
|
echo "Testing relay operations..."
|
||||||
|
# Publish a simple event to a public relay
|
||||||
|
EVENT_JSON=$(./nak event --sec $SECRET_KEY -c "Test from nak smoke test" nos.lol)
|
||||||
|
EVENT_ID=$(echo $EVENT_JSON | jq -r .id)
|
||||||
|
echo "Published event ID: $EVENT_ID"
|
||||||
|
|
||||||
|
# Wait a moment for propagation
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# Fetch the event we just published
|
||||||
|
./nak req -i $EVENT_ID nos.lol
|
||||||
|
|
||||||
|
# Test serving (just start and immediately kill)
|
||||||
|
echo "Testing serve command..."
|
||||||
|
timeout 2s ./nak serve || true
|
||||||
|
|
||||||
|
# Test filesystem mount (just start and immediately kill)
|
||||||
|
echo "Testing fs mount command..."
|
||||||
|
mkdir -p /tmp/nostr-mount
|
||||||
|
timeout 2s ./nak fs --sec $SECRET_KEY /tmp/nostr-mount || true
|
||||||
|
|
||||||
|
echo "All tests passed"
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1 +1,3 @@
|
|||||||
nak
|
nak
|
||||||
|
mnt
|
||||||
|
nak.exe
|
||||||
|
|||||||
89
README.md
89
README.md
@@ -102,7 +102,7 @@ demo videos with [2](https://njump.me/nevent1qqs8pmmae89agph80928l6gjm0wymechqaz
|
|||||||
|
|
||||||
### generate a private key
|
### generate a private key
|
||||||
```shell
|
```shell
|
||||||
~> nak key generate 18:59
|
~> nak key generate
|
||||||
7b94e287b1fafa694ded1619b27de7effd3646104a158e187ff4edc56bc6148d
|
7b94e287b1fafa694ded1619b27de7effd3646104a158e187ff4edc56bc6148d
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -128,11 +128,18 @@ type the password to decrypt your secret key: **********
|
|||||||
985d66d2644dfa7676e26046914470d66ebc7fa783a3f57f139fde32d0d631d7
|
985d66d2644dfa7676e26046914470d66ebc7fa783a3f57f139fde32d0d631d7
|
||||||
```
|
```
|
||||||
|
|
||||||
### sign an event using a remote NIP-46 bunker
|
### sign an event using [Amber](https://github.com/greenart7c3/Amber) (or other bunker provider)
|
||||||
```shell
|
```shell
|
||||||
~> nak event --connect 'bunker://a9e0f110f636f3191644110c19a33448daf09d7cda9708a769e91b7e91340208?relay=wss%3A%2F%2Frelay.damus.io&relay=wss%3A%2F%2Frelay.nsecbunker.com&relay=wss%3A%2F%2Fnos.lol&secret=TWfGbjQCLxUf' -c 'hello from bunker'
|
~> export NOSTR_CLIENT_KEY="$(nak key generate)"
|
||||||
|
~> nak event --sec 'bunker://a9e0f110f636f3191644110c19a33448daf09d7cda9708a769e91b7e91340208?relay=wss%3A%2F%2Frelay.damus.io&relay=wss%3A%2F%2Frelay.nsecbunker.com&relay=wss%3A%2F%2Fnos.lol&secret=TWfGbjQCLxUf' -c 'hello from bunker'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> Remember to set a `NOSTR_CLIENT_KEY` permanently on your shell, otherwise you'll only be able to use the bunker once. For `bash`:
|
||||||
|
> ```shell
|
||||||
|
> echo 'export NOSTR_CLIENT_KEY="$(nak key generate)"' >> ~/.bashrc
|
||||||
|
> ```
|
||||||
|
|
||||||
### sign an event using a NIP-49 encrypted key
|
### sign an event using a NIP-49 encrypted key
|
||||||
```shell
|
```shell
|
||||||
~> nak event --sec ncryptsec1qggx54cg270zy9y8krwmfz29jyypsuxken2fkk99gr52qhje968n6mwkrfstqaqhq9eq94pnzl4nff437l4lp4ur2cs4f9um8738s35l2esx2tas48thtfhrk5kq94pf9j2tpk54yuermra0xu6hl5ls -c 'hello from encrypted key'
|
~> nak event --sec ncryptsec1qggx54cg270zy9y8krwmfz29jyypsuxken2fkk99gr52qhje968n6mwkrfstqaqhq9eq94pnzl4nff437l4lp4ur2cs4f9um8738s35l2esx2tas48thtfhrk5kq94pf9j2tpk54yuermra0xu6hl5ls -c 'hello from encrypted key'
|
||||||
@@ -200,6 +207,82 @@ listening at [wss://relay.damus.io wss://nos.lol wss://relay.nsecbunker.com]:
|
|||||||
{"kind":1,"id":"0000009dcc7c62056eafdb41fac817379ec2becf0ce27c5fbe98d0735d968147","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1724160828,"tags":[["nonce","515504","24"]],"content":"hello getwired.app and labour.fiatjaf.com","sig":"7edb988065ccc12779fe99270945b212f3723838f315d76d5e90e9ffa27198f13fa556614295f518d968d55bab81878167d4162b3a7cf81a6b423c6761bd504c"}
|
{"kind":1,"id":"0000009dcc7c62056eafdb41fac817379ec2becf0ce27c5fbe98d0735d968147","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1724160828,"tags":[["nonce","515504","24"]],"content":"hello getwired.app and labour.fiatjaf.com","sig":"7edb988065ccc12779fe99270945b212f3723838f315d76d5e90e9ffa27198f13fa556614295f518d968d55bab81878167d4162b3a7cf81a6b423c6761bd504c"}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### make a nostr event signed with a key given as an environment variable
|
||||||
|
|
||||||
|
```shell
|
||||||
|
~> export NOSTR_SECRET_KEY=ncryptsec1qggyy9vw0nclmw8ly9caz6aa7f85a4ufhsct64uva337pulsdw00n6twa2lzhzk2znzsyu60urx9s08lx00ke6ual3lszyn5an9zarm6s70lw5lj6dv3mj3f9p4tvp0we6qyz4gp420mapfmvqheuttv
|
||||||
|
~> nak event -c 'it supports keys as hex, nsec or ncryptsec'
|
||||||
|
type the password to decrypt your secret key: ********
|
||||||
|
{"kind":1,"id":"5cbf3feb9a7d99c3ee2a88693a591caca1a8348fea427b3652c27f7a8a76af48","pubkey":"b00bcab55375d8c7b731dd9841f6d805ff1cf6fdc945e7326786deb5ddac6ce4","created_at":1724247924,"tags":[],"content":"it supports keys as hex, nsec or ncryptsec","sig":"fb3fd170bc10e5042322c7a05dd4bbd8ac9947b39026b8a7afd1ee02524e8e3aa1d9554e9c7b6181ca1b45cab01cd06643bdffa5ce678b475e6b185e1c14b085"}
|
||||||
|
```
|
||||||
|
|
||||||
|
### download some helpful `jq` functions for dealing with nostr events
|
||||||
|
```shell
|
||||||
|
~> nak req -i 412f2d3e73acc312942c055ac2a695dc60bf58ff97e06689a8a79e97796c4cdb relay.westernbtc.com | jq -r .content > ~/.jq
|
||||||
|
```
|
||||||
|
|
||||||
|
### watch a NIP-53 livestream (zap.stream etc)
|
||||||
|
```shell
|
||||||
|
~> # this requires the jq utils from the step above
|
||||||
|
~> mpv $(nak fetch naddr1qqjxvvm9xscnsdtx95cxvcfk956rsvtx943rje3k95mx2dp389jnwwrp8ymxgqg4waehxw309aex2mrp0yhxgctdw4eju6t09upzpn6956apxcad0mfp8grcuugdysg44eepex68h50t73zcathmfs49qvzqqqrkvu7ed38k | jq -r 'tag_value("streaming")')
|
||||||
|
~>
|
||||||
|
~> # or without the utils
|
||||||
|
~> mpv $(nak fetch naddr1qqjxvvm9xscnsdtx95cxvcfk956rsvtx943rje3k95mx2dp389jnwwrp8ymxgqg4waehxw309aex2mrp0yhxgctdw4eju6t09upzpn6956apxcad0mfp8grcuugdysg44eepex68h50t73zcathmfs49qvzqqqrkvu7ed38k | jq -r '.tags | map(select(.[0] == "streaming") | .[1])[0]')
|
||||||
|
```
|
||||||
|
|
||||||
|
### download a NIP-35 torrent from an `nevent`
|
||||||
|
```shell
|
||||||
|
~> # this requires the jq utils from two steps above
|
||||||
|
~> aria2c $(nak fetch nevent1qqsdsg6x7uujekac4ga7k7qa9q9sx8gqj7xzjf5w9us0dm0ghvf4ugspp4mhxue69uhkummn9ekx7mq6dw9y4 | jq -r '"magnet:?xt=urn:btih:\(tag_value("x"))&dn=\(tag_value("title"))&tr=http%3A%2F%2Ftracker.loadpeers.org%3A8080%2FxvRKfvAlnfuf5EfxTT5T0KIVPtbqAHnX%2Fannounce&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A6969%2Fannounce&tr=udp%3A%2F%2Fopen.stealth.si%3A80%2Fannounce&tr=udp%3A%2F%2Ftracker.torrent.eu.org%3A451%2Fannounce&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337&tr=\(tags("tracker") | map(.[1] | @uri) | join("&tr="))"')
|
||||||
|
```
|
||||||
|
|
||||||
|
### mount Nostr as a FUSE filesystem and publish a note
|
||||||
|
```shell
|
||||||
|
~> nak fs --sec 01 ~/nostr
|
||||||
|
- mounting at /home/user/nostr... ok.
|
||||||
|
~> cd ~/nostr/npub1xxxxxx/notes/
|
||||||
|
~> echo "satellites are bad!" > new
|
||||||
|
pending note updated, timer reset.
|
||||||
|
- `touch publish` to publish immediately
|
||||||
|
- `rm new` to erase and cancel the publication.
|
||||||
|
~> touch publish
|
||||||
|
publishing now!
|
||||||
|
{"id":"f1cbfa6...","pubkey":"...","content":"satellites are bad!","sig":"..."}
|
||||||
|
publishing to 3 relays... offchain.pub: ok, nostr.wine: ok, pyramid.fiatjaf.com: ok
|
||||||
|
event published as f1cbfa6... and updated locally.
|
||||||
|
```
|
||||||
|
|
||||||
|
### list NIP-60 wallet tokens and send some
|
||||||
|
```shell
|
||||||
|
~> nak wallet tokens
|
||||||
|
91a10b6fc8bbe7ef2ad9ad0142871d80468b697716d9d2820902db304ff1165e 500 cashu.space
|
||||||
|
cac7f89f0611021984d92a7daca219e4cd1c9798950e50e952bba7cde1ac1337 1000 legend.lnbits.com
|
||||||
|
~> nak wallet send 100
|
||||||
|
cashuA1psxqyry8...
|
||||||
|
~> nak wallet pay lnbc1...
|
||||||
|
```
|
||||||
|
|
||||||
|
### upload and download files with blossom
|
||||||
|
```shell
|
||||||
|
~> nak blossom --server blossom.azzamo.net --sec 01 upload image.png
|
||||||
|
{"sha256":"38c51756f3e9fedf039488a1f6e513286f6743194e7a7f25effdc84a0ee4c2cf","url":"https://blossom.azzamo.net/38c51756f3e9fedf039488a1f6e513286f6743194e7a7f25effdc84a0ee4c2cf.png"}
|
||||||
|
~> nak blossom --server aegis.utxo.one download acc8ea43d4e6b706f68b249144364f446854b7f63ba1927371831c05dcf0256c -o downloaded.png
|
||||||
|
```
|
||||||
|
|
||||||
|
### publish a fully formed event with correct tags, URIs and to the correct read and write relays
|
||||||
|
```shell
|
||||||
|
echo "#surely you're joking, mr npub1l2vyh47mk2p0qlsku7hg0vn29faehy9hy34ygaclpn66ukqp3afqutajft olas.app is broken again" | nak publish
|
||||||
|
|
||||||
|
# it will add the hashtag, turn the npub1 code into a nostr:npub1 URI, turn the olas.app string into https://olas.app, add the "p" tag (and "q" tags too if you were mentioning an nevent1 code or naddr1 code) and finally publish it to your "write" relays and to any mentioned person (or author of mentioned events)'s "read" relays.
|
||||||
|
# there is also a --reply flag that you can pass an nevent, naddr or hex id to and it will do the right thing (including setting the correct kind to either 1 or 1111).
|
||||||
|
# and there is a --confirm flag that gives you a chance to confirm before actually publishing the result to relays.
|
||||||
|
```
|
||||||
|
|
||||||
|
### record and publish an audio note of 10s (yakbak etc) signed from a bunker
|
||||||
|
```shell
|
||||||
|
ffmpeg -f alsa -i default -f webm -t 00:00:03 pipe:1 | nak blossom --server blossom.primal.net upload | jq -rc '{content: .url}' | nak event -k 1222 --sec 'bunker://urlgoeshere' pyramid.fiatjaf.com nostr.wine
|
||||||
|
```
|
||||||
|
|
||||||
## contributing to this repository
|
## contributing to this repository
|
||||||
|
|
||||||
Use NIP-34 to send your patches to `naddr1qqpkucttqy28wumn8ghj7un9d3shjtnwdaehgu3wvfnsz9nhwden5te0wfjkccte9ehx7um5wghxyctwvsq3gamnwvaz7tmjv4kxz7fwv3sk6atn9e5k7q3q80cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsxpqqqpmej2wctpn`.
|
Use NIP-34 to send your patches to `naddr1qqpkucttqy28wumn8ghj7un9d3shjtnwdaehgu3wvfnsz9nhwden5te0wfjkccte9ehx7um5wghxyctwvsq3gamnwvaz7tmjv4kxz7fwv3sk6atn9e5k7q3q80cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsxpqqqpmej2wctpn`.
|
||||||
|
|||||||
251
blossom.go
Normal file
251
blossom.go
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"fiatjaf.com/nostr"
|
||||||
|
"fiatjaf.com/nostr/keyer"
|
||||||
|
"fiatjaf.com/nostr/nipb0/blossom"
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
var blossomCmd = &cli.Command{
|
||||||
|
Name: "blossom",
|
||||||
|
Suggest: true,
|
||||||
|
UseShortOptionHandling: true,
|
||||||
|
Usage: "an army knife for blossom things",
|
||||||
|
DisableSliceFlagSeparator: true,
|
||||||
|
Flags: append(defaultKeyFlags,
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "server",
|
||||||
|
Aliases: []string{"s"},
|
||||||
|
Usage: "the hostname of the target mediaserver",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Commands: []*cli.Command{
|
||||||
|
{
|
||||||
|
Name: "list",
|
||||||
|
Usage: "lists blobs from a pubkey",
|
||||||
|
Description: `takes one pubkey passed as an argument or derives one from the --sec supplied. if that is given then it will also pre-authorize the list, which some servers may require.`,
|
||||||
|
DisableSliceFlagSeparator: true,
|
||||||
|
ArgsUsage: "[pubkey]",
|
||||||
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
|
var client *blossom.Client
|
||||||
|
pubkey := c.Args().First()
|
||||||
|
if pubkey != "" {
|
||||||
|
if pk, err := nostr.PubKeyFromHex(pubkey); err != nil {
|
||||||
|
return fmt.Errorf("invalid public key '%s': %w", pubkey, err)
|
||||||
|
} else {
|
||||||
|
client = blossom.NewClient(client.GetMediaServer(), keyer.NewReadOnlySigner(pk))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var err error
|
||||||
|
client, err = getBlossomClient(ctx, c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bds, err := client.List(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, bd := range bds {
|
||||||
|
stdout(bd)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "upload",
|
||||||
|
Usage: "uploads a file to a specific mediaserver.",
|
||||||
|
Description: `takes any number of local file paths and uploads them to a mediaserver, printing the resulting blob descriptions when successful.`,
|
||||||
|
DisableSliceFlagSeparator: true,
|
||||||
|
ArgsUsage: "[files...]",
|
||||||
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
|
client, err := getBlossomClient(ctx, c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if isPiped() {
|
||||||
|
// get file from stdin
|
||||||
|
if c.Args().Len() > 0 {
|
||||||
|
return fmt.Errorf("do not pass arguments when piping from stdin")
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := io.ReadAll(os.Stdin)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read stdin: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
bd, err := client.UploadBlob(ctx, bytes.NewReader(data), "")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
j, _ := json.Marshal(bd)
|
||||||
|
stdout(string(j))
|
||||||
|
} else {
|
||||||
|
// get filenames from arguments
|
||||||
|
hasError := false
|
||||||
|
for _, fpath := range c.Args().Slice() {
|
||||||
|
bd, err := client.UploadFilePath(ctx, fpath)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "%s\n", err)
|
||||||
|
hasError = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
j, _ := json.Marshal(bd)
|
||||||
|
stdout(string(j))
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasError {
|
||||||
|
os.Exit(3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "download",
|
||||||
|
Usage: "downloads files from mediaservers",
|
||||||
|
Description: `takes any number of sha256 hashes as hex, downloads them and prints them to stdout (unless --output is specified).`,
|
||||||
|
DisableSliceFlagSeparator: true,
|
||||||
|
ArgsUsage: "[sha256...]",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.StringSliceFlag{
|
||||||
|
Name: "output",
|
||||||
|
Aliases: []string{"o"},
|
||||||
|
Usage: "file name to save downloaded file to, can be passed multiple times when downloading multiple hashes",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
|
client, err := getBlossomClient(ctx, c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
outputs := c.StringSlice("output")
|
||||||
|
|
||||||
|
hasError := false
|
||||||
|
for i, hash := range c.Args().Slice() {
|
||||||
|
if len(outputs)-1 >= i && outputs[i] != "--" {
|
||||||
|
// save to this file
|
||||||
|
err := client.DownloadToFile(ctx, hash, outputs[i])
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "%s\n", err)
|
||||||
|
hasError = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// if output wasn't specified, print to stdout
|
||||||
|
data, err := client.Download(ctx, hash)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "%s\n", err)
|
||||||
|
hasError = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
stdout(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasError {
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "del",
|
||||||
|
Aliases: []string{"delete"},
|
||||||
|
Usage: "deletes a file from a mediaserver",
|
||||||
|
Description: `takes any number of sha256 hashes, signs authorizations and deletes them from the current mediaserver.
|
||||||
|
|
||||||
|
if any of the files are not deleted command will fail, otherwise it will succeed. it will also print error messages to stderr and the hashes it successfully deletes to stdout.`,
|
||||||
|
DisableSliceFlagSeparator: true,
|
||||||
|
ArgsUsage: "[sha256...]",
|
||||||
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
|
client, err := getBlossomClient(ctx, c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
hasError := false
|
||||||
|
for _, hash := range c.Args().Slice() {
|
||||||
|
err := client.Delete(ctx, hash)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "%s\n", err)
|
||||||
|
hasError = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
stdout(hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasError {
|
||||||
|
os.Exit(3)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "check",
|
||||||
|
Usage: "asks the mediaserver if it has the specified hashes.",
|
||||||
|
Description: `uses the HEAD request to succintly check if the server has the specified sha256 hash.
|
||||||
|
|
||||||
|
if any of the files are not found the command will fail, otherwise it will succeed. it will also print error messages to stderr and the hashes it finds to stdout.`,
|
||||||
|
DisableSliceFlagSeparator: true,
|
||||||
|
ArgsUsage: "[sha256...]",
|
||||||
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
|
client, err := getBlossomClient(ctx, c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
hasError := false
|
||||||
|
for _, hash := range c.Args().Slice() {
|
||||||
|
err := client.Check(ctx, hash)
|
||||||
|
if err != nil {
|
||||||
|
hasError = true
|
||||||
|
fmt.Fprintf(os.Stderr, "%s\n", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
stdout(hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasError {
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "mirror",
|
||||||
|
Usage: "",
|
||||||
|
Description: ``,
|
||||||
|
DisableSliceFlagSeparator: true,
|
||||||
|
ArgsUsage: "",
|
||||||
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func getBlossomClient(ctx context.Context, c *cli.Command) (*blossom.Client, error) {
|
||||||
|
keyer, _, err := gatherKeyerFromArguments(ctx, c)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return blossom.NewClient(c.String("server"), keyer), nil
|
||||||
|
}
|
||||||
99
bunker.go
99
bunker.go
@@ -2,25 +2,24 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"fiatjaf.com/nostr"
|
||||||
|
"fiatjaf.com/nostr/nip19"
|
||||||
|
"fiatjaf.com/nostr/nip46"
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
"github.com/fiatjaf/cli/v3"
|
"github.com/urfave/cli/v3"
|
||||||
"github.com/nbd-wtf/go-nostr"
|
|
||||||
"github.com/nbd-wtf/go-nostr/nip19"
|
|
||||||
"github.com/nbd-wtf/go-nostr/nip46"
|
|
||||||
"golang.org/x/exp/slices"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var bunker = &cli.Command{
|
var bunker = &cli.Command{
|
||||||
Name: "bunker",
|
Name: "bunker",
|
||||||
Usage: "starts a NIP-46 signer daemon with the given --sec key",
|
Usage: "starts a nip46 signer daemon with the given --sec key",
|
||||||
ArgsUsage: "[relay...]",
|
ArgsUsage: "[relay...]",
|
||||||
Description: ``,
|
Description: ``,
|
||||||
DisableSliceFlagSeparator: true,
|
DisableSliceFlagSeparator: true,
|
||||||
@@ -29,7 +28,6 @@ var bunker = &cli.Command{
|
|||||||
Name: "sec",
|
Name: "sec",
|
||||||
Usage: "secret key to sign the event, as hex or nsec",
|
Usage: "secret key to sign the event, as hex or nsec",
|
||||||
DefaultText: "the key '1'",
|
DefaultText: "the key '1'",
|
||||||
Value: "0000000000000000000000000000000000000000000000000000000000000001",
|
|
||||||
},
|
},
|
||||||
&cli.BoolFlag{
|
&cli.BoolFlag{
|
||||||
Name: "prompt-sec",
|
Name: "prompt-sec",
|
||||||
@@ -40,7 +38,7 @@ var bunker = &cli.Command{
|
|||||||
Aliases: []string{"s"},
|
Aliases: []string{"s"},
|
||||||
Usage: "secrets for which we will always respond",
|
Usage: "secrets for which we will always respond",
|
||||||
},
|
},
|
||||||
&cli.StringSliceFlag{
|
&PubKeySliceFlag{
|
||||||
Name: "authorized-keys",
|
Name: "authorized-keys",
|
||||||
Aliases: []string{"k"},
|
Aliases: []string{"k"},
|
||||||
Usage: "pubkeys for which we will always respond",
|
Usage: "pubkeys for which we will always respond",
|
||||||
@@ -51,7 +49,7 @@ var bunker = &cli.Command{
|
|||||||
qs := url.Values{}
|
qs := url.Values{}
|
||||||
relayURLs := make([]string, 0, c.Args().Len())
|
relayURLs := make([]string, 0, c.Args().Len())
|
||||||
if relayUrls := c.Args().Slice(); len(relayUrls) > 0 {
|
if relayUrls := c.Args().Slice(); len(relayUrls) > 0 {
|
||||||
_, relays := connectToAllRelays(ctx, relayUrls, false)
|
relays := connectToAllRelays(ctx, c, relayUrls, nil, nostr.PoolOptions{})
|
||||||
if len(relays) == 0 {
|
if len(relays) == 0 {
|
||||||
log("failed to connect to any of the given relays.\n")
|
log("failed to connect to any of the given relays.\n")
|
||||||
os.Exit(3)
|
os.Exit(3)
|
||||||
@@ -72,7 +70,7 @@ var bunker = &cli.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
// other arguments
|
// other arguments
|
||||||
authorizedKeys := c.StringSlice("authorized-keys")
|
authorizedKeys := getPubKeySlice(c, "authorized-keys")
|
||||||
authorizedSecrets := c.StringSlice("authorized-secrets")
|
authorizedSecrets := c.StringSlice("authorized-secrets")
|
||||||
|
|
||||||
// this will be used to auto-authorize the next person who connects who isn't pre-authorized
|
// this will be used to auto-authorize the next person who connects who isn't pre-authorized
|
||||||
@@ -80,32 +78,30 @@ var bunker = &cli.Command{
|
|||||||
newSecret := randString(12)
|
newSecret := randString(12)
|
||||||
|
|
||||||
// static information
|
// static information
|
||||||
pubkey, err := nostr.GetPublicKey(sec)
|
pubkey := sec.Public()
|
||||||
if err != nil {
|
npub := nip19.EncodeNpub(pubkey)
|
||||||
return err
|
|
||||||
}
|
|
||||||
npub, _ := nip19.EncodePublicKey(pubkey)
|
|
||||||
bold := color.New(color.Bold).Sprintf
|
|
||||||
italic := color.New(color.Italic).Sprintf
|
|
||||||
|
|
||||||
// this function will be called every now and then
|
// this function will be called every now and then
|
||||||
printBunkerInfo := func() {
|
printBunkerInfo := func() {
|
||||||
qs.Set("secret", newSecret)
|
qs.Set("secret", newSecret)
|
||||||
bunkerURI := fmt.Sprintf("bunker://%s?%s", pubkey, qs.Encode())
|
bunkerURI := fmt.Sprintf("bunker://%s?%s", pubkey.Hex(), qs.Encode())
|
||||||
|
|
||||||
authorizedKeysStr := ""
|
authorizedKeysStr := ""
|
||||||
if len(authorizedKeys) != 0 {
|
if len(authorizedKeys) != 0 {
|
||||||
authorizedKeysStr = "\n authorized keys:\n - " + italic(strings.Join(authorizedKeys, "\n - "))
|
authorizedKeysStr = "\n authorized keys:"
|
||||||
|
for _, pubkey := range authorizedKeys {
|
||||||
|
authorizedKeysStr += "\n - " + colors.italic(pubkey.Hex())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
authorizedSecretsStr := ""
|
authorizedSecretsStr := ""
|
||||||
if len(authorizedSecrets) != 0 {
|
if len(authorizedSecrets) != 0 {
|
||||||
authorizedSecretsStr = "\n authorized secrets:\n - " + italic(strings.Join(authorizedSecrets, "\n - "))
|
authorizedSecretsStr = "\n authorized secrets:\n - " + colors.italic(strings.Join(authorizedSecrets, "\n - "))
|
||||||
}
|
}
|
||||||
|
|
||||||
preauthorizedFlags := ""
|
preauthorizedFlags := ""
|
||||||
for _, k := range authorizedKeys {
|
for _, k := range authorizedKeys {
|
||||||
preauthorizedFlags += " -k " + k
|
preauthorizedFlags += " -k " + k.Hex()
|
||||||
}
|
}
|
||||||
for _, s := range authorizedSecrets {
|
for _, s := range authorizedSecrets {
|
||||||
preauthorizedFlags += " -s " + s
|
preauthorizedFlags += " -s " + s
|
||||||
@@ -132,27 +128,25 @@ var bunker = &cli.Command{
|
|||||||
)
|
)
|
||||||
|
|
||||||
log("listening at %v:\n pubkey: %s \n npub: %s%s%s\n to restart: %s\n bunker: %s\n\n",
|
log("listening at %v:\n pubkey: %s \n npub: %s%s%s\n to restart: %s\n bunker: %s\n\n",
|
||||||
bold("%v", relayURLs),
|
colors.bold(relayURLs),
|
||||||
bold(pubkey),
|
colors.bold(pubkey.Hex()),
|
||||||
bold(npub),
|
colors.bold(npub),
|
||||||
authorizedKeysStr,
|
authorizedKeysStr,
|
||||||
authorizedSecretsStr,
|
authorizedSecretsStr,
|
||||||
color.CyanString(restartCommand),
|
color.CyanString(restartCommand),
|
||||||
bold(bunkerURI),
|
colors.bold(bunkerURI),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
printBunkerInfo()
|
printBunkerInfo()
|
||||||
|
|
||||||
// subscribe to relays
|
// subscribe to relays
|
||||||
pool := nostr.NewSimplePool(ctx)
|
events := sys.Pool.SubscribeMany(ctx, relayURLs, nostr.Filter{
|
||||||
now := nostr.Now()
|
Kinds: []nostr.Kind{nostr.KindNostrConnect},
|
||||||
events := pool.SubMany(ctx, relayURLs, nostr.Filters{
|
Tags: nostr.TagMap{"p": []string{pubkey.Hex()}},
|
||||||
{
|
Since: nostr.Now(),
|
||||||
Kinds: []int{nostr.KindNostrConnect},
|
LimitZero: true,
|
||||||
Tags: nostr.TagMap{"p": []string{pubkey}},
|
}, nostr.SubscriptionOptions{
|
||||||
Since: &now,
|
Label: "nak-bunker",
|
||||||
LimitZero: true,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
signer := nip46.NewStaticKeySigner(sec)
|
signer := nip46.NewStaticKeySigner(sec)
|
||||||
@@ -165,7 +159,7 @@ var bunker = &cli.Command{
|
|||||||
cancelPreviousBunkerInfoPrint = cancel
|
cancelPreviousBunkerInfoPrint = cancel
|
||||||
|
|
||||||
// asking user for authorization
|
// asking user for authorization
|
||||||
signer.AuthorizeRequest = func(harmless bool, from string, secret string) bool {
|
signer.AuthorizeRequest = func(harmless bool, from nostr.PubKey, secret string) bool {
|
||||||
if secret == newSecret {
|
if secret == newSecret {
|
||||||
// store this key
|
// store this key
|
||||||
authorizedKeys = append(authorizedKeys, from)
|
authorizedKeys = append(authorizedKeys, from)
|
||||||
@@ -185,21 +179,21 @@ var bunker = &cli.Command{
|
|||||||
cancelPreviousBunkerInfoPrint() // this prevents us from printing a million bunker info blocks
|
cancelPreviousBunkerInfoPrint() // this prevents us from printing a million bunker info blocks
|
||||||
|
|
||||||
// handle the NIP-46 request event
|
// handle the NIP-46 request event
|
||||||
req, resp, eventResponse, err := signer.HandleRequest(ie.Event)
|
req, resp, eventResponse, err := signer.HandleRequest(ctx, ie.Event)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log("< failed to handle request from %s: %s\n", ie.Event.PubKey, err.Error())
|
log("< failed to handle request from %s: %s\n", ie.Event.PubKey, err.Error())
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
jreq, _ := json.MarshalIndent(req, " ", " ")
|
jreq, _ := json.MarshalIndent(req, "", " ")
|
||||||
log("- got request from '%s': %s\n", color.New(color.Bold, color.FgBlue).Sprint(ie.Event.PubKey), string(jreq))
|
log("- got request from '%s': %s\n", color.New(color.Bold, color.FgBlue).Sprint(ie.Event.PubKey.Hex()), string(jreq))
|
||||||
jresp, _ := json.MarshalIndent(resp, " ", " ")
|
jresp, _ := json.MarshalIndent(resp, "", " ")
|
||||||
log("~ responding with %s\n", string(jresp))
|
log("~ responding with %s\n", string(jresp))
|
||||||
|
|
||||||
handlerWg.Add(len(relayURLs))
|
handlerWg.Add(len(relayURLs))
|
||||||
for _, relayURL := range relayURLs {
|
for _, relayURL := range relayURLs {
|
||||||
go func(relayURL string) {
|
go func(relayURL string) {
|
||||||
if relay, _ := pool.EnsureRelay(relayURL); relay != nil {
|
if relay, _ := sys.Pool.EnsureRelay(relayURL); relay != nil {
|
||||||
err := relay.Publish(ctx, eventResponse)
|
err := relay.Publish(ctx, eventResponse)
|
||||||
printLock.Lock()
|
printLock.Lock()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -224,7 +218,7 @@ var bunker = &cli.Command{
|
|||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
case <-time.After(time.Minute * 5):
|
case <-time.After(time.Minute * 5):
|
||||||
fmt.Fprintf(os.Stderr, "\n")
|
log("\n")
|
||||||
printBunkerInfo()
|
printBunkerInfo()
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -232,4 +226,25 @@ var bunker = &cli.Command{
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
|
Commands: []*cli.Command{
|
||||||
|
{
|
||||||
|
Name: "connect",
|
||||||
|
Usage: "use the client-initiated NostrConnect flow of NIP46",
|
||||||
|
ArgsUsage: "<nostrconnect-uri>",
|
||||||
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
|
if c.Args().Len() != 1 {
|
||||||
|
return fmt.Errorf("must be called with a nostrconnect://... uri")
|
||||||
|
}
|
||||||
|
|
||||||
|
uri, err := url.Parse(c.Args().First())
|
||||||
|
if err != nil || uri.Scheme != "nostrconnect" {
|
||||||
|
return fmt.Errorf("invalid uri")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
|
||||||
|
return fmt.Errorf("this is not implemented yet")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
231
cli_test.go
Normal file
231
cli_test.go
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/hex"
|
||||||
|
stdjson "encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"fiatjaf.com/nostr"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// these tests are tricky because commands and flags are declared as globals and values set in one call may persist
|
||||||
|
// to the next. for example, if in the first test we set --limit 2 then doesn't specify --limit in the second then
|
||||||
|
// it will still return true for cmd.IsSet("limit") and then we will set .LimitZero = true
|
||||||
|
|
||||||
|
func call(t *testing.T, cmd string) string {
|
||||||
|
var output strings.Builder
|
||||||
|
stdout = func(a ...any) {
|
||||||
|
output.WriteString(fmt.Sprint(a...))
|
||||||
|
output.WriteString("\n")
|
||||||
|
}
|
||||||
|
err := app.Run(t.Context(), strings.Split(cmd, " "))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return strings.TrimSpace(output.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEventBasic(t *testing.T) {
|
||||||
|
output := call(t, "nak event --ts 1699485669")
|
||||||
|
|
||||||
|
var evt nostr.Event
|
||||||
|
err := stdjson.Unmarshal([]byte(output), &evt)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, nostr.Kind(1), evt.Kind)
|
||||||
|
require.Equal(t, nostr.Timestamp(1699485669), evt.CreatedAt)
|
||||||
|
require.Equal(t, "hello from the nostr army knife", evt.Content)
|
||||||
|
require.Equal(t, "36d88cf5fcc449f2390a424907023eda7a74278120eebab8d02797cd92e7e29c", evt.ID.Hex())
|
||||||
|
require.Equal(t, "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", evt.PubKey.Hex())
|
||||||
|
require.Equal(t, "68e71a192e8abcf8582a222434ac823ecc50607450ebe8cc4c145eb047794cc382dc3f888ce879d2f404f5ba6085a47601360a0fa2dd4b50d317bd0c6197c2c2", hex.EncodeToString(evt.Sig[:]))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEventComplex(t *testing.T) {
|
||||||
|
output := call(t, "nak event --ts 1699485669 -k 11 -c skjdbaskd --sec 17 -t t=spam -e 36d88cf5fcc449f2390a424907023eda7a74278120eebab8d02797cd92e7e29c -t r=https://abc.def?name=foobar;nothing")
|
||||||
|
|
||||||
|
var evt nostr.Event
|
||||||
|
err := stdjson.Unmarshal([]byte(output), &evt)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, nostr.Kind(11), evt.Kind)
|
||||||
|
require.Equal(t, nostr.Timestamp(1699485669), evt.CreatedAt)
|
||||||
|
require.Equal(t, "skjdbaskd", evt.Content)
|
||||||
|
require.Equal(t, "19aba166dcf354bf5ef64f4afe69ada1eb851495001ee05e07d393ee8c8ea179", evt.ID.Hex())
|
||||||
|
require.Equal(t, "2fa2104d6b38d11b0230010559879124e42ab8dfeff5ff29dc9cdadd4ecacc3f", evt.PubKey.Hex())
|
||||||
|
require.Equal(t, "cf452def4a68341c897c3fc96fa34dc6895a5b8cc266d4c041bcdf758ec992ec5adb8b0179e98552aaaf9450526a26d7e62e413b15b1c57e0cfc8db6b29215d7", hex.EncodeToString(evt.Sig[:]))
|
||||||
|
|
||||||
|
require.Len(t, evt.Tags, 3)
|
||||||
|
require.Equal(t, nostr.Tag{"t", "spam"}, evt.Tags[0])
|
||||||
|
require.Equal(t, nostr.Tag{"r", "https://abc.def?name=foobar", "nothing"}, evt.Tags[1])
|
||||||
|
require.Equal(t, nostr.Tag{"e", "36d88cf5fcc449f2390a424907023eda7a74278120eebab8d02797cd92e7e29c"}, evt.Tags[2])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncode(t *testing.T) {
|
||||||
|
require.Equal(t,
|
||||||
|
"npub156n8a7wuhwk9tgrzjh8gwzc8q2dlekedec5djk0js9d3d7qhnq3qjpdq28",
|
||||||
|
call(t, "nak encode npub a6a67ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179822"),
|
||||||
|
)
|
||||||
|
require.Equal(t,
|
||||||
|
`nprofile1qqs2dfn7l8wthtz45p3ftn58pvrs9xlumvkuu2xet8egzkcklqtesgspz9mhxue69uhk27rpd4cxcefwvdhk6fl5jug
|
||||||
|
nprofile1qqs22kfpwwt4mmvlsd4f2uh23vg60ctvadnyvntx659jw93l0upe6tqpz9mhxue69uhk27rpd4cxcefwvdhk64h265a`,
|
||||||
|
call(t, "nak encode nprofile -r wss://example.com a6a67ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179822 a5592173975ded9f836a9572ea8b11a7e16ceb66464d66d50b27163f7f039d2c"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecodeNaddr(t *testing.T) {
|
||||||
|
output := call(t, "nak decode naddr1qqyrgcmyxe3kvefhqyxhwumn8ghj7mn0wvhxcmmvqgs9kqvr4dkruv3t7n2pc6e6a7v9v2s5fprmwjv4gde8c4fe5y29v0srqsqqql9ngrt6tu")
|
||||||
|
|
||||||
|
var result map[string]interface{}
|
||||||
|
err := stdjson.Unmarshal([]byte(output), &result)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, "5b0183ab6c3e322bf4d41c6b3aef98562a144847b7499543727c5539a114563e", result["pubkey"])
|
||||||
|
require.Equal(t, float64(31923), result["kind"])
|
||||||
|
require.Equal(t, "4cd6cfe7", result["identifier"])
|
||||||
|
require.Equal(t, []interface{}{"wss://nos.lol"}, result["relays"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecodePubkey(t *testing.T) {
|
||||||
|
output := call(t, "nak decode -p npub10xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqpkge6d npub1ccz8l9zpa47k6vz9gphftsrumpw80rjt3nhnefat4symjhrsnmjs38mnyd")
|
||||||
|
|
||||||
|
expected := "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798\nc6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5"
|
||||||
|
require.Equal(t, expected, output)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecodeMultipleNpubs(t *testing.T) {
|
||||||
|
output := call(t, "nak decode npub1l2vyh47mk2p0qlsku7hg0vn29faehy9hy34ygaclpn66ukqp3afqutajft npub10000003zmk89narqpczy4ff6rnuht2wu05na7kpnh3mak7z2tqzsv8vwqk")
|
||||||
|
require.Len(t, strings.Split(output, "\n"), 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecodeEventId(t *testing.T) {
|
||||||
|
output := call(t, "nak decode -e nevent1qyd8wumn8ghj7urewfsk66ty9enxjct5dfskvtnrdakj7qgmwaehxw309aex2mrp0yh8wetnw3jhymnzw33jucm0d5hszxthwden5te0wfjkccte9eekummjwsh8xmmrd9skctcpzamhxue69uhkzarvv9ejumn0wd68ytnvv9hxgtcqyqllp5v5j0nxr74fptqxkhvfv0h3uj870qpk3ln8a58agyxl3fka296ewr8 nevent1qqswh48lurxs8u0pll9qj2rzctvjncwhstpzlstq59rdtzlty79awns5hl5uf")
|
||||||
|
|
||||||
|
expected := "3ff0d19493e661faa90ac06b5d8963ef1e48fe780368fe67ed0fd410df8a6dd5\nebd4ffe0cd03f1e1ffca092862c2d929e1d782c22fc160a146d58beb278bd74e"
|
||||||
|
require.Equal(t, expected, output)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReq(t *testing.T) {
|
||||||
|
output := call(t, "nak req -k 1 -l 18 -a 2fa2104d6b38d11b0230010559879124e42ab8dfeff5ff29dc9cdadd4ecacc3f -e aec4de6d051a7c2b6ca2d087903d42051a31e07fb742f1240970084822de10a6")
|
||||||
|
|
||||||
|
var result []interface{}
|
||||||
|
err := stdjson.Unmarshal([]byte(output), &result)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, "REQ", result[0])
|
||||||
|
require.Equal(t, "nak", result[1])
|
||||||
|
|
||||||
|
filter := result[2].(map[string]interface{})
|
||||||
|
require.Equal(t, []interface{}{float64(1)}, filter["kinds"])
|
||||||
|
require.Equal(t, []interface{}{"2fa2104d6b38d11b0230010559879124e42ab8dfeff5ff29dc9cdadd4ecacc3f"}, filter["authors"])
|
||||||
|
require.Equal(t, float64(18), filter["limit"])
|
||||||
|
require.Equal(t, []interface{}{"aec4de6d051a7c2b6ca2d087903d42051a31e07fb742f1240970084822de10a6"}, filter["#e"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMultipleFetch(t *testing.T) {
|
||||||
|
output := call(t, "nak fetch naddr1qqyrgcmyxe3kvefhqyxhwumn8ghj7mn0wvhxcmmvqgs9kqvr4dkruv3t7n2pc6e6a7v9v2s5fprmwjv4gde8c4fe5y29v0srqsqqql9ngrt6tu nevent1qyd8wumn8ghj7urewfsk66ty9enxjct5dfskvtnrdakj7qgmwaehxw309aex2mrp0yh8wetnw3jhymnzw33jucm0d5hszxthwden5te0wfjkccte9eekummjwsh8xmmrd9skctcpzamhxue69uhkzarvv9ejumn0wd68ytnvv9hxgtcqyqllp5v5j0nxr74fptqxkhvfv0h3uj870qpk3ln8a58agyxl3fka296ewr8")
|
||||||
|
|
||||||
|
var events []nostr.Event
|
||||||
|
for _, line := range strings.Split(output, "\n") {
|
||||||
|
var evt nostr.Event
|
||||||
|
err := stdjson.Unmarshal([]byte(line), &evt)
|
||||||
|
require.NoError(t, err)
|
||||||
|
events = append(events, evt)
|
||||||
|
}
|
||||||
|
|
||||||
|
require.Len(t, events, 2)
|
||||||
|
|
||||||
|
// First event validation
|
||||||
|
require.Equal(t, nostr.Kind(31923), events[0].Kind)
|
||||||
|
require.Equal(t, "9ae5014573fc75ced00b343868d2cd9343ebcbbae50591c6fa8ae1cd99568f05", events[0].ID.Hex())
|
||||||
|
require.Equal(t, "5b0183ab6c3e322bf4d41c6b3aef98562a144847b7499543727c5539a114563e", events[0].PubKey.Hex())
|
||||||
|
require.Equal(t, nostr.Timestamp(1707764605), events[0].CreatedAt)
|
||||||
|
|
||||||
|
// Second event validation
|
||||||
|
require.Equal(t, nostr.Kind(1), events[1].Kind)
|
||||||
|
require.Equal(t, "3ff0d19493e661faa90ac06b5d8963ef1e48fe780368fe67ed0fd410df8a6dd5", events[1].ID.Hex())
|
||||||
|
require.Equal(t, "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d", events[1].PubKey.Hex())
|
||||||
|
require.Equal(t, nostr.Timestamp(1710759386), events[1].CreatedAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestKeyPublic(t *testing.T) {
|
||||||
|
output := call(t, "nak key public 3ff0d19493e661faa90ac06b5d8963ef1e48fe780368fe67ed0fd410df8a6dd5 3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d")
|
||||||
|
|
||||||
|
expected := "70f7120d065870513a6bddb61c8d400ad1e43449b1900ffdb5551e4c421375c8\n718d756f60cf5179ef35b39dc6db3ff58f04c0734f81f6d4410f0b047ddf9029"
|
||||||
|
require.Equal(t, expected, output)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestKeyDecrypt(t *testing.T) {
|
||||||
|
output := call(t, "nak key decrypt ncryptsec1qgg2gx2a7hxpsse2zulrv7m8qwccvl3mh8e9k8vtz3wpyrwuuclaq73gz7ddt5kpa93qyfhfjakguuf8uhw90jn6mszh7kqeh9mxzlyw8hy75fluzx4h75frwmu2yngsq7hx7w32d0vdyxyns5g6rqft banana")
|
||||||
|
require.Equal(t, "718d756f60cf5179ef35b39dc6db3ff58f04c0734f81f6d4410f0b047ddf9029", output)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReqIdFromRelay(t *testing.T) {
|
||||||
|
output := call(t, "nak req -i 20a6606ed548fe7107533cf3416ce1aa5e957c315c2a40249e12bd9873dca7da --limit 1 nos.lol")
|
||||||
|
|
||||||
|
var evt nostr.Event
|
||||||
|
err := stdjson.Unmarshal([]byte(output), &evt)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, nostr.Kind(1), evt.Kind)
|
||||||
|
require.Equal(t, "20a6606ed548fe7107533cf3416ce1aa5e957c315c2a40249e12bd9873dca7da", evt.ID.Hex())
|
||||||
|
require.Equal(t, "dd664d5e4016433a8cd69f005ae1480804351789b59de5af06276de65633d319", evt.PubKey.Hex())
|
||||||
|
require.Equal(t, nostr.Timestamp(1720972243), evt.CreatedAt)
|
||||||
|
require.Equal(t, "Yeah, so bizarre, but I guess most people are meant to be serfs.", evt.Content)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReqWithFlagsAfter1(t *testing.T) {
|
||||||
|
output := call(t, "nak req nos.lol -i 20a6606ed548fe7107533cf3416ce1aa5e957c315c2a40249e12bd9873dca7da --limit 1")
|
||||||
|
|
||||||
|
var evt nostr.Event
|
||||||
|
err := stdjson.Unmarshal([]byte(output), &evt)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, nostr.Kind(1), evt.Kind)
|
||||||
|
require.Equal(t, "20a6606ed548fe7107533cf3416ce1aa5e957c315c2a40249e12bd9873dca7da", evt.ID.Hex())
|
||||||
|
require.Equal(t, "dd664d5e4016433a8cd69f005ae1480804351789b59de5af06276de65633d319", evt.PubKey.Hex())
|
||||||
|
require.Equal(t, nostr.Timestamp(1720972243), evt.CreatedAt)
|
||||||
|
require.Equal(t, "Yeah, so bizarre, but I guess most people are meant to be serfs.", evt.Content)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReqWithFlagsAfter2(t *testing.T) {
|
||||||
|
output := call(t, "nak req -e 893d4c10f1c230240812c6bdf9ad877eed1e29e87029d153820c24680bb183b1 nostr.mom --author 2a7dcf382bcc96a393ada5c975f500393b3f7be6e466bff220aa161ad6b15eb6 --limit 1 -k 7")
|
||||||
|
|
||||||
|
var evt nostr.Event
|
||||||
|
err := stdjson.Unmarshal([]byte(output), &evt)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, nostr.Kind(7), evt.Kind)
|
||||||
|
require.Equal(t, "9b4868b068ea34ae51092807586c4541b3569d9efc23862aea48ef13de275857", evt.ID.Hex())
|
||||||
|
require.Equal(t, "2a7dcf382bcc96a393ada5c975f500393b3f7be6e466bff220aa161ad6b15eb6", evt.PubKey.Hex())
|
||||||
|
require.Equal(t, nostr.Timestamp(1720987327), evt.CreatedAt)
|
||||||
|
require.Equal(t, "❤️", evt.Content)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReqWithFlagsAfter3(t *testing.T) {
|
||||||
|
output := call(t, "nak req --limit 1 pyramid.fiatjaf.com -a 3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24 -qp 3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24 -e 9f3c1121c96edf17d84b9194f74d66d012b28c4e25b3ef190582c76b8546a188")
|
||||||
|
|
||||||
|
var evt nostr.Event
|
||||||
|
err := stdjson.Unmarshal([]byte(output), &evt)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, nostr.Kind(1), evt.Kind)
|
||||||
|
require.Equal(t, "101572c80ebdc963dab8440f6307387a3023b6d90f7e495d6c5ee1ef77045a67", evt.ID.Hex())
|
||||||
|
require.Equal(t, "3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24", evt.PubKey.Hex())
|
||||||
|
require.Equal(t, nostr.Timestamp(1720987305), evt.CreatedAt)
|
||||||
|
require.Equal(t, "Nope. I grew up playing in the woods. Never once saw a bear in the woods. If I did, I'd probably shiy my pants, then scream at it like I was a crazy person with my arms above my head to make me seem huge.", evt.Content)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNaturalTimestamps(t *testing.T) {
|
||||||
|
output := call(t, "nak event -t plu=pla -e 3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24 --ts '2018-May-19T03:37:19' -c nn")
|
||||||
|
|
||||||
|
var evt nostr.Event
|
||||||
|
err := stdjson.Unmarshal([]byte(output), &evt)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, nostr.Kind(1), evt.Kind)
|
||||||
|
require.Equal(t, "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", evt.PubKey.Hex())
|
||||||
|
require.Equal(t, nostr.Timestamp(1526711839), evt.CreatedAt)
|
||||||
|
require.Equal(t, "nn", evt.Content)
|
||||||
|
}
|
||||||
89
count.go
89
count.go
@@ -2,22 +2,23 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/fiatjaf/cli/v3"
|
"fiatjaf.com/nostr"
|
||||||
"github.com/nbd-wtf/go-nostr"
|
"fiatjaf.com/nostr/nip45"
|
||||||
|
"fiatjaf.com/nostr/nip45/hyperloglog"
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
var count = &cli.Command{
|
var count = &cli.Command{
|
||||||
Name: "count",
|
Name: "count",
|
||||||
Usage: "generates encoded COUNT messages and optionally use them to talk to relays",
|
Usage: "generates encoded COUNT messages and optionally use them to talk to relays",
|
||||||
Description: `outputs a NIP-45 request (the flags are mostly the same as 'nak req').`,
|
Description: `outputs a nip45 request (the flags are mostly the same as 'nak req').`,
|
||||||
DisableSliceFlagSeparator: true,
|
DisableSliceFlagSeparator: true,
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
&cli.StringSliceFlag{
|
&PubKeySliceFlag{
|
||||||
Name: "author",
|
Name: "author",
|
||||||
Aliases: []string{"a"},
|
Aliases: []string{"a"},
|
||||||
Usage: "only accept events from these authors (pubkey as hex)",
|
Usage: "only accept events from these authors (pubkey as hex)",
|
||||||
@@ -45,13 +46,13 @@ var count = &cli.Command{
|
|||||||
Usage: "shortcut for --tag p=<value>",
|
Usage: "shortcut for --tag p=<value>",
|
||||||
Category: CATEGORY_FILTER_ATTRIBUTES,
|
Category: CATEGORY_FILTER_ATTRIBUTES,
|
||||||
},
|
},
|
||||||
&cli.IntFlag{
|
&NaturalTimeFlag{
|
||||||
Name: "since",
|
Name: "since",
|
||||||
Aliases: []string{"s"},
|
Aliases: []string{"s"},
|
||||||
Usage: "only accept events newer than this (unix timestamp)",
|
Usage: "only accept events newer than this (unix timestamp)",
|
||||||
Category: CATEGORY_FILTER_ATTRIBUTES,
|
Category: CATEGORY_FILTER_ATTRIBUTES,
|
||||||
},
|
},
|
||||||
&cli.IntFlag{
|
&NaturalTimeFlag{
|
||||||
Name: "until",
|
Name: "until",
|
||||||
Aliases: []string{"u"},
|
Aliases: []string{"u"},
|
||||||
Usage: "only accept events older than this (unix timestamp)",
|
Usage: "only accept events older than this (unix timestamp)",
|
||||||
@@ -66,18 +67,32 @@ var count = &cli.Command{
|
|||||||
},
|
},
|
||||||
ArgsUsage: "[relay...]",
|
ArgsUsage: "[relay...]",
|
||||||
Action: func(ctx context.Context, c *cli.Command) error {
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
|
biggerUrlSize := 0
|
||||||
|
relayUrls := c.Args().Slice()
|
||||||
|
if len(relayUrls) > 0 {
|
||||||
|
relays := connectToAllRelays(ctx, c, relayUrls, nil, nostr.PoolOptions{})
|
||||||
|
if len(relays) == 0 {
|
||||||
|
log("failed to connect to any of the given relays.\n")
|
||||||
|
os.Exit(3)
|
||||||
|
}
|
||||||
|
relayUrls = make([]string, len(relays))
|
||||||
|
for i, relay := range relays {
|
||||||
|
relayUrls[i] = relay.URL
|
||||||
|
if len(relay.URL) > biggerUrlSize {
|
||||||
|
biggerUrlSize = len(relay.URL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
filter := nostr.Filter{}
|
filter := nostr.Filter{}
|
||||||
|
|
||||||
if authors := c.StringSlice("author"); len(authors) > 0 {
|
if authors := getPubKeySlice(c, "author"); len(authors) > 0 {
|
||||||
filter.Authors = authors
|
filter.Authors = authors
|
||||||
}
|
}
|
||||||
if ids := c.StringSlice("id"); len(ids) > 0 {
|
|
||||||
filter.IDs = ids
|
|
||||||
}
|
|
||||||
if kinds64 := c.IntSlice("kind"); len(kinds64) > 0 {
|
if kinds64 := c.IntSlice("kind"); len(kinds64) > 0 {
|
||||||
kinds := make([]int, len(kinds64))
|
kinds := make([]nostr.Kind, len(kinds64))
|
||||||
for i, v := range kinds64 {
|
for i, v := range kinds64 {
|
||||||
kinds[i] = int(v)
|
kinds[i] = nostr.Kind(v)
|
||||||
}
|
}
|
||||||
filter.Kinds = kinds
|
filter.Kinds = kinds
|
||||||
}
|
}
|
||||||
@@ -85,7 +100,7 @@ var count = &cli.Command{
|
|||||||
tags := make([][]string, 0, 5)
|
tags := make([][]string, 0, 5)
|
||||||
for _, tagFlag := range c.StringSlice("tag") {
|
for _, tagFlag := range c.StringSlice("tag") {
|
||||||
spl := strings.SplitN(tagFlag, "=", 2)
|
spl := strings.SplitN(tagFlag, "=", 2)
|
||||||
if len(spl) == 2 && len(spl[0]) == 1 {
|
if len(spl) == 2 {
|
||||||
tags = append(tags, spl)
|
tags = append(tags, spl)
|
||||||
} else {
|
} else {
|
||||||
return fmt.Errorf("invalid --tag '%s'", tagFlag)
|
return fmt.Errorf("invalid --tag '%s'", tagFlag)
|
||||||
@@ -107,38 +122,46 @@ var count = &cli.Command{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if since := c.Int("since"); since != 0 {
|
if c.IsSet("since") {
|
||||||
ts := nostr.Timestamp(since)
|
filter.Since = getNaturalDate(c, "since")
|
||||||
filter.Since = &ts
|
|
||||||
}
|
}
|
||||||
if until := c.Int("until"); until != 0 {
|
if c.IsSet("until") {
|
||||||
ts := nostr.Timestamp(until)
|
filter.Until = getNaturalDate(c, "until")
|
||||||
filter.Until = &ts
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if limit := c.Int("limit"); limit != 0 {
|
if limit := c.Int("limit"); limit != 0 {
|
||||||
filter.Limit = int(limit)
|
filter.Limit = int(limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
relays := c.Args().Slice()
|
|
||||||
successes := 0
|
successes := 0
|
||||||
failures := make([]error, 0, len(relays))
|
if len(relayUrls) > 0 {
|
||||||
if len(relays) > 0 {
|
var hll *hyperloglog.HyperLogLog
|
||||||
for _, relayUrl := range relays {
|
if offset := nip45.HyperLogLogEventPubkeyOffsetForFilter(filter); offset != -1 && len(relayUrls) > 1 {
|
||||||
relay, err := nostr.RelayConnect(ctx, relayUrl)
|
hll = hyperloglog.New(offset)
|
||||||
|
}
|
||||||
|
for _, relayUrl := range relayUrls {
|
||||||
|
relay, _ := sys.Pool.EnsureRelay(relayUrl)
|
||||||
|
count, hllRegisters, err := relay.Count(ctx, filter, nostr.SubscriptionOptions{})
|
||||||
|
fmt.Fprintf(os.Stderr, "%s%s: ", strings.Repeat(" ", biggerUrlSize-len(relayUrl)), relayUrl)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
failures = append(failures, err)
|
fmt.Fprintf(os.Stderr, "❌ %s\n", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
count, err := relay.Count(ctx, nostr.Filters{filter})
|
|
||||||
if err != nil {
|
var hasHLLStr string
|
||||||
failures = append(failures, err)
|
if hll != nil && len(hllRegisters) == 256 {
|
||||||
continue
|
hll.MergeRegisters(hllRegisters)
|
||||||
|
hasHLLStr = " 📋"
|
||||||
}
|
}
|
||||||
fmt.Printf("%s: %d\n", relay.URL, count)
|
|
||||||
|
fmt.Fprintf(os.Stderr, "%d%s\n", count, hasHLLStr)
|
||||||
successes++
|
successes++
|
||||||
}
|
}
|
||||||
if successes == 0 {
|
if successes == 0 {
|
||||||
return errors.Join(failures...)
|
return fmt.Errorf("all relays have failed")
|
||||||
|
} else if hll != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "📋 HyperLogLog sum: %d\n", hll.Count())
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// no relays given, will just print the filter
|
// no relays given, will just print the filter
|
||||||
|
|||||||
132
curl.go
Normal file
132
curl.go
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"fiatjaf.com/nostr"
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
var curlFlags []string
|
||||||
|
|
||||||
|
var curl = &cli.Command{
|
||||||
|
Name: "curl",
|
||||||
|
Usage: "calls curl but with a nip98 header",
|
||||||
|
Description: "accepts all flags and arguments exactly as they would be passed to curl.",
|
||||||
|
Flags: defaultKeyFlags,
|
||||||
|
DisableSliceFlagSeparator: true,
|
||||||
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
|
kr, _, err := gatherKeyerFromArguments(ctx, c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// cowboy parsing of curl flags to get the data we need for nip98
|
||||||
|
var url string
|
||||||
|
var method string
|
||||||
|
var presumedMethod string
|
||||||
|
|
||||||
|
curlBodyBuildingFlags := []string{
|
||||||
|
"-d",
|
||||||
|
"--data",
|
||||||
|
"--data-binary",
|
||||||
|
"--data-ascii",
|
||||||
|
"--data-raw",
|
||||||
|
"--data-urlencode",
|
||||||
|
"-F",
|
||||||
|
"--form",
|
||||||
|
"--form-string",
|
||||||
|
"--form-escape",
|
||||||
|
"--upload-file",
|
||||||
|
}
|
||||||
|
|
||||||
|
nextIsMethod := false
|
||||||
|
for _, f := range curlFlags {
|
||||||
|
if nextIsMethod {
|
||||||
|
method = f
|
||||||
|
method, _ = strings.CutPrefix(method, `"`)
|
||||||
|
method, _ = strings.CutSuffix(method, `"`)
|
||||||
|
method = strings.ToUpper(method)
|
||||||
|
} else if strings.HasPrefix(f, "https://") || strings.HasPrefix(f, "http://") {
|
||||||
|
url = f
|
||||||
|
} else if f == "--request" || f == "-X" {
|
||||||
|
nextIsMethod = true
|
||||||
|
continue
|
||||||
|
} else if slices.Contains(curlBodyBuildingFlags, f) ||
|
||||||
|
slices.ContainsFunc(curlBodyBuildingFlags, func(s string) bool {
|
||||||
|
return strings.HasPrefix(f, s)
|
||||||
|
}) {
|
||||||
|
presumedMethod = "POST"
|
||||||
|
}
|
||||||
|
nextIsMethod = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if url == "" {
|
||||||
|
return fmt.Errorf("can't create nip98 event: target url is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if method == "" {
|
||||||
|
if presumedMethod != "" {
|
||||||
|
method = presumedMethod
|
||||||
|
} else {
|
||||||
|
method = "GET"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// make and sign event
|
||||||
|
evt := nostr.Event{
|
||||||
|
Kind: 27235,
|
||||||
|
CreatedAt: nostr.Now(),
|
||||||
|
Tags: nostr.Tags{
|
||||||
|
{"u", url},
|
||||||
|
{"method", method},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := kr.SignEvent(ctx, &evt); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// the first 2 indexes of curlFlags were reserved for this
|
||||||
|
curlFlags[0] = "-H"
|
||||||
|
curlFlags[1] = fmt.Sprintf("Authorization: Nostr %s", base64.StdEncoding.EncodeToString([]byte(evt.String())))
|
||||||
|
|
||||||
|
// call curl
|
||||||
|
cmd := exec.Command("curl", curlFlags...)
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
cmd.Stdin = os.Stdin
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Run()
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func realCurl() error {
|
||||||
|
curlFlags = make([]string, 2, max(len(os.Args)-4, 2))
|
||||||
|
keyFlags := make([]string, 0, 5)
|
||||||
|
|
||||||
|
for i := 0; i < len(os.Args[2:]); i++ {
|
||||||
|
arg := os.Args[i+2]
|
||||||
|
if slices.ContainsFunc(defaultKeyFlags, func(f cli.Flag) bool {
|
||||||
|
bareArg, _ := strings.CutPrefix(arg, "-")
|
||||||
|
bareArg, _ = strings.CutPrefix(bareArg, "-")
|
||||||
|
return slices.Contains(f.Names(), bareArg)
|
||||||
|
}) {
|
||||||
|
keyFlags = append(keyFlags, arg)
|
||||||
|
if arg != "--prompt-sec" {
|
||||||
|
i++
|
||||||
|
val := os.Args[i+2]
|
||||||
|
keyFlags = append(keyFlags, val)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
curlFlags = append(curlFlags, arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return curl.Run(context.Background(), keyFlags)
|
||||||
|
}
|
||||||
111
decode.go
111
decode.go
@@ -3,13 +3,13 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
stdjson "encoding/json"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/fiatjaf/cli/v3"
|
"fiatjaf.com/nostr"
|
||||||
"github.com/nbd-wtf/go-nostr"
|
"fiatjaf.com/nostr/nip05"
|
||||||
"github.com/nbd-wtf/go-nostr/nip19"
|
"fiatjaf.com/nostr/nip19"
|
||||||
sdk "github.com/nbd-wtf/nostr-sdk"
|
"github.com/urfave/cli/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
var decode = &cli.Command{
|
var decode = &cli.Command{
|
||||||
@@ -40,73 +40,56 @@ var decode = &cli.Command{
|
|||||||
input = input[6:]
|
input = input[6:]
|
||||||
}
|
}
|
||||||
|
|
||||||
var decodeResult DecodeResult
|
_, data, err := nip19.Decode(input)
|
||||||
if b, err := hex.DecodeString(input); err == nil {
|
if err == nil {
|
||||||
if len(b) == 64 {
|
switch v := data.(type) {
|
||||||
decodeResult.HexResult.PossibleTypes = []string{"sig"}
|
case nostr.SecretKey:
|
||||||
decodeResult.HexResult.Signature = hex.EncodeToString(b)
|
stdout(v.Hex())
|
||||||
} else if len(b) == 32 {
|
continue
|
||||||
decodeResult.HexResult.PossibleTypes = []string{"pubkey", "private_key", "event_id"}
|
case nostr.PubKey:
|
||||||
decodeResult.HexResult.ID = hex.EncodeToString(b)
|
stdout(v.Hex())
|
||||||
decodeResult.HexResult.PrivateKey = hex.EncodeToString(b)
|
continue
|
||||||
decodeResult.HexResult.PublicKey = hex.EncodeToString(b)
|
case [32]byte:
|
||||||
} else {
|
stdout(hex.EncodeToString(v[:]))
|
||||||
ctx = lineProcessingError(ctx, "hex string with invalid number of bytes: %d", len(b))
|
continue
|
||||||
|
case nostr.EventPointer:
|
||||||
|
if c.Bool("id") {
|
||||||
|
stdout(v.ID.Hex())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out, _ := stdjson.MarshalIndent(v, "", " ")
|
||||||
|
stdout(string(out))
|
||||||
|
continue
|
||||||
|
case nostr.ProfilePointer:
|
||||||
|
if c.Bool("pubkey") {
|
||||||
|
stdout(v.PublicKey.Hex())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out, _ := stdjson.MarshalIndent(v, "", " ")
|
||||||
|
stdout(string(out))
|
||||||
|
continue
|
||||||
|
case nostr.EntityPointer:
|
||||||
|
out, _ := stdjson.MarshalIndent(v, "", " ")
|
||||||
|
stdout(string(out))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
} else if evp := sdk.InputToEventPointer(input); evp != nil {
|
}
|
||||||
decodeResult = DecodeResult{EventPointer: evp}
|
|
||||||
} else if pp := sdk.InputToProfile(ctx, input); pp != nil {
|
pp, _ := nip05.QueryIdentifier(ctx, input)
|
||||||
decodeResult = DecodeResult{ProfilePointer: pp}
|
if pp != nil {
|
||||||
} else if prefix, value, err := nip19.Decode(input); err == nil && prefix == "naddr" {
|
if c.Bool("pubkey") {
|
||||||
ep := value.(nostr.EntityPointer)
|
stdout(pp.PublicKey.Hex())
|
||||||
decodeResult = DecodeResult{EntityPointer: &ep}
|
continue
|
||||||
} else if prefix, value, err := nip19.Decode(input); err == nil && prefix == "nsec" {
|
}
|
||||||
decodeResult.PrivateKey.PrivateKey = value.(string)
|
out, _ := stdjson.MarshalIndent(pp, "", " ")
|
||||||
decodeResult.PrivateKey.PublicKey, _ = nostr.GetPublicKey(value.(string))
|
stdout(string(out))
|
||||||
} else {
|
|
||||||
ctx = lineProcessingError(ctx, "couldn't decode input '%s': %s", input, err)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
stdout(decodeResult.JSON())
|
ctx = lineProcessingError(ctx, "couldn't decode input '%s'", input)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
exitIfLineProcessingError(ctx)
|
exitIfLineProcessingError(ctx)
|
||||||
return nil
|
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)
|
|
||||||
}
|
|
||||||
|
|||||||
173
encode.go
173
encode.go
@@ -4,9 +4,9 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/fiatjaf/cli/v3"
|
"fiatjaf.com/nostr"
|
||||||
"github.com/nbd-wtf/go-nostr"
|
"fiatjaf.com/nostr/nip19"
|
||||||
"github.com/nbd-wtf/go-nostr/nip19"
|
"github.com/urfave/cli/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
var encode = &cli.Command{
|
var encode = &cli.Command{
|
||||||
@@ -18,14 +18,68 @@ var encode = &cli.Command{
|
|||||||
nak encode nprofile --relay <relay-url> <pubkey-hex>
|
nak encode nprofile --relay <relay-url> <pubkey-hex>
|
||||||
nak encode nevent <event-id>
|
nak encode nevent <event-id>
|
||||||
nak encode nevent --author <pubkey-hex> --relay <relay-url> --relay <other-relay> <event-id>
|
nak encode nevent --author <pubkey-hex> --relay <relay-url> --relay <other-relay> <event-id>
|
||||||
nak encode nsec <privkey-hex>`,
|
nak encode nsec <privkey-hex>
|
||||||
Before: func(ctx context.Context, c *cli.Command) error {
|
echo '{"pubkey":"7b225d32d3edb978dba1adfd9440105646babbabbda181ea383f74ba53c3be19","relays":["wss://nada.zero"]}' | nak encode
|
||||||
if c.Args().Len() < 1 {
|
echo '{
|
||||||
return fmt.Errorf("expected more than 1 argument.")
|
"id":"7b225d32d3edb978dba1adfd9440105646babbabbda181ea383f74ba53c3be19"
|
||||||
}
|
"relays":["wss://nada.zero"],
|
||||||
return nil
|
"author":"ebb6ff85430705651b311ed51328767078fd790b14f02d22efba68d5513376bc"
|
||||||
|
} | nak encode`,
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.StringSliceFlag{
|
||||||
|
Name: "relay",
|
||||||
|
Aliases: []string{"r"},
|
||||||
|
Usage: "attach relay hints to naddr code",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
DisableSliceFlagSeparator: true,
|
DisableSliceFlagSeparator: true,
|
||||||
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
|
if c.Args().Len() != 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
relays := c.StringSlice("relay")
|
||||||
|
if err := normalizeAndValidateRelayURLs(relays); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
hasStdin := false
|
||||||
|
for jsonStr := range getJsonsOrBlank() {
|
||||||
|
if jsonStr == "{}" {
|
||||||
|
hasStdin = false
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
hasStdin = true
|
||||||
|
}
|
||||||
|
|
||||||
|
var eventPtr nostr.EventPointer
|
||||||
|
if err := json.Unmarshal([]byte(jsonStr), &eventPtr); err == nil && eventPtr.ID != nostr.ZeroID {
|
||||||
|
stdout(nip19.EncodeNevent(eventPtr.ID, appendUnique(relays, eventPtr.Relays...), eventPtr.Author))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var profilePtr nostr.ProfilePointer
|
||||||
|
if err := json.Unmarshal([]byte(jsonStr), &profilePtr); err == nil && profilePtr.PublicKey != nostr.ZeroPK {
|
||||||
|
stdout(nip19.EncodeNprofile(profilePtr.PublicKey, appendUnique(relays, profilePtr.Relays...)))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var entityPtr nostr.EntityPointer
|
||||||
|
if err := json.Unmarshal([]byte(jsonStr), &entityPtr); err == nil && entityPtr.PublicKey != nostr.ZeroPK {
|
||||||
|
stdout(nip19.EncodeNaddr(entityPtr.PublicKey, entityPtr.Kind, entityPtr.Identifier, appendUnique(relays, entityPtr.Relays...)))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx = lineProcessingError(ctx, "couldn't decode JSON '%s'", jsonStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hasStdin {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
exitIfLineProcessingError(ctx)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
Commands: []*cli.Command{
|
Commands: []*cli.Command{
|
||||||
{
|
{
|
||||||
Name: "npub",
|
Name: "npub",
|
||||||
@@ -33,16 +87,13 @@ var encode = &cli.Command{
|
|||||||
DisableSliceFlagSeparator: true,
|
DisableSliceFlagSeparator: true,
|
||||||
Action: func(ctx context.Context, c *cli.Command) error {
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
for target := range getStdinLinesOrArguments(c.Args()) {
|
for target := range getStdinLinesOrArguments(c.Args()) {
|
||||||
if ok := nostr.IsValidPublicKey(target); !ok {
|
pk, err := nostr.PubKeyFromHexCheap(target)
|
||||||
ctx = lineProcessingError(ctx, "invalid public key: %s", target)
|
if err != nil {
|
||||||
|
ctx = lineProcessingError(ctx, "invalid public key '%s': %w", target, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if npub, err := nip19.EncodePublicKey(target); err == nil {
|
stdout(nip19.EncodeNpub(pk))
|
||||||
stdout(npub)
|
|
||||||
} else {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
exitIfLineProcessingError(ctx)
|
exitIfLineProcessingError(ctx)
|
||||||
@@ -55,16 +106,13 @@ var encode = &cli.Command{
|
|||||||
DisableSliceFlagSeparator: true,
|
DisableSliceFlagSeparator: true,
|
||||||
Action: func(ctx context.Context, c *cli.Command) error {
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
for target := range getStdinLinesOrArguments(c.Args()) {
|
for target := range getStdinLinesOrArguments(c.Args()) {
|
||||||
if ok := nostr.IsValid32ByteHex(target); !ok {
|
sk, err := nostr.SecretKeyFromHex(target)
|
||||||
ctx = lineProcessingError(ctx, "invalid private key: %s", target)
|
if err != nil {
|
||||||
|
ctx = lineProcessingError(ctx, "invalid private key '%s': %w", target, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if npub, err := nip19.EncodePrivateKey(target); err == nil {
|
stdout(nip19.EncodeNsec(sk))
|
||||||
stdout(npub)
|
|
||||||
} else {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
exitIfLineProcessingError(ctx)
|
exitIfLineProcessingError(ctx)
|
||||||
@@ -84,8 +132,9 @@ var encode = &cli.Command{
|
|||||||
DisableSliceFlagSeparator: true,
|
DisableSliceFlagSeparator: true,
|
||||||
Action: func(ctx context.Context, c *cli.Command) error {
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
for target := range getStdinLinesOrArguments(c.Args()) {
|
for target := range getStdinLinesOrArguments(c.Args()) {
|
||||||
if ok := nostr.IsValid32ByteHex(target); !ok {
|
pk, err := nostr.PubKeyFromHexCheap(target)
|
||||||
ctx = lineProcessingError(ctx, "invalid public key: %s", target)
|
if err != nil {
|
||||||
|
ctx = lineProcessingError(ctx, "invalid public key '%s': %w", target, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,11 +143,7 @@ var encode = &cli.Command{
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if npub, err := nip19.EncodeProfile(target, relays); err == nil {
|
stdout(nip19.EncodeNprofile(pk, relays))
|
||||||
stdout(npub)
|
|
||||||
} else {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
exitIfLineProcessingError(ctx)
|
exitIfLineProcessingError(ctx)
|
||||||
@@ -109,12 +154,7 @@ var encode = &cli.Command{
|
|||||||
Name: "nevent",
|
Name: "nevent",
|
||||||
Usage: "generate event codes with optionally attached relay information",
|
Usage: "generate event codes with optionally attached relay information",
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
&cli.StringSliceFlag{
|
&PubKeyFlag{
|
||||||
Name: "relay",
|
|
||||||
Aliases: []string{"r"},
|
|
||||||
Usage: "attach relay hints to nevent code",
|
|
||||||
},
|
|
||||||
&cli.StringFlag{
|
|
||||||
Name: "author",
|
Name: "author",
|
||||||
Aliases: []string{"a"},
|
Aliases: []string{"a"},
|
||||||
Usage: "attach an author pubkey as a hint to the nevent code",
|
Usage: "attach an author pubkey as a hint to the nevent code",
|
||||||
@@ -123,28 +163,19 @@ var encode = &cli.Command{
|
|||||||
DisableSliceFlagSeparator: true,
|
DisableSliceFlagSeparator: true,
|
||||||
Action: func(ctx context.Context, c *cli.Command) error {
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
for target := range getStdinLinesOrArguments(c.Args()) {
|
for target := range getStdinLinesOrArguments(c.Args()) {
|
||||||
if ok := nostr.IsValid32ByteHex(target); !ok {
|
id, err := nostr.IDFromHex(target)
|
||||||
|
if err != nil {
|
||||||
ctx = lineProcessingError(ctx, "invalid event id: %s", target)
|
ctx = lineProcessingError(ctx, "invalid event id: %s", target)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
author := c.String("author")
|
author := getPubKey(c, "author")
|
||||||
if author != "" {
|
|
||||||
if ok := nostr.IsValidPublicKey(author); !ok {
|
|
||||||
return fmt.Errorf("invalid 'author' public key")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
relays := c.StringSlice("relay")
|
relays := c.StringSlice("relay")
|
||||||
if err := normalizeAndValidateRelayURLs(relays); err != nil {
|
if err := normalizeAndValidateRelayURLs(relays); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if npub, err := nip19.EncodeEvent(target, relays, author); err == nil {
|
stdout(nip19.EncodeNevent(id, relays, author))
|
||||||
stdout(npub)
|
|
||||||
} else {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
exitIfLineProcessingError(ctx)
|
exitIfLineProcessingError(ctx)
|
||||||
@@ -153,7 +184,7 @@ var encode = &cli.Command{
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "naddr",
|
Name: "naddr",
|
||||||
Usage: "generate codes for NIP-33 parameterized replaceable events",
|
Usage: "generate codes for addressable events",
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: "identifier",
|
Name: "identifier",
|
||||||
@@ -161,7 +192,7 @@ var encode = &cli.Command{
|
|||||||
Usage: "the \"d\" tag identifier of this replaceable event -- can also be read from stdin",
|
Usage: "the \"d\" tag identifier of this replaceable event -- can also be read from stdin",
|
||||||
Required: true,
|
Required: true,
|
||||||
},
|
},
|
||||||
&cli.StringFlag{
|
&PubKeyFlag{
|
||||||
Name: "pubkey",
|
Name: "pubkey",
|
||||||
Usage: "pubkey of the naddr author",
|
Usage: "pubkey of the naddr author",
|
||||||
Aliases: []string{"author", "a", "p"},
|
Aliases: []string{"author", "a", "p"},
|
||||||
@@ -173,23 +204,15 @@ var encode = &cli.Command{
|
|||||||
Usage: "kind of referred replaceable event",
|
Usage: "kind of referred replaceable event",
|
||||||
Required: true,
|
Required: true,
|
||||||
},
|
},
|
||||||
&cli.StringSliceFlag{
|
|
||||||
Name: "relay",
|
|
||||||
Aliases: []string{"r"},
|
|
||||||
Usage: "attach relay hints to naddr code",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
DisableSliceFlagSeparator: true,
|
DisableSliceFlagSeparator: true,
|
||||||
Action: func(ctx context.Context, c *cli.Command) error {
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
for d := range getStdinLinesOrBlank() {
|
for d := range getStdinLinesOrBlank() {
|
||||||
pubkey := c.String("pubkey")
|
pubkey := getPubKey(c, "pubkey")
|
||||||
if ok := nostr.IsValidPublicKey(pubkey); !ok {
|
|
||||||
return fmt.Errorf("invalid 'pubkey'")
|
|
||||||
}
|
|
||||||
|
|
||||||
kind := c.Int("kind")
|
kind := c.Int("kind")
|
||||||
if kind < 30000 || kind >= 40000 {
|
if kind < 30000 || kind >= 40000 {
|
||||||
return fmt.Errorf("kind must be between 30000 and 39999, as per NIP-16, got %d", kind)
|
return fmt.Errorf("kind must be between 30000 and 39999, got %d", kind)
|
||||||
}
|
}
|
||||||
|
|
||||||
if d == "" {
|
if d == "" {
|
||||||
@@ -205,33 +228,7 @@ var encode = &cli.Command{
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if npub, err := nip19.EncodeEntity(pubkey, int(kind), d, relays); err == nil {
|
stdout(nip19.EncodeNaddr(pubkey, nostr.Kind(kind), d, relays))
|
||||||
stdout(npub)
|
|
||||||
} else {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exitIfLineProcessingError(ctx)
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "note",
|
|
||||||
Usage: "generate note1 event codes (not recommended)",
|
|
||||||
DisableSliceFlagSeparator: true,
|
|
||||||
Action: func(ctx context.Context, c *cli.Command) error {
|
|
||||||
for target := range getStdinLinesOrArguments(c.Args()) {
|
|
||||||
if ok := nostr.IsValid32ByteHex(target); !ok {
|
|
||||||
ctx = lineProcessingError(ctx, "invalid event id: %s", target)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if note, err := nip19.EncodeNote(target); err == nil {
|
|
||||||
stdout(note)
|
|
||||||
} else {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
exitIfLineProcessingError(ctx)
|
exitIfLineProcessingError(ctx)
|
||||||
|
|||||||
133
encrypt_decrypt.go
Normal file
133
encrypt_decrypt.go
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"fiatjaf.com/nostr/nip04"
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
var encrypt = &cli.Command{
|
||||||
|
Name: "encrypt",
|
||||||
|
Usage: "encrypts a string with nip44 (or nip04 if specified using a flag) and returns the resulting ciphertext as base64",
|
||||||
|
ArgsUsage: "[plaintext string]",
|
||||||
|
DisableSliceFlagSeparator: true,
|
||||||
|
Flags: append(
|
||||||
|
defaultKeyFlags,
|
||||||
|
&PubKeyFlag{
|
||||||
|
Name: "recipient-pubkey",
|
||||||
|
Aliases: []string{"p", "tgt", "target", "pubkey"},
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "nip04",
|
||||||
|
Usage: "use nip04 encryption instead of nip44",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
|
target := getPubKey(c, "recipient-pubkey")
|
||||||
|
|
||||||
|
plaintext := c.Args().First()
|
||||||
|
|
||||||
|
if c.Bool("nip04") {
|
||||||
|
sec, bunker, err := gatherSecretKeyOrBunkerFromArguments(ctx, c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if bunker != nil {
|
||||||
|
ciphertext, err := bunker.NIP04Encrypt(ctx, target, plaintext)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
stdout(ciphertext)
|
||||||
|
} else {
|
||||||
|
ss, err := nip04.ComputeSharedSecret(target, sec)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to compute nip04 shared secret: %w", err)
|
||||||
|
}
|
||||||
|
ciphertext, err := nip04.Encrypt(plaintext, ss)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to encrypt as nip04: %w", err)
|
||||||
|
}
|
||||||
|
stdout(ciphertext)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
kr, _, err := gatherKeyerFromArguments(ctx, c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := kr.Encrypt(ctx, plaintext, target)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to encrypt: %w", err)
|
||||||
|
}
|
||||||
|
stdout(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var decrypt = &cli.Command{
|
||||||
|
Name: "decrypt",
|
||||||
|
Usage: "decrypts a base64 nip44 ciphertext (or nip04 if specified using a flag) and returns the resulting plaintext",
|
||||||
|
ArgsUsage: "[ciphertext base64]",
|
||||||
|
DisableSliceFlagSeparator: true,
|
||||||
|
Flags: append(
|
||||||
|
defaultKeyFlags,
|
||||||
|
&PubKeyFlag{
|
||||||
|
Name: "sender-pubkey",
|
||||||
|
Aliases: []string{"p", "src", "source", "pubkey"},
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "nip04",
|
||||||
|
Usage: "use nip04 encryption instead of nip44",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
|
source := getPubKey(c, "sender-pubkey")
|
||||||
|
|
||||||
|
ciphertext := c.Args().First()
|
||||||
|
|
||||||
|
if c.Bool("nip04") {
|
||||||
|
sec, bunker, err := gatherSecretKeyOrBunkerFromArguments(ctx, c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if bunker != nil {
|
||||||
|
plaintext, err := bunker.NIP04Decrypt(ctx, source, ciphertext)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
stdout(plaintext)
|
||||||
|
} else {
|
||||||
|
ss, err := nip04.ComputeSharedSecret(source, sec)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to compute nip04 shared secret: %w", err)
|
||||||
|
}
|
||||||
|
plaintext, err := nip04.Decrypt(ciphertext, ss)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to encrypt as nip04: %w", err)
|
||||||
|
}
|
||||||
|
stdout(plaintext)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
kr, _, err := gatherKeyerFromArguments(ctx, c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := kr.Decrypt(ctx, ciphertext, source)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to encrypt: %w", err)
|
||||||
|
}
|
||||||
|
stdout(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
334
event.go
334
event.go
@@ -2,18 +2,20 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/fiatjaf/cli/v3"
|
"fiatjaf.com/nostr"
|
||||||
|
"fiatjaf.com/nostr/keyer"
|
||||||
|
"fiatjaf.com/nostr/nip13"
|
||||||
|
"fiatjaf.com/nostr/nip19"
|
||||||
|
"github.com/fatih/color"
|
||||||
"github.com/mailru/easyjson"
|
"github.com/mailru/easyjson"
|
||||||
"github.com/nbd-wtf/go-nostr"
|
"github.com/urfave/cli/v3"
|
||||||
"github.com/nbd-wtf/go-nostr/nip13"
|
|
||||||
"github.com/nbd-wtf/go-nostr/nip19"
|
|
||||||
"golang.org/x/exp/slices"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -37,30 +39,7 @@ example:
|
|||||||
echo '{"id":"a889df6a387419ff204305f4c2d296ee328c3cd4f8b62f205648a541b4554dfb","pubkey":"c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5","created_at":1698623783,"kind":1,"tags":[],"content":"hello from the nostr army knife","sig":"84876e1ee3e726da84e5d195eb79358b2b3eaa4d9bd38456fde3e8a2af3f1cd4cda23f23fda454869975b3688797d4c66e12f4c51c1b43c6d2997c5e61865661"}' | nak event wss://offchain.pub
|
echo '{"id":"a889df6a387419ff204305f4c2d296ee328c3cd4f8b62f205648a541b4554dfb","pubkey":"c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5","created_at":1698623783,"kind":1,"tags":[],"content":"hello from the nostr army knife","sig":"84876e1ee3e726da84e5d195eb79358b2b3eaa4d9bd38456fde3e8a2af3f1cd4cda23f23fda454869975b3688797d4c66e12f4c51c1b43c6d2997c5e61865661"}' | nak event wss://offchain.pub
|
||||||
echo '{"tags": [["t", "spam"]]}' | nak event -c 'this is spam'`,
|
echo '{"tags": [["t", "spam"]]}' | nak event -c 'this is spam'`,
|
||||||
DisableSliceFlagSeparator: true,
|
DisableSliceFlagSeparator: true,
|
||||||
Flags: []cli.Flag{
|
Flags: append(defaultKeyFlags,
|
||||||
&cli.StringFlag{
|
|
||||||
Name: "sec",
|
|
||||||
Usage: "secret key to sign the event, as nsec, ncryptsec or hex",
|
|
||||||
DefaultText: "the key '1'",
|
|
||||||
Value: "0000000000000000000000000000000000000000000000000000000000000001",
|
|
||||||
Category: CATEGORY_SIGNER,
|
|
||||||
},
|
|
||||||
&cli.BoolFlag{
|
|
||||||
Name: "prompt-sec",
|
|
||||||
Usage: "prompt the user to paste a hex or nsec with which to sign the event",
|
|
||||||
Category: CATEGORY_SIGNER,
|
|
||||||
},
|
|
||||||
&cli.StringFlag{
|
|
||||||
Name: "connect",
|
|
||||||
Usage: "sign event using NIP-46, expects a bunker://... URL",
|
|
||||||
Category: CATEGORY_SIGNER,
|
|
||||||
},
|
|
||||||
&cli.StringFlag{
|
|
||||||
Name: "connect-as",
|
|
||||||
Usage: "private key to when communicating with the bunker given on --connect",
|
|
||||||
DefaultText: "a random key",
|
|
||||||
Category: CATEGORY_SIGNER,
|
|
||||||
},
|
|
||||||
// ~ these args are only for the convoluted musig2 signing process
|
// ~ these args are only for the convoluted musig2 signing process
|
||||||
// they will be generally copy-shared-pasted across some manual coordination method between participants
|
// they will be generally copy-shared-pasted across some manual coordination method between participants
|
||||||
&cli.UintFlag{
|
&cli.UintFlag{
|
||||||
@@ -89,7 +68,7 @@ example:
|
|||||||
// ~~~
|
// ~~~
|
||||||
&cli.UintFlag{
|
&cli.UintFlag{
|
||||||
Name: "pow",
|
Name: "pow",
|
||||||
Usage: "NIP-13 difficulty to target when doing hash work on the event id",
|
Usage: "nip13 difficulty to target when doing hash work on the event id",
|
||||||
Category: CATEGORY_EXTRAS,
|
Category: CATEGORY_EXTRAS,
|
||||||
},
|
},
|
||||||
&cli.BoolFlag{
|
&cli.BoolFlag{
|
||||||
@@ -99,7 +78,7 @@ example:
|
|||||||
},
|
},
|
||||||
&cli.BoolFlag{
|
&cli.BoolFlag{
|
||||||
Name: "auth",
|
Name: "auth",
|
||||||
Usage: "always perform NIP-42 \"AUTH\" when facing an \"auth-required: \" rejection and try again",
|
Usage: "always perform nip42 \"AUTH\" when facing an \"auth-required: \" rejection and try again",
|
||||||
Category: CATEGORY_EXTRAS,
|
Category: CATEGORY_EXTRAS,
|
||||||
},
|
},
|
||||||
&cli.BoolFlag{
|
&cli.BoolFlag{
|
||||||
@@ -118,7 +97,7 @@ example:
|
|||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: "content",
|
Name: "content",
|
||||||
Aliases: []string{"c"},
|
Aliases: []string{"c"},
|
||||||
Usage: "event content",
|
Usage: "event content (if it starts with an '@' will read from a file)",
|
||||||
DefaultText: "hello from the nostr army knife",
|
DefaultText: "hello from the nostr army knife",
|
||||||
Value: "",
|
Value: "",
|
||||||
Category: CATEGORY_EVENT_FIELDS,
|
Category: CATEGORY_EVENT_FIELDS,
|
||||||
@@ -126,7 +105,7 @@ example:
|
|||||||
&cli.StringSliceFlag{
|
&cli.StringSliceFlag{
|
||||||
Name: "tag",
|
Name: "tag",
|
||||||
Aliases: []string{"t"},
|
Aliases: []string{"t"},
|
||||||
Usage: "sets a tag field on the event, takes a value like -t e=<id>",
|
Usage: "sets a tag field on the event, takes a value like -t e=<id> or -t sometag=\"value one;value two;value three\"",
|
||||||
Category: CATEGORY_EVENT_FIELDS,
|
Category: CATEGORY_EVENT_FIELDS,
|
||||||
},
|
},
|
||||||
&cli.StringSliceFlag{
|
&cli.StringSliceFlag{
|
||||||
@@ -152,61 +131,73 @@ example:
|
|||||||
Value: nostr.Now(),
|
Value: nostr.Now(),
|
||||||
Category: CATEGORY_EVENT_FIELDS,
|
Category: CATEGORY_EVENT_FIELDS,
|
||||||
},
|
},
|
||||||
},
|
&cli.BoolFlag{
|
||||||
|
Name: "confirm",
|
||||||
|
Usage: "ask before publishing the event",
|
||||||
|
Category: CATEGORY_EXTRAS,
|
||||||
|
},
|
||||||
|
),
|
||||||
ArgsUsage: "[relay...]",
|
ArgsUsage: "[relay...]",
|
||||||
Action: func(ctx context.Context, c *cli.Command) error {
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
// try to connect to the relays here
|
// try to connect to the relays here
|
||||||
var relays []*nostr.Relay
|
var relays []*nostr.Relay
|
||||||
|
|
||||||
if relayUrls := c.Args().Slice(); len(relayUrls) > 0 {
|
if relayUrls := c.Args().Slice(); len(relayUrls) > 0 {
|
||||||
_, relays = connectToAllRelays(ctx, relayUrls, false)
|
relays = connectToAllRelays(ctx, c, relayUrls, nil,
|
||||||
|
nostr.PoolOptions{
|
||||||
|
AuthHandler: func(ctx context.Context, authEvent *nostr.Event) error {
|
||||||
|
return authSigner(ctx, c, func(s string, args ...any) {}, authEvent)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
if len(relays) == 0 {
|
if len(relays) == 0 {
|
||||||
log("failed to connect to any of the given relays.\n")
|
log("failed to connect to any of the given relays.\n")
|
||||||
os.Exit(3)
|
os.Exit(3)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
kr, sec, err := gatherKeyerFromArguments(ctx, c)
|
||||||
defer func() {
|
|
||||||
for _, relay := range relays {
|
|
||||||
relay.Close()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
sec, bunker, err := gatherSecretKeyOrBunkerFromArguments(ctx, c)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
doAuth := c.Bool("auth")
|
// then process input and generate events:
|
||||||
|
|
||||||
// then process input and generate events
|
// will reuse this
|
||||||
for stdinEvent := range getStdinLinesOrBlank() {
|
var evt nostr.Event
|
||||||
evt := nostr.Event{
|
|
||||||
Tags: make(nostr.Tags, 0, 3),
|
|
||||||
}
|
|
||||||
|
|
||||||
kindWasSupplied := false
|
// this is called when we have a valid json from stdin
|
||||||
|
handleEvent := func(stdinEvent string) error {
|
||||||
|
evt.Content = ""
|
||||||
|
|
||||||
|
kindWasSupplied := strings.Contains(stdinEvent, `"kind"`)
|
||||||
|
contentWasSupplied := strings.Contains(stdinEvent, `"content"`)
|
||||||
mustRehashAndResign := false
|
mustRehashAndResign := false
|
||||||
|
|
||||||
if stdinEvent != "" {
|
if err := easyjson.Unmarshal([]byte(stdinEvent), &evt); err != nil {
|
||||||
if err := easyjson.Unmarshal([]byte(stdinEvent), &evt); err != nil {
|
return fmt.Errorf("invalid event received from stdin: %s", err)
|
||||||
ctx = lineProcessingError(ctx, "invalid event received from stdin: %s", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
kindWasSupplied = strings.Contains(stdinEvent, `"kind"`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if kind := c.Uint("kind"); slices.Contains(c.FlagNames(), "kind") {
|
if kind := c.Uint("kind"); slices.Contains(c.FlagNames(), "kind") {
|
||||||
evt.Kind = int(kind)
|
evt.Kind = nostr.Kind(kind)
|
||||||
mustRehashAndResign = true
|
mustRehashAndResign = true
|
||||||
} else if !kindWasSupplied {
|
} else if !kindWasSupplied {
|
||||||
evt.Kind = 1
|
evt.Kind = 1
|
||||||
mustRehashAndResign = true
|
mustRehashAndResign = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if content := c.String("content"); content != "" {
|
if c.IsSet("content") {
|
||||||
evt.Content = content
|
content := c.String("content")
|
||||||
|
if strings.HasPrefix(content, "@") {
|
||||||
|
filedata, err := os.ReadFile(content[1:])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read file '%s' for content: %w", content[1:], err)
|
||||||
|
}
|
||||||
|
evt.Content = string(filedata)
|
||||||
|
} else {
|
||||||
|
evt.Content = content
|
||||||
|
}
|
||||||
mustRehashAndResign = true
|
mustRehashAndResign = true
|
||||||
} else if evt.Content == "" && evt.Kind == 1 {
|
} else if !contentWasSupplied && evt.Content == "" && evt.Kind == 1 {
|
||||||
evt.Content = "hello from the nostr army knife"
|
evt.Content = "hello from the nostr army knife"
|
||||||
mustRehashAndResign = true
|
mustRehashAndResign = true
|
||||||
}
|
}
|
||||||
@@ -226,13 +217,19 @@ example:
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, etag := range c.StringSlice("e") {
|
for _, etag := range c.StringSlice("e") {
|
||||||
tags = tags.AppendUnique([]string{"e", etag})
|
if tags.FindWithValue("e", etag) == nil {
|
||||||
|
tags = append(tags, nostr.Tag{"e", etag})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for _, ptag := range c.StringSlice("p") {
|
for _, ptag := range c.StringSlice("p") {
|
||||||
tags = tags.AppendUnique([]string{"p", ptag})
|
if tags.FindWithValue("p", ptag) == nil {
|
||||||
|
tags = append(tags, nostr.Tag{"p", ptag})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for _, dtag := range c.StringSlice("d") {
|
for _, dtag := range c.StringSlice("d") {
|
||||||
tags = tags.AppendUnique([]string{"d", dtag})
|
if tags.FindWithValue("d", dtag) == nil {
|
||||||
|
tags = append(tags, nostr.Tag{"d", dtag})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if len(tags) > 0 {
|
if len(tags) > 0 {
|
||||||
for _, tag := range tags {
|
for _, tag := range tags {
|
||||||
@@ -249,14 +246,13 @@ example:
|
|||||||
mustRehashAndResign = true
|
mustRehashAndResign = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if c.IsSet("musig") || c.IsSet("sec") || c.IsSet("prompt-sec") {
|
||||||
|
mustRehashAndResign = true
|
||||||
|
}
|
||||||
|
|
||||||
if difficulty := c.Uint("pow"); difficulty > 0 {
|
if difficulty := c.Uint("pow"); difficulty > 0 {
|
||||||
// before doing pow we need the pubkey
|
// before doing pow we need the pubkey
|
||||||
if bunker != nil {
|
if numSigners := c.Uint("musig"); numSigners > 1 {
|
||||||
evt.PubKey, err = bunker.GetPublicKey(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't pow: failed to get public key from bunker: %w", err)
|
|
||||||
}
|
|
||||||
} else if numSigners := c.Uint("musig"); numSigners > 1 && sec != "" {
|
|
||||||
pubkeys := c.StringSlice("musig-pubkey")
|
pubkeys := c.StringSlice("musig-pubkey")
|
||||||
if int(numSigners) != len(pubkeys) {
|
if int(numSigners) != len(pubkeys) {
|
||||||
return fmt.Errorf("when doing a pow with musig we must know all signer pubkeys upfront")
|
return fmt.Errorf("when doing a pow with musig we must know all signer pubkeys upfront")
|
||||||
@@ -266,20 +262,19 @@ example:
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
evt.PubKey, _ = nostr.GetPublicKey(sec)
|
evt.PubKey, _ = kr.GetPublicKey(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
// try to generate work with this difficulty -- essentially forever
|
// try to generate work with this difficulty -- runs forever
|
||||||
nip13.Generate(&evt, int(difficulty), time.Hour*24*365)
|
nonceTag, _ := nip13.DoWork(ctx, evt, int(difficulty))
|
||||||
|
evt.Tags = append(evt.Tags, nonceTag)
|
||||||
|
|
||||||
mustRehashAndResign = true
|
mustRehashAndResign = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if evt.Sig == "" || mustRehashAndResign {
|
if evt.Sig == [64]byte{} || mustRehashAndResign {
|
||||||
if bunker != nil {
|
if numSigners := c.Uint("musig"); numSigners > 1 {
|
||||||
if err := bunker.SignEvent(ctx, &evt); err != nil {
|
// must do musig
|
||||||
return fmt.Errorf("failed to sign with bunker: %w", err)
|
|
||||||
}
|
|
||||||
} else if numSigners := c.Uint("musig"); numSigners > 1 && sec != "" {
|
|
||||||
pubkeys := c.StringSlice("musig-pubkey")
|
pubkeys := c.StringSlice("musig-pubkey")
|
||||||
secNonce := c.String("musig-nonce-secret")
|
secNonce := c.String("musig-nonce-secret")
|
||||||
pubNonces := c.StringSlice("musig-nonce")
|
pubNonces := c.StringSlice("musig-nonce")
|
||||||
@@ -294,7 +289,10 @@ example:
|
|||||||
// instructions for what to do should have been printed by the performMusig() function
|
// instructions for what to do should have been printed by the performMusig() function
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
} else if err := evt.Sign(sec); err != nil {
|
} else if err := kr.SignEvent(ctx, &evt); err != nil {
|
||||||
|
if _, isBunker := kr.(keyer.BunkerSigner); isBunker && errors.Is(ctx.Err(), context.DeadlineExceeded) {
|
||||||
|
err = fmt.Errorf("timeout waiting for bunker to respond")
|
||||||
|
}
|
||||||
return fmt.Errorf("error signing with provided key: %w", err)
|
return fmt.Errorf("error signing with provided key: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -310,56 +308,12 @@ example:
|
|||||||
}
|
}
|
||||||
stdout(result)
|
stdout(result)
|
||||||
|
|
||||||
// publish to relays
|
return publishFlow(ctx, c, kr, evt, relays)
|
||||||
successRelays := make([]string, 0, len(relays))
|
}
|
||||||
if len(relays) > 0 {
|
|
||||||
os.Stdout.Sync()
|
|
||||||
for _, relay := range relays {
|
|
||||||
publish:
|
|
||||||
log("publishing to %s... ", relay.URL)
|
|
||||||
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
err := relay.Publish(ctx, evt)
|
for stdinEvent := range getJsonsOrBlank() {
|
||||||
if err == nil {
|
if err := handleEvent(stdinEvent); err != nil {
|
||||||
// published fine
|
ctx = lineProcessingError(ctx, err.Error())
|
||||||
log("success.\n")
|
|
||||||
successRelays = append(successRelays, relay.URL)
|
|
||||||
continue // continue to next relay
|
|
||||||
}
|
|
||||||
|
|
||||||
// error publishing
|
|
||||||
if strings.HasPrefix(err.Error(), "msg: auth-required:") && (sec != "" || bunker != nil) && doAuth {
|
|
||||||
// if the relay is requesting auth and we can auth, let's do it
|
|
||||||
var pk string
|
|
||||||
if bunker != nil {
|
|
||||||
pk, err = bunker.GetPublicKey(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get public key from bunker: %w", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
pk, _ = nostr.GetPublicKey(sec)
|
|
||||||
}
|
|
||||||
log("performing auth as %s... ", pk)
|
|
||||||
if err := relay.Auth(ctx, func(evt *nostr.Event) error {
|
|
||||||
if bunker != nil {
|
|
||||||
return bunker.SignEvent(ctx, evt)
|
|
||||||
}
|
|
||||||
return evt.Sign(sec)
|
|
||||||
}); err == nil {
|
|
||||||
// try to publish again, but this time don't try to auth again
|
|
||||||
doAuth = false
|
|
||||||
goto publish
|
|
||||||
} else {
|
|
||||||
log("auth error: %s. ", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log("failed: %s\n", err)
|
|
||||||
}
|
|
||||||
if len(successRelays) > 0 && c.Bool("nevent") {
|
|
||||||
nevent, _ := nip19.EncodeEvent(evt.ID, successRelays, evt.PubKey)
|
|
||||||
log(nevent + "\n")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -367,3 +321,125 @@ example:
|
|||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func publishFlow(ctx context.Context, c *cli.Command, kr nostr.Signer, evt nostr.Event, relays []*nostr.Relay) error {
|
||||||
|
doAuth := c.Bool("auth")
|
||||||
|
|
||||||
|
// publish to relays
|
||||||
|
successRelays := make([]string, 0, len(relays))
|
||||||
|
if len(relays) > 0 {
|
||||||
|
os.Stdout.Sync()
|
||||||
|
|
||||||
|
if c.Bool("confirm") {
|
||||||
|
relaysStr := make([]string, len(relays))
|
||||||
|
for i, r := range relays {
|
||||||
|
relaysStr[i] = strings.ToLower(strings.Split(r.URL, "://")[1])
|
||||||
|
}
|
||||||
|
time.Sleep(time.Millisecond * 10)
|
||||||
|
if !askConfirmation("publish to [ " + strings.Join(relaysStr, " ") + " ]? ") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if supportsDynamicMultilineMagic() {
|
||||||
|
// overcomplicated multiline rendering magic
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
urls := make([]string, len(relays))
|
||||||
|
lines := make([][][]byte, len(urls))
|
||||||
|
flush := func() {
|
||||||
|
for _, line := range lines {
|
||||||
|
for _, part := range line {
|
||||||
|
os.Stderr.Write(part)
|
||||||
|
}
|
||||||
|
os.Stderr.Write([]byte{'\n'})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
render := func() {
|
||||||
|
clearLines(len(lines))
|
||||||
|
flush()
|
||||||
|
}
|
||||||
|
flush()
|
||||||
|
|
||||||
|
logthis := func(relayUrl, s string, args ...any) {
|
||||||
|
idx := slices.Index(urls, relayUrl)
|
||||||
|
lines[idx] = append(lines[idx], []byte(fmt.Sprintf(s, args...)))
|
||||||
|
render()
|
||||||
|
}
|
||||||
|
colorizethis := func(relayUrl string, colorize func(string, ...any) string) {
|
||||||
|
cleanUrl, _ := strings.CutPrefix(relayUrl, "wss://")
|
||||||
|
idx := slices.Index(urls, relayUrl)
|
||||||
|
lines[idx][0] = []byte(fmt.Sprintf("publishing to %s... ", colorize(cleanUrl)))
|
||||||
|
render()
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, relay := range relays {
|
||||||
|
urls[i] = relay.URL
|
||||||
|
lines[i] = make([][]byte, 1, 3)
|
||||||
|
colorizethis(relay.URL, color.CyanString)
|
||||||
|
}
|
||||||
|
render()
|
||||||
|
|
||||||
|
for res := range sys.Pool.PublishMany(ctx, urls, evt) {
|
||||||
|
if res.Error == nil {
|
||||||
|
colorizethis(res.RelayURL, colors.successf)
|
||||||
|
logthis(res.RelayURL, "success.")
|
||||||
|
successRelays = append(successRelays, res.RelayURL)
|
||||||
|
} else {
|
||||||
|
colorizethis(res.RelayURL, colors.errorf)
|
||||||
|
|
||||||
|
// in this case it's likely that the lowest-level error is the one that will be more helpful
|
||||||
|
low := unwrapAll(res.Error)
|
||||||
|
|
||||||
|
// hack for some messages such as from relay.westernbtc.com
|
||||||
|
msg := strings.ReplaceAll(low.Error(), evt.PubKey.Hex(), "author")
|
||||||
|
|
||||||
|
// do not allow the message to overflow the term window
|
||||||
|
msg = clampMessage(msg, 20+len(res.RelayURL))
|
||||||
|
|
||||||
|
logthis(res.RelayURL, msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// normal dumb flow
|
||||||
|
for _, relay := range relays {
|
||||||
|
publish:
|
||||||
|
cleanUrl, _ := strings.CutPrefix(relay.URL, "wss://")
|
||||||
|
log("publishing to %s... ", color.CyanString(cleanUrl))
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
err := relay.Publish(ctx, evt)
|
||||||
|
if err == nil {
|
||||||
|
// published fine
|
||||||
|
log("success.\n")
|
||||||
|
successRelays = append(successRelays, relay.URL)
|
||||||
|
continue // continue to next relay
|
||||||
|
}
|
||||||
|
|
||||||
|
// error publishing
|
||||||
|
if strings.HasPrefix(err.Error(), "msg: auth-required:") && kr != nil && doAuth {
|
||||||
|
// if the relay is requesting auth and we can auth, let's do it
|
||||||
|
pk, _ := kr.GetPublicKey(ctx)
|
||||||
|
npub := nip19.EncodeNpub(pk)
|
||||||
|
log("authenticating as %s... ", color.YellowString("%s…%s", npub[0:7], npub[58:]))
|
||||||
|
if err := relay.Auth(ctx, kr.SignEvent); err == nil {
|
||||||
|
// try to publish again, but this time don't try to auth again
|
||||||
|
doAuth = false
|
||||||
|
goto publish
|
||||||
|
} else {
|
||||||
|
log("auth error: %s. ", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log("failed: %s\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(successRelays) > 0 && c.Bool("nevent") {
|
||||||
|
log(nip19.EncodeNevent(evt.ID, successRelays, evt.PubKey) + "\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
124
example_test.go
124
example_test.go
@@ -1,124 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
)
|
|
||||||
|
|
||||||
// these tests are tricky because commands and flags are declared as globals and values set in one call may persist
|
|
||||||
// to the next. for example, if in the first test we set --limit 2 then doesn't specify --limit in the second then
|
|
||||||
// it will still return true for cmd.IsSet("limit") and then we will set .LimitZero = true
|
|
||||||
|
|
||||||
var ctx = context.Background()
|
|
||||||
|
|
||||||
func ExampleEventBasic() {
|
|
||||||
app.Run(ctx, []string{"nak", "event", "--ts", "1699485669"})
|
|
||||||
// Output:
|
|
||||||
// {"kind":1,"id":"36d88cf5fcc449f2390a424907023eda7a74278120eebab8d02797cd92e7e29c","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1699485669,"tags":[],"content":"hello from the nostr army knife","sig":"68e71a192e8abcf8582a222434ac823ecc50607450ebe8cc4c145eb047794cc382dc3f888ce879d2f404f5ba6085a47601360a0fa2dd4b50d317bd0c6197c2c2"}
|
|
||||||
}
|
|
||||||
|
|
||||||
// (for some reason there can only be one test dealing with stdin in the suite otherwise it halts)
|
|
||||||
// func ExampleEventParsingFromStdin() {
|
|
||||||
// prevStdin := os.Stdin
|
|
||||||
// defer func() { os.Stdin = prevStdin }()
|
|
||||||
// r, w, _ := os.Pipe()
|
|
||||||
// os.Stdin = r
|
|
||||||
// w.WriteString("{\"content\":\"hello world\"}\n{\"content\":\"hello sun\"}\n")
|
|
||||||
// app.Run(ctx, []string{"nak", "event", "-t", "t=spam", "--ts", "1699485669"})
|
|
||||||
// // Output:
|
|
||||||
// // {"id":"bda134f9077c11973afe6aa5a1cc6f5bcea01c40d318b8f91dcb8e50507cfa52","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1699485669,"kind":1,"tags":[["t","spam"]],"content":"hello world","sig":"7552454bb8e7944230142634e3e34ac7468bad9b21ed6909da572c611018dff1d14d0792e98b5806f6330edc51e09efa6d0b66a9694dc34606c70f4e580e7493"}
|
|
||||||
// // {"id":"879c36ec73acca288825b53585389581d3836e7f0fe4d46e5eba237ca56d6af5","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1699485669,"kind":1,"tags":[["t","spam"]],"content":"hello sun","sig":"6c7e6b13ebdf931d26acfdd00bec2ec1140ddaf8d1ed61453543a14e729a460fe36c40c488ccb194a0e1ab9511cb6c36741485f501bdb93c39ca4c51bc59cbd4"}
|
|
||||||
// }
|
|
||||||
|
|
||||||
func ExampleEventComplex() {
|
|
||||||
app.Run(ctx, []string{"nak", "event", "--ts", "1699485669", "-k", "11", "-c", "skjdbaskd", "--sec", "17", "-t", "t=spam", "-e", "36d88cf5fcc449f2390a424907023eda7a74278120eebab8d02797cd92e7e29c", "-t", "r=https://abc.def?name=foobar;nothing"})
|
|
||||||
// Output:
|
|
||||||
// {"kind":11,"id":"19aba166dcf354bf5ef64f4afe69ada1eb851495001ee05e07d393ee8c8ea179","pubkey":"2fa2104d6b38d11b0230010559879124e42ab8dfeff5ff29dc9cdadd4ecacc3f","created_at":1699485669,"tags":[["t","spam"],["r","https://abc.def?name=foobar","nothing"],["e","36d88cf5fcc449f2390a424907023eda7a74278120eebab8d02797cd92e7e29c"]],"content":"skjdbaskd","sig":"cf452def4a68341c897c3fc96fa34dc6895a5b8cc266d4c041bcdf758ec992ec5adb8b0179e98552aaaf9450526a26d7e62e413b15b1c57e0cfc8db6b29215d7"}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExampleEncode() {
|
|
||||||
app.Run(ctx, []string{"nak", "encode", "npub", "a6a67ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179822"})
|
|
||||||
app.Run(ctx, []string{"nak", "encode", "nprofile", "-r", "wss://example.com", "a6a67ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179822"})
|
|
||||||
app.Run(ctx, []string{"nak", "encode", "nprofile", "-r", "wss://example.com", "a6a67ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179822", "a5592173975ded9f836a9572ea8b11a7e16ceb66464d66d50b27163f7f039d2c"})
|
|
||||||
// npub156n8a7wuhwk9tgrzjh8gwzc8q2dlekedec5djk0js9d3d7qhnq3qjpdq28
|
|
||||||
// nprofile1qqs2dfn7l8wthtz45p3ftn58pvrs9xlumvkuu2xet8egzkcklqtesgspz9mhxue69uhk27rpd4cxcefwvdhk6fl5jug
|
|
||||||
// nprofile1qqs2dfn7l8wthtz45p3ftn58pvrs9xlumvkuu2xet8egzkcklqtesgspz9mhxue69uhk27rpd4cxcefwvdhk6fl5jug
|
|
||||||
// nprofile1qqs22kfpwwt4mmvlsd4f2uh23vg60ctvadnyvntx659jw93l0upe6tqpz9mhxue69uhk27rpd4cxcefwvdhk64h265a
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExampleDecode() {
|
|
||||||
app.Run(ctx, []string{"nak", "decode", "naddr1qqyrgcmyxe3kvefhqyxhwumn8ghj7mn0wvhxcmmvqgs9kqvr4dkruv3t7n2pc6e6a7v9v2s5fprmwjv4gde8c4fe5y29v0srqsqqql9ngrt6tu", "nevent1qyd8wumn8ghj7urewfsk66ty9enxjct5dfskvtnrdakj7qgmwaehxw309aex2mrp0yh8wetnw3jhymnzw33jucm0d5hszxthwden5te0wfjkccte9eekummjwsh8xmmrd9skctcpzamhxue69uhkzarvv9ejumn0wd68ytnvv9hxgtcqyqllp5v5j0nxr74fptqxkhvfv0h3uj870qpk3ln8a58agyxl3fka296ewr8"})
|
|
||||||
// Output:
|
|
||||||
// {
|
|
||||||
// "pubkey": "5b0183ab6c3e322bf4d41c6b3aef98562a144847b7499543727c5539a114563e",
|
|
||||||
// "kind": 31923,
|
|
||||||
// "identifier": "4cd6cfe7",
|
|
||||||
// "relays": [
|
|
||||||
// "wss://nos.lol"
|
|
||||||
// ]
|
|
||||||
// }
|
|
||||||
// {
|
|
||||||
// "id": "3ff0d19493e661faa90ac06b5d8963ef1e48fe780368fe67ed0fd410df8a6dd5",
|
|
||||||
// "relays": [
|
|
||||||
// "wss://pyramid.fiatjaf.com/",
|
|
||||||
// "wss://relay.westernbtc.com/",
|
|
||||||
// "wss://relay.snort.social/",
|
|
||||||
// "wss://atlas.nostr.land/"
|
|
||||||
// ]
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExampleReq() {
|
|
||||||
app.Run(ctx, []string{"nak", "req", "-k", "1", "-l", "18", "-a", "2fa2104d6b38d11b0230010559879124e42ab8dfeff5ff29dc9cdadd4ecacc3f", "-e", "aec4de6d051a7c2b6ca2d087903d42051a31e07fb742f1240970084822de10a6"})
|
|
||||||
// Output:
|
|
||||||
// ["REQ","nak",{"kinds":[1],"authors":["2fa2104d6b38d11b0230010559879124e42ab8dfeff5ff29dc9cdadd4ecacc3f"],"limit":18,"#e":["aec4de6d051a7c2b6ca2d087903d42051a31e07fb742f1240970084822de10a6"]}]
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExampleMultipleFetch() {
|
|
||||||
app.Run(ctx, []string{"nak", "fetch", "naddr1qqyrgcmyxe3kvefhqyxhwumn8ghj7mn0wvhxcmmvqgs9kqvr4dkruv3t7n2pc6e6a7v9v2s5fprmwjv4gde8c4fe5y29v0srqsqqql9ngrt6tu", "nevent1qyd8wumn8ghj7urewfsk66ty9enxjct5dfskvtnrdakj7qgmwaehxw309aex2mrp0yh8wetnw3jhymnzw33jucm0d5hszxthwden5te0wfjkccte9eekummjwsh8xmmrd9skctcpzamhxue69uhkzarvv9ejumn0wd68ytnvv9hxgtcqyqllp5v5j0nxr74fptqxkhvfv0h3uj870qpk3ln8a58agyxl3fka296ewr8"})
|
|
||||||
// Output:
|
|
||||||
// {"kind":31923,"id":"9ae5014573fc75ced00b343868d2cd9343ebcbbae50591c6fa8ae1cd99568f05","pubkey":"5b0183ab6c3e322bf4d41c6b3aef98562a144847b7499543727c5539a114563e","created_at":1707764605,"tags":[["d","4cd6cfe7"],["name","Nostr PHX Presents Culture Shock"],["description","Nostr PHX presents Culture Shock the first Value 4 Value Cultural Event in Downtown Phoenix. We will showcase the power of Nostr + Bitcoin / Lightning with a full day of education, food, drinks, conversation, vendors and best of all, a live convert which will stream globally for the world to zap. "],["start","1708185600"],["end","1708228800"],["start_tzid","America/Phoenix"],["p","5b0183ab6c3e322bf4d41c6b3aef98562a144847b7499543727c5539a114563e","","host"],["location","Hello Merch, 850 W Lincoln St, Phoenix, AZ 85007, USA","Hello Merch","850 W Lincoln St, Phoenix, AZ 85007, USA"],["address","Hello Merch, 850 W Lincoln St, Phoenix, AZ 85007, USA","Hello Merch","850 W Lincoln St, Phoenix, AZ 85007, USA"],["g","9tbq1rzn"],["image","https://flockstr.s3.amazonaws.com/event/15vSaiscDhVH1KBXhA0i8"],["about","Nostr PHX presents Culture Shock : the first Value 4 Value Cultural Event in Downtown Phoenix. We will showcase the power of Nostr + Bitcoin / Lightning with a full day of education, conversation, food and goods which will be capped off with a live concert streamed globally for the world to boost \u0026 zap. \n\nWe strive to source local vendors, local artists, local partnerships. Please reach out to us if you are interested in participating in this historic event. "],["calendar","31924:5b0183ab6c3e322bf4d41c6b3aef98562a144847b7499543727c5539a114563e:1f238c94"]],"content":"Nostr PHX presents Culture Shock : the first Value 4 Value Cultural Event in Downtown Phoenix. We will showcase the power of Nostr + Bitcoin / Lightning with a full day of education, conversation, food and goods which will be capped off with a live concert streamed globally for the world to boost \u0026 zap. \n\nWe strive to source local vendors, local artists, local partnerships. Please reach out to us if you are interested in participating in this historic event. ","sig":"f676629d1414d96b464644de6babde0c96bd21ef9b41ba69ad886a1d13a942b855b715b22ccf38bc07fead18d3bdeee82d9e3825cf6f003fb5ff1766d95c70a0"}
|
|
||||||
// {"kind":1,"id":"3ff0d19493e661faa90ac06b5d8963ef1e48fe780368fe67ed0fd410df8a6dd5","pubkey":"3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d","created_at":1710759386,"tags":[],"content":"Nostr was coopted by our the corporate overlords. It is now featured in https://www.iana.org/assignments/well-known-uris/well-known-uris.xhtml.","sig":"faaec167cca4de50b562b7702e8854e2023f0ccd5f36d1b95b6eac20d352206342d6987e9516d283068c768e94dbe8858e2990c3e05405e707fb6fb771ef92f9"}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExampleKeyPublic() {
|
|
||||||
app.Run(ctx, []string{"nak", "key", "public", "3ff0d19493e661faa90ac06b5d8963ef1e48fe780368fe67ed0fd410df8a6dd5", "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"})
|
|
||||||
// Output:
|
|
||||||
// 70f7120d065870513a6bddb61c8d400ad1e43449b1900ffdb5551e4c421375c8
|
|
||||||
// 718d756f60cf5179ef35b39dc6db3ff58f04c0734f81f6d4410f0b047ddf9029
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExampleKeyDecrypt() {
|
|
||||||
app.Run(ctx, []string{"nak", "key", "decrypt", "ncryptsec1qgg2gx2a7hxpsse2zulrv7m8qwccvl3mh8e9k8vtz3wpyrwuuclaq73gz7ddt5kpa93qyfhfjakguuf8uhw90jn6mszh7kqeh9mxzlyw8hy75fluzx4h75frwmu2yngsq7hx7w32d0vdyxyns5g6rqft", "banana"})
|
|
||||||
// Output:
|
|
||||||
// 718d756f60cf5179ef35b39dc6db3ff58f04c0734f81f6d4410f0b047ddf9029
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExampleReqIdFromRelay() {
|
|
||||||
app.Run(ctx, []string{"nak", "req", "-i", "20a6606ed548fe7107533cf3416ce1aa5e957c315c2a40249e12bd9873dca7da", "--limit", "1", "nos.lol"})
|
|
||||||
// Output:
|
|
||||||
// {"kind":1,"id":"20a6606ed548fe7107533cf3416ce1aa5e957c315c2a40249e12bd9873dca7da","pubkey":"dd664d5e4016433a8cd69f005ae1480804351789b59de5af06276de65633d319","created_at":1720972243,"tags":[["e","bdb2210fe6d9c4b141f08b5d9d1147cd8e1dc1d82f552a889ab171894249d21d","","root"],["e","c2e45f09e7d62ed12afe2b8b1bcf6be823b560a53ef06905365a78979a1b9ee3","","reply"],["p","036533caa872376946d4e4fdea4c1a0441eda38ca2d9d9417bb36006cbaabf58","","mention"]],"content":"Yeah, so bizarre, but I guess most people are meant to be serfs.","sig":"9ea7488415c250d0ac8fcb2219f211cb369dddf2a75c0f63d2db773c6dc1ef9dd9679b8941c0e7551744ea386afebad2024be8ce3ac418d4f47c95e7491af38e"}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExampleReqWithFlagsAfter1() {
|
|
||||||
app.Run(ctx, []string{"nak", "req", "nos.lol", "-i", "20a6606ed548fe7107533cf3416ce1aa5e957c315c2a40249e12bd9873dca7da", "--limit", "1"})
|
|
||||||
// Output:
|
|
||||||
// {"kind":1,"id":"20a6606ed548fe7107533cf3416ce1aa5e957c315c2a40249e12bd9873dca7da","pubkey":"dd664d5e4016433a8cd69f005ae1480804351789b59de5af06276de65633d319","created_at":1720972243,"tags":[["e","bdb2210fe6d9c4b141f08b5d9d1147cd8e1dc1d82f552a889ab171894249d21d","","root"],["e","c2e45f09e7d62ed12afe2b8b1bcf6be823b560a53ef06905365a78979a1b9ee3","","reply"],["p","036533caa872376946d4e4fdea4c1a0441eda38ca2d9d9417bb36006cbaabf58","","mention"]],"content":"Yeah, so bizarre, but I guess most people are meant to be serfs.","sig":"9ea7488415c250d0ac8fcb2219f211cb369dddf2a75c0f63d2db773c6dc1ef9dd9679b8941c0e7551744ea386afebad2024be8ce3ac418d4f47c95e7491af38e"}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExampleReqWithFlagsAfter2() {
|
|
||||||
app.Run(ctx, []string{"nak", "req", "-e", "893d4c10f1c230240812c6bdf9ad877eed1e29e87029d153820c24680bb183b1", "nostr.mom", "--author", "2a7dcf382bcc96a393ada5c975f500393b3f7be6e466bff220aa161ad6b15eb6", "--limit", "1", "-k", "7"})
|
|
||||||
// Output:
|
|
||||||
// {"kind":7,"id":"9b4868b068ea34ae51092807586c4541b3569d9efc23862aea48ef13de275857","pubkey":"2a7dcf382bcc96a393ada5c975f500393b3f7be6e466bff220aa161ad6b15eb6","created_at":1720987327,"tags":[["e","893d4c10f1c230240812c6bdf9ad877eed1e29e87029d153820c24680bb183b1"],["p","1e978baae414eee990dba992871549ad4a099b9d6f7e71c8059b254ea024dddc"],["k","1"]],"content":"❤️","sig":"7eddd112c642ecdb031330dadc021790642b3c10ecc64158ba3ae63edd798b26afb9b5a3bba72835ce171719a724de1472f65c9b3339b6bead0ce2846f93dfc9"}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExampleReqWithFlagsAfter3() {
|
|
||||||
app.Run(ctx, []string{"nak", "req", "--limit", "1", "pyramid.fiatjaf.com", "-a", "3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24", "-qp", "3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24", "-e", "9f3c1121c96edf17d84b9194f74d66d012b28c4e25b3ef190582c76b8546a188"})
|
|
||||||
// Output:
|
|
||||||
// {"kind":1,"id":"101572c80ebdc963dab8440f6307387a3023b6d90f7e495d6c5ee1ef77045a67","pubkey":"3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24","created_at":1720987305,"tags":[["e","ceacdc29fa7a0b51640b30d2424e188215460617db5ba5bb52d3fbf0094eebb3","","root"],["e","9f3c1121c96edf17d84b9194f74d66d012b28c4e25b3ef190582c76b8546a188","","reply"],["p","3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24"],["p","6b96c3eb36c6cd457d906bbaafe7b36cacfb8bcc4ab235be6eab3b71c6669251"]],"content":"Nope. I grew up playing in the woods. Never once saw a bear in the woods. If I did, I'd probably shiy my pants, then scream at it like I was a crazy person with my arms above my head to make me seem huge.","sig":"b098820b4a5635865cada9f9a5813be2bc6dd7180e16e590cf30e07916d8ed6ed98ab38b64f3bfba12d88d37335f229f7ef8c084bc48132e936c664a54d3e650"}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExampleNaturalTimestamps() {
|
|
||||||
app.Run(ctx, []string{"nak", "event", "-t", "plu=pla", "-e", "3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24", "--ts", "2018-05-19 03:37:19", "-c", "nn"})
|
|
||||||
// Output:
|
|
||||||
// {"kind":1,"id":"0000d199127d5e15046b0a3f2885d464ee18f70968303665ef76326a7d828312","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1724160467,"tags":[["plu","pla"],["e","3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24"],["nonce","24783","16"]],"content":"nn","sig":"99471b43ce82ca01fb9b61f36b45ca542870854b2466a9d3884891598f7d7baef36d07f4b02bb194f2f6f781973f24c3d946f702c82321c6cb0c564e76cf43db"}
|
|
||||||
}
|
|
||||||
48
fetch.go
48
fetch.go
@@ -2,17 +2,18 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/fiatjaf/cli/v3"
|
"fiatjaf.com/nostr"
|
||||||
"github.com/nbd-wtf/go-nostr"
|
"fiatjaf.com/nostr/nip05"
|
||||||
"github.com/nbd-wtf/go-nostr/nip05"
|
"fiatjaf.com/nostr/nip19"
|
||||||
"github.com/nbd-wtf/go-nostr/nip19"
|
"fiatjaf.com/nostr/sdk/hints"
|
||||||
sdk "github.com/nbd-wtf/nostr-sdk"
|
"github.com/urfave/cli/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
var fetch = &cli.Command{
|
var fetch = &cli.Command{
|
||||||
Name: "fetch",
|
Name: "fetch",
|
||||||
Usage: "fetches events related to the given nip19 or nip05 code from the included relay hints or the author's NIP-65 relays.",
|
Usage: "fetches events related to the given nip19 or nip05 code from the included relay hints or the author's outbox relays.",
|
||||||
Description: `example usage:
|
Description: `example usage:
|
||||||
nak fetch nevent1qqsxrwm0hd3s3fddh4jc2574z3xzufq6qwuyz2rvv3n087zvym3dpaqprpmhxue69uhhqatzd35kxtnjv4kxz7tfdenju6t0xpnej4
|
nak fetch nevent1qqsxrwm0hd3s3fddh4jc2574z3xzufq6qwuyz2rvv3n087zvym3dpaqprpmhxue69uhhqatzd35kxtnjv4kxz7tfdenju6t0xpnej4
|
||||||
echo npub1h8spmtw9m2huyv6v2j2qd5zv956z2zdugl6mgx02f2upffwpm3nqv0j4ps | nak fetch --relay wss://relay.nostr.band`,
|
echo npub1h8spmtw9m2huyv6v2j2qd5zv956z2zdugl6mgx02f2upffwpm3nqv0j4ps | nak fetch --relay wss://relay.nostr.band`,
|
||||||
@@ -26,18 +27,9 @@ var fetch = &cli.Command{
|
|||||||
),
|
),
|
||||||
ArgsUsage: "[nip05_or_nip19_code]",
|
ArgsUsage: "[nip05_or_nip19_code]",
|
||||||
Action: func(ctx context.Context, c *cli.Command) error {
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
sys := sdk.NewSystem()
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
sys.Pool.Relays.Range(func(_ string, relay *nostr.Relay) bool {
|
|
||||||
relay.Close()
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
}()
|
|
||||||
|
|
||||||
for code := range getStdinLinesOrArguments(c.Args()) {
|
for code := range getStdinLinesOrArguments(c.Args()) {
|
||||||
filter := nostr.Filter{}
|
filter := nostr.Filter{}
|
||||||
var authorHint string
|
var authorHint nostr.PubKey
|
||||||
relays := c.StringSlice("relay")
|
relays := c.StringSlice("relay")
|
||||||
|
|
||||||
if nip05.IsValidIdentifier(code) {
|
if nip05.IsValidIdentifier(code) {
|
||||||
@@ -64,12 +56,15 @@ var fetch = &cli.Command{
|
|||||||
case "nevent":
|
case "nevent":
|
||||||
v := value.(nostr.EventPointer)
|
v := value.(nostr.EventPointer)
|
||||||
filter.IDs = append(filter.IDs, v.ID)
|
filter.IDs = append(filter.IDs, v.ID)
|
||||||
if v.Author != "" {
|
if v.Author != nostr.ZeroPK {
|
||||||
authorHint = v.Author
|
authorHint = v.Author
|
||||||
}
|
}
|
||||||
relays = append(relays, v.Relays...)
|
relays = append(relays, v.Relays...)
|
||||||
|
case "note":
|
||||||
|
filter.IDs = append(filter.IDs, value.([32]byte))
|
||||||
case "naddr":
|
case "naddr":
|
||||||
v := value.(nostr.EntityPointer)
|
v := value.(nostr.EntityPointer)
|
||||||
|
filter.Kinds = []nostr.Kind{v.Kind}
|
||||||
filter.Tags = nostr.TagMap{"d": []string{v.Identifier}}
|
filter.Tags = nostr.TagMap{"d": []string{v.Identifier}}
|
||||||
filter.Authors = append(filter.Authors, v.PublicKey)
|
filter.Authors = append(filter.Authors, v.PublicKey)
|
||||||
authorHint = v.PublicKey
|
authorHint = v.PublicKey
|
||||||
@@ -80,20 +75,21 @@ var fetch = &cli.Command{
|
|||||||
authorHint = v.PublicKey
|
authorHint = v.PublicKey
|
||||||
relays = append(relays, v.Relays...)
|
relays = append(relays, v.Relays...)
|
||||||
case "npub":
|
case "npub":
|
||||||
v := value.(string)
|
v := value.(nostr.PubKey)
|
||||||
filter.Authors = append(filter.Authors, v)
|
filter.Authors = append(filter.Authors, v)
|
||||||
authorHint = v
|
authorHint = v
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unexpected prefix %s", prefix)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if authorHint != "" {
|
if authorHint != nostr.ZeroPK {
|
||||||
relays := sys.FetchOutboxRelays(ctx, authorHint, 3)
|
|
||||||
for _, url := range relays {
|
for _, url := range relays {
|
||||||
relays = append(relays, url)
|
sys.Hints.Save(authorHint, nostr.NormalizeURL(url), hints.LastInHint, nostr.Now())
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(filter.Kinds) == 0 {
|
for _, url := range sys.FetchOutboxRelays(ctx, authorHint, 3) {
|
||||||
filter.Kinds = append(filter.Kinds, 0)
|
relays = append(relays, url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,12 +97,16 @@ var fetch = &cli.Command{
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(filter.Authors) > 0 && len(filter.Kinds) == 0 {
|
||||||
|
filter.Kinds = append(filter.Kinds, 0)
|
||||||
|
}
|
||||||
|
|
||||||
if len(relays) == 0 {
|
if len(relays) == 0 {
|
||||||
ctx = lineProcessingError(ctx, "no relay hints found")
|
ctx = lineProcessingError(ctx, "no relay hints found")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
for ie := range sys.Pool.SubManyEose(ctx, relays, nostr.Filters{filter}) {
|
for ie := range sys.Pool.FetchMany(ctx, relays, filter, nostr.SubscriptionOptions{}) {
|
||||||
stdout(ie.Event)
|
stdout(ie.Event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
96
filter.go
Normal file
96
filter.go
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"fiatjaf.com/nostr"
|
||||||
|
"github.com/mailru/easyjson"
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
var filter = &cli.Command{
|
||||||
|
Name: "filter",
|
||||||
|
Usage: "applies an event filter to an event to see if it matches.",
|
||||||
|
Description: `
|
||||||
|
example:
|
||||||
|
echo '{"kind": 1, "content": "hello"}' | nak filter -k 1
|
||||||
|
nak filter '{"kind": 1, "content": "hello"}' -k 1
|
||||||
|
nak filter '{"kind": 1, "content": "hello"}' '{"kinds": [1]}' -k 0
|
||||||
|
`,
|
||||||
|
DisableSliceFlagSeparator: true,
|
||||||
|
Flags: reqFilterFlags,
|
||||||
|
ArgsUsage: "[event_json] [base_filter_json]",
|
||||||
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
|
args := c.Args().Slice()
|
||||||
|
|
||||||
|
var baseFilter nostr.Filter
|
||||||
|
var baseEvent nostr.Event
|
||||||
|
|
||||||
|
if len(args) == 2 {
|
||||||
|
// two arguments: first is event, second is base filter
|
||||||
|
if err := easyjson.Unmarshal([]byte(args[0]), &baseEvent); err != nil {
|
||||||
|
return fmt.Errorf("invalid base event: %w", err)
|
||||||
|
}
|
||||||
|
if err := easyjson.Unmarshal([]byte(args[1]), &baseFilter); err != nil {
|
||||||
|
return fmt.Errorf("invalid base filter: %w", err)
|
||||||
|
}
|
||||||
|
} else if len(args) == 1 {
|
||||||
|
if isPiped() {
|
||||||
|
// one argument + stdin: argument is base filter
|
||||||
|
if err := easyjson.Unmarshal([]byte(args[0]), &baseFilter); err != nil {
|
||||||
|
return fmt.Errorf("invalid base filter: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// one argument, no stdin: argument is event
|
||||||
|
if err := easyjson.Unmarshal([]byte(args[0]), &baseEvent); err != nil {
|
||||||
|
return fmt.Errorf("invalid base event: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// apply flags to filter
|
||||||
|
if err := applyFlagsToFilter(c, &baseFilter); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// if there is no stdin we'll still get an empty object here
|
||||||
|
for evtj := range getJsonsOrBlank() {
|
||||||
|
var evt nostr.Event
|
||||||
|
if err := easyjson.Unmarshal([]byte(evtj), &evt); err != nil {
|
||||||
|
ctx = lineProcessingError(ctx, "invalid event: %s", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// merge that with the base event
|
||||||
|
if evt.ID == nostr.ZeroID {
|
||||||
|
evt.ID = baseEvent.ID
|
||||||
|
}
|
||||||
|
if evt.PubKey == nostr.ZeroPK {
|
||||||
|
evt.PubKey = baseEvent.PubKey
|
||||||
|
}
|
||||||
|
if evt.Sig == [64]byte{} {
|
||||||
|
evt.Sig = baseEvent.Sig
|
||||||
|
}
|
||||||
|
if evt.Content == "" {
|
||||||
|
evt.Content = baseEvent.Content
|
||||||
|
}
|
||||||
|
if len(evt.Tags) == 0 {
|
||||||
|
evt.Tags = baseEvent.Tags
|
||||||
|
}
|
||||||
|
if evt.CreatedAt == 0 {
|
||||||
|
evt.CreatedAt = baseEvent.CreatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
if baseFilter.Matches(evt) {
|
||||||
|
stdout(evt)
|
||||||
|
} else {
|
||||||
|
fmt.Println(baseFilter.LimitZero)
|
||||||
|
logverbose("event %s didn't match %s", evt, baseFilter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exitIfLineProcessingError(ctx)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
136
flags.go
136
flags.go
@@ -6,14 +6,13 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/fiatjaf/cli/v3"
|
"fiatjaf.com/nostr"
|
||||||
"github.com/markusmobius/go-dateparser"
|
"github.com/markusmobius/go-dateparser"
|
||||||
"github.com/nbd-wtf/go-nostr"
|
"github.com/urfave/cli/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
type NaturalTimeFlag = cli.FlagBase[nostr.Timestamp, struct{}, naturalTimeValue]
|
type NaturalTimeFlag = cli.FlagBase[nostr.Timestamp, struct{}, naturalTimeValue]
|
||||||
|
|
||||||
// wrap to satisfy golang's flag interface.
|
|
||||||
type naturalTimeValue struct {
|
type naturalTimeValue struct {
|
||||||
timestamp *nostr.Timestamp
|
timestamp *nostr.Timestamp
|
||||||
hasBeenSet bool
|
hasBeenSet bool
|
||||||
@@ -21,8 +20,6 @@ type naturalTimeValue struct {
|
|||||||
|
|
||||||
var _ cli.ValueCreator[nostr.Timestamp, struct{}] = naturalTimeValue{}
|
var _ cli.ValueCreator[nostr.Timestamp, struct{}] = naturalTimeValue{}
|
||||||
|
|
||||||
// Below functions are to satisfy the ValueCreator interface
|
|
||||||
|
|
||||||
func (t naturalTimeValue) Create(val nostr.Timestamp, p *nostr.Timestamp, c struct{}) cli.Value {
|
func (t naturalTimeValue) Create(val nostr.Timestamp, p *nostr.Timestamp, c struct{}) cli.Value {
|
||||||
*p = val
|
*p = val
|
||||||
return &naturalTimeValue{
|
return &naturalTimeValue{
|
||||||
@@ -32,21 +29,12 @@ func (t naturalTimeValue) Create(val nostr.Timestamp, p *nostr.Timestamp, c stru
|
|||||||
|
|
||||||
func (t naturalTimeValue) ToString(b nostr.Timestamp) string {
|
func (t naturalTimeValue) ToString(b nostr.Timestamp) string {
|
||||||
ts := b.Time()
|
ts := b.Time()
|
||||||
|
|
||||||
if ts.IsZero() {
|
if ts.IsZero() {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("%v", ts)
|
return fmt.Sprintf("%v", ts)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Timestamp constructor(for internal testing only)
|
|
||||||
func newTimestamp(timestamp nostr.Timestamp) *naturalTimeValue {
|
|
||||||
return &naturalTimeValue{timestamp: ×tamp}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Below functions are to satisfy the flag.Value interface
|
|
||||||
|
|
||||||
// Parses the string value to timestamp
|
|
||||||
func (t *naturalTimeValue) Set(value string) error {
|
func (t *naturalTimeValue) Set(value string) error {
|
||||||
var ts time.Time
|
var ts time.Time
|
||||||
if n, err := strconv.ParseInt(value, 10, 64); err == nil {
|
if n, err := strconv.ParseInt(value, 10, 64); err == nil {
|
||||||
@@ -75,21 +63,113 @@ func (t *naturalTimeValue) Set(value string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// String returns a readable representation of this value (for usage defaults)
|
func (t *naturalTimeValue) String() string { return fmt.Sprintf("%#v", t.timestamp) }
|
||||||
func (t *naturalTimeValue) String() string {
|
func (t *naturalTimeValue) Value() *nostr.Timestamp { return t.timestamp }
|
||||||
return fmt.Sprintf("%#v", t.timestamp)
|
func (t *naturalTimeValue) Get() any { return *t.timestamp }
|
||||||
}
|
|
||||||
|
|
||||||
// Value returns the timestamp value stored in the flag
|
|
||||||
func (t *naturalTimeValue) Value() *nostr.Timestamp {
|
|
||||||
return t.timestamp
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get returns the flag structure
|
|
||||||
func (t *naturalTimeValue) Get() any {
|
|
||||||
return *t.timestamp
|
|
||||||
}
|
|
||||||
|
|
||||||
func getNaturalDate(cmd *cli.Command, name string) nostr.Timestamp {
|
func getNaturalDate(cmd *cli.Command, name string) nostr.Timestamp {
|
||||||
return cmd.Value(name).(nostr.Timestamp)
|
return cmd.Value(name).(nostr.Timestamp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
|
|
||||||
|
type (
|
||||||
|
PubKeyFlag = cli.FlagBase[nostr.PubKey, struct{}, pubkeyValue]
|
||||||
|
)
|
||||||
|
|
||||||
|
type pubkeyValue struct {
|
||||||
|
pubkey nostr.PubKey
|
||||||
|
hasBeenSet bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ cli.ValueCreator[nostr.PubKey, struct{}] = pubkeyValue{}
|
||||||
|
|
||||||
|
func (t pubkeyValue) Create(val nostr.PubKey, p *nostr.PubKey, c struct{}) cli.Value {
|
||||||
|
*p = val
|
||||||
|
return &pubkeyValue{
|
||||||
|
pubkey: val,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t pubkeyValue) ToString(b nostr.PubKey) string { return t.pubkey.String() }
|
||||||
|
|
||||||
|
func (t *pubkeyValue) Set(value string) error {
|
||||||
|
pk, err := nostr.PubKeyFromHex(value)
|
||||||
|
t.pubkey = pk
|
||||||
|
t.hasBeenSet = true
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *pubkeyValue) String() string { return fmt.Sprintf("%#v", t.pubkey) }
|
||||||
|
func (t *pubkeyValue) Value() nostr.PubKey { return t.pubkey }
|
||||||
|
func (t *pubkeyValue) Get() any { return t.pubkey }
|
||||||
|
|
||||||
|
func getPubKey(cmd *cli.Command, name string) nostr.PubKey {
|
||||||
|
return cmd.Value(name).(nostr.PubKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
|
|
||||||
|
type (
|
||||||
|
pubkeySlice = cli.SliceBase[nostr.PubKey, struct{}, pubkeyValue]
|
||||||
|
PubKeySliceFlag = cli.FlagBase[[]nostr.PubKey, struct{}, pubkeySlice]
|
||||||
|
)
|
||||||
|
|
||||||
|
func getPubKeySlice(cmd *cli.Command, name string) []nostr.PubKey {
|
||||||
|
return cmd.Value(name).([]nostr.PubKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
|
|
||||||
|
type (
|
||||||
|
IDFlag = cli.FlagBase[nostr.ID, struct{}, idValue]
|
||||||
|
)
|
||||||
|
|
||||||
|
type idValue struct {
|
||||||
|
id nostr.ID
|
||||||
|
hasBeenSet bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ cli.ValueCreator[nostr.ID, struct{}] = idValue{}
|
||||||
|
|
||||||
|
func (t idValue) Create(val nostr.ID, p *nostr.ID, c struct{}) cli.Value {
|
||||||
|
*p = val
|
||||||
|
return &idValue{
|
||||||
|
id: val,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (t idValue) ToString(b nostr.ID) string { return t.id.String() }
|
||||||
|
|
||||||
|
func (t *idValue) Set(value string) error {
|
||||||
|
pk, err := nostr.IDFromHex(value)
|
||||||
|
t.id = pk
|
||||||
|
t.hasBeenSet = true
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *idValue) String() string { return fmt.Sprintf("%#v", t.id) }
|
||||||
|
func (t *idValue) Value() nostr.ID { return t.id }
|
||||||
|
func (t *idValue) Get() any { return t.id }
|
||||||
|
|
||||||
|
func getID(cmd *cli.Command, name string) nostr.ID {
|
||||||
|
return cmd.Value(name).(nostr.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
|
|
||||||
|
type (
|
||||||
|
idSlice = cli.SliceBase[nostr.ID, struct{}, idValue]
|
||||||
|
IDSliceFlag = cli.FlagBase[[]nostr.ID, struct{}, idSlice]
|
||||||
|
)
|
||||||
|
|
||||||
|
func getIDSlice(cmd *cli.Command, name string) []nostr.ID {
|
||||||
|
return cmd.Value(name).([]nostr.ID)
|
||||||
|
}
|
||||||
|
|||||||
123
fs.go
Normal file
123
fs.go
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"fiatjaf.com/nostr"
|
||||||
|
"fiatjaf.com/nostr/keyer"
|
||||||
|
"github.com/fatih/color"
|
||||||
|
"github.com/fiatjaf/nak/nostrfs"
|
||||||
|
"github.com/hanwen/go-fuse/v2/fs"
|
||||||
|
"github.com/hanwen/go-fuse/v2/fuse"
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
var fsCmd = &cli.Command{
|
||||||
|
Name: "fs",
|
||||||
|
Usage: "mount a FUSE filesystem that exposes Nostr events as files.",
|
||||||
|
Description: `(experimental)`,
|
||||||
|
ArgsUsage: "<mountpoint>",
|
||||||
|
Flags: append(defaultKeyFlags,
|
||||||
|
&PubKeyFlag{
|
||||||
|
Name: "pubkey",
|
||||||
|
Usage: "public key from where to to prepopulate directories",
|
||||||
|
},
|
||||||
|
&cli.DurationFlag{
|
||||||
|
Name: "auto-publish-notes",
|
||||||
|
Usage: "delay after which new notes will be auto-published, set to -1 to not publish.",
|
||||||
|
Value: time.Second * 30,
|
||||||
|
},
|
||||||
|
&cli.DurationFlag{
|
||||||
|
Name: "auto-publish-articles",
|
||||||
|
Usage: "delay after which edited articles will be auto-published.",
|
||||||
|
Value: time.Hour * 24 * 365 * 2,
|
||||||
|
DefaultText: "basically infinite",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
DisableSliceFlagSeparator: true,
|
||||||
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
|
mountpoint := c.Args().First()
|
||||||
|
if mountpoint == "" {
|
||||||
|
return fmt.Errorf("must be called with a directory path to serve as the mountpoint as an argument")
|
||||||
|
}
|
||||||
|
|
||||||
|
var kr nostr.User
|
||||||
|
if signer, _, err := gatherKeyerFromArguments(ctx, c); err == nil {
|
||||||
|
kr = signer
|
||||||
|
} else {
|
||||||
|
kr = keyer.NewReadOnlyUser(getPubKey(c, "pubkey"))
|
||||||
|
}
|
||||||
|
|
||||||
|
apnt := c.Duration("auto-publish-notes")
|
||||||
|
if apnt < 0 {
|
||||||
|
apnt = time.Hour * 24 * 365 * 3
|
||||||
|
}
|
||||||
|
apat := c.Duration("auto-publish-articles")
|
||||||
|
if apat < 0 {
|
||||||
|
apat = time.Hour * 24 * 365 * 3
|
||||||
|
}
|
||||||
|
|
||||||
|
root := nostrfs.NewNostrRoot(
|
||||||
|
context.WithValue(
|
||||||
|
context.WithValue(
|
||||||
|
ctx,
|
||||||
|
"log", log,
|
||||||
|
),
|
||||||
|
"logverbose", logverbose,
|
||||||
|
),
|
||||||
|
sys,
|
||||||
|
kr,
|
||||||
|
mountpoint,
|
||||||
|
nostrfs.Options{
|
||||||
|
AutoPublishNotesTimeout: apnt,
|
||||||
|
AutoPublishArticlesTimeout: apat,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// create the server
|
||||||
|
log("- mounting at %s... ", color.HiCyanString(mountpoint))
|
||||||
|
timeout := time.Second * 120
|
||||||
|
server, err := fs.Mount(mountpoint, root, &fs.Options{
|
||||||
|
MountOptions: fuse.MountOptions{
|
||||||
|
Debug: isVerbose,
|
||||||
|
Name: "nak",
|
||||||
|
FsName: "nak",
|
||||||
|
RememberInodes: true,
|
||||||
|
},
|
||||||
|
AttrTimeout: &timeout,
|
||||||
|
EntryTimeout: &timeout,
|
||||||
|
Logger: nostr.DebugLogger,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("mount failed: %w", err)
|
||||||
|
}
|
||||||
|
log("ok.\n")
|
||||||
|
|
||||||
|
// setup signal handling for clean unmount
|
||||||
|
ch := make(chan os.Signal, 1)
|
||||||
|
chErr := make(chan error)
|
||||||
|
signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
go func() {
|
||||||
|
<-ch
|
||||||
|
log("- unmounting... ")
|
||||||
|
err := server.Unmount()
|
||||||
|
if err != nil {
|
||||||
|
chErr <- fmt.Errorf("unmount failed: %w", err)
|
||||||
|
} else {
|
||||||
|
log("ok\n")
|
||||||
|
chErr <- nil
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// serve the filesystem until unmounted
|
||||||
|
server.Wait()
|
||||||
|
return <-chErr
|
||||||
|
},
|
||||||
|
}
|
||||||
20
fs_windows.go
Normal file
20
fs_windows.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
var fsCmd = &cli.Command{
|
||||||
|
Name: "fs",
|
||||||
|
Usage: "mount a FUSE filesystem that exposes Nostr events as files.",
|
||||||
|
Description: `doesn't work on Windows.`,
|
||||||
|
DisableSliceFlagSeparator: true,
|
||||||
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
|
return fmt.Errorf("this doesn't work on Windows.")
|
||||||
|
},
|
||||||
|
}
|
||||||
93
go.mod
93
go.mod
@@ -1,63 +1,88 @@
|
|||||||
module github.com/fiatjaf/nak
|
module github.com/fiatjaf/nak
|
||||||
|
|
||||||
go 1.22
|
go 1.24.1
|
||||||
|
|
||||||
toolchain go1.22.4
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
fiatjaf.com/lib v0.3.1
|
||||||
|
fiatjaf.com/nostr v0.0.0-20250627165101-028a1637fbd0
|
||||||
github.com/bep/debounce v1.2.1
|
github.com/bep/debounce v1.2.1
|
||||||
github.com/btcsuite/btcd/btcec/v2 v2.3.3
|
github.com/btcsuite/btcd/btcec/v2 v2.3.5
|
||||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e
|
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0
|
||||||
github.com/fatih/color v1.16.0
|
github.com/fatih/color v1.16.0
|
||||||
github.com/fiatjaf/cli/v3 v3.0.0-20240723181502-e7dd498b16ae
|
github.com/hanwen/go-fuse/v2 v2.7.2
|
||||||
github.com/fiatjaf/eventstore v0.7.1
|
github.com/json-iterator/go v1.1.12
|
||||||
github.com/fiatjaf/khatru v0.7.5
|
github.com/liamg/magic v0.0.1
|
||||||
github.com/mailru/easyjson v0.7.7
|
github.com/mailru/easyjson v0.9.0
|
||||||
|
github.com/mark3labs/mcp-go v0.8.3
|
||||||
github.com/markusmobius/go-dateparser v1.2.3
|
github.com/markusmobius/go-dateparser v1.2.3
|
||||||
github.com/nbd-wtf/go-nostr v0.34.7
|
github.com/mattn/go-tty v0.0.7
|
||||||
github.com/nbd-wtf/nostr-sdk v0.5.0
|
github.com/stretchr/testify v1.10.0
|
||||||
golang.org/x/exp v0.0.0-20240707233637-46b078467d37
|
github.com/urfave/cli/v3 v3.0.0-beta1
|
||||||
|
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b
|
||||||
|
golang.org/x/term v0.30.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/andybalholm/brotli v1.0.5 // indirect
|
github.com/FastFilter/xorfilter v0.2.1 // indirect
|
||||||
github.com/btcsuite/btcd/btcutil v1.1.3 // indirect
|
github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3 // indirect
|
||||||
|
github.com/andybalholm/brotli v1.1.1 // indirect
|
||||||
|
github.com/btcsuite/btcd v0.24.2 // indirect
|
||||||
|
github.com/btcsuite/btcd/btcutil v1.1.5 // indirect
|
||||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect
|
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
github.com/bytedance/sonic v1.13.3 // indirect
|
||||||
|
github.com/bytedance/sonic/loader v0.2.4 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/chzyer/logex v1.1.10 // indirect
|
github.com/chzyer/logex v1.1.10 // indirect
|
||||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect
|
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect
|
||||||
github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect
|
github.com/cloudwego/base64x v0.1.5 // indirect
|
||||||
|
github.com/coder/websocket v1.8.13 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect
|
||||||
|
github.com/dgraph-io/badger/v4 v4.5.0 // indirect
|
||||||
|
github.com/dgraph-io/ristretto v1.0.0 // indirect
|
||||||
|
github.com/dgraph-io/ristretto/v2 v2.1.0 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/elliotchance/pie/v2 v2.7.0 // indirect
|
github.com/elliotchance/pie/v2 v2.7.0 // indirect
|
||||||
github.com/fasthttp/websocket v1.5.7 // indirect
|
github.com/elnosh/gonuts v0.3.1-0.20250123162555-7c0381a585e3 // indirect
|
||||||
github.com/fiatjaf/generic-ristretto v0.0.1 // indirect
|
github.com/fasthttp/websocket v1.5.12 // indirect
|
||||||
github.com/gobwas/httphead v0.1.0 // indirect
|
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
|
||||||
github.com/gobwas/pool v0.2.1 // indirect
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||||
github.com/gobwas/ws v1.4.0 // indirect
|
github.com/google/flatbuffers v24.12.23+incompatible // indirect
|
||||||
github.com/golang/glog v1.1.2 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/graph-gophers/dataloader/v7 v7.1.0 // indirect
|
|
||||||
github.com/hablullah/go-hijri v1.0.2 // indirect
|
github.com/hablullah/go-hijri v1.0.2 // indirect
|
||||||
github.com/hablullah/go-juliandays v1.0.0 // indirect
|
github.com/hablullah/go-juliandays v1.0.0 // indirect
|
||||||
github.com/jalaali/go-jalaali v0.0.0-20210801064154-80525e88d958 // indirect
|
github.com/jalaali/go-jalaali v0.0.0-20210801064154-80525e88d958 // indirect
|
||||||
github.com/josharian/intern v1.0.0 // indirect
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
github.com/klauspost/compress v1.17.8 // indirect
|
github.com/klauspost/compress v1.18.0 // indirect
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.11 // indirect
|
||||||
|
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||||
github.com/magefile/mage v1.14.0 // indirect
|
github.com/magefile/mage v1.14.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/puzpuzpuz/xsync/v3 v3.1.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/rs/cors v1.7.0 // indirect
|
github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
|
||||||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect
|
github.com/rs/cors v1.11.1 // indirect
|
||||||
github.com/tetratelabs/wazero v1.2.1 // indirect
|
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 // indirect
|
||||||
github.com/tidwall/gjson v1.17.1 // indirect
|
github.com/tetratelabs/wazero v1.8.0 // indirect
|
||||||
|
github.com/tidwall/gjson v1.18.0 // indirect
|
||||||
github.com/tidwall/match v1.1.1 // indirect
|
github.com/tidwall/match v1.1.1 // indirect
|
||||||
github.com/tidwall/pretty v1.2.1 // indirect
|
github.com/tidwall/pretty v1.2.1 // indirect
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
github.com/valyala/fasthttp v1.51.0 // indirect
|
github.com/valyala/fasthttp v1.59.0 // indirect
|
||||||
github.com/wasilibs/go-re2 v1.3.0 // indirect
|
github.com/wasilibs/go-re2 v1.3.0 // indirect
|
||||||
golang.org/x/crypto v0.21.0 // indirect
|
github.com/x448/float16 v0.8.4 // indirect
|
||||||
golang.org/x/net v0.22.0 // indirect
|
go.opencensus.io v0.24.0 // indirect
|
||||||
golang.org/x/sys v0.22.0 // indirect
|
golang.org/x/arch v0.18.0 // indirect
|
||||||
golang.org/x/text v0.16.0 // indirect
|
golang.org/x/crypto v0.36.0 // indirect
|
||||||
|
golang.org/x/net v0.37.0 // indirect
|
||||||
|
golang.org/x/sync v0.15.0 // indirect
|
||||||
|
golang.org/x/sys v0.33.0 // indirect
|
||||||
|
golang.org/x/text v0.23.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.2 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
266
go.sum
266
go.sum
@@ -1,19 +1,35 @@
|
|||||||
|
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
|
fiatjaf.com/lib v0.3.1 h1:/oFQwNtFRfV+ukmOCxfBEAuayoLwXp4wu2/fz5iHpwA=
|
||||||
|
fiatjaf.com/lib v0.3.1/go.mod h1:Ycqq3+mJ9jAWu7XjbQI1cVr+OFgnHn79dQR5oTII47g=
|
||||||
|
fiatjaf.com/nostr v0.0.0-20250610194330-027d016d9706 h1:G0xS5h9dsbODWh+f8rYvDkY328h79MsNs2dGPGqm8nY=
|
||||||
|
fiatjaf.com/nostr v0.0.0-20250610194330-027d016d9706/go.mod h1:VPs38Fc8J1XAErV750CXAmMUqIq3XEX9VZVj/LuQzzM=
|
||||||
|
fiatjaf.com/nostr v0.0.0-20250627165101-028a1637fbd0 h1:Se07jECWueD3fZyaHO08oIzFOPwT6A6wNPQ8QWccX5c=
|
||||||
|
fiatjaf.com/nostr v0.0.0-20250627165101-028a1637fbd0/go.mod h1:VPs38Fc8J1XAErV750CXAmMUqIq3XEX9VZVj/LuQzzM=
|
||||||
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
|
github.com/FastFilter/xorfilter v0.2.1 h1:lbdeLG9BdpquK64ZsleBS8B4xO/QW1IM0gMzF7KaBKc=
|
||||||
|
github.com/FastFilter/xorfilter v0.2.1/go.mod h1:aumvdkhscz6YBZF9ZA/6O4fIoNod4YR50kIVGGZ7l9I=
|
||||||
|
github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3 h1:ClzzXMDDuUbWfNNZqGeYq4PnYOlwlOVIvSyNaIy0ykg=
|
||||||
|
github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3/go.mod h1:we0YA5CsBbH5+/NUzC/AlMmxaDtWlXeNsqrwXjTzmzA=
|
||||||
|
github.com/PowerDNS/lmdb-go v1.9.3 h1:AUMY2pZT8WRpkEv39I9Id3MuoHd+NZbTVpNhruVkPTg=
|
||||||
|
github.com/PowerDNS/lmdb-go v1.9.3/go.mod h1:TE0l+EZK8Z1B4dx070ZxkWTlp8RG1mjN0/+FkFRQMtU=
|
||||||
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
|
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
|
||||||
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
|
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
||||||
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
||||||
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
|
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
|
||||||
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
|
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
|
||||||
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
|
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
|
||||||
github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M=
|
github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M=
|
||||||
github.com/btcsuite/btcd v0.23.0/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY=
|
github.com/btcsuite/btcd v0.23.5-0.20231215221805-96c9fd8078fd/go.mod h1:nm3Bko6zh6bWP60UxwoT5LzdGJsQJaPo6HjduXq9p6A=
|
||||||
|
github.com/btcsuite/btcd v0.24.2 h1:aLmxPguqxza+4ag8R1I2nnJjSu2iFn/kqtHTIImswcY=
|
||||||
|
github.com/btcsuite/btcd v0.24.2/go.mod h1:5C8ChTkl5ejr3WHj8tkQSCmydiMEPB0ZhQhehpq7Dgg=
|
||||||
github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA=
|
github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA=
|
||||||
github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE=
|
github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE=
|
||||||
github.com/btcsuite/btcd/btcec/v2 v2.3.3 h1:6+iXlDKE8RMtKsvK0gshlXIuPbyWM/h84Ensb7o3sC0=
|
github.com/btcsuite/btcd/btcec/v2 v2.3.5 h1:dpAlnAwmT1yIBm3exhT1/8iUSD98RDJM5vqJVQDQLiU=
|
||||||
github.com/btcsuite/btcd/btcec/v2 v2.3.3/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04=
|
github.com/btcsuite/btcd/btcec/v2 v2.3.5/go.mod h1:m22FrOAiuxl/tht9wIqAoGHcbnCCaPWyauO8y2LGGtQ=
|
||||||
github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A=
|
github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A=
|
||||||
github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE=
|
github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE=
|
||||||
github.com/btcsuite/btcd/btcutil v1.1.3 h1:xfbtw8lwpp0G6NwSHb+UE67ryTFHJAiNuipusjXSohQ=
|
github.com/btcsuite/btcd/btcutil v1.1.5 h1:+wER79R5670vs/ZusMTF1yTcRYE5GUsFbdjdisflzM8=
|
||||||
github.com/btcsuite/btcd/btcutil v1.1.3/go.mod h1:UR7dsSJzJUfMmFiiLlIrMq1lS9jh9EdCV7FStZSnpi0=
|
github.com/btcsuite/btcd/btcutil v1.1.5/go.mod h1:PSZZ4UitpLBWzxGd5VGOrLnmOjtPP/a6HaFo12zMs00=
|
||||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
|
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
|
||||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
|
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
|
||||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 h1:59Kx4K6lzOW5w6nFlA0v5+lk/6sjybR934QNHSJZPTQ=
|
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 h1:59Kx4K6lzOW5w6nFlA0v5+lk/6sjybR934QNHSJZPTQ=
|
||||||
@@ -27,70 +43,104 @@ github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku
|
|||||||
github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
|
github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
|
||||||
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
|
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
|
||||||
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
|
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
|
||||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0=
|
||||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
|
||||||
|
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||||
|
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
|
||||||
|
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||||
|
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||||
|
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
|
||||||
|
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
|
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
|
||||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
|
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
|
||||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
|
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
|
||||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||||
|
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||||
|
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
|
||||||
|
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||||
|
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||||
|
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||||
|
github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE=
|
||||||
|
github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
||||||
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
|
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
|
||||||
github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y=
|
github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8=
|
||||||
github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
|
github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg=
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
|
||||||
github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218=
|
github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218=
|
||||||
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA=
|
github.com/dgraph-io/badger/v4 v4.5.0 h1:TeJE3I1pIWLBjYhIYCA1+uxrjWEoJXImFBMEBVSm16g=
|
||||||
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
github.com/dgraph-io/badger/v4 v4.5.0/go.mod h1:ysgYmIeG8dS/E8kwxT7xHyc7MkmwNYLRoYnFbr7387A=
|
||||||
|
github.com/dgraph-io/ristretto v1.0.0 h1:SYG07bONKMlFDUYu5pEu3DGAh8c2OFNzKm6G9J4Si84=
|
||||||
|
github.com/dgraph-io/ristretto v1.0.0/go.mod h1:jTi2FiYEhQ1NsMmA7DeBykizjOuY88NhKBkepyu1jPc=
|
||||||
|
github.com/dgraph-io/ristretto/v2 v2.1.0 h1:59LjpOJLNDULHh8MC4UaegN52lC4JnO2dITsie/Pa8I=
|
||||||
|
github.com/dgraph-io/ristretto/v2 v2.1.0/go.mod h1:uejeqfYXpUomfse0+lO+13ATz4TypQYLJZzBSAemuB4=
|
||||||
|
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y=
|
||||||
|
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
|
github.com/dvyukov/go-fuzz v0.0.0-20200318091601-be3528f3a813/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
|
||||||
github.com/elliotchance/pie/v2 v2.7.0 h1:FqoIKg4uj0G/CrLGuMS9ejnFKa92lxE1dEgBD3pShXg=
|
github.com/elliotchance/pie/v2 v2.7.0 h1:FqoIKg4uj0G/CrLGuMS9ejnFKa92lxE1dEgBD3pShXg=
|
||||||
github.com/elliotchance/pie/v2 v2.7.0/go.mod h1:18t0dgGFH006g4eVdDtWfgFZPQEgl10IoEO8YWEq3Og=
|
github.com/elliotchance/pie/v2 v2.7.0/go.mod h1:18t0dgGFH006g4eVdDtWfgFZPQEgl10IoEO8YWEq3Og=
|
||||||
github.com/fasthttp/websocket v1.5.7 h1:0a6o2OfeATvtGgoMKleURhLT6JqWPg7fYfWnH4KHau4=
|
github.com/elnosh/gonuts v0.3.1-0.20250123162555-7c0381a585e3 h1:k7evIqJ2BtFn191DgY/b03N2bMYA/iQwzr4f/uHYn20=
|
||||||
github.com/fasthttp/websocket v1.5.7/go.mod h1:bC4fxSono9czeXHQUVKxsC0sNjbm7lPJR04GDFqClfU=
|
github.com/elnosh/gonuts v0.3.1-0.20250123162555-7c0381a585e3/go.mod h1:vgZomh4YQk7R3w4ltZc0sHwCmndfHkuX6V4sga/8oNs=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||||
|
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||||
|
github.com/fasthttp/websocket v1.5.12 h1:e4RGPpWW2HTbL3zV0Y/t7g0ub294LkiuXXUuTOUInlE=
|
||||||
|
github.com/fasthttp/websocket v1.5.12/go.mod h1:I+liyL7/4moHojiOgUOIKEWm9EIxHqxZChS+aMFltyg=
|
||||||
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||||
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
||||||
github.com/fiatjaf/cli/v3 v3.0.0-20240723181502-e7dd498b16ae h1:0B/1dU3YECIbPoBIRTQ4c0scZCNz9TVHtQpiODGrTTo=
|
|
||||||
github.com/fiatjaf/cli/v3 v3.0.0-20240723181502-e7dd498b16ae/go.mod h1:aAWPO4bixZZxPtOnH6K3q4GbQ0jftUNDW9Oa861IRew=
|
|
||||||
github.com/fiatjaf/eventstore v0.7.1 h1:5f2yvEtYvsvMBNttysmXhSSum5M1qwvPzjEQ/BFue7Q=
|
|
||||||
github.com/fiatjaf/eventstore v0.7.1/go.mod h1:ek/yWbanKVG767fK51Q3+6Mvi5oEHYSsdPym40nZexw=
|
|
||||||
github.com/fiatjaf/generic-ristretto v0.0.1 h1:LUJSU87X/QWFsBXTwnH3moFe4N8AjUxT+Rfa0+bo6YM=
|
|
||||||
github.com/fiatjaf/generic-ristretto v0.0.1/go.mod h1:cvV6ANHDA/GrfzVrig7N7i6l8CWnkVZvtQ2/wk9DPVE=
|
|
||||||
github.com/fiatjaf/khatru v0.7.5 h1:UFo+cdbqHDn1W4Q4h03y3vzh1BiU+6fLYK48oWU2K34=
|
|
||||||
github.com/fiatjaf/khatru v0.7.5/go.mod h1:WVqij7X9Vr9UAMIwafQbKVFKxc42Np37vyficwUr/nQ=
|
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||||
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
|
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
|
||||||
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
|
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
|
||||||
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||||
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
||||||
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
||||||
github.com/golang/glog v1.1.2 h1:DVjP2PbBOzHyzA+dn3WhHIq4NdVu3Q+pvivFICf/7fo=
|
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
github.com/golang/glog v1.1.2/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ=
|
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
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-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.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||||
|
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||||
|
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
|
github.com/google/flatbuffers v24.12.23+incompatible h1:ubBKR94NR4pXUCY/MUsRVzd9umNW7ht7EG9hHfS9FX8=
|
||||||
|
github.com/google/flatbuffers v24.12.23+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
|
||||||
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/graph-gophers/dataloader/v7 v7.1.0 h1:Wn8HGF/q7MNXcvfaBnLEPEFJttVHR8zuEqP1obys/oc=
|
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/graph-gophers/dataloader/v7 v7.1.0/go.mod h1:1bKE0Dm6OUcTB/OAuYVOZctgIz7Q3d0XrYtlIzTgg6Q=
|
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/hablullah/go-hijri v1.0.2 h1:drT/MZpSZJQXo7jftf5fthArShcaMtsal0Zf/dnmp6k=
|
github.com/hablullah/go-hijri v1.0.2 h1:drT/MZpSZJQXo7jftf5fthArShcaMtsal0Zf/dnmp6k=
|
||||||
github.com/hablullah/go-hijri v1.0.2/go.mod h1:OS5qyYLDjORXzK4O1adFw9Q5WfhOcMdAKglDkcTxgWQ=
|
github.com/hablullah/go-hijri v1.0.2/go.mod h1:OS5qyYLDjORXzK4O1adFw9Q5WfhOcMdAKglDkcTxgWQ=
|
||||||
github.com/hablullah/go-juliandays v1.0.0 h1:A8YM7wIj16SzlKT0SRJc9CD29iiaUzpBLzh5hr0/5p0=
|
github.com/hablullah/go-juliandays v1.0.0 h1:A8YM7wIj16SzlKT0SRJc9CD29iiaUzpBLzh5hr0/5p0=
|
||||||
github.com/hablullah/go-juliandays v1.0.0/go.mod h1:0JOYq4oFOuDja+oospuc61YoX+uNEn7Z6uHYTbBzdGc=
|
github.com/hablullah/go-juliandays v1.0.0/go.mod h1:0JOYq4oFOuDja+oospuc61YoX+uNEn7Z6uHYTbBzdGc=
|
||||||
|
github.com/hanwen/go-fuse/v2 v2.7.2 h1:SbJP1sUP+n1UF8NXBA14BuojmTez+mDgOk0bC057HQw=
|
||||||
|
github.com/hanwen/go-fuse/v2 v2.7.2/go.mod h1:ugNaD/iv5JYyS1Rcvi57Wz7/vrLQJo10mmketmoef48=
|
||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
github.com/jalaali/go-jalaali v0.0.0-20210801064154-80525e88d958 h1:qxLoi6CAcXVzjfvu+KXIXJOAsQB62LXjsfbOaErsVzE=
|
github.com/jalaali/go-jalaali v0.0.0-20210801064154-80525e88d958 h1:qxLoi6CAcXVzjfvu+KXIXJOAsQB62LXjsfbOaErsVzE=
|
||||||
github.com/jalaali/go-jalaali v0.0.0-20210801064154-80525e88d958/go.mod h1:Wqfu7mjUHj9WDzSSPI5KfBclTTEnLveRUFr/ujWnTgE=
|
github.com/jalaali/go-jalaali v0.0.0-20210801064154-80525e88d958/go.mod h1:Wqfu7mjUHj9WDzSSPI5KfBclTTEnLveRUFr/ujWnTgE=
|
||||||
@@ -99,13 +149,27 @@ github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJS
|
|||||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||||
github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
|
github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
|
||||||
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
|
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
|
||||||
github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU=
|
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||||
github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.11 h1:0OwqZRYI2rFrjS4kvkDnqJkKHdHaRnCm68/DY4OxRzU=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.11/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
|
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||||
|
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||||
|
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||||
|
github.com/liamg/magic v0.0.1 h1:Ru22ElY+sCh6RvRTWjQzKKCxsEco8hE0co8n1qe7TBM=
|
||||||
|
github.com/liamg/magic v0.0.1/go.mod h1:yQkOmZZI52EA+SQ2xyHpVw8fNvTBruF873Y+Vt6S+fk=
|
||||||
github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo=
|
github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo=
|
||||||
github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
|
||||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
||||||
|
github.com/mark3labs/mcp-go v0.8.3 h1:IzlyN8BaP4YwUMUDqxOGJhGdZXEDQiAPX43dNPgnzrg=
|
||||||
|
github.com/mark3labs/mcp-go v0.8.3/go.mod h1:cjMlBU0cv/cj9kjlgmRhoJ5JREdS7YX83xeIG9Ko/jE=
|
||||||
github.com/markusmobius/go-dateparser v1.2.3 h1:TvrsIvr5uk+3v6poDjaicnAFJ5IgtFHgLiuMY2Eb7Nw=
|
github.com/markusmobius/go-dateparser v1.2.3 h1:TvrsIvr5uk+3v6poDjaicnAFJ5IgtFHgLiuMY2Eb7Nw=
|
||||||
github.com/markusmobius/go-dateparser v1.2.3/go.mod h1:cMwQRrBUQlK1UI5TIFHEcvpsMbkWrQLXuaPNMFzuYLk=
|
github.com/markusmobius/go-dateparser v1.2.3/go.mod h1:cMwQRrBUQlK1UI5TIFHEcvpsMbkWrQLXuaPNMFzuYLk=
|
||||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
@@ -113,10 +177,15 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
|
|||||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/nbd-wtf/go-nostr v0.34.7 h1:gQP3rHC+aBw3dsu9ubZn8tV0eDBmLrMpmNCjj5nFUTE=
|
github.com/mattn/go-tty v0.0.7 h1:KJ486B6qI8+wBO7kQxYgmmEFDaFEE96JMBQ7h400N8Q=
|
||||||
github.com/nbd-wtf/go-nostr v0.34.7/go.mod h1:NZQkxl96ggbO8rvDpVjcsojJqKTPwqhP4i82O7K5DJs=
|
github.com/mattn/go-tty v0.0.7/go.mod h1:f2i5ZOvXBU/tCABmLmOfzLz9azMo5wdAaElRNnJKr+k=
|
||||||
github.com/nbd-wtf/nostr-sdk v0.5.0 h1:zrMxcvMSxkw29RyfXEdF3XW5rUWLuT5Q9oBAhd5dyew=
|
github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78=
|
||||||
github.com/nbd-wtf/nostr-sdk v0.5.0/go.mod h1:MJ7gYv3XiZKU6MHSM0N7oHqQAQhbvpgGQk4Q+XUdIUs=
|
github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
@@ -130,49 +199,84 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
|||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/puzpuzpuz/xsync/v3 v3.1.0 h1:EewKT7/LNac5SLiEblJeUu8z5eERHrmRLnMQL2d7qX4=
|
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
github.com/puzpuzpuz/xsync/v3 v3.1.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
|
||||||
github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik=
|
github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||||
github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
|
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
|
||||||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk=
|
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
|
||||||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g=
|
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 h1:D0vL7YNisV2yqE55+q0lFuGse6U8lxlg7fYTctlT5Gc=
|
||||||
|
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38/go.mod h1:sM7Mt7uEoCeFSCBM+qBrqvEo+/9vdmj19wzp3yzUhmg=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
|
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
|
||||||
github.com/tetratelabs/wazero v1.2.1 h1:J4X2hrGzJvt+wqltuvcSjHQ7ujQxA9gb6PeMs4qlUWs=
|
github.com/tetratelabs/wazero v1.8.0 h1:iEKu0d4c2Pd+QSRieYbnQC9yiFlMS9D+Jr0LsRmcF4g=
|
||||||
github.com/tetratelabs/wazero v1.2.1/go.mod h1:wYx2gNRg8/WihJfSDxA1TIL8H+GkfLYm+bIfbblu9VQ=
|
github.com/tetratelabs/wazero v1.8.0/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs=
|
||||||
github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U=
|
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||||
github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||||
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
|
github.com/urfave/cli/v3 v3.0.0-beta1 h1:6DTaaUarcM0wX7qj5Hcvs+5Dm3dyUTBbEwIWAjcw9Zg=
|
||||||
|
github.com/urfave/cli/v3 v3.0.0-beta1/go.mod h1:FnIeEMYu+ko8zP1F9Ypr3xkZMIDqW3DR92yUtY39q1Y=
|
||||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
|
github.com/valyala/fasthttp v1.59.0 h1:Qu0qYHfXvPk1mSLNqcFtEk6DpxgA26hy6bmydotDpRI=
|
||||||
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
|
github.com/valyala/fasthttp v1.59.0/go.mod h1:GTxNb9Bc6r2a9D0TWNSPwDz78UxnTGBViY3xZNEqyYU=
|
||||||
github.com/wasilibs/go-re2 v1.3.0 h1:LFhBNzoStM3wMie6rN2slD1cuYH2CGiHpvNL3UtcsMw=
|
github.com/wasilibs/go-re2 v1.3.0 h1:LFhBNzoStM3wMie6rN2slD1cuYH2CGiHpvNL3UtcsMw=
|
||||||
github.com/wasilibs/go-re2 v1.3.0/go.mod h1:AafrCXVvGRJJOImMajgJ2M7rVmWyisVK7sFshbxnVrg=
|
github.com/wasilibs/go-re2 v1.3.0/go.mod h1:AafrCXVvGRJJOImMajgJ2M7rVmWyisVK7sFshbxnVrg=
|
||||||
github.com/wasilibs/nottinygc v0.4.0 h1:h1TJMihMC4neN6Zq+WKpLxgd9xCFMw7O9ETLwY2exJQ=
|
github.com/wasilibs/nottinygc v0.4.0 h1:h1TJMihMC4neN6Zq+WKpLxgd9xCFMw7O9ETLwY2exJQ=
|
||||||
github.com/wasilibs/nottinygc v0.4.0/go.mod h1:oDcIotskuYNMpqMF23l7Z8uzD4TC0WXHK8jetlB3HIo=
|
github.com/wasilibs/nottinygc v0.4.0/go.mod h1:oDcIotskuYNMpqMF23l7Z8uzD4TC0WXHK8jetlB3HIo=
|
||||||
|
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||||
|
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||||
|
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||||
|
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||||
|
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||||
|
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||||
|
golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc=
|
||||||
|
golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
||||||
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
|
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||||
golang.org/x/exp v0.0.0-20240707233637-46b078467d37 h1:uLDX+AfeFCct3a2C7uIWBKMJIR3CJMhcgfrUAqjRK6w=
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20240707233637-46b078467d37/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
|
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
|
||||||
|
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
|
||||||
|
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
|
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||||
|
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||||
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
|
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
|
||||||
|
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
||||||
|
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@@ -182,24 +286,47 @@ golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-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-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-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
|
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
|
||||||
|
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||||
|
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||||
|
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
|
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||||
|
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||||
|
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||||
|
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||||
|
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||||
|
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||||
|
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||||
|
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
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-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 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.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.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||||
|
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
|
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
|
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||||
|
google.golang.org/protobuf v1.36.2 h1:R8FeyR1/eLmkutZOM5CWghmo5itiG9z0ktFlTVLuTmU=
|
||||||
|
google.golang.org/protobuf v1.36.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
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/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/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||||
@@ -209,3 +336,8 @@ 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.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
|
||||||
|
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||||
|
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||||
|
|||||||
577
helpers.go
577
helpers.go
@@ -3,90 +3,131 @@ package main
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
"encoding/hex"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"iter"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
|
"net/http"
|
||||||
|
"net/textproto"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"runtime"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"fiatjaf.com/nostr"
|
||||||
|
"fiatjaf.com/nostr/nip19"
|
||||||
|
"fiatjaf.com/nostr/nip42"
|
||||||
|
"fiatjaf.com/nostr/sdk"
|
||||||
"github.com/chzyer/readline"
|
"github.com/chzyer/readline"
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
"github.com/fiatjaf/cli/v3"
|
jsoniter "github.com/json-iterator/go"
|
||||||
"github.com/nbd-wtf/go-nostr"
|
"github.com/mattn/go-tty"
|
||||||
"github.com/nbd-wtf/go-nostr/nip19"
|
"github.com/urfave/cli/v3"
|
||||||
"github.com/nbd-wtf/go-nostr/nip46"
|
"golang.org/x/term"
|
||||||
"github.com/nbd-wtf/go-nostr/nip49"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var sys *sdk.System
|
||||||
|
|
||||||
|
var json = jsoniter.ConfigFastest
|
||||||
|
|
||||||
const (
|
const (
|
||||||
LINE_PROCESSING_ERROR = iota
|
LINE_PROCESSING_ERROR = iota
|
||||||
)
|
)
|
||||||
|
|
||||||
var log = func(msg string, args ...any) {
|
var (
|
||||||
fmt.Fprintf(color.Error, msg, args...)
|
log = func(msg string, args ...any) { fmt.Fprintf(color.Error, msg, args...) }
|
||||||
}
|
logverbose = func(msg string, args ...any) {} // by default do nothing
|
||||||
|
stdout = func(args ...any) { fmt.Fprintln(color.Output, args...) }
|
||||||
var stdout = fmt.Println
|
)
|
||||||
|
|
||||||
func isPiped() bool {
|
func isPiped() bool {
|
||||||
stat, _ := os.Stdin.Stat()
|
stat, _ := os.Stdin.Stat()
|
||||||
return stat.Mode()&os.ModeCharDevice == 0
|
return stat.Mode()&os.ModeCharDevice == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func getStdinLinesOrBlank() chan string {
|
func getJsonsOrBlank() iter.Seq[string] {
|
||||||
multi := make(chan string)
|
var curr strings.Builder
|
||||||
if hasStdinLines := writeStdinLinesOrNothing(multi); !hasStdinLines {
|
|
||||||
single := make(chan string, 1)
|
var finalJsonErr error
|
||||||
single <- ""
|
return func(yield func(string) bool) {
|
||||||
close(single)
|
hasStdin := writeStdinLinesOrNothing(func(stdinLine string) bool {
|
||||||
return single
|
// we're look for an event, but it may be in multiple lines, so if json parsing fails
|
||||||
} else {
|
// we'll try the next line until we're successful
|
||||||
return multi
|
curr.WriteString(stdinLine)
|
||||||
|
stdinEvent := curr.String()
|
||||||
|
|
||||||
|
var dummy any
|
||||||
|
if err := json.Unmarshal([]byte(stdinEvent), &dummy); err != nil {
|
||||||
|
finalJsonErr = err
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
finalJsonErr = nil
|
||||||
|
|
||||||
|
if !yield(stdinEvent) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
curr.Reset()
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
if !hasStdin {
|
||||||
|
yield("{}")
|
||||||
|
}
|
||||||
|
|
||||||
|
if finalJsonErr != nil {
|
||||||
|
log(color.YellowString("stdin json parse error: %s", finalJsonErr))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getStdinLinesOrArguments(args cli.Args) chan string {
|
func getStdinLinesOrBlank() iter.Seq[string] {
|
||||||
|
return func(yield func(string) bool) {
|
||||||
|
hasStdin := writeStdinLinesOrNothing(func(stdinLine string) bool {
|
||||||
|
if !yield(stdinLine) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
if !hasStdin {
|
||||||
|
yield("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getStdinLinesOrArguments(args cli.Args) iter.Seq[string] {
|
||||||
return getStdinLinesOrArgumentsFromSlice(args.Slice())
|
return getStdinLinesOrArgumentsFromSlice(args.Slice())
|
||||||
}
|
}
|
||||||
|
|
||||||
func getStdinLinesOrArgumentsFromSlice(args []string) chan string {
|
func getStdinLinesOrArgumentsFromSlice(args []string) iter.Seq[string] {
|
||||||
// try the first argument
|
// try the first argument
|
||||||
if len(args) > 0 {
|
if len(args) > 0 {
|
||||||
argsCh := make(chan string, 1)
|
return slices.Values(args)
|
||||||
go func() {
|
|
||||||
for _, arg := range args {
|
|
||||||
argsCh <- arg
|
|
||||||
}
|
|
||||||
close(argsCh)
|
|
||||||
}()
|
|
||||||
return argsCh
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// try the stdin
|
// try the stdin
|
||||||
multi := make(chan string)
|
return func(yield func(string) bool) {
|
||||||
writeStdinLinesOrNothing(multi)
|
writeStdinLinesOrNothing(yield)
|
||||||
return multi
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeStdinLinesOrNothing(ch chan string) (hasStdinLines bool) {
|
func writeStdinLinesOrNothing(yield func(string) bool) (hasStdinLines bool) {
|
||||||
if isPiped() {
|
if isPiped() {
|
||||||
// piped
|
// piped
|
||||||
go func() {
|
scanner := bufio.NewScanner(os.Stdin)
|
||||||
scanner := bufio.NewScanner(os.Stdin)
|
scanner.Buffer(make([]byte, 16*1024*1024), 256*1024*1024)
|
||||||
scanner.Buffer(make([]byte, 16*1024*1024), 256*1024*1024)
|
hasEmittedAtLeastOne := false
|
||||||
hasEmittedAtLeastOne := false
|
for scanner.Scan() {
|
||||||
for scanner.Scan() {
|
if !yield(strings.TrimSpace(scanner.Text())) {
|
||||||
ch <- strings.TrimSpace(scanner.Text())
|
return
|
||||||
hasEmittedAtLeastOne = true
|
|
||||||
}
|
}
|
||||||
if !hasEmittedAtLeastOne {
|
hasEmittedAtLeastOne = true
|
||||||
ch <- ""
|
}
|
||||||
}
|
return hasEmittedAtLeastOne
|
||||||
close(ch)
|
|
||||||
}()
|
|
||||||
return true
|
|
||||||
} else {
|
} else {
|
||||||
// not piped
|
// not piped
|
||||||
return false
|
return false
|
||||||
@@ -117,56 +158,195 @@ func normalizeAndValidateRelayURLs(wsurls []string) error {
|
|||||||
|
|
||||||
func connectToAllRelays(
|
func connectToAllRelays(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
|
c *cli.Command,
|
||||||
relayUrls []string,
|
relayUrls []string,
|
||||||
forcePreAuth bool,
|
preAuthSigner func(ctx context.Context, c *cli.Command, log func(s string, args ...any), authEvent *nostr.Event) (err error), // if this exists we will force preauth
|
||||||
opts ...nostr.PoolOption,
|
opts nostr.PoolOptions,
|
||||||
) (*nostr.SimplePool, []*nostr.Relay) {
|
) []*nostr.Relay {
|
||||||
relays := make([]*nostr.Relay, 0, len(relayUrls))
|
// first pass to check if these are valid relay URLs
|
||||||
pool := nostr.NewSimplePool(ctx, opts...)
|
|
||||||
relayLoop:
|
|
||||||
for _, url := range relayUrls {
|
for _, url := range relayUrls {
|
||||||
log("connecting to %s... ", url)
|
if !nostr.IsValidRelayURL(nostr.NormalizeURL(url)) {
|
||||||
if relay, err := pool.EnsureRelay(url); err == nil {
|
log("invalid relay URL: %s\n", url)
|
||||||
if forcePreAuth {
|
os.Exit(4)
|
||||||
log("waiting for auth challenge... ")
|
|
||||||
signer := opts[0].(nostr.WithAuthHandler)
|
|
||||||
time.Sleep(time.Millisecond * 200)
|
|
||||||
challengeWaitLoop:
|
|
||||||
for {
|
|
||||||
// beginhack
|
|
||||||
// here starts the biggest and ugliest hack of this codebase
|
|
||||||
if err := relay.Auth(ctx, func(authEvent *nostr.Event) error {
|
|
||||||
challengeTag := authEvent.Tags.GetFirst([]string{"challenge", ""})
|
|
||||||
if (*challengeTag)[1] == "" {
|
|
||||||
return fmt.Errorf("auth not received yet *****")
|
|
||||||
}
|
|
||||||
return signer(authEvent)
|
|
||||||
}); err == nil {
|
|
||||||
// auth succeeded
|
|
||||||
break challengeWaitLoop
|
|
||||||
} else {
|
|
||||||
// auth failed
|
|
||||||
if strings.HasSuffix(err.Error(), "auth not received yet *****") {
|
|
||||||
// it failed because we didn't receive the challenge yet, so keep waiting
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
continue challengeWaitLoop
|
|
||||||
} else {
|
|
||||||
// it failed for some other reason, so skip this relay
|
|
||||||
log(err.Error() + "\n")
|
|
||||||
continue relayLoop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// endhack
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
relays = append(relays, relay)
|
|
||||||
log("ok.\n")
|
|
||||||
} else {
|
|
||||||
log(err.Error() + "\n")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return pool, relays
|
|
||||||
|
opts.EventMiddleware = sys.TrackEventHints
|
||||||
|
opts.PenaltyBox = true
|
||||||
|
opts.RelayOptions = nostr.RelayOptions{
|
||||||
|
RequestHeader: http.Header{textproto.CanonicalMIMEHeaderKey("user-agent"): {"nak/s"}},
|
||||||
|
}
|
||||||
|
sys.Pool = nostr.NewPool(opts)
|
||||||
|
|
||||||
|
relays := make([]*nostr.Relay, 0, len(relayUrls))
|
||||||
|
|
||||||
|
if supportsDynamicMultilineMagic() {
|
||||||
|
// overcomplicated multiline rendering magic
|
||||||
|
lines := make([][][]byte, len(relayUrls))
|
||||||
|
flush := func() {
|
||||||
|
for _, line := range lines {
|
||||||
|
for _, part := range line {
|
||||||
|
os.Stderr.Write(part)
|
||||||
|
}
|
||||||
|
os.Stderr.Write([]byte{'\n'})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
render := func() {
|
||||||
|
clearLines(len(lines))
|
||||||
|
flush()
|
||||||
|
}
|
||||||
|
flush()
|
||||||
|
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
wg.Add(len(relayUrls))
|
||||||
|
for i, url := range relayUrls {
|
||||||
|
lines[i] = make([][]byte, 1, 2)
|
||||||
|
logthis := func(s string, args ...any) {
|
||||||
|
lines[i] = append(lines[i], []byte(fmt.Sprintf(s, args...)))
|
||||||
|
render()
|
||||||
|
}
|
||||||
|
colorizepreamble := func(c func(string, ...any) string) {
|
||||||
|
lines[i][0] = []byte(fmt.Sprintf("%s... ", c(url)))
|
||||||
|
}
|
||||||
|
colorizepreamble(color.CyanString)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
relay := connectToSingleRelay(ctx, c, url, preAuthSigner, colorizepreamble, logthis)
|
||||||
|
if relay != nil {
|
||||||
|
relays = append(relays, relay)
|
||||||
|
}
|
||||||
|
wg.Done()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
} else {
|
||||||
|
// simple flow
|
||||||
|
for _, url := range relayUrls {
|
||||||
|
log("connecting to %s... ", color.CyanString(url))
|
||||||
|
relay := connectToSingleRelay(ctx, c, url, preAuthSigner, nil, log)
|
||||||
|
if relay != nil {
|
||||||
|
relays = append(relays, relay)
|
||||||
|
}
|
||||||
|
log("\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return relays
|
||||||
|
}
|
||||||
|
|
||||||
|
func connectToSingleRelay(
|
||||||
|
ctx context.Context,
|
||||||
|
c *cli.Command,
|
||||||
|
url string,
|
||||||
|
preAuthSigner func(ctx context.Context, c *cli.Command, log func(s string, args ...any), authEvent *nostr.Event) (err error),
|
||||||
|
colorizepreamble func(c func(string, ...any) string),
|
||||||
|
logthis func(s string, args ...any),
|
||||||
|
) *nostr.Relay {
|
||||||
|
if relay, err := sys.Pool.EnsureRelay(url); err == nil {
|
||||||
|
if preAuthSigner != nil {
|
||||||
|
if colorizepreamble != nil {
|
||||||
|
colorizepreamble(color.YellowString)
|
||||||
|
}
|
||||||
|
logthis("waiting for auth challenge... ")
|
||||||
|
time.Sleep(time.Millisecond * 200)
|
||||||
|
|
||||||
|
for range 5 {
|
||||||
|
if err := relay.Auth(ctx, func(ctx context.Context, authEvent *nostr.Event) error {
|
||||||
|
challengeTag := authEvent.Tags.Find("challenge")
|
||||||
|
if challengeTag[1] == "" {
|
||||||
|
return fmt.Errorf("auth not received yet *****") // what a giant hack
|
||||||
|
}
|
||||||
|
return preAuthSigner(ctx, c, logthis, authEvent)
|
||||||
|
}); err == nil {
|
||||||
|
// auth succeeded
|
||||||
|
goto preauthSuccess
|
||||||
|
} else {
|
||||||
|
// auth failed
|
||||||
|
if strings.HasSuffix(err.Error(), "auth not received yet *****") {
|
||||||
|
// it failed because we didn't receive the challenge yet, so keep waiting
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
// it failed for some other reason, so skip this relay
|
||||||
|
if colorizepreamble != nil {
|
||||||
|
colorizepreamble(colors.errorf)
|
||||||
|
}
|
||||||
|
logthis(err.Error())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if colorizepreamble != nil {
|
||||||
|
colorizepreamble(colors.errorf)
|
||||||
|
}
|
||||||
|
logthis("failed to get an AUTH challenge in enough time.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
preauthSuccess:
|
||||||
|
if colorizepreamble != nil {
|
||||||
|
colorizepreamble(colors.successf)
|
||||||
|
}
|
||||||
|
logthis("ok.")
|
||||||
|
return relay
|
||||||
|
} else {
|
||||||
|
if colorizepreamble != nil {
|
||||||
|
colorizepreamble(colors.errorf)
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we're here that means we've failed to connect, this may be a huge message
|
||||||
|
// but we're likely to only be interested in the lowest level error (although we can leave space)
|
||||||
|
logthis(clampError(err, len(url)+12))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func clearLines(lineCount int) {
|
||||||
|
for i := 0; i < lineCount; i++ {
|
||||||
|
os.Stderr.Write([]byte("\033[0A\033[2K\r"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func supportsDynamicMultilineMagic() bool {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !term.IsTerminal(0) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
width, _, err := term.GetSize(int(os.Stderr.Fd()))
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if width < 110 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func authSigner(ctx context.Context, c *cli.Command, log func(s string, args ...any), authEvent *nostr.Event) (err error) {
|
||||||
|
defer func() {
|
||||||
|
if err != nil {
|
||||||
|
cleanUrl, _ := strings.CutPrefix(nip42.GetRelayURLFromAuthEvent(*authEvent), "wss://")
|
||||||
|
log("%s auth failed: %s", colors.errorf(cleanUrl), err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if !c.Bool("auth") && !c.Bool("force-pre-auth") {
|
||||||
|
return fmt.Errorf("auth required, but --auth flag not given")
|
||||||
|
}
|
||||||
|
kr, _, err := gatherKeyerFromArguments(ctx, c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
pk, _ := kr.GetPublicKey(ctx)
|
||||||
|
npub := nip19.EncodeNpub(pk)
|
||||||
|
log("authenticating as %s... ", color.YellowString("%s…%s", npub[0:7], npub[58:]))
|
||||||
|
|
||||||
|
return kr.SignEvent(ctx, authEvent)
|
||||||
}
|
}
|
||||||
|
|
||||||
func lineProcessingError(ctx context.Context, msg string, args ...any) context.Context {
|
func lineProcessingError(ctx context.Context, msg string, args ...any) context.Context {
|
||||||
@@ -180,106 +360,6 @@ func exitIfLineProcessingError(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func gatherSecretKeyOrBunkerFromArguments(ctx context.Context, c *cli.Command) (string, *nip46.BunkerClient, error) {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
if bunkerURL := c.String("connect"); bunkerURL != "" {
|
|
||||||
clientKey := c.String("connect-as")
|
|
||||||
if clientKey != "" {
|
|
||||||
clientKey = strings.Repeat("0", 64-len(clientKey)) + clientKey
|
|
||||||
} else {
|
|
||||||
clientKey = nostr.GeneratePrivateKey()
|
|
||||||
}
|
|
||||||
bunker, err := nip46.ConnectBunker(ctx, clientKey, bunkerURL, nil, func(s string) {
|
|
||||||
fmt.Fprintf(color.Error, color.CyanString("[nip46]: open the following URL: %s"), s)
|
|
||||||
})
|
|
||||||
return "", bunker, err
|
|
||||||
}
|
|
||||||
|
|
||||||
sec := c.String("sec")
|
|
||||||
|
|
||||||
// check in the environment for the secret key
|
|
||||||
if sec == "" {
|
|
||||||
if key, ok := os.LookupEnv("NOSTR_PRIVATE_KEY"); ok {
|
|
||||||
sec = key
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.Bool("prompt-sec") {
|
|
||||||
if isPiped() {
|
|
||||||
return "", nil, fmt.Errorf("can't prompt for a secret key when processing data from a pipe, try again without --prompt-sec")
|
|
||||||
}
|
|
||||||
sec, err = askPassword("type your secret key as ncryptsec, nsec or hex: ", nil)
|
|
||||||
if err != nil {
|
|
||||||
return "", nil, fmt.Errorf("failed to get secret key: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(sec, "ncryptsec1") {
|
|
||||||
sec, err = promptDecrypt(sec)
|
|
||||||
if err != nil {
|
|
||||||
return "", nil, fmt.Errorf("failed to decrypt: %w", err)
|
|
||||||
}
|
|
||||||
} else if bsec, err := hex.DecodeString(leftPadKey(sec)); err == nil {
|
|
||||||
sec = hex.EncodeToString(bsec)
|
|
||||||
} else if prefix, hexvalue, err := nip19.Decode(sec); err != nil {
|
|
||||||
return "", nil, fmt.Errorf("invalid nsec: %w", err)
|
|
||||||
} else if prefix == "nsec" {
|
|
||||||
sec = hexvalue.(string)
|
|
||||||
}
|
|
||||||
|
|
||||||
if ok := nostr.IsValid32ByteHex(sec); !ok {
|
|
||||||
return "", nil, fmt.Errorf("invalid secret key")
|
|
||||||
}
|
|
||||||
return sec, nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func promptDecrypt(ncryptsec string) (string, error) {
|
|
||||||
for i := 1; i < 4; i++ {
|
|
||||||
var attemptStr string
|
|
||||||
if i > 1 {
|
|
||||||
attemptStr = fmt.Sprintf(" [%d/3]", i)
|
|
||||||
}
|
|
||||||
password, err := askPassword("type the password to decrypt your secret key"+attemptStr+": ", nil)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
sec, err := nip49.Decrypt(ncryptsec, password)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return sec, nil
|
|
||||||
}
|
|
||||||
return "", fmt.Errorf("couldn't decrypt private key")
|
|
||||||
}
|
|
||||||
|
|
||||||
func askPassword(msg string, shouldAskAgain func(answer string) bool) (string, error) {
|
|
||||||
config := &readline.Config{
|
|
||||||
Stdout: color.Error,
|
|
||||||
Prompt: color.YellowString(msg),
|
|
||||||
InterruptPrompt: "^C",
|
|
||||||
DisableAutoSaveHistory: true,
|
|
||||||
EnableMask: true,
|
|
||||||
MaskRune: '*',
|
|
||||||
}
|
|
||||||
|
|
||||||
rl, err := readline.NewEx(config)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
|
||||||
answer, err := rl.Readline()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
answer = strings.TrimSpace(answer)
|
|
||||||
if shouldAskAgain != nil && shouldAskAgain(answer) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return answer, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||||
|
|
||||||
func randString(n int) string {
|
func randString(n int) string {
|
||||||
@@ -290,6 +370,113 @@ func randString(n int) string {
|
|||||||
return string(b)
|
return string(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
func leftPadKey(k string) string {
|
func unwrapAll(err error) error {
|
||||||
return strings.Repeat("0", 64-len(k)) + k
|
low := err
|
||||||
|
for n := low; n != nil; n = errors.Unwrap(low) {
|
||||||
|
low = n
|
||||||
|
}
|
||||||
|
return low
|
||||||
|
}
|
||||||
|
|
||||||
|
func clampMessage(msg string, prefixAlreadyPrinted int) string {
|
||||||
|
termSize, _, _ := term.GetSize(int(os.Stderr.Fd()))
|
||||||
|
|
||||||
|
prf := "expected handshake response status code 101 but got "
|
||||||
|
if len(msg) > len(prf) && msg[0:len(prf)] == prf {
|
||||||
|
msg = "status " + msg[len(prf):]
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(msg) > termSize-prefixAlreadyPrinted && prefixAlreadyPrinted+1 < termSize {
|
||||||
|
msg = msg[0:termSize-prefixAlreadyPrinted-1] + "…"
|
||||||
|
}
|
||||||
|
|
||||||
|
return msg
|
||||||
|
}
|
||||||
|
|
||||||
|
func clampError(err error, prefixAlreadyPrinted int) string {
|
||||||
|
termSize, _, _ := term.GetSize(0)
|
||||||
|
msg := err.Error()
|
||||||
|
if len(msg) > termSize-prefixAlreadyPrinted {
|
||||||
|
err = unwrapAll(err)
|
||||||
|
msg = clampMessage(err.Error(), prefixAlreadyPrinted)
|
||||||
|
}
|
||||||
|
return msg
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendUnique[A comparable](list []A, newEls ...A) []A {
|
||||||
|
ex:
|
||||||
|
for _, newEl := range newEls {
|
||||||
|
for _, el := range list {
|
||||||
|
if el == newEl {
|
||||||
|
continue ex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
list = append(list, newEl)
|
||||||
|
}
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
func askConfirmation(msg string) bool {
|
||||||
|
if isPiped() {
|
||||||
|
tty, err := tty.Open()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
defer tty.Close()
|
||||||
|
|
||||||
|
log(color.YellowString(msg))
|
||||||
|
answer, err := tty.ReadString()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// print newline after password input
|
||||||
|
fmt.Fprintln(os.Stderr)
|
||||||
|
|
||||||
|
answer = strings.TrimSpace(string(answer))
|
||||||
|
return answer == "y" || answer == "yes"
|
||||||
|
} else {
|
||||||
|
config := &readline.Config{
|
||||||
|
Stdout: color.Error,
|
||||||
|
Prompt: color.YellowString(msg),
|
||||||
|
InterruptPrompt: "^C",
|
||||||
|
DisableAutoSaveHistory: true,
|
||||||
|
EnableMask: false,
|
||||||
|
MaskRune: '*',
|
||||||
|
}
|
||||||
|
|
||||||
|
rl, err := readline.NewEx(config)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
answer, err := rl.Readline()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
answer = strings.ToLower(strings.TrimSpace(answer))
|
||||||
|
return answer == "y" || answer == "yes"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var colors = struct {
|
||||||
|
reset func(...any) (int, error)
|
||||||
|
italic func(...any) string
|
||||||
|
italicf func(string, ...any) string
|
||||||
|
bold func(...any) string
|
||||||
|
boldf func(string, ...any) string
|
||||||
|
error func(...any) string
|
||||||
|
errorf func(string, ...any) string
|
||||||
|
success func(...any) string
|
||||||
|
successf func(string, ...any) string
|
||||||
|
}{
|
||||||
|
color.New(color.Reset).Print,
|
||||||
|
color.New(color.Italic).Sprint,
|
||||||
|
color.New(color.Italic).Sprintf,
|
||||||
|
color.New(color.Bold).Sprint,
|
||||||
|
color.New(color.Bold).Sprintf,
|
||||||
|
color.New(color.Bold, color.FgHiRed).Sprint,
|
||||||
|
color.New(color.Bold, color.FgHiRed).Sprintf,
|
||||||
|
color.New(color.Bold, color.FgHiGreen).Sprint,
|
||||||
|
color.New(color.Bold, color.FgHiGreen).Sprintf,
|
||||||
}
|
}
|
||||||
|
|||||||
192
helpers_key.go
Normal file
192
helpers_key.go
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"fiatjaf.com/nostr"
|
||||||
|
"fiatjaf.com/nostr/keyer"
|
||||||
|
"fiatjaf.com/nostr/nip19"
|
||||||
|
"fiatjaf.com/nostr/nip46"
|
||||||
|
"fiatjaf.com/nostr/nip49"
|
||||||
|
"github.com/chzyer/readline"
|
||||||
|
"github.com/fatih/color"
|
||||||
|
"github.com/mattn/go-tty"
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
var defaultKeyFlags = []cli.Flag{
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "sec",
|
||||||
|
Usage: "secret key to sign the event, as nsec, ncryptsec or hex, or a bunker URL",
|
||||||
|
DefaultText: "the key '1'",
|
||||||
|
Category: CATEGORY_SIGNER,
|
||||||
|
Sources: cli.EnvVars("NOSTR_SECRET_KEY"),
|
||||||
|
Value: nostr.KeyOne.Hex(),
|
||||||
|
HideDefault: true,
|
||||||
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "prompt-sec",
|
||||||
|
Usage: "prompt the user to paste a hex or nsec with which to sign the event",
|
||||||
|
Category: CATEGORY_SIGNER,
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "connect-as",
|
||||||
|
Usage: "private key to use when communicating with nip46 bunkers",
|
||||||
|
DefaultText: "a random key",
|
||||||
|
Category: CATEGORY_SIGNER,
|
||||||
|
Sources: cli.EnvVars("NOSTR_CLIENT_KEY"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func gatherKeyerFromArguments(ctx context.Context, c *cli.Command) (nostr.Keyer, nostr.SecretKey, error) {
|
||||||
|
key, bunker, err := gatherSecretKeyOrBunkerFromArguments(ctx, c)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nostr.SecretKey{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var kr nostr.Keyer
|
||||||
|
if bunker != nil {
|
||||||
|
kr = keyer.NewBunkerSignerFromBunkerClient(bunker)
|
||||||
|
} else {
|
||||||
|
kr = keyer.NewPlainKeySigner(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
return kr, key, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func gatherSecretKeyOrBunkerFromArguments(ctx context.Context, c *cli.Command) (nostr.SecretKey, *nip46.BunkerClient, error) {
|
||||||
|
sec := c.String("sec")
|
||||||
|
if strings.HasPrefix(sec, "bunker://") {
|
||||||
|
// it's a bunker
|
||||||
|
bunkerURL := sec
|
||||||
|
clientKeyHex := c.String("connect-as")
|
||||||
|
var clientKey nostr.SecretKey
|
||||||
|
|
||||||
|
if clientKeyHex != "" {
|
||||||
|
var err error
|
||||||
|
clientKey, err = nostr.SecretKeyFromHex(clientKeyHex)
|
||||||
|
if err != nil {
|
||||||
|
return nostr.SecretKey{}, nil, fmt.Errorf("bunker client key '%s' is invalid: %w", clientKeyHex, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
clientKey = nostr.Generate()
|
||||||
|
}
|
||||||
|
|
||||||
|
logverbose("[nip46]: connecting to %s with client key %s", bunkerURL, clientKey.Hex())
|
||||||
|
|
||||||
|
bunker, err := nip46.ConnectBunker(ctx, clientKey, bunkerURL, nil, func(s string) {
|
||||||
|
log(color.CyanString("[nip46]: open the following URL: %s"), s)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nostr.SecretKey{}, nil, fmt.Errorf("failed to connect to %s: %w", bunkerURL, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nostr.SecretKey{}, bunker, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Bool("prompt-sec") {
|
||||||
|
var err error
|
||||||
|
sec, err = askPassword("type your secret key as ncryptsec, nsec or hex: ", nil)
|
||||||
|
if err != nil {
|
||||||
|
return nostr.SecretKey{}, nil, fmt.Errorf("failed to get secret key: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(sec, "ncryptsec1") {
|
||||||
|
sk, err := promptDecrypt(sec)
|
||||||
|
if err != nil {
|
||||||
|
return nostr.SecretKey{}, nil, fmt.Errorf("failed to decrypt: %w", err)
|
||||||
|
}
|
||||||
|
return sk, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if prefix, ski, err := nip19.Decode(sec); err == nil && prefix == "nsec" {
|
||||||
|
return ski.(nostr.SecretKey), nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
sk, err := nostr.SecretKeyFromHex(sec)
|
||||||
|
if err != nil {
|
||||||
|
return nostr.SecretKey{}, nil, fmt.Errorf("invalid secret key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return sk, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func promptDecrypt(ncryptsec string) (nostr.SecretKey, error) {
|
||||||
|
for i := 1; i < 4; i++ {
|
||||||
|
var attemptStr string
|
||||||
|
if i > 1 {
|
||||||
|
attemptStr = fmt.Sprintf(" [%d/3]", i)
|
||||||
|
}
|
||||||
|
password, err := askPassword("type the password to decrypt your secret key"+attemptStr+": ", nil)
|
||||||
|
if err != nil {
|
||||||
|
return nostr.SecretKey{}, err
|
||||||
|
}
|
||||||
|
sec, err := nip49.Decrypt(ncryptsec, password)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return sec, nil
|
||||||
|
}
|
||||||
|
return nostr.SecretKey{}, fmt.Errorf("couldn't decrypt private key")
|
||||||
|
}
|
||||||
|
|
||||||
|
func askPassword(msg string, shouldAskAgain func(answer string) bool) (string, error) {
|
||||||
|
if isPiped() {
|
||||||
|
// use TTY method when stdin is piped
|
||||||
|
tty, err := tty.Open()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("can't prompt for a secret key when processing data from a pipe on this system (failed to open /dev/tty: %w), try again without --prompt-sec or provide the key via --sec or NOSTR_SECRET_KEY environment variable", err)
|
||||||
|
}
|
||||||
|
defer tty.Close()
|
||||||
|
for {
|
||||||
|
// print the prompt to stderr so it's visible to the user
|
||||||
|
log(color.YellowString(msg))
|
||||||
|
|
||||||
|
// read password from TTY with masking
|
||||||
|
password, err := tty.ReadPassword()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// print newline after password input
|
||||||
|
fmt.Fprintln(os.Stderr)
|
||||||
|
|
||||||
|
answer := strings.TrimSpace(string(password))
|
||||||
|
if shouldAskAgain != nil && shouldAskAgain(answer) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return answer, nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// use normal readline method when stdin is not piped
|
||||||
|
config := &readline.Config{
|
||||||
|
Stdout: os.Stderr,
|
||||||
|
Prompt: color.YellowString(msg),
|
||||||
|
InterruptPrompt: "^C",
|
||||||
|
DisableAutoSaveHistory: true,
|
||||||
|
EnableMask: true,
|
||||||
|
MaskRune: '*',
|
||||||
|
}
|
||||||
|
|
||||||
|
rl, err := readline.NewEx(config)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
answer, err := rl.Readline()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
answer = strings.TrimSpace(answer)
|
||||||
|
if shouldAskAgain != nil && shouldAskAgain(answer) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return answer, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
5
justfile
Normal file
5
justfile
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
test:
|
||||||
|
#!/usr/bin/env fish
|
||||||
|
for test in (go test -list .)
|
||||||
|
go test -run=$test -v
|
||||||
|
end
|
||||||
83
key.go
83
key.go
@@ -3,30 +3,28 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"fiatjaf.com/nostr"
|
||||||
|
"fiatjaf.com/nostr/nip19"
|
||||||
|
"fiatjaf.com/nostr/nip49"
|
||||||
"github.com/btcsuite/btcd/btcec/v2"
|
"github.com/btcsuite/btcd/btcec/v2"
|
||||||
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
|
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
|
||||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||||
"github.com/fiatjaf/cli/v3"
|
"github.com/urfave/cli/v3"
|
||||||
"github.com/nbd-wtf/go-nostr"
|
|
||||||
"github.com/nbd-wtf/go-nostr/nip19"
|
|
||||||
"github.com/nbd-wtf/go-nostr/nip49"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var key = &cli.Command{
|
var key = &cli.Command{
|
||||||
Name: "key",
|
Name: "key",
|
||||||
Usage: "operations on secret keys: generate, derive, encrypt, decrypt.",
|
Usage: "operations on secret keys: generate, derive, encrypt, decrypt",
|
||||||
Description: ``,
|
Description: ``,
|
||||||
DisableSliceFlagSeparator: true,
|
DisableSliceFlagSeparator: true,
|
||||||
Commands: []*cli.Command{
|
Commands: []*cli.Command{
|
||||||
generate,
|
generate,
|
||||||
public,
|
public,
|
||||||
encrypt,
|
encryptKey,
|
||||||
decrypt,
|
decryptKey,
|
||||||
combine,
|
combine,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -37,8 +35,8 @@ var generate = &cli.Command{
|
|||||||
Description: ``,
|
Description: ``,
|
||||||
DisableSliceFlagSeparator: true,
|
DisableSliceFlagSeparator: true,
|
||||||
Action: func(ctx context.Context, c *cli.Command) error {
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
sec := nostr.GeneratePrivateKey()
|
sec := nostr.Generate()
|
||||||
stdout(sec)
|
stdout(sec.Hex())
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -49,23 +47,30 @@ var public = &cli.Command{
|
|||||||
Description: ``,
|
Description: ``,
|
||||||
ArgsUsage: "[secret]",
|
ArgsUsage: "[secret]",
|
||||||
DisableSliceFlagSeparator: true,
|
DisableSliceFlagSeparator: true,
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "with-parity",
|
||||||
|
Usage: "output 33 bytes instead of 32, the first one being either '02' or '03', a prefix indicating whether this pubkey is even or odd.",
|
||||||
|
},
|
||||||
|
},
|
||||||
Action: func(ctx context.Context, c *cli.Command) error {
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
for sec := range getSecretKeysFromStdinLinesOrSlice(ctx, c, c.Args().Slice()) {
|
for sk := range getSecretKeysFromStdinLinesOrSlice(ctx, c, c.Args().Slice()) {
|
||||||
pubkey, err := nostr.GetPublicKey(sec)
|
_, pk := btcec.PrivKeyFromBytes(sk[:])
|
||||||
if err != nil {
|
|
||||||
ctx = lineProcessingError(ctx, "failed to derive public key: %s", err)
|
if c.Bool("with-parity") {
|
||||||
continue
|
stdout(hex.EncodeToString(pk.SerializeCompressed()))
|
||||||
|
} else {
|
||||||
|
stdout(hex.EncodeToString(pk.SerializeCompressed()[1:]))
|
||||||
}
|
}
|
||||||
stdout(pubkey)
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
var encrypt = &cli.Command{
|
var encryptKey = &cli.Command{
|
||||||
Name: "encrypt",
|
Name: "encrypt",
|
||||||
Usage: "encrypts a secret key and prints an ncryptsec code",
|
Usage: "encrypts a secret key and prints an ncryptsec code",
|
||||||
Description: `uses the NIP-49 standard.`,
|
Description: `uses the nip49 standard.`,
|
||||||
ArgsUsage: "<secret> <password>",
|
ArgsUsage: "<secret> <password>",
|
||||||
DisableSliceFlagSeparator: true,
|
DisableSliceFlagSeparator: true,
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
@@ -101,10 +106,10 @@ var encrypt = &cli.Command{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
var decrypt = &cli.Command{
|
var decryptKey = &cli.Command{
|
||||||
Name: "decrypt",
|
Name: "decrypt",
|
||||||
Usage: "takes an ncrypsec and a password and decrypts it into an nsec",
|
Usage: "takes an ncrypsec and a password and decrypts it into an nsec",
|
||||||
Description: `uses the NIP-49 standard.`,
|
Description: `uses the nip49 standard.`,
|
||||||
ArgsUsage: "<ncryptsec-code> <password>",
|
ArgsUsage: "<ncryptsec-code> <password>",
|
||||||
DisableSliceFlagSeparator: true,
|
DisableSliceFlagSeparator: true,
|
||||||
Action: func(ctx context.Context, c *cli.Command) error {
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
@@ -117,30 +122,30 @@ var decrypt = &cli.Command{
|
|||||||
if password == "" {
|
if password == "" {
|
||||||
return fmt.Errorf("no password given")
|
return fmt.Errorf("no password given")
|
||||||
}
|
}
|
||||||
sec, err := nip49.Decrypt(ncryptsec, password)
|
sk, err := nip49.Decrypt(ncryptsec, password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to decrypt: %s", err)
|
return fmt.Errorf("failed to decrypt: %s", err)
|
||||||
}
|
}
|
||||||
stdout(sec)
|
stdout(sk.Hex())
|
||||||
return nil
|
return nil
|
||||||
case 1:
|
case 1:
|
||||||
if arg := c.Args().Get(0); strings.HasPrefix(arg, "ncryptsec1") {
|
if arg := c.Args().Get(0); strings.HasPrefix(arg, "ncryptsec1") {
|
||||||
ncryptsec = arg
|
ncryptsec = arg
|
||||||
if res, err := promptDecrypt(ncryptsec); err != nil {
|
if sk, err := promptDecrypt(ncryptsec); err != nil {
|
||||||
return err
|
return err
|
||||||
} else {
|
} else {
|
||||||
stdout(res)
|
stdout(sk.Hex())
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
password = c.Args().Get(0)
|
password = c.Args().Get(0)
|
||||||
for ncryptsec := range getStdinLinesOrArgumentsFromSlice([]string{ncryptsec}) {
|
for ncryptsec := range getStdinLinesOrArgumentsFromSlice([]string{ncryptsec}) {
|
||||||
sec, err := nip49.Decrypt(ncryptsec, password)
|
sk, err := nip49.Decrypt(ncryptsec, password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx = lineProcessingError(ctx, "failed to decrypt: %s", err)
|
ctx = lineProcessingError(ctx, "failed to decrypt: %s", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
stdout(sec)
|
stdout(sk.Hex())
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -188,7 +193,7 @@ However, if the intent is to check if two existing Nostr pubkeys match a given c
|
|||||||
for i, prefix := range []byte{0x02, 0x03} {
|
for i, prefix := range []byte{0x02, 0x03} {
|
||||||
pubk, err := btcec.ParsePubKey(append([]byte{prefix}, keyb...))
|
pubk, err := btcec.ParsePubKey(append([]byte{prefix}, keyb...))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "error parsing key %s: %s", keyhex, err)
|
log("error parsing key %s: %s", keyhex, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
group[i] = pubk
|
group[i] = pubk
|
||||||
@@ -229,7 +234,7 @@ However, if the intent is to check if two existing Nostr pubkeys match a given c
|
|||||||
|
|
||||||
agg, _, _, err := musig2.AggregateKeys(combining, true)
|
agg, _, _, err := musig2.AggregateKeys(combining, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "error aggregating: %s", err)
|
log("error aggregating: %s", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,33 +257,37 @@ However, if the intent is to check if two existing Nostr pubkeys match a given c
|
|||||||
}
|
}
|
||||||
|
|
||||||
res, _ := json.MarshalIndent(result, "", " ")
|
res, _ := json.MarshalIndent(result, "", " ")
|
||||||
fmt.Println(string(res))
|
stdout(string(res))
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func getSecretKeysFromStdinLinesOrSlice(ctx context.Context, c *cli.Command, keys []string) chan string {
|
func getSecretKeysFromStdinLinesOrSlice(ctx context.Context, _ *cli.Command, keys []string) chan nostr.SecretKey {
|
||||||
ch := make(chan string)
|
ch := make(chan nostr.SecretKey)
|
||||||
go func() {
|
go func() {
|
||||||
for sec := range getStdinLinesOrArgumentsFromSlice(keys) {
|
for sec := range getStdinLinesOrArgumentsFromSlice(keys) {
|
||||||
if sec == "" {
|
if sec == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var sk nostr.SecretKey
|
||||||
if strings.HasPrefix(sec, "nsec1") {
|
if strings.HasPrefix(sec, "nsec1") {
|
||||||
_, data, err := nip19.Decode(sec)
|
_, data, err := nip19.Decode(sec)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx = lineProcessingError(ctx, "invalid nsec code: %s", err)
|
ctx = lineProcessingError(ctx, "invalid nsec code: %s", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
sec = data.(string)
|
sk = data.(nostr.SecretKey)
|
||||||
}
|
}
|
||||||
sec = leftPadKey(sec)
|
|
||||||
if !nostr.IsValid32ByteHex(sec) {
|
sk, err := nostr.SecretKeyFromHex(sec)
|
||||||
ctx = lineProcessingError(ctx, "invalid hex key")
|
if err != nil {
|
||||||
|
ctx = lineProcessingError(ctx, "invalid hex key: %s", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
ch <- sec
|
|
||||||
|
ch <- sk
|
||||||
}
|
}
|
||||||
close(ch)
|
close(ch)
|
||||||
}()
|
}()
|
||||||
|
|||||||
100
main.go
100
main.go
@@ -2,23 +2,34 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/textproto"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/fiatjaf/cli/v3"
|
"fiatjaf.com/nostr"
|
||||||
|
"fiatjaf.com/nostr/sdk"
|
||||||
|
"github.com/fatih/color"
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
version string = "debug"
|
||||||
|
isVerbose bool = false
|
||||||
)
|
)
|
||||||
|
|
||||||
var app = &cli.Command{
|
var app = &cli.Command{
|
||||||
Name: "nak",
|
Name: "nak",
|
||||||
Suggest: true,
|
Suggest: true,
|
||||||
UseShortOptionHandling: true,
|
UseShortOptionHandling: true,
|
||||||
AllowFlagsAfterArguments: true,
|
|
||||||
Usage: "the nostr army knife command-line tool",
|
Usage: "the nostr army knife command-line tool",
|
||||||
DisableSliceFlagSeparator: true,
|
DisableSliceFlagSeparator: true,
|
||||||
Commands: []*cli.Command{
|
Commands: []*cli.Command{
|
||||||
req,
|
|
||||||
count,
|
|
||||||
fetch,
|
|
||||||
event,
|
event,
|
||||||
|
req,
|
||||||
|
filter,
|
||||||
|
fetch,
|
||||||
|
count,
|
||||||
decode,
|
decode,
|
||||||
encode,
|
encode,
|
||||||
key,
|
key,
|
||||||
@@ -26,30 +37,97 @@ var app = &cli.Command{
|
|||||||
relay,
|
relay,
|
||||||
bunker,
|
bunker,
|
||||||
serve,
|
serve,
|
||||||
|
blossomCmd,
|
||||||
|
encrypt,
|
||||||
|
decrypt,
|
||||||
|
outbox,
|
||||||
|
wallet,
|
||||||
|
mcpServer,
|
||||||
|
curl,
|
||||||
|
fsCmd,
|
||||||
|
publish,
|
||||||
},
|
},
|
||||||
|
Version: version,
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "config-path",
|
||||||
|
Hidden: true,
|
||||||
|
},
|
||||||
&cli.BoolFlag{
|
&cli.BoolFlag{
|
||||||
Name: "quiet",
|
Name: "quiet",
|
||||||
Usage: "do not print logs and info messages to stderr, use -qq to also not print anything to stdout",
|
Usage: "do not print logs and info messages to stderr, use -qq to also not print anything to stdout",
|
||||||
Aliases: []string{"q"},
|
Aliases: []string{"q"},
|
||||||
Persistent: true,
|
|
||||||
Action: func(ctx context.Context, c *cli.Command, b bool) error {
|
Action: func(ctx context.Context, c *cli.Command, b bool) error {
|
||||||
q := c.Count("quiet")
|
q := c.Count("quiet")
|
||||||
if q >= 1 {
|
if q >= 1 {
|
||||||
log = func(msg string, args ...any) {}
|
log = func(msg string, args ...any) {}
|
||||||
if q >= 2 {
|
if q >= 2 {
|
||||||
stdout = func(_ ...any) (int, error) { return 0, nil }
|
stdout = func(_ ...any) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "verbose",
|
||||||
|
Usage: "print more stuff than normally",
|
||||||
|
Aliases: []string{"v"},
|
||||||
|
Action: func(ctx context.Context, c *cli.Command, b bool) error {
|
||||||
|
v := c.Count("verbose")
|
||||||
|
if v >= 1 {
|
||||||
|
logverbose = log
|
||||||
|
isVerbose = true
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Before: func(ctx context.Context, c *cli.Command) (context.Context, error) {
|
||||||
|
sys = sdk.NewSystem()
|
||||||
|
|
||||||
|
if err := initializeOutboxHintsDB(c, sys); err != nil {
|
||||||
|
return ctx, fmt.Errorf("failed to initialize outbox hints: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sys.Pool = nostr.NewPool(nostr.PoolOptions{
|
||||||
|
AuthorKindQueryMiddleware: sys.TrackQueryAttempts,
|
||||||
|
EventMiddleware: sys.TrackEventHints,
|
||||||
|
RelayOptions: nostr.RelayOptions{
|
||||||
|
RequestHeader: http.Header{textproto.CanonicalMIMEHeaderKey("user-agent"): {"nak/b"}},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return ctx, nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
cli.VersionFlag = &cli.BoolFlag{
|
||||||
|
Name: "version",
|
||||||
|
Usage: "prints the version",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
defer colors.reset()
|
||||||
|
|
||||||
|
// a megahack to enable this curl command proxy
|
||||||
|
if len(os.Args) > 2 && os.Args[1] == "curl" {
|
||||||
|
if err := realCurl(); err != nil {
|
||||||
|
if err != nil {
|
||||||
|
log(color.YellowString(err.Error()) + "\n")
|
||||||
|
}
|
||||||
|
colors.reset()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if err := app.Run(context.Background(), os.Args); err != nil {
|
if err := app.Run(context.Background(), os.Args); err != nil {
|
||||||
stdout(err)
|
if err != nil {
|
||||||
|
log("%s\n", color.YellowString(err.Error()))
|
||||||
|
}
|
||||||
|
colors.reset()
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
264
mcp.go
Normal file
264
mcp.go
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"fiatjaf.com/nostr"
|
||||||
|
"fiatjaf.com/nostr/nip19"
|
||||||
|
"fiatjaf.com/nostr/sdk"
|
||||||
|
"github.com/mark3labs/mcp-go/mcp"
|
||||||
|
"github.com/mark3labs/mcp-go/server"
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
var mcpServer = &cli.Command{
|
||||||
|
Name: "mcp",
|
||||||
|
Usage: "pander to the AI gods",
|
||||||
|
Description: ``,
|
||||||
|
DisableSliceFlagSeparator: true,
|
||||||
|
Flags: append(
|
||||||
|
defaultKeyFlags,
|
||||||
|
),
|
||||||
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
|
s := server.NewMCPServer(
|
||||||
|
"nak",
|
||||||
|
version,
|
||||||
|
)
|
||||||
|
|
||||||
|
keyer, _, err := gatherKeyerFromArguments(ctx, c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.AddTool(mcp.NewTool("publish_note",
|
||||||
|
mcp.WithDescription("Publish a short note event to Nostr with the given text content"),
|
||||||
|
mcp.WithString("content", mcp.Description("Arbitrary string to be published"), mcp.Required()),
|
||||||
|
mcp.WithString("relay", mcp.Description("Relay to publish the note to")),
|
||||||
|
mcp.WithString("mention", mcp.Description("Nostr user's public key to be mentioned")),
|
||||||
|
), func(ctx context.Context, r mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
content := required[string](r, "content")
|
||||||
|
mention, _ := optional[string](r, "mention")
|
||||||
|
relay, _ := optional[string](r, "relay")
|
||||||
|
|
||||||
|
var relays []string
|
||||||
|
|
||||||
|
evt := nostr.Event{
|
||||||
|
Kind: 1,
|
||||||
|
Tags: nostr.Tags{{"client", "goose/nak"}},
|
||||||
|
Content: content,
|
||||||
|
CreatedAt: nostr.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if mention != "" {
|
||||||
|
pk, err := nostr.PubKeyFromHex(mention)
|
||||||
|
if err != nil {
|
||||||
|
return mcp.NewToolResultError("the given mention isn't a valid public key, it must be 32 bytes hex, like the ones returned by search_profile. Got error: " + err.Error()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
evt.Tags = append(evt.Tags, nostr.Tag{"p", pk.Hex()})
|
||||||
|
// their inbox relays
|
||||||
|
relays = sys.FetchInboxRelays(ctx, pk, 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := keyer.SignEvent(ctx, &evt); err != nil {
|
||||||
|
return mcp.NewToolResultError("it was impossible to sign the event, so we can't proceed to publishwith publishing it."), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// our write relays
|
||||||
|
relays = append(relays, sys.FetchOutboxRelays(ctx, evt.PubKey, 3)...)
|
||||||
|
|
||||||
|
if len(relays) == 0 {
|
||||||
|
relays = []string{"nos.lol", "relay.damus.io"}
|
||||||
|
}
|
||||||
|
|
||||||
|
// extra relay specified
|
||||||
|
if relay != "" {
|
||||||
|
relays = append(relays, relay)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := strings.Builder{}
|
||||||
|
result.WriteString(
|
||||||
|
fmt.Sprintf("the event we generated has id '%s', kind '%d' and is signed by pubkey '%s'. ",
|
||||||
|
evt.ID,
|
||||||
|
evt.Kind,
|
||||||
|
evt.PubKey,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
for res := range sys.Pool.PublishMany(ctx, relays, evt) {
|
||||||
|
if res.Error != nil {
|
||||||
|
result.WriteString(
|
||||||
|
fmt.Sprintf("there was an error publishing the event to the relay %s. ",
|
||||||
|
res.RelayURL),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
result.WriteString(
|
||||||
|
fmt.Sprintf("the event was successfully published to the relay %s. ",
|
||||||
|
res.RelayURL),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mcp.NewToolResultText(result.String()), nil
|
||||||
|
})
|
||||||
|
|
||||||
|
s.AddTool(mcp.NewTool("resolve_nostr_uri",
|
||||||
|
mcp.WithDescription("Resolve URIs prefixed with nostr:, including nostr:nevent1..., nostr:npub1..., nostr:nprofile1... and nostr:naddr1..."),
|
||||||
|
mcp.WithString("uri", mcp.Description("URI to be resolved"), mcp.Required()),
|
||||||
|
), func(ctx context.Context, r mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
uri := required[string](r, "uri")
|
||||||
|
if strings.HasPrefix(uri, "nostr:") {
|
||||||
|
uri = uri[6:]
|
||||||
|
}
|
||||||
|
|
||||||
|
prefix, data, err := nip19.Decode(uri)
|
||||||
|
if err != nil {
|
||||||
|
return mcp.NewToolResultError("this Nostr uri is invalid"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch prefix {
|
||||||
|
case "npub":
|
||||||
|
pm := sys.FetchProfileMetadata(ctx, data.(nostr.PubKey))
|
||||||
|
return mcp.NewToolResultText(
|
||||||
|
fmt.Sprintf("this is a Nostr profile named '%s', their public key is '%s'",
|
||||||
|
pm.ShortName(), pm.PubKey),
|
||||||
|
), nil
|
||||||
|
case "nprofile":
|
||||||
|
pm, _ := sys.FetchProfileFromInput(ctx, uri)
|
||||||
|
return mcp.NewToolResultText(
|
||||||
|
fmt.Sprintf("this is a Nostr profile named '%s', their public key is '%s'",
|
||||||
|
pm.ShortName(), pm.PubKey),
|
||||||
|
), nil
|
||||||
|
case "nevent":
|
||||||
|
event, _, err := sys.FetchSpecificEventFromInput(ctx, uri, sdk.FetchSpecificEventParameters{
|
||||||
|
WithRelays: false,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return mcp.NewToolResultError("Couldn't find this event anywhere"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return mcp.NewToolResultText(
|
||||||
|
fmt.Sprintf("this is a Nostr event: %s", event),
|
||||||
|
), nil
|
||||||
|
case "naddr":
|
||||||
|
return mcp.NewToolResultError("For now we can't handle this kind of Nostr uri"), nil
|
||||||
|
default:
|
||||||
|
return mcp.NewToolResultError("We don't know how to handle this Nostr uri"), nil
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
s.AddTool(mcp.NewTool("search_profile",
|
||||||
|
mcp.WithDescription("Search for the public key of a Nostr user given their name"),
|
||||||
|
mcp.WithString("name", mcp.Description("Name to be searched"), mcp.Required()),
|
||||||
|
mcp.WithNumber("limit", mcp.Description("How many results to return")),
|
||||||
|
), func(ctx context.Context, r mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
name := required[string](r, "name")
|
||||||
|
limit, _ := optional[float64](r, "limit")
|
||||||
|
|
||||||
|
filter := nostr.Filter{Search: name, Kinds: []nostr.Kind{0}}
|
||||||
|
if limit > 0 {
|
||||||
|
filter.Limit = int(limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
res := strings.Builder{}
|
||||||
|
res.WriteString("Search results: ")
|
||||||
|
l := 0
|
||||||
|
for result := range sys.Pool.FetchMany(ctx, []string{"relay.nostr.band", "nostr.wine"}, filter, nostr.SubscriptionOptions{}) {
|
||||||
|
l++
|
||||||
|
pm, _ := sdk.ParseMetadata(result.Event)
|
||||||
|
res.WriteString(fmt.Sprintf("\n\nResult %d\nUser name: \"%s\"\nPublic key: \"%s\"\nDescription: \"%s\"\n",
|
||||||
|
l, pm.ShortName(), pm.PubKey.Hex(), pm.About))
|
||||||
|
|
||||||
|
if l >= int(limit) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if l == 0 {
|
||||||
|
return mcp.NewToolResultError("Couldn't find anyone with that name."), nil
|
||||||
|
}
|
||||||
|
return mcp.NewToolResultText(res.String()), nil
|
||||||
|
})
|
||||||
|
|
||||||
|
s.AddTool(mcp.NewTool("get_outbox_relay_for_pubkey",
|
||||||
|
mcp.WithDescription("Get the best relay from where to read notes from a specific Nostr user"),
|
||||||
|
mcp.WithString("pubkey", mcp.Description("Public key of Nostr user we want to know the relay from where to read"), mcp.Required()),
|
||||||
|
), func(ctx context.Context, r mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
pubkey, err := nostr.PubKeyFromHex(required[string](r, "pubkey"))
|
||||||
|
if err != nil {
|
||||||
|
return mcp.NewToolResultError("the pubkey given isn't a valid public key, it must be 32 bytes hex, like the ones returned by search_profile. Got error: " + err.Error()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
res := sys.FetchOutboxRelays(ctx, pubkey, 1)
|
||||||
|
return mcp.NewToolResultText(res[0]), nil
|
||||||
|
})
|
||||||
|
|
||||||
|
s.AddTool(mcp.NewTool("read_events_from_relay",
|
||||||
|
mcp.WithDescription("Makes a REQ query to one relay using the specified parameters, this can be used to fetch notes from a profile"),
|
||||||
|
mcp.WithString("relay", mcp.Description("relay URL to send the query to"), mcp.Required()),
|
||||||
|
mcp.WithNumber("kind", mcp.Description("event kind number to include in the 'kinds' field"), mcp.Required()),
|
||||||
|
mcp.WithNumber("limit", mcp.Description("maximum number of events to query"), mcp.Required()),
|
||||||
|
mcp.WithString("pubkey", mcp.Description("pubkey to include in the 'authors' field, if this is not given we will read any events from this relay")),
|
||||||
|
), func(ctx context.Context, r mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
relay := required[string](r, "relay")
|
||||||
|
kind := int(required[float64](r, "kind"))
|
||||||
|
limit := int(required[float64](r, "limit"))
|
||||||
|
pubkey, hasPubKey := optional[string](r, "pubkey")
|
||||||
|
|
||||||
|
filter := nostr.Filter{
|
||||||
|
Limit: limit,
|
||||||
|
Kinds: []nostr.Kind{nostr.Kind(kind)},
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasPubKey {
|
||||||
|
if pk, err := nostr.PubKeyFromHex(pubkey); err != nil {
|
||||||
|
return mcp.NewToolResultError("the pubkey given isn't a valid public key, it must be 32 bytes hex, like the ones returned by search_profile. Got error: " + err.Error()), nil
|
||||||
|
} else {
|
||||||
|
filter.Authors = append(filter.Authors, pk)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
events := sys.Pool.FetchMany(ctx, []string{relay}, filter, nostr.SubscriptionOptions{})
|
||||||
|
|
||||||
|
result := strings.Builder{}
|
||||||
|
for ie := range events {
|
||||||
|
result.WriteString("author public key: ")
|
||||||
|
result.WriteString(ie.PubKey.Hex())
|
||||||
|
result.WriteString("content: '")
|
||||||
|
result.WriteString(ie.Content)
|
||||||
|
result.WriteString("'")
|
||||||
|
result.WriteString("\n---\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
return mcp.NewToolResultText(result.String()), nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return server.ServeStdio(s)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func required[T comparable](r mcp.CallToolRequest, p string) T {
|
||||||
|
var zero T
|
||||||
|
if _, ok := r.Params.Arguments[p]; !ok {
|
||||||
|
return zero
|
||||||
|
}
|
||||||
|
if _, ok := r.Params.Arguments[p].(T); !ok {
|
||||||
|
return zero
|
||||||
|
}
|
||||||
|
if r.Params.Arguments[p].(T) == zero {
|
||||||
|
return zero
|
||||||
|
}
|
||||||
|
return r.Params.Arguments[p].(T)
|
||||||
|
}
|
||||||
|
|
||||||
|
func optional[T any](r mcp.CallToolRequest, p string) (T, bool) {
|
||||||
|
var zero T
|
||||||
|
if _, ok := r.Params.Arguments[p]; !ok {
|
||||||
|
return zero, false
|
||||||
|
}
|
||||||
|
if _, ok := r.Params.Arguments[p].(T); !ok {
|
||||||
|
return zero, false
|
||||||
|
}
|
||||||
|
return r.Params.Arguments[p].(T), true
|
||||||
|
}
|
||||||
45
musig2.go
45
musig2.go
@@ -6,43 +6,42 @@ import (
|
|||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"fiatjaf.com/nostr"
|
||||||
"github.com/btcsuite/btcd/btcec/v2"
|
"github.com/btcsuite/btcd/btcec/v2"
|
||||||
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
|
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
|
||||||
"github.com/nbd-wtf/go-nostr"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func getMusigAggregatedKey(_ context.Context, keys []string) (string, error) {
|
func getMusigAggregatedKey(_ context.Context, keys []string) (nostr.PubKey, error) {
|
||||||
knownSigners := make([]*btcec.PublicKey, len(keys))
|
knownSigners := make([]*btcec.PublicKey, len(keys))
|
||||||
for i, spk := range keys {
|
for i, spk := range keys {
|
||||||
bpk, err := hex.DecodeString(spk)
|
bpk, err := hex.DecodeString(spk)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("'%s' is invalid hex: %w", spk, err)
|
return nostr.ZeroPK, fmt.Errorf("'%s' is invalid hex: %w", spk, err)
|
||||||
}
|
}
|
||||||
if len(bpk) == 32 {
|
if len(bpk) == 32 {
|
||||||
return "", fmt.Errorf("'%s' is missing the leading parity byte", spk)
|
return nostr.ZeroPK, fmt.Errorf("'%s' is missing the leading parity byte", spk)
|
||||||
}
|
}
|
||||||
pk, err := btcec.ParsePubKey(bpk)
|
pk, err := btcec.ParsePubKey(bpk)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("'%s' is not a valid pubkey: %w", spk, err)
|
return nostr.ZeroPK, fmt.Errorf("'%s' is not a valid pubkey: %w", spk, err)
|
||||||
}
|
}
|
||||||
knownSigners[i] = pk
|
knownSigners[i] = pk
|
||||||
}
|
}
|
||||||
|
|
||||||
aggpk, _, _, err := musig2.AggregateKeys(knownSigners, true)
|
aggpk, _, _, err := musig2.AggregateKeys(knownSigners, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("aggregation failed: %w", err)
|
return nostr.ZeroPK, fmt.Errorf("aggregation failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return hex.EncodeToString(aggpk.FinalKey.SerializeCompressed()[1:]), nil
|
return nostr.PubKey(aggpk.FinalKey.SerializeCompressed()[1:]), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func performMusig(
|
func performMusig(
|
||||||
_ context.Context,
|
_ context.Context,
|
||||||
sec string,
|
sec nostr.SecretKey,
|
||||||
evt *nostr.Event,
|
evt *nostr.Event,
|
||||||
numSigners int,
|
numSigners int,
|
||||||
keys []string,
|
keys []string,
|
||||||
@@ -51,11 +50,7 @@ func performMusig(
|
|||||||
partialSigs []string,
|
partialSigs []string,
|
||||||
) (signed bool, err error) {
|
) (signed bool, err error) {
|
||||||
// preprocess data received
|
// preprocess data received
|
||||||
secb, err := hex.DecodeString(sec)
|
seck, pubk := btcec.PrivKeyFromBytes(sec[:])
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
seck, pubk := btcec.PrivKeyFromBytes(secb)
|
|
||||||
|
|
||||||
knownSigners := make([]*btcec.PublicKey, 0, numSigners)
|
knownSigners := make([]*btcec.PublicKey, 0, numSigners)
|
||||||
includesUs := false
|
includesUs := false
|
||||||
@@ -135,8 +130,8 @@ func performMusig(
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
fmt.Fprintf(os.Stderr, "the following code should be saved secretly until the next step an included with --musig-nonce-secret:\n")
|
log("the following code should be saved secretly until the next step an included with --musig-nonce-secret:\n")
|
||||||
fmt.Fprintf(os.Stderr, "%s\n\n", base64.StdEncoding.EncodeToString(nonce.SecNonce[:]))
|
log("%s\n\n", base64.StdEncoding.EncodeToString(nonce.SecNonce[:]))
|
||||||
|
|
||||||
knownNonces = append(knownNonces, nonce.PubNonce)
|
knownNonces = append(knownNonces, nonce.PubNonce)
|
||||||
printPublicCommandForNextPeer(evt, numSigners, knownSigners, knownNonces, nil, false)
|
printPublicCommandForNextPeer(evt, numSigners, knownSigners, knownNonces, nil, false)
|
||||||
@@ -147,9 +142,9 @@ func performMusig(
|
|||||||
if comb, err := mctx.CombinedKey(); err != nil {
|
if comb, err := mctx.CombinedKey(); err != nil {
|
||||||
return false, fmt.Errorf("failed to combine keys (after %d signers): %w", len(knownSigners), err)
|
return false, fmt.Errorf("failed to combine keys (after %d signers): %w", len(knownSigners), err)
|
||||||
} else {
|
} else {
|
||||||
evt.PubKey = hex.EncodeToString(comb.SerializeCompressed()[1:])
|
evt.PubKey = nostr.PubKey(comb.SerializeCompressed()[1:])
|
||||||
evt.ID = evt.GetID()
|
evt.ID = evt.GetID()
|
||||||
fmt.Fprintf(os.Stderr, "combined key: %x\n\n", comb.SerializeCompressed())
|
log("combined key: %x\n\n", comb.SerializeCompressed())
|
||||||
}
|
}
|
||||||
|
|
||||||
// we have all the signers, which means we must also have all the nonces
|
// we have all the signers, which means we must also have all the nonces
|
||||||
@@ -201,11 +196,7 @@ func performMusig(
|
|||||||
|
|
||||||
// signing phase
|
// signing phase
|
||||||
// we always have to sign, so let's do this
|
// we always have to sign, so let's do this
|
||||||
id := evt.GetID()
|
partialSig, err := session.Sign(evt.GetID()) // this will already include our sig in the bundle
|
||||||
hash, _ := hex.DecodeString(id)
|
|
||||||
var msg32 [32]byte
|
|
||||||
copy(msg32[:], hash)
|
|
||||||
partialSig, err := session.Sign(msg32) // this will already include our sig in the bundle
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf("failed to produce partial signature: %w", err)
|
return false, fmt.Errorf("failed to produce partial signature: %w", err)
|
||||||
}
|
}
|
||||||
@@ -226,7 +217,7 @@ func performMusig(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// we have the signature
|
// we have the signature
|
||||||
evt.Sig = hex.EncodeToString(session.FinalSig().Serialize())
|
evt.Sig = [64]byte(session.FinalSig().Serialize())
|
||||||
|
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
@@ -244,7 +235,7 @@ func printPublicCommandForNextPeer(
|
|||||||
maybeNonceSecret = " --musig-nonce-secret '<insert-nonce-secret>'"
|
maybeNonceSecret = " --musig-nonce-secret '<insert-nonce-secret>'"
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Fprintf(os.Stderr, "the next signer and they should call this on their side:\nnak event --sec <insert-secret-key> --musig %d %s%s%s%s%s\n",
|
log("the next signer and they should call this on their side:\nnak event --sec <insert-secret-key> --musig %d %s%s%s%s%s\n",
|
||||||
numSigners,
|
numSigners,
|
||||||
eventToCliArgs(evt),
|
eventToCliArgs(evt),
|
||||||
signersToCliArgs(knownSigners),
|
signersToCliArgs(knownSigners),
|
||||||
@@ -259,7 +250,7 @@ func eventToCliArgs(evt *nostr.Event) string {
|
|||||||
b.Grow(100)
|
b.Grow(100)
|
||||||
|
|
||||||
b.WriteString("-k ")
|
b.WriteString("-k ")
|
||||||
b.WriteString(strconv.Itoa(evt.Kind))
|
b.WriteString(strconv.Itoa(int(evt.Kind)))
|
||||||
|
|
||||||
b.WriteString(" -ts ")
|
b.WriteString(" -ts ")
|
||||||
b.WriteString(strconv.FormatInt(int64(evt.CreatedAt), 10))
|
b.WriteString(strconv.FormatInt(int64(evt.CreatedAt), 10))
|
||||||
@@ -270,7 +261,7 @@ func eventToCliArgs(evt *nostr.Event) string {
|
|||||||
|
|
||||||
for _, tag := range evt.Tags {
|
for _, tag := range evt.Tags {
|
||||||
b.WriteString(" -t '")
|
b.WriteString(" -t '")
|
||||||
b.WriteString(tag.Key())
|
b.WriteString(tag[0])
|
||||||
if len(tag) > 1 {
|
if len(tag) > 1 {
|
||||||
b.WriteString("=")
|
b.WriteString("=")
|
||||||
b.WriteString(tag[1])
|
b.WriteString(tag[1])
|
||||||
|
|||||||
56
nostrfs/asyncfile.go
Normal file
56
nostrfs/asyncfile.go
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
package nostrfs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync/atomic"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/hanwen/go-fuse/v2/fs"
|
||||||
|
"github.com/hanwen/go-fuse/v2/fuse"
|
||||||
|
"fiatjaf.com/nostr"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AsyncFile struct {
|
||||||
|
fs.Inode
|
||||||
|
ctx context.Context
|
||||||
|
fetched atomic.Bool
|
||||||
|
data []byte
|
||||||
|
ts nostr.Timestamp
|
||||||
|
load func() ([]byte, nostr.Timestamp)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ = (fs.NodeOpener)((*AsyncFile)(nil))
|
||||||
|
_ = (fs.NodeGetattrer)((*AsyncFile)(nil))
|
||||||
|
)
|
||||||
|
|
||||||
|
func (af *AsyncFile) Getattr(ctx context.Context, f fs.FileHandle, out *fuse.AttrOut) syscall.Errno {
|
||||||
|
if af.fetched.CompareAndSwap(false, true) {
|
||||||
|
af.data, af.ts = af.load()
|
||||||
|
}
|
||||||
|
|
||||||
|
out.Size = uint64(len(af.data))
|
||||||
|
out.Mtime = uint64(af.ts)
|
||||||
|
return fs.OK
|
||||||
|
}
|
||||||
|
|
||||||
|
func (af *AsyncFile) Open(ctx context.Context, flags uint32) (fs.FileHandle, uint32, syscall.Errno) {
|
||||||
|
if af.fetched.CompareAndSwap(false, true) {
|
||||||
|
af.data, af.ts = af.load()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fuse.FOPEN_KEEP_CACHE, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (af *AsyncFile) Read(
|
||||||
|
ctx context.Context,
|
||||||
|
f fs.FileHandle,
|
||||||
|
dest []byte,
|
||||||
|
off int64,
|
||||||
|
) (fuse.ReadResult, syscall.Errno) {
|
||||||
|
end := int(off) + len(dest)
|
||||||
|
if end > len(af.data) {
|
||||||
|
end = len(af.data)
|
||||||
|
}
|
||||||
|
return fuse.ReadResultData(af.data[off:end]), 0
|
||||||
|
}
|
||||||
50
nostrfs/deterministicfile.go
Normal file
50
nostrfs/deterministicfile.go
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package nostrfs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/hanwen/go-fuse/v2/fs"
|
||||||
|
"github.com/hanwen/go-fuse/v2/fuse"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DeterministicFile struct {
|
||||||
|
fs.Inode
|
||||||
|
get func() (ctime, mtime uint64, data string)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ = (fs.NodeOpener)((*DeterministicFile)(nil))
|
||||||
|
_ = (fs.NodeReader)((*DeterministicFile)(nil))
|
||||||
|
_ = (fs.NodeGetattrer)((*DeterministicFile)(nil))
|
||||||
|
)
|
||||||
|
|
||||||
|
func (r *NostrRoot) NewDeterministicFile(get func() (ctime, mtime uint64, data string)) *DeterministicFile {
|
||||||
|
return &DeterministicFile{
|
||||||
|
get: get,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *DeterministicFile) Open(ctx context.Context, flags uint32) (fh fs.FileHandle, fuseFlags uint32, errno syscall.Errno) {
|
||||||
|
return nil, fuse.FOPEN_KEEP_CACHE, fs.OK
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *DeterministicFile) Getattr(ctx context.Context, fh fs.FileHandle, out *fuse.AttrOut) syscall.Errno {
|
||||||
|
var content string
|
||||||
|
out.Mode = 0444
|
||||||
|
out.Ctime, out.Mtime, content = f.get()
|
||||||
|
out.Size = uint64(len(content))
|
||||||
|
return fs.OK
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *DeterministicFile) Read(ctx context.Context, fh fs.FileHandle, dest []byte, off int64) (fuse.ReadResult, syscall.Errno) {
|
||||||
|
_, _, content := f.get()
|
||||||
|
data := unsafe.Slice(unsafe.StringData(content), len(content))
|
||||||
|
|
||||||
|
end := int(off) + len(dest)
|
||||||
|
if end > len(data) {
|
||||||
|
end = len(data)
|
||||||
|
}
|
||||||
|
return fuse.ReadResultData(data[off:end]), fs.OK
|
||||||
|
}
|
||||||
408
nostrfs/entitydir.go
Normal file
408
nostrfs/entitydir.go
Normal file
@@ -0,0 +1,408 @@
|
|||||||
|
package nostrfs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"fiatjaf.com/lib/debouncer"
|
||||||
|
"fiatjaf.com/nostr"
|
||||||
|
"fiatjaf.com/nostr/nip19"
|
||||||
|
"fiatjaf.com/nostr/nip27"
|
||||||
|
"fiatjaf.com/nostr/nip73"
|
||||||
|
"fiatjaf.com/nostr/nip92"
|
||||||
|
sdk "fiatjaf.com/nostr/sdk"
|
||||||
|
"github.com/fatih/color"
|
||||||
|
"github.com/hanwen/go-fuse/v2/fs"
|
||||||
|
"github.com/hanwen/go-fuse/v2/fuse"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EntityDir struct {
|
||||||
|
fs.Inode
|
||||||
|
root *NostrRoot
|
||||||
|
|
||||||
|
publisher *debouncer.Debouncer
|
||||||
|
event *nostr.Event
|
||||||
|
updating struct {
|
||||||
|
title string
|
||||||
|
content string
|
||||||
|
publishedAt uint64
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ = (fs.NodeOnAdder)((*EntityDir)(nil))
|
||||||
|
_ = (fs.NodeGetattrer)((*EntityDir)(nil))
|
||||||
|
_ = (fs.NodeSetattrer)((*EntityDir)(nil))
|
||||||
|
_ = (fs.NodeCreater)((*EntityDir)(nil))
|
||||||
|
_ = (fs.NodeUnlinker)((*EntityDir)(nil))
|
||||||
|
)
|
||||||
|
|
||||||
|
func (e *EntityDir) Getattr(_ context.Context, f fs.FileHandle, out *fuse.AttrOut) syscall.Errno {
|
||||||
|
out.Ctime = uint64(e.event.CreatedAt)
|
||||||
|
if e.updating.publishedAt != 0 {
|
||||||
|
out.Mtime = e.updating.publishedAt
|
||||||
|
} else {
|
||||||
|
out.Mtime = e.PublishedAt()
|
||||||
|
}
|
||||||
|
return fs.OK
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *EntityDir) Create(
|
||||||
|
_ context.Context,
|
||||||
|
name string,
|
||||||
|
flags uint32,
|
||||||
|
mode uint32,
|
||||||
|
out *fuse.EntryOut,
|
||||||
|
) (node *fs.Inode, fh fs.FileHandle, fuseFlags uint32, errno syscall.Errno) {
|
||||||
|
if name == "publish" && e.publisher.IsRunning() {
|
||||||
|
// this causes the publish process to be triggered faster
|
||||||
|
log := e.root.ctx.Value("log").(func(msg string, args ...any))
|
||||||
|
log("publishing now!\n")
|
||||||
|
e.publisher.Flush()
|
||||||
|
return nil, nil, 0, syscall.ENOTDIR
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil, 0, syscall.ENOTSUP
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *EntityDir) Unlink(ctx context.Context, name string) syscall.Errno {
|
||||||
|
switch name {
|
||||||
|
case "content" + kindToExtension(e.event.Kind):
|
||||||
|
e.updating.content = e.event.Content
|
||||||
|
return syscall.ENOTDIR
|
||||||
|
case "title":
|
||||||
|
e.updating.title = e.Title()
|
||||||
|
return syscall.ENOTDIR
|
||||||
|
default:
|
||||||
|
return syscall.EINTR
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *EntityDir) Setattr(_ context.Context, _ fs.FileHandle, in *fuse.SetAttrIn, _ *fuse.AttrOut) syscall.Errno {
|
||||||
|
e.updating.publishedAt = in.Mtime
|
||||||
|
return fs.OK
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *EntityDir) OnAdd(_ context.Context) {
|
||||||
|
log := e.root.ctx.Value("log").(func(msg string, args ...any))
|
||||||
|
|
||||||
|
e.AddChild("@author", e.NewPersistentInode(
|
||||||
|
e.root.ctx,
|
||||||
|
&fs.MemSymlink{
|
||||||
|
Data: []byte(e.root.wd + "/" + nip19.EncodeNpub(e.event.PubKey)),
|
||||||
|
},
|
||||||
|
fs.StableAttr{Mode: syscall.S_IFLNK},
|
||||||
|
), true)
|
||||||
|
|
||||||
|
e.AddChild("event.json", e.NewPersistentInode(
|
||||||
|
e.root.ctx,
|
||||||
|
&DeterministicFile{
|
||||||
|
get: func() (ctime uint64, mtime uint64, data string) {
|
||||||
|
eventj, _ := json.MarshalIndent(e.event, "", " ")
|
||||||
|
return uint64(e.event.CreatedAt),
|
||||||
|
uint64(e.event.CreatedAt),
|
||||||
|
unsafe.String(unsafe.SliceData(eventj), len(eventj))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fs.StableAttr{},
|
||||||
|
), true)
|
||||||
|
|
||||||
|
e.AddChild("identifier", e.NewPersistentInode(
|
||||||
|
e.root.ctx,
|
||||||
|
&fs.MemRegularFile{
|
||||||
|
Data: []byte(e.event.Tags.GetD()),
|
||||||
|
Attr: fuse.Attr{
|
||||||
|
Mode: 0444,
|
||||||
|
Ctime: uint64(e.event.CreatedAt),
|
||||||
|
Mtime: uint64(e.event.CreatedAt),
|
||||||
|
Size: uint64(len(e.event.Tags.GetD())),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fs.StableAttr{},
|
||||||
|
), true)
|
||||||
|
|
||||||
|
if e.root.signer == nil || e.root.rootPubKey != e.event.PubKey {
|
||||||
|
// read-only
|
||||||
|
e.AddChild("title", e.NewPersistentInode(
|
||||||
|
e.root.ctx,
|
||||||
|
&DeterministicFile{
|
||||||
|
get: func() (ctime uint64, mtime uint64, data string) {
|
||||||
|
return uint64(e.event.CreatedAt), e.PublishedAt(), e.Title()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fs.StableAttr{},
|
||||||
|
), true)
|
||||||
|
e.AddChild("content."+kindToExtension(e.event.Kind), e.NewPersistentInode(
|
||||||
|
e.root.ctx,
|
||||||
|
&DeterministicFile{
|
||||||
|
get: func() (ctime uint64, mtime uint64, data string) {
|
||||||
|
return uint64(e.event.CreatedAt), e.PublishedAt(), e.event.Content
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fs.StableAttr{},
|
||||||
|
), true)
|
||||||
|
} else {
|
||||||
|
// writeable
|
||||||
|
e.updating.title = e.Title()
|
||||||
|
e.updating.publishedAt = e.PublishedAt()
|
||||||
|
e.updating.content = e.event.Content
|
||||||
|
|
||||||
|
e.AddChild("title", e.NewPersistentInode(
|
||||||
|
e.root.ctx,
|
||||||
|
e.root.NewWriteableFile(e.updating.title, uint64(e.event.CreatedAt), e.updating.publishedAt, func(s string) {
|
||||||
|
log("title updated")
|
||||||
|
e.updating.title = strings.TrimSpace(s)
|
||||||
|
e.handleWrite()
|
||||||
|
}),
|
||||||
|
fs.StableAttr{},
|
||||||
|
), true)
|
||||||
|
|
||||||
|
e.AddChild("content."+kindToExtension(e.event.Kind), e.NewPersistentInode(
|
||||||
|
e.root.ctx,
|
||||||
|
e.root.NewWriteableFile(e.updating.content, uint64(e.event.CreatedAt), e.updating.publishedAt, func(s string) {
|
||||||
|
log("content updated")
|
||||||
|
e.updating.content = strings.TrimSpace(s)
|
||||||
|
e.handleWrite()
|
||||||
|
}),
|
||||||
|
fs.StableAttr{},
|
||||||
|
), true)
|
||||||
|
}
|
||||||
|
|
||||||
|
var refsdir *fs.Inode
|
||||||
|
i := 0
|
||||||
|
for ref := range nip27.Parse(e.event.Content) {
|
||||||
|
if _, isExternal := ref.Pointer.(nip73.ExternalPointer); isExternal {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
|
||||||
|
if refsdir == nil {
|
||||||
|
refsdir = e.NewPersistentInode(e.root.ctx, &fs.Inode{}, fs.StableAttr{Mode: syscall.S_IFDIR})
|
||||||
|
e.root.AddChild("references", refsdir, true)
|
||||||
|
}
|
||||||
|
refsdir.AddChild(fmt.Sprintf("ref_%02d", i), refsdir.NewPersistentInode(
|
||||||
|
e.root.ctx,
|
||||||
|
&fs.MemSymlink{
|
||||||
|
Data: []byte(e.root.wd + "/" + nip19.EncodePointer(ref.Pointer)),
|
||||||
|
},
|
||||||
|
fs.StableAttr{Mode: syscall.S_IFLNK},
|
||||||
|
), true)
|
||||||
|
}
|
||||||
|
|
||||||
|
var imagesdir *fs.Inode
|
||||||
|
addImage := func(url string) {
|
||||||
|
if imagesdir == nil {
|
||||||
|
in := &fs.Inode{}
|
||||||
|
imagesdir = e.NewPersistentInode(e.root.ctx, in, fs.StableAttr{Mode: syscall.S_IFDIR})
|
||||||
|
e.AddChild("images", imagesdir, true)
|
||||||
|
}
|
||||||
|
imagesdir.AddChild(filepath.Base(url), imagesdir.NewPersistentInode(
|
||||||
|
e.root.ctx,
|
||||||
|
&AsyncFile{
|
||||||
|
ctx: e.root.ctx,
|
||||||
|
load: func() ([]byte, nostr.Timestamp) {
|
||||||
|
ctx, cancel := context.WithTimeout(e.root.ctx, time.Second*20)
|
||||||
|
defer cancel()
|
||||||
|
r, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||||
|
if err != nil {
|
||||||
|
log("failed to load image %s: %s\n", url, err)
|
||||||
|
return nil, 0
|
||||||
|
}
|
||||||
|
resp, err := http.DefaultClient.Do(r)
|
||||||
|
if err != nil {
|
||||||
|
log("failed to load image %s: %s\n", url, err)
|
||||||
|
return nil, 0
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode >= 300 {
|
||||||
|
log("failed to load image %s: %s\n", url, err)
|
||||||
|
return nil, 0
|
||||||
|
}
|
||||||
|
w := &bytes.Buffer{}
|
||||||
|
io.Copy(w, resp.Body)
|
||||||
|
return w.Bytes(), 0
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fs.StableAttr{},
|
||||||
|
), true)
|
||||||
|
}
|
||||||
|
|
||||||
|
images := nip92.ParseTags(e.event.Tags)
|
||||||
|
for _, imeta := range images {
|
||||||
|
if imeta.URL == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
addImage(imeta.URL)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tag := e.event.Tags.Find("image"); tag != nil {
|
||||||
|
addImage(tag[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *EntityDir) IsNew() bool {
|
||||||
|
return e.event.CreatedAt == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *EntityDir) PublishedAt() uint64 {
|
||||||
|
if tag := e.event.Tags.Find("published_at"); tag != nil {
|
||||||
|
publishedAt, _ := strconv.ParseUint(tag[1], 10, 64)
|
||||||
|
return publishedAt
|
||||||
|
}
|
||||||
|
return uint64(e.event.CreatedAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *EntityDir) Title() string {
|
||||||
|
if tag := e.event.Tags.Find("title"); tag != nil {
|
||||||
|
return tag[1]
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *EntityDir) handleWrite() {
|
||||||
|
log := e.root.ctx.Value("log").(func(msg string, args ...any))
|
||||||
|
logverbose := e.root.ctx.Value("logverbose").(func(msg string, args ...any))
|
||||||
|
|
||||||
|
if e.root.opts.AutoPublishArticlesTimeout.Hours() < 24*365 {
|
||||||
|
if e.publisher.IsRunning() {
|
||||||
|
log(", timer reset")
|
||||||
|
}
|
||||||
|
log(", publishing the ")
|
||||||
|
if e.IsNew() {
|
||||||
|
log("new")
|
||||||
|
} else {
|
||||||
|
log("updated")
|
||||||
|
}
|
||||||
|
log(" event in %d seconds...\n", int(e.root.opts.AutoPublishArticlesTimeout.Seconds()))
|
||||||
|
} else {
|
||||||
|
log(".\n")
|
||||||
|
}
|
||||||
|
if !e.publisher.IsRunning() {
|
||||||
|
log("- `touch publish` to publish immediately\n")
|
||||||
|
log("- `rm title content." + kindToExtension(e.event.Kind) + "` to erase and cancel the edits\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
e.publisher.Call(func() {
|
||||||
|
if e.Title() == e.updating.title && e.event.Content == e.updating.content {
|
||||||
|
log("not modified, publish canceled.\n")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
evt := nostr.Event{
|
||||||
|
Kind: e.event.Kind,
|
||||||
|
Content: e.updating.content,
|
||||||
|
Tags: make(nostr.Tags, len(e.event.Tags)),
|
||||||
|
CreatedAt: nostr.Now(),
|
||||||
|
}
|
||||||
|
copy(evt.Tags, e.event.Tags) // copy tags because that's the rule
|
||||||
|
if e.updating.title != "" {
|
||||||
|
if titleTag := evt.Tags.Find("title"); titleTag != nil {
|
||||||
|
titleTag[1] = e.updating.title
|
||||||
|
} else {
|
||||||
|
evt.Tags = append(evt.Tags, nostr.Tag{"title", e.updating.title})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// "published_at" tag
|
||||||
|
publishedAtStr := strconv.FormatUint(e.updating.publishedAt, 10)
|
||||||
|
if publishedAtStr != "0" {
|
||||||
|
if publishedAtTag := evt.Tags.Find("published_at"); publishedAtTag != nil {
|
||||||
|
publishedAtTag[1] = publishedAtStr
|
||||||
|
} else {
|
||||||
|
evt.Tags = append(evt.Tags, nostr.Tag{"published_at", publishedAtStr})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add "p" tags from people mentioned and "q" tags from events mentioned
|
||||||
|
for ref := range nip27.Parse(evt.Content) {
|
||||||
|
if _, isExternal := ref.Pointer.(nip73.ExternalPointer); isExternal {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
tag := ref.Pointer.AsTag()
|
||||||
|
key := tag[0]
|
||||||
|
val := tag[1]
|
||||||
|
if key == "e" || key == "a" {
|
||||||
|
key = "q"
|
||||||
|
}
|
||||||
|
if existing := evt.Tags.FindWithValue(key, val); existing == nil {
|
||||||
|
evt.Tags = append(evt.Tags, tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sign and publish
|
||||||
|
if err := e.root.signer.SignEvent(e.root.ctx, &evt); err != nil {
|
||||||
|
log("failed to sign: '%s'.\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logverbose("%s\n", evt)
|
||||||
|
|
||||||
|
relays := e.root.sys.FetchWriteRelays(e.root.ctx, e.root.rootPubKey)
|
||||||
|
if len(relays) == 0 {
|
||||||
|
relays = e.root.sys.FetchOutboxRelays(e.root.ctx, e.root.rootPubKey, 6)
|
||||||
|
}
|
||||||
|
|
||||||
|
log("publishing to %d relays... ", len(relays))
|
||||||
|
success := false
|
||||||
|
first := true
|
||||||
|
for res := range e.root.sys.Pool.PublishMany(e.root.ctx, relays, evt) {
|
||||||
|
cleanUrl, _ := strings.CutPrefix(res.RelayURL, "wss://")
|
||||||
|
if !first {
|
||||||
|
log(", ")
|
||||||
|
}
|
||||||
|
first = false
|
||||||
|
|
||||||
|
if res.Error != nil {
|
||||||
|
log("%s: %s", color.RedString(cleanUrl), res.Error)
|
||||||
|
} else {
|
||||||
|
success = true
|
||||||
|
log("%s: ok", color.GreenString(cleanUrl))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log("\n")
|
||||||
|
|
||||||
|
if success {
|
||||||
|
e.event = &evt
|
||||||
|
log("event updated locally.\n")
|
||||||
|
e.updating.publishedAt = uint64(evt.CreatedAt) // set this so subsequent edits get the correct value
|
||||||
|
} else {
|
||||||
|
log("failed.\n")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *NostrRoot) FetchAndCreateEntityDir(
|
||||||
|
parent fs.InodeEmbedder,
|
||||||
|
extension string,
|
||||||
|
pointer nostr.EntityPointer,
|
||||||
|
) (*fs.Inode, error) {
|
||||||
|
event, _, err := r.sys.FetchSpecificEvent(r.ctx, pointer, sdk.FetchSpecificEventParameters{
|
||||||
|
WithRelays: false,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to fetch: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.CreateEntityDir(parent, event), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *NostrRoot) CreateEntityDir(
|
||||||
|
parent fs.InodeEmbedder,
|
||||||
|
event *nostr.Event,
|
||||||
|
) *fs.Inode {
|
||||||
|
return parent.EmbeddedInode().NewPersistentInode(
|
||||||
|
r.ctx,
|
||||||
|
&EntityDir{root: r, event: event, publisher: debouncer.New(r.opts.AutoPublishArticlesTimeout)},
|
||||||
|
fs.StableAttr{Mode: syscall.S_IFDIR},
|
||||||
|
)
|
||||||
|
}
|
||||||
241
nostrfs/eventdir.go
Normal file
241
nostrfs/eventdir.go
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
package nostrfs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/binary"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"path/filepath"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"fiatjaf.com/nostr"
|
||||||
|
"fiatjaf.com/nostr/nip10"
|
||||||
|
"fiatjaf.com/nostr/nip19"
|
||||||
|
"fiatjaf.com/nostr/nip22"
|
||||||
|
"fiatjaf.com/nostr/nip27"
|
||||||
|
"fiatjaf.com/nostr/nip73"
|
||||||
|
"fiatjaf.com/nostr/nip92"
|
||||||
|
sdk "fiatjaf.com/nostr/sdk"
|
||||||
|
"github.com/hanwen/go-fuse/v2/fs"
|
||||||
|
"github.com/hanwen/go-fuse/v2/fuse"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EventDir struct {
|
||||||
|
fs.Inode
|
||||||
|
ctx context.Context
|
||||||
|
wd string
|
||||||
|
evt *nostr.Event
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = (fs.NodeGetattrer)((*EventDir)(nil))
|
||||||
|
|
||||||
|
func (e *EventDir) Getattr(_ context.Context, f fs.FileHandle, out *fuse.AttrOut) syscall.Errno {
|
||||||
|
out.Mtime = uint64(e.evt.CreatedAt)
|
||||||
|
return fs.OK
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *NostrRoot) FetchAndCreateEventDir(
|
||||||
|
parent fs.InodeEmbedder,
|
||||||
|
pointer nostr.EventPointer,
|
||||||
|
) (*fs.Inode, error) {
|
||||||
|
event, _, err := r.sys.FetchSpecificEvent(r.ctx, pointer, sdk.FetchSpecificEventParameters{
|
||||||
|
WithRelays: false,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to fetch: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.CreateEventDir(parent, event), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *NostrRoot) CreateEventDir(
|
||||||
|
parent fs.InodeEmbedder,
|
||||||
|
event *nostr.Event,
|
||||||
|
) *fs.Inode {
|
||||||
|
h := parent.EmbeddedInode().NewPersistentInode(
|
||||||
|
r.ctx,
|
||||||
|
&EventDir{ctx: r.ctx, wd: r.wd, evt: event},
|
||||||
|
fs.StableAttr{Mode: syscall.S_IFDIR, Ino: binary.BigEndian.Uint64(event.ID[8:16])},
|
||||||
|
)
|
||||||
|
|
||||||
|
h.AddChild("@author", h.NewPersistentInode(
|
||||||
|
r.ctx,
|
||||||
|
&fs.MemSymlink{
|
||||||
|
Data: []byte(r.wd + "/" + nip19.EncodeNpub(event.PubKey)),
|
||||||
|
},
|
||||||
|
fs.StableAttr{Mode: syscall.S_IFLNK},
|
||||||
|
), true)
|
||||||
|
|
||||||
|
eventj, _ := json.MarshalIndent(event, "", " ")
|
||||||
|
h.AddChild("event.json", h.NewPersistentInode(
|
||||||
|
r.ctx,
|
||||||
|
&fs.MemRegularFile{
|
||||||
|
Data: eventj,
|
||||||
|
Attr: fuse.Attr{
|
||||||
|
Mode: 0444,
|
||||||
|
Ctime: uint64(event.CreatedAt),
|
||||||
|
Mtime: uint64(event.CreatedAt),
|
||||||
|
Size: uint64(len(event.Content)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fs.StableAttr{},
|
||||||
|
), true)
|
||||||
|
|
||||||
|
h.AddChild("id", h.NewPersistentInode(
|
||||||
|
r.ctx,
|
||||||
|
&fs.MemRegularFile{
|
||||||
|
Data: []byte(event.ID.Hex()),
|
||||||
|
Attr: fuse.Attr{
|
||||||
|
Mode: 0444,
|
||||||
|
Ctime: uint64(event.CreatedAt),
|
||||||
|
Mtime: uint64(event.CreatedAt),
|
||||||
|
Size: uint64(64),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fs.StableAttr{},
|
||||||
|
), true)
|
||||||
|
|
||||||
|
h.AddChild("content.txt", h.NewPersistentInode(
|
||||||
|
r.ctx,
|
||||||
|
&fs.MemRegularFile{
|
||||||
|
Data: []byte(event.Content),
|
||||||
|
Attr: fuse.Attr{
|
||||||
|
Mode: 0444,
|
||||||
|
Ctime: uint64(event.CreatedAt),
|
||||||
|
Mtime: uint64(event.CreatedAt),
|
||||||
|
Size: uint64(len(event.Content)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fs.StableAttr{},
|
||||||
|
), true)
|
||||||
|
|
||||||
|
var refsdir *fs.Inode
|
||||||
|
i := 0
|
||||||
|
for ref := range nip27.Parse(event.Content) {
|
||||||
|
if _, isExternal := ref.Pointer.(nip73.ExternalPointer); isExternal {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
|
||||||
|
if refsdir == nil {
|
||||||
|
refsdir = h.NewPersistentInode(r.ctx, &fs.Inode{}, fs.StableAttr{Mode: syscall.S_IFDIR})
|
||||||
|
h.AddChild("references", refsdir, true)
|
||||||
|
}
|
||||||
|
refsdir.AddChild(fmt.Sprintf("ref_%02d", i), refsdir.NewPersistentInode(
|
||||||
|
r.ctx,
|
||||||
|
&fs.MemSymlink{
|
||||||
|
Data: []byte(r.wd + "/" + nip19.EncodePointer(ref.Pointer)),
|
||||||
|
},
|
||||||
|
fs.StableAttr{Mode: syscall.S_IFLNK},
|
||||||
|
), true)
|
||||||
|
}
|
||||||
|
|
||||||
|
var imagesdir *fs.Inode
|
||||||
|
images := nip92.ParseTags(event.Tags)
|
||||||
|
for _, imeta := range images {
|
||||||
|
if imeta.URL == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if imagesdir == nil {
|
||||||
|
in := &fs.Inode{}
|
||||||
|
imagesdir = h.NewPersistentInode(r.ctx, in, fs.StableAttr{Mode: syscall.S_IFDIR})
|
||||||
|
h.AddChild("images", imagesdir, true)
|
||||||
|
}
|
||||||
|
imagesdir.AddChild(filepath.Base(imeta.URL), imagesdir.NewPersistentInode(
|
||||||
|
r.ctx,
|
||||||
|
&AsyncFile{
|
||||||
|
ctx: r.ctx,
|
||||||
|
load: func() ([]byte, nostr.Timestamp) {
|
||||||
|
ctx, cancel := context.WithTimeout(r.ctx, time.Second*20)
|
||||||
|
defer cancel()
|
||||||
|
r, err := http.NewRequestWithContext(ctx, "GET", imeta.URL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0
|
||||||
|
}
|
||||||
|
resp, err := http.DefaultClient.Do(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode >= 300 {
|
||||||
|
return nil, 0
|
||||||
|
}
|
||||||
|
w := &bytes.Buffer{}
|
||||||
|
io.Copy(w, resp.Body)
|
||||||
|
return w.Bytes(), 0
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fs.StableAttr{},
|
||||||
|
), true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if event.Kind == 1 {
|
||||||
|
if pointer := nip10.GetThreadRoot(event.Tags); pointer != nil {
|
||||||
|
nevent := nip19.EncodePointer(pointer)
|
||||||
|
h.AddChild("@root", h.NewPersistentInode(
|
||||||
|
r.ctx,
|
||||||
|
&fs.MemSymlink{
|
||||||
|
Data: []byte(r.wd + "/" + nevent),
|
||||||
|
},
|
||||||
|
fs.StableAttr{Mode: syscall.S_IFLNK},
|
||||||
|
), true)
|
||||||
|
}
|
||||||
|
if pointer := nip10.GetImmediateParent(event.Tags); pointer != nil {
|
||||||
|
nevent := nip19.EncodePointer(pointer)
|
||||||
|
h.AddChild("@parent", h.NewPersistentInode(
|
||||||
|
r.ctx,
|
||||||
|
&fs.MemSymlink{
|
||||||
|
Data: []byte(r.wd + "/" + nevent),
|
||||||
|
},
|
||||||
|
fs.StableAttr{Mode: syscall.S_IFLNK},
|
||||||
|
), true)
|
||||||
|
}
|
||||||
|
} else if event.Kind == 1111 {
|
||||||
|
if pointer := nip22.GetThreadRoot(event.Tags); pointer != nil {
|
||||||
|
if xp, ok := pointer.(nip73.ExternalPointer); ok {
|
||||||
|
h.AddChild("@root", h.NewPersistentInode(
|
||||||
|
r.ctx,
|
||||||
|
&fs.MemRegularFile{
|
||||||
|
Data: []byte(`<!doctype html><meta http-equiv="refresh" content="0; url=` + xp.Thing + `" />`),
|
||||||
|
},
|
||||||
|
fs.StableAttr{},
|
||||||
|
), true)
|
||||||
|
} else {
|
||||||
|
nevent := nip19.EncodePointer(pointer)
|
||||||
|
h.AddChild("@parent", h.NewPersistentInode(
|
||||||
|
r.ctx,
|
||||||
|
&fs.MemSymlink{
|
||||||
|
Data: []byte(r.wd + "/" + nevent),
|
||||||
|
},
|
||||||
|
fs.StableAttr{Mode: syscall.S_IFLNK},
|
||||||
|
), true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if pointer := nip22.GetImmediateParent(event.Tags); pointer != nil {
|
||||||
|
if xp, ok := pointer.(nip73.ExternalPointer); ok {
|
||||||
|
h.AddChild("@parent", h.NewPersistentInode(
|
||||||
|
r.ctx,
|
||||||
|
&fs.MemRegularFile{
|
||||||
|
Data: []byte(`<!doctype html><meta http-equiv="refresh" content="0; url=` + xp.Thing + `" />`),
|
||||||
|
},
|
||||||
|
fs.StableAttr{},
|
||||||
|
), true)
|
||||||
|
} else {
|
||||||
|
nevent := nip19.EncodePointer(pointer)
|
||||||
|
h.AddChild("@parent", h.NewPersistentInode(
|
||||||
|
r.ctx,
|
||||||
|
&fs.MemSymlink{
|
||||||
|
Data: []byte(r.wd + "/" + nevent),
|
||||||
|
},
|
||||||
|
fs.StableAttr{Mode: syscall.S_IFLNK},
|
||||||
|
), true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return h
|
||||||
|
}
|
||||||
16
nostrfs/helpers.go
Normal file
16
nostrfs/helpers.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package nostrfs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fiatjaf.com/nostr"
|
||||||
|
)
|
||||||
|
|
||||||
|
func kindToExtension(kind nostr.Kind) string {
|
||||||
|
switch kind {
|
||||||
|
case 30023:
|
||||||
|
return "md"
|
||||||
|
case 30818:
|
||||||
|
return "adoc"
|
||||||
|
default:
|
||||||
|
return "txt"
|
||||||
|
}
|
||||||
|
}
|
||||||
261
nostrfs/npubdir.go
Normal file
261
nostrfs/npubdir.go
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
package nostrfs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/binary"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"sync/atomic"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"fiatjaf.com/nostr"
|
||||||
|
"fiatjaf.com/nostr/nip19"
|
||||||
|
"github.com/fatih/color"
|
||||||
|
"github.com/hanwen/go-fuse/v2/fs"
|
||||||
|
"github.com/hanwen/go-fuse/v2/fuse"
|
||||||
|
"github.com/liamg/magic"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NpubDir struct {
|
||||||
|
fs.Inode
|
||||||
|
root *NostrRoot
|
||||||
|
pointer nostr.ProfilePointer
|
||||||
|
fetched atomic.Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = (fs.NodeOnAdder)((*NpubDir)(nil))
|
||||||
|
|
||||||
|
func (r *NostrRoot) CreateNpubDir(
|
||||||
|
parent fs.InodeEmbedder,
|
||||||
|
pointer nostr.ProfilePointer,
|
||||||
|
signer nostr.Signer,
|
||||||
|
) *fs.Inode {
|
||||||
|
npubdir := &NpubDir{root: r, pointer: pointer}
|
||||||
|
return parent.EmbeddedInode().NewPersistentInode(
|
||||||
|
r.ctx,
|
||||||
|
npubdir,
|
||||||
|
fs.StableAttr{Mode: syscall.S_IFDIR, Ino: binary.BigEndian.Uint64(pointer.PublicKey[8:16])},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *NpubDir) OnAdd(_ context.Context) {
|
||||||
|
log := h.root.ctx.Value("log").(func(msg string, args ...any))
|
||||||
|
|
||||||
|
relays := h.root.sys.FetchOutboxRelays(h.root.ctx, h.pointer.PublicKey, 2)
|
||||||
|
log("- adding folder for %s with relays %s\n",
|
||||||
|
color.HiYellowString(nip19.EncodePointer(h.pointer)), color.HiGreenString("%v", relays))
|
||||||
|
|
||||||
|
h.AddChild("pubkey", h.NewPersistentInode(
|
||||||
|
h.root.ctx,
|
||||||
|
&fs.MemRegularFile{Data: []byte(h.pointer.PublicKey.Hex() + "\n"), Attr: fuse.Attr{Mode: 0444}},
|
||||||
|
fs.StableAttr{},
|
||||||
|
), true)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
pm := h.root.sys.FetchProfileMetadata(h.root.ctx, h.pointer.PublicKey)
|
||||||
|
if pm.Event == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
metadataj, _ := json.MarshalIndent(pm, "", " ")
|
||||||
|
h.AddChild(
|
||||||
|
"metadata.json",
|
||||||
|
h.NewPersistentInode(
|
||||||
|
h.root.ctx,
|
||||||
|
&fs.MemRegularFile{
|
||||||
|
Data: metadataj,
|
||||||
|
Attr: fuse.Attr{
|
||||||
|
Mtime: uint64(pm.Event.CreatedAt),
|
||||||
|
Mode: 0444,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fs.StableAttr{},
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(h.root.ctx, time.Second*20)
|
||||||
|
defer cancel()
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "GET", pm.Picture, nil)
|
||||||
|
if err == nil {
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err == nil {
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode < 300 {
|
||||||
|
b := &bytes.Buffer{}
|
||||||
|
io.Copy(b, resp.Body)
|
||||||
|
|
||||||
|
ext := "png"
|
||||||
|
if ft, err := magic.Lookup(b.Bytes()); err == nil {
|
||||||
|
ext = ft.Extension
|
||||||
|
}
|
||||||
|
|
||||||
|
h.AddChild("picture."+ext, h.NewPersistentInode(
|
||||||
|
ctx,
|
||||||
|
&fs.MemRegularFile{
|
||||||
|
Data: b.Bytes(),
|
||||||
|
Attr: fuse.Attr{
|
||||||
|
Mtime: uint64(pm.Event.CreatedAt),
|
||||||
|
Mode: 0444,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fs.StableAttr{},
|
||||||
|
), true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if h.GetChild("notes") == nil {
|
||||||
|
h.AddChild(
|
||||||
|
"notes",
|
||||||
|
h.NewPersistentInode(
|
||||||
|
h.root.ctx,
|
||||||
|
&ViewDir{
|
||||||
|
root: h.root,
|
||||||
|
filter: nostr.Filter{
|
||||||
|
Kinds: []nostr.Kind{1},
|
||||||
|
Authors: []nostr.PubKey{h.pointer.PublicKey},
|
||||||
|
},
|
||||||
|
paginate: true,
|
||||||
|
relays: relays,
|
||||||
|
replaceable: false,
|
||||||
|
createable: true,
|
||||||
|
},
|
||||||
|
fs.StableAttr{Mode: syscall.S_IFDIR},
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if h.GetChild("comments") == nil {
|
||||||
|
h.AddChild(
|
||||||
|
"comments",
|
||||||
|
h.NewPersistentInode(
|
||||||
|
h.root.ctx,
|
||||||
|
&ViewDir{
|
||||||
|
root: h.root,
|
||||||
|
filter: nostr.Filter{
|
||||||
|
Kinds: []nostr.Kind{1111},
|
||||||
|
Authors: []nostr.PubKey{h.pointer.PublicKey},
|
||||||
|
},
|
||||||
|
paginate: true,
|
||||||
|
relays: relays,
|
||||||
|
replaceable: false,
|
||||||
|
},
|
||||||
|
fs.StableAttr{Mode: syscall.S_IFDIR},
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if h.GetChild("photos") == nil {
|
||||||
|
h.AddChild(
|
||||||
|
"photos",
|
||||||
|
h.NewPersistentInode(
|
||||||
|
h.root.ctx,
|
||||||
|
&ViewDir{
|
||||||
|
root: h.root,
|
||||||
|
filter: nostr.Filter{
|
||||||
|
Kinds: []nostr.Kind{20},
|
||||||
|
Authors: []nostr.PubKey{h.pointer.PublicKey},
|
||||||
|
},
|
||||||
|
paginate: true,
|
||||||
|
relays: relays,
|
||||||
|
replaceable: false,
|
||||||
|
},
|
||||||
|
fs.StableAttr{Mode: syscall.S_IFDIR},
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if h.GetChild("videos") == nil {
|
||||||
|
h.AddChild(
|
||||||
|
"videos",
|
||||||
|
h.NewPersistentInode(
|
||||||
|
h.root.ctx,
|
||||||
|
&ViewDir{
|
||||||
|
root: h.root,
|
||||||
|
filter: nostr.Filter{
|
||||||
|
Kinds: []nostr.Kind{21, 22},
|
||||||
|
Authors: []nostr.PubKey{h.pointer.PublicKey},
|
||||||
|
},
|
||||||
|
paginate: false,
|
||||||
|
relays: relays,
|
||||||
|
replaceable: false,
|
||||||
|
},
|
||||||
|
fs.StableAttr{Mode: syscall.S_IFDIR},
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if h.GetChild("highlights") == nil {
|
||||||
|
h.AddChild(
|
||||||
|
"highlights",
|
||||||
|
h.NewPersistentInode(
|
||||||
|
h.root.ctx,
|
||||||
|
&ViewDir{
|
||||||
|
root: h.root,
|
||||||
|
filter: nostr.Filter{
|
||||||
|
Kinds: []nostr.Kind{9802},
|
||||||
|
Authors: []nostr.PubKey{h.pointer.PublicKey},
|
||||||
|
},
|
||||||
|
paginate: false,
|
||||||
|
relays: relays,
|
||||||
|
replaceable: false,
|
||||||
|
},
|
||||||
|
fs.StableAttr{Mode: syscall.S_IFDIR},
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if h.GetChild("articles") == nil {
|
||||||
|
h.AddChild(
|
||||||
|
"articles",
|
||||||
|
h.NewPersistentInode(
|
||||||
|
h.root.ctx,
|
||||||
|
&ViewDir{
|
||||||
|
root: h.root,
|
||||||
|
filter: nostr.Filter{
|
||||||
|
Kinds: []nostr.Kind{30023},
|
||||||
|
Authors: []nostr.PubKey{h.pointer.PublicKey},
|
||||||
|
},
|
||||||
|
paginate: false,
|
||||||
|
relays: relays,
|
||||||
|
replaceable: true,
|
||||||
|
createable: true,
|
||||||
|
},
|
||||||
|
fs.StableAttr{Mode: syscall.S_IFDIR},
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if h.GetChild("wiki") == nil {
|
||||||
|
h.AddChild(
|
||||||
|
"wiki",
|
||||||
|
h.NewPersistentInode(
|
||||||
|
h.root.ctx,
|
||||||
|
&ViewDir{
|
||||||
|
root: h.root,
|
||||||
|
filter: nostr.Filter{
|
||||||
|
Kinds: []nostr.Kind{30818},
|
||||||
|
Authors: []nostr.PubKey{h.pointer.PublicKey},
|
||||||
|
},
|
||||||
|
paginate: false,
|
||||||
|
relays: relays,
|
||||||
|
replaceable: true,
|
||||||
|
createable: true,
|
||||||
|
},
|
||||||
|
fs.StableAttr{Mode: syscall.S_IFDIR},
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
130
nostrfs/root.go
Normal file
130
nostrfs/root.go
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
package nostrfs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"path/filepath"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"fiatjaf.com/nostr"
|
||||||
|
"fiatjaf.com/nostr/nip05"
|
||||||
|
"fiatjaf.com/nostr/nip19"
|
||||||
|
"fiatjaf.com/nostr/sdk"
|
||||||
|
"github.com/hanwen/go-fuse/v2/fs"
|
||||||
|
"github.com/hanwen/go-fuse/v2/fuse"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Options struct {
|
||||||
|
AutoPublishNotesTimeout time.Duration
|
||||||
|
AutoPublishArticlesTimeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
type NostrRoot struct {
|
||||||
|
fs.Inode
|
||||||
|
|
||||||
|
ctx context.Context
|
||||||
|
wd string
|
||||||
|
sys *sdk.System
|
||||||
|
rootPubKey nostr.PubKey
|
||||||
|
signer nostr.Signer
|
||||||
|
|
||||||
|
opts Options
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = (fs.NodeOnAdder)((*NostrRoot)(nil))
|
||||||
|
|
||||||
|
func NewNostrRoot(ctx context.Context, sys *sdk.System, user nostr.User, mountpoint string, o Options) *NostrRoot {
|
||||||
|
pubkey, _ := user.GetPublicKey(ctx)
|
||||||
|
abs, _ := filepath.Abs(mountpoint)
|
||||||
|
|
||||||
|
var signer nostr.Signer
|
||||||
|
if user != nil {
|
||||||
|
signer, _ = user.(nostr.Signer)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &NostrRoot{
|
||||||
|
ctx: ctx,
|
||||||
|
sys: sys,
|
||||||
|
rootPubKey: pubkey,
|
||||||
|
signer: signer,
|
||||||
|
wd: abs,
|
||||||
|
|
||||||
|
opts: o,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *NostrRoot) OnAdd(_ context.Context) {
|
||||||
|
if r.rootPubKey == nostr.ZeroPK {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
time.Sleep(time.Millisecond * 100)
|
||||||
|
|
||||||
|
// add our contacts
|
||||||
|
fl := r.sys.FetchFollowList(r.ctx, r.rootPubKey)
|
||||||
|
for _, f := range fl.Items {
|
||||||
|
pointer := nostr.ProfilePointer{PublicKey: f.Pubkey, Relays: []string{f.Relay}}
|
||||||
|
r.AddChild(
|
||||||
|
nip19.EncodeNpub(f.Pubkey),
|
||||||
|
r.CreateNpubDir(r, pointer, nil),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// add ourselves
|
||||||
|
npub := nip19.EncodeNpub(r.rootPubKey)
|
||||||
|
if r.GetChild(npub) == nil {
|
||||||
|
pointer := nostr.ProfilePointer{PublicKey: r.rootPubKey}
|
||||||
|
|
||||||
|
r.AddChild(
|
||||||
|
npub,
|
||||||
|
r.CreateNpubDir(r, pointer, r.signer),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// add a link to ourselves
|
||||||
|
r.AddChild("@me", r.NewPersistentInode(
|
||||||
|
r.ctx,
|
||||||
|
&fs.MemSymlink{Data: []byte(r.wd + "/" + npub)},
|
||||||
|
fs.StableAttr{Mode: syscall.S_IFLNK},
|
||||||
|
), true)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *NostrRoot) Lookup(_ context.Context, name string, out *fuse.EntryOut) (*fs.Inode, syscall.Errno) {
|
||||||
|
out.SetEntryTimeout(time.Minute * 5)
|
||||||
|
|
||||||
|
child := r.GetChild(name)
|
||||||
|
if child != nil {
|
||||||
|
return child, fs.OK
|
||||||
|
}
|
||||||
|
|
||||||
|
if pp, err := nip05.QueryIdentifier(r.ctx, name); err == nil {
|
||||||
|
return r.NewPersistentInode(
|
||||||
|
r.ctx,
|
||||||
|
&fs.MemSymlink{Data: []byte(r.wd + "/" + nip19.EncodePointer(*pp))},
|
||||||
|
fs.StableAttr{Mode: syscall.S_IFLNK},
|
||||||
|
), fs.OK
|
||||||
|
}
|
||||||
|
|
||||||
|
pointer, err := nip19.ToPointer(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, syscall.ENOENT
|
||||||
|
}
|
||||||
|
|
||||||
|
switch p := pointer.(type) {
|
||||||
|
case nostr.ProfilePointer:
|
||||||
|
npubdir := r.CreateNpubDir(r, p, nil)
|
||||||
|
return npubdir, fs.OK
|
||||||
|
case nostr.EventPointer:
|
||||||
|
eventdir, err := r.FetchAndCreateEventDir(r, p)
|
||||||
|
if err != nil {
|
||||||
|
return nil, syscall.ENOENT
|
||||||
|
}
|
||||||
|
return eventdir, fs.OK
|
||||||
|
default:
|
||||||
|
return nil, syscall.ENOENT
|
||||||
|
}
|
||||||
|
}
|
||||||
267
nostrfs/viewdir.go
Normal file
267
nostrfs/viewdir.go
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
package nostrfs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
"sync/atomic"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"fiatjaf.com/lib/debouncer"
|
||||||
|
"fiatjaf.com/nostr"
|
||||||
|
"github.com/fatih/color"
|
||||||
|
"github.com/hanwen/go-fuse/v2/fs"
|
||||||
|
"github.com/hanwen/go-fuse/v2/fuse"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ViewDir struct {
|
||||||
|
fs.Inode
|
||||||
|
root *NostrRoot
|
||||||
|
fetched atomic.Bool
|
||||||
|
filter nostr.Filter
|
||||||
|
paginate bool
|
||||||
|
relays []string
|
||||||
|
replaceable bool
|
||||||
|
createable bool
|
||||||
|
publisher *debouncer.Debouncer
|
||||||
|
publishing struct {
|
||||||
|
note string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ = (fs.NodeOpendirer)((*ViewDir)(nil))
|
||||||
|
_ = (fs.NodeGetattrer)((*ViewDir)(nil))
|
||||||
|
_ = (fs.NodeMkdirer)((*ViewDir)(nil))
|
||||||
|
_ = (fs.NodeSetattrer)((*ViewDir)(nil))
|
||||||
|
_ = (fs.NodeCreater)((*ViewDir)(nil))
|
||||||
|
_ = (fs.NodeUnlinker)((*ViewDir)(nil))
|
||||||
|
)
|
||||||
|
|
||||||
|
func (f *ViewDir) Setattr(_ context.Context, _ fs.FileHandle, _ *fuse.SetAttrIn, _ *fuse.AttrOut) syscall.Errno {
|
||||||
|
return fs.OK
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *ViewDir) Create(
|
||||||
|
_ context.Context,
|
||||||
|
name string,
|
||||||
|
flags uint32,
|
||||||
|
mode uint32,
|
||||||
|
out *fuse.EntryOut,
|
||||||
|
) (node *fs.Inode, fh fs.FileHandle, fuseFlags uint32, errno syscall.Errno) {
|
||||||
|
if !n.createable || n.root.rootPubKey != n.filter.Authors[0] {
|
||||||
|
return nil, nil, 0, syscall.EPERM
|
||||||
|
}
|
||||||
|
if n.publisher == nil {
|
||||||
|
n.publisher = debouncer.New(n.root.opts.AutoPublishNotesTimeout)
|
||||||
|
}
|
||||||
|
if n.filter.Kinds[0] != 1 {
|
||||||
|
return nil, nil, 0, syscall.ENOTSUP
|
||||||
|
}
|
||||||
|
|
||||||
|
switch name {
|
||||||
|
case "new":
|
||||||
|
log := n.root.ctx.Value("log").(func(msg string, args ...any))
|
||||||
|
|
||||||
|
if n.publisher.IsRunning() {
|
||||||
|
log("pending note updated, timer reset.")
|
||||||
|
} else {
|
||||||
|
log("new note detected")
|
||||||
|
if n.root.opts.AutoPublishNotesTimeout.Hours() < 24*365 {
|
||||||
|
log(", publishing it in %d seconds...\n", int(n.root.opts.AutoPublishNotesTimeout.Seconds()))
|
||||||
|
} else {
|
||||||
|
log(".\n")
|
||||||
|
}
|
||||||
|
log("- `touch publish` to publish immediately\n")
|
||||||
|
log("- `rm new` to erase and cancel the publication.\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
n.publisher.Call(n.publishNote)
|
||||||
|
|
||||||
|
first := true
|
||||||
|
|
||||||
|
return n.NewPersistentInode(
|
||||||
|
n.root.ctx,
|
||||||
|
n.root.NewWriteableFile(n.publishing.note, uint64(nostr.Now()), uint64(nostr.Now()), func(s string) {
|
||||||
|
if !first {
|
||||||
|
log("pending note updated, timer reset.\n")
|
||||||
|
}
|
||||||
|
first = false
|
||||||
|
n.publishing.note = strings.TrimSpace(s)
|
||||||
|
n.publisher.Call(n.publishNote)
|
||||||
|
}),
|
||||||
|
fs.StableAttr{},
|
||||||
|
), nil, 0, fs.OK
|
||||||
|
case "publish":
|
||||||
|
if n.publisher.IsRunning() {
|
||||||
|
// this causes the publish process to be triggered faster
|
||||||
|
log := n.root.ctx.Value("log").(func(msg string, args ...any))
|
||||||
|
log("publishing now!\n")
|
||||||
|
n.publisher.Flush()
|
||||||
|
return nil, nil, 0, syscall.ENOTDIR
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil, 0, syscall.ENOTSUP
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *ViewDir) Unlink(ctx context.Context, name string) syscall.Errno {
|
||||||
|
if !n.createable || n.root.rootPubKey != n.filter.Authors[0] {
|
||||||
|
return syscall.EPERM
|
||||||
|
}
|
||||||
|
if n.publisher == nil {
|
||||||
|
n.publisher = debouncer.New(n.root.opts.AutoPublishNotesTimeout)
|
||||||
|
}
|
||||||
|
if n.filter.Kinds[0] != 1 {
|
||||||
|
return syscall.ENOTSUP
|
||||||
|
}
|
||||||
|
|
||||||
|
switch name {
|
||||||
|
case "new":
|
||||||
|
log := n.root.ctx.Value("log").(func(msg string, args ...any))
|
||||||
|
log("publishing canceled.\n")
|
||||||
|
n.publisher.Stop()
|
||||||
|
n.publishing.note = ""
|
||||||
|
return fs.OK
|
||||||
|
}
|
||||||
|
|
||||||
|
return syscall.ENOTSUP
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *ViewDir) publishNote() {
|
||||||
|
log := n.root.ctx.Value("log").(func(msg string, args ...any))
|
||||||
|
|
||||||
|
log("publishing note...\n")
|
||||||
|
evt := nostr.Event{
|
||||||
|
Kind: 1,
|
||||||
|
CreatedAt: nostr.Now(),
|
||||||
|
Content: n.publishing.note,
|
||||||
|
Tags: make(nostr.Tags, 0, 2),
|
||||||
|
}
|
||||||
|
|
||||||
|
// our write relays
|
||||||
|
relays := n.root.sys.FetchWriteRelays(n.root.ctx, n.root.rootPubKey)
|
||||||
|
if len(relays) == 0 {
|
||||||
|
relays = n.root.sys.FetchOutboxRelays(n.root.ctx, n.root.rootPubKey, 6)
|
||||||
|
}
|
||||||
|
|
||||||
|
// massage and extract tags from raw text
|
||||||
|
targetRelays := n.root.sys.PrepareNoteEvent(n.root.ctx, &evt)
|
||||||
|
relays = nostr.AppendUnique(relays, targetRelays...)
|
||||||
|
|
||||||
|
// sign and publish
|
||||||
|
if err := n.root.signer.SignEvent(n.root.ctx, &evt); err != nil {
|
||||||
|
log("failed to sign: %s\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log(evt.String() + "\n")
|
||||||
|
|
||||||
|
log("publishing to %d relays... ", len(relays))
|
||||||
|
success := false
|
||||||
|
first := true
|
||||||
|
for res := range n.root.sys.Pool.PublishMany(n.root.ctx, relays, evt) {
|
||||||
|
cleanUrl, _ := strings.CutPrefix(res.RelayURL, "wss://")
|
||||||
|
if !first {
|
||||||
|
log(", ")
|
||||||
|
}
|
||||||
|
first = false
|
||||||
|
|
||||||
|
if res.Error != nil {
|
||||||
|
log("%s: %s", color.RedString(cleanUrl), res.Error)
|
||||||
|
} else {
|
||||||
|
success = true
|
||||||
|
log("%s: ok", color.GreenString(cleanUrl))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log("\n")
|
||||||
|
|
||||||
|
if success {
|
||||||
|
n.RmChild("new")
|
||||||
|
n.AddChild(evt.ID.Hex(), n.root.CreateEventDir(n, &evt), true)
|
||||||
|
log("event published as %s and updated locally.\n", color.BlueString(evt.ID.Hex()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *ViewDir) Getattr(_ context.Context, f fs.FileHandle, out *fuse.AttrOut) syscall.Errno {
|
||||||
|
now := nostr.Now()
|
||||||
|
if n.filter.Until != 0 {
|
||||||
|
now = n.filter.Until
|
||||||
|
}
|
||||||
|
aMonthAgo := now - 30*24*60*60
|
||||||
|
out.Mtime = uint64(aMonthAgo)
|
||||||
|
|
||||||
|
return fs.OK
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *ViewDir) Opendir(ctx context.Context) syscall.Errno {
|
||||||
|
if n.fetched.CompareAndSwap(true, true) {
|
||||||
|
return fs.OK
|
||||||
|
}
|
||||||
|
|
||||||
|
if n.paginate {
|
||||||
|
now := nostr.Now()
|
||||||
|
if n.filter.Until != 0 {
|
||||||
|
now = n.filter.Until
|
||||||
|
}
|
||||||
|
aMonthAgo := now - 30*24*60*60
|
||||||
|
n.filter.Since = aMonthAgo
|
||||||
|
|
||||||
|
filter := n.filter
|
||||||
|
filter.Until = aMonthAgo
|
||||||
|
|
||||||
|
n.AddChild("@previous", n.NewPersistentInode(
|
||||||
|
n.root.ctx,
|
||||||
|
&ViewDir{
|
||||||
|
root: n.root,
|
||||||
|
filter: filter,
|
||||||
|
relays: n.relays,
|
||||||
|
replaceable: n.replaceable,
|
||||||
|
},
|
||||||
|
fs.StableAttr{Mode: syscall.S_IFDIR},
|
||||||
|
), true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if n.replaceable {
|
||||||
|
for rkey, evt := range n.root.sys.Pool.FetchManyReplaceable(n.root.ctx, n.relays, n.filter, nostr.SubscriptionOptions{
|
||||||
|
Label: "nakfs",
|
||||||
|
}).Range {
|
||||||
|
name := rkey.D
|
||||||
|
if name == "" {
|
||||||
|
name = "_"
|
||||||
|
}
|
||||||
|
if n.GetChild(name) == nil {
|
||||||
|
n.AddChild(name, n.root.CreateEntityDir(n, &evt), true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for ie := range n.root.sys.Pool.FetchMany(n.root.ctx, n.relays, n.filter,
|
||||||
|
nostr.SubscriptionOptions{
|
||||||
|
Label: "nakfs",
|
||||||
|
}) {
|
||||||
|
if n.GetChild(ie.Event.ID.Hex()) == nil {
|
||||||
|
n.AddChild(ie.Event.ID.Hex(), n.root.CreateEventDir(n, &ie.Event), true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fs.OK
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *ViewDir) Mkdir(ctx context.Context, name string, mode uint32, out *fuse.EntryOut) (*fs.Inode, syscall.Errno) {
|
||||||
|
if !n.createable || n.root.signer == nil || n.root.rootPubKey != n.filter.Authors[0] {
|
||||||
|
return nil, syscall.ENOTSUP
|
||||||
|
}
|
||||||
|
|
||||||
|
if n.replaceable {
|
||||||
|
// create a template event that can later be modified and published as new
|
||||||
|
return n.root.CreateEntityDir(n, &nostr.Event{
|
||||||
|
PubKey: n.root.rootPubKey,
|
||||||
|
CreatedAt: 0,
|
||||||
|
Kind: n.filter.Kinds[0],
|
||||||
|
Tags: nostr.Tags{
|
||||||
|
nostr.Tag{"d", name},
|
||||||
|
},
|
||||||
|
}), fs.OK
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, syscall.ENOTSUP
|
||||||
|
}
|
||||||
93
nostrfs/writeablefile.go
Normal file
93
nostrfs/writeablefile.go
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
package nostrfs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/hanwen/go-fuse/v2/fs"
|
||||||
|
"github.com/hanwen/go-fuse/v2/fuse"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WriteableFile struct {
|
||||||
|
fs.Inode
|
||||||
|
root *NostrRoot
|
||||||
|
mu sync.Mutex
|
||||||
|
data []byte
|
||||||
|
attr fuse.Attr
|
||||||
|
onWrite func(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ = (fs.NodeOpener)((*WriteableFile)(nil))
|
||||||
|
_ = (fs.NodeReader)((*WriteableFile)(nil))
|
||||||
|
_ = (fs.NodeWriter)((*WriteableFile)(nil))
|
||||||
|
_ = (fs.NodeGetattrer)((*WriteableFile)(nil))
|
||||||
|
_ = (fs.NodeSetattrer)((*WriteableFile)(nil))
|
||||||
|
_ = (fs.NodeFlusher)((*WriteableFile)(nil))
|
||||||
|
)
|
||||||
|
|
||||||
|
func (r *NostrRoot) NewWriteableFile(data string, ctime, mtime uint64, onWrite func(string)) *WriteableFile {
|
||||||
|
return &WriteableFile{
|
||||||
|
root: r,
|
||||||
|
data: []byte(data),
|
||||||
|
attr: fuse.Attr{
|
||||||
|
Mode: 0666,
|
||||||
|
Ctime: ctime,
|
||||||
|
Mtime: mtime,
|
||||||
|
Size: uint64(len(data)),
|
||||||
|
},
|
||||||
|
onWrite: onWrite,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *WriteableFile) Open(ctx context.Context, flags uint32) (fh fs.FileHandle, fuseFlags uint32, errno syscall.Errno) {
|
||||||
|
return nil, fuse.FOPEN_KEEP_CACHE, fs.OK
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *WriteableFile) Write(ctx context.Context, fh fs.FileHandle, data []byte, off int64) (uint32, syscall.Errno) {
|
||||||
|
f.mu.Lock()
|
||||||
|
defer f.mu.Unlock()
|
||||||
|
|
||||||
|
offset := int(off)
|
||||||
|
end := offset + len(data)
|
||||||
|
if len(f.data) < end {
|
||||||
|
newData := make([]byte, offset+len(data))
|
||||||
|
copy(newData, f.data)
|
||||||
|
f.data = newData
|
||||||
|
}
|
||||||
|
copy(f.data[offset:], data)
|
||||||
|
f.data = f.data[0:end]
|
||||||
|
|
||||||
|
f.onWrite(string(f.data))
|
||||||
|
return uint32(len(data)), fs.OK
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *WriteableFile) Getattr(ctx context.Context, fh fs.FileHandle, out *fuse.AttrOut) syscall.Errno {
|
||||||
|
f.mu.Lock()
|
||||||
|
defer f.mu.Unlock()
|
||||||
|
out.Attr = f.attr
|
||||||
|
out.Attr.Size = uint64(len(f.data))
|
||||||
|
return fs.OK
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *WriteableFile) Setattr(_ context.Context, _ fs.FileHandle, in *fuse.SetAttrIn, _ *fuse.AttrOut) syscall.Errno {
|
||||||
|
f.attr.Mtime = in.Mtime
|
||||||
|
f.attr.Atime = in.Atime
|
||||||
|
f.attr.Ctime = in.Ctime
|
||||||
|
return fs.OK
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *WriteableFile) Flush(ctx context.Context, fh fs.FileHandle) syscall.Errno {
|
||||||
|
return fs.OK
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *WriteableFile) Read(ctx context.Context, fh fs.FileHandle, dest []byte, off int64) (fuse.ReadResult, syscall.Errno) {
|
||||||
|
f.mu.Lock()
|
||||||
|
defer f.mu.Unlock()
|
||||||
|
end := int(off) + len(dest)
|
||||||
|
if end > len(f.data) {
|
||||||
|
end = len(f.data)
|
||||||
|
}
|
||||||
|
return fuse.ReadResultData(f.data[off:end]), fs.OK
|
||||||
|
}
|
||||||
103
outbox.go
Normal file
103
outbox.go
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"fiatjaf.com/nostr"
|
||||||
|
"fiatjaf.com/nostr/sdk"
|
||||||
|
"fiatjaf.com/nostr/sdk/hints/badgerh"
|
||||||
|
"github.com/fatih/color"
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
hintsFilePath string
|
||||||
|
hintsFileExists bool
|
||||||
|
)
|
||||||
|
|
||||||
|
func initializeOutboxHintsDB(c *cli.Command, sys *sdk.System) error {
|
||||||
|
configPath := c.String("config-path")
|
||||||
|
if configPath == "" {
|
||||||
|
if home, err := os.UserHomeDir(); err == nil {
|
||||||
|
configPath = filepath.Join(home, ".config/nak")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if configPath != "" {
|
||||||
|
hintsFilePath = filepath.Join(configPath, "outbox/hints.bg")
|
||||||
|
}
|
||||||
|
if hintsFilePath != "" {
|
||||||
|
if _, err := os.Stat(hintsFilePath); err == nil {
|
||||||
|
hintsFileExists = true
|
||||||
|
} else if !os.IsNotExist(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if hintsFileExists && hintsFilePath != "" {
|
||||||
|
hintsdb, err := badgerh.NewBadgerHints(hintsFilePath)
|
||||||
|
if err == nil {
|
||||||
|
sys.Hints = hintsdb
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var outbox = &cli.Command{
|
||||||
|
Name: "outbox",
|
||||||
|
Usage: "manage outbox relay hints database",
|
||||||
|
DisableSliceFlagSeparator: true,
|
||||||
|
Commands: []*cli.Command{
|
||||||
|
{
|
||||||
|
Name: "init",
|
||||||
|
Usage: "initialize the outbox hints database",
|
||||||
|
DisableSliceFlagSeparator: true,
|
||||||
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
|
if hintsFileExists {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if hintsFilePath == "" {
|
||||||
|
return fmt.Errorf("couldn't find a place to store the hints, pass --config-path to fix.")
|
||||||
|
}
|
||||||
|
|
||||||
|
os.MkdirAll(hintsFilePath, 0755)
|
||||||
|
_, err := badgerh.NewBadgerHints(hintsFilePath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create badger hints db at '%s': %w", hintsFilePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log("initialized hints database at %s\n", hintsFilePath)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "list",
|
||||||
|
Usage: "list outbox relays for a given pubkey",
|
||||||
|
ArgsUsage: "<pubkey>",
|
||||||
|
DisableSliceFlagSeparator: true,
|
||||||
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
|
if !hintsFileExists {
|
||||||
|
log(color.YellowString("running with temporary fragile data.\n"))
|
||||||
|
log(color.YellowString("call `nak outbox init` to setup persistence.\n"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Args().Len() != 1 {
|
||||||
|
return fmt.Errorf("expected exactly one argument (pubkey)")
|
||||||
|
}
|
||||||
|
|
||||||
|
pk, err := nostr.PubKeyFromHex(c.Args().First())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid public key '%s': %w", c.Args().First(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, relay := range sys.FetchOutboxRelays(ctx, pk, 6) {
|
||||||
|
stdout(relay)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
73
paginate.go
73
paginate.go
@@ -1,73 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"math"
|
|
||||||
"slices"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/nbd-wtf/go-nostr"
|
|
||||||
)
|
|
||||||
|
|
||||||
func paginateWithPoolAndParams(pool *nostr.SimplePool, interval time.Duration, globalLimit uint64) func(ctx context.Context, urls []string, filters nostr.Filters) chan nostr.IncomingEvent {
|
|
||||||
return func(ctx context.Context, urls []string, filters nostr.Filters) chan nostr.IncomingEvent {
|
|
||||||
// filters will always be just one
|
|
||||||
filter := filters[0]
|
|
||||||
|
|
||||||
nextUntil := nostr.Now()
|
|
||||||
if filter.Until != nil {
|
|
||||||
nextUntil = *filter.Until
|
|
||||||
}
|
|
||||||
|
|
||||||
if globalLimit == 0 {
|
|
||||||
globalLimit = uint64(filter.Limit)
|
|
||||||
if globalLimit == 0 && !filter.LimitZero {
|
|
||||||
globalLimit = math.MaxUint64
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var globalCount uint64 = 0
|
|
||||||
globalCh := make(chan nostr.IncomingEvent)
|
|
||||||
|
|
||||||
repeatedCache := make([]string, 0, 300)
|
|
||||||
nextRepeatedCache := make([]string, 0, 300)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
defer close(globalCh)
|
|
||||||
|
|
||||||
for {
|
|
||||||
filter.Until = &nextUntil
|
|
||||||
time.Sleep(interval)
|
|
||||||
|
|
||||||
keepGoing := false
|
|
||||||
for evt := range pool.SubManyEose(ctx, urls, nostr.Filters{filter}) {
|
|
||||||
if slices.Contains(repeatedCache, evt.ID) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
keepGoing = true // if we get one that isn't repeated, then keep trying to get more
|
|
||||||
nextRepeatedCache = append(nextRepeatedCache, evt.ID)
|
|
||||||
|
|
||||||
globalCh <- evt
|
|
||||||
|
|
||||||
globalCount++
|
|
||||||
if globalCount >= globalLimit {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if evt.CreatedAt < *filter.Until {
|
|
||||||
nextUntil = evt.CreatedAt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !keepGoing {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
repeatedCache = nextRepeatedCache
|
|
||||||
nextRepeatedCache = nextRepeatedCache[:0]
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return globalCh
|
|
||||||
}
|
|
||||||
}
|
|
||||||
181
publish.go
Normal file
181
publish.go
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"fiatjaf.com/nostr"
|
||||||
|
"fiatjaf.com/nostr/nip19"
|
||||||
|
"fiatjaf.com/nostr/sdk"
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
var publish = &cli.Command{
|
||||||
|
Name: "publish",
|
||||||
|
Usage: "publishes a note with content from stdin",
|
||||||
|
Description: `reads content from stdin and publishes it as a note, optionally as a reply to another note.
|
||||||
|
|
||||||
|
example:
|
||||||
|
echo "hello world" | nak publish
|
||||||
|
echo "I agree!" | nak publish --reply nevent1...
|
||||||
|
echo "tagged post" | nak publish -t t=mytag -t e=someeventid`,
|
||||||
|
DisableSliceFlagSeparator: true,
|
||||||
|
Flags: append(defaultKeyFlags,
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "reply",
|
||||||
|
Usage: "event id, naddr1 or nevent1 code to reply to",
|
||||||
|
},
|
||||||
|
&cli.StringSliceFlag{
|
||||||
|
Name: "tag",
|
||||||
|
Aliases: []string{"t"},
|
||||||
|
Usage: "sets a tag field on the event, takes a value like -t e=<id> or -t sometag=\"value one;value two;value three\"",
|
||||||
|
},
|
||||||
|
&NaturalTimeFlag{
|
||||||
|
Name: "created-at",
|
||||||
|
Aliases: []string{"time", "ts"},
|
||||||
|
Usage: "unix timestamp value for the created_at field",
|
||||||
|
DefaultText: "now",
|
||||||
|
Value: nostr.Now(),
|
||||||
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "auth",
|
||||||
|
Usage: "always perform nip42 \"AUTH\" when facing an \"auth-required: \" rejection and try again",
|
||||||
|
Category: CATEGORY_EXTRAS,
|
||||||
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "nevent",
|
||||||
|
Usage: "print the nevent code (to stderr) after the event is published",
|
||||||
|
Category: CATEGORY_EXTRAS,
|
||||||
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "confirm",
|
||||||
|
Usage: "ask before publishing the event",
|
||||||
|
Category: CATEGORY_EXTRAS,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
|
content, err := io.ReadAll(os.Stdin)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read from stdin: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
evt := nostr.Event{
|
||||||
|
Kind: 1,
|
||||||
|
Content: strings.TrimSpace(string(content)),
|
||||||
|
Tags: make(nostr.Tags, 0, 4),
|
||||||
|
CreatedAt: nostr.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle timestamp flag
|
||||||
|
if c.IsSet("created-at") {
|
||||||
|
evt.CreatedAt = getNaturalDate(c, "created-at")
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle reply flag
|
||||||
|
var replyRelays []string
|
||||||
|
if replyTo := c.String("reply"); replyTo != "" {
|
||||||
|
var replyEvent *nostr.Event
|
||||||
|
|
||||||
|
// try to decode as nevent or naddr first
|
||||||
|
if strings.HasPrefix(replyTo, "nevent1") || strings.HasPrefix(replyTo, "naddr1") {
|
||||||
|
_, value, err := nip19.Decode(replyTo)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid reply target: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch pointer := value.(type) {
|
||||||
|
case nostr.EventPointer:
|
||||||
|
replyEvent, _, err = sys.FetchSpecificEvent(ctx, pointer, sdk.FetchSpecificEventParameters{})
|
||||||
|
case nostr.EntityPointer:
|
||||||
|
replyEvent, _, err = sys.FetchSpecificEvent(ctx, pointer, sdk.FetchSpecificEventParameters{})
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to fetch reply target event: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// try as raw event ID
|
||||||
|
id, err := nostr.IDFromHex(replyTo)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid event id: %w", err)
|
||||||
|
}
|
||||||
|
replyEvent, _, err = sys.FetchSpecificEvent(ctx, nostr.EventPointer{ID: id}, sdk.FetchSpecificEventParameters{})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to fetch reply target event: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if replyEvent.Kind != 1 {
|
||||||
|
evt.Kind = 1111
|
||||||
|
}
|
||||||
|
|
||||||
|
// add reply tags
|
||||||
|
evt.Tags = append(evt.Tags,
|
||||||
|
nostr.Tag{"e", replyEvent.ID.Hex(), "", "reply"},
|
||||||
|
nostr.Tag{"p", replyEvent.PubKey.Hex()},
|
||||||
|
)
|
||||||
|
|
||||||
|
replyRelays = sys.FetchInboxRelays(ctx, replyEvent.PubKey, 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle other tags -- copied from event.go
|
||||||
|
tagFlags := c.StringSlice("tag")
|
||||||
|
for _, tagFlag := range tagFlags {
|
||||||
|
// tags are in the format key=value
|
||||||
|
tagName, tagValue, found := strings.Cut(tagFlag, "=")
|
||||||
|
tag := []string{tagName}
|
||||||
|
if found {
|
||||||
|
// tags may also contain extra elements separated with a ";"
|
||||||
|
tagValues := strings.Split(tagValue, ";")
|
||||||
|
tag = append(tag, tagValues...)
|
||||||
|
}
|
||||||
|
evt.Tags = append(evt.Tags, tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
// process the content
|
||||||
|
targetRelays := sys.PrepareNoteEvent(ctx, &evt)
|
||||||
|
|
||||||
|
// connect to all the relays (like event.go)
|
||||||
|
kr, _, err := gatherKeyerFromArguments(ctx, c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pk, err := kr.GetPublicKey(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get our public key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
relayUrls := sys.FetchWriteRelays(ctx, pk)
|
||||||
|
relayUrls = nostr.AppendUnique(relayUrls, targetRelays...)
|
||||||
|
relayUrls = nostr.AppendUnique(relayUrls, replyRelays...)
|
||||||
|
relayUrls = nostr.AppendUnique(relayUrls, c.Args().Slice()...)
|
||||||
|
relays := connectToAllRelays(ctx, c, relayUrls, nil,
|
||||||
|
nostr.PoolOptions{
|
||||||
|
AuthHandler: func(ctx context.Context, authEvent *nostr.Event) error {
|
||||||
|
return authSigner(ctx, c, func(s string, args ...any) {}, authEvent)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(relays) == 0 {
|
||||||
|
if len(relayUrls) == 0 {
|
||||||
|
return fmt.Errorf("no relays to publish this note to.")
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("failed to connect to any of [ %v ].", relayUrls)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sign the event
|
||||||
|
if err := kr.SignEvent(ctx, &evt); err != nil {
|
||||||
|
return fmt.Errorf("error signing event: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// print
|
||||||
|
stdout(evt.String())
|
||||||
|
|
||||||
|
// publish (like event.go)
|
||||||
|
return publishFlow(ctx, c, kr, evt, relays)
|
||||||
|
},
|
||||||
|
}
|
||||||
67
relay.go
67
relay.go
@@ -6,22 +6,25 @@ import (
|
|||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/fiatjaf/cli/v3"
|
"fiatjaf.com/nostr"
|
||||||
"github.com/nbd-wtf/go-nostr"
|
"fiatjaf.com/nostr/nip11"
|
||||||
"github.com/nbd-wtf/go-nostr/nip11"
|
"fiatjaf.com/nostr/nip86"
|
||||||
"github.com/nbd-wtf/go-nostr/nip86"
|
"github.com/urfave/cli/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
var relay = &cli.Command{
|
var relay = &cli.Command{
|
||||||
Name: "relay",
|
Name: "relay",
|
||||||
Usage: "gets the relay information document for the given relay, as JSON",
|
Usage: "gets the relay information document for the given relay, as JSON -- or allows usage of the relay management API.",
|
||||||
Description: `example:
|
Description: `examples:
|
||||||
nak relay nostr.wine`,
|
fetching relay information:
|
||||||
|
nak relay nostr.wine
|
||||||
|
|
||||||
|
managing a relay
|
||||||
|
nak relay nostr.wine banevent --sec 1234 --id 037eb3751073770ff17483b1b1ff125866cd5147668271975ef0a8a8e7ee184a --reason "I don't like it"`,
|
||||||
ArgsUsage: "<relay-url>",
|
ArgsUsage: "<relay-url>",
|
||||||
DisableSliceFlagSeparator: true,
|
DisableSliceFlagSeparator: true,
|
||||||
Action: func(ctx context.Context, c *cli.Command) error {
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
@@ -42,9 +45,7 @@ var relay = &cli.Command{
|
|||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
Commands: (func() []*cli.Command {
|
Commands: (func() []*cli.Command {
|
||||||
commands := make([]*cli.Command, 0, 12)
|
methods := []struct {
|
||||||
|
|
||||||
for _, def := range []struct {
|
|
||||||
method string
|
method string
|
||||||
args []string
|
args []string
|
||||||
}{
|
}{
|
||||||
@@ -66,7 +67,10 @@ var relay = &cli.Command{
|
|||||||
{"blockip", []string{"ip", "reason"}},
|
{"blockip", []string{"ip", "reason"}},
|
||||||
{"unblockip", []string{"ip", "reason"}},
|
{"unblockip", []string{"ip", "reason"}},
|
||||||
{"listblockedips", nil},
|
{"listblockedips", nil},
|
||||||
} {
|
}
|
||||||
|
|
||||||
|
commands := make([]*cli.Command, 0, len(methods))
|
||||||
|
for _, def := range methods {
|
||||||
def := def
|
def := def
|
||||||
|
|
||||||
flags := make([]cli.Flag, len(def.args), len(def.args)+4)
|
flags := make([]cli.Flag, len(def.args), len(def.args)+4)
|
||||||
@@ -74,33 +78,15 @@ var relay = &cli.Command{
|
|||||||
flags[i] = declareFlag(argName)
|
flags[i] = declareFlag(argName)
|
||||||
}
|
}
|
||||||
|
|
||||||
flags = append(flags,
|
flags = append(flags, defaultKeyFlags...)
|
||||||
&cli.StringFlag{
|
|
||||||
Name: "sec",
|
|
||||||
Usage: "secret key to sign the event, as nsec, ncryptsec or hex",
|
|
||||||
DefaultText: "the key '1'",
|
|
||||||
Value: "0000000000000000000000000000000000000000000000000000000000000001",
|
|
||||||
},
|
|
||||||
&cli.BoolFlag{
|
|
||||||
Name: "prompt-sec",
|
|
||||||
Usage: "prompt the user to paste a hex or nsec with which to sign the event",
|
|
||||||
},
|
|
||||||
&cli.StringFlag{
|
|
||||||
Name: "connect",
|
|
||||||
Usage: "sign event using NIP-46, expects a bunker://... URL",
|
|
||||||
},
|
|
||||||
&cli.StringFlag{
|
|
||||||
Name: "connect-as",
|
|
||||||
Usage: "private key to when communicating with the bunker given on --connect",
|
|
||||||
DefaultText: "a random key",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
cmd := &cli.Command{
|
cmd := &cli.Command{
|
||||||
Name: def.method,
|
Name: def.method,
|
||||||
Usage: fmt.Sprintf(`the "%s" relay management RPC call`, def.method),
|
Usage: fmt.Sprintf(`the "%s" relay management RPC call`, def.method),
|
||||||
Description: fmt.Sprintf(
|
Description: fmt.Sprintf(
|
||||||
`the "%s" management RPC call, see https://nips.nostr.com/86 for more information`, def.method),
|
`the "%s" management RPC call, see https://nips.nostr.com/86 for more information`, def.method),
|
||||||
|
Flags: flags,
|
||||||
|
DisableSliceFlagSeparator: true,
|
||||||
Action: func(ctx context.Context, c *cli.Command) error {
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
params := make([]any, len(def.args))
|
params := make([]any, len(def.args))
|
||||||
for i, argName := range def.args {
|
for i, argName := range def.args {
|
||||||
@@ -115,7 +101,7 @@ var relay = &cli.Command{
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
sec, bunker, err := gatherSecretKeyOrBunkerFromArguments(ctx, c)
|
kr, _, err := gatherKeyerFromArguments(ctx, c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -132,7 +118,7 @@ var relay = &cli.Command{
|
|||||||
|
|
||||||
// Authorization
|
// Authorization
|
||||||
payloadHash := sha256.Sum256(reqj)
|
payloadHash := sha256.Sum256(reqj)
|
||||||
authEvent := nostr.Event{
|
tokenEvent := nostr.Event{
|
||||||
Kind: 27235,
|
Kind: 27235,
|
||||||
CreatedAt: nostr.Now(),
|
CreatedAt: nostr.Now(),
|
||||||
Tags: nostr.Tags{
|
Tags: nostr.Tags{
|
||||||
@@ -141,14 +127,10 @@ var relay = &cli.Command{
|
|||||||
{"payload", hex.EncodeToString(payloadHash[:])},
|
{"payload", hex.EncodeToString(payloadHash[:])},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if bunker != nil {
|
if err := kr.SignEvent(ctx, &tokenEvent); err != nil {
|
||||||
if err := bunker.SignEvent(ctx, &authEvent); err != nil {
|
return fmt.Errorf("failed to sign token event: %w", err)
|
||||||
return fmt.Errorf("failed to sign with bunker: %w", err)
|
|
||||||
}
|
|
||||||
} else if err := authEvent.Sign(sec); err != nil {
|
|
||||||
return fmt.Errorf("error signing with provided key: %w", err)
|
|
||||||
}
|
}
|
||||||
evtj, _ := json.Marshal(authEvent)
|
evtj, _ := json.Marshal(tokenEvent)
|
||||||
req.Header.Set("Authorization", "Nostr "+base64.StdEncoding.EncodeToString(evtj))
|
req.Header.Set("Authorization", "Nostr "+base64.StdEncoding.EncodeToString(evtj))
|
||||||
|
|
||||||
// Content-Type
|
// Content-Type
|
||||||
@@ -194,7 +176,6 @@ var relay = &cli.Command{
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
Flags: flags,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
commands = append(commands, cmd)
|
commands = append(commands, cmd)
|
||||||
|
|||||||
229
req.go
229
req.go
@@ -2,14 +2,16 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/fiatjaf/cli/v3"
|
"fiatjaf.com/nostr"
|
||||||
|
"fiatjaf.com/nostr/nip42"
|
||||||
|
"fiatjaf.com/nostr/nip77"
|
||||||
|
"github.com/fatih/color"
|
||||||
"github.com/mailru/easyjson"
|
"github.com/mailru/easyjson"
|
||||||
"github.com/nbd-wtf/go-nostr"
|
"github.com/urfave/cli/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -20,7 +22,7 @@ const (
|
|||||||
var req = &cli.Command{
|
var req = &cli.Command{
|
||||||
Name: "req",
|
Name: "req",
|
||||||
Usage: "generates encoded REQ messages and optionally use them to talk to relays",
|
Usage: "generates encoded REQ messages and optionally use them to talk to relays",
|
||||||
Description: `outputs a NIP-01 Nostr filter. when a relay is not given, will print the filter, otherwise will connect to the given relay and send the filter.
|
Description: `outputs a nip01 Nostr filter. when a relay is not given, will print the filter, otherwise will connect to the given relay and send the filter.
|
||||||
|
|
||||||
example:
|
example:
|
||||||
nak req -k 1 -l 15 wss://nostr.wine wss://nostr-pub.wellorder.net
|
nak req -k 1 -l 15 wss://nostr.wine wss://nostr-pub.wellorder.net
|
||||||
@@ -31,97 +33,78 @@ it can also take a filter from stdin, optionally modify it with flags and send i
|
|||||||
example:
|
example:
|
||||||
echo '{"kinds": [1], "#t": ["test"]}' | nak req -l 5 -k 4549 --tag t=spam wss://nostr-pub.wellorder.net`,
|
echo '{"kinds": [1], "#t": ["test"]}' | nak req -l 5 -k 4549 --tag t=spam wss://nostr-pub.wellorder.net`,
|
||||||
DisableSliceFlagSeparator: true,
|
DisableSliceFlagSeparator: true,
|
||||||
Flags: append(reqFilterFlags,
|
Flags: append(defaultKeyFlags,
|
||||||
&cli.BoolFlag{
|
append(reqFilterFlags,
|
||||||
Name: "stream",
|
&cli.BoolFlag{
|
||||||
Usage: "keep the subscription open, printing all events as they are returned",
|
Name: "ids-only",
|
||||||
DefaultText: "false, will close on EOSE",
|
Usage: "use nip77 to fetch just a list of ids",
|
||||||
},
|
},
|
||||||
&cli.BoolFlag{
|
&cli.BoolFlag{
|
||||||
Name: "paginate",
|
Name: "stream",
|
||||||
Usage: "make multiple REQs to the relay decreasing the value of 'until' until 'limit' or 'since' conditions are met",
|
Usage: "keep the subscription open, printing all events as they are returned",
|
||||||
DefaultText: "false",
|
DefaultText: "false, will close on EOSE",
|
||||||
},
|
},
|
||||||
&cli.DurationFlag{
|
&cli.BoolFlag{
|
||||||
Name: "paginate-interval",
|
Name: "paginate",
|
||||||
Usage: "time between queries when using --paginate",
|
Usage: "make multiple REQs to the relay decreasing the value of 'until' until 'limit' or 'since' conditions are met",
|
||||||
},
|
DefaultText: "false",
|
||||||
&cli.UintFlag{
|
},
|
||||||
Name: "paginate-global-limit",
|
&cli.DurationFlag{
|
||||||
Usage: "global limit at which --paginate should stop",
|
Name: "paginate-interval",
|
||||||
DefaultText: "uses the value given by --limit/-l or infinite",
|
Usage: "time between queries when using --paginate",
|
||||||
},
|
},
|
||||||
&cli.BoolFlag{
|
&cli.UintFlag{
|
||||||
Name: "bare",
|
Name: "paginate-global-limit",
|
||||||
Usage: "when printing the filter, print just the filter, not enveloped in a [\"REQ\", ...] array",
|
Usage: "global limit at which --paginate should stop",
|
||||||
},
|
DefaultText: "uses the value given by --limit/-l or infinite",
|
||||||
&cli.BoolFlag{
|
},
|
||||||
Name: "auth",
|
&cli.BoolFlag{
|
||||||
Usage: "always perform NIP-42 \"AUTH\" when facing an \"auth-required: \" rejection and try again",
|
Name: "bare",
|
||||||
},
|
Usage: "when printing the filter, print just the filter, not enveloped in a [\"REQ\", ...] array",
|
||||||
&cli.BoolFlag{
|
},
|
||||||
Name: "force-pre-auth",
|
&cli.BoolFlag{
|
||||||
Aliases: []string{"fpa"},
|
Name: "auth",
|
||||||
Usage: "after connecting, for a NIP-42 \"AUTH\" message to be received, act on it and only then send the \"REQ\"",
|
Usage: "always perform nip42 \"AUTH\" when facing an \"auth-required: \" rejection and try again",
|
||||||
Category: CATEGORY_SIGNER,
|
},
|
||||||
},
|
&cli.BoolFlag{
|
||||||
&cli.StringFlag{
|
Name: "force-pre-auth",
|
||||||
Name: "sec",
|
Aliases: []string{"fpa"},
|
||||||
Usage: "secret key to sign the AUTH challenge, as hex or nsec",
|
Usage: "after connecting, for a nip42 \"AUTH\" message to be received, act on it and only then send the \"REQ\"",
|
||||||
DefaultText: "the key '1'",
|
Category: CATEGORY_SIGNER,
|
||||||
Value: "0000000000000000000000000000000000000000000000000000000000000001",
|
},
|
||||||
Category: CATEGORY_SIGNER,
|
)...,
|
||||||
},
|
|
||||||
&cli.BoolFlag{
|
|
||||||
Name: "prompt-sec",
|
|
||||||
Usage: "prompt the user to paste a hex or nsec with which to sign the AUTH challenge",
|
|
||||||
Category: CATEGORY_SIGNER,
|
|
||||||
},
|
|
||||||
&cli.StringFlag{
|
|
||||||
Name: "connect",
|
|
||||||
Usage: "sign AUTH using NIP-46, expects a bunker://... URL",
|
|
||||||
Category: CATEGORY_SIGNER,
|
|
||||||
},
|
|
||||||
&cli.StringFlag{
|
|
||||||
Name: "connect-as",
|
|
||||||
Usage: "private key to when communicating with the bunker given on --connect",
|
|
||||||
DefaultText: "a random key",
|
|
||||||
Category: CATEGORY_SIGNER,
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
ArgsUsage: "[relay...]",
|
ArgsUsage: "[relay...]",
|
||||||
Action: func(ctx context.Context, c *cli.Command) error {
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
var pool *nostr.SimplePool
|
|
||||||
|
|
||||||
relayUrls := c.Args().Slice()
|
relayUrls := c.Args().Slice()
|
||||||
if len(relayUrls) > 0 {
|
if len(relayUrls) > 0 {
|
||||||
var relays []*nostr.Relay
|
// this is used both for the normal AUTH (after "auth-required:" is received) or forced pre-auth
|
||||||
pool, relays = connectToAllRelays(ctx, relayUrls, c.Bool("force-pre-auth"), nostr.WithAuthHandler(func(evt *nostr.Event) error {
|
// connect to all relays we expect to use in this call in parallel
|
||||||
if !c.Bool("auth") && !c.Bool("force-pre-auth") {
|
forcePreAuthSigner := authSigner
|
||||||
return fmt.Errorf("auth not authorized")
|
if !c.Bool("force-pre-auth") {
|
||||||
}
|
forcePreAuthSigner = nil
|
||||||
sec, bunker, err := gatherSecretKeyOrBunkerFromArguments(ctx, c)
|
}
|
||||||
if err != nil {
|
relays := connectToAllRelays(
|
||||||
return err
|
ctx,
|
||||||
}
|
c,
|
||||||
|
relayUrls,
|
||||||
|
forcePreAuthSigner,
|
||||||
|
nostr.PoolOptions{
|
||||||
|
AuthHandler: func(ctx context.Context, authEvent *nostr.Event) error {
|
||||||
|
return authSigner(ctx, c, func(s string, args ...any) {
|
||||||
|
if strings.HasPrefix(s, "authenticating as") {
|
||||||
|
cleanUrl, _ := strings.CutPrefix(
|
||||||
|
nip42.GetRelayURLFromAuthEvent(*authEvent),
|
||||||
|
"wss://",
|
||||||
|
)
|
||||||
|
s = "authenticating to " + color.CyanString(cleanUrl) + " as" + s[len("authenticating as"):]
|
||||||
|
}
|
||||||
|
log(s+"\n", args...)
|
||||||
|
}, authEvent)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
var pk string
|
// stop here already if all connections failed
|
||||||
if bunker != nil {
|
|
||||||
pk, err = bunker.GetPublicKey(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get public key from bunker: %w", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
pk, _ = nostr.GetPublicKey(sec)
|
|
||||||
}
|
|
||||||
log("performing auth as %s... ", pk)
|
|
||||||
|
|
||||||
if bunker != nil {
|
|
||||||
return bunker.SignEvent(ctx, evt)
|
|
||||||
} else {
|
|
||||||
return evt.Sign(sec)
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
if len(relays) == 0 {
|
if len(relays) == 0 {
|
||||||
log("failed to connect to any of the given relays.\n")
|
log("failed to connect to any of the given relays.\n")
|
||||||
os.Exit(3)
|
os.Exit(3)
|
||||||
@@ -130,15 +113,10 @@ example:
|
|||||||
for i, relay := range relays {
|
for i, relay := range relays {
|
||||||
relayUrls[i] = relay.URL
|
relayUrls[i] = relay.URL
|
||||||
}
|
}
|
||||||
|
|
||||||
defer func() {
|
|
||||||
for _, relay := range relays {
|
|
||||||
relay.Close()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for stdinFilter := range getStdinLinesOrBlank() {
|
// go line by line from stdin or run once with input from flags
|
||||||
|
for stdinFilter := range getJsonsOrBlank() {
|
||||||
filter := nostr.Filter{}
|
filter := nostr.Filter{}
|
||||||
if stdinFilter != "" {
|
if stdinFilter != "" {
|
||||||
if err := easyjson.Unmarshal([]byte(stdinFilter), &filter); err != nil {
|
if err := easyjson.Unmarshal([]byte(stdinFilter), &filter); err != nil {
|
||||||
@@ -152,15 +130,33 @@ example:
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(relayUrls) > 0 {
|
if len(relayUrls) > 0 {
|
||||||
fn := pool.SubManyEose
|
if c.Bool("ids-only") {
|
||||||
if c.Bool("paginate") {
|
seen := make(map[nostr.ID]struct{}, max(500, filter.Limit))
|
||||||
fn = paginateWithPoolAndParams(pool, c.Duration("paginate-interval"), c.Uint("paginate-global-limit"))
|
for _, url := range relayUrls {
|
||||||
} else if c.Bool("stream") {
|
ch, err := nip77.FetchIDsOnly(ctx, url, filter)
|
||||||
fn = pool.SubMany
|
if err != nil {
|
||||||
}
|
log("negentropy call to %s failed: %s", url, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for id := range ch {
|
||||||
|
if _, ok := seen[id]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[id] = struct{}{}
|
||||||
|
stdout(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fn := sys.Pool.FetchMany
|
||||||
|
if c.Bool("paginate") {
|
||||||
|
fn = sys.Pool.PaginatorWithInterval(c.Duration("paginate-interval"))
|
||||||
|
} else if c.Bool("stream") {
|
||||||
|
fn = sys.Pool.SubscribeMany
|
||||||
|
}
|
||||||
|
|
||||||
for ie := range fn(ctx, relayUrls, nostr.Filters{filter}) {
|
for ie := range fn(ctx, relayUrls, filter, nostr.SubscriptionOptions{}) {
|
||||||
stdout(ie.Event)
|
stdout(ie.Event)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// no relays given, will just print the filter
|
// no relays given, will just print the filter
|
||||||
@@ -168,7 +164,7 @@ example:
|
|||||||
if c.Bool("bare") {
|
if c.Bool("bare") {
|
||||||
result = filter.String()
|
result = filter.String()
|
||||||
} else {
|
} else {
|
||||||
j, _ := json.Marshal(nostr.ReqEnvelope{SubscriptionID: "nak", Filters: nostr.Filters{filter}})
|
j, _ := json.Marshal(nostr.ReqEnvelope{SubscriptionID: "nak", Filter: filter})
|
||||||
result = string(j)
|
result = string(j)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,13 +178,13 @@ example:
|
|||||||
}
|
}
|
||||||
|
|
||||||
var reqFilterFlags = []cli.Flag{
|
var reqFilterFlags = []cli.Flag{
|
||||||
&cli.StringSliceFlag{
|
&PubKeySliceFlag{
|
||||||
Name: "author",
|
Name: "author",
|
||||||
Aliases: []string{"a"},
|
Aliases: []string{"a"},
|
||||||
Usage: "only accept events from these authors (pubkey as hex)",
|
Usage: "only accept events from these authors (pubkey as hex)",
|
||||||
Category: CATEGORY_FILTER_ATTRIBUTES,
|
Category: CATEGORY_FILTER_ATTRIBUTES,
|
||||||
},
|
},
|
||||||
&cli.StringSliceFlag{
|
&IDSliceFlag{
|
||||||
Name: "id",
|
Name: "id",
|
||||||
Aliases: []string{"i"},
|
Aliases: []string{"i"},
|
||||||
Usage: "only accept events with these ids (hex)",
|
Usage: "only accept events with these ids (hex)",
|
||||||
@@ -241,28 +237,28 @@ var reqFilterFlags = []cli.Flag{
|
|||||||
},
|
},
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: "search",
|
Name: "search",
|
||||||
Usage: "a NIP-50 search query, use it only with relays that explicitly support it",
|
Usage: "a nip50 search query, use it only with relays that explicitly support it",
|
||||||
Category: CATEGORY_FILTER_ATTRIBUTES,
|
Category: CATEGORY_FILTER_ATTRIBUTES,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyFlagsToFilter(c *cli.Command, filter *nostr.Filter) error {
|
func applyFlagsToFilter(c *cli.Command, filter *nostr.Filter) error {
|
||||||
if authors := c.StringSlice("author"); len(authors) > 0 {
|
if authors := getPubKeySlice(c, "author"); len(authors) > 0 {
|
||||||
filter.Authors = append(filter.Authors, authors...)
|
filter.Authors = append(filter.Authors, authors...)
|
||||||
}
|
}
|
||||||
if ids := c.StringSlice("id"); len(ids) > 0 {
|
if ids := getIDSlice(c, "id"); len(ids) > 0 {
|
||||||
filter.IDs = append(filter.IDs, ids...)
|
filter.IDs = append(filter.IDs, ids...)
|
||||||
}
|
}
|
||||||
for _, kind64 := range c.IntSlice("kind") {
|
for _, kind64 := range c.IntSlice("kind") {
|
||||||
filter.Kinds = append(filter.Kinds, int(kind64))
|
filter.Kinds = append(filter.Kinds, nostr.Kind(kind64))
|
||||||
}
|
}
|
||||||
if search := c.String("search"); search != "" {
|
if search := c.String("search"); search != "" {
|
||||||
filter.Search = search
|
filter.Search = search
|
||||||
}
|
}
|
||||||
tags := make([][]string, 0, 5)
|
tags := make([][]string, 0, 5)
|
||||||
for _, tagFlag := range c.StringSlice("tag") {
|
for _, tagFlag := range c.StringSlice("tag") {
|
||||||
spl := strings.Split(tagFlag, "=")
|
spl := strings.SplitN(tagFlag, "=", 2)
|
||||||
if len(spl) == 2 && len(spl[0]) == 1 {
|
if len(spl) == 2 {
|
||||||
tags = append(tags, spl)
|
tags = append(tags, spl)
|
||||||
} else {
|
} else {
|
||||||
return fmt.Errorf("invalid --tag '%s'", tagFlag)
|
return fmt.Errorf("invalid --tag '%s'", tagFlag)
|
||||||
@@ -290,13 +286,10 @@ func applyFlagsToFilter(c *cli.Command, filter *nostr.Filter) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if c.IsSet("since") {
|
if c.IsSet("since") {
|
||||||
nts := getNaturalDate(c, "since")
|
filter.Since = getNaturalDate(c, "since")
|
||||||
filter.Since = &nts
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.IsSet("until") {
|
if c.IsSet("until") {
|
||||||
nts := getNaturalDate(c, "until")
|
filter.Until = getNaturalDate(c, "until")
|
||||||
filter.Until = &nts
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if limit := c.Uint("limit"); limit != 0 {
|
if limit := c.Uint("limit"); limit != 0 {
|
||||||
|
|||||||
74
serve.go
74
serve.go
@@ -3,18 +3,17 @@ package main
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
|
||||||
"os"
|
"os"
|
||||||
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"fiatjaf.com/nostr"
|
||||||
|
"fiatjaf.com/nostr/eventstore/slicestore"
|
||||||
|
"fiatjaf.com/nostr/khatru"
|
||||||
"github.com/bep/debounce"
|
"github.com/bep/debounce"
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
"github.com/fiatjaf/cli/v3"
|
"github.com/urfave/cli/v3"
|
||||||
"github.com/fiatjaf/eventstore/slicestore"
|
|
||||||
"github.com/fiatjaf/khatru"
|
|
||||||
"github.com/nbd-wtf/go-nostr"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var serve = &cli.Command{
|
var serve = &cli.Command{
|
||||||
@@ -39,7 +38,7 @@ var serve = &cli.Command{
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Action: func(ctx context.Context, c *cli.Command) error {
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
db := slicestore.SliceStore{MaxLimit: math.MaxInt}
|
db := &slicestore.SliceStore{}
|
||||||
|
|
||||||
var scanner *bufio.Scanner
|
var scanner *bufio.Scanner
|
||||||
if path := c.String("events"); path != "" {
|
if path := c.String("events"); path != "" {
|
||||||
@@ -60,16 +59,19 @@ var serve = &cli.Command{
|
|||||||
if err := json.Unmarshal(scanner.Bytes(), &evt); err != nil {
|
if err := json.Unmarshal(scanner.Bytes(), &evt); err != nil {
|
||||||
return fmt.Errorf("invalid event received at line %d: %s (`%s`)", i, err, scanner.Text())
|
return fmt.Errorf("invalid event received at line %d: %s (`%s`)", i, err, scanner.Text())
|
||||||
}
|
}
|
||||||
db.SaveEvent(ctx, &evt)
|
db.SaveEvent(evt)
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rl := khatru.NewRelay()
|
rl := khatru.NewRelay()
|
||||||
rl.QueryEvents = append(rl.QueryEvents, db.QueryEvents)
|
|
||||||
rl.CountEvents = append(rl.CountEvents, db.CountEvents)
|
rl.Info.Name = "nak serve"
|
||||||
rl.DeleteEvent = append(rl.DeleteEvent, db.DeleteEvent)
|
rl.Info.Description = "a local relay for testing, debugging and development."
|
||||||
rl.StoreEvent = append(rl.StoreEvent, db.SaveEvent)
|
rl.Info.Software = "https://github.com/fiatjaf/nak"
|
||||||
|
rl.Info.Version = version
|
||||||
|
|
||||||
|
rl.UseEventstore(db, 1_000_000)
|
||||||
|
|
||||||
started := make(chan bool)
|
started := make(chan bool)
|
||||||
exited := make(chan error)
|
exited := make(chan error)
|
||||||
@@ -82,44 +84,56 @@ var serve = &cli.Command{
|
|||||||
exited <- err
|
exited <- err
|
||||||
}()
|
}()
|
||||||
|
|
||||||
bold := color.New(color.Bold).Sprintf
|
|
||||||
italic := color.New(color.Italic).Sprint
|
|
||||||
|
|
||||||
var printStatus func()
|
var printStatus func()
|
||||||
|
|
||||||
// relay logging
|
// relay logging
|
||||||
rl.RejectFilter = append(rl.RejectFilter, func(ctx context.Context, filter nostr.Filter) (reject bool, msg string) {
|
rl.OnRequest = func(ctx context.Context, filter nostr.Filter) (reject bool, msg string) {
|
||||||
log(" got %s %v\n", color.HiYellowString("request"), italic(filter))
|
log(" got %s %v\n", color.HiYellowString("request"), colors.italic(filter))
|
||||||
printStatus()
|
printStatus()
|
||||||
return false, ""
|
return false, ""
|
||||||
})
|
}
|
||||||
rl.RejectCountFilter = append(rl.RejectCountFilter, func(ctx context.Context, filter nostr.Filter) (reject bool, msg string) {
|
|
||||||
log(" got %s %v\n", color.HiCyanString("count request"), italic(filter))
|
rl.OnCount = func(ctx context.Context, filter nostr.Filter) (reject bool, msg string) {
|
||||||
|
log(" got %s %v\n", color.HiCyanString("count request"), colors.italic(filter))
|
||||||
printStatus()
|
printStatus()
|
||||||
return false, ""
|
return false, ""
|
||||||
})
|
}
|
||||||
rl.RejectEvent = append(rl.RejectEvent, func(ctx context.Context, event *nostr.Event) (reject bool, msg string) {
|
|
||||||
log(" got %s %v\n", color.BlueString("event"), italic(event))
|
rl.OnEvent = func(ctx context.Context, event nostr.Event) (reject bool, msg string) {
|
||||||
|
log(" got %s %v\n", color.BlueString("event"), colors.italic(event))
|
||||||
printStatus()
|
printStatus()
|
||||||
return false, ""
|
return false, ""
|
||||||
})
|
}
|
||||||
|
|
||||||
|
totalConnections := atomic.Int32{}
|
||||||
|
rl.OnConnect = func(ctx context.Context) {
|
||||||
|
totalConnections.Add(1)
|
||||||
|
go func() {
|
||||||
|
<-ctx.Done()
|
||||||
|
totalConnections.Add(-1)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
d := debounce.New(time.Second * 2)
|
d := debounce.New(time.Second * 2)
|
||||||
printStatus = func() {
|
printStatus = func() {
|
||||||
d(func() {
|
d(func() {
|
||||||
totalEvents := 0
|
totalEvents, err := db.CountEvents(nostr.Filter{})
|
||||||
ch, _ := db.QueryEvents(ctx, nostr.Filter{})
|
if err != nil {
|
||||||
for range ch {
|
log("failed to count: %s\n", err)
|
||||||
totalEvents++
|
|
||||||
}
|
}
|
||||||
subs := rl.GetListeningFilters()
|
subs := rl.GetListeningFilters()
|
||||||
|
|
||||||
log(" %s events stored: %s, subscriptions opened: %s\n", color.HiMagentaString("•"), color.HiMagentaString("%d", totalEvents), color.HiMagentaString("%d", len(subs)))
|
log(" %s events: %s, connections: %s, subscriptions: %s\n",
|
||||||
|
color.HiMagentaString("•"),
|
||||||
|
color.HiMagentaString("%d", totalEvents),
|
||||||
|
color.HiMagentaString("%d", totalConnections.Load()),
|
||||||
|
color.HiMagentaString("%d", len(subs)),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
<-started
|
<-started
|
||||||
log("%s relay running at %s\n", color.HiRedString(">"), bold("ws://%s:%d", hostname, port))
|
log("%s relay running at %s\n", color.HiRedString(">"), colors.boldf("ws://%s:%d", hostname, port))
|
||||||
|
|
||||||
return <-exited
|
return <-exited
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,10 +2,9 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
|
|
||||||
"github.com/fiatjaf/cli/v3"
|
"fiatjaf.com/nostr"
|
||||||
"github.com/nbd-wtf/go-nostr"
|
"github.com/urfave/cli/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
var verify = &cli.Command{
|
var verify = &cli.Command{
|
||||||
@@ -31,8 +30,8 @@ it outputs nothing if the verification is successful.`,
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if ok, err := evt.CheckSignature(); !ok {
|
if !evt.VerifySignature() {
|
||||||
ctx = lineProcessingError(ctx, "invalid signature: %s", err)
|
ctx = lineProcessingError(ctx, "invalid signature")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
484
wallet.go
Normal file
484
wallet.go
Normal file
@@ -0,0 +1,484 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"fiatjaf.com/nostr"
|
||||||
|
"fiatjaf.com/nostr/nip60"
|
||||||
|
"fiatjaf.com/nostr/nip61"
|
||||||
|
"fiatjaf.com/nostr/sdk"
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func prepareWallet(ctx context.Context, c *cli.Command) (*nip60.Wallet, func(), error) {
|
||||||
|
kr, _, err := gatherKeyerFromArguments(ctx, c)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pk, err := kr.GetPublicKey(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
relays := sys.FetchOutboxRelays(ctx, pk, 3)
|
||||||
|
w := nip60.LoadWallet(ctx, kr, sys.Pool, relays, nip60.WalletOptions{})
|
||||||
|
if w == nil {
|
||||||
|
return nil, nil, fmt.Errorf("error loading walle")
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Processed = func(evt nostr.Event, err error) {
|
||||||
|
if err == nil {
|
||||||
|
logverbose("processed event %s\n", evt)
|
||||||
|
} else {
|
||||||
|
log("error processing event %s: %s\n", evt, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
w.PublishUpdate = func(event nostr.Event, deleted, received, change *nip60.Token, isHistory bool) {
|
||||||
|
desc := "wallet"
|
||||||
|
if received != nil {
|
||||||
|
mint, _ := strings.CutPrefix(received.Mint, "https://")
|
||||||
|
desc = fmt.Sprintf("received from %s with %d proofs totalling %d",
|
||||||
|
mint, len(received.Proofs), received.Proofs.Amount())
|
||||||
|
} else if change != nil {
|
||||||
|
mint, _ := strings.CutPrefix(change.Mint, "https://")
|
||||||
|
desc = fmt.Sprintf("change from %s with %d proofs totalling %d",
|
||||||
|
mint, len(change.Proofs), change.Proofs.Amount())
|
||||||
|
} else if deleted != nil {
|
||||||
|
mint, _ := strings.CutPrefix(deleted.Mint, "https://")
|
||||||
|
desc = fmt.Sprintf("deleting a used token from %s with %d proofs totalling %d",
|
||||||
|
mint, len(deleted.Proofs), deleted.Proofs.Amount())
|
||||||
|
} else if isHistory {
|
||||||
|
desc = "history entry"
|
||||||
|
}
|
||||||
|
|
||||||
|
log("- saving kind:%d event (%s)... ", event.Kind, desc)
|
||||||
|
first := true
|
||||||
|
for res := range sys.Pool.PublishMany(ctx, relays, event) {
|
||||||
|
cleanUrl, _ := strings.CutPrefix(res.RelayURL, "wss://")
|
||||||
|
if !first {
|
||||||
|
log(", ")
|
||||||
|
}
|
||||||
|
first = false
|
||||||
|
|
||||||
|
if res.Error != nil {
|
||||||
|
log("%s: %s", colors.errorf(cleanUrl), res.Error)
|
||||||
|
} else {
|
||||||
|
log("%s: ok", colors.successf(cleanUrl))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
<-w.Stable
|
||||||
|
|
||||||
|
return w, func() {
|
||||||
|
w.Close()
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var wallet = &cli.Command{
|
||||||
|
Name: "wallet",
|
||||||
|
Usage: "displays the current wallet balance",
|
||||||
|
Description: "all wallet data is stored on Nostr relays, signed and encrypted with the given key, and reloaded again from relays on every call.\n\nthe same data can be accessed by other compatible nip60 clients.",
|
||||||
|
DisableSliceFlagSeparator: true,
|
||||||
|
Flags: defaultKeyFlags,
|
||||||
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
|
w, closew, err := prepareWallet(ctx, c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
stdout(w.Balance())
|
||||||
|
|
||||||
|
closew()
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
Commands: []*cli.Command{
|
||||||
|
{
|
||||||
|
Name: "mints",
|
||||||
|
Usage: "lists, adds or remove default mints from the wallet",
|
||||||
|
DisableSliceFlagSeparator: true,
|
||||||
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
|
w, closew, err := prepareWallet(ctx, c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, url := range w.Mints {
|
||||||
|
stdout(strings.Split(url, "://")[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
closew()
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
Commands: []*cli.Command{
|
||||||
|
{
|
||||||
|
Name: "add",
|
||||||
|
DisableSliceFlagSeparator: true,
|
||||||
|
ArgsUsage: "<mint>...",
|
||||||
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
|
w, closew, err := prepareWallet(ctx, c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := w.AddMint(ctx, c.Args().Slice()...); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
closew()
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "remove",
|
||||||
|
DisableSliceFlagSeparator: true,
|
||||||
|
ArgsUsage: "<mint>...",
|
||||||
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
|
w, closew, err := prepareWallet(ctx, c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := w.RemoveMint(ctx, c.Args().Slice()...); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
closew()
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "tokens",
|
||||||
|
Usage: "lists existing tokens with their mints and aggregated amounts",
|
||||||
|
DisableSliceFlagSeparator: true,
|
||||||
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
|
w, closew, err := prepareWallet(ctx, c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, token := range w.Tokens {
|
||||||
|
stdout(token.ID(), token.Proofs.Amount(), strings.Split(token.Mint, "://")[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
closew()
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "receive",
|
||||||
|
Usage: "takes a cashu token string as an argument and adds it to the wallet",
|
||||||
|
ArgsUsage: "<token>",
|
||||||
|
DisableSliceFlagSeparator: true,
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.StringSliceFlag{
|
||||||
|
Name: "mint",
|
||||||
|
Usage: "mint to swap the token into",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
|
args := c.Args().Slice()
|
||||||
|
if len(args) != 1 {
|
||||||
|
return fmt.Errorf("must be called as `nak wallet receive <token>")
|
||||||
|
}
|
||||||
|
|
||||||
|
w, closew, err := prepareWallet(ctx, c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
proofs, mint, err := nip60.GetProofsAndMint(args[0])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := w.Receive(ctx, proofs, mint, nip60.ReceiveOptions{
|
||||||
|
IntoMint: c.StringSlice("mint"),
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
closew()
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "send",
|
||||||
|
Usage: "prints a cashu token with the given amount for sending to someone else",
|
||||||
|
ArgsUsage: "<amount>",
|
||||||
|
DisableSliceFlagSeparator: true,
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "mint",
|
||||||
|
Usage: "send from a specific mint",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
|
args := c.Args().Slice()
|
||||||
|
if len(args) != 1 {
|
||||||
|
return fmt.Errorf("must be called as `nak wallet send <amount>")
|
||||||
|
}
|
||||||
|
amount, err := strconv.ParseUint(args[0], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("amount '%s' is invalid", args[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
w, closew, err := prepareWallet(ctx, c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var sourceMint string
|
||||||
|
if mint := c.String("mint"); mint != "" {
|
||||||
|
sourceMint = "http" + nostr.NormalizeURL(mint)[2:]
|
||||||
|
}
|
||||||
|
proofs, mint, err := w.SendInternal(ctx, amount, nip60.SendOptions{
|
||||||
|
SpecificSourceMint: sourceMint,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
stdout(nip60.MakeTokenString(proofs, mint))
|
||||||
|
|
||||||
|
closew()
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "pay",
|
||||||
|
Usage: "pays a bolt11 lightning invoice and outputs the preimage",
|
||||||
|
ArgsUsage: "<invoice>",
|
||||||
|
DisableSliceFlagSeparator: true,
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "mint",
|
||||||
|
Usage: "pay from a specific mint",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
|
args := c.Args().Slice()
|
||||||
|
if len(args) != 1 {
|
||||||
|
return fmt.Errorf("must be called as `nak wallet pay <invoice>")
|
||||||
|
}
|
||||||
|
|
||||||
|
w, closew, err := prepareWallet(ctx, c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var sourceMint string
|
||||||
|
if mint := c.String("mint"); mint != "" {
|
||||||
|
sourceMint = "http" + nostr.NormalizeURL(mint)[2:]
|
||||||
|
}
|
||||||
|
|
||||||
|
preimage, err := w.PayBolt11(ctx, args[0], nip60.PayOptions{
|
||||||
|
FromMint: sourceMint,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
stdout(preimage)
|
||||||
|
|
||||||
|
closew()
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "nutzap",
|
||||||
|
Usage: "sends a nip61 nutzap to one or more Nostr profiles and/or events",
|
||||||
|
ArgsUsage: "<amount> <target>",
|
||||||
|
Description: "<amount> is in satoshis, <target> can be an npub, nprofile, nevent or hex pubkey.",
|
||||||
|
DisableSliceFlagSeparator: true,
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "mint",
|
||||||
|
Usage: "send from a specific mint",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "message",
|
||||||
|
Usage: "attach a message to the nutzap",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
|
args := c.Args().Slice()
|
||||||
|
if len(args) < 2 {
|
||||||
|
return fmt.Errorf("must be called as `nak wallet nutzap <amount> <target>...")
|
||||||
|
}
|
||||||
|
|
||||||
|
w, closew, err := prepareWallet(ctx, c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
amount, err := strconv.ParseInt(c.Args().First(), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid amount '%s': %w", c.Args().First(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
target := c.String("target")
|
||||||
|
var pm sdk.ProfileMetadata
|
||||||
|
|
||||||
|
var evt *nostr.Event
|
||||||
|
var eventId nostr.ID
|
||||||
|
|
||||||
|
if strings.HasPrefix(target, "nevent1") {
|
||||||
|
evt, _, err = sys.FetchSpecificEventFromInput(ctx, target, sdk.FetchSpecificEventParameters{
|
||||||
|
WithRelays: false,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
eventId = evt.ID
|
||||||
|
pm = sys.FetchProfileMetadata(ctx, evt.PubKey)
|
||||||
|
} else {
|
||||||
|
pm, err = sys.FetchProfileFromInput(ctx, target)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log("sending %d sat to '%s' (%s)", amount, pm.ShortName(), pm.Npub())
|
||||||
|
|
||||||
|
var sourceMint string
|
||||||
|
if mint := c.String("mint"); mint != "" {
|
||||||
|
sourceMint = "http" + nostr.NormalizeURL(mint)[2:]
|
||||||
|
}
|
||||||
|
|
||||||
|
kr, _, _ := gatherKeyerFromArguments(ctx, c)
|
||||||
|
results, err := nip61.SendNutzap(
|
||||||
|
ctx,
|
||||||
|
kr,
|
||||||
|
w,
|
||||||
|
sys.Pool,
|
||||||
|
uint64(amount),
|
||||||
|
pm.PubKey,
|
||||||
|
sys.FetchWriteRelays(ctx, pm.PubKey),
|
||||||
|
nip61.NutzapOptions{
|
||||||
|
Message: c.String("message"),
|
||||||
|
SendToRelays: sys.FetchInboxRelays(ctx, pm.PubKey, 3),
|
||||||
|
EventID: eventId,
|
||||||
|
SpecificSourceMint: sourceMint,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log("- publishing nutzap... ")
|
||||||
|
first := true
|
||||||
|
for res := range results {
|
||||||
|
cleanUrl, _ := strings.CutPrefix(res.RelayURL, "wss://")
|
||||||
|
if !first {
|
||||||
|
log(", ")
|
||||||
|
}
|
||||||
|
first = false
|
||||||
|
if res.Error != nil {
|
||||||
|
log("%s: %s", colors.errorf(cleanUrl), res.Error)
|
||||||
|
} else {
|
||||||
|
log("%s: ok", colors.successf(cleanUrl))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
closew()
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
Commands: []*cli.Command{
|
||||||
|
{
|
||||||
|
Name: "setup",
|
||||||
|
Usage: "setup your wallet private key and kind:10019 event for receiving nutzaps",
|
||||||
|
DisableSliceFlagSeparator: true,
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.StringSliceFlag{
|
||||||
|
Name: "mint",
|
||||||
|
Usage: "mints to receive nutzaps in",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "private-key",
|
||||||
|
Usage: "private key used for receiving nutzaps",
|
||||||
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "force",
|
||||||
|
Aliases: []string{"f"},
|
||||||
|
Usage: "forces replacement of private-key",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
|
w, closew, err := prepareWallet(ctx, c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if w.PrivateKey == nil {
|
||||||
|
if sk := c.String("private-key"); sk != "" {
|
||||||
|
if err := w.SetPrivateKey(ctx, sk); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("missing --private-key")
|
||||||
|
}
|
||||||
|
} else if sk := c.String("private-key"); sk != "" && !c.Bool("force") {
|
||||||
|
return fmt.Errorf("refusing to replace existing private key, use the --force flag")
|
||||||
|
}
|
||||||
|
|
||||||
|
kr, _, _ := gatherKeyerFromArguments(ctx, c)
|
||||||
|
pk, _ := kr.GetPublicKey(ctx)
|
||||||
|
relays := sys.FetchWriteRelays(ctx, pk)
|
||||||
|
|
||||||
|
info := nip61.Info{}
|
||||||
|
ie := sys.Pool.QuerySingle(ctx, relays, nostr.Filter{
|
||||||
|
Kinds: []nostr.Kind{10019},
|
||||||
|
Authors: []nostr.PubKey{pk},
|
||||||
|
Limit: 1,
|
||||||
|
}, nostr.SubscriptionOptions{})
|
||||||
|
if ie != nil {
|
||||||
|
info.ParseEvent(ie.Event)
|
||||||
|
}
|
||||||
|
|
||||||
|
if mints := c.StringSlice("mints"); len(mints) == 0 && len(info.Mints) == 0 {
|
||||||
|
info.Mints = w.Mints
|
||||||
|
}
|
||||||
|
if len(info.Mints) == 0 {
|
||||||
|
return fmt.Errorf("missing --mint")
|
||||||
|
}
|
||||||
|
|
||||||
|
evt := nostr.Event{}
|
||||||
|
if err := info.ToEvent(ctx, kr, &evt); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
stdout(evt)
|
||||||
|
log("- saving kind:10019 event... ")
|
||||||
|
first := true
|
||||||
|
for res := range sys.Pool.PublishMany(ctx, relays, evt) {
|
||||||
|
cleanUrl, _ := strings.CutPrefix(res.RelayURL, "wss://")
|
||||||
|
|
||||||
|
if !first {
|
||||||
|
log(", ")
|
||||||
|
}
|
||||||
|
first = false
|
||||||
|
|
||||||
|
if res.Error != nil {
|
||||||
|
log("%s: %s", colors.errorf(cleanUrl), res.Error)
|
||||||
|
} else {
|
||||||
|
log("%s: ok", colors.successf(cleanUrl))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
closew()
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
7
zapstore.yaml
Normal file
7
zapstore.yaml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
repository: https://github.com/fiatjaf/nak
|
||||||
|
assets:
|
||||||
|
- nak-v\d+\.\d+\.\d+-darwin-arm64
|
||||||
|
- nak-v\d+\.\d+\.\d+-linux-amd64
|
||||||
|
- nak-v\d+\.\d+\.\d+-linux-arm64
|
||||||
|
remote_metadata:
|
||||||
|
- github
|
||||||
Reference in New Issue
Block a user