mirror of
https://github.com/fiatjaf/nak.git
synced 2025-12-08 16:48:51 +00:00
Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1,3 @@
|
|||||||
nak
|
nak
|
||||||
mnt
|
mnt
|
||||||
|
nak.exe
|
||||||
|
|||||||
46
README.md
46
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
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -130,7 +130,7 @@ type the password to decrypt your secret key: **********
|
|||||||
|
|
||||||
### sign an event using a remote NIP-46 bunker
|
### sign an event using a remote NIP-46 bunker
|
||||||
```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'
|
~> 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'
|
||||||
```
|
```
|
||||||
|
|
||||||
### sign an event using a NIP-49 encrypted key
|
### sign an event using a NIP-49 encrypted key
|
||||||
@@ -229,6 +229,48 @@ type the password to decrypt your secret key: ********
|
|||||||
~> 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="))"')
|
~> 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.
|
||||||
|
```
|
||||||
|
|
||||||
## 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`.
|
||||||
|
|||||||
55
blossom.go
55
blossom.go
@@ -1,12 +1,15 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/nbd-wtf/go-nostr/keyer"
|
"fiatjaf.com/nostr"
|
||||||
"github.com/nbd-wtf/go-nostr/nipb0/blossom"
|
"fiatjaf.com/nostr/keyer"
|
||||||
|
"fiatjaf.com/nostr/nipb0/blossom"
|
||||||
"github.com/urfave/cli/v3"
|
"github.com/urfave/cli/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -35,7 +38,11 @@ var blossomCmd = &cli.Command{
|
|||||||
var client *blossom.Client
|
var client *blossom.Client
|
||||||
pubkey := c.Args().First()
|
pubkey := c.Args().First()
|
||||||
if pubkey != "" {
|
if pubkey != "" {
|
||||||
client = blossom.NewClient(client.GetMediaServer(), keyer.NewReadOnlySigner(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 {
|
} else {
|
||||||
var err error
|
var err error
|
||||||
client, err = getBlossomClient(ctx, c)
|
client, err = getBlossomClient(ctx, c)
|
||||||
@@ -68,22 +75,44 @@ var blossomCmd = &cli.Command{
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
hasError := false
|
if isPiped() {
|
||||||
for _, fpath := range c.Args().Slice() {
|
// get file from stdin
|
||||||
bd, err := client.UploadFile(ctx, fpath)
|
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 {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "%s\n", err)
|
return fmt.Errorf("failed to read stdin: %w", err)
|
||||||
hasError = true
|
}
|
||||||
continue
|
|
||||||
|
bd, err := client.UploadBlob(ctx, bytes.NewReader(data), "")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
j, _ := json.Marshal(bd)
|
j, _ := json.Marshal(bd)
|
||||||
stdout(string(j))
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if hasError {
|
|
||||||
os.Exit(3)
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -125,7 +154,7 @@ var blossomCmd = &cli.Command{
|
|||||||
hasError = true
|
hasError = true
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
os.Stdout.Write(data)
|
stdout(data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
49
bunker.go
49
bunker.go
@@ -10,11 +10,11 @@ import (
|
|||||||
"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/urfave/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"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var bunker = &cli.Command{
|
var bunker = &cli.Command{
|
||||||
@@ -38,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",
|
||||||
@@ -49,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)
|
||||||
@@ -70,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
|
||||||
@@ -78,20 +78,20 @@ 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)
|
|
||||||
|
|
||||||
// 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 - " + colors.italic(strings.Join(authorizedKeys, "\n - "))
|
authorizedKeysStr = "\n authorized keys:"
|
||||||
|
for _, pubkey := range authorizedKeys {
|
||||||
|
authorizedKeysStr += "\n - " + colors.italic(pubkey.Hex())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
authorizedSecretsStr := ""
|
authorizedSecretsStr := ""
|
||||||
@@ -101,7 +101,7 @@ var bunker = &cli.Command{
|
|||||||
|
|
||||||
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
|
||||||
@@ -129,7 +129,7 @@ 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",
|
||||||
colors.bold(relayURLs),
|
colors.bold(relayURLs),
|
||||||
colors.bold(pubkey),
|
colors.bold(pubkey.Hex()),
|
||||||
colors.bold(npub),
|
colors.bold(npub),
|
||||||
authorizedKeysStr,
|
authorizedKeysStr,
|
||||||
authorizedSecretsStr,
|
authorizedSecretsStr,
|
||||||
@@ -140,12 +140,13 @@ var bunker = &cli.Command{
|
|||||||
printBunkerInfo()
|
printBunkerInfo()
|
||||||
|
|
||||||
// subscribe to relays
|
// subscribe to relays
|
||||||
now := nostr.Now()
|
|
||||||
events := sys.Pool.SubscribeMany(ctx, relayURLs, nostr.Filter{
|
events := sys.Pool.SubscribeMany(ctx, relayURLs, nostr.Filter{
|
||||||
Kinds: []int{nostr.KindNostrConnect},
|
Kinds: []nostr.Kind{nostr.KindNostrConnect},
|
||||||
Tags: nostr.TagMap{"p": []string{pubkey}},
|
Tags: nostr.TagMap{"p": []string{pubkey.Hex()}},
|
||||||
Since: &now,
|
Since: nostr.Now(),
|
||||||
LimitZero: true,
|
LimitZero: true,
|
||||||
|
}, nostr.SubscriptionOptions{
|
||||||
|
Label: "nak-bunker",
|
||||||
})
|
})
|
||||||
|
|
||||||
signer := nip46.NewStaticKeySigner(sec)
|
signer := nip46.NewStaticKeySigner(sec)
|
||||||
@@ -158,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,7 +186,7 @@ var bunker = &cli.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
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))
|
||||||
|
|
||||||
@@ -236,11 +237,13 @@ var bunker = &cli.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
uri, err := url.Parse(c.Args().First())
|
uri, err := url.Parse(c.Args().First())
|
||||||
if err != nil || uri.Scheme != "nostrconnect" || !nostr.IsValidPublicKey(uri.Host) {
|
if err != nil || uri.Scheme != "nostrconnect" {
|
||||||
return fmt.Errorf("invalid uri")
|
return fmt.Errorf("invalid uri")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
// TODO
|
||||||
|
|
||||||
|
return fmt.Errorf("this is not implemented yet")
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
232
cli_test.go
Normal file
232
cli_test.go
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
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) (int, error) {
|
||||||
|
output.WriteString(fmt.Sprint(a...))
|
||||||
|
output.WriteString("\n")
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
45
count.go
45
count.go
@@ -6,10 +6,10 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"fiatjaf.com/nostr"
|
||||||
|
"fiatjaf.com/nostr/nip45"
|
||||||
|
"fiatjaf.com/nostr/nip45/hyperloglog"
|
||||||
"github.com/urfave/cli/v3"
|
"github.com/urfave/cli/v3"
|
||||||
"github.com/nbd-wtf/go-nostr"
|
|
||||||
"github.com/nbd-wtf/go-nostr/nip45"
|
|
||||||
"github.com/nbd-wtf/go-nostr/nip45/hyperloglog"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var count = &cli.Command{
|
var count = &cli.Command{
|
||||||
@@ -18,7 +18,7 @@ var count = &cli.Command{
|
|||||||
Description: `outputs a nip45 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)",
|
||||||
@@ -46,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)",
|
||||||
@@ -70,10 +70,7 @@ var count = &cli.Command{
|
|||||||
biggerUrlSize := 0
|
biggerUrlSize := 0
|
||||||
relayUrls := c.Args().Slice()
|
relayUrls := c.Args().Slice()
|
||||||
if len(relayUrls) > 0 {
|
if len(relayUrls) > 0 {
|
||||||
relays := connectToAllRelays(ctx,
|
relays := connectToAllRelays(ctx, c, relayUrls, nil, nostr.PoolOptions{})
|
||||||
relayUrls,
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
if len(relays) == 0 {
|
if len(relays) == 0 {
|
||||||
log("failed to connect to any of the given relays.\n")
|
log("failed to connect to any of the given relays.\n")
|
||||||
os.Exit(3)
|
os.Exit(3)
|
||||||
@@ -85,26 +82,17 @@ var count = &cli.Command{
|
|||||||
biggerUrlSize = len(relay.URL)
|
biggerUrlSize = len(relay.URL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
defer func() {
|
|
||||||
for _, relay := range relays {
|
|
||||||
relay.Close()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
filter := nostr.Filter{}
|
filter := nostr.Filter{}
|
||||||
|
|
||||||
if authors := c.StringSlice("author"); len(authors) > 0 {
|
if authors := 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
|
||||||
}
|
}
|
||||||
@@ -134,14 +122,13 @@ 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)
|
||||||
}
|
}
|
||||||
@@ -154,7 +141,7 @@ var count = &cli.Command{
|
|||||||
}
|
}
|
||||||
for _, relayUrl := range relayUrls {
|
for _, relayUrl := range relayUrls {
|
||||||
relay, _ := sys.Pool.EnsureRelay(relayUrl)
|
relay, _ := sys.Pool.EnsureRelay(relayUrl)
|
||||||
count, hllRegisters, err := relay.Count(ctx, nostr.Filters{filter})
|
count, hllRegisters, err := relay.Count(ctx, filter, nostr.SubscriptionOptions{})
|
||||||
fmt.Fprintf(os.Stderr, "%s%s: ", strings.Repeat(" ", biggerUrlSize-len(relayUrl)), relayUrl)
|
fmt.Fprintf(os.Stderr, "%s%s: ", strings.Repeat(" ", biggerUrlSize-len(relayUrl)), relayUrl)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
2
curl.go
2
curl.go
@@ -8,7 +8,7 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/nbd-wtf/go-nostr"
|
"fiatjaf.com/nostr"
|
||||||
"github.com/urfave/cli/v3"
|
"github.com/urfave/cli/v3"
|
||||||
"golang.org/x/exp/slices"
|
"golang.org/x/exp/slices"
|
||||||
)
|
)
|
||||||
|
|||||||
117
decode.go
117
decode.go
@@ -3,12 +3,13 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
stdjson "encoding/json"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"fiatjaf.com/nostr"
|
||||||
|
"fiatjaf.com/nostr/nip05"
|
||||||
|
"fiatjaf.com/nostr/nip19"
|
||||||
"github.com/urfave/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/sdk"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var decode = &cli.Command{
|
var decode = &cli.Command{
|
||||||
@@ -39,88 +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}
|
|
||||||
if c.Bool("id") {
|
pp, _ := nip05.QueryIdentifier(ctx, input)
|
||||||
stdout(evp.ID)
|
if pp != nil {
|
||||||
continue
|
|
||||||
}
|
|
||||||
} else if pp := sdk.InputToProfile(ctx, input); pp != nil {
|
|
||||||
decodeResult = DecodeResult{ProfilePointer: pp}
|
|
||||||
if c.Bool("pubkey") {
|
if c.Bool("pubkey") {
|
||||||
stdout(pp.PublicKey)
|
stdout(pp.PublicKey.Hex())
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
} else if prefix, value, err := nip19.Decode(input); err == nil && prefix == "naddr" {
|
out, _ := stdjson.MarshalIndent(pp, "", " ")
|
||||||
if ep, ok := value.(nostr.EntityPointer); ok {
|
stdout(string(out))
|
||||||
decodeResult = DecodeResult{EntityPointer: &ep}
|
|
||||||
} else {
|
|
||||||
ctx = lineProcessingError(ctx, "couldn't decode naddr: %s", err)
|
|
||||||
}
|
|
||||||
} else if prefix, value, err := nip19.Decode(input); err == nil && prefix == "nsec" {
|
|
||||||
decodeResult.PrivateKey.PrivateKey = value.(string)
|
|
||||||
decodeResult.PrivateKey.PublicKey, _ = nostr.GetPublicKey(value.(string))
|
|
||||||
} else {
|
|
||||||
ctx = lineProcessingError(ctx, "couldn't decode input '%s': %s", input, err)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.Bool("pubkey") || c.Bool("id") {
|
ctx = lineProcessingError(ctx, "couldn't decode input '%s'", input)
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
stdout(decodeResult.JSON())
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|||||||
137
dvm.go
137
dvm.go
@@ -1,137 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/fatih/color"
|
|
||||||
"github.com/nbd-wtf/go-nostr"
|
|
||||||
"github.com/nbd-wtf/go-nostr/nip90"
|
|
||||||
"github.com/urfave/cli/v3"
|
|
||||||
)
|
|
||||||
|
|
||||||
var dvm = &cli.Command{
|
|
||||||
Name: "dvm",
|
|
||||||
Usage: "deal with nip90 data-vending-machine things (experimental)",
|
|
||||||
DisableSliceFlagSeparator: true,
|
|
||||||
Flags: append(defaultKeyFlags,
|
|
||||||
&cli.StringSliceFlag{
|
|
||||||
Name: "relay",
|
|
||||||
Aliases: []string{"r"},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Commands: append([]*cli.Command{
|
|
||||||
{
|
|
||||||
Name: "list",
|
|
||||||
Usage: "find DVMs that have announced themselves for a specific kind",
|
|
||||||
DisableSliceFlagSeparator: true,
|
|
||||||
Action: func(ctx context.Context, c *cli.Command) error {
|
|
||||||
return fmt.Errorf("we don't know how to do this yet")
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}, (func() []*cli.Command {
|
|
||||||
commands := make([]*cli.Command, len(nip90.Jobs))
|
|
||||||
for i, job := range nip90.Jobs {
|
|
||||||
flags := make([]cli.Flag, 0, 2+len(job.Params))
|
|
||||||
|
|
||||||
if job.InputType != "" {
|
|
||||||
flags = append(flags, &cli.StringSliceFlag{
|
|
||||||
Name: "input",
|
|
||||||
Aliases: []string{"i"},
|
|
||||||
Category: "INPUT",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, param := range job.Params {
|
|
||||||
flags = append(flags, &cli.StringSliceFlag{
|
|
||||||
Name: param,
|
|
||||||
Category: "PARAMETER",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
commands[i] = &cli.Command{
|
|
||||||
Name: strconv.Itoa(job.InputKind),
|
|
||||||
Usage: job.Name,
|
|
||||||
Description: job.Description,
|
|
||||||
DisableSliceFlagSeparator: true,
|
|
||||||
Flags: flags,
|
|
||||||
Action: func(ctx context.Context, c *cli.Command) error {
|
|
||||||
relayUrls := c.StringSlice("relay")
|
|
||||||
relays := connectToAllRelays(ctx, relayUrls, false)
|
|
||||||
if len(relays) == 0 {
|
|
||||||
log("failed to connect to any of the given relays.\n")
|
|
||||||
os.Exit(3)
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
for _, relay := range relays {
|
|
||||||
relay.Close()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
evt := nostr.Event{
|
|
||||||
Kind: job.InputKind,
|
|
||||||
Tags: make(nostr.Tags, 0, 2+len(job.Params)),
|
|
||||||
CreatedAt: nostr.Now(),
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, input := range c.StringSlice("input") {
|
|
||||||
evt.Tags = append(evt.Tags, nostr.Tag{"i", input, job.InputType})
|
|
||||||
}
|
|
||||||
for _, paramN := range job.Params {
|
|
||||||
for _, paramV := range c.StringSlice(paramN) {
|
|
||||||
tag := nostr.Tag{"param", paramN, "", ""}[0:2]
|
|
||||||
for _, v := range strings.Split(paramV, ";") {
|
|
||||||
tag = append(tag, v)
|
|
||||||
}
|
|
||||||
evt.Tags = append(evt.Tags, tag)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
kr, _, err := gatherKeyerFromArguments(ctx, c)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := kr.SignEvent(ctx, &evt); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
logverbose("%s", evt)
|
|
||||||
|
|
||||||
log("- publishing job request... ")
|
|
||||||
first := true
|
|
||||||
for res := range sys.Pool.PublishMany(ctx, relayUrls, evt) {
|
|
||||||
cleanUrl, ok := strings.CutPrefix(res.RelayURL, "wss://")
|
|
||||||
if !ok {
|
|
||||||
cleanUrl = res.RelayURL
|
|
||||||
}
|
|
||||||
|
|
||||||
if !first {
|
|
||||||
log(", ")
|
|
||||||
}
|
|
||||||
first = false
|
|
||||||
|
|
||||||
if res.Error != nil {
|
|
||||||
log("%s: %s", color.RedString(cleanUrl), res.Error)
|
|
||||||
} else {
|
|
||||||
log("%s: ok", color.GreenString(cleanUrl))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log("\n- waiting for response...\n")
|
|
||||||
for ie := range sys.Pool.SubscribeMany(ctx, relayUrls, nostr.Filter{
|
|
||||||
Kinds: []int{7000, job.OutputKind},
|
|
||||||
Tags: nostr.TagMap{"e": []string{evt.ID}},
|
|
||||||
}) {
|
|
||||||
stdout(ie.Event)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return commands
|
|
||||||
})()...),
|
|
||||||
}
|
|
||||||
145
encode.go
145
encode.go
@@ -4,8 +4,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/nbd-wtf/go-nostr"
|
"fiatjaf.com/nostr"
|
||||||
"github.com/nbd-wtf/go-nostr/nip19"
|
"fiatjaf.com/nostr/nip19"
|
||||||
"github.com/urfave/cli/v3"
|
"github.com/urfave/cli/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -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) (context.Context, error) {
|
echo '{"pubkey":"7b225d32d3edb978dba1adfd9440105646babbabbda181ea383f74ba53c3be19","relays":["wss://nada.zero"]}' | nak encode
|
||||||
if c.Args().Len() < 1 {
|
echo '{
|
||||||
return ctx, fmt.Errorf("expected more than 1 argument.")
|
"id":"7b225d32d3edb978dba1adfd9440105646babbabbda181ea383f74ba53c3be19"
|
||||||
}
|
"relays":["wss://nada.zero"],
|
||||||
return ctx, 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)
|
||||||
@@ -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,19 +204,11 @@ 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 {
|
||||||
@@ -205,11 +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)
|
exitIfLineProcessingError(ctx)
|
||||||
|
|||||||
@@ -4,9 +4,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"fiatjaf.com/nostr/nip04"
|
||||||
"github.com/urfave/cli/v3"
|
"github.com/urfave/cli/v3"
|
||||||
"github.com/nbd-wtf/go-nostr"
|
|
||||||
"github.com/nbd-wtf/go-nostr/nip04"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var encrypt = &cli.Command{
|
var encrypt = &cli.Command{
|
||||||
@@ -16,7 +15,7 @@ var encrypt = &cli.Command{
|
|||||||
DisableSliceFlagSeparator: true,
|
DisableSliceFlagSeparator: true,
|
||||||
Flags: append(
|
Flags: append(
|
||||||
defaultKeyFlags,
|
defaultKeyFlags,
|
||||||
&cli.StringFlag{
|
&PubKeyFlag{
|
||||||
Name: "recipient-pubkey",
|
Name: "recipient-pubkey",
|
||||||
Aliases: []string{"p", "tgt", "target", "pubkey"},
|
Aliases: []string{"p", "tgt", "target", "pubkey"},
|
||||||
Required: true,
|
Required: true,
|
||||||
@@ -27,10 +26,7 @@ var encrypt = &cli.Command{
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
Action: func(ctx context.Context, c *cli.Command) error {
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
target := c.String("recipient-pubkey")
|
target := getPubKey(c, "recipient-pubkey")
|
||||||
if !nostr.IsValidPublicKey(target) {
|
|
||||||
return fmt.Errorf("target %s is not a valid public key", target)
|
|
||||||
}
|
|
||||||
|
|
||||||
plaintext := c.Args().First()
|
plaintext := c.Args().First()
|
||||||
|
|
||||||
@@ -81,7 +77,7 @@ var decrypt = &cli.Command{
|
|||||||
DisableSliceFlagSeparator: true,
|
DisableSliceFlagSeparator: true,
|
||||||
Flags: append(
|
Flags: append(
|
||||||
defaultKeyFlags,
|
defaultKeyFlags,
|
||||||
&cli.StringFlag{
|
&PubKeyFlag{
|
||||||
Name: "sender-pubkey",
|
Name: "sender-pubkey",
|
||||||
Aliases: []string{"p", "src", "source", "pubkey"},
|
Aliases: []string{"p", "src", "source", "pubkey"},
|
||||||
Required: true,
|
Required: true,
|
||||||
@@ -92,10 +88,7 @@ var decrypt = &cli.Command{
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
Action: func(ctx context.Context, c *cli.Command) error {
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
source := c.String("sender-pubkey")
|
source := getPubKey(c, "sender-pubkey")
|
||||||
if !nostr.IsValidPublicKey(source) {
|
|
||||||
return fmt.Errorf("source %s is not a valid public key", source)
|
|
||||||
}
|
|
||||||
|
|
||||||
ciphertext := c.Args().First()
|
ciphertext := c.Args().First()
|
||||||
|
|
||||||
|
|||||||
210
event.go
210
event.go
@@ -8,10 +8,11 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"fiatjaf.com/nostr"
|
||||||
|
"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/nbd-wtf/go-nostr/nip13"
|
|
||||||
"github.com/nbd-wtf/go-nostr/nip19"
|
|
||||||
"github.com/urfave/cli/v3"
|
"github.com/urfave/cli/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -128,31 +129,35 @@ 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
defer func() {
|
|
||||||
for _, relay := range relays {
|
|
||||||
relay.Close()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
kr, sec, err := gatherKeyerFromArguments(ctx, c)
|
kr, sec, err := gatherKeyerFromArguments(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
|
// will reuse this
|
||||||
@@ -170,7 +175,7 @@ example:
|
|||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
@@ -209,13 +214,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 {
|
||||||
@@ -258,7 +269,7 @@ example:
|
|||||||
mustRehashAndResign = true
|
mustRehashAndResign = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if evt.Sig == "" || mustRehashAndResign {
|
if evt.Sig == [64]byte{} || mustRehashAndResign {
|
||||||
if numSigners := c.Uint("musig"); numSigners > 1 {
|
if numSigners := c.Uint("musig"); numSigners > 1 {
|
||||||
// must do musig
|
// must do musig
|
||||||
pubkeys := c.StringSlice("musig-pubkey")
|
pubkeys := c.StringSlice("musig-pubkey")
|
||||||
@@ -291,48 +302,7 @@ 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)
|
|
||||||
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)
|
|
||||||
log("performing auth as %s... ", pk)
|
|
||||||
if err := relay.Auth(ctx, func(authEvent *nostr.Event) error {
|
|
||||||
return kr.SignEvent(ctx, authEvent)
|
|
||||||
}); 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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for stdinEvent := range getJsonsOrBlank() {
|
for stdinEvent := range getJsonsOrBlank() {
|
||||||
@@ -345,3 +315,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
|
||||||
|
}
|
||||||
|
|||||||
138
example_test.go
138
example_test.go
@@ -1,138 +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 ExampleDecodePubkey() {
|
|
||||||
app.Run(ctx, []string{"nak", "decode", "-p", "npub10xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqpkge6d", "npub1ccz8l9zpa47k6vz9gphftsrumpw80rjt3nhnefat4symjhrsnmjs38mnyd"})
|
|
||||||
// Output:
|
|
||||||
// 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798
|
|
||||||
// c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExampleDecodeEventId() {
|
|
||||||
app.Run(ctx, []string{"nak", "decode", "-e", "nevent1qyd8wumn8ghj7urewfsk66ty9enxjct5dfskvtnrdakj7qgmwaehxw309aex2mrp0yh8wetnw3jhymnzw33jucm0d5hszxthwden5te0wfjkccte9eekummjwsh8xmmrd9skctcpzamhxue69uhkzarvv9ejumn0wd68ytnvv9hxgtcqyqllp5v5j0nxr74fptqxkhvfv0h3uj870qpk3ln8a58agyxl3fka296ewr8", "nevent1qqswh48lurxs8u0pll9qj2rzctvjncwhstpzlstq59rdtzlty79awns5hl5uf"})
|
|
||||||
// Output:
|
|
||||||
// 3ff0d19493e661faa90ac06b5d8963ef1e48fe780368fe67ed0fd410df8a6dd5
|
|
||||||
// ebd4ffe0cd03f1e1ffca092862c2d929e1d782c22fc160a146d58beb278bd74e
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExampleReq() {
|
|
||||||
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", "May 19 2018 03:37:19", "-c", "nn"})
|
|
||||||
// Output:
|
|
||||||
// {"kind":0,"id":"b10da0095f96aa2accd99fa3d93bf29a76f51d2594cf5a0a52f8e961aecd0b67","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1526711839,"tags":[["plu","pla"],["e","3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24"]],"content":"nn","sig":"988442c97064a041ba5e2bfbd64e84d3f819b2169e865511d9d53e74667949ff165325942acaa2ca233c8b529adedf12cf44088cf04081b56d098c5f4d52dd8f"}
|
|
||||||
}
|
|
||||||
29
fetch.go
29
fetch.go
@@ -4,11 +4,11 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"fiatjaf.com/nostr"
|
||||||
|
"fiatjaf.com/nostr/nip05"
|
||||||
|
"fiatjaf.com/nostr/nip19"
|
||||||
|
"fiatjaf.com/nostr/sdk/hints"
|
||||||
"github.com/urfave/cli/v3"
|
"github.com/urfave/cli/v3"
|
||||||
"github.com/nbd-wtf/go-nostr"
|
|
||||||
"github.com/nbd-wtf/go-nostr/nip05"
|
|
||||||
"github.com/nbd-wtf/go-nostr/nip19"
|
|
||||||
"github.com/nbd-wtf/go-nostr/sdk/hints"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var fetch = &cli.Command{
|
var fetch = &cli.Command{
|
||||||
@@ -27,16 +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 {
|
||||||
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) {
|
||||||
@@ -63,15 +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":
|
case "note":
|
||||||
filter.IDs = append(filter.IDs, value.(string))
|
filter.IDs = append(filter.IDs, value.([32]byte))
|
||||||
case "naddr":
|
case "naddr":
|
||||||
v := value.(nostr.EntityPointer)
|
v := value.(nostr.EntityPointer)
|
||||||
filter.Kinds = []int{v.Kind}
|
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
|
||||||
@@ -82,7 +75,7 @@ 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:
|
default:
|
||||||
@@ -90,7 +83,7 @@ var fetch = &cli.Command{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if authorHint != "" {
|
if authorHint != nostr.ZeroPK {
|
||||||
for _, url := range relays {
|
for _, url := range relays {
|
||||||
sys.Hints.Save(authorHint, nostr.NormalizeURL(url), hints.LastInHint, nostr.Now())
|
sys.Hints.Save(authorHint, nostr.NormalizeURL(url), hints.LastInHint, nostr.Now())
|
||||||
}
|
}
|
||||||
@@ -113,7 +106,7 @@ var fetch = &cli.Command{
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
for ie := range sys.Pool.FetchMany(ctx, relays, filter) {
|
for ie := range sys.Pool.FetchMany(ctx, relays, filter, nostr.SubscriptionOptions{}) {
|
||||||
stdout(ie.Event)
|
stdout(ie.Event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
136
flags.go
136
flags.go
@@ -6,14 +6,13 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/urfave/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)
|
||||||
|
}
|
||||||
|
|||||||
16
fs.go
16
fs.go
@@ -1,3 +1,5 @@
|
|||||||
|
//go:build !windows
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -8,12 +10,12 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"fiatjaf.com/nostr"
|
||||||
|
"fiatjaf.com/nostr/keyer"
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
"github.com/fiatjaf/nak/nostrfs"
|
"github.com/fiatjaf/nak/nostrfs"
|
||||||
"github.com/hanwen/go-fuse/v2/fs"
|
"github.com/hanwen/go-fuse/v2/fs"
|
||||||
"github.com/hanwen/go-fuse/v2/fuse"
|
"github.com/hanwen/go-fuse/v2/fuse"
|
||||||
"github.com/nbd-wtf/go-nostr"
|
|
||||||
"github.com/nbd-wtf/go-nostr/keyer"
|
|
||||||
"github.com/urfave/cli/v3"
|
"github.com/urfave/cli/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -23,15 +25,9 @@ var fsCmd = &cli.Command{
|
|||||||
Description: `(experimental)`,
|
Description: `(experimental)`,
|
||||||
ArgsUsage: "<mountpoint>",
|
ArgsUsage: "<mountpoint>",
|
||||||
Flags: append(defaultKeyFlags,
|
Flags: append(defaultKeyFlags,
|
||||||
&cli.StringFlag{
|
&PubKeyFlag{
|
||||||
Name: "pubkey",
|
Name: "pubkey",
|
||||||
Usage: "public key from where to to prepopulate directories",
|
Usage: "public key from where to to prepopulate directories",
|
||||||
Validator: func(pk string) error {
|
|
||||||
if nostr.IsValidPublicKey(pk) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return fmt.Errorf("invalid public key '%s'", pk)
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
&cli.DurationFlag{
|
&cli.DurationFlag{
|
||||||
Name: "auto-publish-notes",
|
Name: "auto-publish-notes",
|
||||||
@@ -56,7 +52,7 @@ var fsCmd = &cli.Command{
|
|||||||
if signer, _, err := gatherKeyerFromArguments(ctx, c); err == nil {
|
if signer, _, err := gatherKeyerFromArguments(ctx, c); err == nil {
|
||||||
kr = signer
|
kr = signer
|
||||||
} else {
|
} else {
|
||||||
kr = keyer.NewReadOnlyUser(c.String("pubkey"))
|
kr = keyer.NewReadOnlyUser(getPubKey(c, "pubkey"))
|
||||||
}
|
}
|
||||||
|
|
||||||
apnt := c.Duration("auto-publish-notes")
|
apnt := c.Duration("auto-publish-notes")
|
||||||
|
|||||||
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.")
|
||||||
|
},
|
||||||
|
}
|
||||||
29
go.mod
29
go.mod
@@ -4,46 +4,52 @@ go 1.24.1
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
fiatjaf.com/lib v0.3.1
|
fiatjaf.com/lib v0.3.1
|
||||||
|
fiatjaf.com/nostr v0.0.0-20250521022139-d3fb25441ab3
|
||||||
github.com/bep/debounce v1.2.1
|
github.com/bep/debounce v1.2.1
|
||||||
github.com/btcsuite/btcd/btcec/v2 v2.3.4
|
github.com/btcsuite/btcd/btcec/v2 v2.3.4
|
||||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e
|
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.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/eventstore v0.16.2
|
|
||||||
github.com/fiatjaf/khatru v0.17.3-0.20250312035319-596bca93c3ff
|
|
||||||
github.com/hanwen/go-fuse/v2 v2.7.2
|
github.com/hanwen/go-fuse/v2 v2.7.2
|
||||||
github.com/json-iterator/go v1.1.12
|
github.com/json-iterator/go v1.1.12
|
||||||
github.com/liamg/magic v0.0.1
|
github.com/liamg/magic v0.0.1
|
||||||
github.com/mailru/easyjson v0.9.0
|
github.com/mailru/easyjson v0.9.0
|
||||||
github.com/mark3labs/mcp-go v0.8.3
|
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.51.5
|
github.com/mattn/go-tty v0.0.7
|
||||||
|
github.com/stretchr/testify v1.10.0
|
||||||
github.com/urfave/cli/v3 v3.0.0-beta1
|
github.com/urfave/cli/v3 v3.0.0-beta1
|
||||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394
|
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6
|
||||||
|
golang.org/x/term v0.30.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/FastFilter/xorfilter v0.2.1 // indirect
|
||||||
github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3 // indirect
|
github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3 // indirect
|
||||||
github.com/andybalholm/brotli v1.1.1 // indirect
|
github.com/andybalholm/brotli v1.1.1 // indirect
|
||||||
github.com/btcsuite/btcd v0.24.2 // indirect
|
github.com/btcsuite/btcd v0.24.2 // indirect
|
||||||
github.com/btcsuite/btcd/btcutil v1.1.5 // 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/bytedance/sonic v1.13.1 // indirect
|
github.com/bytedance/sonic v1.13.2 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.2.4 // indirect
|
github.com/bytedance/sonic/loader v0.2.4 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/chzyer/logex v1.1.10 // indirect
|
github.com/chzyer/logex v1.1.10 // indirect
|
||||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect
|
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.5 // indirect
|
github.com/cloudwego/base64x v0.1.5 // indirect
|
||||||
github.com/coder/websocket v1.8.12 // 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/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 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/elnosh/gonuts v0.3.1-0.20250123162555-7c0381a585e3 // indirect
|
github.com/elnosh/gonuts v0.3.1-0.20250123162555-7c0381a585e3 // indirect
|
||||||
github.com/fasthttp/websocket v1.5.12 // indirect
|
github.com/fasthttp/websocket v1.5.12 // indirect
|
||||||
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
|
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
|
||||||
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||||
|
github.com/google/flatbuffers v24.12.23+incompatible // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/graph-gophers/dataloader/v7 v7.1.0 // indirect
|
|
||||||
github.com/hablullah/go-hijri v1.0.2 // indirect
|
github.com/hablullah/go-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
|
||||||
@@ -57,6 +63,7 @@ require (
|
|||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // 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/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
|
github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
|
||||||
github.com/rs/cors v1.11.1 // indirect
|
github.com/rs/cors v1.11.1 // indirect
|
||||||
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 // indirect
|
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 // indirect
|
||||||
@@ -69,9 +76,13 @@ require (
|
|||||||
github.com/valyala/fasthttp v1.59.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
|
||||||
github.com/x448/float16 v0.8.4 // indirect
|
github.com/x448/float16 v0.8.4 // indirect
|
||||||
golang.org/x/arch v0.15.0 // indirect
|
go.opencensus.io v0.24.0 // indirect
|
||||||
|
golang.org/x/arch v0.17.0 // indirect
|
||||||
golang.org/x/crypto v0.36.0 // indirect
|
golang.org/x/crypto v0.36.0 // indirect
|
||||||
golang.org/x/net v0.37.0 // indirect
|
golang.org/x/net v0.37.0 // indirect
|
||||||
golang.org/x/sys v0.31.0 // indirect
|
golang.org/x/sync v0.14.0 // indirect
|
||||||
|
golang.org/x/sys v0.33.0 // indirect
|
||||||
golang.org/x/text v0.23.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
|
||||||
)
|
)
|
||||||
|
|||||||
108
go.sum
108
go.sum
@@ -1,7 +1,15 @@
|
|||||||
|
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 h1:/oFQwNtFRfV+ukmOCxfBEAuayoLwXp4wu2/fz5iHpwA=
|
||||||
fiatjaf.com/lib v0.3.1/go.mod h1:Ycqq3+mJ9jAWu7XjbQI1cVr+OFgnHn79dQR5oTII47g=
|
fiatjaf.com/lib v0.3.1/go.mod h1:Ycqq3+mJ9jAWu7XjbQI1cVr+OFgnHn79dQR5oTII47g=
|
||||||
|
fiatjaf.com/nostr v0.0.0-20250521022139-d3fb25441ab3 h1:JRtme8g4UQ5KYlxI31wBa8YMWmAxvxdwtNn+PiI/XCs=
|
||||||
|
fiatjaf.com/nostr v0.0.0-20250521022139-d3fb25441ab3/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 h1:ClzzXMDDuUbWfNNZqGeYq4PnYOlwlOVIvSyNaIy0ykg=
|
||||||
github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3/go.mod h1:we0YA5CsBbH5+/NUzC/AlMmxaDtWlXeNsqrwXjTzmzA=
|
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.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
||||||
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
||||||
@@ -33,11 +41,14 @@ 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/bytedance/sonic v1.13.1 h1:Jyd5CIvdFnkOWuKXr+wm4Nyk2h0yAFsr8ucJgEasO3g=
|
github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=
|
||||||
github.com/bytedance/sonic v1.13.1/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
|
github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
|
||||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
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 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
|
||||||
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
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 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
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=
|
||||||
@@ -46,11 +57,13 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5O
|
|||||||
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 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
|
||||||
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
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/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||||
github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
|
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||||
github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
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=
|
||||||
@@ -62,8 +75,12 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeC
|
|||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
|
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/dgraph-io/badger/v4 v4.5.0 h1:TeJE3I1pIWLBjYhIYCA1+uxrjWEoJXImFBMEBVSm16g=
|
||||||
|
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 h1:SYG07bONKMlFDUYu5pEu3DGAh8c2OFNzKm6G9J4Si84=
|
||||||
github.com/dgraph-io/ristretto v1.0.0/go.mod h1:jTi2FiYEhQ1NsMmA7DeBykizjOuY88NhKBkepyu1jPc=
|
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 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y=
|
||||||
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
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=
|
||||||
@@ -73,37 +90,49 @@ github.com/elliotchance/pie/v2 v2.7.0 h1:FqoIKg4uj0G/CrLGuMS9ejnFKa92lxE1dEgBD3p
|
|||||||
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/elnosh/gonuts v0.3.1-0.20250123162555-7c0381a585e3 h1:k7evIqJ2BtFn191DgY/b03N2bMYA/iQwzr4f/uHYn20=
|
github.com/elnosh/gonuts v0.3.1-0.20250123162555-7c0381a585e3 h1:k7evIqJ2BtFn191DgY/b03N2bMYA/iQwzr4f/uHYn20=
|
||||||
github.com/elnosh/gonuts v0.3.1-0.20250123162555-7c0381a585e3/go.mod h1:vgZomh4YQk7R3w4ltZc0sHwCmndfHkuX6V4sga/8oNs=
|
github.com/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 h1:e4RGPpWW2HTbL3zV0Y/t7g0ub294LkiuXXUuTOUInlE=
|
||||||
github.com/fasthttp/websocket v1.5.12/go.mod h1:I+liyL7/4moHojiOgUOIKEWm9EIxHqxZChS+aMFltyg=
|
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/eventstore v0.16.2 h1:h4rHwSwPcqAKqWUsAbYWUhDeSgm2Kp+PBkJc3FgBYu4=
|
|
||||||
github.com/fiatjaf/eventstore v0.16.2/go.mod h1:0gU8fzYO/bG+NQAVlHtJWOlt3JKKFefh5Xjj2d1dLIs=
|
|
||||||
github.com/fiatjaf/khatru v0.17.3-0.20250312035319-596bca93c3ff h1:b6LYwWlc8zAW6aoZpXYC3Gx/zkP4XW5amDx0VwyeREs=
|
|
||||||
github.com/fiatjaf/khatru v0.17.3-0.20250312035319-596bca93c3ff/go.mod h1:dAaXV6QZwuMVYlXQigp/0Uyl/m1nKOhtRssjQYsgMu0=
|
|
||||||
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/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
|
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
|
||||||
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
|
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
|
||||||
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||||
|
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
||||||
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
||||||
|
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
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/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
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 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
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/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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
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/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/graph-gophers/dataloader/v7 v7.1.0 h1:Wn8HGF/q7MNXcvfaBnLEPEFJttVHR8zuEqP1obys/oc=
|
|
||||||
github.com/graph-gophers/dataloader/v7 v7.1.0/go.mod h1:1bKE0Dm6OUcTB/OAuYVOZctgIz7Q3d0XrYtlIzTgg6Q=
|
|
||||||
github.com/hablullah/go-hijri v1.0.2 h1:drT/MZpSZJQXo7jftf5fthArShcaMtsal0Zf/dnmp6k=
|
github.com/hablullah/go-hijri v1.0.2 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=
|
||||||
@@ -144,6 +173,8 @@ 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/mattn/go-tty v0.0.7 h1:KJ486B6qI8+wBO7kQxYgmmEFDaFEE96JMBQ7h400N8Q=
|
||||||
|
github.com/mattn/go-tty v0.0.7/go.mod h1:f2i5ZOvXBU/tCABmLmOfzLz9azMo5wdAaElRNnJKr+k=
|
||||||
github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78=
|
github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78=
|
||||||
github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI=
|
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-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
@@ -151,8 +182,6 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
|
|||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
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 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
github.com/nbd-wtf/go-nostr v0.51.5 h1:kztpm/JuavVefyuEjG0QaCgDtzHIW9K/Hzq+y9Ph2DY=
|
|
||||||
github.com/nbd-wtf/go-nostr v0.51.5/go.mod h1:raIUNOilCdhiVIqgwe+9enCtdXu1iuPjbLh1hO7wTqI=
|
|
||||||
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=
|
||||||
@@ -166,6 +195,7 @@ 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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
|
github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
|
||||||
github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||||
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
|
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
|
||||||
@@ -209,23 +239,40 @@ 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/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 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||||
golang.org/x/arch v0.15.0 h1:QtOrQd0bTUnhNVNndMpLHNWrDmYzZ2KDqSrEymqInZw=
|
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||||
golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
|
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||||
|
golang.org/x/arch v0.17.0 h1:4O3dfLzd+lQewptAHqjewQZQDyEdejz3VwgeYwkZneU=
|
||||||
|
golang.org/x/arch v0.17.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.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||||
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
|
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI=
|
||||||
|
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
|
||||||
|
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.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
|
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/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.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
|
||||||
|
golang.org/x/sync v0.14.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=
|
||||||
@@ -235,24 +282,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.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
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.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
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=
|
||||||
@@ -264,4 +334,6 @@ 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 h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
|
||||||
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
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=
|
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||||
|
|||||||
369
helpers.go
369
helpers.go
@@ -3,6 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"iter"
|
"iter"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
@@ -10,24 +11,26 @@ import (
|
|||||||
"net/textproto"
|
"net/textproto"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"runtime"
|
||||||
"slices"
|
"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/fatih/color"
|
"github.com/fatih/color"
|
||||||
jsoniter "github.com/json-iterator/go"
|
jsoniter "github.com/json-iterator/go"
|
||||||
"github.com/nbd-wtf/go-nostr"
|
"github.com/mattn/go-tty"
|
||||||
"github.com/nbd-wtf/go-nostr/sdk"
|
|
||||||
"github.com/urfave/cli/v3"
|
"github.com/urfave/cli/v3"
|
||||||
|
"golang.org/x/term"
|
||||||
)
|
)
|
||||||
|
|
||||||
var sys *sdk.System
|
var sys *sdk.System
|
||||||
|
|
||||||
var (
|
|
||||||
hintsFilePath string
|
|
||||||
hintsFileExists bool
|
|
||||||
)
|
|
||||||
|
|
||||||
var json = jsoniter.ConfigFastest
|
var json = jsoniter.ConfigFastest
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -37,7 +40,7 @@ const (
|
|||||||
var (
|
var (
|
||||||
log = func(msg string, args ...any) { 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
|
logverbose = func(msg string, args ...any) {} // by default do nothing
|
||||||
stdout = fmt.Println
|
stdout = func(args ...any) { fmt.Fprintln(color.Output, args...) }
|
||||||
)
|
)
|
||||||
|
|
||||||
func isPiped() bool {
|
func isPiped() bool {
|
||||||
@@ -48,6 +51,7 @@ func isPiped() bool {
|
|||||||
func getJsonsOrBlank() iter.Seq[string] {
|
func getJsonsOrBlank() iter.Seq[string] {
|
||||||
var curr strings.Builder
|
var curr strings.Builder
|
||||||
|
|
||||||
|
var finalJsonErr error
|
||||||
return func(yield func(string) bool) {
|
return func(yield func(string) bool) {
|
||||||
hasStdin := writeStdinLinesOrNothing(func(stdinLine string) bool {
|
hasStdin := writeStdinLinesOrNothing(func(stdinLine string) bool {
|
||||||
// we're look for an event, but it may be in multiple lines, so if json parsing fails
|
// we're look for an event, but it may be in multiple lines, so if json parsing fails
|
||||||
@@ -57,8 +61,10 @@ func getJsonsOrBlank() iter.Seq[string] {
|
|||||||
|
|
||||||
var dummy any
|
var dummy any
|
||||||
if err := json.Unmarshal([]byte(stdinEvent), &dummy); err != nil {
|
if err := json.Unmarshal([]byte(stdinEvent), &dummy); err != nil {
|
||||||
|
finalJsonErr = err
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
finalJsonErr = nil
|
||||||
|
|
||||||
if !yield(stdinEvent) {
|
if !yield(stdinEvent) {
|
||||||
return false
|
return false
|
||||||
@@ -71,6 +77,10 @@ func getJsonsOrBlank() iter.Seq[string] {
|
|||||||
if !hasStdin {
|
if !hasStdin {
|
||||||
yield("{}")
|
yield("{}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if finalJsonErr != nil {
|
||||||
|
log(color.YellowString("stdin json parse error: %s", finalJsonErr))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,67 +158,197 @@ 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.Relay {
|
) []*nostr.Relay {
|
||||||
sys.Pool = nostr.NewSimplePool(context.Background(),
|
// first pass to check if these are valid relay URLs
|
||||||
append(opts,
|
|
||||||
nostr.WithEventMiddleware(sys.TrackEventHints),
|
|
||||||
nostr.WithPenaltyBox(),
|
|
||||||
nostr.WithRelayOptions(
|
|
||||||
nostr.WithRequestHeader(http.Header{textproto.CanonicalMIMEHeaderKey("user-agent"): {"nak/s"}}),
|
|
||||||
),
|
|
||||||
)...,
|
|
||||||
)
|
|
||||||
|
|
||||||
relays := make([]*nostr.Relay, 0, len(relayUrls))
|
|
||||||
relayLoop:
|
|
||||||
for _, url := range relayUrls {
|
for _, url := range relayUrls {
|
||||||
log("connecting to %s... ", url)
|
if !nostr.IsValidRelayURL(nostr.NormalizeURL(url)) {
|
||||||
if relay, err := sys.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(ctx, nostr.RelayEvent{Event: authEvent, Relay: relay})
|
|
||||||
}); 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")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
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 {
|
||||||
log(msg+"\n", args...)
|
log(msg+"\n", args...)
|
||||||
return context.WithValue(ctx, LINE_PROCESSING_ERROR, true)
|
return context.WithValue(ctx, LINE_PROCESSING_ERROR, true)
|
||||||
@@ -230,20 +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()
|
||||||
|
|
||||||
|
fmt.Fprintf(os.Stderr, 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 {
|
var colors = struct {
|
||||||
reset func(...any) (int, error)
|
reset func(...any) (int, error)
|
||||||
italic func(...any) string
|
italic func(...any) string
|
||||||
italicf func(string, ...any) string
|
italicf func(string, ...any) string
|
||||||
bold func(...any) string
|
bold func(...any) string
|
||||||
boldf func(string, ...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.Reset).Print,
|
||||||
color.New(color.Italic).Sprint,
|
color.New(color.Italic).Sprint,
|
||||||
color.New(color.Italic).Sprintf,
|
color.New(color.Italic).Sprintf,
|
||||||
color.New(color.Bold).Sprint,
|
color.New(color.Bold).Sprint,
|
||||||
color.New(color.Bold).Sprintf,
|
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,
|
||||||
}
|
}
|
||||||
|
|||||||
154
helpers_key.go
154
helpers_key.go
@@ -2,28 +2,30 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"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/chzyer/readline"
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
|
"github.com/mattn/go-tty"
|
||||||
"github.com/urfave/cli/v3"
|
"github.com/urfave/cli/v3"
|
||||||
"github.com/nbd-wtf/go-nostr"
|
|
||||||
"github.com/nbd-wtf/go-nostr/keyer"
|
|
||||||
"github.com/nbd-wtf/go-nostr/nip19"
|
|
||||||
"github.com/nbd-wtf/go-nostr/nip46"
|
|
||||||
"github.com/nbd-wtf/go-nostr/nip49"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var defaultKeyFlags = []cli.Flag{
|
var defaultKeyFlags = []cli.Flag{
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: "sec",
|
Name: "sec",
|
||||||
Usage: "secret key to sign the event, as nsec, ncryptsec or hex, or a bunker URL, it is more secure to use the environment variable NOSTR_SECRET_KEY than this flag",
|
Usage: "secret key to sign the event, as nsec, ncryptsec or hex, or a bunker URL",
|
||||||
DefaultText: "the key '1'",
|
DefaultText: "the key '1'",
|
||||||
Aliases: []string{"connect"},
|
|
||||||
Category: CATEGORY_SIGNER,
|
Category: CATEGORY_SIGNER,
|
||||||
|
Sources: cli.EnvVars("NOSTR_SECRET_KEY"),
|
||||||
|
Value: nostr.KeyOne.Hex(),
|
||||||
|
HideDefault: true,
|
||||||
},
|
},
|
||||||
&cli.BoolFlag{
|
&cli.BoolFlag{
|
||||||
Name: "prompt-sec",
|
Name: "prompt-sec",
|
||||||
@@ -39,81 +41,76 @@ var defaultKeyFlags = []cli.Flag{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func gatherKeyerFromArguments(ctx context.Context, c *cli.Command) (nostr.Keyer, string, error) {
|
func gatherKeyerFromArguments(ctx context.Context, c *cli.Command) (nostr.Keyer, nostr.SecretKey, error) {
|
||||||
key, bunker, err := gatherSecretKeyOrBunkerFromArguments(ctx, c)
|
key, bunker, err := gatherSecretKeyOrBunkerFromArguments(ctx, c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", err
|
return nil, nostr.SecretKey{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var kr nostr.Keyer
|
var kr nostr.Keyer
|
||||||
if bunker != nil {
|
if bunker != nil {
|
||||||
kr = keyer.NewBunkerSignerFromBunkerClient(bunker)
|
kr = keyer.NewBunkerSignerFromBunkerClient(bunker)
|
||||||
} else {
|
} else {
|
||||||
kr, err = keyer.NewPlainKeySigner(key)
|
kr = keyer.NewPlainKeySigner(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
return kr, key, err
|
return kr, key, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func gatherSecretKeyOrBunkerFromArguments(ctx context.Context, c *cli.Command) (string, *nip46.BunkerClient, error) {
|
func gatherSecretKeyOrBunkerFromArguments(ctx context.Context, c *cli.Command) (nostr.SecretKey, *nip46.BunkerClient, error) {
|
||||||
var err error
|
|
||||||
|
|
||||||
sec := c.String("sec")
|
sec := c.String("sec")
|
||||||
if strings.HasPrefix(sec, "bunker://") {
|
if strings.HasPrefix(sec, "bunker://") {
|
||||||
// it's a bunker
|
// it's a bunker
|
||||||
bunkerURL := sec
|
bunkerURL := sec
|
||||||
clientKey := c.String("connect-as")
|
clientKeyHex := c.String("connect-as")
|
||||||
if clientKey != "" {
|
var clientKey nostr.SecretKey
|
||||||
clientKey = strings.Repeat("0", 64-len(clientKey)) + clientKey
|
|
||||||
|
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 {
|
} else {
|
||||||
clientKey = nostr.GeneratePrivateKey()
|
clientKey = nostr.Generate()
|
||||||
}
|
}
|
||||||
|
|
||||||
bunker, err := nip46.ConnectBunker(ctx, clientKey, bunkerURL, nil, func(s string) {
|
bunker, err := nip46.ConnectBunker(ctx, clientKey, bunkerURL, nil, func(s string) {
|
||||||
log(color.CyanString("[nip46]: open the following URL: %s"), s)
|
log(color.CyanString("[nip46]: open the following URL: %s"), s)
|
||||||
})
|
})
|
||||||
return "", bunker, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// take private from flags, environment variable or default to 1
|
return nostr.SecretKey{}, bunker, err
|
||||||
if sec == "" {
|
|
||||||
if key, ok := os.LookupEnv("NOSTR_SECRET_KEY"); ok {
|
|
||||||
sec = key
|
|
||||||
} else {
|
|
||||||
sec = "0000000000000000000000000000000000000000000000000000000000000001"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.Bool("prompt-sec") {
|
if c.Bool("prompt-sec") {
|
||||||
if isPiped() {
|
var err error
|
||||||
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)
|
sec, err = askPassword("type your secret key as ncryptsec, nsec or hex: ", nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil, fmt.Errorf("failed to get secret key: %w", err)
|
return nostr.SecretKey{}, nil, fmt.Errorf("failed to get secret key: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.HasPrefix(sec, "ncryptsec1") {
|
if strings.HasPrefix(sec, "ncryptsec1") {
|
||||||
sec, err = promptDecrypt(sec)
|
sk, err := promptDecrypt(sec)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil, fmt.Errorf("failed to decrypt: %w", err)
|
return nostr.SecretKey{}, nil, fmt.Errorf("failed to decrypt: %w", err)
|
||||||
}
|
}
|
||||||
} else if bsec, err := hex.DecodeString(leftPadKey(sec)); err == nil {
|
return sk, nil, 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 {
|
if prefix, ski, err := nip19.Decode(sec); err == nil && prefix == "nsec" {
|
||||||
return "", nil, fmt.Errorf("invalid secret key")
|
return ski.(nostr.SecretKey), nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return sec, 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) (string, error) {
|
func promptDecrypt(ncryptsec string) (nostr.SecretKey, error) {
|
||||||
for i := 1; i < 4; i++ {
|
for i := 1; i < 4; i++ {
|
||||||
var attemptStr string
|
var attemptStr string
|
||||||
if i > 1 {
|
if i > 1 {
|
||||||
@@ -121,7 +118,7 @@ func promptDecrypt(ncryptsec string) (string, error) {
|
|||||||
}
|
}
|
||||||
password, err := askPassword("type the password to decrypt your secret key"+attemptStr+": ", nil)
|
password, err := askPassword("type the password to decrypt your secret key"+attemptStr+": ", nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return nostr.SecretKey{}, err
|
||||||
}
|
}
|
||||||
sec, err := nip49.Decrypt(ncryptsec, password)
|
sec, err := nip49.Decrypt(ncryptsec, password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -129,33 +126,62 @@ func promptDecrypt(ncryptsec string) (string, error) {
|
|||||||
}
|
}
|
||||||
return sec, nil
|
return sec, nil
|
||||||
}
|
}
|
||||||
return "", fmt.Errorf("couldn't decrypt private key")
|
return nostr.SecretKey{}, fmt.Errorf("couldn't decrypt private key")
|
||||||
}
|
}
|
||||||
|
|
||||||
func askPassword(msg string, shouldAskAgain func(answer string) bool) (string, error) {
|
func askPassword(msg string, shouldAskAgain func(answer string) bool) (string, error) {
|
||||||
config := &readline.Config{
|
if isPiped() {
|
||||||
Stdout: color.Error,
|
// use TTY method when stdin is piped
|
||||||
Prompt: color.YellowString(msg),
|
tty, err := tty.Open()
|
||||||
InterruptPrompt: "^C",
|
if err != nil {
|
||||||
DisableAutoSaveHistory: true,
|
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)
|
||||||
EnableMask: true,
|
}
|
||||||
MaskRune: '*',
|
defer tty.Close()
|
||||||
}
|
for {
|
||||||
|
// print the prompt to stderr so it's visible to the user
|
||||||
|
fmt.Fprintf(os.Stderr, color.YellowString(msg))
|
||||||
|
|
||||||
rl, err := readline.NewEx(config)
|
// read password from TTY with masking
|
||||||
if err != nil {
|
password, err := tty.ReadPassword()
|
||||||
return "", err
|
if err != nil {
|
||||||
}
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
for {
|
// print newline after password input
|
||||||
answer, err := rl.Readline()
|
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 {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
answer = strings.TrimSpace(answer)
|
|
||||||
if shouldAskAgain != nil && shouldAskAgain(answer) {
|
for {
|
||||||
continue
|
answer, err := rl.Readline()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
answer = strings.TrimSpace(answer)
|
||||||
|
if shouldAskAgain != nil && shouldAskAgain(answer) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return answer, err
|
||||||
}
|
}
|
||||||
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
|
||||||
45
key.go
45
key.go
@@ -6,13 +6,13 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"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/urfave/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{
|
||||||
@@ -35,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
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -54,9 +54,8 @@ var public = &cli.Command{
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
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()) {
|
||||||
b, _ := hex.DecodeString(sec)
|
_, pk := btcec.PrivKeyFromBytes(sk[:])
|
||||||
_, pk := btcec.PrivKeyFromBytes(b)
|
|
||||||
|
|
||||||
if c.Bool("with-parity") {
|
if c.Bool("with-parity") {
|
||||||
stdout(hex.EncodeToString(pk.SerializeCompressed()))
|
stdout(hex.EncodeToString(pk.SerializeCompressed()))
|
||||||
@@ -123,30 +122,30 @@ var decryptKey = &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
|
||||||
}
|
}
|
||||||
@@ -264,27 +263,31 @@ However, if the intent is to check if two existing Nostr pubkeys match a given c
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func getSecretKeysFromStdinLinesOrSlice(ctx context.Context, _ *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)
|
||||||
}()
|
}()
|
||||||
|
|||||||
88
main.go
88
main.go
@@ -2,14 +2,14 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/textproto"
|
"net/textproto"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"github.com/nbd-wtf/go-nostr"
|
"fiatjaf.com/nostr"
|
||||||
"github.com/nbd-wtf/go-nostr/sdk"
|
"fiatjaf.com/nostr/sdk"
|
||||||
"github.com/nbd-wtf/go-nostr/sdk/hints/memoryh"
|
"github.com/fatih/color"
|
||||||
"github.com/urfave/cli/v3"
|
"github.com/urfave/cli/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -43,8 +43,8 @@ var app = &cli.Command{
|
|||||||
wallet,
|
wallet,
|
||||||
mcpServer,
|
mcpServer,
|
||||||
curl,
|
curl,
|
||||||
dvm,
|
|
||||||
fsCmd,
|
fsCmd,
|
||||||
|
publish,
|
||||||
},
|
},
|
||||||
Version: version,
|
Version: version,
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
@@ -61,7 +61,7 @@ var app = &cli.Command{
|
|||||||
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
|
||||||
@@ -82,72 +82,40 @@ var app = &cli.Command{
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Before: func(ctx context.Context, c *cli.Command) (context.Context, error) {
|
Before: func(ctx context.Context, c *cli.Command) (context.Context, error) {
|
||||||
configPath := c.String("config-path")
|
|
||||||
if configPath == "" {
|
|
||||||
if home, err := os.UserHomeDir(); err == nil {
|
|
||||||
configPath = filepath.Join(home, ".config/nak")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if configPath != "" {
|
|
||||||
hintsFilePath = filepath.Join(configPath, "outbox/hints.db")
|
|
||||||
}
|
|
||||||
if hintsFilePath != "" {
|
|
||||||
if _, err := os.Stat(hintsFilePath); !os.IsNotExist(err) {
|
|
||||||
hintsFileExists = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if hintsFilePath != "" {
|
|
||||||
if data, err := os.ReadFile(hintsFilePath); err == nil {
|
|
||||||
hintsdb := memoryh.NewHintDB()
|
|
||||||
if err := json.Unmarshal(data, &hintsdb); err == nil {
|
|
||||||
sys = sdk.NewSystem(
|
|
||||||
sdk.WithHintsDB(hintsdb),
|
|
||||||
)
|
|
||||||
goto systemOperational
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sys = sdk.NewSystem()
|
sys = sdk.NewSystem()
|
||||||
|
|
||||||
systemOperational:
|
if err := initializeOutboxHintsDB(c, sys); err != nil {
|
||||||
sys.Pool = nostr.NewSimplePool(context.Background(),
|
return ctx, fmt.Errorf("failed to initialized outbox hints: %w", err)
|
||||||
nostr.WithAuthorKindQueryMiddleware(sys.TrackQueryAttempts),
|
}
|
||||||
nostr.WithEventMiddleware(sys.TrackEventHints),
|
|
||||||
nostr.WithRelayOptions(
|
sys.Pool = nostr.NewPool(nostr.PoolOptions{
|
||||||
nostr.WithRequestHeader(http.Header{textproto.CanonicalMIMEHeaderKey("user-agent"): {"nak/b"}}),
|
AuthorKindQueryMiddleware: sys.TrackQueryAttempts,
|
||||||
),
|
EventMiddleware: sys.TrackEventHints,
|
||||||
)
|
RelayOptions: nostr.RelayOptions{
|
||||||
|
RequestHeader: http.Header{textproto.CanonicalMIMEHeaderKey("user-agent"): {"nak/b"}},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
return ctx, nil
|
return ctx, nil
|
||||||
},
|
},
|
||||||
After: func(ctx context.Context, c *cli.Command) error {
|
}
|
||||||
// save hints database on exit
|
|
||||||
if hintsFileExists {
|
|
||||||
data, err := json.Marshal(sys.Hints)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return os.WriteFile(hintsFilePath, data, 0644)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
func init() {
|
||||||
},
|
cli.VersionFlag = &cli.BoolFlag{
|
||||||
|
Name: "version",
|
||||||
|
Usage: "prints the version",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
defer colors.reset()
|
defer colors.reset()
|
||||||
|
|
||||||
cli.VersionFlag = &cli.BoolFlag{
|
|
||||||
Name: "version",
|
|
||||||
Usage: "prints the version",
|
|
||||||
}
|
|
||||||
|
|
||||||
// a megahack to enable this curl command proxy
|
// a megahack to enable this curl command proxy
|
||||||
if len(os.Args) > 2 && os.Args[1] == "curl" {
|
if len(os.Args) > 2 && os.Args[1] == "curl" {
|
||||||
if err := realCurl(); err != nil {
|
if err := realCurl(); err != nil {
|
||||||
stdout(err)
|
if err != nil {
|
||||||
|
log(color.YellowString(err.Error()) + "\n")
|
||||||
|
}
|
||||||
colors.reset()
|
colors.reset()
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
@@ -155,7 +123,9 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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(color.YellowString(err.Error()) + "\n")
|
||||||
|
}
|
||||||
colors.reset()
|
colors.reset()
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|||||||
206
mcp.go
206
mcp.go
@@ -3,14 +3,13 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"fiatjaf.com/nostr"
|
||||||
|
"fiatjaf.com/nostr/nip19"
|
||||||
|
"fiatjaf.com/nostr/sdk"
|
||||||
"github.com/mark3labs/mcp-go/mcp"
|
"github.com/mark3labs/mcp-go/mcp"
|
||||||
"github.com/mark3labs/mcp-go/server"
|
"github.com/mark3labs/mcp-go/server"
|
||||||
"github.com/nbd-wtf/go-nostr"
|
|
||||||
"github.com/nbd-wtf/go-nostr/nip19"
|
|
||||||
"github.com/nbd-wtf/go-nostr/sdk"
|
|
||||||
"github.com/urfave/cli/v3"
|
"github.com/urfave/cli/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -19,43 +18,30 @@ var mcpServer = &cli.Command{
|
|||||||
Usage: "pander to the AI gods",
|
Usage: "pander to the AI gods",
|
||||||
Description: ``,
|
Description: ``,
|
||||||
DisableSliceFlagSeparator: true,
|
DisableSliceFlagSeparator: true,
|
||||||
Flags: []cli.Flag{},
|
Flags: append(
|
||||||
|
defaultKeyFlags,
|
||||||
|
),
|
||||||
Action: func(ctx context.Context, c *cli.Command) error {
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
s := server.NewMCPServer(
|
s := server.NewMCPServer(
|
||||||
"nak",
|
"nak",
|
||||||
version,
|
version,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
keyer, _, err := gatherKeyerFromArguments(ctx, c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
s.AddTool(mcp.NewTool("publish_note",
|
s.AddTool(mcp.NewTool("publish_note",
|
||||||
mcp.WithDescription("Publish a short note event to Nostr with the given text content"),
|
mcp.WithDescription("Publish a short note event to Nostr with the given text content"),
|
||||||
mcp.WithString("relay",
|
mcp.WithString("content", mcp.Description("Arbitrary string to be published"), mcp.Required()),
|
||||||
mcp.Description("Relay to publish the note to"),
|
mcp.WithString("relay", mcp.Description("Relay to publish the note to")),
|
||||||
),
|
mcp.WithString("mention", mcp.Description("Nostr user's public key to be mentioned")),
|
||||||
mcp.WithString("content",
|
), func(ctx context.Context, r mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
mcp.Required(),
|
content := required[string](r, "content")
|
||||||
mcp.Description("Arbitrary string to be published"),
|
mention, _ := optional[string](r, "mention")
|
||||||
),
|
relay, _ := optional[string](r, "relay")
|
||||||
mcp.WithString("mention",
|
|
||||||
mcp.Required(),
|
|
||||||
mcp.Description("Nostr user's public key to be mentioned"),
|
|
||||||
),
|
|
||||||
), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
||||||
content, _ := request.Params.Arguments["content"].(string)
|
|
||||||
mention, _ := request.Params.Arguments["mention"].(string)
|
|
||||||
relayI, ok := request.Params.Arguments["relay"]
|
|
||||||
var relay string
|
|
||||||
if ok {
|
|
||||||
relay, _ = relayI.(string)
|
|
||||||
}
|
|
||||||
|
|
||||||
if mention != "" && !nostr.IsValidPublicKey(mention) {
|
|
||||||
return mcp.NewToolResultError("the given mention isn't a valid public key, it must be 32 bytes hex, like the ones returned by search_profile"), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
sk := os.Getenv("NOSTR_SECRET_KEY")
|
|
||||||
if sk == "" {
|
|
||||||
sk = "0000000000000000000000000000000000000000000000000000000000000001"
|
|
||||||
}
|
|
||||||
var relays []string
|
var relays []string
|
||||||
|
|
||||||
evt := nostr.Event{
|
evt := nostr.Event{
|
||||||
@@ -66,12 +52,19 @@ var mcpServer = &cli.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
if mention != "" {
|
if mention != "" {
|
||||||
evt.Tags = append(evt.Tags, nostr.Tag{"p", 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
|
// their inbox relays
|
||||||
relays = sys.FetchInboxRelays(ctx, mention, 3)
|
relays = sys.FetchInboxRelays(ctx, pk, 3)
|
||||||
}
|
}
|
||||||
|
|
||||||
evt.Sign(sk)
|
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
|
// our write relays
|
||||||
relays = append(relays, sys.FetchOutboxRelays(ctx, evt.PubKey, 3)...)
|
relays = append(relays, sys.FetchOutboxRelays(ctx, evt.PubKey, 3)...)
|
||||||
@@ -81,7 +74,9 @@ var mcpServer = &cli.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
// extra relay specified
|
// extra relay specified
|
||||||
relays = append(relays, relay)
|
if relay != "" {
|
||||||
|
relays = append(relays, relay)
|
||||||
|
}
|
||||||
|
|
||||||
result := strings.Builder{}
|
result := strings.Builder{}
|
||||||
result.WriteString(
|
result.WriteString(
|
||||||
@@ -111,12 +106,9 @@ var mcpServer = &cli.Command{
|
|||||||
|
|
||||||
s.AddTool(mcp.NewTool("resolve_nostr_uri",
|
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.WithDescription("Resolve URIs prefixed with nostr:, including nostr:nevent1..., nostr:npub1..., nostr:nprofile1... and nostr:naddr1..."),
|
||||||
mcp.WithString("uri",
|
mcp.WithString("uri", mcp.Description("URI to be resolved"), mcp.Required()),
|
||||||
mcp.Required(),
|
), func(ctx context.Context, r mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
mcp.Description("URI to be resolved"),
|
uri := required[string](r, "uri")
|
||||||
),
|
|
||||||
), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
||||||
uri, _ := request.Params.Arguments["uri"].(string)
|
|
||||||
if strings.HasPrefix(uri, "nostr:") {
|
if strings.HasPrefix(uri, "nostr:") {
|
||||||
uri = uri[6:]
|
uri = uri[6:]
|
||||||
}
|
}
|
||||||
@@ -128,7 +120,7 @@ var mcpServer = &cli.Command{
|
|||||||
|
|
||||||
switch prefix {
|
switch prefix {
|
||||||
case "npub":
|
case "npub":
|
||||||
pm := sys.FetchProfileMetadata(ctx, data.(string))
|
pm := sys.FetchProfileMetadata(ctx, data.(nostr.PubKey))
|
||||||
return mcp.NewToolResultText(
|
return mcp.NewToolResultText(
|
||||||
fmt.Sprintf("this is a Nostr profile named '%s', their public key is '%s'",
|
fmt.Sprintf("this is a Nostr profile named '%s', their public key is '%s'",
|
||||||
pm.ShortName(), pm.PubKey),
|
pm.ShortName(), pm.PubKey),
|
||||||
@@ -159,77 +151,80 @@ var mcpServer = &cli.Command{
|
|||||||
|
|
||||||
s.AddTool(mcp.NewTool("search_profile",
|
s.AddTool(mcp.NewTool("search_profile",
|
||||||
mcp.WithDescription("Search for the public key of a Nostr user given their name"),
|
mcp.WithDescription("Search for the public key of a Nostr user given their name"),
|
||||||
mcp.WithString("name",
|
mcp.WithString("name", mcp.Description("Name to be searched"), mcp.Required()),
|
||||||
mcp.Required(),
|
mcp.WithNumber("limit", mcp.Description("How many results to return")),
|
||||||
mcp.Description("Name to be searched"),
|
), func(ctx context.Context, r mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
),
|
name := required[string](r, "name")
|
||||||
), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
limit, _ := optional[float64](r, "limit")
|
||||||
name, _ := request.Params.Arguments["name"].(string)
|
|
||||||
re := sys.Pool.QuerySingle(ctx, []string{"relay.nostr.band", "nostr.wine"}, nostr.Filter{Search: name, Kinds: []int{0}})
|
filter := nostr.Filter{Search: name, Kinds: []nostr.Kind{0}}
|
||||||
if re == nil {
|
if limit > 0 {
|
||||||
return mcp.NewToolResultError("couldn't find anyone with that name"), nil
|
filter.Limit = int(limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
return mcp.NewToolResultText(re.PubKey), nil
|
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",
|
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.WithDescription("Get the best relay from where to read notes from a specific Nostr user"),
|
||||||
mcp.WithString("pubkey",
|
mcp.WithString("pubkey", mcp.Description("Public key of Nostr user we want to know the relay from where to read"), mcp.Required()),
|
||||||
mcp.Required(),
|
), func(ctx context.Context, r mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
mcp.Description("Public key of Nostr user we want to know the relay from where to read"),
|
pubkey, err := nostr.PubKeyFromHex(required[string](r, "pubkey"))
|
||||||
),
|
if err != nil {
|
||||||
), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
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
|
||||||
pubkey, _ := request.Params.Arguments["pubkey"].(string)
|
}
|
||||||
|
|
||||||
res := sys.FetchOutboxRelays(ctx, pubkey, 1)
|
res := sys.FetchOutboxRelays(ctx, pubkey, 1)
|
||||||
return mcp.NewToolResultText(res[0]), nil
|
return mcp.NewToolResultText(res[0]), nil
|
||||||
})
|
})
|
||||||
|
|
||||||
s.AddTool(mcp.NewTool("read_events_from_relay",
|
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.WithDescription("Makes a REQ query to one relay using the specified parameters, this can be used to fetch notes from a profile"),
|
||||||
mcp.WithNumber("kind",
|
mcp.WithString("relay", mcp.Description("relay URL to send the query to"), mcp.Required()),
|
||||||
mcp.Required(),
|
mcp.WithNumber("kind", mcp.Description("event kind number to include in the 'kinds' field"), mcp.Required()),
|
||||||
mcp.Description("event kind number to include in the 'kinds' field"),
|
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")),
|
||||||
mcp.WithString("pubkey",
|
), func(ctx context.Context, r mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
mcp.Description("pubkey to include in the 'authors' field"),
|
relay := required[string](r, "relay")
|
||||||
),
|
kind := int(required[float64](r, "kind"))
|
||||||
mcp.WithNumber("limit",
|
limit := int(required[float64](r, "limit"))
|
||||||
mcp.Required(),
|
pubkey, hasPubKey := optional[string](r, "pubkey")
|
||||||
mcp.Description("maximum number of events to query"),
|
|
||||||
),
|
|
||||||
mcp.WithString("relay",
|
|
||||||
mcp.Required(),
|
|
||||||
mcp.Description("relay URL to send the query to"),
|
|
||||||
),
|
|
||||||
), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
||||||
relay, _ := request.Params.Arguments["relay"].(string)
|
|
||||||
limit, _ := request.Params.Arguments["limit"].(int)
|
|
||||||
kind, _ := request.Params.Arguments["kind"].(int)
|
|
||||||
pubkeyI, ok := request.Params.Arguments["pubkey"]
|
|
||||||
var pubkey string
|
|
||||||
if ok {
|
|
||||||
pubkey, _ = pubkeyI.(string)
|
|
||||||
}
|
|
||||||
|
|
||||||
if pubkey != "" && !nostr.IsValidPublicKey(pubkey) {
|
|
||||||
return mcp.NewToolResultError("the given pubkey isn't a valid public key, it must be 32 bytes hex, like the ones returned by search_profile"), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
filter := nostr.Filter{
|
filter := nostr.Filter{
|
||||||
Limit: limit,
|
Limit: limit,
|
||||||
Kinds: []int{kind},
|
Kinds: []nostr.Kind{nostr.Kind(kind)},
|
||||||
}
|
|
||||||
if pubkey != "" {
|
|
||||||
filter.Authors = []string{pubkey}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
events := sys.Pool.FetchMany(ctx, []string{relay}, filter)
|
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{}
|
result := strings.Builder{}
|
||||||
for ie := range events {
|
for ie := range events {
|
||||||
result.WriteString("author public key: ")
|
result.WriteString("author public key: ")
|
||||||
result.WriteString(ie.PubKey)
|
result.WriteString(ie.PubKey.Hex())
|
||||||
result.WriteString("content: '")
|
result.WriteString("content: '")
|
||||||
result.WriteString(ie.Content)
|
result.WriteString(ie.Content)
|
||||||
result.WriteString("'")
|
result.WriteString("'")
|
||||||
@@ -242,3 +237,28 @@ var mcpServer = &cli.Command{
|
|||||||
return server.ServeStdio(s)
|
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
|
||||||
|
}
|
||||||
|
|||||||
36
musig2.go
36
musig2.go
@@ -9,39 +9,39 @@ import (
|
|||||||
"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,
|
||||||
@@ -50,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
|
||||||
@@ -146,7 +142,7 @@ 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()
|
||||||
log("combined key: %x\n\n", comb.SerializeCompressed())
|
log("combined key: %x\n\n", comb.SerializeCompressed())
|
||||||
}
|
}
|
||||||
@@ -200,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)
|
||||||
}
|
}
|
||||||
@@ -225,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
|
||||||
}
|
}
|
||||||
@@ -258,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))
|
||||||
@@ -269,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])
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/hanwen/go-fuse/v2/fs"
|
"github.com/hanwen/go-fuse/v2/fs"
|
||||||
"github.com/hanwen/go-fuse/v2/fuse"
|
"github.com/hanwen/go-fuse/v2/fuse"
|
||||||
"github.com/nbd-wtf/go-nostr"
|
"fiatjaf.com/nostr"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AsyncFile struct {
|
type AsyncFile struct {
|
||||||
|
|||||||
@@ -15,14 +15,15 @@ import (
|
|||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
"fiatjaf.com/lib/debouncer"
|
"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/fatih/color"
|
||||||
"github.com/hanwen/go-fuse/v2/fs"
|
"github.com/hanwen/go-fuse/v2/fs"
|
||||||
"github.com/hanwen/go-fuse/v2/fuse"
|
"github.com/hanwen/go-fuse/v2/fuse"
|
||||||
"github.com/nbd-wtf/go-nostr"
|
|
||||||
"github.com/nbd-wtf/go-nostr/nip19"
|
|
||||||
"github.com/nbd-wtf/go-nostr/nip27"
|
|
||||||
"github.com/nbd-wtf/go-nostr/nip92"
|
|
||||||
sdk "github.com/nbd-wtf/go-nostr/sdk"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type EntityDir struct {
|
type EntityDir struct {
|
||||||
@@ -95,11 +96,10 @@ func (e *EntityDir) Setattr(_ context.Context, _ fs.FileHandle, in *fuse.SetAttr
|
|||||||
func (e *EntityDir) OnAdd(_ context.Context) {
|
func (e *EntityDir) OnAdd(_ context.Context) {
|
||||||
log := e.root.ctx.Value("log").(func(msg string, args ...any))
|
log := e.root.ctx.Value("log").(func(msg string, args ...any))
|
||||||
|
|
||||||
npub, _ := nip19.EncodePublicKey(e.event.PubKey)
|
|
||||||
e.AddChild("@author", e.NewPersistentInode(
|
e.AddChild("@author", e.NewPersistentInode(
|
||||||
e.root.ctx,
|
e.root.ctx,
|
||||||
&fs.MemSymlink{
|
&fs.MemSymlink{
|
||||||
Data: []byte(e.root.wd + "/" + npub),
|
Data: []byte(e.root.wd + "/" + nip19.EncodeNpub(e.event.PubKey)),
|
||||||
},
|
},
|
||||||
fs.StableAttr{Mode: syscall.S_IFLNK},
|
fs.StableAttr{Mode: syscall.S_IFLNK},
|
||||||
), true)
|
), true)
|
||||||
@@ -180,8 +180,12 @@ func (e *EntityDir) OnAdd(_ context.Context) {
|
|||||||
|
|
||||||
var refsdir *fs.Inode
|
var refsdir *fs.Inode
|
||||||
i := 0
|
i := 0
|
||||||
for ref := range nip27.ParseReferences(*e.event) {
|
for ref := range nip27.Parse(e.event.Content) {
|
||||||
|
if _, isExternal := ref.Pointer.(nip73.ExternalPointer); isExternal {
|
||||||
|
continue
|
||||||
|
}
|
||||||
i++
|
i++
|
||||||
|
|
||||||
if refsdir == nil {
|
if refsdir == nil {
|
||||||
refsdir = e.NewPersistentInode(e.root.ctx, &fs.Inode{}, fs.StableAttr{Mode: syscall.S_IFDIR})
|
refsdir = e.NewPersistentInode(e.root.ctx, &fs.Inode{}, fs.StableAttr{Mode: syscall.S_IFDIR})
|
||||||
e.root.AddChild("references", refsdir, true)
|
e.root.AddChild("references", refsdir, true)
|
||||||
@@ -320,7 +324,11 @@ func (e *EntityDir) handleWrite() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// add "p" tags from people mentioned and "q" tags from events mentioned
|
// add "p" tags from people mentioned and "q" tags from events mentioned
|
||||||
for ref := range nip27.ParseReferences(evt) {
|
for ref := range nip27.Parse(evt.Content) {
|
||||||
|
if _, isExternal := ref.Pointer.(nip73.ExternalPointer); isExternal {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
tag := ref.Pointer.AsTag()
|
tag := ref.Pointer.AsTag()
|
||||||
key := tag[0]
|
key := tag[0]
|
||||||
val := tag[1]
|
val := tag[1]
|
||||||
@@ -339,7 +347,7 @@ func (e *EntityDir) handleWrite() {
|
|||||||
}
|
}
|
||||||
logverbose("%s\n", evt)
|
logverbose("%s\n", evt)
|
||||||
|
|
||||||
relays := e.root.sys.FetchWriteRelays(e.root.ctx, e.root.rootPubKey, 8)
|
relays := e.root.sys.FetchWriteRelays(e.root.ctx, e.root.rootPubKey)
|
||||||
if len(relays) == 0 {
|
if len(relays) == 0 {
|
||||||
relays = e.root.sys.FetchOutboxRelays(e.root.ctx, e.root.rootPubKey, 6)
|
relays = e.root.sys.FetchOutboxRelays(e.root.ctx, e.root.rootPubKey, 6)
|
||||||
}
|
}
|
||||||
@@ -348,11 +356,7 @@ func (e *EntityDir) handleWrite() {
|
|||||||
success := false
|
success := false
|
||||||
first := true
|
first := true
|
||||||
for res := range e.root.sys.Pool.PublishMany(e.root.ctx, relays, evt) {
|
for res := range e.root.sys.Pool.PublishMany(e.root.ctx, relays, evt) {
|
||||||
cleanUrl, ok := strings.CutPrefix(res.RelayURL, "wss://")
|
cleanUrl, _ := strings.CutPrefix(res.RelayURL, "wss://")
|
||||||
if !ok {
|
|
||||||
cleanUrl = res.RelayURL
|
|
||||||
}
|
|
||||||
|
|
||||||
if !first {
|
if !first {
|
||||||
log(", ")
|
log(", ")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package nostrfs
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/binary"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -11,15 +12,16 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"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/fs"
|
||||||
"github.com/hanwen/go-fuse/v2/fuse"
|
"github.com/hanwen/go-fuse/v2/fuse"
|
||||||
"github.com/nbd-wtf/go-nostr"
|
|
||||||
"github.com/nbd-wtf/go-nostr/nip10"
|
|
||||||
"github.com/nbd-wtf/go-nostr/nip19"
|
|
||||||
"github.com/nbd-wtf/go-nostr/nip22"
|
|
||||||
"github.com/nbd-wtf/go-nostr/nip27"
|
|
||||||
"github.com/nbd-wtf/go-nostr/nip92"
|
|
||||||
sdk "github.com/nbd-wtf/go-nostr/sdk"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type EventDir struct {
|
type EventDir struct {
|
||||||
@@ -57,14 +59,13 @@ func (r *NostrRoot) CreateEventDir(
|
|||||||
h := parent.EmbeddedInode().NewPersistentInode(
|
h := parent.EmbeddedInode().NewPersistentInode(
|
||||||
r.ctx,
|
r.ctx,
|
||||||
&EventDir{ctx: r.ctx, wd: r.wd, evt: event},
|
&EventDir{ctx: r.ctx, wd: r.wd, evt: event},
|
||||||
fs.StableAttr{Mode: syscall.S_IFDIR, Ino: hexToUint64(event.ID)},
|
fs.StableAttr{Mode: syscall.S_IFDIR, Ino: binary.BigEndian.Uint64(event.ID[8:16])},
|
||||||
)
|
)
|
||||||
|
|
||||||
npub, _ := nip19.EncodePublicKey(event.PubKey)
|
|
||||||
h.AddChild("@author", h.NewPersistentInode(
|
h.AddChild("@author", h.NewPersistentInode(
|
||||||
r.ctx,
|
r.ctx,
|
||||||
&fs.MemSymlink{
|
&fs.MemSymlink{
|
||||||
Data: []byte(r.wd + "/" + npub),
|
Data: []byte(r.wd + "/" + nip19.EncodeNpub(event.PubKey)),
|
||||||
},
|
},
|
||||||
fs.StableAttr{Mode: syscall.S_IFLNK},
|
fs.StableAttr{Mode: syscall.S_IFLNK},
|
||||||
), true)
|
), true)
|
||||||
@@ -87,7 +88,7 @@ func (r *NostrRoot) CreateEventDir(
|
|||||||
h.AddChild("id", h.NewPersistentInode(
|
h.AddChild("id", h.NewPersistentInode(
|
||||||
r.ctx,
|
r.ctx,
|
||||||
&fs.MemRegularFile{
|
&fs.MemRegularFile{
|
||||||
Data: []byte(event.ID),
|
Data: []byte(event.ID.Hex()),
|
||||||
Attr: fuse.Attr{
|
Attr: fuse.Attr{
|
||||||
Mode: 0444,
|
Mode: 0444,
|
||||||
Ctime: uint64(event.CreatedAt),
|
Ctime: uint64(event.CreatedAt),
|
||||||
@@ -114,8 +115,12 @@ func (r *NostrRoot) CreateEventDir(
|
|||||||
|
|
||||||
var refsdir *fs.Inode
|
var refsdir *fs.Inode
|
||||||
i := 0
|
i := 0
|
||||||
for ref := range nip27.ParseReferences(*event) {
|
for ref := range nip27.Parse(event.Content) {
|
||||||
|
if _, isExternal := ref.Pointer.(nip73.ExternalPointer); isExternal {
|
||||||
|
continue
|
||||||
|
}
|
||||||
i++
|
i++
|
||||||
|
|
||||||
if refsdir == nil {
|
if refsdir == nil {
|
||||||
refsdir = h.NewPersistentInode(r.ctx, &fs.Inode{}, fs.StableAttr{Mode: syscall.S_IFDIR})
|
refsdir = h.NewPersistentInode(r.ctx, &fs.Inode{}, fs.StableAttr{Mode: syscall.S_IFDIR})
|
||||||
h.AddChild("references", refsdir, true)
|
h.AddChild("references", refsdir, true)
|
||||||
@@ -191,7 +196,7 @@ func (r *NostrRoot) CreateEventDir(
|
|||||||
}
|
}
|
||||||
} else if event.Kind == 1111 {
|
} else if event.Kind == 1111 {
|
||||||
if pointer := nip22.GetThreadRoot(event.Tags); pointer != nil {
|
if pointer := nip22.GetThreadRoot(event.Tags); pointer != nil {
|
||||||
if xp, ok := pointer.(nostr.ExternalPointer); ok {
|
if xp, ok := pointer.(nip73.ExternalPointer); ok {
|
||||||
h.AddChild("@root", h.NewPersistentInode(
|
h.AddChild("@root", h.NewPersistentInode(
|
||||||
r.ctx,
|
r.ctx,
|
||||||
&fs.MemRegularFile{
|
&fs.MemRegularFile{
|
||||||
@@ -211,7 +216,7 @@ func (r *NostrRoot) CreateEventDir(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if pointer := nip22.GetImmediateParent(event.Tags); pointer != nil {
|
if pointer := nip22.GetImmediateParent(event.Tags); pointer != nil {
|
||||||
if xp, ok := pointer.(nostr.ExternalPointer); ok {
|
if xp, ok := pointer.(nip73.ExternalPointer); ok {
|
||||||
h.AddChild("@parent", h.NewPersistentInode(
|
h.AddChild("@parent", h.NewPersistentInode(
|
||||||
r.ctx,
|
r.ctx,
|
||||||
&fs.MemRegularFile{
|
&fs.MemRegularFile{
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
package nostrfs
|
package nostrfs
|
||||||
|
|
||||||
import "strconv"
|
import (
|
||||||
|
"fiatjaf.com/nostr"
|
||||||
|
)
|
||||||
|
|
||||||
func kindToExtension(kind int) string {
|
func kindToExtension(kind nostr.Kind) string {
|
||||||
switch kind {
|
switch kind {
|
||||||
case 30023:
|
case 30023:
|
||||||
return "md"
|
return "md"
|
||||||
@@ -12,8 +14,3 @@ func kindToExtension(kind int) string {
|
|||||||
return "txt"
|
return "txt"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func hexToUint64(hexStr string) uint64 {
|
|
||||||
v, _ := strconv.ParseUint(hexStr[16:32], 16, 64)
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package nostrfs
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/binary"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -10,12 +11,12 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"fiatjaf.com/nostr"
|
||||||
|
"fiatjaf.com/nostr/nip19"
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
"github.com/hanwen/go-fuse/v2/fs"
|
"github.com/hanwen/go-fuse/v2/fs"
|
||||||
"github.com/hanwen/go-fuse/v2/fuse"
|
"github.com/hanwen/go-fuse/v2/fuse"
|
||||||
"github.com/liamg/magic"
|
"github.com/liamg/magic"
|
||||||
"github.com/nbd-wtf/go-nostr"
|
|
||||||
"github.com/nbd-wtf/go-nostr/nip19"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type NpubDir struct {
|
type NpubDir struct {
|
||||||
@@ -36,7 +37,7 @@ func (r *NostrRoot) CreateNpubDir(
|
|||||||
return parent.EmbeddedInode().NewPersistentInode(
|
return parent.EmbeddedInode().NewPersistentInode(
|
||||||
r.ctx,
|
r.ctx,
|
||||||
npubdir,
|
npubdir,
|
||||||
fs.StableAttr{Mode: syscall.S_IFDIR, Ino: hexToUint64(pointer.PublicKey)},
|
fs.StableAttr{Mode: syscall.S_IFDIR, Ino: binary.BigEndian.Uint64(pointer.PublicKey[8:16])},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,7 +50,7 @@ func (h *NpubDir) OnAdd(_ context.Context) {
|
|||||||
|
|
||||||
h.AddChild("pubkey", h.NewPersistentInode(
|
h.AddChild("pubkey", h.NewPersistentInode(
|
||||||
h.root.ctx,
|
h.root.ctx,
|
||||||
&fs.MemRegularFile{Data: []byte(h.pointer.PublicKey + "\n"), Attr: fuse.Attr{Mode: 0444}},
|
&fs.MemRegularFile{Data: []byte(h.pointer.PublicKey.Hex() + "\n"), Attr: fuse.Attr{Mode: 0444}},
|
||||||
fs.StableAttr{},
|
fs.StableAttr{},
|
||||||
), true)
|
), true)
|
||||||
|
|
||||||
@@ -116,8 +117,8 @@ func (h *NpubDir) OnAdd(_ context.Context) {
|
|||||||
&ViewDir{
|
&ViewDir{
|
||||||
root: h.root,
|
root: h.root,
|
||||||
filter: nostr.Filter{
|
filter: nostr.Filter{
|
||||||
Kinds: []int{1},
|
Kinds: []nostr.Kind{1},
|
||||||
Authors: []string{h.pointer.PublicKey},
|
Authors: []nostr.PubKey{h.pointer.PublicKey},
|
||||||
},
|
},
|
||||||
paginate: true,
|
paginate: true,
|
||||||
relays: relays,
|
relays: relays,
|
||||||
@@ -138,8 +139,8 @@ func (h *NpubDir) OnAdd(_ context.Context) {
|
|||||||
&ViewDir{
|
&ViewDir{
|
||||||
root: h.root,
|
root: h.root,
|
||||||
filter: nostr.Filter{
|
filter: nostr.Filter{
|
||||||
Kinds: []int{1111},
|
Kinds: []nostr.Kind{1111},
|
||||||
Authors: []string{h.pointer.PublicKey},
|
Authors: []nostr.PubKey{h.pointer.PublicKey},
|
||||||
},
|
},
|
||||||
paginate: true,
|
paginate: true,
|
||||||
relays: relays,
|
relays: relays,
|
||||||
@@ -159,8 +160,8 @@ func (h *NpubDir) OnAdd(_ context.Context) {
|
|||||||
&ViewDir{
|
&ViewDir{
|
||||||
root: h.root,
|
root: h.root,
|
||||||
filter: nostr.Filter{
|
filter: nostr.Filter{
|
||||||
Kinds: []int{20},
|
Kinds: []nostr.Kind{20},
|
||||||
Authors: []string{h.pointer.PublicKey},
|
Authors: []nostr.PubKey{h.pointer.PublicKey},
|
||||||
},
|
},
|
||||||
paginate: true,
|
paginate: true,
|
||||||
relays: relays,
|
relays: relays,
|
||||||
@@ -180,8 +181,8 @@ func (h *NpubDir) OnAdd(_ context.Context) {
|
|||||||
&ViewDir{
|
&ViewDir{
|
||||||
root: h.root,
|
root: h.root,
|
||||||
filter: nostr.Filter{
|
filter: nostr.Filter{
|
||||||
Kinds: []int{21, 22},
|
Kinds: []nostr.Kind{21, 22},
|
||||||
Authors: []string{h.pointer.PublicKey},
|
Authors: []nostr.PubKey{h.pointer.PublicKey},
|
||||||
},
|
},
|
||||||
paginate: false,
|
paginate: false,
|
||||||
relays: relays,
|
relays: relays,
|
||||||
@@ -201,8 +202,8 @@ func (h *NpubDir) OnAdd(_ context.Context) {
|
|||||||
&ViewDir{
|
&ViewDir{
|
||||||
root: h.root,
|
root: h.root,
|
||||||
filter: nostr.Filter{
|
filter: nostr.Filter{
|
||||||
Kinds: []int{9802},
|
Kinds: []nostr.Kind{9802},
|
||||||
Authors: []string{h.pointer.PublicKey},
|
Authors: []nostr.PubKey{h.pointer.PublicKey},
|
||||||
},
|
},
|
||||||
paginate: false,
|
paginate: false,
|
||||||
relays: relays,
|
relays: relays,
|
||||||
@@ -222,8 +223,8 @@ func (h *NpubDir) OnAdd(_ context.Context) {
|
|||||||
&ViewDir{
|
&ViewDir{
|
||||||
root: h.root,
|
root: h.root,
|
||||||
filter: nostr.Filter{
|
filter: nostr.Filter{
|
||||||
Kinds: []int{30023},
|
Kinds: []nostr.Kind{30023},
|
||||||
Authors: []string{h.pointer.PublicKey},
|
Authors: []nostr.PubKey{h.pointer.PublicKey},
|
||||||
},
|
},
|
||||||
paginate: false,
|
paginate: false,
|
||||||
relays: relays,
|
relays: relays,
|
||||||
@@ -244,8 +245,8 @@ func (h *NpubDir) OnAdd(_ context.Context) {
|
|||||||
&ViewDir{
|
&ViewDir{
|
||||||
root: h.root,
|
root: h.root,
|
||||||
filter: nostr.Filter{
|
filter: nostr.Filter{
|
||||||
Kinds: []int{30818},
|
Kinds: []nostr.Kind{30818},
|
||||||
Authors: []string{h.pointer.PublicKey},
|
Authors: []nostr.PubKey{h.pointer.PublicKey},
|
||||||
},
|
},
|
||||||
paginate: false,
|
paginate: false,
|
||||||
relays: relays,
|
relays: relays,
|
||||||
|
|||||||
@@ -6,12 +6,12 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"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/fs"
|
||||||
"github.com/hanwen/go-fuse/v2/fuse"
|
"github.com/hanwen/go-fuse/v2/fuse"
|
||||||
"github.com/nbd-wtf/go-nostr"
|
|
||||||
"github.com/nbd-wtf/go-nostr/nip05"
|
|
||||||
"github.com/nbd-wtf/go-nostr/nip19"
|
|
||||||
"github.com/nbd-wtf/go-nostr/sdk"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Options struct {
|
type Options struct {
|
||||||
@@ -25,7 +25,7 @@ type NostrRoot struct {
|
|||||||
ctx context.Context
|
ctx context.Context
|
||||||
wd string
|
wd string
|
||||||
sys *sdk.System
|
sys *sdk.System
|
||||||
rootPubKey string
|
rootPubKey nostr.PubKey
|
||||||
signer nostr.Signer
|
signer nostr.Signer
|
||||||
|
|
||||||
opts Options
|
opts Options
|
||||||
@@ -54,7 +54,7 @@ func NewNostrRoot(ctx context.Context, sys *sdk.System, user nostr.User, mountpo
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *NostrRoot) OnAdd(_ context.Context) {
|
func (r *NostrRoot) OnAdd(_ context.Context) {
|
||||||
if r.rootPubKey == "" {
|
if r.rootPubKey == nostr.ZeroPK {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,16 +65,15 @@ func (r *NostrRoot) OnAdd(_ context.Context) {
|
|||||||
fl := r.sys.FetchFollowList(r.ctx, r.rootPubKey)
|
fl := r.sys.FetchFollowList(r.ctx, r.rootPubKey)
|
||||||
for _, f := range fl.Items {
|
for _, f := range fl.Items {
|
||||||
pointer := nostr.ProfilePointer{PublicKey: f.Pubkey, Relays: []string{f.Relay}}
|
pointer := nostr.ProfilePointer{PublicKey: f.Pubkey, Relays: []string{f.Relay}}
|
||||||
npub, _ := nip19.EncodePublicKey(f.Pubkey)
|
|
||||||
r.AddChild(
|
r.AddChild(
|
||||||
npub,
|
nip19.EncodeNpub(f.Pubkey),
|
||||||
r.CreateNpubDir(r, pointer, nil),
|
r.CreateNpubDir(r, pointer, nil),
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// add ourselves
|
// add ourselves
|
||||||
npub, _ := nip19.EncodePublicKey(r.rootPubKey)
|
npub := nip19.EncodeNpub(r.rootPubKey)
|
||||||
if r.GetChild(npub) == nil {
|
if r.GetChild(npub) == nil {
|
||||||
pointer := nostr.ProfilePointer{PublicKey: r.rootPubKey}
|
pointer := nostr.ProfilePointer{PublicKey: r.rootPubKey}
|
||||||
|
|
||||||
|
|||||||
@@ -2,17 +2,15 @@ package nostrfs
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"slices"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"fiatjaf.com/lib/debouncer"
|
"fiatjaf.com/lib/debouncer"
|
||||||
|
"fiatjaf.com/nostr"
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
"github.com/hanwen/go-fuse/v2/fs"
|
"github.com/hanwen/go-fuse/v2/fs"
|
||||||
"github.com/hanwen/go-fuse/v2/fuse"
|
"github.com/hanwen/go-fuse/v2/fuse"
|
||||||
"github.com/nbd-wtf/go-nostr"
|
|
||||||
"github.com/nbd-wtf/go-nostr/nip27"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ViewDir struct {
|
type ViewDir struct {
|
||||||
@@ -141,32 +139,14 @@ func (n *ViewDir) publishNote() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// our write relays
|
// our write relays
|
||||||
relays := n.root.sys.FetchWriteRelays(n.root.ctx, n.root.rootPubKey, 8)
|
relays := n.root.sys.FetchWriteRelays(n.root.ctx, n.root.rootPubKey)
|
||||||
if len(relays) == 0 {
|
if len(relays) == 0 {
|
||||||
relays = n.root.sys.FetchOutboxRelays(n.root.ctx, n.root.rootPubKey, 6)
|
relays = n.root.sys.FetchOutboxRelays(n.root.ctx, n.root.rootPubKey, 6)
|
||||||
}
|
}
|
||||||
|
|
||||||
// add "p" tags from people mentioned and "q" tags from events mentioned
|
// massage and extract tags from raw text
|
||||||
for ref := range nip27.ParseReferences(evt) {
|
targetRelays := n.root.sys.PrepareNoteEvent(n.root.ctx, &evt)
|
||||||
tag := ref.Pointer.AsTag()
|
relays = nostr.AppendUnique(relays, targetRelays...)
|
||||||
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)
|
|
||||||
|
|
||||||
// add their "read" relays
|
|
||||||
if key == "p" {
|
|
||||||
for _, r := range n.root.sys.FetchInboxRelays(n.root.ctx, val, 4) {
|
|
||||||
if !slices.Contains(relays, r) {
|
|
||||||
relays = append(relays, r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// sign and publish
|
// sign and publish
|
||||||
if err := n.root.signer.SignEvent(n.root.ctx, &evt); err != nil {
|
if err := n.root.signer.SignEvent(n.root.ctx, &evt); err != nil {
|
||||||
@@ -179,11 +159,7 @@ func (n *ViewDir) publishNote() {
|
|||||||
success := false
|
success := false
|
||||||
first := true
|
first := true
|
||||||
for res := range n.root.sys.Pool.PublishMany(n.root.ctx, relays, evt) {
|
for res := range n.root.sys.Pool.PublishMany(n.root.ctx, relays, evt) {
|
||||||
cleanUrl, ok := strings.CutPrefix(res.RelayURL, "wss://")
|
cleanUrl, _ := strings.CutPrefix(res.RelayURL, "wss://")
|
||||||
if !ok {
|
|
||||||
cleanUrl = res.RelayURL
|
|
||||||
}
|
|
||||||
|
|
||||||
if !first {
|
if !first {
|
||||||
log(", ")
|
log(", ")
|
||||||
}
|
}
|
||||||
@@ -200,15 +176,15 @@ func (n *ViewDir) publishNote() {
|
|||||||
|
|
||||||
if success {
|
if success {
|
||||||
n.RmChild("new")
|
n.RmChild("new")
|
||||||
n.AddChild(evt.ID, n.root.CreateEventDir(n, &evt), true)
|
n.AddChild(evt.ID.Hex(), n.root.CreateEventDir(n, &evt), true)
|
||||||
log("event published as %s and updated locally.\n", color.BlueString(evt.ID))
|
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 {
|
func (n *ViewDir) Getattr(_ context.Context, f fs.FileHandle, out *fuse.AttrOut) syscall.Errno {
|
||||||
now := nostr.Now()
|
now := nostr.Now()
|
||||||
if n.filter.Until != nil {
|
if n.filter.Until != 0 {
|
||||||
now = *n.filter.Until
|
now = n.filter.Until
|
||||||
}
|
}
|
||||||
aMonthAgo := now - 30*24*60*60
|
aMonthAgo := now - 30*24*60*60
|
||||||
out.Mtime = uint64(aMonthAgo)
|
out.Mtime = uint64(aMonthAgo)
|
||||||
@@ -223,14 +199,14 @@ func (n *ViewDir) Opendir(ctx context.Context) syscall.Errno {
|
|||||||
|
|
||||||
if n.paginate {
|
if n.paginate {
|
||||||
now := nostr.Now()
|
now := nostr.Now()
|
||||||
if n.filter.Until != nil {
|
if n.filter.Until != 0 {
|
||||||
now = *n.filter.Until
|
now = n.filter.Until
|
||||||
}
|
}
|
||||||
aMonthAgo := now - 30*24*60*60
|
aMonthAgo := now - 30*24*60*60
|
||||||
n.filter.Since = &aMonthAgo
|
n.filter.Since = aMonthAgo
|
||||||
|
|
||||||
filter := n.filter
|
filter := n.filter
|
||||||
filter.Until = &aMonthAgo
|
filter.Until = aMonthAgo
|
||||||
|
|
||||||
n.AddChild("@previous", n.NewPersistentInode(
|
n.AddChild("@previous", n.NewPersistentInode(
|
||||||
n.root.ctx,
|
n.root.ctx,
|
||||||
@@ -245,23 +221,24 @@ func (n *ViewDir) Opendir(ctx context.Context) syscall.Errno {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if n.replaceable {
|
if n.replaceable {
|
||||||
for rkey, evt := range n.root.sys.Pool.FetchManyReplaceable(n.root.ctx, n.relays, n.filter,
|
for rkey, evt := range n.root.sys.Pool.FetchManyReplaceable(n.root.ctx, n.relays, n.filter, nostr.SubscriptionOptions{
|
||||||
nostr.WithLabel("nakfs"),
|
Label: "nakfs",
|
||||||
).Range {
|
}).Range {
|
||||||
name := rkey.D
|
name := rkey.D
|
||||||
if name == "" {
|
if name == "" {
|
||||||
name = "_"
|
name = "_"
|
||||||
}
|
}
|
||||||
if n.GetChild(name) == nil {
|
if n.GetChild(name) == nil {
|
||||||
n.AddChild(name, n.root.CreateEntityDir(n, evt), true)
|
n.AddChild(name, n.root.CreateEntityDir(n, &evt), true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for ie := range n.root.sys.Pool.FetchMany(n.root.ctx, n.relays, n.filter,
|
for ie := range n.root.sys.Pool.FetchMany(n.root.ctx, n.relays, n.filter,
|
||||||
nostr.WithLabel("nakfs"),
|
nostr.SubscriptionOptions{
|
||||||
) {
|
Label: "nakfs",
|
||||||
if n.GetChild(ie.Event.ID) == nil {
|
}) {
|
||||||
n.AddChild(ie.Event.ID, n.root.CreateEventDir(n, ie.Event), true)
|
if n.GetChild(ie.Event.ID.Hex()) == nil {
|
||||||
|
n.AddChild(ie.Event.ID.Hex(), n.root.CreateEventDir(n, &ie.Event), true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
57
outbox.go
57
outbox.go
@@ -6,10 +6,45 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
"fiatjaf.com/nostr"
|
||||||
|
"fiatjaf.com/nostr/sdk"
|
||||||
|
"fiatjaf.com/nostr/sdk/hints/badgerh"
|
||||||
|
"github.com/fatih/color"
|
||||||
"github.com/urfave/cli/v3"
|
"github.com/urfave/cli/v3"
|
||||||
"github.com/nbd-wtf/go-nostr"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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); !os.IsNotExist(err) {
|
||||||
|
hintsFileExists = true
|
||||||
|
} else if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if hintsFileExists && hintsFilePath != "" {
|
||||||
|
hintsdb, err := badgerh.NewBadgerHints(hintsFilePath)
|
||||||
|
if err == nil {
|
||||||
|
sys.Hints = hintsdb
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
var outbox = &cli.Command{
|
var outbox = &cli.Command{
|
||||||
Name: "outbox",
|
Name: "outbox",
|
||||||
Usage: "manage outbox relay hints database",
|
Usage: "manage outbox relay hints database",
|
||||||
@@ -27,10 +62,10 @@ var outbox = &cli.Command{
|
|||||||
return fmt.Errorf("couldn't find a place to store the hints, pass --config-path to fix.")
|
return fmt.Errorf("couldn't find a place to store the hints, pass --config-path to fix.")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.MkdirAll(filepath.Dir(hintsFilePath), 0777); err == nil {
|
os.MkdirAll(hintsFilePath, 0755)
|
||||||
if err := os.WriteFile(hintsFilePath, []byte("{}"), 0644); err != nil {
|
_, err := badgerh.NewBadgerHints(hintsFilePath)
|
||||||
return fmt.Errorf("failed to create hints database: %w", err)
|
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)
|
log("initialized hints database at %s\n", hintsFilePath)
|
||||||
@@ -44,20 +79,20 @@ var outbox = &cli.Command{
|
|||||||
DisableSliceFlagSeparator: true,
|
DisableSliceFlagSeparator: true,
|
||||||
Action: func(ctx context.Context, c *cli.Command) error {
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
if !hintsFileExists {
|
if !hintsFileExists {
|
||||||
log("running with temporary fragile data.\n")
|
log(color.YellowString("running with temporary fragile data.\n"))
|
||||||
log("call `nak outbox init` to setup persistence.\n")
|
log(color.YellowString("call `nak outbox init` to setup persistence.\n"))
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.Args().Len() != 1 {
|
if c.Args().Len() != 1 {
|
||||||
return fmt.Errorf("expected exactly one argument (pubkey)")
|
return fmt.Errorf("expected exactly one argument (pubkey)")
|
||||||
}
|
}
|
||||||
|
|
||||||
pubkey := c.Args().First()
|
pk, err := nostr.PubKeyFromHex(c.Args().First())
|
||||||
if !nostr.IsValidPublicKey(pubkey) {
|
if err != nil {
|
||||||
return fmt.Errorf("invalid public key: %s", pubkey)
|
return fmt.Errorf("invalid public key '%s': %w", c.Args().First(), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, relay := range sys.FetchOutboxRelays(ctx, pubkey, 6) {
|
for _, relay := range sys.FetchOutboxRelays(ctx, pk, 6) {
|
||||||
stdout(relay)
|
stdout(relay)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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)
|
||||||
|
},
|
||||||
|
}
|
||||||
6
relay.go
6
relay.go
@@ -10,9 +10,9 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/nbd-wtf/go-nostr"
|
"fiatjaf.com/nostr"
|
||||||
"github.com/nbd-wtf/go-nostr/nip11"
|
"fiatjaf.com/nostr/nip11"
|
||||||
"github.com/nbd-wtf/go-nostr/nip86"
|
"fiatjaf.com/nostr/nip86"
|
||||||
"github.com/urfave/cli/v3"
|
"github.com/urfave/cli/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
84
req.go
84
req.go
@@ -6,9 +6,11 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"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/nbd-wtf/go-nostr/nip77"
|
|
||||||
"github.com/urfave/cli/v3"
|
"github.com/urfave/cli/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -76,35 +78,33 @@ example:
|
|||||||
Action: func(ctx context.Context, c *cli.Command) error {
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
relayUrls := c.Args().Slice()
|
relayUrls := c.Args().Slice()
|
||||||
if len(relayUrls) > 0 {
|
if len(relayUrls) > 0 {
|
||||||
relays := connectToAllRelays(ctx,
|
// this is used both for the normal AUTH (after "auth-required:" is received) or forced pre-auth
|
||||||
|
// connect to all relays we expect to use in this call in parallel
|
||||||
|
forcePreAuthSigner := authSigner
|
||||||
|
if !c.Bool("force-pre-auth") {
|
||||||
|
forcePreAuthSigner = nil
|
||||||
|
}
|
||||||
|
relays := connectToAllRelays(
|
||||||
|
ctx,
|
||||||
|
c,
|
||||||
relayUrls,
|
relayUrls,
|
||||||
c.Bool("force-pre-auth"),
|
forcePreAuthSigner,
|
||||||
nostr.WithAuthHandler(
|
nostr.PoolOptions{
|
||||||
func(ctx context.Context, authEvent nostr.RelayEvent) (err error) {
|
AuthHandler: func(ctx context.Context, authEvent *nostr.Event) error {
|
||||||
defer func() {
|
return authSigner(ctx, c, func(s string, args ...any) {
|
||||||
if err != nil {
|
if strings.HasPrefix(s, "authenticating as") {
|
||||||
log("auth to %s failed: %s\n",
|
cleanUrl, _ := strings.CutPrefix(
|
||||||
(*authEvent.Tags.GetFirst([]string{"relay", ""}))[1],
|
nip42.GetRelayURLFromAuthEvent(*authEvent),
|
||||||
err,
|
"wss://",
|
||||||
)
|
)
|
||||||
|
s = "authenticating to " + color.CyanString(cleanUrl) + " as" + s[len("authenticating as"):]
|
||||||
}
|
}
|
||||||
}()
|
log(s+"\n", args...)
|
||||||
|
}, authEvent)
|
||||||
if !c.Bool("auth") && !c.Bool("force-pre-auth") {
|
|
||||||
return fmt.Errorf("auth not authorized")
|
|
||||||
}
|
|
||||||
kr, _, err := gatherKeyerFromArguments(ctx, c)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
pk, _ := kr.GetPublicKey(ctx)
|
|
||||||
log("performing auth as %s... ", pk)
|
|
||||||
|
|
||||||
return kr.SignEvent(ctx, authEvent.Event)
|
|
||||||
},
|
},
|
||||||
),
|
})
|
||||||
)
|
|
||||||
|
// stop here already if all connections failed
|
||||||
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)
|
||||||
@@ -113,14 +113,9 @@ 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()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// go line by line from stdin or run once with input from flags
|
||||||
for stdinFilter := range getJsonsOrBlank() {
|
for stdinFilter := range getJsonsOrBlank() {
|
||||||
filter := nostr.Filter{}
|
filter := nostr.Filter{}
|
||||||
if stdinFilter != "" {
|
if stdinFilter != "" {
|
||||||
@@ -136,7 +131,7 @@ example:
|
|||||||
|
|
||||||
if len(relayUrls) > 0 {
|
if len(relayUrls) > 0 {
|
||||||
if c.Bool("ids-only") {
|
if c.Bool("ids-only") {
|
||||||
seen := make(map[string]struct{}, max(500, filter.Limit))
|
seen := make(map[nostr.ID]struct{}, max(500, filter.Limit))
|
||||||
for _, url := range relayUrls {
|
for _, url := range relayUrls {
|
||||||
ch, err := nip77.FetchIDsOnly(ctx, url, filter)
|
ch, err := nip77.FetchIDsOnly(ctx, url, filter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -159,7 +154,7 @@ example:
|
|||||||
fn = sys.Pool.SubscribeMany
|
fn = sys.Pool.SubscribeMany
|
||||||
}
|
}
|
||||||
|
|
||||||
for ie := range fn(ctx, relayUrls, filter) {
|
for ie := range fn(ctx, relayUrls, filter, nostr.SubscriptionOptions{}) {
|
||||||
stdout(ie.Event)
|
stdout(ie.Event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -169,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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,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)",
|
||||||
@@ -248,14 +243,14 @@ var reqFilterFlags = []cli.Flag{
|
|||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
@@ -291,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 {
|
||||||
|
|||||||
54
serve.go
54
serve.go
@@ -4,15 +4,15 @@ import (
|
|||||||
"bufio"
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
"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/eventstore/slicestore"
|
|
||||||
"github.com/fiatjaf/khatru"
|
|
||||||
"github.com/nbd-wtf/go-nostr"
|
|
||||||
"github.com/urfave/cli/v3"
|
"github.com/urfave/cli/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -38,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 != "" {
|
||||||
@@ -59,7 +59,7 @@ 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++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -71,10 +71,7 @@ var serve = &cli.Command{
|
|||||||
rl.Info.Software = "https://github.com/fiatjaf/nak"
|
rl.Info.Software = "https://github.com/fiatjaf/nak"
|
||||||
rl.Info.Version = version
|
rl.Info.Version = version
|
||||||
|
|
||||||
rl.QueryEvents = append(rl.QueryEvents, db.QueryEvents)
|
rl.UseEventstore(db, 1_000_000)
|
||||||
rl.CountEvents = append(rl.CountEvents, db.CountEvents)
|
|
||||||
rl.DeleteEvent = append(rl.DeleteEvent, db.DeleteEvent)
|
|
||||||
rl.StoreEvent = append(rl.StoreEvent, db.SaveEvent)
|
|
||||||
|
|
||||||
started := make(chan bool)
|
started := make(chan bool)
|
||||||
exited := make(chan error)
|
exited := make(chan error)
|
||||||
@@ -90,33 +87,48 @@ var serve = &cli.Command{
|
|||||||
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"), colors.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) {
|
|
||||||
|
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))
|
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) {
|
|
||||||
|
rl.OnEvent = func(ctx context.Context, event nostr.Event) (reject bool, msg string) {
|
||||||
log(" got %s %v\n", color.BlueString("event"), colors.italic(event))
|
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)),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
"fiatjaf.com/nostr"
|
||||||
"github.com/urfave/cli/v3"
|
"github.com/urfave/cli/v3"
|
||||||
"github.com/nbd-wtf/go-nostr"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var verify = &cli.Command{
|
var verify = &cli.Command{
|
||||||
@@ -30,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: %v", err)
|
ctx = lineProcessingError(ctx, "invalid signature")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
116
wallet.go
116
wallet.go
@@ -6,11 +6,10 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/fatih/color"
|
"fiatjaf.com/nostr"
|
||||||
"github.com/nbd-wtf/go-nostr"
|
"fiatjaf.com/nostr/nip60"
|
||||||
"github.com/nbd-wtf/go-nostr/nip60"
|
"fiatjaf.com/nostr/nip61"
|
||||||
"github.com/nbd-wtf/go-nostr/nip61"
|
"fiatjaf.com/nostr/sdk"
|
||||||
"github.com/nbd-wtf/go-nostr/sdk"
|
|
||||||
"github.com/urfave/cli/v3"
|
"github.com/urfave/cli/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -26,12 +25,12 @@ func prepareWallet(ctx context.Context, c *cli.Command) (*nip60.Wallet, func(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
relays := sys.FetchOutboxRelays(ctx, pk, 3)
|
relays := sys.FetchOutboxRelays(ctx, pk, 3)
|
||||||
w := nip60.LoadWallet(ctx, kr, sys.Pool, relays)
|
w := nip60.LoadWallet(ctx, kr, sys.Pool, relays, nip60.WalletOptions{})
|
||||||
if w == nil {
|
if w == nil {
|
||||||
return nil, nil, fmt.Errorf("error loading walle")
|
return nil, nil, fmt.Errorf("error loading walle")
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Processed = func(evt *nostr.Event, err error) {
|
w.Processed = func(evt nostr.Event, err error) {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
logverbose("processed event %s\n", evt)
|
logverbose("processed event %s\n", evt)
|
||||||
} else {
|
} else {
|
||||||
@@ -60,20 +59,16 @@ func prepareWallet(ctx context.Context, c *cli.Command) (*nip60.Wallet, func(),
|
|||||||
log("- saving kind:%d event (%s)... ", event.Kind, desc)
|
log("- saving kind:%d event (%s)... ", event.Kind, desc)
|
||||||
first := true
|
first := true
|
||||||
for res := range sys.Pool.PublishMany(ctx, relays, event) {
|
for res := range sys.Pool.PublishMany(ctx, relays, event) {
|
||||||
cleanUrl, ok := strings.CutPrefix(res.RelayURL, "wss://")
|
cleanUrl, _ := strings.CutPrefix(res.RelayURL, "wss://")
|
||||||
if !ok {
|
|
||||||
cleanUrl = res.RelayURL
|
|
||||||
}
|
|
||||||
|
|
||||||
if !first {
|
if !first {
|
||||||
log(", ")
|
log(", ")
|
||||||
}
|
}
|
||||||
first = false
|
first = false
|
||||||
|
|
||||||
if res.Error != nil {
|
if res.Error != nil {
|
||||||
log("%s: %s", color.RedString(cleanUrl), res.Error)
|
log("%s: %s", colors.errorf(cleanUrl), res.Error)
|
||||||
} else {
|
} else {
|
||||||
log("%s: ok", color.GreenString(cleanUrl))
|
log("%s: ok", colors.successf(cleanUrl))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log("\n")
|
log("\n")
|
||||||
@@ -205,12 +200,9 @@ var wallet = &cli.Command{
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := make([]nip60.ReceiveOption, 0, 1)
|
if err := w.Receive(ctx, proofs, mint, nip60.ReceiveOptions{
|
||||||
for _, url := range c.StringSlice("mint") {
|
IntoMint: c.StringSlice("mint"),
|
||||||
opts = append(opts, nip60.WithMintDestination(url))
|
}); err != nil {
|
||||||
}
|
|
||||||
|
|
||||||
if err := w.Receive(ctx, proofs, mint, opts...); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,12 +236,13 @@ var wallet = &cli.Command{
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := make([]nip60.SendOption, 0, 1)
|
var sourceMint string
|
||||||
if mint := c.String("mint"); mint != "" {
|
if mint := c.String("mint"); mint != "" {
|
||||||
mint = "http" + nostr.NormalizeURL(mint)[2:]
|
sourceMint = "http" + nostr.NormalizeURL(mint)[2:]
|
||||||
opts = append(opts, nip60.WithMint(mint))
|
|
||||||
}
|
}
|
||||||
proofs, mint, err := w.Send(ctx, amount, opts...)
|
proofs, mint, err := w.SendInternal(ctx, amount, nip60.SendOptions{
|
||||||
|
SpecificSourceMint: sourceMint,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -282,13 +275,14 @@ var wallet = &cli.Command{
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := make([]nip60.SendOption, 0, 1)
|
var sourceMint string
|
||||||
if mint := c.String("mint"); mint != "" {
|
if mint := c.String("mint"); mint != "" {
|
||||||
mint = "http" + nostr.NormalizeURL(mint)[2:]
|
sourceMint = "http" + nostr.NormalizeURL(mint)[2:]
|
||||||
opts = append(opts, nip60.WithMint(mint))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
preimage, err := w.PayBolt11(ctx, args[0], opts...)
|
preimage, err := w.PayBolt11(ctx, args[0], nip60.PayOptions{
|
||||||
|
FromMint: sourceMint,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -326,11 +320,16 @@ var wallet = &cli.Command{
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
amount := c.Uint("amount")
|
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")
|
target := c.String("target")
|
||||||
|
var pm sdk.ProfileMetadata
|
||||||
|
|
||||||
var evt *nostr.Event
|
var evt *nostr.Event
|
||||||
var eventId string
|
var eventId nostr.ID
|
||||||
|
|
||||||
if strings.HasPrefix(target, "nevent1") {
|
if strings.HasPrefix(target, "nevent1") {
|
||||||
evt, _, err = sys.FetchSpecificEventFromInput(ctx, target, sdk.FetchSpecificEventParameters{
|
evt, _, err = sys.FetchSpecificEventFromInput(ctx, target, sdk.FetchSpecificEventParameters{
|
||||||
@@ -340,20 +339,19 @@ var wallet = &cli.Command{
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
eventId = evt.ID
|
eventId = evt.ID
|
||||||
target = evt.PubKey
|
pm = sys.FetchProfileMetadata(ctx, evt.PubKey)
|
||||||
}
|
} else {
|
||||||
|
pm, err = sys.FetchProfileFromInput(ctx, target)
|
||||||
pm, err := sys.FetchProfileFromInput(ctx, target)
|
if err != nil {
|
||||||
if err != nil {
|
return err
|
||||||
return err
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log("sending %d sat to '%s' (%s)", amount, pm.ShortName(), pm.Npub())
|
log("sending %d sat to '%s' (%s)", amount, pm.ShortName(), pm.Npub())
|
||||||
|
|
||||||
opts := make([]nip60.SendOption, 0, 1)
|
var sourceMint string
|
||||||
if mint := c.String("mint"); mint != "" {
|
if mint := c.String("mint"); mint != "" {
|
||||||
mint = "http" + nostr.NormalizeURL(mint)[2:]
|
sourceMint = "http" + nostr.NormalizeURL(mint)[2:]
|
||||||
opts = append(opts, nip60.WithMint(mint))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
kr, _, _ := gatherKeyerFromArguments(ctx, c)
|
kr, _, _ := gatherKeyerFromArguments(ctx, c)
|
||||||
@@ -362,12 +360,15 @@ var wallet = &cli.Command{
|
|||||||
kr,
|
kr,
|
||||||
w,
|
w,
|
||||||
sys.Pool,
|
sys.Pool,
|
||||||
|
uint64(amount),
|
||||||
pm.PubKey,
|
pm.PubKey,
|
||||||
sys.FetchInboxRelays,
|
sys.FetchWriteRelays(ctx, pm.PubKey),
|
||||||
sys.FetchOutboxRelays(ctx, pm.PubKey, 3),
|
nip61.NutzapOptions{
|
||||||
eventId,
|
Message: c.String("message"),
|
||||||
amount,
|
SendToRelays: sys.FetchInboxRelays(ctx, pm.PubKey, 3),
|
||||||
c.String("message"),
|
EventID: eventId,
|
||||||
|
SpecificSourceMint: sourceMint,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -376,19 +377,15 @@ var wallet = &cli.Command{
|
|||||||
log("- publishing nutzap... ")
|
log("- publishing nutzap... ")
|
||||||
first := true
|
first := true
|
||||||
for res := range results {
|
for res := range results {
|
||||||
cleanUrl, ok := strings.CutPrefix(res.RelayURL, "wss://")
|
cleanUrl, _ := strings.CutPrefix(res.RelayURL, "wss://")
|
||||||
if !ok {
|
|
||||||
cleanUrl = res.RelayURL
|
|
||||||
}
|
|
||||||
|
|
||||||
if !first {
|
if !first {
|
||||||
log(", ")
|
log(", ")
|
||||||
}
|
}
|
||||||
first = false
|
first = false
|
||||||
if res.Error != nil {
|
if res.Error != nil {
|
||||||
log("%s: %s", color.RedString(cleanUrl), res.Error)
|
log("%s: %s", colors.errorf(cleanUrl), res.Error)
|
||||||
} else {
|
} else {
|
||||||
log("%s: ok", color.GreenString(cleanUrl))
|
log("%s: ok", colors.successf(cleanUrl))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -435,14 +432,14 @@ var wallet = &cli.Command{
|
|||||||
|
|
||||||
kr, _, _ := gatherKeyerFromArguments(ctx, c)
|
kr, _, _ := gatherKeyerFromArguments(ctx, c)
|
||||||
pk, _ := kr.GetPublicKey(ctx)
|
pk, _ := kr.GetPublicKey(ctx)
|
||||||
relays := sys.FetchWriteRelays(ctx, pk, 6)
|
relays := sys.FetchWriteRelays(ctx, pk)
|
||||||
|
|
||||||
info := nip61.Info{}
|
info := nip61.Info{}
|
||||||
ie := sys.Pool.QuerySingle(ctx, relays, nostr.Filter{
|
ie := sys.Pool.QuerySingle(ctx, relays, nostr.Filter{
|
||||||
Kinds: []int{10019},
|
Kinds: []nostr.Kind{10019},
|
||||||
Authors: []string{pk},
|
Authors: []nostr.PubKey{pk},
|
||||||
Limit: 1,
|
Limit: 1,
|
||||||
})
|
}, nostr.SubscriptionOptions{})
|
||||||
if ie != nil {
|
if ie != nil {
|
||||||
info.ParseEvent(ie.Event)
|
info.ParseEvent(ie.Event)
|
||||||
}
|
}
|
||||||
@@ -463,10 +460,7 @@ var wallet = &cli.Command{
|
|||||||
log("- saving kind:10019 event... ")
|
log("- saving kind:10019 event... ")
|
||||||
first := true
|
first := true
|
||||||
for res := range sys.Pool.PublishMany(ctx, relays, evt) {
|
for res := range sys.Pool.PublishMany(ctx, relays, evt) {
|
||||||
cleanUrl, ok := strings.CutPrefix(res.RelayURL, "wss://")
|
cleanUrl, _ := strings.CutPrefix(res.RelayURL, "wss://")
|
||||||
if !ok {
|
|
||||||
cleanUrl = res.RelayURL
|
|
||||||
}
|
|
||||||
|
|
||||||
if !first {
|
if !first {
|
||||||
log(", ")
|
log(", ")
|
||||||
@@ -474,9 +468,9 @@ var wallet = &cli.Command{
|
|||||||
first = false
|
first = false
|
||||||
|
|
||||||
if res.Error != nil {
|
if res.Error != nil {
|
||||||
log("%s: %s", color.RedString(cleanUrl), res.Error)
|
log("%s: %s", colors.errorf(cleanUrl), res.Error)
|
||||||
} else {
|
} else {
|
||||||
log("%s: ok", color.GreenString(cleanUrl))
|
log("%s: ok", colors.successf(cleanUrl))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,7 @@
|
|||||||
nak:
|
repository: https://github.com/fiatjaf/nak
|
||||||
cli:
|
assets:
|
||||||
name: nak
|
- nak-v\d+\.\d+\.\d+-darwin-arm64
|
||||||
summary: a command line tool for doing all things nostr
|
- nak-v\d+\.\d+\.\d+-linux-amd64
|
||||||
repository: https://github.com/fiatjaf/nak
|
- nak-v\d+\.\d+\.\d+-linux-arm64
|
||||||
artifacts:
|
remote_metadata:
|
||||||
nak-v%v-darwin-arm64:
|
- github
|
||||||
platforms: [darwin-arm64]
|
|
||||||
nak-v%v-darwin-amd64:
|
|
||||||
platforms: [darwin-x86_64]
|
|
||||||
nak-v%v-linux-arm64:
|
|
||||||
platforms: [linux-aarch64]
|
|
||||||
nak-v%v-linux-amd64:
|
|
||||||
platforms: [linux-x86_64]
|
|
||||||
|
|||||||
Reference in New Issue
Block a user