mirror of
https://github.com/fiatjaf/nak.git
synced 2025-12-08 16:48:51 +00:00
Compare commits
41 Commits
local-rela
...
v0.8.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d32739573 | ||
|
|
a187e448f2 | ||
|
|
9a9e96a829 | ||
|
|
4c6181d649 | ||
|
|
71b106fd45 | ||
|
|
40892c1228 | ||
|
|
847f8aaa69 | ||
|
|
134d1225d6 | ||
|
|
464766a836 | ||
|
|
ea53eca74f | ||
|
|
38ed370c59 | ||
|
|
5b04bc4859 | ||
|
|
2988c71ccb | ||
|
|
d7c0ff2bb7 | ||
|
|
43fe41df5d | ||
|
|
3215726417 | ||
|
|
a4886dc445 | ||
|
|
dae7eba8ca | ||
|
|
2b5f3355bc | ||
|
|
bd5ca27661 | ||
|
|
9d02301b2d | ||
|
|
9bbc87b27a | ||
|
|
88a07a3504 | ||
|
|
8a934cc76b | ||
|
|
e0c967efa9 | ||
|
|
36c32ae308 | ||
|
|
6d23509d8c | ||
|
|
29b6ecbafe | ||
|
|
11f37afa5b | ||
|
|
cf1694704e | ||
|
|
b3ef2c1289 | ||
|
|
cfdea699bc | ||
|
|
014c6bc11d | ||
|
|
0240866fa1 | ||
|
|
a4d9ceecfa | ||
|
|
56657d8aa9 | ||
|
|
ea7b88cfd7 | ||
|
|
2042b14578 | ||
|
|
9d43e66fac | ||
|
|
85e9610265 | ||
|
|
2edfa5cbea |
9
.github/workflows/release-cli.yml
vendored
9
.github/workflows/release-cli.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
goos: [linux, freebsd, darwin, windows]
|
||||
goarch: [amd64, arm64, riscv64]
|
||||
goarch: [arm, amd64, arm64, riscv64]
|
||||
exclude:
|
||||
- goarch: arm64
|
||||
goos: windows
|
||||
@@ -33,6 +33,12 @@ jobs:
|
||||
goos: windows
|
||||
- goarch: riscv64
|
||||
goos: darwin
|
||||
- goarch: arm
|
||||
goos: windows
|
||||
- goarch: arm
|
||||
goos: darwin
|
||||
- goarch: arm
|
||||
goos: freebsd
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: wangyoucao577/go-release-action@v1.40
|
||||
@@ -40,6 +46,7 @@ jobs:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
goos: ${{ matrix.goos }}
|
||||
goarch: ${{ matrix.goarch }}
|
||||
ldflags: -X main.version=${{ github.ref_name }}
|
||||
overwrite: true
|
||||
md5sum: false
|
||||
sha256sum: false
|
||||
|
||||
49
README.md
49
README.md
@@ -180,6 +180,55 @@ listening at [wss://relay.damus.io wss://nos.lol wss://relay.nsecbunker.com]:
|
||||
50000 events.jsonl
|
||||
```
|
||||
|
||||
### run a somewhat verbose local relay for test purposes
|
||||
```shell
|
||||
~> nak serve
|
||||
> relay running at ws://localhost:10547
|
||||
got request {"kinds":[1],"authors":["79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"],"since":1724082362}
|
||||
got event {"kind":1,"id":"e3c6bf630d6deea74c0ee2f7f7ba6da55a627498a32f1e72029229bb1810bce3","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1724082366,"tags":[],"content":"two","sig":"34261cf226c3fee2df24e55a89f43f5349c98a64bce46bdc46807b0329f334cea93e9e8bc285c1259a5684cf23f5e507c8e6dad47a31a6615d706b1130d09e69"}
|
||||
got event {"kind":1,"id":"0bbb397c8f87ae557650b9d6ee847292df8e530c458ffea1b24bdcb7bed0ec5e","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1724082369,"tags":[],"content":"three","sig":"aa1cb7d5f0f03f358fc4c0a4351a4f1c66e3a7627021b618601c56ba598b825b6d95d9c8720a4c60666a7eb21e17018cf326222f9f574a9396f2f2da7f007546"}
|
||||
• events stored: 2, subscriptions opened: 1
|
||||
got event {"kind":1,"id":"029ebff759dd54dbd01b929f879fea5802de297e1c3768ca16d9b97cc8bca38f","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1724082371,"tags":[],"content":"four","sig":"9816de517d87d4c3ede57c1c50e3c237486794241afadcd891e1acbba2c5e672286090e6ad3402b047d69bae8095bc4e20e57ac70d92386dfa26db216379330f"}
|
||||
got event {"kind":1,"id":"fe6489fa6fbb925be839377b9b7049d73be755dc2bdad97ff6dd9eecbf8b3a32","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1724082383,"tags":[],"content":"five","sig":"865ce5e32eead5bdb950ac1fbc55bc92dde26818ee3136634538ec42914de179a51e672c2d4269d4362176e5e8cd5e08e69b35b91c6c2af867e129b93d607635"}
|
||||
got request {"kinds":[30818]}
|
||||
• events stored: 4, subscriptions opened: 1
|
||||
```
|
||||
|
||||
### make an event with a PoW target
|
||||
```shell
|
||||
~> nak event -c 'hello getwired.app and labour.fiatjaf.com' --pow 24
|
||||
{"kind":1,"id":"0000009dcc7c62056eafdb41fac817379ec2becf0ce27c5fbe98d0735d968147","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1724160828,"tags":[["nonce","515504","24"]],"content":"hello getwired.app and labour.fiatjaf.com","sig":"7edb988065ccc12779fe99270945b212f3723838f315d76d5e90e9ffa27198f13fa556614295f518d968d55bab81878167d4162b3a7cf81a6b423c6761bd504c"}
|
||||
```
|
||||
|
||||
### make a nostr event signed with a key given as an environment variable
|
||||
|
||||
```shell
|
||||
~> export NOSTR_SECRET_KEY=ncryptsec1qggyy9vw0nclmw8ly9caz6aa7f85a4ufhsct64uva337pulsdw00n6twa2lzhzk2znzsyu60urx9s08lx00ke6ual3lszyn5an9zarm6s70lw5lj6dv3mj3f9p4tvp0we6qyz4gp420mapfmvqheuttv
|
||||
~> nak event -c 'it supports keys as hex, nsec or ncryptsec'
|
||||
type the password to decrypt your secret key: ********
|
||||
{"kind":1,"id":"5cbf3feb9a7d99c3ee2a88693a591caca1a8348fea427b3652c27f7a8a76af48","pubkey":"b00bcab55375d8c7b731dd9841f6d805ff1cf6fdc945e7326786deb5ddac6ce4","created_at":1724247924,"tags":[],"content":"it supports keys as hex, nsec or ncryptsec","sig":"fb3fd170bc10e5042322c7a05dd4bbd8ac9947b39026b8a7afd1ee02524e8e3aa1d9554e9c7b6181ca1b45cab01cd06643bdffa5ce678b475e6b185e1c14b085"}
|
||||
```
|
||||
|
||||
### download some helpful `jq` functions for dealing with nostr events
|
||||
```shell
|
||||
~> nak req -i 412f2d3e73acc312942c055ac2a695dc60bf58ff97e06689a8a79e97796c4cdb relay.westernbtc.com | jq -r .content > ~/.jq
|
||||
```
|
||||
|
||||
### watch a NIP-53 livestream (zap.stream etc)
|
||||
```shell
|
||||
~> # this requires the jq utils from the step above
|
||||
~> mpv $(nak fetch naddr1qqjxvvm9xscnsdtx95cxvcfk956rsvtx943rje3k95mx2dp389jnwwrp8ymxgqg4waehxw309aex2mrp0yhxgctdw4eju6t09upzpn6956apxcad0mfp8grcuugdysg44eepex68h50t73zcathmfs49qvzqqqrkvu7ed38k | jq -r 'tag_value("streaming")')
|
||||
~>
|
||||
~> # or without the utils
|
||||
~> mpv $(nak fetch naddr1qqjxvvm9xscnsdtx95cxvcfk956rsvtx943rje3k95mx2dp389jnwwrp8ymxgqg4waehxw309aex2mrp0yhxgctdw4eju6t09upzpn6956apxcad0mfp8grcuugdysg44eepex68h50t73zcathmfs49qvzqqqrkvu7ed38k | jq -r '.tags | map(select(.[0] == "streaming") | .[1])[0]')
|
||||
```
|
||||
|
||||
### download a NIP-35 torrent from an `nevent`
|
||||
```shell
|
||||
~> # this requires the jq utils from two steps above
|
||||
~> aria2c $(nak fetch nevent1qqsdsg6x7uujekac4ga7k7qa9q9sx8gqj7xzjf5w9us0dm0ghvf4ugspp4mhxue69uhkummn9ekx7mq6dw9y4 | jq -r '"magnet:?xt=urn:btih:\(tag_value("x"))&dn=\(tag_value("title"))&tr=http%3A%2F%2Ftracker.loadpeers.org%3A8080%2FxvRKfvAlnfuf5EfxTT5T0KIVPtbqAHnX%2Fannounce&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A6969%2Fannounce&tr=udp%3A%2F%2Fopen.stealth.si%3A80%2Fannounce&tr=udp%3A%2F%2Ftracker.torrent.eu.org%3A451%2Fannounce&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337&tr=\(tags("tracker") | map(.[1] | @uri) | join("&tr="))"')
|
||||
```
|
||||
|
||||
## contributing to this repository
|
||||
|
||||
Use NIP-34 to send your patches to `naddr1qqpkucttqy28wumn8ghj7un9d3shjtnwdaehgu3wvfnsz9nhwden5te0wfjkccte9ehx7um5wghxyctwvsq3gamnwvaz7tmjv4kxz7fwv3sk6atn9e5k7q3q80cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsxpqqqpmej2wctpn`.
|
||||
|
||||
12
bunker.go
12
bunker.go
@@ -29,7 +29,6 @@ var bunker = &cli.Command{
|
||||
Name: "sec",
|
||||
Usage: "secret key to sign the event, as hex or nsec",
|
||||
DefaultText: "the key '1'",
|
||||
Value: "0000000000000000000000000000000000000000000000000000000000000001",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "prompt-sec",
|
||||
@@ -51,7 +50,7 @@ var bunker = &cli.Command{
|
||||
qs := url.Values{}
|
||||
relayURLs := make([]string, 0, c.Args().Len())
|
||||
if relayUrls := c.Args().Slice(); len(relayUrls) > 0 {
|
||||
_, relays := connectToAllRelays(ctx, relayUrls, false)
|
||||
relays := connectToAllRelays(ctx, relayUrls, false)
|
||||
if len(relays) == 0 {
|
||||
log("failed to connect to any of the given relays.\n")
|
||||
os.Exit(3)
|
||||
@@ -144,9 +143,8 @@ var bunker = &cli.Command{
|
||||
printBunkerInfo()
|
||||
|
||||
// subscribe to relays
|
||||
pool := nostr.NewSimplePool(ctx)
|
||||
now := nostr.Now()
|
||||
events := pool.SubMany(ctx, relayURLs, nostr.Filters{
|
||||
events := sys.Pool.SubMany(ctx, relayURLs, nostr.Filters{
|
||||
{
|
||||
Kinds: []int{nostr.KindNostrConnect},
|
||||
Tags: nostr.TagMap{"p": []string{pubkey}},
|
||||
@@ -185,7 +183,7 @@ var bunker = &cli.Command{
|
||||
cancelPreviousBunkerInfoPrint() // this prevents us from printing a million bunker info blocks
|
||||
|
||||
// handle the NIP-46 request event
|
||||
req, resp, eventResponse, err := signer.HandleRequest(ie.Event)
|
||||
req, resp, eventResponse, err := signer.HandleRequest(ctx, ie.Event)
|
||||
if err != nil {
|
||||
log("< failed to handle request from %s: %s\n", ie.Event.PubKey, err.Error())
|
||||
continue
|
||||
@@ -199,7 +197,7 @@ var bunker = &cli.Command{
|
||||
handlerWg.Add(len(relayURLs))
|
||||
for _, relayURL := range relayURLs {
|
||||
go func(relayURL string) {
|
||||
if relay, _ := pool.EnsureRelay(relayURL); relay != nil {
|
||||
if relay, _ := sys.Pool.EnsureRelay(relayURL); relay != nil {
|
||||
err := relay.Publish(ctx, eventResponse)
|
||||
printLock.Lock()
|
||||
if err == nil {
|
||||
@@ -224,7 +222,7 @@ var bunker = &cli.Command{
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case <-time.After(time.Minute * 5):
|
||||
fmt.Fprintf(os.Stderr, "\n")
|
||||
log("\n")
|
||||
printBunkerInfo()
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"github.com/fiatjaf/cli/v3"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/nbd-wtf/go-nostr/nip19"
|
||||
sdk "github.com/nbd-wtf/nostr-sdk"
|
||||
"github.com/nbd-wtf/go-nostr/sdk"
|
||||
)
|
||||
|
||||
var decode = &cli.Command{
|
||||
@@ -59,8 +59,11 @@ var decode = &cli.Command{
|
||||
} else if pp := sdk.InputToProfile(ctx, input); pp != nil {
|
||||
decodeResult = DecodeResult{ProfilePointer: pp}
|
||||
} else if prefix, value, err := nip19.Decode(input); err == nil && prefix == "naddr" {
|
||||
ep := value.(nostr.EntityPointer)
|
||||
decodeResult = DecodeResult{EntityPointer: &ep}
|
||||
if ep, ok := value.(nostr.EntityPointer); ok {
|
||||
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))
|
||||
|
||||
140
encrypt_decrypt.go
Normal file
140
encrypt_decrypt.go
Normal file
@@ -0,0 +1,140 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/fiatjaf/cli/v3"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/nbd-wtf/go-nostr/nip04"
|
||||
)
|
||||
|
||||
var encrypt = &cli.Command{
|
||||
Name: "encrypt",
|
||||
Usage: "encrypts a string with nip44 (or nip04 if specified using a flag) and returns the resulting ciphertext as base64",
|
||||
ArgsUsage: "[plaintext string]",
|
||||
DisableSliceFlagSeparator: true,
|
||||
Flags: append(
|
||||
defaultKeyFlags,
|
||||
&cli.StringFlag{
|
||||
Name: "recipient-pubkey",
|
||||
Aliases: []string{"p", "tgt", "target", "pubkey"},
|
||||
Required: true,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "nip04",
|
||||
Usage: "use nip04 encryption instead of nip44",
|
||||
},
|
||||
),
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
target := c.String("recipient-pubkey")
|
||||
if !nostr.IsValidPublicKey(target) {
|
||||
return fmt.Errorf("target %s is not a valid public key", target)
|
||||
}
|
||||
|
||||
plaintext := c.Args().First()
|
||||
|
||||
if c.Bool("nip04") {
|
||||
sec, bunker, err := gatherSecretKeyOrBunkerFromArguments(ctx, c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if bunker != nil {
|
||||
ciphertext, err := bunker.NIP04Encrypt(ctx, target, plaintext)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stdout(ciphertext)
|
||||
} else {
|
||||
ss, err := nip04.ComputeSharedSecret(target, sec)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to compute nip04 shared secret: %w", err)
|
||||
}
|
||||
ciphertext, err := nip04.Encrypt(plaintext, ss)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to encrypt as nip04: %w", err)
|
||||
}
|
||||
stdout(ciphertext)
|
||||
}
|
||||
} else {
|
||||
kr, _, err := gatherKeyerFromArguments(ctx, c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := kr.Encrypt(ctx, plaintext, target)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to encrypt: %w", err)
|
||||
}
|
||||
stdout(res)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var decrypt = &cli.Command{
|
||||
Name: "decrypt",
|
||||
Usage: "decrypts a base64 nip44 ciphertext (or nip04 if specified using a flag) and returns the resulting plaintext",
|
||||
ArgsUsage: "[ciphertext base64]",
|
||||
DisableSliceFlagSeparator: true,
|
||||
Flags: append(
|
||||
defaultKeyFlags,
|
||||
&cli.StringFlag{
|
||||
Name: "sender-pubkey",
|
||||
Aliases: []string{"p", "src", "source", "pubkey"},
|
||||
Required: true,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "nip04",
|
||||
Usage: "use nip04 encryption instead of nip44",
|
||||
},
|
||||
),
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
source := c.String("sender-pubkey")
|
||||
if !nostr.IsValidPublicKey(source) {
|
||||
return fmt.Errorf("source %s is not a valid public key", source)
|
||||
}
|
||||
|
||||
ciphertext := c.Args().First()
|
||||
|
||||
if c.Bool("nip04") {
|
||||
sec, bunker, err := gatherSecretKeyOrBunkerFromArguments(ctx, c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if bunker != nil {
|
||||
plaintext, err := bunker.NIP04Decrypt(ctx, source, ciphertext)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stdout(plaintext)
|
||||
} else {
|
||||
ss, err := nip04.ComputeSharedSecret(source, sec)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to compute nip04 shared secret: %w", err)
|
||||
}
|
||||
plaintext, err := nip04.Decrypt(ciphertext, ss)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to encrypt as nip04: %w", err)
|
||||
}
|
||||
stdout(plaintext)
|
||||
}
|
||||
} else {
|
||||
kr, _, err := gatherKeyerFromArguments(ctx, c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := kr.Decrypt(ctx, ciphertext, source)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to encrypt: %w", err)
|
||||
}
|
||||
stdout(res)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
130
event.go
130
event.go
@@ -2,7 +2,6 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
@@ -11,11 +10,16 @@ import (
|
||||
"github.com/fiatjaf/cli/v3"
|
||||
"github.com/mailru/easyjson"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/nbd-wtf/go-nostr/nip13"
|
||||
"github.com/nbd-wtf/go-nostr/nip19"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
const CATEGORY_EVENT_FIELDS = "EVENT FIELDS"
|
||||
const (
|
||||
CATEGORY_EVENT_FIELDS = "EVENT FIELDS"
|
||||
CATEGORY_SIGNER = "SIGNER OPTIONS"
|
||||
CATEGORY_EXTRAS = "EXTRAS"
|
||||
)
|
||||
|
||||
var event = &cli.Command{
|
||||
Name: "event",
|
||||
@@ -32,26 +36,7 @@ example:
|
||||
echo '{"id":"a889df6a387419ff204305f4c2d296ee328c3cd4f8b62f205648a541b4554dfb","pubkey":"c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5","created_at":1698623783,"kind":1,"tags":[],"content":"hello from the nostr army knife","sig":"84876e1ee3e726da84e5d195eb79358b2b3eaa4d9bd38456fde3e8a2af3f1cd4cda23f23fda454869975b3688797d4c66e12f4c51c1b43c6d2997c5e61865661"}' | nak event wss://offchain.pub
|
||||
echo '{"tags": [["t", "spam"]]}' | nak event -c 'this is spam'`,
|
||||
DisableSliceFlagSeparator: true,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "sec",
|
||||
Usage: "secret key to sign the event, as nsec, ncryptsec or hex",
|
||||
DefaultText: "the key '1'",
|
||||
Value: "0000000000000000000000000000000000000000000000000000000000000001",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "prompt-sec",
|
||||
Usage: "prompt the user to paste a hex or nsec with which to sign the event",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "connect",
|
||||
Usage: "sign event using NIP-46, expects a bunker://... URL",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "connect-as",
|
||||
Usage: "private key to when communicating with the bunker given on --connect",
|
||||
DefaultText: "a random key",
|
||||
},
|
||||
Flags: append(defaultKeyFlags,
|
||||
// ~ these args are only for the convoluted musig2 signing process
|
||||
// they will be generally copy-shared-pasted across some manual coordination method between participants
|
||||
&cli.UintFlag{
|
||||
@@ -59,6 +44,7 @@ example:
|
||||
Usage: "number of signers to use for musig2",
|
||||
Value: 1,
|
||||
DefaultText: "1 -- i.e. do not use musig2 at all",
|
||||
Category: CATEGORY_SIGNER,
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "musig-pubkey",
|
||||
@@ -77,17 +63,25 @@ example:
|
||||
Hidden: true,
|
||||
},
|
||||
// ~~~
|
||||
&cli.BoolFlag{
|
||||
Name: "envelope",
|
||||
Usage: "print the event enveloped in a [\"EVENT\", ...] message ready to be sent to a relay",
|
||||
&cli.UintFlag{
|
||||
Name: "pow",
|
||||
Usage: "NIP-13 difficulty to target when doing hash work on the event id",
|
||||
Category: CATEGORY_EXTRAS,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "auth",
|
||||
Usage: "always perform NIP-42 \"AUTH\" when facing an \"auth-required: \" rejection and try again",
|
||||
Name: "envelope",
|
||||
Usage: "print the event enveloped in a [\"EVENT\", ...] message ready to be sent to a relay",
|
||||
Category: CATEGORY_EXTRAS,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "nevent",
|
||||
Usage: "print the nevent code (to stderr) after the event is published",
|
||||
Name: "auth",
|
||||
Usage: "always perform NIP-42 \"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.UintFlag{
|
||||
Name: "kind",
|
||||
@@ -108,7 +102,7 @@ example:
|
||||
&cli.StringSliceFlag{
|
||||
Name: "tag",
|
||||
Aliases: []string{"t"},
|
||||
Usage: "sets a tag field on the event, takes a value like -t e=<id>",
|
||||
Usage: "sets a tag field on the event, takes a value like -t e=<id> or -t sometag=\"value one;value two;value three\"",
|
||||
Category: CATEGORY_EVENT_FIELDS,
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
@@ -134,13 +128,13 @@ example:
|
||||
Value: nostr.Now(),
|
||||
Category: CATEGORY_EVENT_FIELDS,
|
||||
},
|
||||
},
|
||||
),
|
||||
ArgsUsage: "[relay...]",
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
// try to connect to the relays here
|
||||
var relays []*nostr.Relay
|
||||
if relayUrls := c.Args().Slice(); len(relayUrls) > 0 {
|
||||
_, relays = connectToAllRelays(ctx, relayUrls, false)
|
||||
relays = connectToAllRelays(ctx, relayUrls, false)
|
||||
if len(relays) == 0 {
|
||||
log("failed to connect to any of the given relays.\n")
|
||||
os.Exit(3)
|
||||
@@ -153,7 +147,7 @@ example:
|
||||
}
|
||||
}()
|
||||
|
||||
sec, bunker, err := gatherSecretKeyOrBunkerFromArguments(ctx, c)
|
||||
kr, sec, err := gatherKeyerFromArguments(ctx, c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -185,16 +179,17 @@ example:
|
||||
mustRehashAndResign = true
|
||||
}
|
||||
|
||||
if content := c.String("content"); content != "" {
|
||||
evt.Content = content
|
||||
if c.IsSet("content") {
|
||||
evt.Content = c.String("content")
|
||||
mustRehashAndResign = true
|
||||
} else if evt.Content == "" && evt.Kind == 1 {
|
||||
evt.Content = "hello from the nostr army knife"
|
||||
mustRehashAndResign = true
|
||||
}
|
||||
|
||||
tags := make(nostr.Tags, 0, 5)
|
||||
for _, tagFlag := range c.StringSlice("tag") {
|
||||
tagFlags := c.StringSlice("tag")
|
||||
tags := make(nostr.Tags, 0, len(tagFlags)+2)
|
||||
for _, tagFlag := range tagFlags {
|
||||
// tags are in the format key=value
|
||||
tagName, tagValue, found := strings.Cut(tagFlag, "=")
|
||||
tag := []string{tagName}
|
||||
@@ -203,20 +198,17 @@ example:
|
||||
tagValues := strings.Split(tagValue, ";")
|
||||
tag = append(tag, tagValues...)
|
||||
}
|
||||
tags = tags.AppendUnique(tag)
|
||||
tags = append(tags, tag)
|
||||
}
|
||||
|
||||
for _, etag := range c.StringSlice("e") {
|
||||
tags = tags.AppendUnique([]string{"e", etag})
|
||||
mustRehashAndResign = true
|
||||
}
|
||||
for _, ptag := range c.StringSlice("p") {
|
||||
tags = tags.AppendUnique([]string{"p", ptag})
|
||||
mustRehashAndResign = true
|
||||
}
|
||||
for _, dtag := range c.StringSlice("d") {
|
||||
tags = tags.AppendUnique([]string{"d", dtag})
|
||||
mustRehashAndResign = true
|
||||
}
|
||||
if len(tags) > 0 {
|
||||
for _, tag := range tags {
|
||||
@@ -233,12 +225,35 @@ example:
|
||||
mustRehashAndResign = true
|
||||
}
|
||||
|
||||
if evt.Sig == "" || mustRehashAndResign {
|
||||
if bunker != nil {
|
||||
if err := bunker.SignEvent(ctx, &evt); err != nil {
|
||||
return fmt.Errorf("failed to sign with bunker: %w", err)
|
||||
if c.IsSet("musig") || c.IsSet("sec") || c.IsSet("prompt-sec") {
|
||||
mustRehashAndResign = true
|
||||
}
|
||||
|
||||
if difficulty := c.Uint("pow"); difficulty > 0 {
|
||||
// before doing pow we need the pubkey
|
||||
if numSigners := c.Uint("musig"); numSigners > 1 {
|
||||
pubkeys := c.StringSlice("musig-pubkey")
|
||||
if int(numSigners) != len(pubkeys) {
|
||||
return fmt.Errorf("when doing a pow with musig we must know all signer pubkeys upfront")
|
||||
}
|
||||
} else if numSigners := c.Uint("musig"); numSigners > 1 && sec != "" {
|
||||
evt.PubKey, err = getMusigAggregatedKey(ctx, pubkeys)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
evt.PubKey, _ = kr.GetPublicKey(ctx)
|
||||
}
|
||||
|
||||
// try to generate work with this difficulty -- runs forever
|
||||
nonceTag, _ := nip13.DoWork(ctx, evt, int(difficulty))
|
||||
evt.Tags = append(evt.Tags, nonceTag)
|
||||
|
||||
mustRehashAndResign = true
|
||||
}
|
||||
|
||||
if evt.Sig == "" || mustRehashAndResign {
|
||||
if numSigners := c.Uint("musig"); numSigners > 1 {
|
||||
// must do musig
|
||||
pubkeys := c.StringSlice("musig-pubkey")
|
||||
secNonce := c.String("musig-nonce-secret")
|
||||
pubNonces := c.StringSlice("musig-nonce")
|
||||
@@ -253,7 +268,7 @@ example:
|
||||
// instructions for what to do should have been printed by the performMusig() function
|
||||
return nil
|
||||
}
|
||||
} else if err := evt.Sign(sec); err != nil {
|
||||
} else if err := kr.SignEvent(ctx, &evt); err != nil {
|
||||
return fmt.Errorf("error signing with provided key: %w", err)
|
||||
}
|
||||
}
|
||||
@@ -261,7 +276,7 @@ example:
|
||||
// print event as json
|
||||
var result string
|
||||
if c.Bool("envelope") {
|
||||
j, _ := json.Marshal(nostr.EventEnvelope{Event: evt})
|
||||
j, _ := easyjson.Marshal(nostr.EventEnvelope{Event: evt})
|
||||
result = string(j)
|
||||
} else {
|
||||
j, _ := easyjson.Marshal(&evt)
|
||||
@@ -288,23 +303,12 @@ example:
|
||||
}
|
||||
|
||||
// error publishing
|
||||
if strings.HasPrefix(err.Error(), "msg: auth-required:") && (sec != "" || bunker != nil) && doAuth {
|
||||
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
|
||||
var pk string
|
||||
if bunker != nil {
|
||||
pk, err = bunker.GetPublicKey(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get public key from bunker: %w", err)
|
||||
}
|
||||
} else {
|
||||
pk, _ = nostr.GetPublicKey(sec)
|
||||
}
|
||||
pk, _ := kr.GetPublicKey(ctx)
|
||||
log("performing auth as %s... ", pk)
|
||||
if err := relay.Auth(ctx, func(evt *nostr.Event) error {
|
||||
if bunker != nil {
|
||||
return bunker.SignEvent(ctx, evt)
|
||||
}
|
||||
return evt.Sign(sec)
|
||||
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
|
||||
|
||||
@@ -116,3 +116,9 @@ func ExampleReqWithFlagsAfter3() {
|
||||
// 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"}
|
||||
}
|
||||
|
||||
120
fetch.go
120
fetch.go
@@ -2,33 +2,32 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/fiatjaf/cli/v3"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/nbd-wtf/go-nostr/nip05"
|
||||
"github.com/nbd-wtf/go-nostr/nip19"
|
||||
sdk "github.com/nbd-wtf/nostr-sdk"
|
||||
)
|
||||
|
||||
var fetch = &cli.Command{
|
||||
Name: "fetch",
|
||||
Usage: "fetches events related to the given nip19 code from the included relay hints or the author's NIP-65 relays.",
|
||||
Usage: "fetches events related to the given nip19 or nip05 code from the included relay hints or the author's NIP-65 relays.",
|
||||
Description: `example usage:
|
||||
nak fetch nevent1qqsxrwm0hd3s3fddh4jc2574z3xzufq6qwuyz2rvv3n087zvym3dpaqprpmhxue69uhhqatzd35kxtnjv4kxz7tfdenju6t0xpnej4
|
||||
echo npub1h8spmtw9m2huyv6v2j2qd5zv956z2zdugl6mgx02f2upffwpm3nqv0j4ps | nak fetch --relay wss://relay.nostr.band`,
|
||||
DisableSliceFlagSeparator: true,
|
||||
Flags: []cli.Flag{
|
||||
Flags: append(reqFilterFlags,
|
||||
&cli.StringSliceFlag{
|
||||
Name: "relay",
|
||||
Aliases: []string{"r"},
|
||||
Usage: "also use these relays to fetch from",
|
||||
},
|
||||
},
|
||||
ArgsUsage: "[nip19code]",
|
||||
),
|
||||
ArgsUsage: "[nip05_or_nip19_code]",
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
pool := nostr.NewSimplePool(ctx)
|
||||
|
||||
defer func() {
|
||||
pool.Relays.Range(func(_ string, relay *nostr.Relay) bool {
|
||||
sys.Pool.Relays.Range(func(_ string, relay *nostr.Relay) bool {
|
||||
relay.Close()
|
||||
return true
|
||||
})
|
||||
@@ -36,64 +35,81 @@ var fetch = &cli.Command{
|
||||
|
||||
for code := range getStdinLinesOrArguments(c.Args()) {
|
||||
filter := nostr.Filter{}
|
||||
|
||||
prefix, value, err := nip19.Decode(code)
|
||||
if err != nil {
|
||||
ctx = lineProcessingError(ctx, "failed to decode: %s", err)
|
||||
continue
|
||||
}
|
||||
|
||||
relays := c.StringSlice("relay")
|
||||
if err := normalizeAndValidateRelayURLs(relays); err != nil {
|
||||
return err
|
||||
}
|
||||
var authorHint string
|
||||
relays := c.StringSlice("relay")
|
||||
|
||||
switch prefix {
|
||||
case "nevent":
|
||||
v := value.(nostr.EventPointer)
|
||||
filter.IDs = append(filter.IDs, v.ID)
|
||||
if v.Author != "" {
|
||||
authorHint = v.Author
|
||||
if nip05.IsValidIdentifier(code) {
|
||||
pp, err := nip05.QueryIdentifier(ctx, code)
|
||||
if err != nil {
|
||||
ctx = lineProcessingError(ctx, "failed to fetch nip05: %s", err)
|
||||
continue
|
||||
}
|
||||
authorHint = pp.PublicKey
|
||||
relays = append(relays, pp.Relays...)
|
||||
filter.Authors = append(filter.Authors, pp.PublicKey)
|
||||
} else {
|
||||
prefix, value, err := nip19.Decode(code)
|
||||
if err != nil {
|
||||
ctx = lineProcessingError(ctx, "failed to decode: %s", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if err := normalizeAndValidateRelayURLs(relays); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch prefix {
|
||||
case "nevent":
|
||||
v := value.(nostr.EventPointer)
|
||||
filter.IDs = append(filter.IDs, v.ID)
|
||||
if v.Author != "" {
|
||||
authorHint = v.Author
|
||||
}
|
||||
relays = append(relays, v.Relays...)
|
||||
case "note":
|
||||
filter.IDs = append(filter.IDs, value.(string))
|
||||
case "naddr":
|
||||
v := value.(nostr.EntityPointer)
|
||||
filter.Kinds = []int{v.Kind}
|
||||
filter.Tags = nostr.TagMap{"d": []string{v.Identifier}}
|
||||
filter.Authors = append(filter.Authors, v.PublicKey)
|
||||
authorHint = v.PublicKey
|
||||
relays = append(relays, v.Relays...)
|
||||
case "nprofile":
|
||||
v := value.(nostr.ProfilePointer)
|
||||
filter.Authors = append(filter.Authors, v.PublicKey)
|
||||
authorHint = v.PublicKey
|
||||
relays = append(relays, v.Relays...)
|
||||
case "npub":
|
||||
v := value.(string)
|
||||
filter.Authors = append(filter.Authors, v)
|
||||
authorHint = v
|
||||
default:
|
||||
return fmt.Errorf("unexpected prefix %s", prefix)
|
||||
}
|
||||
relays = append(relays, v.Relays...)
|
||||
case "naddr":
|
||||
v := value.(nostr.EntityPointer)
|
||||
filter.Tags = nostr.TagMap{"d": []string{v.Identifier}}
|
||||
filter.Kinds = append(filter.Kinds, v.Kind)
|
||||
filter.Authors = append(filter.Authors, v.PublicKey)
|
||||
authorHint = v.PublicKey
|
||||
relays = append(relays, v.Relays...)
|
||||
case "nprofile":
|
||||
v := value.(nostr.ProfilePointer)
|
||||
filter.Authors = append(filter.Authors, v.PublicKey)
|
||||
filter.Kinds = append(filter.Kinds, 0)
|
||||
authorHint = v.PublicKey
|
||||
relays = append(relays, v.Relays...)
|
||||
case "npub":
|
||||
v := value.(string)
|
||||
filter.Authors = append(filter.Authors, v)
|
||||
filter.Kinds = append(filter.Kinds, 0)
|
||||
authorHint = v
|
||||
}
|
||||
|
||||
if authorHint != "" {
|
||||
relayList := sdk.FetchRelaysForPubkey(ctx, pool, authorHint,
|
||||
"wss://purplepag.es", "wss://relay.damus.io", "wss://relay.noswhere.com",
|
||||
"wss://nos.lol", "wss://public.relaying.io", "wss://relay.nostr.band")
|
||||
for _, relayListItem := range relayList {
|
||||
if relayListItem.Outbox {
|
||||
relays = append(relays, relayListItem.URL)
|
||||
}
|
||||
relays := sys.FetchOutboxRelays(ctx, authorHint, 3)
|
||||
for _, url := range relays {
|
||||
relays = append(relays, url)
|
||||
}
|
||||
}
|
||||
|
||||
if len(filter.Authors) > 0 && len(filter.Kinds) == 0 {
|
||||
filter.Kinds = append(filter.Kinds, 0)
|
||||
}
|
||||
|
||||
if err := applyFlagsToFilter(c, &filter); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(relays) == 0 {
|
||||
ctx = lineProcessingError(ctx, "no relay hints found")
|
||||
continue
|
||||
}
|
||||
|
||||
for ie := range pool.SubManyEose(ctx, relays, nostr.Filters{filter}) {
|
||||
for ie := range sys.Pool.SubManyEose(ctx, relays, nostr.Filters{filter}) {
|
||||
stdout(ie.Event)
|
||||
}
|
||||
}
|
||||
|
||||
44
go.mod
44
go.mod
@@ -1,47 +1,61 @@
|
||||
module github.com/fiatjaf/nak
|
||||
|
||||
go 1.22
|
||||
|
||||
toolchain go1.22.4
|
||||
go 1.23.1
|
||||
|
||||
require (
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.3.3
|
||||
github.com/bep/debounce v1.2.1
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.3.4
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0
|
||||
github.com/fatih/color v1.16.0
|
||||
github.com/fiatjaf/cli/v3 v3.0.0-20240723181502-e7dd498b16ae
|
||||
github.com/fiatjaf/eventstore v0.12.0
|
||||
github.com/fiatjaf/khatru v0.10.0
|
||||
github.com/mailru/easyjson v0.7.7
|
||||
github.com/markusmobius/go-dateparser v1.2.3
|
||||
github.com/nbd-wtf/go-nostr v0.34.2
|
||||
github.com/nbd-wtf/nostr-sdk v0.0.5
|
||||
golang.org/x/exp v0.0.0-20240707233637-46b078467d37
|
||||
github.com/nbd-wtf/go-nostr v0.42.2
|
||||
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.0.5 // indirect
|
||||
github.com/btcsuite/btcd/btcutil v1.1.3 // indirect
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect
|
||||
github.com/cespare/xxhash v1.1.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/chzyer/logex v1.1.10 // indirect
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect
|
||||
github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect
|
||||
github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect
|
||||
github.com/dgraph-io/ristretto v1.0.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/elliotchance/pie/v2 v2.7.0 // indirect
|
||||
github.com/fiatjaf/eventstore v0.2.16 // indirect
|
||||
github.com/fasthttp/websocket v1.5.7 // indirect
|
||||
github.com/gobwas/httphead v0.1.0 // indirect
|
||||
github.com/gobwas/pool v0.2.1 // indirect
|
||||
github.com/gobwas/ws v1.4.0 // indirect
|
||||
github.com/graph-gophers/dataloader/v7 v7.1.0 // indirect
|
||||
github.com/greatroar/blobloom v0.8.0 // indirect
|
||||
github.com/hablullah/go-hijri v1.0.2 // indirect
|
||||
github.com/hablullah/go-juliandays v1.0.0 // indirect
|
||||
github.com/jalaali/go-jalaali v0.0.0-20210801064154-80525e88d958 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/klauspost/compress v1.17.10 // indirect
|
||||
github.com/magefile/mage v1.14.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/puzpuzpuz/xsync/v3 v3.1.0 // indirect
|
||||
github.com/tetratelabs/wazero v1.2.1 // indirect
|
||||
github.com/tidwall/gjson v1.17.1 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/puzpuzpuz/xsync/v3 v3.4.0 // indirect
|
||||
github.com/rs/cors v1.7.0 // indirect
|
||||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect
|
||||
github.com/tetratelabs/wazero v1.8.0 // indirect
|
||||
github.com/tidwall/gjson v1.17.3 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.51.0 // indirect
|
||||
github.com/wasilibs/go-re2 v1.3.0 // indirect
|
||||
golang.org/x/crypto v0.21.0 // indirect
|
||||
golang.org/x/sys v0.22.0 // indirect
|
||||
golang.org/x/text v0.16.0 // indirect
|
||||
golang.org/x/crypto v0.28.0 // indirect
|
||||
golang.org/x/net v0.30.0 // indirect
|
||||
golang.org/x/sys v0.26.0 // indirect
|
||||
golang.org/x/text v0.19.0 // indirect
|
||||
)
|
||||
|
||||
91
go.sum
91
go.sum
@@ -1,11 +1,17 @@
|
||||
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
|
||||
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
|
||||
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
|
||||
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
|
||||
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
|
||||
github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M=
|
||||
github.com/btcsuite/btcd v0.23.0/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY=
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA=
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE=
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.3.3 h1:6+iXlDKE8RMtKsvK0gshlXIuPbyWM/h84Ensb7o3sC0=
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.3.3/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04=
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.3.4 h1:3EJjcN70HCu/mwqlUsGK8GcNVyLVxFDlWurTXGPFfiQ=
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.3.4/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04=
|
||||
github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A=
|
||||
github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE=
|
||||
github.com/btcsuite/btcd/btcutil v1.1.3 h1:xfbtw8lwpp0G6NwSHb+UE67ryTFHJAiNuipusjXSohQ=
|
||||
@@ -23,6 +29,10 @@ github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku
|
||||
github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
|
||||
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
|
||||
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
|
||||
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
|
||||
@@ -34,20 +44,30 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
|
||||
github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218=
|
||||
github.com/dgraph-io/ristretto v1.0.0 h1:SYG07bONKMlFDUYu5pEu3DGAh8c2OFNzKm6G9J4Si84=
|
||||
github.com/dgraph-io/ristretto v1.0.0/go.mod h1:jTi2FiYEhQ1NsMmA7DeBykizjOuY88NhKBkepyu1jPc=
|
||||
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y=
|
||||
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/elliotchance/pie/v2 v2.7.0 h1:FqoIKg4uj0G/CrLGuMS9ejnFKa92lxE1dEgBD3pShXg=
|
||||
github.com/elliotchance/pie/v2 v2.7.0/go.mod h1:18t0dgGFH006g4eVdDtWfgFZPQEgl10IoEO8YWEq3Og=
|
||||
github.com/fasthttp/websocket v1.5.7 h1:0a6o2OfeATvtGgoMKleURhLT6JqWPg7fYfWnH4KHau4=
|
||||
github.com/fasthttp/websocket v1.5.7/go.mod h1:bC4fxSono9czeXHQUVKxsC0sNjbm7lPJR04GDFqClfU=
|
||||
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/fiatjaf/cli/v3 v3.0.0-20240723181502-e7dd498b16ae h1:0B/1dU3YECIbPoBIRTQ4c0scZCNz9TVHtQpiODGrTTo=
|
||||
github.com/fiatjaf/cli/v3 v3.0.0-20240723181502-e7dd498b16ae/go.mod h1:aAWPO4bixZZxPtOnH6K3q4GbQ0jftUNDW9Oa861IRew=
|
||||
github.com/fiatjaf/eventstore v0.2.16 h1:NR64mnyUT5nJR8Sj2AwJTd1Hqs5kKJcCFO21ggUkvWg=
|
||||
github.com/fiatjaf/eventstore v0.2.16/go.mod h1:rUc1KhVufVmC+HUOiuPweGAcvG6lEOQCkRCn2Xn5VRA=
|
||||
github.com/fiatjaf/eventstore v0.12.0 h1:ZdL+dZkIgBgIp5A3+3XLdPg/uucv5Tiws6DHzNfZG4M=
|
||||
github.com/fiatjaf/eventstore v0.12.0/go.mod h1:PxeYbZ3MsH0XLobANsp6c0cJjJYkfmBJ3TwrplFy/08=
|
||||
github.com/fiatjaf/khatru v0.10.0 h1:f43om33RZfkIAIW9vhHelFJXp8XCij/Jh30AmJ0AVF8=
|
||||
github.com/fiatjaf/khatru v0.10.0/go.mod h1:iCLz0bPcFSBHJrY2kZ1lji5IrIsv9YFwRS7aaXIPv+o=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
|
||||
@@ -67,6 +87,10 @@ github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/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/greatroar/blobloom v0.8.0 h1:I9RlEkfqK9/6f1v9mFmDYegDQ/x0mISCpiNpAm23Pt4=
|
||||
github.com/greatroar/blobloom v0.8.0/go.mod h1:mjMJ1hh1wjGVfr93QIHJ6FfDNVrA0IELv8OvMHJxHKs=
|
||||
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-juliandays v1.0.0 h1:A8YM7wIj16SzlKT0SRJc9CD29iiaUzpBLzh5hr0/5p0=
|
||||
@@ -80,6 +104,8 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
|
||||
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
|
||||
github.com/klauspost/compress v1.17.10 h1:oXAz+Vh0PMUvJczoi+flxpnBEPxoER1IaAnU/NMPtT0=
|
||||
github.com/klauspost/compress v1.17.10/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
||||
github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo=
|
||||
github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
@@ -91,10 +117,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.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/nbd-wtf/go-nostr v0.34.2 h1:9b4qZ29DhQf9xEWN8/7zfDD868r1jFbpjrR3c+BHc+E=
|
||||
github.com/nbd-wtf/go-nostr v0.34.2/go.mod h1:NZQkxl96ggbO8rvDpVjcsojJqKTPwqhP4i82O7K5DJs=
|
||||
github.com/nbd-wtf/nostr-sdk v0.0.5 h1:rec+FcDizDVO0W25PX0lgYMXvP7zNNOgI3Fu9UCm4BY=
|
||||
github.com/nbd-wtf/nostr-sdk v0.0.5/go.mod h1:iJJsikesCGLNFZ9dLqhLPDzdt924EagUmdQxT3w2Lmk=
|
||||
github.com/nbd-wtf/go-nostr v0.42.2 h1:X8vpfLutvmyxqjsroKPHdIyPliNa6sYD8+CA0kDVySw=
|
||||
github.com/nbd-wtf/go-nostr v0.42.2/go.mod h1:FBa4FBJO7NuANvkeKSlrf0BIyxGufmrUbuelr6Q4Ick=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
@@ -104,24 +128,39 @@ github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5
|
||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.1.0 h1:EewKT7/LNac5SLiEblJeUu8z5eERHrmRLnMQL2d7qX4=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.1.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.4.0 h1:DuVBAdXuGFHv8adVXjWWZ63pJq+NRXOWVXlKDBZ+mJ4=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.4.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||
github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik=
|
||||
github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
|
||||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk=
|
||||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
|
||||
github.com/tetratelabs/wazero v1.2.1 h1:J4X2hrGzJvt+wqltuvcSjHQ7ujQxA9gb6PeMs4qlUWs=
|
||||
github.com/tetratelabs/wazero v1.2.1/go.mod h1:wYx2gNRg8/WihJfSDxA1TIL8H+GkfLYm+bIfbblu9VQ=
|
||||
github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U=
|
||||
github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tetratelabs/wazero v1.8.0 h1:iEKu0d4c2Pd+QSRieYbnQC9yiFlMS9D+Jr0LsRmcF4g=
|
||||
github.com/tetratelabs/wazero v1.8.0/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs=
|
||||
github.com/tidwall/gjson v1.17.3 h1:bwWLZU7icoKRG+C+0PNwIKC6FCJO/Q3p2pZvuP0jN94=
|
||||
github.com/tidwall/gjson v1.17.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
|
||||
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
|
||||
github.com/wasilibs/go-re2 v1.3.0 h1:LFhBNzoStM3wMie6rN2slD1cuYH2CGiHpvNL3UtcsMw=
|
||||
github.com/wasilibs/go-re2 v1.3.0/go.mod h1:AafrCXVvGRJJOImMajgJ2M7rVmWyisVK7sFshbxnVrg=
|
||||
github.com/wasilibs/nottinygc v0.4.0 h1:h1TJMihMC4neN6Zq+WKpLxgd9xCFMw7O9ETLwY2exJQ=
|
||||
@@ -129,17 +168,17 @@ github.com/wasilibs/nottinygc v0.4.0/go.mod h1:oDcIotskuYNMpqMF23l7Z8uzD4TC0WXHK
|
||||
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
|
||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
golang.org/x/exp v0.0.0-20240707233637-46b078467d37 h1:uLDX+AfeFCct3a2C7uIWBKMJIR3CJMhcgfrUAqjRK6w=
|
||||
golang.org/x/exp v0.0.0-20240707233637-46b078467d37/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
|
||||
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
|
||||
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
|
||||
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk=
|
||||
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY=
|
||||
golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
|
||||
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
|
||||
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -152,13 +191,13 @@ golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
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.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
|
||||
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
||||
131
helpers.go
131
helpers.go
@@ -3,7 +3,6 @@ package main
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net/url"
|
||||
@@ -11,15 +10,20 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/chzyer/readline"
|
||||
"github.com/fatih/color"
|
||||
"github.com/fiatjaf/cli/v3"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/nbd-wtf/go-nostr/nip19"
|
||||
"github.com/nbd-wtf/go-nostr/nip46"
|
||||
"github.com/nbd-wtf/go-nostr/nip49"
|
||||
"github.com/nbd-wtf/go-nostr/sdk"
|
||||
)
|
||||
|
||||
var sys = sdk.NewSystem()
|
||||
|
||||
func init() {
|
||||
sys.Pool = nostr.NewSimplePool(context.Background(),
|
||||
nostr.WithUserAgent("nak/b"),
|
||||
)
|
||||
}
|
||||
|
||||
const (
|
||||
LINE_PROCESSING_ERROR = iota
|
||||
)
|
||||
@@ -120,13 +124,20 @@ func connectToAllRelays(
|
||||
relayUrls []string,
|
||||
forcePreAuth bool,
|
||||
opts ...nostr.PoolOption,
|
||||
) (*nostr.SimplePool, []*nostr.Relay) {
|
||||
) []*nostr.Relay {
|
||||
sys.Pool = nostr.NewSimplePool(context.Background(),
|
||||
append(opts,
|
||||
nostr.WithEventMiddleware(sys.TrackEventHints),
|
||||
nostr.WithPenaltyBox(),
|
||||
nostr.WithUserAgent("nak/s"),
|
||||
)...,
|
||||
)
|
||||
|
||||
relays := make([]*nostr.Relay, 0, len(relayUrls))
|
||||
pool := nostr.NewSimplePool(ctx, opts...)
|
||||
relayLoop:
|
||||
for _, url := range relayUrls {
|
||||
log("connecting to %s... ", url)
|
||||
if relay, err := pool.EnsureRelay(url); err == nil {
|
||||
if relay, err := sys.Pool.EnsureRelay(url); err == nil {
|
||||
if forcePreAuth {
|
||||
log("waiting for auth challenge... ")
|
||||
signer := opts[0].(nostr.WithAuthHandler)
|
||||
@@ -140,7 +151,7 @@ relayLoop:
|
||||
if (*challengeTag)[1] == "" {
|
||||
return fmt.Errorf("auth not received yet *****")
|
||||
}
|
||||
return signer(authEvent)
|
||||
return signer(ctx, nostr.RelayEvent{Event: authEvent, Relay: relay})
|
||||
}); err == nil {
|
||||
// auth succeeded
|
||||
break challengeWaitLoop
|
||||
@@ -166,7 +177,7 @@ relayLoop:
|
||||
log(err.Error() + "\n")
|
||||
}
|
||||
}
|
||||
return pool, relays
|
||||
return relays
|
||||
}
|
||||
|
||||
func lineProcessingError(ctx context.Context, msg string, args ...any) context.Context {
|
||||
@@ -180,106 +191,6 @@ func exitIfLineProcessingError(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
func gatherSecretKeyOrBunkerFromArguments(ctx context.Context, c *cli.Command) (string, *nip46.BunkerClient, error) {
|
||||
var err error
|
||||
|
||||
if bunkerURL := c.String("connect"); bunkerURL != "" {
|
||||
clientKey := c.String("connect-as")
|
||||
if clientKey != "" {
|
||||
clientKey = strings.Repeat("0", 64-len(clientKey)) + clientKey
|
||||
} else {
|
||||
clientKey = nostr.GeneratePrivateKey()
|
||||
}
|
||||
bunker, err := nip46.ConnectBunker(ctx, clientKey, bunkerURL, nil, func(s string) {
|
||||
fmt.Fprintf(color.Error, color.CyanString("[nip46]: open the following URL: %s"), s)
|
||||
})
|
||||
return "", bunker, err
|
||||
}
|
||||
|
||||
sec := c.String("sec")
|
||||
|
||||
// check in the environment for the secret key
|
||||
if sec == "" {
|
||||
if key, ok := os.LookupEnv("NOSTR_PRIVATE_KEY"); ok {
|
||||
sec = key
|
||||
}
|
||||
}
|
||||
|
||||
if c.Bool("prompt-sec") {
|
||||
if isPiped() {
|
||||
return "", nil, fmt.Errorf("can't prompt for a secret key when processing data from a pipe, try again without --prompt-sec")
|
||||
}
|
||||
sec, err = askPassword("type your secret key as ncryptsec, nsec or hex: ", nil)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("failed to get secret key: %w", err)
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(sec, "ncryptsec1") {
|
||||
sec, err = promptDecrypt(sec)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("failed to decrypt: %w", err)
|
||||
}
|
||||
} else if bsec, err := hex.DecodeString(leftPadKey(sec)); err == nil {
|
||||
sec = hex.EncodeToString(bsec)
|
||||
} else if prefix, hexvalue, err := nip19.Decode(sec); err != nil {
|
||||
return "", nil, fmt.Errorf("invalid nsec: %w", err)
|
||||
} else if prefix == "nsec" {
|
||||
sec = hexvalue.(string)
|
||||
}
|
||||
|
||||
if ok := nostr.IsValid32ByteHex(sec); !ok {
|
||||
return "", nil, fmt.Errorf("invalid secret key")
|
||||
}
|
||||
return sec, nil, nil
|
||||
}
|
||||
|
||||
func promptDecrypt(ncryptsec string) (string, error) {
|
||||
for i := 1; i < 4; i++ {
|
||||
var attemptStr string
|
||||
if i > 1 {
|
||||
attemptStr = fmt.Sprintf(" [%d/3]", i)
|
||||
}
|
||||
password, err := askPassword("type the password to decrypt your secret key"+attemptStr+": ", nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
sec, err := nip49.Decrypt(ncryptsec, password)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
return sec, nil
|
||||
}
|
||||
return "", fmt.Errorf("couldn't decrypt private key")
|
||||
}
|
||||
|
||||
func askPassword(msg string, shouldAskAgain func(answer string) bool) (string, error) {
|
||||
config := &readline.Config{
|
||||
Stdout: color.Error,
|
||||
Prompt: color.YellowString(msg),
|
||||
InterruptPrompt: "^C",
|
||||
DisableAutoSaveHistory: true,
|
||||
EnableMask: true,
|
||||
MaskRune: '*',
|
||||
}
|
||||
|
||||
rl, err := readline.NewEx(config)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for {
|
||||
answer, err := rl.Readline()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
answer = strings.TrimSpace(answer)
|
||||
if shouldAskAgain != nil && shouldAskAgain(answer) {
|
||||
continue
|
||||
}
|
||||
return answer, err
|
||||
}
|
||||
}
|
||||
|
||||
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
|
||||
func randString(n int) string {
|
||||
|
||||
161
helpers_key.go
Normal file
161
helpers_key.go
Normal file
@@ -0,0 +1,161 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/chzyer/readline"
|
||||
"github.com/fatih/color"
|
||||
"github.com/fiatjaf/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{
|
||||
&cli.StringFlag{
|
||||
Name: "sec",
|
||||
Usage: "secret key to sign the event, as nsec, ncryptsec or hex, or a bunker URL",
|
||||
DefaultText: "the key '1'",
|
||||
Aliases: []string{"connect"},
|
||||
Category: CATEGORY_SIGNER,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "prompt-sec",
|
||||
Usage: "prompt the user to paste a hex or nsec with which to sign the event",
|
||||
Category: CATEGORY_SIGNER,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "connect-as",
|
||||
Usage: "private key to use when communicating with NIP-46 bunkers",
|
||||
DefaultText: "a random key",
|
||||
Category: CATEGORY_SIGNER,
|
||||
Sources: cli.EnvVars("NOSTR_CLIENT_KEY"),
|
||||
},
|
||||
}
|
||||
|
||||
func gatherKeyerFromArguments(ctx context.Context, c *cli.Command) (nostr.Keyer, string, error) {
|
||||
key, bunker, err := gatherSecretKeyOrBunkerFromArguments(ctx, c)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
var kr nostr.Keyer
|
||||
if bunker != nil {
|
||||
kr = keyer.NewBunkerSignerFromBunkerClient(bunker)
|
||||
} else {
|
||||
kr, err = keyer.NewPlainKeySigner(key)
|
||||
}
|
||||
|
||||
return kr, key, err
|
||||
}
|
||||
|
||||
func gatherSecretKeyOrBunkerFromArguments(ctx context.Context, c *cli.Command) (string, *nip46.BunkerClient, error) {
|
||||
var err error
|
||||
|
||||
sec := c.String("sec")
|
||||
if strings.HasPrefix(sec, "bunker://") {
|
||||
// it's a bunker
|
||||
bunkerURL := sec
|
||||
clientKey := c.String("connect-as")
|
||||
if clientKey != "" {
|
||||
clientKey = strings.Repeat("0", 64-len(clientKey)) + clientKey
|
||||
} else {
|
||||
clientKey = nostr.GeneratePrivateKey()
|
||||
}
|
||||
bunker, err := nip46.ConnectBunker(ctx, clientKey, bunkerURL, nil, func(s string) {
|
||||
log(color.CyanString("[nip46]: open the following URL: %s"), s)
|
||||
})
|
||||
return "", bunker, err
|
||||
}
|
||||
|
||||
// take private from flags, environment variable or default to 1
|
||||
if sec == "" {
|
||||
if key, ok := os.LookupEnv("NOSTR_SECRET_KEY"); ok {
|
||||
sec = key
|
||||
} else {
|
||||
sec = "0000000000000000000000000000000000000000000000000000000000000001"
|
||||
}
|
||||
}
|
||||
|
||||
if c.Bool("prompt-sec") {
|
||||
if isPiped() {
|
||||
return "", nil, fmt.Errorf("can't prompt for a secret key when processing data from a pipe, try again without --prompt-sec")
|
||||
}
|
||||
sec, err = askPassword("type your secret key as ncryptsec, nsec or hex: ", nil)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("failed to get secret key: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if strings.HasPrefix(sec, "ncryptsec1") {
|
||||
sec, err = promptDecrypt(sec)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("failed to decrypt: %w", err)
|
||||
}
|
||||
} else if bsec, err := hex.DecodeString(leftPadKey(sec)); err == nil {
|
||||
sec = hex.EncodeToString(bsec)
|
||||
} else if prefix, hexvalue, err := nip19.Decode(sec); err != nil {
|
||||
return "", nil, fmt.Errorf("invalid nsec: %w", err)
|
||||
} else if prefix == "nsec" {
|
||||
sec = hexvalue.(string)
|
||||
}
|
||||
|
||||
if ok := nostr.IsValid32ByteHex(sec); !ok {
|
||||
return "", nil, fmt.Errorf("invalid secret key")
|
||||
}
|
||||
|
||||
return sec, nil, nil
|
||||
}
|
||||
|
||||
func promptDecrypt(ncryptsec string) (string, error) {
|
||||
for i := 1; i < 4; i++ {
|
||||
var attemptStr string
|
||||
if i > 1 {
|
||||
attemptStr = fmt.Sprintf(" [%d/3]", i)
|
||||
}
|
||||
password, err := askPassword("type the password to decrypt your secret key"+attemptStr+": ", nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
sec, err := nip49.Decrypt(ncryptsec, password)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
return sec, nil
|
||||
}
|
||||
return "", fmt.Errorf("couldn't decrypt private key")
|
||||
}
|
||||
|
||||
func askPassword(msg string, shouldAskAgain func(answer string) bool) (string, error) {
|
||||
config := &readline.Config{
|
||||
Stdout: color.Error,
|
||||
Prompt: color.YellowString(msg),
|
||||
InterruptPrompt: "^C",
|
||||
DisableAutoSaveHistory: true,
|
||||
EnableMask: true,
|
||||
MaskRune: '*',
|
||||
}
|
||||
|
||||
rl, err := readline.NewEx(config)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for {
|
||||
answer, err := rl.Readline()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
answer = strings.TrimSpace(answer)
|
||||
if shouldAskAgain != nil && shouldAskAgain(answer) {
|
||||
continue
|
||||
}
|
||||
return answer, err
|
||||
}
|
||||
}
|
||||
35
key.go
35
key.go
@@ -5,7 +5,6 @@ import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
@@ -25,8 +24,8 @@ var key = &cli.Command{
|
||||
Commands: []*cli.Command{
|
||||
generate,
|
||||
public,
|
||||
encrypt,
|
||||
decrypt,
|
||||
encryptKey,
|
||||
decryptKey,
|
||||
combine,
|
||||
},
|
||||
}
|
||||
@@ -49,20 +48,28 @@ var public = &cli.Command{
|
||||
Description: ``,
|
||||
ArgsUsage: "[secret]",
|
||||
DisableSliceFlagSeparator: true,
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "with-parity",
|
||||
Usage: "output 33 bytes instead of 32, the first one being either '02' or '03', a prefix indicating whether this pubkey is even or odd.",
|
||||
},
|
||||
},
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
for sec := range getSecretKeysFromStdinLinesOrSlice(ctx, c, c.Args().Slice()) {
|
||||
pubkey, err := nostr.GetPublicKey(sec)
|
||||
if err != nil {
|
||||
ctx = lineProcessingError(ctx, "failed to derive public key: %s", err)
|
||||
continue
|
||||
b, _ := hex.DecodeString(sec)
|
||||
_, pk := btcec.PrivKeyFromBytes(b)
|
||||
|
||||
if c.Bool("with-parity") {
|
||||
stdout(hex.EncodeToString(pk.SerializeCompressed()))
|
||||
} else {
|
||||
stdout(hex.EncodeToString(pk.SerializeCompressed()[1:]))
|
||||
}
|
||||
stdout(pubkey)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var encrypt = &cli.Command{
|
||||
var encryptKey = &cli.Command{
|
||||
Name: "encrypt",
|
||||
Usage: "encrypts a secret key and prints an ncryptsec code",
|
||||
Description: `uses the NIP-49 standard.`,
|
||||
@@ -101,7 +108,7 @@ var encrypt = &cli.Command{
|
||||
},
|
||||
}
|
||||
|
||||
var decrypt = &cli.Command{
|
||||
var decryptKey = &cli.Command{
|
||||
Name: "decrypt",
|
||||
Usage: "takes an ncrypsec and a password and decrypts it into an nsec",
|
||||
Description: `uses the NIP-49 standard.`,
|
||||
@@ -188,7 +195,7 @@ However, if the intent is to check if two existing Nostr pubkeys match a given c
|
||||
for i, prefix := range []byte{0x02, 0x03} {
|
||||
pubk, err := btcec.ParsePubKey(append([]byte{prefix}, keyb...))
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error parsing key %s: %s", keyhex, err)
|
||||
log("error parsing key %s: %s", keyhex, err)
|
||||
continue
|
||||
}
|
||||
group[i] = pubk
|
||||
@@ -229,7 +236,7 @@ However, if the intent is to check if two existing Nostr pubkeys match a given c
|
||||
|
||||
agg, _, _, err := musig2.AggregateKeys(combining, true)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error aggregating: %s", err)
|
||||
log("error aggregating: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -252,13 +259,13 @@ However, if the intent is to check if two existing Nostr pubkeys match a given c
|
||||
}
|
||||
|
||||
res, _ := json.MarshalIndent(result, "", " ")
|
||||
fmt.Println(string(res))
|
||||
stdout(string(res))
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func getSecretKeysFromStdinLinesOrSlice(ctx context.Context, c *cli.Command, keys []string) chan string {
|
||||
func getSecretKeysFromStdinLinesOrSlice(ctx context.Context, _ *cli.Command, keys []string) chan string {
|
||||
ch := make(chan string)
|
||||
go func() {
|
||||
for sec := range getStdinLinesOrArgumentsFromSlice(keys) {
|
||||
|
||||
8
main.go
8
main.go
@@ -7,6 +7,8 @@ import (
|
||||
"github.com/fiatjaf/cli/v3"
|
||||
)
|
||||
|
||||
var version string = "debug"
|
||||
|
||||
var app = &cli.Command{
|
||||
Name: "nak",
|
||||
Suggest: true,
|
||||
@@ -25,7 +27,11 @@ var app = &cli.Command{
|
||||
verify,
|
||||
relay,
|
||||
bunker,
|
||||
serve,
|
||||
encrypt,
|
||||
decrypt,
|
||||
},
|
||||
Version: version,
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "quiet",
|
||||
@@ -37,7 +43,7 @@ var app = &cli.Command{
|
||||
if q >= 1 {
|
||||
log = func(msg string, args ...any) {}
|
||||
if q >= 2 {
|
||||
stdout = func(a ...any) (int, error) { return 0, nil }
|
||||
stdout = func(_ ...any) (int, error) { return 0, nil }
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
34
musig2.go
34
musig2.go
@@ -6,7 +6,6 @@ import (
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -15,6 +14,31 @@ import (
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
)
|
||||
|
||||
func getMusigAggregatedKey(_ context.Context, keys []string) (string, error) {
|
||||
knownSigners := make([]*btcec.PublicKey, len(keys))
|
||||
for i, spk := range keys {
|
||||
bpk, err := hex.DecodeString(spk)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("'%s' is invalid hex: %w", spk, err)
|
||||
}
|
||||
if len(bpk) == 32 {
|
||||
return "", fmt.Errorf("'%s' is missing the leading parity byte", spk)
|
||||
}
|
||||
pk, err := btcec.ParsePubKey(bpk)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("'%s' is not a valid pubkey: %w", spk, err)
|
||||
}
|
||||
knownSigners[i] = pk
|
||||
}
|
||||
|
||||
aggpk, _, _, err := musig2.AggregateKeys(knownSigners, true)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("aggregation failed: %w", err)
|
||||
}
|
||||
|
||||
return hex.EncodeToString(aggpk.FinalKey.SerializeCompressed()[1:]), nil
|
||||
}
|
||||
|
||||
func performMusig(
|
||||
_ context.Context,
|
||||
sec string,
|
||||
@@ -110,8 +134,8 @@ func performMusig(
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "the following code should be saved secretly until the next step an included with --musig-nonce-secret:\n")
|
||||
fmt.Fprintf(os.Stderr, "%s\n\n", base64.StdEncoding.EncodeToString(nonce.SecNonce[:]))
|
||||
log("the following code should be saved secretly until the next step an included with --musig-nonce-secret:\n")
|
||||
log("%s\n\n", base64.StdEncoding.EncodeToString(nonce.SecNonce[:]))
|
||||
|
||||
knownNonces = append(knownNonces, nonce.PubNonce)
|
||||
printPublicCommandForNextPeer(evt, numSigners, knownSigners, knownNonces, nil, false)
|
||||
@@ -124,7 +148,7 @@ func performMusig(
|
||||
} else {
|
||||
evt.PubKey = hex.EncodeToString(comb.SerializeCompressed()[1:])
|
||||
evt.ID = evt.GetID()
|
||||
fmt.Fprintf(os.Stderr, "combined key: %x\n\n", comb.SerializeCompressed())
|
||||
log("combined key: %x\n\n", comb.SerializeCompressed())
|
||||
}
|
||||
|
||||
// we have all the signers, which means we must also have all the nonces
|
||||
@@ -219,7 +243,7 @@ func printPublicCommandForNextPeer(
|
||||
maybeNonceSecret = " --musig-nonce-secret '<insert-nonce-secret>'"
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stderr, "the next signer and they should call this on their side:\nnak event --sec <insert-secret-key> --musig %d %s%s%s%s%s\n",
|
||||
log("the next signer and they should call this on their side:\nnak event --sec <insert-secret-key> --musig %d %s%s%s%s%s\n",
|
||||
numSigners,
|
||||
eventToCliArgs(evt),
|
||||
signersToCliArgs(knownSigners),
|
||||
|
||||
11
paginate.go
11
paginate.go
@@ -9,8 +9,11 @@ import (
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
)
|
||||
|
||||
func paginateWithPoolAndParams(pool *nostr.SimplePool, interval time.Duration, globalLimit uint64) func(ctx context.Context, urls []string, filters nostr.Filters) chan nostr.IncomingEvent {
|
||||
return func(ctx context.Context, urls []string, filters nostr.Filters) chan nostr.IncomingEvent {
|
||||
func paginateWithParams(
|
||||
interval time.Duration,
|
||||
globalLimit uint64,
|
||||
) func(ctx context.Context, urls []string, filters nostr.Filters, opts ...nostr.SubscriptionOption) chan nostr.RelayEvent {
|
||||
return func(ctx context.Context, urls []string, filters nostr.Filters, opts ...nostr.SubscriptionOption) chan nostr.RelayEvent {
|
||||
// filters will always be just one
|
||||
filter := filters[0]
|
||||
|
||||
@@ -26,7 +29,7 @@ func paginateWithPoolAndParams(pool *nostr.SimplePool, interval time.Duration, g
|
||||
}
|
||||
}
|
||||
var globalCount uint64 = 0
|
||||
globalCh := make(chan nostr.IncomingEvent)
|
||||
globalCh := make(chan nostr.RelayEvent)
|
||||
|
||||
repeatedCache := make([]string, 0, 300)
|
||||
nextRepeatedCache := make([]string, 0, 300)
|
||||
@@ -39,7 +42,7 @@ func paginateWithPoolAndParams(pool *nostr.SimplePool, interval time.Duration, g
|
||||
time.Sleep(interval)
|
||||
|
||||
keepGoing := false
|
||||
for evt := range pool.SubManyEose(ctx, urls, nostr.Filters{filter}) {
|
||||
for evt := range sys.Pool.SubManyEose(ctx, urls, nostr.Filters{filter}, opts...) {
|
||||
if slices.Contains(repeatedCache, evt.ID) {
|
||||
continue
|
||||
}
|
||||
|
||||
36
relay.go
36
relay.go
@@ -74,27 +74,7 @@ var relay = &cli.Command{
|
||||
flags[i] = declareFlag(argName)
|
||||
}
|
||||
|
||||
flags = append(flags,
|
||||
&cli.StringFlag{
|
||||
Name: "sec",
|
||||
Usage: "secret key to sign the event, as nsec, ncryptsec or hex",
|
||||
DefaultText: "the key '1'",
|
||||
Value: "0000000000000000000000000000000000000000000000000000000000000001",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "prompt-sec",
|
||||
Usage: "prompt the user to paste a hex or nsec with which to sign the event",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "connect",
|
||||
Usage: "sign event using NIP-46, expects a bunker://... URL",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "connect-as",
|
||||
Usage: "private key to when communicating with the bunker given on --connect",
|
||||
DefaultText: "a random key",
|
||||
},
|
||||
)
|
||||
flags = append(flags, defaultKeyFlags...)
|
||||
|
||||
cmd := &cli.Command{
|
||||
Name: def.method,
|
||||
@@ -115,7 +95,7 @@ var relay = &cli.Command{
|
||||
return nil
|
||||
}
|
||||
|
||||
sec, bunker, err := gatherSecretKeyOrBunkerFromArguments(ctx, c)
|
||||
kr, _, err := gatherKeyerFromArguments(ctx, c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -132,7 +112,7 @@ var relay = &cli.Command{
|
||||
|
||||
// Authorization
|
||||
payloadHash := sha256.Sum256(reqj)
|
||||
authEvent := nostr.Event{
|
||||
tokenEvent := nostr.Event{
|
||||
Kind: 27235,
|
||||
CreatedAt: nostr.Now(),
|
||||
Tags: nostr.Tags{
|
||||
@@ -141,14 +121,10 @@ var relay = &cli.Command{
|
||||
{"payload", hex.EncodeToString(payloadHash[:])},
|
||||
},
|
||||
}
|
||||
if bunker != nil {
|
||||
if err := bunker.SignEvent(ctx, &authEvent); err != nil {
|
||||
return fmt.Errorf("failed to sign with bunker: %w", err)
|
||||
}
|
||||
} else if err := authEvent.Sign(sec); err != nil {
|
||||
return fmt.Errorf("error signing with provided key: %w", err)
|
||||
if err := kr.SignEvent(ctx, &tokenEvent); err != nil {
|
||||
return fmt.Errorf("failed to sign token event: %w", err)
|
||||
}
|
||||
evtj, _ := json.Marshal(authEvent)
|
||||
evtj, _ := json.Marshal(tokenEvent)
|
||||
req.Header.Set("Authorization", "Nostr "+base64.StdEncoding.EncodeToString(evtj))
|
||||
|
||||
// Content-Type
|
||||
|
||||
393
req.go
393
req.go
@@ -12,7 +12,10 @@ import (
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
)
|
||||
|
||||
const CATEGORY_FILTER_ATTRIBUTES = "FILTER ATTRIBUTES"
|
||||
const (
|
||||
CATEGORY_FILTER_ATTRIBUTES = "FILTER ATTRIBUTES"
|
||||
// CATEGORY_SIGNER = "SIGNER OPTIONS" -- defined at event.go as the same (yes, I know)
|
||||
)
|
||||
|
||||
var req = &cli.Command{
|
||||
Name: "req",
|
||||
@@ -28,154 +31,67 @@ it can also take a filter from stdin, optionally modify it with flags and send i
|
||||
example:
|
||||
echo '{"kinds": [1], "#t": ["test"]}' | nak req -l 5 -k 4549 --tag t=spam wss://nostr-pub.wellorder.net`,
|
||||
DisableSliceFlagSeparator: true,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringSliceFlag{
|
||||
Name: "author",
|
||||
Aliases: []string{"a"},
|
||||
Usage: "only accept events from these authors (pubkey as hex)",
|
||||
Category: CATEGORY_FILTER_ATTRIBUTES,
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "id",
|
||||
Aliases: []string{"i"},
|
||||
Usage: "only accept events with these ids (hex)",
|
||||
Category: CATEGORY_FILTER_ATTRIBUTES,
|
||||
},
|
||||
&cli.IntSliceFlag{
|
||||
Name: "kind",
|
||||
Aliases: []string{"k"},
|
||||
Usage: "only accept events with these kind numbers",
|
||||
Category: CATEGORY_FILTER_ATTRIBUTES,
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "tag",
|
||||
Aliases: []string{"t"},
|
||||
Usage: "takes a tag like -t e=<id>, only accept events with these tags",
|
||||
Category: CATEGORY_FILTER_ATTRIBUTES,
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "e",
|
||||
Usage: "shortcut for --tag e=<value>",
|
||||
Category: CATEGORY_FILTER_ATTRIBUTES,
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "p",
|
||||
Usage: "shortcut for --tag p=<value>",
|
||||
Category: CATEGORY_FILTER_ATTRIBUTES,
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "d",
|
||||
Usage: "shortcut for --tag d=<value>",
|
||||
Category: CATEGORY_FILTER_ATTRIBUTES,
|
||||
},
|
||||
&NaturalTimeFlag{
|
||||
Name: "since",
|
||||
Aliases: []string{"s"},
|
||||
Usage: "only accept events newer than this (unix timestamp)",
|
||||
Category: CATEGORY_FILTER_ATTRIBUTES,
|
||||
},
|
||||
&NaturalTimeFlag{
|
||||
Name: "until",
|
||||
Aliases: []string{"u"},
|
||||
Usage: "only accept events older than this (unix timestamp)",
|
||||
Category: CATEGORY_FILTER_ATTRIBUTES,
|
||||
},
|
||||
&cli.UintFlag{
|
||||
Name: "limit",
|
||||
Aliases: []string{"l"},
|
||||
Usage: "only accept up to this number of events",
|
||||
Category: CATEGORY_FILTER_ATTRIBUTES,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "search",
|
||||
Usage: "a NIP-50 search query, use it only with relays that explicitly support it",
|
||||
Category: CATEGORY_FILTER_ATTRIBUTES,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "stream",
|
||||
Usage: "keep the subscription open, printing all events as they are returned",
|
||||
DefaultText: "false, will close on EOSE",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "paginate",
|
||||
Usage: "make multiple REQs to the relay decreasing the value of 'until' until 'limit' or 'since' conditions are met",
|
||||
DefaultText: "false",
|
||||
},
|
||||
&cli.DurationFlag{
|
||||
Name: "paginate-interval",
|
||||
Usage: "time between queries when using --paginate",
|
||||
},
|
||||
&cli.UintFlag{
|
||||
Name: "paginate-global-limit",
|
||||
Usage: "global limit at which --paginate should stop",
|
||||
DefaultText: "uses the value given by --limit/-l or infinite",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "bare",
|
||||
Usage: "when printing the filter, print just the filter, not enveloped in a [\"REQ\", ...] array",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "auth",
|
||||
Usage: "always perform NIP-42 \"AUTH\" when facing an \"auth-required: \" rejection and try again",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "force-pre-auth",
|
||||
Aliases: []string{"fpa"},
|
||||
Usage: "after connecting, for a NIP-42 \"AUTH\" message to be received, act on it and only then send the \"REQ\"",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "sec",
|
||||
Usage: "secret key to sign the AUTH challenge, as hex or nsec",
|
||||
DefaultText: "the key '1'",
|
||||
Value: "0000000000000000000000000000000000000000000000000000000000000001",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "prompt-sec",
|
||||
Usage: "prompt the user to paste a hex or nsec with which to sign the AUTH challenge",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "connect",
|
||||
Usage: "sign AUTH using NIP-46, expects a bunker://... URL",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "connect-as",
|
||||
Usage: "private key to when communicating with the bunker given on --connect",
|
||||
DefaultText: "a random key",
|
||||
},
|
||||
},
|
||||
Flags: append(defaultKeyFlags,
|
||||
append(reqFilterFlags,
|
||||
&cli.BoolFlag{
|
||||
Name: "stream",
|
||||
Usage: "keep the subscription open, printing all events as they are returned",
|
||||
DefaultText: "false, will close on EOSE",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "paginate",
|
||||
Usage: "make multiple REQs to the relay decreasing the value of 'until' until 'limit' or 'since' conditions are met",
|
||||
DefaultText: "false",
|
||||
},
|
||||
&cli.DurationFlag{
|
||||
Name: "paginate-interval",
|
||||
Usage: "time between queries when using --paginate",
|
||||
},
|
||||
&cli.UintFlag{
|
||||
Name: "paginate-global-limit",
|
||||
Usage: "global limit at which --paginate should stop",
|
||||
DefaultText: "uses the value given by --limit/-l or infinite",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "bare",
|
||||
Usage: "when printing the filter, print just the filter, not enveloped in a [\"REQ\", ...] array",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "auth",
|
||||
Usage: "always perform NIP-42 \"AUTH\" when facing an \"auth-required: \" rejection and try again",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "force-pre-auth",
|
||||
Aliases: []string{"fpa"},
|
||||
Usage: "after connecting, for a NIP-42 \"AUTH\" message to be received, act on it and only then send the \"REQ\"",
|
||||
Category: CATEGORY_SIGNER,
|
||||
},
|
||||
)...,
|
||||
),
|
||||
ArgsUsage: "[relay...]",
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
var pool *nostr.SimplePool
|
||||
|
||||
relayUrls := c.Args().Slice()
|
||||
if len(relayUrls) > 0 {
|
||||
var relays []*nostr.Relay
|
||||
pool, relays = connectToAllRelays(ctx, relayUrls, c.Bool("force-pre-auth"), nostr.WithAuthHandler(func(evt *nostr.Event) error {
|
||||
if !c.Bool("auth") && !c.Bool("force-pre-auth") {
|
||||
return fmt.Errorf("auth not authorized")
|
||||
}
|
||||
sec, bunker, err := gatherSecretKeyOrBunkerFromArguments(ctx, c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
relays := connectToAllRelays(ctx,
|
||||
relayUrls,
|
||||
c.Bool("force-pre-auth"),
|
||||
nostr.WithAuthHandler(
|
||||
func(ctx context.Context, authEvent nostr.RelayEvent) error {
|
||||
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
|
||||
}
|
||||
|
||||
var pk string
|
||||
if bunker != nil {
|
||||
pk, err = bunker.GetPublicKey(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get public key from bunker: %w", err)
|
||||
}
|
||||
} else {
|
||||
pk, _ = nostr.GetPublicKey(sec)
|
||||
}
|
||||
log("performing auth as %s... ", pk)
|
||||
pk, _ := kr.GetPublicKey(ctx)
|
||||
log("performing auth as %s... ", pk)
|
||||
|
||||
if bunker != nil {
|
||||
return bunker.SignEvent(ctx, evt)
|
||||
} else {
|
||||
return evt.Sign(sec)
|
||||
}
|
||||
}))
|
||||
return kr.SignEvent(ctx, authEvent.Event)
|
||||
},
|
||||
),
|
||||
)
|
||||
if len(relays) == 0 {
|
||||
log("failed to connect to any of the given relays.\n")
|
||||
os.Exit(3)
|
||||
@@ -201,70 +117,16 @@ example:
|
||||
}
|
||||
}
|
||||
|
||||
if authors := c.StringSlice("author"); len(authors) > 0 {
|
||||
filter.Authors = append(filter.Authors, authors...)
|
||||
}
|
||||
if ids := c.StringSlice("id"); len(ids) > 0 {
|
||||
filter.IDs = append(filter.IDs, ids...)
|
||||
}
|
||||
for _, kind64 := range c.IntSlice("kind") {
|
||||
filter.Kinds = append(filter.Kinds, int(kind64))
|
||||
}
|
||||
if search := c.String("search"); search != "" {
|
||||
filter.Search = search
|
||||
}
|
||||
tags := make([][]string, 0, 5)
|
||||
for _, tagFlag := range c.StringSlice("tag") {
|
||||
spl := strings.Split(tagFlag, "=")
|
||||
if len(spl) == 2 && len(spl[0]) == 1 {
|
||||
tags = append(tags, spl)
|
||||
} else {
|
||||
return fmt.Errorf("invalid --tag '%s'", tagFlag)
|
||||
}
|
||||
}
|
||||
for _, etag := range c.StringSlice("e") {
|
||||
tags = append(tags, []string{"e", etag})
|
||||
}
|
||||
for _, ptag := range c.StringSlice("p") {
|
||||
tags = append(tags, []string{"p", ptag})
|
||||
}
|
||||
for _, dtag := range c.StringSlice("d") {
|
||||
tags = append(tags, []string{"d", dtag})
|
||||
}
|
||||
|
||||
if len(tags) > 0 && filter.Tags == nil {
|
||||
filter.Tags = make(nostr.TagMap)
|
||||
}
|
||||
|
||||
for _, tag := range tags {
|
||||
if _, ok := filter.Tags[tag[0]]; !ok {
|
||||
filter.Tags[tag[0]] = make([]string, 0, 3)
|
||||
}
|
||||
filter.Tags[tag[0]] = append(filter.Tags[tag[0]], tag[1])
|
||||
}
|
||||
|
||||
if c.IsSet("since") {
|
||||
nts := getNaturalDate(c, "since")
|
||||
filter.Since = &nts
|
||||
}
|
||||
|
||||
if c.IsSet("until") {
|
||||
nts := getNaturalDate(c, "until")
|
||||
filter.Until = &nts
|
||||
}
|
||||
|
||||
if limit := c.Uint("limit"); limit != 0 {
|
||||
filter.Limit = int(limit)
|
||||
} else if c.IsSet("limit") {
|
||||
filter.LimitZero = true
|
||||
if err := applyFlagsToFilter(c, &filter); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(relayUrls) > 0 {
|
||||
fn := pool.SubManyEose
|
||||
fn := sys.Pool.SubManyEose
|
||||
if c.Bool("paginate") {
|
||||
fn = paginateWithPoolAndParams(pool, c.Duration("paginate-interval"), c.Uint("paginate-global-limit"))
|
||||
fn = paginateWithParams(c.Duration("paginate-interval"), c.Uint("paginate-global-limit"))
|
||||
} else if c.Bool("stream") {
|
||||
fn = pool.SubMany
|
||||
fn = sys.Pool.SubMany
|
||||
}
|
||||
|
||||
for ie := range fn(ctx, relayUrls, nostr.Filters{filter}) {
|
||||
@@ -288,3 +150,130 @@ example:
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var reqFilterFlags = []cli.Flag{
|
||||
&cli.StringSliceFlag{
|
||||
Name: "author",
|
||||
Aliases: []string{"a"},
|
||||
Usage: "only accept events from these authors (pubkey as hex)",
|
||||
Category: CATEGORY_FILTER_ATTRIBUTES,
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "id",
|
||||
Aliases: []string{"i"},
|
||||
Usage: "only accept events with these ids (hex)",
|
||||
Category: CATEGORY_FILTER_ATTRIBUTES,
|
||||
},
|
||||
&cli.IntSliceFlag{
|
||||
Name: "kind",
|
||||
Aliases: []string{"k"},
|
||||
Usage: "only accept events with these kind numbers",
|
||||
Category: CATEGORY_FILTER_ATTRIBUTES,
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "tag",
|
||||
Aliases: []string{"t"},
|
||||
Usage: "takes a tag like -t e=<id>, only accept events with these tags",
|
||||
Category: CATEGORY_FILTER_ATTRIBUTES,
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "e",
|
||||
Usage: "shortcut for --tag e=<value>",
|
||||
Category: CATEGORY_FILTER_ATTRIBUTES,
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "p",
|
||||
Usage: "shortcut for --tag p=<value>",
|
||||
Category: CATEGORY_FILTER_ATTRIBUTES,
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "d",
|
||||
Usage: "shortcut for --tag d=<value>",
|
||||
Category: CATEGORY_FILTER_ATTRIBUTES,
|
||||
},
|
||||
&NaturalTimeFlag{
|
||||
Name: "since",
|
||||
Aliases: []string{"s"},
|
||||
Usage: "only accept events newer than this (unix timestamp)",
|
||||
Category: CATEGORY_FILTER_ATTRIBUTES,
|
||||
},
|
||||
&NaturalTimeFlag{
|
||||
Name: "until",
|
||||
Aliases: []string{"u"},
|
||||
Usage: "only accept events older than this (unix timestamp)",
|
||||
Category: CATEGORY_FILTER_ATTRIBUTES,
|
||||
},
|
||||
&cli.UintFlag{
|
||||
Name: "limit",
|
||||
Aliases: []string{"l"},
|
||||
Usage: "only accept up to this number of events",
|
||||
Category: CATEGORY_FILTER_ATTRIBUTES,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "search",
|
||||
Usage: "a NIP-50 search query, use it only with relays that explicitly support it",
|
||||
Category: CATEGORY_FILTER_ATTRIBUTES,
|
||||
},
|
||||
}
|
||||
|
||||
func applyFlagsToFilter(c *cli.Command, filter *nostr.Filter) error {
|
||||
if authors := c.StringSlice("author"); len(authors) > 0 {
|
||||
filter.Authors = append(filter.Authors, authors...)
|
||||
}
|
||||
if ids := c.StringSlice("id"); len(ids) > 0 {
|
||||
filter.IDs = append(filter.IDs, ids...)
|
||||
}
|
||||
for _, kind64 := range c.IntSlice("kind") {
|
||||
filter.Kinds = append(filter.Kinds, int(kind64))
|
||||
}
|
||||
if search := c.String("search"); search != "" {
|
||||
filter.Search = search
|
||||
}
|
||||
tags := make([][]string, 0, 5)
|
||||
for _, tagFlag := range c.StringSlice("tag") {
|
||||
spl := strings.SplitN(tagFlag, "=", 2)
|
||||
if len(spl) == 2 && len(spl[0]) == 1 {
|
||||
tags = append(tags, spl)
|
||||
} else {
|
||||
return fmt.Errorf("invalid --tag '%s'", tagFlag)
|
||||
}
|
||||
}
|
||||
for _, etag := range c.StringSlice("e") {
|
||||
tags = append(tags, []string{"e", etag})
|
||||
}
|
||||
for _, ptag := range c.StringSlice("p") {
|
||||
tags = append(tags, []string{"p", ptag})
|
||||
}
|
||||
for _, dtag := range c.StringSlice("d") {
|
||||
tags = append(tags, []string{"d", dtag})
|
||||
}
|
||||
|
||||
if len(tags) > 0 && filter.Tags == nil {
|
||||
filter.Tags = make(nostr.TagMap)
|
||||
}
|
||||
|
||||
for _, tag := range tags {
|
||||
if _, ok := filter.Tags[tag[0]]; !ok {
|
||||
filter.Tags[tag[0]] = make([]string, 0, 3)
|
||||
}
|
||||
filter.Tags[tag[0]] = append(filter.Tags[tag[0]], tag[1])
|
||||
}
|
||||
|
||||
if c.IsSet("since") {
|
||||
nts := getNaturalDate(c, "since")
|
||||
filter.Since = &nts
|
||||
}
|
||||
|
||||
if c.IsSet("until") {
|
||||
nts := getNaturalDate(c, "until")
|
||||
filter.Until = &nts
|
||||
}
|
||||
|
||||
if limit := c.Uint("limit"); limit != 0 {
|
||||
filter.Limit = int(limit)
|
||||
} else if c.IsSet("limit") {
|
||||
filter.LimitZero = true
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
126
serve.go
Normal file
126
serve.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/bep/debounce"
|
||||
"github.com/fatih/color"
|
||||
"github.com/fiatjaf/cli/v3"
|
||||
"github.com/fiatjaf/eventstore/slicestore"
|
||||
"github.com/fiatjaf/khatru"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
)
|
||||
|
||||
var serve = &cli.Command{
|
||||
Name: "serve",
|
||||
Usage: "starts an in-memory relay for testing purposes",
|
||||
DisableSliceFlagSeparator: true,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "hostname",
|
||||
Usage: "hostname where to listen for connections",
|
||||
Value: "localhost",
|
||||
},
|
||||
&cli.UintFlag{
|
||||
Name: "port",
|
||||
Usage: "port where to listen for connections",
|
||||
Value: 10547,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "events",
|
||||
Usage: "file containing the initial batch of events that will be served by the relay as newline-separated JSON (jsonl)",
|
||||
DefaultText: "the relay will start empty",
|
||||
},
|
||||
},
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
db := slicestore.SliceStore{MaxLimit: math.MaxInt}
|
||||
|
||||
var scanner *bufio.Scanner
|
||||
if path := c.String("events"); path != "" {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to file at '%s': %w", path, err)
|
||||
}
|
||||
scanner = bufio.NewScanner(f)
|
||||
} else if isPiped() {
|
||||
scanner = bufio.NewScanner(os.Stdin)
|
||||
}
|
||||
|
||||
if scanner != nil {
|
||||
scanner.Buffer(make([]byte, 16*1024*1024), 256*1024*1024)
|
||||
i := 0
|
||||
for scanner.Scan() {
|
||||
var evt nostr.Event
|
||||
if err := json.Unmarshal(scanner.Bytes(), &evt); err != nil {
|
||||
return fmt.Errorf("invalid event received at line %d: %s (`%s`)", i, err, scanner.Text())
|
||||
}
|
||||
db.SaveEvent(ctx, &evt)
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
rl := khatru.NewRelay()
|
||||
rl.QueryEvents = append(rl.QueryEvents, db.QueryEvents)
|
||||
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)
|
||||
exited := make(chan error)
|
||||
|
||||
hostname := c.String("hostname")
|
||||
port := int(c.Uint("port"))
|
||||
|
||||
go func() {
|
||||
err := rl.Start(hostname, port, started)
|
||||
exited <- err
|
||||
}()
|
||||
|
||||
bold := color.New(color.Bold).Sprintf
|
||||
italic := color.New(color.Italic).Sprint
|
||||
|
||||
var printStatus func()
|
||||
|
||||
// relay logging
|
||||
rl.RejectFilter = append(rl.RejectFilter, func(ctx context.Context, filter nostr.Filter) (reject bool, msg string) {
|
||||
log(" got %s %v\n", color.HiYellowString("request"), italic(filter))
|
||||
printStatus()
|
||||
return false, ""
|
||||
})
|
||||
rl.RejectCountFilter = append(rl.RejectCountFilter, func(ctx context.Context, filter nostr.Filter) (reject bool, msg string) {
|
||||
log(" got %s %v\n", color.HiCyanString("count request"), italic(filter))
|
||||
printStatus()
|
||||
return false, ""
|
||||
})
|
||||
rl.RejectEvent = append(rl.RejectEvent, func(ctx context.Context, event *nostr.Event) (reject bool, msg string) {
|
||||
log(" got %s %v\n", color.BlueString("event"), italic(event))
|
||||
printStatus()
|
||||
return false, ""
|
||||
})
|
||||
|
||||
d := debounce.New(time.Second * 2)
|
||||
printStatus = func() {
|
||||
d(func() {
|
||||
totalEvents := 0
|
||||
ch, _ := db.QueryEvents(ctx, nostr.Filter{})
|
||||
for range ch {
|
||||
totalEvents++
|
||||
}
|
||||
subs := rl.GetListeningFilters()
|
||||
|
||||
log(" %s events stored: %s, subscriptions opened: %s\n", color.HiMagentaString("•"), color.HiMagentaString("%d", totalEvents), color.HiMagentaString("%d", len(subs)))
|
||||
})
|
||||
}
|
||||
|
||||
<-started
|
||||
log("%s relay running at %s\n", color.HiRedString(">"), bold("ws://%s:%d", hostname, port))
|
||||
|
||||
return <-exited
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user