mirror of
https://github.com/fiatjaf/nak.git
synced 2026-08-01 12:16:14 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
160afb540c | ||
|
|
5a82298d95 | ||
|
|
a3890c4976 | ||
|
|
1403c7d2cd | ||
|
|
6855722ba5 | ||
|
|
aac995714b | ||
|
|
a9df9c6a21 | ||
|
|
57be7627d4 | ||
|
|
5b7f3eb4db | ||
|
|
6d4f4c03ec | ||
|
|
eb9afd0b54 | ||
|
|
9c35de2b02 | ||
|
|
b65683886b | ||
|
|
e36d546548 | ||
|
|
b6fa2e4f64 | ||
|
|
df2aaefb6d | ||
|
|
3e50a8cba7 | ||
|
|
c525a25e51 | ||
|
|
1cb730f34d | ||
|
|
1d6c352e92 | ||
|
|
d3347ec2b0 | ||
|
|
f288a59b3f | ||
|
|
549903dff2 | ||
|
|
2298e37cdf | ||
|
|
f10555037e | ||
|
|
e493fcdb03 | ||
|
|
03432424d2 | ||
|
|
ade6935c91 | ||
|
|
a73979c570 | ||
|
|
5874bbe588 | ||
|
|
59bcd97617 | ||
|
|
b420b81fd5 | ||
|
|
461e9c612e | ||
|
|
49713588bd | ||
|
|
80ce8059ea | ||
|
|
631c57b4bf | ||
|
|
0d3574c9fe | ||
|
|
e3aba47046 | ||
|
|
b46e9c7dd5 | ||
|
|
6bd793bea5 | ||
|
|
f96b9d458e | ||
|
|
d8e951e881 | ||
|
|
51f79e1d3c | ||
|
|
49f2839fb1 | ||
|
|
4911811e75 | ||
|
|
f1de27c88c | ||
|
|
514fed0b79 | ||
|
|
c1ebb829b9 | ||
|
|
d4d808e9b5 | ||
|
|
c9f7632571 | ||
|
|
9101a3a543 | ||
|
|
d419b35b66 | ||
|
|
fd8fdad26a | ||
|
|
232ef8d02e | ||
|
|
09539a317f | ||
|
|
bcc4f42cd7 | ||
|
|
23bc1d3aa3 | ||
|
|
469c99fb51 | ||
|
|
f2638ab2de | ||
|
|
dd0b25816a | ||
|
|
4fa512edf3 | ||
|
|
0055037943 | ||
|
|
ab32de2fb3 | ||
|
|
6a011ae7d9 | ||
|
|
7c28e75c4b | ||
|
|
4e0b7ecc08 | ||
|
|
586a8920e2 | ||
|
|
e1e8b11001 | ||
|
|
61b20c6e7a | ||
|
|
72e6e735f3 | ||
|
|
11abe3afca | ||
|
|
58cfd06eab | ||
|
|
c16a456811 | ||
|
|
5c754b1b4f | ||
|
|
0ad2fa3e65 | ||
|
|
3fa1e2267e | ||
|
|
97b544fb6e | ||
|
|
44db7af6c0 | ||
|
|
34281a9c8e | ||
|
|
4f798759dd | ||
|
|
198cbc6d4a | ||
|
|
5d45849595 | ||
|
|
8eb6b7303c | ||
|
|
002763669b | ||
|
|
ecb7514716 | ||
|
|
b188d7f803 | ||
|
|
d79fd1ba59 | ||
|
|
e62bf97acc | ||
|
|
c709647823 | ||
|
|
8563178bfd | ||
|
|
d374fe9bd3 | ||
|
|
4ea0edc52b | ||
|
|
6087c71072 | ||
|
|
0bcb90e37e | ||
|
|
98b7a1ae6a | ||
|
|
46a809d284 | ||
|
|
cbfc16f354 | ||
|
|
62fa610a06 | ||
|
|
5afdce2672 | ||
|
|
4fd7400272 | ||
|
|
40a934e3f9 | ||
|
|
12000232a7 | ||
|
|
cc879b6bf6 | ||
|
|
483bf94ff4 | ||
|
|
814059fcec | ||
|
|
b3e009730c | ||
|
|
7bbff0a3ad | ||
|
|
c97e1815b4 | ||
|
|
29113783b3 | ||
|
|
59c691f4d5 | ||
|
|
0c8e643609 | ||
|
|
7872c61a4d | ||
|
|
97950d1203 | ||
|
|
0960ceb6a6 |
@@ -167,6 +167,9 @@ relays:
|
||||
wss://140.f7z.io (write)
|
||||
wss://r.f7z.io (read/write)
|
||||
follows: 1051
|
||||
~>
|
||||
~> nak profile f7z.io --name
|
||||
PABLOF7z
|
||||
```
|
||||
|
||||
### sign an event using a bunker provider (amber, promenade etc)
|
||||
@@ -308,6 +311,28 @@ type the password to decrypt your secret key: ********
|
||||
~> nak req -i 412f2d3e73acc312942c055ac2a695dc60bf58ff97e06689a8a79e97796c4cdb relay.westernbtc.com | jq -r .content > ~/.jq
|
||||
```
|
||||
|
||||
### filter and transform events with the built-in `--jq` helpers
|
||||
|
||||
extra-filter events with `select()` based on tags:
|
||||
|
||||
```shell
|
||||
~> nak req -k 1 -l 10 relay.damus.io --jq 'select(has("t"))'
|
||||
```
|
||||
|
||||
extract a tag value:
|
||||
|
||||
```shell
|
||||
~> nak req -k 1 -l 10 -t t=gm relay.damus.io --jq 'value("t")'
|
||||
```
|
||||
|
||||
make an event easier to read by removing fields and adding human-readable time:
|
||||
|
||||
```shell
|
||||
~> nak req -k 1 -l 5 relay.damus.io --jq '. + { d: datetime } | del(.sig, .created_at)'
|
||||
```
|
||||
|
||||
you can do these same things by piping to standalone `jq`, take the functions in https://github.com/fiatjaf/nak/blob/master/jq.go#L10 and add them to your `~/.jq` file.
|
||||
|
||||
### watch a NIP-53 livestream (zap.stream, amethyst, shosho etc)
|
||||
```shell
|
||||
~> # this requires the jq utils from the step above
|
||||
|
||||
@@ -25,7 +25,6 @@ var admin = &cli.Command{
|
||||
nak admin changerelayname myrelay.com --name "My Relay"`,
|
||||
ArgsUsage: "<relay-url>",
|
||||
DisableSliceFlagSeparator: true,
|
||||
Flags: defaultKeyFlags,
|
||||
Commands: (func() []*cli.Command {
|
||||
methods := []struct {
|
||||
method string
|
||||
@@ -48,6 +47,17 @@ var admin = &cli.Command{
|
||||
{"blockip", []string{"ip", "reason"}},
|
||||
{"unblockip", []string{"ip", "reason"}},
|
||||
{"listblockedips", nil},
|
||||
{"listallowedevents", nil},
|
||||
{"listdisallowedkinds", nil},
|
||||
{"supportedmethods", nil},
|
||||
{"stats", nil},
|
||||
{"grantadmin", []string{"pubkey", "methods"}},
|
||||
{"revokeadmin", []string{"pubkey", "methods"}},
|
||||
{"createrole", []string{"role_id", "label", "description", "color", "order"}},
|
||||
{"editrole", []string{"role_id", "label", "description", "color", "order"}},
|
||||
{"deleterole", []string{"role_id"}},
|
||||
{"assignrole", []string{"pubkey", "role_id"}},
|
||||
{"unassignrole", []string{"pubkey", "role_id"}},
|
||||
}
|
||||
|
||||
commands := make([]*cli.Command, 0, len(methods))
|
||||
@@ -122,6 +132,7 @@ var admin = &cli.Command{
|
||||
continue
|
||||
}
|
||||
b, err := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
log("failed to read response: %s\n", err)
|
||||
continue
|
||||
@@ -132,7 +143,7 @@ var admin = &cli.Command{
|
||||
if len(bodyPrintable) > 300 {
|
||||
bodyPrintable = bodyPrintable[0:297] + "..."
|
||||
}
|
||||
log(bodyPrintable)
|
||||
log("%s", bodyPrintable)
|
||||
continue
|
||||
}
|
||||
var response nip86.Response
|
||||
@@ -142,10 +153,9 @@ var admin = &cli.Command{
|
||||
if len(bodyPrintable) > 300 {
|
||||
bodyPrintable = bodyPrintable[0:297] + "..."
|
||||
}
|
||||
log(bodyPrintable)
|
||||
log("%s", bodyPrintable)
|
||||
continue
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
// print the result
|
||||
log("\n")
|
||||
@@ -167,10 +177,16 @@ var admin = &cli.Command{
|
||||
func declareFlag(argName string) cli.Flag {
|
||||
usage := "parameter for this management RPC call, see https://nips.nostr.com/86 for more information."
|
||||
switch argName {
|
||||
case "kind":
|
||||
case "pubkey":
|
||||
return &PubKeyFlag{Name: argName, Usage: usage}
|
||||
case "id":
|
||||
return &IDFlag{Name: argName, Usage: usage}
|
||||
case "kind", "order", "color":
|
||||
return &cli.IntFlag{Name: argName, Required: true, Usage: usage}
|
||||
case "reason":
|
||||
return &cli.StringFlag{Name: argName, Usage: usage}
|
||||
case "methods":
|
||||
return &cli.StringSliceFlag{Name: argName, Required: true, Usage: usage}
|
||||
default:
|
||||
return &cli.StringFlag{Name: argName, Required: true, Usage: usage}
|
||||
}
|
||||
@@ -178,8 +194,14 @@ func declareFlag(argName string) cli.Flag {
|
||||
|
||||
func getArgument(c *cli.Command, argName string) any {
|
||||
switch argName {
|
||||
case "kind":
|
||||
case "pubkey":
|
||||
return getPubKey(c, argName)
|
||||
case "id":
|
||||
return getID(c, argName)
|
||||
case "kind", "order", "color":
|
||||
return c.Int(argName)
|
||||
case "methods":
|
||||
return c.StringSlice(argName)
|
||||
default:
|
||||
return c.String(argName)
|
||||
}
|
||||
|
||||
+9
-4
@@ -20,7 +20,7 @@ var blossomCmd = &cli.Command{
|
||||
UseShortOptionHandling: true,
|
||||
Usage: "an army knife for blossom things",
|
||||
DisableSliceFlagSeparator: true,
|
||||
Flags: append(defaultKeyFlags,
|
||||
Flags: combineFlags([][]cli.Flag{},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "server",
|
||||
Aliases: []string{"s"},
|
||||
@@ -179,7 +179,7 @@ var blossomCmd = &cli.Command{
|
||||
hasError = true
|
||||
continue
|
||||
}
|
||||
stdout(data)
|
||||
os.Stdout.Write(data)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,6 +238,11 @@ if any of the files are not found the command will fail, otherwise it will succe
|
||||
|
||||
hasError := false
|
||||
for _, hash := range c.Args().Slice() {
|
||||
// a sha256 is 64 hex characters, anything after that is an extension
|
||||
if len(hash) > 64 {
|
||||
hash = hash[0:64]
|
||||
}
|
||||
|
||||
err := client.Check(ctx, hash)
|
||||
if err != nil {
|
||||
hasError = true
|
||||
@@ -281,7 +286,7 @@ if any of the files are not found the command will fail, otherwise it will succe
|
||||
return err
|
||||
}
|
||||
out, _ := json.Marshal(bd)
|
||||
stdout(out)
|
||||
stdout(string(out))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -300,7 +305,7 @@ if any of the files are not found the command will fail, otherwise it will succe
|
||||
continue
|
||||
}
|
||||
out, _ := json.Marshal(bd)
|
||||
stdout(out)
|
||||
stdout(string(out))
|
||||
}
|
||||
|
||||
exitIfLineProcessingError(ctx)
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"fiatjaf.com/nostr"
|
||||
@@ -44,15 +45,6 @@ var bunker = &cli.Command{
|
||||
OnlyOnce: true,
|
||||
Category: PERSISTENCE,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "sec",
|
||||
Usage: "secret key to sign the event, as hex or nsec",
|
||||
DefaultText: "the key '1'",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "prompt-sec",
|
||||
Usage: "prompt the user to paste a hex or nsec with which to sign the event",
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "authorized-secrets",
|
||||
Aliases: []string{"s"},
|
||||
@@ -162,10 +154,10 @@ var bunker = &cli.Command{
|
||||
}
|
||||
|
||||
if config.Secret.Plain == nil && config.Secret.Encrypted == nil {
|
||||
// we don't have any secret key stored, so just use whatever was given via flags
|
||||
// we don't have any secret key stored, so just use whatever was given via flags (or defaults)
|
||||
config.Secret = baseSecret
|
||||
} else if baseSecret.Plain == nil && baseSecret.Encrypted == nil {
|
||||
// we didn't provide any keys, so we just use the stored
|
||||
} else if !c.IsSet("sec") && !c.IsSet("prompt-sec") {
|
||||
// we didn't provide any keys explicitly, so we just use the stored
|
||||
} else {
|
||||
// we have a secret key stored
|
||||
// if we also provided a key we check if they match and fail otherwise
|
||||
@@ -185,7 +177,7 @@ var bunker = &cli.Command{
|
||||
if config.Secret.Plain == nil && config.Secret.Encrypted == nil {
|
||||
sec := os.Getenv("NOSTR_SECRET_KEY")
|
||||
if sec == "" {
|
||||
sec = defaultKey
|
||||
sec = defaultKey().Hex()
|
||||
}
|
||||
sk, err := nostr.SecretKeyFromHex(sec)
|
||||
if err != nil {
|
||||
@@ -225,7 +217,7 @@ var bunker = &cli.Command{
|
||||
}
|
||||
}
|
||||
}
|
||||
relays := connectToAllRelays(ctx, c, allRelays, nil)
|
||||
relays := connectToAllRelays(ctx, c, allRelays)
|
||||
if len(relays) == 0 {
|
||||
log("failed to connect to any of the given relays.\n")
|
||||
os.Exit(3)
|
||||
@@ -244,12 +236,19 @@ var bunker = &cli.Command{
|
||||
// it will be stored
|
||||
newSecret := randString(12)
|
||||
|
||||
// guards config.Clients, newSecret and cancelPreviousBunkerInfoPrint, which are
|
||||
// accessed from the socket goroutine, the per-request handler goroutines and here
|
||||
var mu sync.Mutex
|
||||
|
||||
// static information
|
||||
pubkey := sec.Public()
|
||||
npub := nip19.EncodeNpub(pubkey)
|
||||
|
||||
// this function will be called every now and then
|
||||
printBunkerInfo := func() {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
iqs := make(url.Values)
|
||||
maps.Copy(iqs, qs)
|
||||
iqs.Set("secret", newSecret)
|
||||
@@ -351,70 +350,15 @@ var bunker = &cli.Command{
|
||||
signer := nip46.NewStaticKeySigner(sec)
|
||||
signer.DefaultRelays = config.Relays
|
||||
|
||||
// unix socket nostrconnect:// handling
|
||||
go func() {
|
||||
for uri := range onSocketConnect(ctx, c) {
|
||||
clientPublicKey, err := nostr.PubKeyFromHex(uri.Host)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
log("- got nostrconnect:// request from '%s': %s\n", color.New(color.Bold, color.FgBlue).Sprint(clientPublicKey.Hex()), uri.String())
|
||||
|
||||
relays := uri.Query()["relay"]
|
||||
|
||||
// pre-authorize this client since the user has explicitly added it
|
||||
if !slices.ContainsFunc(config.Clients, func(c BunkerConfigClient) bool {
|
||||
return c.PubKey == clientPublicKey
|
||||
}) {
|
||||
config.Clients = append(config.Clients, BunkerConfigClient{
|
||||
PubKey: clientPublicKey,
|
||||
Name: uri.Query().Get("name"),
|
||||
URL: uri.Query().Get("url"),
|
||||
Icon: uri.Query().Get("icon"),
|
||||
CustomRelays: relays,
|
||||
})
|
||||
}
|
||||
|
||||
if persist != nil {
|
||||
persist()
|
||||
}
|
||||
|
||||
resp, eventResponse, err := signer.HandleNostrConnectURI(ctx, uri)
|
||||
if err != nil {
|
||||
log("* failed to handle: %s\n", err)
|
||||
continue
|
||||
}
|
||||
|
||||
go func() {
|
||||
for event := range sys.Pool.SubscribeMany(ctx, relays, nostr.Filter{
|
||||
Kinds: []nostr.Kind{nostr.KindNostrConnect},
|
||||
Tags: nostr.TagMap{"p": []string{pubkey.Hex()}},
|
||||
Since: nostr.Now(),
|
||||
LimitZero: true,
|
||||
}, nostr.SubscriptionOptions{Label: "nak-bunker"}) {
|
||||
events <- event
|
||||
}
|
||||
}()
|
||||
|
||||
time.Sleep(time.Millisecond * 25)
|
||||
jresp, _ := json.MarshalIndent(resp, "", " ")
|
||||
log("~ responding with %s\n", string(jresp))
|
||||
for res := range sys.Pool.PublishMany(ctx, relays, eventResponse) {
|
||||
if res.Error == nil {
|
||||
log("* sent through %s\n", res.Relay.URL)
|
||||
} else {
|
||||
log("* failed to send through %s: %s\n", res.RelayURL, res.Error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// just a gimmick
|
||||
var cancelPreviousBunkerInfoPrint context.CancelFunc
|
||||
_, cancel := context.WithCancel(ctx)
|
||||
cancelPreviousBunkerInfoPrint = cancel
|
||||
|
||||
signer.AuthorizeRequest = func(harmless bool, from nostr.PubKey, secret string) bool {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
if slices.ContainsFunc(config.Clients, func(b BunkerConfigClient) bool { return b.PubKey == from }) {
|
||||
return true
|
||||
}
|
||||
@@ -450,60 +394,130 @@ var bunker = &cli.Command{
|
||||
return false
|
||||
}
|
||||
|
||||
for ie := range events {
|
||||
handleBunkerRequest := func(ie nostr.RelayEvent) {
|
||||
mu.Lock()
|
||||
cancelPreviousBunkerInfoPrint() // this prevents us from printing a million bunker info blocks
|
||||
mu.Unlock()
|
||||
|
||||
go func() {
|
||||
// handle the NIP-46 request event
|
||||
from := ie.Event.PubKey
|
||||
req, resp, eventResponse, err := signer.HandleRequest(ctx, ie.Event)
|
||||
if err != nil {
|
||||
if errors.Is(err, nip46.AlreadyHandled) {
|
||||
return
|
||||
}
|
||||
|
||||
log("< failed to handle request from %s: %s\n", from.Hex(), err.Error())
|
||||
// handle the NIP-46 request event
|
||||
from := ie.Event.PubKey
|
||||
req, resp, eventResponse, err := signer.HandleRequest(ctx, ie.Event)
|
||||
if err != nil {
|
||||
if errors.Is(err, nip46.AlreadyHandled) {
|
||||
return
|
||||
}
|
||||
|
||||
jreq, _ := json.MarshalIndent(req, "", " ")
|
||||
log("- got request from '%s': %s\n", color.New(color.Bold, color.FgBlue).Sprint(from.Hex()), string(jreq))
|
||||
jresp, _ := json.MarshalIndent(resp, "", " ")
|
||||
log("~ responding with %s\n", string(jresp))
|
||||
log("< failed to handle request from %s: %s\n", from.Hex(), err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// use custom relays if they are defined for this client
|
||||
// (normally if the initial connection came from a nostrconnect:// URL)
|
||||
relays := config.Relays
|
||||
for _, c := range config.Clients {
|
||||
if c.PubKey == from && len(c.CustomRelays) > 0 {
|
||||
relays = c.CustomRelays
|
||||
break
|
||||
}
|
||||
jreq, _ := json.MarshalIndent(req, "", " ")
|
||||
log("- got request from '%s': %s\n", color.New(color.Bold, color.FgBlue).Sprint(from.Hex()), string(jreq))
|
||||
jresp, _ := json.MarshalIndent(resp, "", " ")
|
||||
log("~ responding with %s\n", string(jresp))
|
||||
|
||||
// use custom relays if they are defined for this client
|
||||
// (normally if the initial connection came from a nostrconnect:// URL)
|
||||
relays := config.Relays
|
||||
mu.Lock()
|
||||
for _, c := range config.Clients {
|
||||
if c.PubKey == from && len(c.CustomRelays) > 0 {
|
||||
relays = c.CustomRelays
|
||||
break
|
||||
}
|
||||
}
|
||||
mu.Unlock()
|
||||
|
||||
for res := range sys.Pool.PublishMany(ctx, relays, eventResponse) {
|
||||
if res.Error == nil {
|
||||
log("* sent response through %s\n", res.Relay.URL)
|
||||
} else {
|
||||
log("* failed to send response through %s: %s\n", res.RelayURL, res.Error)
|
||||
}
|
||||
}
|
||||
|
||||
// just after handling one request we trigger this
|
||||
go func() {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
mu.Lock()
|
||||
cancelPreviousBunkerInfoPrint = cancel
|
||||
mu.Unlock()
|
||||
// the idea is that we will print the bunker URL again so it is easier to copy-paste by users
|
||||
// but we will only do if the bunker is inactive for more than 5 minutes
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case <-time.After(time.Minute * 5):
|
||||
log("\n")
|
||||
printBunkerInfo()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// unix socket nostrconnect:// handling
|
||||
go func() {
|
||||
for uri := range onSocketConnect(ctx, c) {
|
||||
clientPublicKey, err := nostr.PubKeyFromHex(uri.Host)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
log("- got nostrconnect:// request from '%s': %s\n", color.New(color.Bold, color.FgBlue).Sprint(clientPublicKey.Hex()), uri.String())
|
||||
|
||||
relays := uri.Query()["relay"]
|
||||
|
||||
// pre-authorize this client since the user has explicitly added it
|
||||
mu.Lock()
|
||||
if !slices.ContainsFunc(config.Clients, func(c BunkerConfigClient) bool {
|
||||
return c.PubKey == clientPublicKey
|
||||
}) {
|
||||
config.Clients = append(config.Clients, BunkerConfigClient{
|
||||
PubKey: clientPublicKey,
|
||||
Name: uri.Query().Get("name"),
|
||||
URL: uri.Query().Get("url"),
|
||||
Icon: uri.Query().Get("icon"),
|
||||
CustomRelays: relays,
|
||||
})
|
||||
}
|
||||
|
||||
for res := range sys.Pool.PublishMany(ctx, relays, eventResponse) {
|
||||
if res.Error == nil {
|
||||
log("* sent response through %s\n", res.Relay.URL)
|
||||
} else {
|
||||
log("* failed to send response through %s: %s\n", res.RelayURL, res.Error)
|
||||
}
|
||||
if persist != nil {
|
||||
persist()
|
||||
}
|
||||
mu.Unlock()
|
||||
|
||||
resp, eventResponse, err := signer.HandleNostrConnectURI(ctx, uri)
|
||||
if err != nil {
|
||||
log("* failed to handle: %s\n", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// just after handling one request we trigger this
|
||||
go func() {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
cancelPreviousBunkerInfoPrint = cancel
|
||||
// the idea is that we will print the bunker URL again so it is easier to copy-paste by users
|
||||
// but we will only do if the bunker is inactive for more than 5 minutes
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case <-time.After(time.Minute * 5):
|
||||
log("\n")
|
||||
printBunkerInfo()
|
||||
for event := range sys.Pool.SubscribeMany(ctx, relays, nostr.Filter{
|
||||
Kinds: []nostr.Kind{nostr.KindNostrConnect},
|
||||
Tags: nostr.TagMap{"p": []string{pubkey.Hex()}},
|
||||
Since: nostr.Now(),
|
||||
LimitZero: true,
|
||||
}, nostr.SubscriptionOptions{Label: "nak-bunker"}) {
|
||||
// handle directly instead of forwarding into the main events
|
||||
// channel, which is owned (and eventually closed) by the pool
|
||||
go handleBunkerRequest(event)
|
||||
}
|
||||
}()
|
||||
}()
|
||||
|
||||
time.Sleep(time.Millisecond * 25)
|
||||
jresp, _ := json.MarshalIndent(resp, "", " ")
|
||||
log("~ responding with %s\n", string(jresp))
|
||||
for res := range sys.Pool.PublishMany(ctx, relays, eventResponse) {
|
||||
if res.Error == nil {
|
||||
log("* sent through %s\n", res.Relay.URL)
|
||||
} else {
|
||||
log("* failed to send through %s: %s\n", res.RelayURL, res.Error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
for ie := range events {
|
||||
go handleBunkerRequest(ie)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
+14
@@ -229,3 +229,17 @@ func TestNaturalTimestamps(t *testing.T) {
|
||||
require.Equal(t, nostr.Timestamp(1526711839), evt.CreatedAt)
|
||||
require.Equal(t, "nn", evt.Content)
|
||||
}
|
||||
|
||||
func TestEventJQ(t *testing.T) {
|
||||
// default: string results come out JSON-quoted, like plain jq
|
||||
quoted := call(t, "nak event --ts 1699485669 -c hello --jq .content")
|
||||
require.Equal(t, `"hello"`, quoted)
|
||||
|
||||
// --jq-raw: string results come out unquoted, like `jq -r`
|
||||
raw := call(t, "nak event --ts 1699485669 -c hello --jq .content --jq-raw")
|
||||
require.Equal(t, "hello", raw)
|
||||
|
||||
// --jq-raw on a non-string result is still JSON-encoded, like `jq -r`
|
||||
num := call(t, "nak event --ts 1699485669 -k 7 --jq .kind --jq-raw")
|
||||
require.Equal(t, "7", num)
|
||||
}
|
||||
|
||||
@@ -7,10 +7,8 @@ import (
|
||||
"strings"
|
||||
|
||||
"fiatjaf.com/nostr"
|
||||
"fiatjaf.com/nostr/nip42"
|
||||
"fiatjaf.com/nostr/nip45"
|
||||
"fiatjaf.com/nostr/nip45/hyperloglog"
|
||||
"github.com/fatih/color"
|
||||
"github.com/mailru/easyjson"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
@@ -20,47 +18,16 @@ var count = &cli.Command{
|
||||
Usage: "generates encoded COUNT messages and optionally use them to talk to relays",
|
||||
Description: `like 'nak req', but does a "COUNT" call instead. Will attempt to perform HyperLogLog aggregation if more than one relay is specified.`,
|
||||
DisableSliceFlagSeparator: true,
|
||||
Flags: append(defaultKeyFlags,
|
||||
append(reqFilterFlags,
|
||||
&cli.BoolFlag{
|
||||
Name: "auth",
|
||||
Usage: "always perform nip42 \"AUTH\" when facing an \"auth-required: \" rejection and try again",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "force-pre-auth",
|
||||
Aliases: []string{"fpa"},
|
||||
Usage: "after connecting, for a nip42 \"AUTH\" message to be received, act on it and only then send the \"COUNT\"",
|
||||
Category: CATEGORY_SIGNER,
|
||||
},
|
||||
)...,
|
||||
),
|
||||
ArgsUsage: "[relay...]",
|
||||
Flags: combineFlags([][]cli.Flag{reqFilterFlags}),
|
||||
ArgsUsage: "[relay...]",
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
biggerUrlSize := 0
|
||||
relayUrls := c.Args().Slice()
|
||||
if len(relayUrls) > 0 {
|
||||
forcePreAuthSigner := authSigner
|
||||
if !c.Bool("force-pre-auth") {
|
||||
forcePreAuthSigner = nil
|
||||
}
|
||||
|
||||
sys.Pool.AuthRequiredHandler = func(ctx context.Context, authEvent *nostr.Event) error {
|
||||
return authSigner(ctx, c, func(s string, args ...any) {
|
||||
if strings.HasPrefix(s, "authenticating as") {
|
||||
cleanUrl, _ := strings.CutPrefix(
|
||||
nip42.GetRelayURLFromAuthEvent(*authEvent),
|
||||
"wss://",
|
||||
)
|
||||
s = "authenticating to " + color.CyanString(cleanUrl) + " as" + s[len("authenticating as"):]
|
||||
}
|
||||
log(s+"\n", args...)
|
||||
}, authEvent)
|
||||
}
|
||||
relays := connectToAllRelays(
|
||||
ctx,
|
||||
c,
|
||||
relayUrls,
|
||||
forcePreAuthSigner,
|
||||
)
|
||||
if len(relays) == 0 {
|
||||
log("failed to connect to any of the given relays.\n")
|
||||
@@ -96,7 +63,21 @@ var count = &cli.Command{
|
||||
hll = hyperloglog.New(offset)
|
||||
}
|
||||
for _, relayUrl := range relayUrls {
|
||||
relay, _ := sys.Pool.EnsureRelay(relayUrl)
|
||||
nm := nostr.NormalizeURL(relayUrl)
|
||||
relay, ok := sys.Pool.Relays.Load(nm)
|
||||
if !ok || relay == nil || !relay.IsConnected() {
|
||||
ct, cancel := context.WithTimeout(context.Background(), connectTimeout)
|
||||
var err error
|
||||
relay, err = nostr.RelayConnect(ct, relayUrl, sys.Pool.RelayOptions)
|
||||
cancel()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%s%s: ", strings.Repeat(" ", biggerUrlSize-len(relayUrl)), relayUrl)
|
||||
fmt.Fprintf(os.Stderr, "error: %s\n", err)
|
||||
continue
|
||||
}
|
||||
sys.Pool.Relays.Store(nm, relay)
|
||||
}
|
||||
|
||||
count, hllRegisters, err := relay.Count(ctx, filter, nostr.SubscriptionOptions{
|
||||
Label: "nak-count",
|
||||
})
|
||||
|
||||
@@ -19,8 +19,10 @@ var curl = &cli.Command{
|
||||
Name: "curl",
|
||||
Usage: "calls curl but with a nip98 header",
|
||||
Description: "accepts all flags and arguments exactly as they would be passed to curl.",
|
||||
Flags: defaultKeyFlags,
|
||||
DisableSliceFlagSeparator: true,
|
||||
// this command is always run standalone (see realCurl()), never under the root
|
||||
// command, so it needs its own copy of the key flags
|
||||
Flags: defaultKeyFlags,
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
kr, _, err := gatherKeyerFromArguments(ctx, c)
|
||||
if err != nil {
|
||||
@@ -130,5 +132,6 @@ func realCurl() error {
|
||||
}
|
||||
}
|
||||
|
||||
return curl.Run(context.Background(), keyFlags)
|
||||
// Run treats the first element as the program name, so prepend one
|
||||
return curl.Run(context.Background(), append([]string{"nak curl"}, keyFlags...))
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ var dekey = &cli.Command{
|
||||
Usage: "handles NIP-4E decoupled encryption keys",
|
||||
Description: "maybe this picture will explain better than I can do here for now: https://cdn.azzamo.net/89c543d261ad0d665c1dea78f91e527c2e39e7fe503b440265a3c47e63c9139f.png",
|
||||
DisableSliceFlagSeparator: true,
|
||||
Flags: append(defaultKeyFlags,
|
||||
Flags: combineFlags([][]cli.Flag{},
|
||||
&cli.StringFlag{
|
||||
Name: "device",
|
||||
Usage: "name of this device that will be published and displayed on other clients",
|
||||
@@ -87,7 +87,7 @@ var dekey = &cli.Command{
|
||||
// get relays for the user
|
||||
log("fetching write relays for %s\n", color.CyanString(nip19.EncodeNpub(userPub)))
|
||||
relays := sys.FetchWriteRelays(ctx, userPub)
|
||||
relayList := connectToAllRelays(ctx, c, relays, nil)
|
||||
relayList := connectToAllRelays(ctx, c, relays)
|
||||
if len(relayList) == 0 {
|
||||
return fmt.Errorf("no relays to use")
|
||||
}
|
||||
@@ -164,7 +164,7 @@ var dekey = &cli.Command{
|
||||
return fmt.Errorf("invalid main key: %w", err)
|
||||
}
|
||||
if eSec.Public() != ePub {
|
||||
return fmt.Errorf("stored decoupled encryption key is corrupted: %w", err)
|
||||
return fmt.Errorf("stored decoupled encryption key at %s doesn't match the announced key %s", eKeyPath, ePub.Hex())
|
||||
}
|
||||
} else {
|
||||
log("- decoupled encryption key not found locally, attempting to fetch the key from other devices\n")
|
||||
@@ -236,12 +236,14 @@ var dekey = &cli.Command{
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
eSec, err = nostr.SecretKeyFromHex(eSecHex)
|
||||
candidate, err := nostr.SecretKeyFromHex(eSecHex)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
// check if it matches mainPub
|
||||
if eSec.Public() == ePub {
|
||||
// check if it matches mainPub -- only keep it if it does, otherwise a
|
||||
// stale key received from another device would end up being redistributed
|
||||
if candidate.Public() == ePub {
|
||||
eSec = candidate
|
||||
log(color.GreenString("successfully received decoupled encryption key from another device\n"))
|
||||
// store it
|
||||
os.MkdirAll(filepath.Dir(eKeyPath), 0700)
|
||||
|
||||
+4
-6
@@ -13,8 +13,7 @@ var encrypt = &cli.Command{
|
||||
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,
|
||||
Flags: []cli.Flag{
|
||||
&PubKeyFlag{
|
||||
Name: "recipient-pubkey",
|
||||
Aliases: []string{"p", "tgt", "target", "pubkey", "to"},
|
||||
@@ -24,7 +23,7 @@ var encrypt = &cli.Command{
|
||||
Name: "nip04",
|
||||
Usage: "use nip04 encryption instead of nip44",
|
||||
},
|
||||
),
|
||||
},
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
target := getPubKey(c, "recipient-pubkey")
|
||||
|
||||
@@ -75,8 +74,7 @@ var decrypt = &cli.Command{
|
||||
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,
|
||||
Flags: []cli.Flag{
|
||||
&PubKeyFlag{
|
||||
Name: "sender-pubkey",
|
||||
Aliases: []string{"p", "src", "source", "pubkey", "from"},
|
||||
@@ -86,7 +84,7 @@ var decrypt = &cli.Command{
|
||||
Name: "nip04",
|
||||
Usage: "use nip04 encryption instead of nip44",
|
||||
},
|
||||
),
|
||||
},
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
source := getPubKey(c, "sender-pubkey")
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ const (
|
||||
CATEGORY_EVENT_FIELDS = "EVENT FIELDS"
|
||||
CATEGORY_SIGNER = "SIGNER OPTIONS"
|
||||
CATEGORY_EXTRAS = "EXTRAS"
|
||||
CATEGORY_AUTH = "AUTH"
|
||||
)
|
||||
|
||||
var eventCmd = &cli.Command{
|
||||
@@ -40,7 +41,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: append(defaultKeyFlags,
|
||||
Flags: combineFlags([][]cli.Flag{},
|
||||
// ~ 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{
|
||||
@@ -89,9 +90,14 @@ example:
|
||||
Usage: "print the event enveloped in a [\"EVENT\", ...] message ready to be sent to a relay",
|
||||
Category: CATEGORY_EXTRAS,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "jq",
|
||||
Usage: "filter returned events with jq expression",
|
||||
Category: CATEGORY_EXTRAS,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "auth",
|
||||
Usage: "always perform nip42 \"AUTH\" when facing an \"auth-required: \" rejection and try again",
|
||||
Name: "jq-raw",
|
||||
Usage: "print --jq string results without JSON quoting, like `jq -r`",
|
||||
Category: CATEGORY_EXTRAS,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
@@ -179,13 +185,18 @@ example:
|
||||
),
|
||||
ArgsUsage: "[relay...]",
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
relayUrls := c.Args().Slice()
|
||||
argRelayUrls := c.Args().Slice()
|
||||
|
||||
kr, sec, err := gatherKeyerFromArguments(ctx, c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
jq, err := jqPrepare(c.String("jq"), c.Bool("jq-raw"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// then process input and generate events:
|
||||
|
||||
// will reuse this
|
||||
@@ -195,7 +206,7 @@ example:
|
||||
handleEvent := func(stdinEvent string) error {
|
||||
evt.Content = ""
|
||||
evt.CreatedAt = 0
|
||||
clear(evt.Tags)
|
||||
evt.Tags = evt.Tags[:0]
|
||||
evt.ID = nostr.ZeroID
|
||||
evt.PubKey = nostr.ZeroPK
|
||||
evt.Sig = [64]byte{}
|
||||
@@ -372,6 +383,9 @@ example:
|
||||
}
|
||||
|
||||
var relays []*nostr.Relay
|
||||
// start from the given relays on every event so --outbox additions
|
||||
// for one event don't leak into the next
|
||||
relayUrls := slices.Clone(argRelayUrls)
|
||||
if len(relayUrls) > 0 || c.Bool("outbox") {
|
||||
if c.Bool("outbox") {
|
||||
if evt.PubKey != nostr.ZeroPK {
|
||||
@@ -396,10 +410,7 @@ example:
|
||||
}
|
||||
|
||||
if len(relayUrls) > 0 {
|
||||
sys.Pool.AuthRequiredHandler = func(ctx context.Context, authEvent *nostr.Event) error {
|
||||
return authSigner(ctx, c, func(s string, args ...any) {}, authEvent)
|
||||
}
|
||||
relays = connectToAllRelays(ctx, c, relayUrls, nil)
|
||||
relays = connectToAllRelays(ctx, c, relayUrls)
|
||||
if len(relays) == 0 {
|
||||
log("failed to connect to any of the given relays.\n")
|
||||
os.Exit(3)
|
||||
@@ -408,15 +419,25 @@ example:
|
||||
}
|
||||
|
||||
// print event as json
|
||||
var result string
|
||||
if c.Bool("envelope") {
|
||||
j, _ := json.Marshal(nostr.EventEnvelope{Event: evt})
|
||||
result = string(j)
|
||||
if jq == nil {
|
||||
var result string
|
||||
if c.Bool("envelope") {
|
||||
j, _ := json.Marshal(nostr.EventEnvelope{Event: evt})
|
||||
result = string(j)
|
||||
} else {
|
||||
j, _ := easyjson.Marshal(&evt)
|
||||
result = string(j)
|
||||
}
|
||||
stdout(result)
|
||||
} else {
|
||||
j, _ := easyjson.Marshal(&evt)
|
||||
result = string(j)
|
||||
out, matches, err := jq(evt)
|
||||
if err != nil {
|
||||
return fmt.Errorf("jq filter failed: %w", err)
|
||||
}
|
||||
if matches {
|
||||
stdout(out)
|
||||
}
|
||||
}
|
||||
stdout(result)
|
||||
|
||||
return publishFlow(ctx, c, kr, evt, relays)
|
||||
}
|
||||
@@ -522,9 +543,19 @@ func publishFlow(ctx context.Context, c *cli.Command, kr nostr.Signer, evt nostr
|
||||
defer cancel()
|
||||
|
||||
if !relay.IsConnected() {
|
||||
if new_, err := sys.Pool.EnsureRelay(relay.URL); err == nil {
|
||||
relays[i] = new_
|
||||
relay = new_
|
||||
nm := nostr.NormalizeURL(relay.URL)
|
||||
if r, ok := sys.Pool.Relays.Load(nm); ok && r != nil && r.IsConnected() {
|
||||
relays[i] = r
|
||||
relay = r
|
||||
} else {
|
||||
ct, cancel := context.WithTimeout(context.Background(), connectTimeout)
|
||||
new_, err := nostr.RelayConnect(ct, relay.URL, sys.Pool.RelayOptions)
|
||||
cancel()
|
||||
if err == nil {
|
||||
sys.Pool.Relays.Store(nm, new_)
|
||||
relays[i] = new_
|
||||
relay = new_
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -557,6 +588,10 @@ func publishFlow(ctx context.Context, c *cli.Command, kr nostr.Signer, evt nostr
|
||||
if len(successRelays) > 0 && c.Bool("nevent") {
|
||||
log(nip19.EncodeNevent(evt.ID, successRelays, evt.PubKey) + "\n")
|
||||
}
|
||||
|
||||
if len(successRelays) == 0 {
|
||||
return fmt.Errorf("failed to publish to any of the relays")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -3,14 +3,11 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"fiatjaf.com/nostr"
|
||||
"fiatjaf.com/nostr/nip05"
|
||||
"fiatjaf.com/nostr/nip19"
|
||||
"fiatjaf.com/nostr/nip42"
|
||||
"fiatjaf.com/nostr/sdk/hints"
|
||||
"github.com/fatih/color"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
@@ -21,21 +18,28 @@ var fetch = &cli.Command{
|
||||
nak fetch nevent1qqsxrwm0hd3s3fddh4jc2574z3xzufq6qwuyz2rvv3n087zvym3dpaqprpmhxue69uhhqatzd35kxtnjv4kxz7tfdenju6t0xpnej4
|
||||
echo npub1h8spmtw9m2huyv6v2j2qd5zv956z2zdugl6mgx02f2upffwpm3nqv0j4ps | nak fetch --relay wss://relay.nostr.band`,
|
||||
DisableSliceFlagSeparator: true,
|
||||
Flags: append(defaultKeyFlags,
|
||||
append(reqFilterFlags,
|
||||
&cli.StringSliceFlag{
|
||||
Name: "relay",
|
||||
Aliases: []string{"r"},
|
||||
Usage: "also use these relays to fetch from",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "auth",
|
||||
Usage: "always perform nip42 \"AUTH\" when facing an \"auth-required: \" rejection and try again",
|
||||
},
|
||||
)...,
|
||||
Flags: combineFlags([][]cli.Flag{reqFilterFlags},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "relay",
|
||||
Aliases: []string{"r"},
|
||||
Usage: "also use these relays to fetch from",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "jq",
|
||||
Usage: "filter returned events with jq expression",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "jq-raw",
|
||||
Usage: "print --jq string results without JSON quoting, like `jq -r`",
|
||||
},
|
||||
),
|
||||
ArgsUsage: "[nip05_or_nip19_code]",
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
jq, err := jqPrepare(c.String("jq"), c.Bool("jq-raw"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for code := range getStdinLinesOrArguments(c.Args()) {
|
||||
filter := nostr.Filter{}
|
||||
var authorHint nostr.PubKey
|
||||
@@ -70,7 +74,7 @@ var fetch = &cli.Command{
|
||||
}
|
||||
relays = append(relays, v.Relays...)
|
||||
case "note":
|
||||
filter.IDs = append(filter.IDs, value.([32]byte))
|
||||
filter.IDs = append(filter.IDs, value.(nostr.EventPointer).ID)
|
||||
case "naddr":
|
||||
v := value.(nostr.EntityPointer)
|
||||
filter.Kinds = []nostr.Kind{v.Kind}
|
||||
@@ -97,7 +101,7 @@ var fetch = &cli.Command{
|
||||
sys.Hints.Save(authorHint, nostr.NormalizeURL(url), hints.LastInHint, nostr.Now())
|
||||
}
|
||||
|
||||
for _, url := range sys.FetchOutboxRelays(ctx, authorHint, 3) {
|
||||
for _, url := range sys.FetchOutboxRelays(ctx, authorHint, 6) {
|
||||
relays = append(relays, url)
|
||||
}
|
||||
}
|
||||
@@ -115,25 +119,25 @@ var fetch = &cli.Command{
|
||||
continue
|
||||
}
|
||||
|
||||
sys.Pool.AuthRequiredHandler = func(ctx context.Context, authEvent *nostr.Event) error {
|
||||
return authSigner(ctx, c, func(s string, args ...any) {
|
||||
if strings.HasPrefix(s, "authenticating as") {
|
||||
cleanUrl, _ := strings.CutPrefix(
|
||||
nip42.GetRelayURLFromAuthEvent(*authEvent),
|
||||
"wss://",
|
||||
)
|
||||
s = "authenticating to " + color.CyanString(cleanUrl) + " as" + s[len("authenticating as"):]
|
||||
}
|
||||
log(s+"\n", args...)
|
||||
}, authEvent)
|
||||
}
|
||||
|
||||
found := false
|
||||
for ie := range sys.Pool.FetchMany(ctx, relays, filter, nostr.SubscriptionOptions{
|
||||
Label: "nak-fetch",
|
||||
}) {
|
||||
found = true
|
||||
stdout(ie.Event)
|
||||
var out string
|
||||
if jq == nil {
|
||||
out = ie.Event.String()
|
||||
} else {
|
||||
v, matches, err := jq(ie.Event)
|
||||
if err != nil {
|
||||
return fmt.Errorf("jq filter failed: %w", err)
|
||||
}
|
||||
if !matches {
|
||||
continue
|
||||
}
|
||||
out = v
|
||||
}
|
||||
stdout(out)
|
||||
}
|
||||
|
||||
if !found {
|
||||
|
||||
@@ -19,11 +19,15 @@ example:
|
||||
nak filter '{"kind": 1, "content": "hello"}' '{"kinds": [1]}' -k 0
|
||||
`,
|
||||
DisableSliceFlagSeparator: true,
|
||||
Flags: append(append([]cli.Flag{}, reqFilterFlags...),
|
||||
Flags: combineFlags([][]cli.Flag{reqFilterFlags},
|
||||
&cli.StringFlag{
|
||||
Name: "jq",
|
||||
Usage: "filter matching events with jq expression",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "jq-raw",
|
||||
Usage: "print --jq string results without JSON quoting, like `jq -r`",
|
||||
},
|
||||
),
|
||||
ArgsUsage: "[event_json] [base_filter_json]",
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
@@ -59,7 +63,7 @@ example:
|
||||
return err
|
||||
}
|
||||
|
||||
jq, err := jqPrepare(c.String("jq"))
|
||||
jq, err := jqPrepare(c.String("jq"), c.Bool("jq-raw"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -82,6 +86,9 @@ example:
|
||||
if evt.Sig == [64]byte{} {
|
||||
evt.Sig = baseEvent.Sig
|
||||
}
|
||||
if evt.Kind == 0 {
|
||||
evt.Kind = baseEvent.Kind
|
||||
}
|
||||
if evt.Content == "" {
|
||||
evt.Content = baseEvent.Content
|
||||
}
|
||||
@@ -104,7 +111,7 @@ example:
|
||||
if !matches {
|
||||
continue
|
||||
}
|
||||
out, _ = json.MarshalToString(v)
|
||||
out = v
|
||||
}
|
||||
stdout(out)
|
||||
} else {
|
||||
|
||||
@@ -131,6 +131,45 @@ func getNaturalDate(cmd *cli.Command, name string) nostr.Timestamp {
|
||||
//
|
||||
//
|
||||
|
||||
type (
|
||||
SecretKeyFlag = cli.FlagBase[nostr.SecretKey, struct{}, secretkeyValue]
|
||||
)
|
||||
|
||||
type secretkeyValue struct {
|
||||
secretkey nostr.SecretKey
|
||||
hasBeenSet bool
|
||||
}
|
||||
|
||||
var _ cli.ValueCreator[nostr.SecretKey, struct{}] = secretkeyValue{}
|
||||
|
||||
func (t secretkeyValue) Create(val nostr.SecretKey, p *nostr.SecretKey, c struct{}) cli.Value {
|
||||
*p = val
|
||||
return &secretkeyValue{
|
||||
secretkey: val,
|
||||
}
|
||||
}
|
||||
|
||||
func (t secretkeyValue) ToString(b nostr.SecretKey) string { return t.secretkey.String() }
|
||||
|
||||
func (t *secretkeyValue) Set(value string) error {
|
||||
secretkey, err := parseSecretKey(value)
|
||||
t.secretkey = secretkey
|
||||
t.hasBeenSet = true
|
||||
return err
|
||||
}
|
||||
|
||||
func (t *secretkeyValue) String() string { return fmt.Sprintf("%#v", t.secretkey) }
|
||||
func (t *secretkeyValue) Value() nostr.SecretKey { return t.secretkey }
|
||||
func (t *secretkeyValue) Get() any { return t.secretkey }
|
||||
|
||||
func getSecretKey(cmd *cli.Command, name string) nostr.SecretKey {
|
||||
return cmd.Value(name).(nostr.SecretKey)
|
||||
}
|
||||
|
||||
//
|
||||
//
|
||||
//
|
||||
|
||||
type (
|
||||
PubKeyFlag = cli.FlagBase[nostr.PubKey, struct{}, pubkeyValue]
|
||||
)
|
||||
|
||||
@@ -24,7 +24,7 @@ var fsCmd = &cli.Command{
|
||||
Usage: "mount a FUSE filesystem that exposes Nostr events as files.",
|
||||
Description: `(experimental)`,
|
||||
ArgsUsage: "<mountpoint>",
|
||||
Flags: append(defaultKeyFlags,
|
||||
Flags: combineFlags([][]cli.Flag{},
|
||||
&PubKeyFlag{
|
||||
Name: "pubkey",
|
||||
Usage: "public key from where to to prepopulate directories",
|
||||
|
||||
@@ -23,7 +23,7 @@ var fsCmd = &cli.Command{
|
||||
Usage: "mount a FUSE filesystem that exposes Nostr events as files.",
|
||||
Description: `(experimental)`,
|
||||
ArgsUsage: "<mountpoint>",
|
||||
Flags: append(defaultKeyFlags,
|
||||
Flags: combineFlags([][]cli.Flag{},
|
||||
&PubKeyFlag{
|
||||
Name: "pubkey",
|
||||
Usage: "public key from where to to prepopulate directories",
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ var fsCmd = &cli.Command{
|
||||
Usage: "mount a FUSE filesystem that exposes Nostr events as files.",
|
||||
Description: `(experimental)`,
|
||||
ArgsUsage: "<mountpoint>",
|
||||
Flags: append(defaultKeyFlags,
|
||||
Flags: combineFlags([][]cli.Flag{},
|
||||
&PubKeyFlag{
|
||||
Name: "pubkey",
|
||||
Usage: "public key from where to to prepopulate directories",
|
||||
|
||||
@@ -24,7 +24,6 @@ var gift = &cli.Command{
|
||||
|
||||
a decoupled key (if it has been created or received with "nak dekey" previously) will be used by default.`,
|
||||
DisableSliceFlagSeparator: true,
|
||||
Flags: defaultKeyFlags,
|
||||
Commands: []*cli.Command{
|
||||
{
|
||||
Name: "wrap",
|
||||
@@ -333,7 +332,7 @@ func getDecoupledEncryptionSecretKey(ctx context.Context, configPath string, pub
|
||||
return [32]byte{}, true, fmt.Errorf("invalid main key: %w", err)
|
||||
}
|
||||
if eSec.Public() != ePub {
|
||||
return [32]byte{}, true, fmt.Errorf("stored decoupled encryption key is corrupted: %w", err)
|
||||
return [32]byte{}, true, fmt.Errorf("stored decoupled encryption key at %s doesn't match the announced key %s", eKeyPath, ePub.Hex())
|
||||
}
|
||||
return eSec, true, nil
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ module github.com/fiatjaf/nak
|
||||
go 1.25
|
||||
|
||||
require (
|
||||
fiatjaf.com/nostr v0.0.0-20260603164911-395c9609550b
|
||||
fiatjaf.com/nostr v0.0.0-20260716191248-c205ed45b97e
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7
|
||||
github.com/bep/debounce v1.2.1
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.3.6
|
||||
@@ -29,8 +29,10 @@ require (
|
||||
|
||||
require (
|
||||
fiatjaf.com/lib v0.3.7
|
||||
github.com/denisbrodbeck/machineid v1.0.1
|
||||
github.com/hanwen/go-fuse/v2 v2.9.0
|
||||
github.com/itchyny/gojq v0.12.19
|
||||
github.com/lithammer/fuzzysearch v1.1.8
|
||||
github.com/mattn/go-tty/v2 v2.0.0
|
||||
)
|
||||
|
||||
@@ -77,7 +79,6 @@ require (
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/lithammer/fuzzysearch v1.1.8 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/magefile/mage v1.14.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
fiatjaf.com/lib v0.3.7 h1:mXZOn7NrUcjSdy4oNvwQyAmes7Ueb+Zr5hjqMIe2dxI=
|
||||
fiatjaf.com/lib v0.3.7/go.mod h1:UlHaZvPHj25PtKLh9GjZkUHRmQ2xZ8Jkoa4VRaLeeQ8=
|
||||
fiatjaf.com/nostr v0.0.0-20260603164911-395c9609550b h1:uFCYH+AyyhyqL9a04BEJQ/vEZ9QP793mJWXUkIHKxWc=
|
||||
fiatjaf.com/nostr v0.0.0-20260603164911-395c9609550b/go.mod h1:b1EIUDnd133Ie8Pg8O/biaKdFyCMz28aD4n64g1GqvM=
|
||||
fiatjaf.com/nostr v0.0.0-20260716191248-c205ed45b97e h1:/vgoytiH4qQ28O/oX1hj656X69CnTy1pGj/17HFjH+A=
|
||||
fiatjaf.com/nostr v0.0.0-20260716191248-c205ed45b97e/go.mod h1:b1EIUDnd133Ie8Pg8O/biaKdFyCMz28aD4n64g1GqvM=
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
|
||||
github.com/FastFilter/xorfilter v0.2.1 h1:lbdeLG9BdpquK64ZsleBS8B4xO/QW1IM0gMzF7KaBKc=
|
||||
@@ -102,6 +102,8 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeC
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
|
||||
github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218=
|
||||
github.com/denisbrodbeck/machineid v1.0.1 h1:geKr9qtkB876mXguW2X6TU4ZynleN6ezuMSRhl4D7AQ=
|
||||
github.com/denisbrodbeck/machineid v1.0.1/go.mod h1:dJUwb7PTidGDeYyUBmXZ2GphQBbjJCrnectwCyxcUSI=
|
||||
github.com/dgraph-io/ristretto/v2 v2.3.0 h1:qTQ38m7oIyd4GAed/QkUZyPFNMnvVWyazGXRwvOt5zk=
|
||||
github.com/dgraph-io/ristretto/v2 v2.3.0/go.mod h1:gpoRV3VzrEY1a9dWAYV6T1U7YzfgttXdd/ZzL1s9OZM=
|
||||
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38=
|
||||
|
||||
@@ -16,7 +16,6 @@ import (
|
||||
"fiatjaf.com/nostr"
|
||||
"fiatjaf.com/nostr/nip11"
|
||||
"fiatjaf.com/nostr/nip29"
|
||||
"fiatjaf.com/nostr/nip42"
|
||||
"github.com/fatih/color"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
@@ -28,26 +27,8 @@ var group = &cli.Command{
|
||||
Description: `manage and interact with Nostr communities (NIP-29). Use "nak group <subcommand> <relay>'<identifier>" where host.tld is the relay and identifier is the group identifier.`,
|
||||
DisableSliceFlagSeparator: true,
|
||||
ArgsUsage: "<subcommand> <relay>'<identifier> [flags]",
|
||||
Flags: append(defaultKeyFlags,
|
||||
&cli.BoolFlag{
|
||||
Name: "auth",
|
||||
Usage: "always perform nip42 \"AUTH\" when facing an \"auth-required: \" rejection and try again",
|
||||
},
|
||||
),
|
||||
Flags: combineFlags([][]cli.Flag{}),
|
||||
Before: func(ctx context.Context, c *cli.Command) (context.Context, error) {
|
||||
sys.Pool.AuthRequiredHandler = func(ctx context.Context, authEvent *nostr.Event) error {
|
||||
return authSigner(ctx, c, func(s string, args ...any) {
|
||||
if strings.HasPrefix(s, "authenticating as") {
|
||||
cleanUrl, _ := strings.CutPrefix(
|
||||
nip42.GetRelayURLFromAuthEvent(*authEvent),
|
||||
"wss://",
|
||||
)
|
||||
s = "authenticating to " + color.CyanString(cleanUrl) + " as" + s[len("authenticating as"):]
|
||||
}
|
||||
log(s+"\n", args...)
|
||||
}, authEvent)
|
||||
}
|
||||
|
||||
return ctx, nil
|
||||
},
|
||||
Commands: []*cli.Command{
|
||||
@@ -262,9 +243,17 @@ var group = &cli.Command{
|
||||
return err
|
||||
}
|
||||
|
||||
r, err := sys.Pool.EnsureRelay(relay)
|
||||
if err != nil {
|
||||
return err
|
||||
nm := nostr.NormalizeURL(relay)
|
||||
r, ok := sys.Pool.Relays.Load(nm)
|
||||
if !ok || r == nil || !r.IsConnected() {
|
||||
ct, cancel := context.WithTimeout(context.Background(), connectTimeout)
|
||||
var err error
|
||||
r, err = nostr.RelayConnect(ct, relay, sys.Pool.RelayOptions)
|
||||
cancel()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sys.Pool.Relays.Store(nm, r)
|
||||
}
|
||||
|
||||
sub, err := r.Subscribe(ctx, nostr.Filter{
|
||||
@@ -335,11 +324,19 @@ var group = &cli.Command{
|
||||
return fmt.Errorf("failed to sign message: %w", err)
|
||||
}
|
||||
|
||||
if r, err := sys.Pool.EnsureRelay(relay); err != nil {
|
||||
return err
|
||||
} else {
|
||||
return r.Publish(ctx, msg)
|
||||
nm := nostr.NormalizeURL(relay)
|
||||
r, ok := sys.Pool.Relays.Load(nm)
|
||||
if !ok || r == nil || !r.IsConnected() {
|
||||
ct, cancel := context.WithTimeout(context.Background(), connectTimeout)
|
||||
var err error
|
||||
r, err = nostr.RelayConnect(ct, relay, sys.Pool.RelayOptions)
|
||||
cancel()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sys.Pool.Relays.Store(nm, r)
|
||||
}
|
||||
return r.Publish(ctx, msg)
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -493,9 +490,17 @@ write your forum post
|
||||
return fmt.Errorf("failed to sign forum topic event: %w", err)
|
||||
}
|
||||
|
||||
r, err := sys.Pool.EnsureRelay(relay)
|
||||
if err != nil {
|
||||
return err
|
||||
nm := nostr.NormalizeURL(relay)
|
||||
r, ok := sys.Pool.Relays.Load(nm)
|
||||
if !ok || r == nil || !r.IsConnected() {
|
||||
ct, cancel := context.WithTimeout(context.Background(), connectTimeout)
|
||||
var err error
|
||||
r, err = nostr.RelayConnect(ct, relay, sys.Pool.RelayOptions)
|
||||
cancel()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sys.Pool.Relays.Store(nm, r)
|
||||
}
|
||||
|
||||
return r.Publish(ctx, evt)
|
||||
@@ -600,9 +605,17 @@ write your forum post
|
||||
return fmt.Errorf("failed to sign forum comment event: %w", err)
|
||||
}
|
||||
|
||||
r, err := sys.Pool.EnsureRelay(relay)
|
||||
if err != nil {
|
||||
return err
|
||||
nm := nostr.NormalizeURL(relay)
|
||||
r, ok := sys.Pool.Relays.Load(nm)
|
||||
if !ok || r == nil || !r.IsConnected() {
|
||||
ct, cancel := context.WithTimeout(context.Background(), connectTimeout)
|
||||
var err error
|
||||
r, err = nostr.RelayConnect(ct, relay, sys.Pool.RelayOptions)
|
||||
cancel()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sys.Pool.Relays.Store(nm, r)
|
||||
}
|
||||
|
||||
return r.Publish(ctx, evt)
|
||||
@@ -714,6 +727,18 @@ write your forum post
|
||||
Name: "all-kinds",
|
||||
Usage: "specify this to delete the supported_kinds property, meaning everything will be supported",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "parent",
|
||||
Usage: "set parent group identifier",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "children",
|
||||
Usage: "set child group identifiers (semicolon-separated)",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "orphan",
|
||||
Usage: "remove parent tag, making this group an orphan",
|
||||
},
|
||||
},
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
relay, identifier, err := parseGroupIdentifier(c)
|
||||
@@ -778,6 +803,18 @@ write your forum post
|
||||
} else if c.Bool("all-kinds") {
|
||||
group.SupportedKinds = nil
|
||||
}
|
||||
if parent := c.String("parent"); parent != "" {
|
||||
group.Parent = parent
|
||||
}
|
||||
if children := c.String("children"); children != "" {
|
||||
group.Children = strings.Split(children, ";")
|
||||
for i := range group.Children {
|
||||
group.Children[i] = strings.TrimSpace(group.Children[i])
|
||||
}
|
||||
}
|
||||
if c.Bool("orphan") {
|
||||
group.Parent = ""
|
||||
}
|
||||
|
||||
return publishModerationEvent(ctx, c, 9002, func(evt *nostr.Event, args []string) error {
|
||||
evt.Tags = append(evt.Tags, nostr.Tag{"name", group.Name})
|
||||
@@ -808,6 +845,12 @@ write your forum post
|
||||
}
|
||||
evt.Tags = append(evt.Tags, tag)
|
||||
}
|
||||
if group.Parent != "" {
|
||||
evt.Tags = append(evt.Tags, nostr.Tag{"parent", group.Parent})
|
||||
}
|
||||
for _, child := range group.Children {
|
||||
evt.Tags = append(evt.Tags, nostr.Tag{"child", child})
|
||||
}
|
||||
return nil
|
||||
})
|
||||
},
|
||||
@@ -895,9 +938,17 @@ func publishModerationEvent(ctx context.Context, c *cli.Command, kind nostr.Kind
|
||||
|
||||
stdout(evt.String())
|
||||
|
||||
r, err := sys.Pool.EnsureRelay(relay)
|
||||
if err != nil {
|
||||
return err
|
||||
nm := nostr.NormalizeURL(relay)
|
||||
r, ok := sys.Pool.Relays.Load(nm)
|
||||
if !ok || r == nil || !r.IsConnected() {
|
||||
ct, cancel := context.WithTimeout(context.Background(), connectTimeout)
|
||||
var err error
|
||||
r, err = nostr.RelayConnect(ct, relay, sys.Pool.RelayOptions)
|
||||
cancel()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sys.Pool.Relays.Store(nm, r)
|
||||
}
|
||||
|
||||
return r.Publish(ctx, evt)
|
||||
|
||||
+98
-73
@@ -22,7 +22,6 @@ import (
|
||||
"fiatjaf.com/nostr"
|
||||
"fiatjaf.com/nostr/nip05"
|
||||
"fiatjaf.com/nostr/nip19"
|
||||
"fiatjaf.com/nostr/nip42"
|
||||
"fiatjaf.com/nostr/schema"
|
||||
"fiatjaf.com/nostr/sdk"
|
||||
"github.com/chzyer/readline"
|
||||
@@ -49,6 +48,8 @@ var (
|
||||
stdout = func(args ...any) { fmt.Fprintln(color.Output, args...) }
|
||||
)
|
||||
|
||||
var connectTimeout = 2 * time.Second
|
||||
|
||||
func isPiped() bool {
|
||||
stat, err := os.Stdin.Stat()
|
||||
if err != nil {
|
||||
@@ -188,7 +189,6 @@ func connectToAllRelays(
|
||||
ctx context.Context,
|
||||
c *cli.Command,
|
||||
relayUrls []string,
|
||||
preAuthSigner func(ctx context.Context, c *cli.Command, log func(s string, args ...any), authEvent *nostr.Event) (err error), // if this exists we will force preauth
|
||||
) []*nostr.Relay {
|
||||
// first pass to check if these are valid relay URLs
|
||||
for i, url := range relayUrls {
|
||||
@@ -221,21 +221,28 @@ func connectToAllRelays(
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(len(relayUrls))
|
||||
mu := sync.Mutex{} // guards lines and relays, written from the goroutines below
|
||||
for i, url := range relayUrls {
|
||||
lines[i] = make([][]byte, 1, 2)
|
||||
logthis := func(s string, args ...any) {
|
||||
mu.Lock()
|
||||
lines[i] = append(lines[i], []byte(fmt.Sprintf(s, args...)))
|
||||
render()
|
||||
mu.Unlock()
|
||||
}
|
||||
colorizepreamble := func(c func(string, ...any) string) {
|
||||
mu.Lock()
|
||||
lines[i][0] = []byte(fmt.Sprintf("%s... ", c(url)))
|
||||
mu.Unlock()
|
||||
}
|
||||
colorizepreamble(color.CyanString)
|
||||
|
||||
go func() {
|
||||
relay := connectToSingleRelay(ctx, c, url, preAuthSigner, colorizepreamble, logthis)
|
||||
relay := connectToSingleRelay(ctx, c, url, colorizepreamble, logthis)
|
||||
if relay != nil {
|
||||
mu.Lock()
|
||||
relays = append(relays, relay)
|
||||
mu.Unlock()
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
@@ -245,7 +252,7 @@ func connectToAllRelays(
|
||||
// simple flow
|
||||
for _, url := range relayUrls {
|
||||
log("connecting to %s... ", color.CyanString(strings.Split(url, "/")[2]))
|
||||
relay := connectToSingleRelay(ctx, c, url, preAuthSigner, nil, log)
|
||||
relay := connectToSingleRelay(ctx, c, url, nil, log)
|
||||
if relay != nil {
|
||||
relays = append(relays, relay)
|
||||
}
|
||||
@@ -260,67 +267,78 @@ func connectToSingleRelay(
|
||||
ctx context.Context,
|
||||
c *cli.Command,
|
||||
url string,
|
||||
preAuthSigner func(ctx context.Context, c *cli.Command, log func(s string, args ...any), authEvent *nostr.Event) (err error),
|
||||
colorizepreamble func(c func(string, ...any) string),
|
||||
logthis func(s string, args ...any),
|
||||
) *nostr.Relay {
|
||||
if relay, err := sys.Pool.EnsureRelay(url); err == nil {
|
||||
if preAuthSigner != nil {
|
||||
if colorizepreamble != nil {
|
||||
colorizepreamble(color.YellowString)
|
||||
}
|
||||
logthis("waiting for auth challenge... ")
|
||||
time.Sleep(time.Millisecond * 200)
|
||||
nm := nostr.NormalizeURL(url)
|
||||
|
||||
for range 5 {
|
||||
if err := relay.Auth(ctx, func(ctx context.Context, authEvent *nostr.Event) error {
|
||||
challengeTag := authEvent.Tags.Find("challenge")
|
||||
if challengeTag == nil || len(challengeTag) < 2 || challengeTag[1] == "" {
|
||||
return fmt.Errorf("auth not received yet *****") // what a giant hack
|
||||
}
|
||||
return preAuthSigner(ctx, c, logthis, authEvent)
|
||||
}); err == nil {
|
||||
// auth succeeded
|
||||
goto preauthSuccess
|
||||
} else {
|
||||
// auth failed
|
||||
if strings.HasSuffix(err.Error(), "auth not received yet *****") {
|
||||
// it failed because we didn't receive the challenge yet, so keep waiting
|
||||
time.Sleep(time.Second)
|
||||
continue
|
||||
} else {
|
||||
// it failed for some other reason, so skip this relay
|
||||
if colorizepreamble != nil {
|
||||
colorizepreamble(colors.errorf)
|
||||
}
|
||||
logthis(err.Error())
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
relay, ok := sys.Pool.Relays.Load(nm)
|
||||
if !ok || relay == nil || !relay.IsConnected() {
|
||||
connectCtx, cancel := context.WithTimeout(context.Background(), connectTimeout)
|
||||
defer cancel()
|
||||
|
||||
var err error
|
||||
if relay, err = nostr.RelayConnect(connectCtx, url, sys.Pool.RelayOptions); err != nil {
|
||||
if colorizepreamble != nil {
|
||||
colorizepreamble(colors.errorf)
|
||||
}
|
||||
logthis("failed to get an AUTH challenge in enough time.")
|
||||
logthis(clampError(err, len(url)+12))
|
||||
return nil
|
||||
}
|
||||
|
||||
preauthSuccess:
|
||||
sys.Pool.Relays.Store(nm, relay)
|
||||
go func(r *nostr.Relay, relayURL string) {
|
||||
<-r.Context().Done()
|
||||
if current, ok := sys.Pool.Relays.Load(relayURL); ok && current == r {
|
||||
sys.Pool.Relays.Delete(relayURL)
|
||||
}
|
||||
}(relay, nm)
|
||||
}
|
||||
|
||||
if c.Bool("force-pre-auth") {
|
||||
if colorizepreamble != nil {
|
||||
colorizepreamble(colors.successf)
|
||||
colorizepreamble(color.YellowString)
|
||||
}
|
||||
logthis("waiting for auth challenge... ")
|
||||
time.Sleep(time.Millisecond * 200)
|
||||
|
||||
for range 5 {
|
||||
if err := relay.Auth(ctx, func(ctx context.Context, authEvent *nostr.Event) error {
|
||||
challengeTag := authEvent.Tags.Find("challenge")
|
||||
if challengeTag == nil || len(challengeTag) < 2 || challengeTag[1] == "" {
|
||||
return fmt.Errorf("auth not received yet *****") // what a giant hack
|
||||
}
|
||||
return authSigner(ctx, c, logthis, authEvent)
|
||||
}); err == nil {
|
||||
// auth succeeded
|
||||
goto preauthSuccess
|
||||
} else {
|
||||
// auth failed
|
||||
if strings.HasSuffix(err.Error(), "auth not received yet *****") {
|
||||
time.Sleep(time.Second)
|
||||
continue
|
||||
} else {
|
||||
if colorizepreamble != nil {
|
||||
colorizepreamble(colors.errorf)
|
||||
}
|
||||
logthis(err.Error())
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
logthis("ok.")
|
||||
return relay
|
||||
} else {
|
||||
if colorizepreamble != nil {
|
||||
colorizepreamble(colors.errorf)
|
||||
}
|
||||
|
||||
// if we're here that means we've failed to connect, this may be a huge message
|
||||
// but we're likely to only be interested in the lowest level error (although we can leave space)
|
||||
logthis(clampError(err, len(url)+12))
|
||||
logthis("failed to get an AUTH challenge in enough time.")
|
||||
return nil
|
||||
}
|
||||
|
||||
preauthSuccess:
|
||||
if colorizepreamble != nil {
|
||||
colorizepreamble(colors.successf)
|
||||
}
|
||||
logthis("ok.")
|
||||
return relay
|
||||
}
|
||||
|
||||
func clearLines(lineCount int) {
|
||||
@@ -352,29 +370,6 @@ func supportsDynamicMultilineMagic() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func authSigner(ctx context.Context, c *cli.Command, log func(s string, args ...any), authEvent *nostr.Event) (err error) {
|
||||
defer func() {
|
||||
if err != nil {
|
||||
cleanUrl, _ := strings.CutPrefix(nip42.GetRelayURLFromAuthEvent(*authEvent), "wss://")
|
||||
log("%s auth failed: %s", colors.errorf(cleanUrl), err)
|
||||
}
|
||||
}()
|
||||
|
||||
if !c.Bool("auth") && !c.Bool("force-pre-auth") {
|
||||
return fmt.Errorf("auth required, but --auth flag not given")
|
||||
}
|
||||
kr, _, err := gatherKeyerFromArguments(ctx, c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pk, _ := kr.GetPublicKey(ctx)
|
||||
npub := nip19.EncodeNpub(pk)
|
||||
log("authenticating as %s... ", color.YellowString("%s…%s", npub[0:7], npub[58:]))
|
||||
|
||||
return kr.SignEvent(ctx, authEvent)
|
||||
}
|
||||
|
||||
func lineProcessingError(ctx context.Context, msg string, args ...any) context.Context {
|
||||
log(msg+"\n", args...)
|
||||
return context.WithValue(ctx, LINE_PROCESSING_ERROR, true)
|
||||
@@ -504,6 +499,19 @@ func parsePubKey(value string) (nostr.PubKey, error) {
|
||||
return nostr.PubKey{}, fmt.Errorf("invalid pubkey (\"%s\"): expected hex, npub, or nprofile", value)
|
||||
}
|
||||
|
||||
func parseSecretKey(input string) (nostr.SecretKey, error) {
|
||||
if prefix, ski, err := nip19.Decode(input); err == nil && prefix == "nsec" {
|
||||
return ski.(nostr.SecretKey), nil
|
||||
}
|
||||
|
||||
sk, err := nostr.SecretKeyFromHex(input)
|
||||
if err != nil {
|
||||
return nostr.SecretKey{}, fmt.Errorf("invalid secret key: %w", err)
|
||||
}
|
||||
|
||||
return sk, nil
|
||||
}
|
||||
|
||||
func parseEventID(value string) (nostr.ID, error) {
|
||||
id, err := nostr.IDFromHex(value)
|
||||
if err == nil {
|
||||
@@ -513,8 +521,8 @@ func parseEventID(value string) (nostr.ID, error) {
|
||||
if prefix, decoded, err := nip19.Decode(value); err == nil {
|
||||
switch prefix {
|
||||
case "note":
|
||||
if id, ok := decoded.(nostr.ID); ok {
|
||||
return id, nil
|
||||
if event, ok := decoded.(nostr.EventPointer); ok {
|
||||
return event.ID, nil
|
||||
}
|
||||
case "nevent":
|
||||
if event, ok := decoded.(nostr.EventPointer); ok {
|
||||
@@ -633,6 +641,9 @@ func getSchema() (schema.Schema, error) {
|
||||
|
||||
func stringToKind(value string) (nostr.Kind, error) {
|
||||
if n, err := strconv.Atoi(value); err == nil && n >= 0 {
|
||||
if n > 65535 {
|
||||
return 0, fmt.Errorf("kind number %d is out of range (0-65535)", n)
|
||||
}
|
||||
return nostr.Kind(n), nil
|
||||
}
|
||||
|
||||
@@ -697,3 +708,17 @@ var colors = struct {
|
||||
color.New(color.Bold, color.FgHiGreen).Sprint,
|
||||
color.New(color.Bold, color.FgHiGreen).Sprintf,
|
||||
}
|
||||
|
||||
func combineFlags(flagSlices [][]cli.Flag, extraFlags ...cli.Flag) []cli.Flag {
|
||||
total := 0
|
||||
for _, s := range flagSlices {
|
||||
total += len(s)
|
||||
}
|
||||
total += len(extraFlags)
|
||||
result := make([]cli.Flag, 0, total)
|
||||
for _, s := range flagSlices {
|
||||
result = append(result, s...)
|
||||
}
|
||||
result = append(result, extraFlags...)
|
||||
return result
|
||||
}
|
||||
|
||||
+47
-46
@@ -2,45 +2,35 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"fiatjaf.com/nostr"
|
||||
"fiatjaf.com/nostr/keyer"
|
||||
"fiatjaf.com/nostr/nip19"
|
||||
"fiatjaf.com/nostr/nip42"
|
||||
"fiatjaf.com/nostr/nip46"
|
||||
"fiatjaf.com/nostr/nip49"
|
||||
"github.com/chzyer/readline"
|
||||
"github.com/denisbrodbeck/machineid"
|
||||
"github.com/fatih/color"
|
||||
"github.com/mattn/go-tty/v2"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
var defaultKey = nostr.KeyOne.Hex()
|
||||
// the default secret key we will use when "--sec" is not provided
|
||||
func defaultKey() nostr.SecretKey {
|
||||
mid, err := machineid.ID()
|
||||
if err != nil {
|
||||
k := nostr.SecretKey{}
|
||||
k[29] = 'n'
|
||||
k[30] = 'a'
|
||||
k[31] = 'k'
|
||||
return k
|
||||
}
|
||||
|
||||
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 '01'",
|
||||
Category: CATEGORY_SIGNER,
|
||||
Sources: cli.EnvVars("NOSTR_SECRET_KEY"),
|
||||
Value: defaultKey,
|
||||
HideDefault: true,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "prompt-sec",
|
||||
Usage: "prompt the user to paste a hex or nsec with which to sign the event",
|
||||
Category: CATEGORY_SIGNER,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "connect-as",
|
||||
Usage: "private key to use when communicating with nip46 bunkers",
|
||||
DefaultText: "a random key",
|
||||
Category: CATEGORY_SIGNER,
|
||||
Sources: cli.EnvVars("NOSTR_CLIENT_KEY"),
|
||||
},
|
||||
return sha256.Sum256([]byte(mid))
|
||||
}
|
||||
|
||||
func gatherKeyerFromArguments(ctx context.Context, c *cli.Command) (nostr.Keyer, nostr.SecretKey, error) {
|
||||
@@ -50,10 +40,10 @@ func gatherKeyerFromArguments(ctx context.Context, c *cli.Command) (nostr.Keyer,
|
||||
}
|
||||
|
||||
var kr nostr.Keyer
|
||||
if bunker != nil {
|
||||
kr = keyer.NewBunkerSignerFromBunkerClient(bunker)
|
||||
} else {
|
||||
if bunker == nil {
|
||||
kr = keyer.NewPlainKeySigner(key)
|
||||
} else {
|
||||
kr = keyer.NewBunkerSignerFromBunkerClient(bunker)
|
||||
}
|
||||
|
||||
return kr, key, nil
|
||||
@@ -61,25 +51,15 @@ func gatherKeyerFromArguments(ctx context.Context, c *cli.Command) (nostr.Keyer,
|
||||
|
||||
func gatherSecretKeyOrBunkerFromArguments(ctx context.Context, c *cli.Command) (nostr.SecretKey, *nip46.BunkerClient, error) {
|
||||
sec := c.String("sec")
|
||||
|
||||
if strings.HasPrefix(sec, "bunker://") {
|
||||
// it's a bunker
|
||||
bunkerURL := sec
|
||||
clientKeyHex := c.String("connect-as")
|
||||
var clientKey nostr.SecretKey
|
||||
|
||||
if clientKeyHex != "" {
|
||||
var err error
|
||||
clientKey, err = nostr.SecretKeyFromHex(clientKeyHex)
|
||||
if err != nil {
|
||||
return nostr.SecretKey{}, nil, fmt.Errorf("bunker client key '%s' is invalid: %w", clientKeyHex, err)
|
||||
}
|
||||
} else {
|
||||
clientKey = nostr.Generate()
|
||||
}
|
||||
|
||||
clientKey := getSecretKey(c, "connect-as")
|
||||
logverbose("[nip46]: connecting to %s with client key %s\n", bunkerURL, clientKey.Hex())
|
||||
|
||||
bunker, err := nip46.ConnectBunker(ctx, clientKey, bunkerURL, nil, func(s string) {
|
||||
bunker, err := nip46.ConnectBunker(ctx, clientKey, bunkerURL, sys.Pool, func(s string) {
|
||||
log(color.CyanString("[nip46]: open the following URL: %s"), s)
|
||||
})
|
||||
if err != nil {
|
||||
@@ -105,16 +85,37 @@ func gatherSecretKeyOrBunkerFromArguments(ctx context.Context, c *cli.Command) (
|
||||
return sk, nil, nil
|
||||
}
|
||||
|
||||
if prefix, ski, err := nip19.Decode(sec); err == nil && prefix == "nsec" {
|
||||
return ski.(nostr.SecretKey), nil, nil
|
||||
sk, err := parseSecretKey(sec)
|
||||
return sk, nil, err
|
||||
}
|
||||
|
||||
func authSigner(ctx context.Context, c *cli.Command, log func(s string, args ...any), authEvent *nostr.Event) (err error) {
|
||||
defer func() {
|
||||
if err != nil {
|
||||
cleanUrl, _ := strings.CutPrefix(nip42.GetRelayURLFromAuthEvent(*authEvent), "wss://")
|
||||
log("%s auth failed: %s\n", colors.errorf(cleanUrl), err)
|
||||
}
|
||||
}()
|
||||
|
||||
if !c.Bool("auth") && !c.Bool("force-pre-auth") {
|
||||
return fmt.Errorf("auth required, but --auth flag not given")
|
||||
}
|
||||
|
||||
sk, err := nostr.SecretKeyFromHex(sec)
|
||||
if err != nil {
|
||||
return nostr.SecretKey{}, nil, fmt.Errorf("invalid secret key: %w", err)
|
||||
var kr nostr.Keyer
|
||||
|
||||
if nip46.IsBunkerClientOperation(ctx) {
|
||||
kr = keyer.NewPlainKeySigner(getSecretKey(c, "connect-as"))
|
||||
} else {
|
||||
kr, _, err = gatherKeyerFromArguments(ctx, c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return sk, nil, nil
|
||||
pk, _ := kr.GetPublicKey(ctx)
|
||||
log("authenticating as %s... ", color.YellowString("%s…", pk.Hex()[0:16]))
|
||||
|
||||
return kr.SignEvent(ctx, authEvent)
|
||||
}
|
||||
|
||||
func promptDecrypt(ncryptsec string) (nostr.SecretKey, error) {
|
||||
|
||||
@@ -14,11 +14,16 @@ def value(tagName): tag(tagName)[1];
|
||||
def has(tagName): (tags(tagName) | length) > 0;
|
||||
def hasnt(tagName): (tags(tagName) | length) == 0;
|
||||
def has_value(tagName; tagValue): tags(tagName) | map(select(.[1] == tagValue)) | length > 0;
|
||||
def date: .created_at | gmtime | strftime("%Y-%m-%d");
|
||||
def time: .created_at | gmtime | strftime("%H:%M:%S");
|
||||
def datetime: .created_at | gmtime | strftime("%Y-%m-%dT%H:%M:%SZ");
|
||||
`
|
||||
|
||||
type jqProcessor func(nostr.Event) (any, bool, error)
|
||||
// jqProcessor runs the compiled jq expression against an event and returns the
|
||||
// rendered output string. When matches is false the event should be skipped.
|
||||
type jqProcessor func(nostr.Event) (out string, matches bool, err error)
|
||||
|
||||
func jqPrepare(expr string) (jqProcessor, error) {
|
||||
func jqPrepare(expr string, raw bool) (jqProcessor, error) {
|
||||
if expr == "" {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -33,30 +38,43 @@ func jqPrepare(expr string) (jqProcessor, error) {
|
||||
return nil, fmt.Errorf("failed to compile jq expression: %w", err)
|
||||
}
|
||||
|
||||
return func(evt nostr.Event) (any, bool, error) {
|
||||
return func(evt nostr.Event) (string, bool, error) {
|
||||
input, err := toJQInput(evt)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
return "", false, err
|
||||
}
|
||||
|
||||
iter := code.Run(input)
|
||||
for {
|
||||
v, ok := iter.Next()
|
||||
if !ok {
|
||||
return v, false, nil
|
||||
return "", false, nil
|
||||
}
|
||||
|
||||
if err, ok := v.(error); ok {
|
||||
return v, false, err
|
||||
return "", false, err
|
||||
}
|
||||
|
||||
if jqTruthy(v) {
|
||||
return v, true, nil
|
||||
return jqStringify(v, raw), true, nil
|
||||
}
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
// jqStringify renders a jq result value. In raw mode string results are printed
|
||||
// without JSON quoting (like `jq -r`); non-string results are always
|
||||
// JSON-encoded.
|
||||
func jqStringify(v any, raw bool) string {
|
||||
if raw {
|
||||
if s, ok := v.(string); ok {
|
||||
return s
|
||||
}
|
||||
}
|
||||
out, _ := json.MarshalToString(v)
|
||||
return out
|
||||
}
|
||||
|
||||
func toJQInput(v any) (any, error) {
|
||||
data, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
|
||||
@@ -28,6 +28,7 @@ var key = &cli.Command{
|
||||
decryptKey,
|
||||
combine,
|
||||
validate,
|
||||
defaultCommand,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -56,7 +57,21 @@ var public = &cli.Command{
|
||||
},
|
||||
},
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
for sk := range getSecretKeysFromStdinLinesOrSlice(ctx, c, c.Args().Slice()) {
|
||||
args := c.Args().Slice()
|
||||
if len(args) == 0 && !isPiped() {
|
||||
kr, _, err := gatherKeyerFromArguments(ctx, c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pk, err := kr.GetPublicKey(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get public key: %w", err)
|
||||
}
|
||||
stdout(pk.Hex())
|
||||
return nil
|
||||
}
|
||||
|
||||
for sk := range getSecretKeysFromStdinLinesOrSlice(ctx, c, args) {
|
||||
_, pk := btcec.PrivKeyFromBytes(sk[:])
|
||||
|
||||
if c.Bool("with-parity") {
|
||||
@@ -172,7 +187,8 @@ var decryptKey = &cli.Command{
|
||||
}
|
||||
} else {
|
||||
password = c.Args().Get(0)
|
||||
for ncryptsec := range getStdinLinesOrArgumentsFromSlice([]string{ncryptsec}) {
|
||||
// the ncryptsec codes come from stdin
|
||||
for ncryptsec := range getStdinLinesOrArgumentsFromSlice(nil) {
|
||||
sk, err := nip49.Decrypt(ncryptsec, password)
|
||||
if err != nil {
|
||||
ctx = lineProcessingError(ctx, "failed to decrypt: %s", err)
|
||||
@@ -180,6 +196,7 @@ var decryptKey = &cli.Command{
|
||||
}
|
||||
stdout(sk.Hex())
|
||||
}
|
||||
exitIfLineProcessingError(ctx)
|
||||
return nil
|
||||
}
|
||||
default:
|
||||
@@ -211,7 +228,7 @@ Returns error if key is invalid, otherwise exits successfully.`,
|
||||
ctx = lineProcessingError(ctx, "invalid npub: %s", err)
|
||||
continue
|
||||
}
|
||||
tmp := data.([32]byte)
|
||||
tmp := data.(nostr.PubKey)
|
||||
pkBytes = tmp[:]
|
||||
} else {
|
||||
pkBytes, err = hex.DecodeString(pk)
|
||||
@@ -235,6 +252,17 @@ Returns error if key is invalid, otherwise exits successfully.`,
|
||||
},
|
||||
}
|
||||
|
||||
var defaultCommand = &cli.Command{
|
||||
Name: "default",
|
||||
Usage: "prints the default secret key",
|
||||
Description: `the default secret key is generated differently for each machine (or falls back to a hardcoded one in case of failure), it is not save in any way, but this command makes it available if that is needed.`,
|
||||
DisableSliceFlagSeparator: true,
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
stdout(nip19.EncodeNsec(defaultKey()))
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var combine = &cli.Command{
|
||||
Name: "combine",
|
||||
Usage: "combines two or more pubkeys using musig2",
|
||||
@@ -298,6 +326,12 @@ However, if the intent is to check if two existing Nostr pubkeys match a given c
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
return fmt.Errorf("invalid key %s: expected 32 or 33 bytes hex", keyhex)
|
||||
}
|
||||
|
||||
if len(keyGroups) < 2 {
|
||||
return fmt.Errorf("at least two pubkeys are required")
|
||||
}
|
||||
|
||||
result.Combinations = make([]Combination, 0, 16)
|
||||
|
||||
@@ -22,9 +22,10 @@ func setupLocalDatabases(c *cli.Command, sys *sdk.System) {
|
||||
|
||||
hintsPath := filepath.Join(configPath, "outbox/hints")
|
||||
os.MkdirAll(hintsPath, 0755)
|
||||
_, err := lmdbh.NewLMDBHints(hintsPath)
|
||||
if err != nil {
|
||||
if hdb, err := lmdbh.NewLMDBHints(hintsPath); err != nil {
|
||||
log("failed to create lmdb hints db at '%s': %s\n", hintsPath, err)
|
||||
} else {
|
||||
sys.Hints = hdb
|
||||
}
|
||||
|
||||
eventsPath := filepath.Join(configPath, "events")
|
||||
|
||||
@@ -6,13 +6,56 @@ import (
|
||||
"net/textproto"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"fiatjaf.com/nostr"
|
||||
"fiatjaf.com/nostr/nip42"
|
||||
"fiatjaf.com/nostr/sdk"
|
||||
"github.com/fatih/color"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
var authFlags = []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "auth",
|
||||
Usage: "always perform nip42 \"AUTH\" when facing an \"auth-required: \" rejection and try again",
|
||||
Category: CATEGORY_AUTH,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "force-pre-auth",
|
||||
Aliases: []string{"fpa"},
|
||||
Usage: "after connecting, wait for a nip42 \"AUTH\" message to be received, act on it and only then send the query",
|
||||
Category: CATEGORY_AUTH,
|
||||
},
|
||||
}
|
||||
|
||||
var defaultKeyFlags = []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "sec",
|
||||
Usage: "secret key to sign the event, as nsec, ncryptsec or hex, or a bunker URL",
|
||||
Category: CATEGORY_SIGNER,
|
||||
Sources: cli.EnvVars("NOSTR_SECRET_KEY"),
|
||||
Value: defaultKey().Hex(),
|
||||
DefaultText: "a default key specific to your machine, see it with `nak key default`",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "prompt-sec",
|
||||
Usage: "prompt the user to paste a hex or nsec with which to sign the event",
|
||||
Category: CATEGORY_SIGNER,
|
||||
Hidden: true,
|
||||
},
|
||||
&SecretKeyFlag{
|
||||
Name: "connect-as",
|
||||
Usage: "private key to use when communicating with nip46 bunkers",
|
||||
Category: CATEGORY_SIGNER,
|
||||
Sources: cli.EnvVars("NOSTR_CLIENT_KEY"),
|
||||
Value: defaultKey(),
|
||||
DefaultText: "the default key (see `nak key default`)",
|
||||
Hidden: true,
|
||||
},
|
||||
}
|
||||
|
||||
var (
|
||||
version string = "debug"
|
||||
isVerbose bool = false
|
||||
@@ -61,47 +104,63 @@ var app = &cli.Command{
|
||||
validateCmd,
|
||||
},
|
||||
Version: version,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "config-path",
|
||||
Hidden: true,
|
||||
Value: (func() string {
|
||||
if home, err := os.UserHomeDir(); err == nil {
|
||||
return filepath.Join(home, ".config/nak")
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
})(),
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "quiet",
|
||||
Usage: "do not print logs and info messages to stderr, use -qq to also not print anything to stdout",
|
||||
Aliases: []string{"q"},
|
||||
Action: func(ctx context.Context, c *cli.Command, b bool) error {
|
||||
q := c.Count("quiet")
|
||||
if q >= 1 {
|
||||
log = func(msg string, args ...any) {}
|
||||
if q >= 2 {
|
||||
stdout = func(_ ...any) {}
|
||||
Flags: combineFlags([][]cli.Flag{
|
||||
{
|
||||
&cli.StringFlag{
|
||||
Name: "config-path",
|
||||
Hidden: true,
|
||||
Value: (func() string {
|
||||
if home, err := os.UserHomeDir(); err == nil {
|
||||
return filepath.Join(home, ".config/nak")
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})(),
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "quiet",
|
||||
Usage: "do not print logs and info messages to stderr, use -qq to also not print anything to stdout",
|
||||
Aliases: []string{"q"},
|
||||
Action: func(ctx context.Context, c *cli.Command, b bool) error {
|
||||
q := c.Count("quiet")
|
||||
if q >= 1 {
|
||||
log = func(msg string, args ...any) {}
|
||||
if q >= 2 {
|
||||
stdout = func(_ ...any) {}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Hidden: true,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "verbose",
|
||||
Usage: "print more stuff than normally",
|
||||
Aliases: []string{"v"},
|
||||
Action: func(ctx context.Context, c *cli.Command, b bool) error {
|
||||
v := c.Count("verbose")
|
||||
if v >= 1 {
|
||||
logverbose = log
|
||||
isVerbose = true
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Hidden: true,
|
||||
},
|
||||
&cli.DurationFlag{
|
||||
Name: "connect-timeout",
|
||||
Usage: "timeout for connecting to relays",
|
||||
Value: connectTimeout,
|
||||
Action: func(ctx context.Context, c *cli.Command, d time.Duration) error {
|
||||
connectTimeout = d
|
||||
return nil
|
||||
},
|
||||
Hidden: true,
|
||||
},
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "verbose",
|
||||
Usage: "print more stuff than normally",
|
||||
Aliases: []string{"v"},
|
||||
Action: func(ctx context.Context, c *cli.Command, b bool) error {
|
||||
v := c.Count("verbose")
|
||||
if v >= 1 {
|
||||
logverbose = log
|
||||
isVerbose = true
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
defaultKeyFlags,
|
||||
authFlags,
|
||||
}),
|
||||
Before: func(ctx context.Context, c *cli.Command) (context.Context, error) {
|
||||
sys = sdk.NewSystem()
|
||||
|
||||
@@ -112,6 +171,18 @@ var app = &cli.Command{
|
||||
sys.Pool.RelayOptions = nostr.RelayOptions{
|
||||
RequestHeader: http.Header{textproto.CanonicalMIMEHeaderKey("user-agent"): {"nak/b"}},
|
||||
}
|
||||
sys.Pool.AuthRequiredHandler = func(ctx context.Context, authEvent *nostr.Event) error {
|
||||
return authSigner(ctx, c, func(s string, args ...any) {
|
||||
if strings.HasPrefix(s, "authenticating as") {
|
||||
cleanUrl, _ := strings.CutPrefix(
|
||||
nip42.GetRelayURLFromAuthEvent(*authEvent),
|
||||
"wss://",
|
||||
)
|
||||
s = "authenticating to " + color.CyanString(cleanUrl) + " as" + s[len("authenticating as"):]
|
||||
}
|
||||
log(s+"\n", args...)
|
||||
}, authEvent)
|
||||
}
|
||||
|
||||
return ctx, nil
|
||||
},
|
||||
|
||||
@@ -19,9 +19,7 @@ var mcpServer = &cli.Command{
|
||||
Usage: "pander to the AI gods",
|
||||
Description: ``,
|
||||
DisableSliceFlagSeparator: true,
|
||||
Flags: append(
|
||||
defaultKeyFlags,
|
||||
),
|
||||
Flags: []cli.Flag{},
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
s := server.NewMCPServer(
|
||||
"nak",
|
||||
@@ -157,6 +155,9 @@ var mcpServer = &cli.Command{
|
||||
), func(ctx context.Context, r mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
name := required[string](r, "name")
|
||||
limit, _ := optional[float64](r, "limit")
|
||||
if limit == 0 {
|
||||
limit = 10
|
||||
}
|
||||
|
||||
res := strings.Builder{}
|
||||
res.Grow(500)
|
||||
@@ -241,7 +242,7 @@ var mcpServer = &cli.Command{
|
||||
for ie := range events {
|
||||
result.WriteString("author public key: ")
|
||||
result.WriteString(ie.PubKey.Hex())
|
||||
result.WriteString("content: '")
|
||||
result.WriteString("\ncontent: '")
|
||||
result.WriteString(ie.Content)
|
||||
result.WriteString("'")
|
||||
result.WriteString("\n---\n")
|
||||
|
||||
@@ -5,12 +5,14 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/glamour"
|
||||
"github.com/urfave/cli/v3"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
type nipInfo struct {
|
||||
@@ -63,7 +65,6 @@ examples:
|
||||
}
|
||||
|
||||
url := "https://github.com/nostr-protocol/nips/blob/master/" + foundLink
|
||||
fmt.Println("Opening " + url)
|
||||
|
||||
var cmd *exec.Cmd
|
||||
switch runtime.GOOS {
|
||||
@@ -129,12 +130,23 @@ examples:
|
||||
}
|
||||
|
||||
// render markdown
|
||||
rendered, err := glamour.Render(string(body), "auto")
|
||||
width, _, err := term.GetSize(int(os.Stdout.Fd()))
|
||||
if err != nil || width < 80 {
|
||||
width = 120
|
||||
}
|
||||
r, err := glamour.NewTermRenderer(
|
||||
glamour.WithAutoStyle(),
|
||||
glamour.WithWordWrap(width),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create renderer: %w", err)
|
||||
}
|
||||
rendered, err := r.Render(string(body))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to render markdown: %w", err)
|
||||
}
|
||||
|
||||
fmt.Print(rendered)
|
||||
stdout(rendered)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
@@ -5,9 +5,9 @@ import (
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
|
||||
"fiatjaf.com/nostr"
|
||||
"github.com/hanwen/go-fuse/v2/fs"
|
||||
"github.com/hanwen/go-fuse/v2/fuse"
|
||||
"fiatjaf.com/nostr"
|
||||
)
|
||||
|
||||
type AsyncFile struct {
|
||||
|
||||
+4
-1
@@ -996,10 +996,13 @@ func (r *NostrRoot) publishNote(path string) {
|
||||
}
|
||||
|
||||
for _, url := range relays {
|
||||
relay, err := r.sys.Pool.EnsureRelay(url)
|
||||
connectCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
relay, err := nostr.RelayConnect(connectCtx, url, r.sys.Pool.RelayOptions)
|
||||
cancel()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
r.sys.Pool.Relays.Store(nostr.NormalizeURL(url), relay)
|
||||
relay.Publish(ctx, *evt)
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ var nsite = &cli.Command{
|
||||
Usage: "publishes and downloads nip-5A static sites",
|
||||
ArgsUsage: "<directory> [relay...]",
|
||||
DisableSliceFlagSeparator: true,
|
||||
Flags: defaultKeyFlags,
|
||||
Flags: combineFlags([][]cli.Flag{}),
|
||||
Commands: []*cli.Command{
|
||||
{
|
||||
Name: "upload",
|
||||
@@ -31,6 +31,10 @@ var nsite = &cli.Command{
|
||||
ArgsUsage: "<directory> [relay...]",
|
||||
DisableSliceFlagSeparator: true,
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "outbox",
|
||||
Usage: "append given relays to user's outbox relays instead of using them exclusively",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "root",
|
||||
Usage: "publish root site as kind 15128",
|
||||
@@ -40,6 +44,10 @@ var nsite = &cli.Command{
|
||||
Aliases: []string{"d"},
|
||||
Usage: "publish named site as kind 35128 with this d tag",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "title",
|
||||
Usage: "a human-readable title for the site",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "description",
|
||||
Usage: "a human-readable description of the site",
|
||||
@@ -54,6 +62,11 @@ var nsite = &cli.Command{
|
||||
Usage: "blossom server hostname or URL, can be given multiple times",
|
||||
DefaultText: "defaults to the publisher's list of preferred blossom servers",
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "tag",
|
||||
Aliases: []string{"t"},
|
||||
Usage: "inject extra tag into the manifest event, e.g. -t action=profile",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "yes",
|
||||
Aliases: []string{"y"},
|
||||
@@ -94,6 +107,7 @@ var nsite = &cli.Command{
|
||||
Root: root,
|
||||
Identifier: identifier,
|
||||
Paths: make(map[string][32]byte),
|
||||
Title: c.String("title"),
|
||||
Description: c.String("description"),
|
||||
Source: c.String("source"),
|
||||
}
|
||||
@@ -179,19 +193,34 @@ var nsite = &cli.Command{
|
||||
}
|
||||
|
||||
evt := manifest.ToEvent()
|
||||
|
||||
for _, tagFlag := range c.StringSlice("tag") {
|
||||
tagName, tagValue, found := strings.Cut(tagFlag, "=")
|
||||
tag := []string{tagName}
|
||||
if found {
|
||||
tag = append(tag, strings.Split(tagValue, ";")...)
|
||||
}
|
||||
evt.Tags = append(evt.Tags, tag)
|
||||
}
|
||||
|
||||
if err := kr.SignEvent(ctx, &evt); err != nil {
|
||||
return fmt.Errorf("error signing manifest event: %w", err)
|
||||
}
|
||||
|
||||
relayURLs := nostr.AppendUnique(sys.FetchWriteRelays(ctx, pk), c.Args().Slice()[1:]...)
|
||||
givenRelays := c.Args().Slice()[1:]
|
||||
var relayURLs []string
|
||||
if len(givenRelays) == 0 {
|
||||
relayURLs = sys.FetchWriteRelays(ctx, pk)
|
||||
} else if c.Bool("outbox") {
|
||||
relayURLs = nostr.AppendUnique(sys.FetchWriteRelays(ctx, pk), givenRelays...)
|
||||
} else {
|
||||
relayURLs = givenRelays
|
||||
}
|
||||
if len(relayURLs) == 0 {
|
||||
return fmt.Errorf("no relays to publish this nsite to")
|
||||
}
|
||||
|
||||
sys.Pool.AuthRequiredHandler = func(ctx context.Context, authEvent *nostr.Event) error {
|
||||
return authSigner(ctx, c, func(string, ...any) {}, authEvent)
|
||||
}
|
||||
relays := connectToAllRelays(ctx, c, relayURLs, nil)
|
||||
relays := connectToAllRelays(ctx, c, relayURLs)
|
||||
if len(relays) == 0 {
|
||||
return fmt.Errorf("failed to connect to any of [ %v ]", relayURLs)
|
||||
}
|
||||
|
||||
+9
-10
@@ -157,8 +157,7 @@ func resolvePodcastEpisode(ctx context.Context, target string, relayHints []stri
|
||||
pointer := value.(nostr.EventPointer)
|
||||
return fetchSpecificPodcastEpisode(ctx, pointer, relayHints)
|
||||
case "note":
|
||||
pointer := nostr.EventPointer{ID: value.(nostr.ID)}
|
||||
return fetchSpecificPodcastEpisode(ctx, pointer, relayHints)
|
||||
return fetchSpecificPodcastEpisode(ctx, value.(nostr.EventPointer), relayHints)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -557,16 +556,16 @@ func printPodcastInfo(p podcastInfo) {
|
||||
|
||||
authors := make([]string, 0, 2)
|
||||
for _, tag := range p.Metadata.Tags {
|
||||
if len(tag) >= 3 && tag[0] == "p" {
|
||||
role := ""
|
||||
if len(tag) >= 3 {
|
||||
role = tag[2]
|
||||
}
|
||||
if len(tag) >= 2 && tag[0] == "p" {
|
||||
pk, err := nostr.PubKeyFromHex(tag[1])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
authors = append(authors, nip19.EncodeNpub(pk)+" "+role)
|
||||
author := nip19.EncodeNpub(pk)
|
||||
if len(tag) >= 3 && tag[2] != "" {
|
||||
author += " " + tag[2]
|
||||
}
|
||||
authors = append(authors, author)
|
||||
}
|
||||
}
|
||||
if len(authors) > 0 {
|
||||
@@ -583,11 +582,11 @@ func printPodcastEpisodeList(ctx context.Context, p podcastInfo, limit int) erro
|
||||
}
|
||||
|
||||
if len(episodes) == 0 {
|
||||
stdout(color.YellowString("no episodes found for %s"), p.Title)
|
||||
stdout(color.YellowString("no episodes found for %s", p.Title))
|
||||
return nil
|
||||
}
|
||||
|
||||
stdout(color.CyanString("episodes for %s:"), p.Title)
|
||||
stdout(color.CyanString("episodes for %s:", p.Title))
|
||||
for _, ep := range episodes {
|
||||
title := podcastTagValue(ep, "title")
|
||||
if title == "" {
|
||||
|
||||
+38
@@ -3,6 +3,8 @@ package main
|
||||
import (
|
||||
"context"
|
||||
|
||||
stdjson "encoding/json"
|
||||
|
||||
"fiatjaf.com/nostr/nip19"
|
||||
"github.com/fatih/color"
|
||||
"github.com/urfave/cli/v3"
|
||||
@@ -16,6 +18,24 @@ var profile = &cli.Command{
|
||||
example usage:
|
||||
nak profile npub1h8spmtw9m2huyv6v2j2qd5zv956z2zdugl6mgx02f2upffwpm3nqv0j4ps
|
||||
nak profile user@example.com`,
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "name",
|
||||
Usage: "print only the name",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "picture",
|
||||
Usage: "print only the picture URL",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "about",
|
||||
Usage: "print only the about text",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "metadata",
|
||||
Usage: "print the profile metadata as JSON",
|
||||
},
|
||||
},
|
||||
ArgsUsage: "[pubkey]",
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
for pubkeyInput := range getStdinLinesOrArguments(c.Args()) {
|
||||
@@ -27,6 +47,24 @@ example usage:
|
||||
|
||||
pm := sys.FetchProfileMetadata(ctx, pk)
|
||||
|
||||
if c.Bool("name") {
|
||||
stdout(pm.Name)
|
||||
continue
|
||||
}
|
||||
if c.Bool("picture") {
|
||||
stdout(pm.Picture)
|
||||
continue
|
||||
}
|
||||
if c.Bool("about") {
|
||||
stdout(pm.About)
|
||||
continue
|
||||
}
|
||||
if c.Bool("metadata") {
|
||||
j, _ := stdjson.Marshal(pm)
|
||||
stdout(string(j))
|
||||
continue
|
||||
}
|
||||
|
||||
npub := nip19.EncodeNpub(pk)
|
||||
stdout(colors.bold("pubkey (hex):"), pk.Hex())
|
||||
stdout(colors.bold("npub:"), color.HiCyanString(npub))
|
||||
|
||||
+183
-58
@@ -8,26 +8,36 @@ import (
|
||||
"strings"
|
||||
|
||||
"fiatjaf.com/nostr"
|
||||
"fiatjaf.com/nostr/nip19"
|
||||
"fiatjaf.com/nostr/sdk"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
var publish = &cli.Command{
|
||||
Name: "publish",
|
||||
Usage: "publishes a note with content from stdin",
|
||||
Description: `reads content from stdin and publishes it as a note, optionally as a reply to another note.
|
||||
Usage: "publishes a note with content from stdin or argument",
|
||||
Description: `reads content and publishes it as a note, optionally as a reply to another note.
|
||||
Either pipe content or pass it as the first argument.
|
||||
|
||||
example:
|
||||
echo "hello world" | nak publish
|
||||
nak publish "hello world"
|
||||
echo "I agree!" | nak publish --reply nevent1...
|
||||
echo "tagged post" | nak publish -t t=mytag -t e=someeventid`,
|
||||
nak publish -t t=mytag -t e=someeventid "tagged post"`,
|
||||
DisableSliceFlagSeparator: true,
|
||||
Flags: append(defaultKeyFlags,
|
||||
Flags: combineFlags([][]cli.Flag{},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "relay",
|
||||
Aliases: []string{"r"},
|
||||
Usage: "also publish to these relays",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "reply",
|
||||
Usage: "event id, naddr1 or nevent1 code to reply to",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "comment",
|
||||
Usage: "event id, naddr1 or nevent1 code to comment on (NIP-22 kind 1111, takes precedence over --reply)",
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "tag",
|
||||
Aliases: []string{"t"},
|
||||
@@ -40,11 +50,6 @@ example:
|
||||
DefaultText: "now",
|
||||
Value: nostr.Now(),
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "auth",
|
||||
Usage: "always perform nip42 \"AUTH\" when facing an \"auth-required: \" rejection and try again",
|
||||
Category: CATEGORY_EXTRAS,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "nevent",
|
||||
Usage: "print the nevent code (to stderr) after the event is published",
|
||||
@@ -57,9 +62,17 @@ example:
|
||||
},
|
||||
),
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
content, err := io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read from stdin: %w", err)
|
||||
var content []byte
|
||||
if isPiped() {
|
||||
var err error
|
||||
content, err = io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read from stdin: %w", err)
|
||||
}
|
||||
} else if firstArg := c.Args().First(); firstArg != "" {
|
||||
content = []byte(firstArg)
|
||||
} else {
|
||||
return fmt.Errorf("no content. pipe or pass content as argument.")
|
||||
}
|
||||
|
||||
evt := nostr.Event{
|
||||
@@ -74,51 +87,67 @@ example:
|
||||
evt.CreatedAt = getNaturalDate(c, "created-at")
|
||||
}
|
||||
|
||||
// handle reply flag
|
||||
// handle reply/comment flag
|
||||
// --comment takes precedence over --reply
|
||||
target := c.String("comment")
|
||||
isComment := c.IsSet("comment")
|
||||
if target == "" {
|
||||
target = c.String("reply")
|
||||
}
|
||||
var replyRelays []string
|
||||
if replyTo := c.String("reply"); replyTo != "" {
|
||||
var replyEvent *nostr.Event
|
||||
|
||||
// try to decode as nevent or naddr first
|
||||
if strings.HasPrefix(replyTo, "nevent1") || strings.HasPrefix(replyTo, "naddr1") {
|
||||
_, value, err := nip19.Decode(replyTo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid reply target: %w", err)
|
||||
}
|
||||
|
||||
switch pointer := value.(type) {
|
||||
case nostr.EventPointer:
|
||||
replyEvent, _, err = sys.FetchSpecificEvent(ctx, pointer, sdk.FetchSpecificEventParameters{})
|
||||
case nostr.EntityPointer:
|
||||
replyEvent, _, err = sys.FetchSpecificEvent(ctx, pointer, sdk.FetchSpecificEventParameters{})
|
||||
default:
|
||||
return fmt.Errorf("unexpected reply target type: %T", value)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to fetch reply target event: %w", err)
|
||||
}
|
||||
} else {
|
||||
// try as raw event ID
|
||||
id, err := nostr.IDFromHex(replyTo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid event id: %w", err)
|
||||
}
|
||||
replyEvent, _, err = sys.FetchSpecificEvent(ctx, nostr.EventPointer{ID: id}, sdk.FetchSpecificEventParameters{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to fetch reply target event: %w", err)
|
||||
}
|
||||
if target != "" {
|
||||
replyEvent, relayHints, err := sys.FetchSpecificEventFromInput(ctx, target, sdk.FetchSpecificEventParameters{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to fetch reply target event: %w", err)
|
||||
}
|
||||
|
||||
if replyEvent.Kind != 1 {
|
||||
if isComment || replyEvent.Kind != 1 {
|
||||
// NIP-22 comment
|
||||
evt.Kind = 1111
|
||||
evt.Tags = append(evt.Tags, nostr.Tag{"K", fmt.Sprint(replyEvent.Kind)})
|
||||
}
|
||||
buildNIP22Tags(&evt, replyEvent, relayHints)
|
||||
} else {
|
||||
// NIP-10 reply
|
||||
if replyEvent.Kind != 1 {
|
||||
evt.Kind = 1111
|
||||
evt.Tags = append(evt.Tags, nostr.Tag{"K", fmt.Sprint(replyEvent.Kind)})
|
||||
}
|
||||
|
||||
// add reply tags
|
||||
evt.Tags = append(evt.Tags,
|
||||
nostr.Tag{"e", replyEvent.ID.Hex(), "", "reply"},
|
||||
nostr.Tag{"p", replyEvent.PubKey.Hex()},
|
||||
)
|
||||
// add reply tags per NIP-10
|
||||
rootID := ""
|
||||
rootRelay := ""
|
||||
for _, tag := range replyEvent.Tags {
|
||||
if len(tag) >= 2 && tag[0] == "e" {
|
||||
if len(tag) >= 4 && tag[3] == "root" {
|
||||
rootID = tag[1]
|
||||
if len(tag) >= 3 {
|
||||
rootRelay = tag[2]
|
||||
}
|
||||
break
|
||||
}
|
||||
if rootID == "" {
|
||||
rootID = tag[1]
|
||||
if len(tag) >= 3 {
|
||||
rootRelay = tag[2]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if rootID == "" {
|
||||
// replying to root event
|
||||
evt.Tags = append(evt.Tags,
|
||||
nostr.Tag{"e", replyEvent.ID.Hex(), "", "root"},
|
||||
)
|
||||
} else {
|
||||
// replying to a reply
|
||||
evt.Tags = append(evt.Tags,
|
||||
nostr.Tag{"e", rootID, rootRelay, "root"},
|
||||
nostr.Tag{"e", replyEvent.ID.Hex(), "", "reply"},
|
||||
)
|
||||
}
|
||||
evt.Tags = append(evt.Tags,
|
||||
nostr.Tag{"p", replyEvent.PubKey.Hex()},
|
||||
)
|
||||
}
|
||||
|
||||
replyRelays = sys.FetchInboxRelays(ctx, replyEvent.PubKey, 3)
|
||||
}
|
||||
@@ -153,12 +182,9 @@ example:
|
||||
relayUrls := sys.FetchWriteRelays(ctx, pk)
|
||||
relayUrls = nostr.AppendUnique(relayUrls, targetRelays...)
|
||||
relayUrls = nostr.AppendUnique(relayUrls, replyRelays...)
|
||||
relayUrls = nostr.AppendUnique(relayUrls, c.Args().Slice()...)
|
||||
relayUrls = nostr.AppendUnique(relayUrls, c.StringSlice("relay")...)
|
||||
|
||||
sys.Pool.AuthRequiredHandler = func(ctx context.Context, authEvent *nostr.Event) error {
|
||||
return authSigner(ctx, c, func(s string, args ...any) {}, authEvent)
|
||||
}
|
||||
relays := connectToAllRelays(ctx, c, relayUrls, nil)
|
||||
relays := connectToAllRelays(ctx, c, relayUrls)
|
||||
|
||||
if len(relays) == 0 {
|
||||
if len(relayUrls) == 0 {
|
||||
@@ -180,3 +206,102 @@ example:
|
||||
return publishFlow(ctx, c, kr, evt, relays)
|
||||
},
|
||||
}
|
||||
|
||||
func buildNIP22Tags(evt *nostr.Event, target *nostr.Event, relayHints []string) {
|
||||
var relayHint string
|
||||
if len(relayHints) > 0 {
|
||||
relayHint = relayHints[0]
|
||||
}
|
||||
|
||||
dtag := func() string {
|
||||
for _, tag := range target.Tags {
|
||||
if len(tag) >= 2 && tag[0] == "d" {
|
||||
return tag[1]
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// find root scope in target's uppercase NIP-22 tags
|
||||
var rootScopeName, rootScopeValue, rootScopeRelay, rootKind, rootPubkey string
|
||||
for _, tag := range target.Tags {
|
||||
if len(tag) >= 2 && (tag[0] == "E" || tag[0] == "A" || tag[0] == "I") {
|
||||
rootScopeName = tag[0]
|
||||
rootScopeValue = tag[1]
|
||||
if len(tag) >= 3 {
|
||||
rootScopeRelay = tag[2]
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if rootScopeName != "" {
|
||||
for _, tag := range target.Tags {
|
||||
if len(tag) >= 2 && tag[0] == "K" {
|
||||
rootKind = tag[1]
|
||||
break
|
||||
}
|
||||
}
|
||||
for _, tag := range target.Tags {
|
||||
if len(tag) >= 2 && tag[0] == "P" {
|
||||
rootPubkey = tag[1]
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if target.Kind.IsAddressable() || target.Kind.IsReplaceable() {
|
||||
rootScopeName = "A"
|
||||
rootScopeValue = fmt.Sprintf("%d:%s:%s", target.Kind, target.PubKey.Hex(), dtag())
|
||||
} else {
|
||||
rootScopeName = "E"
|
||||
rootScopeValue = target.ID.Hex()
|
||||
}
|
||||
rootScopeRelay = relayHint
|
||||
rootKind = fmt.Sprint(target.Kind)
|
||||
rootPubkey = target.PubKey.Hex()
|
||||
}
|
||||
|
||||
// root scope tag
|
||||
rootScopeTag := nostr.Tag{rootScopeName, rootScopeValue}
|
||||
if rootScopeName == "E" && rootPubkey != "" {
|
||||
// the pubkey goes in the 4th position, so the relay hint must be
|
||||
// present (even if empty) to keep it there
|
||||
rootScopeTag = append(rootScopeTag, rootScopeRelay, rootPubkey)
|
||||
} else if rootScopeRelay != "" {
|
||||
rootScopeTag = append(rootScopeTag, rootScopeRelay)
|
||||
}
|
||||
evt.Tags = append(evt.Tags, rootScopeTag)
|
||||
|
||||
if rootKind != "" {
|
||||
evt.Tags = append(evt.Tags, nostr.Tag{"K", rootKind})
|
||||
}
|
||||
if rootPubkey != "" && rootScopeName != "I" {
|
||||
evt.Tags = append(evt.Tags, nostr.Tag{"P", rootPubkey})
|
||||
}
|
||||
|
||||
// parent tags (lowercase) - always reference the target event
|
||||
if target.Kind.IsAddressable() || target.Kind.IsReplaceable() {
|
||||
aValue := fmt.Sprintf("%d:%s:%s", target.Kind, target.PubKey.Hex(), dtag())
|
||||
aTag := nostr.Tag{"a", aValue}
|
||||
if relayHint != "" {
|
||||
aTag = append(aTag, relayHint)
|
||||
}
|
||||
evt.Tags = append(evt.Tags, aTag)
|
||||
}
|
||||
eTag := nostr.Tag{"e", target.ID.Hex()}
|
||||
if target.PubKey != (nostr.PubKey{}) {
|
||||
// same here: keep the pubkey in the 4th position
|
||||
eTag = append(eTag, relayHint, target.PubKey.Hex())
|
||||
} else if relayHint != "" {
|
||||
eTag = append(eTag, relayHint)
|
||||
}
|
||||
evt.Tags = append(evt.Tags, eTag)
|
||||
|
||||
evt.Tags = append(evt.Tags, nostr.Tag{"k", fmt.Sprint(target.Kind)})
|
||||
|
||||
pTag := nostr.Tag{"p", target.PubKey.Hex()}
|
||||
if relayHint != "" {
|
||||
pTag = append(pTag, relayHint)
|
||||
}
|
||||
evt.Tags = append(evt.Tags, pTag)
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ var relay = &cli.Command{
|
||||
pretty, _ := json.MarshalIndent(info, "", " ")
|
||||
stdout(string(pretty))
|
||||
}
|
||||
exitIfLineProcessingError(ctx)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
@@ -15,10 +15,8 @@ import (
|
||||
"fiatjaf.com/nostr/eventstore"
|
||||
"fiatjaf.com/nostr/eventstore/slicestore"
|
||||
"fiatjaf.com/nostr/eventstore/wrappers"
|
||||
"fiatjaf.com/nostr/nip42"
|
||||
"fiatjaf.com/nostr/nip77"
|
||||
"fiatjaf.com/nostr/schema"
|
||||
"github.com/fatih/color"
|
||||
"github.com/mailru/easyjson"
|
||||
"github.com/urfave/cli/v3"
|
||||
"golang.org/x/sync/errgroup"
|
||||
@@ -43,69 +41,61 @@ 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: append(defaultKeyFlags,
|
||||
append(reqFilterFlags,
|
||||
&cli.StringFlag{
|
||||
Name: "jq",
|
||||
Usage: "filter returned events with jq expression",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "no-verify",
|
||||
Usage: "skip event signature verification from relays",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "only-missing",
|
||||
Usage: "use nip77 negentropy to only fetch events that aren't present in the given jsonl file",
|
||||
TakesFile: true,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "ids-only",
|
||||
Usage: "use nip77 to fetch just a list of ids",
|
||||
},
|
||||
&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: "outbox",
|
||||
Usage: "read from \"write\" relays of \"authors\" and/or from the \"read\" relays of any \"#p\" or \"#P\" tags",
|
||||
DefaultText: "false, will only use manually-specified relays",
|
||||
},
|
||||
&cli.UintFlag{
|
||||
Name: "outbox-relays-per-pubkey",
|
||||
Aliases: []string{"n"},
|
||||
Usage: "number of outbox relays to use for each pubkey",
|
||||
Value: 3,
|
||||
},
|
||||
&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.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 nip42 \"AUTH\" when facing an \"auth-required: \" rejection and try again",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "force-pre-auth",
|
||||
Aliases: []string{"fpa"},
|
||||
Usage: "after connecting, for a nip42 \"AUTH\" message to be received, act on it and only then send the \"REQ\"",
|
||||
Category: CATEGORY_SIGNER,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "spell",
|
||||
Usage: "output a spell event (kind 777) instead of a filter",
|
||||
},
|
||||
)...,
|
||||
Flags: combineFlags([][]cli.Flag{reqFilterFlags},
|
||||
&cli.StringFlag{
|
||||
Name: "jq",
|
||||
Usage: "filter returned events with jq expression",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "jq-raw",
|
||||
Usage: "print --jq string results without JSON quoting, like `jq -r`",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "no-verify",
|
||||
Usage: "skip event signature verification from relays",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "only-missing",
|
||||
Usage: "use nip77 negentropy to only fetch events that aren't present in the given jsonl file",
|
||||
TakesFile: true,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "ids-only",
|
||||
Usage: "use nip77 to fetch just a list of ids",
|
||||
},
|
||||
&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: "outbox",
|
||||
Usage: "read from \"write\" relays of \"authors\" and/or from the \"read\" relays of any \"#p\" or \"#P\" tags",
|
||||
DefaultText: "false, will only use manually-specified relays",
|
||||
},
|
||||
&cli.UintFlag{
|
||||
Name: "outbox-relays-per-pubkey",
|
||||
Aliases: []string{"n"},
|
||||
Usage: "number of outbox relays to use for each pubkey",
|
||||
Value: 3,
|
||||
},
|
||||
&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.BoolFlag{
|
||||
Name: "bare",
|
||||
Usage: "when printing the filter, print just the filter, not enveloped in a [\"REQ\", ...] array",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "spell",
|
||||
Usage: "output a spell event (kind 777) instead of a filter",
|
||||
},
|
||||
),
|
||||
ArgsUsage: "[relay...]",
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
@@ -138,7 +128,7 @@ example:
|
||||
return fmt.Errorf("relay URLs are incompatible with --bare or --spell")
|
||||
}
|
||||
|
||||
jq, err := jqPrepare(c.String("jq"))
|
||||
jq, err := jqPrepare(c.String("jq"), c.Bool("jq-raw"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -147,30 +137,10 @@ example:
|
||||
}
|
||||
|
||||
if len(relayUrls) > 0 && !negentropy {
|
||||
// this is used both for the normal AUTH (after "auth-required:" is received) or forced pre-auth
|
||||
// connect to all relays we expect to use in this call in parallel
|
||||
forcePreAuthSigner := authSigner
|
||||
if !c.Bool("force-pre-auth") {
|
||||
forcePreAuthSigner = nil
|
||||
}
|
||||
|
||||
sys.Pool.AuthRequiredHandler = func(ctx context.Context, authEvent *nostr.Event) error {
|
||||
return authSigner(ctx, c, func(s string, args ...any) {
|
||||
if strings.HasPrefix(s, "authenticating as") {
|
||||
cleanUrl, _ := strings.CutPrefix(
|
||||
nip42.GetRelayURLFromAuthEvent(*authEvent),
|
||||
"wss://",
|
||||
)
|
||||
s = "authenticating to " + color.CyanString(cleanUrl) + " as" + s[len("authenticating as"):]
|
||||
}
|
||||
log(s+"\n", args...)
|
||||
}, authEvent)
|
||||
}
|
||||
relays := connectToAllRelays(
|
||||
ctx,
|
||||
c,
|
||||
relayUrls,
|
||||
forcePreAuthSigner,
|
||||
)
|
||||
|
||||
// stop here already if all connections failed
|
||||
@@ -363,21 +333,15 @@ func performReq(
|
||||
}
|
||||
|
||||
matchUrl := func(def nostr.DirectedFilter) bool { return def.Relay == url }
|
||||
idx := slices.IndexFunc(defs, matchUrl)
|
||||
if idx == -1 {
|
||||
mu.Lock()
|
||||
if slices.IndexFunc(defs, matchUrl) == -1 {
|
||||
// new relay, add it
|
||||
mu.Lock()
|
||||
idx = slices.IndexFunc(defs, matchUrl)
|
||||
if idx == -1 {
|
||||
defs = append(defs, nostr.DirectedFilter{
|
||||
Filter: filter,
|
||||
Relay: url,
|
||||
})
|
||||
mu.Unlock()
|
||||
continue
|
||||
}
|
||||
mu.Unlock()
|
||||
defs = append(defs, nostr.DirectedFilter{
|
||||
Filter: filter,
|
||||
Relay: url,
|
||||
})
|
||||
}
|
||||
mu.Unlock()
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -403,30 +367,20 @@ func performReq(
|
||||
}
|
||||
|
||||
matchUrl := func(def nostr.DirectedFilter) bool { return def.Relay == url }
|
||||
idx := slices.IndexFunc(defs, matchUrl)
|
||||
if idx == -1 {
|
||||
mu.Lock()
|
||||
if idx := slices.IndexFunc(defs, matchUrl); idx == -1 {
|
||||
// new relay, add it
|
||||
mu.Lock()
|
||||
// check again after locking to prevent races
|
||||
idx = slices.IndexFunc(defs, matchUrl)
|
||||
if idx == -1 {
|
||||
// then add it
|
||||
filter := filter.Clone()
|
||||
filter.Authors = []nostr.PubKey{pubkey}
|
||||
defs = append(defs, nostr.DirectedFilter{
|
||||
Filter: filter,
|
||||
Relay: url,
|
||||
})
|
||||
mu.Unlock()
|
||||
continue // done with this relay url
|
||||
}
|
||||
|
||||
// otherwise we'll just use the idx
|
||||
mu.Unlock()
|
||||
filter := filter.Clone()
|
||||
filter.Authors = []nostr.PubKey{pubkey}
|
||||
defs = append(defs, nostr.DirectedFilter{
|
||||
Filter: filter,
|
||||
Relay: url,
|
||||
})
|
||||
} else {
|
||||
// existing relay, add this pubkey
|
||||
defs[idx].Authors = append(defs[idx].Authors, pubkey)
|
||||
}
|
||||
|
||||
// existing relay, add this pubkey
|
||||
defs[idx].Authors = append(defs[idx].Authors, pubkey)
|
||||
mu.Unlock()
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -471,7 +425,7 @@ readevents:
|
||||
if !matches {
|
||||
continue
|
||||
}
|
||||
out, _ = json.MarshalToString(v)
|
||||
out = v
|
||||
}
|
||||
stdout(out)
|
||||
|
||||
@@ -675,7 +629,7 @@ func (p PrintingQuerierPublisher) Publish(ctx context.Context, evt nostr.Event)
|
||||
if !matches {
|
||||
return nil
|
||||
}
|
||||
out, _ = json.MarshalToString(v)
|
||||
out = v
|
||||
}
|
||||
stdout(out)
|
||||
return nil
|
||||
|
||||
@@ -64,6 +64,14 @@ var serve = &cli.Command{
|
||||
Name: "blossom",
|
||||
Usage: "enable blossom server",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "auth",
|
||||
Usage: "require AUTH for all operations",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "eager-auth",
|
||||
Usage: "send AUTH challenge immediately on connect",
|
||||
},
|
||||
},
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
db := &slicestore.SliceStore{}
|
||||
@@ -116,12 +124,19 @@ var serve = &cli.Command{
|
||||
totalConnections := atomic.Int32{}
|
||||
rl.OnConnect = func(ctx context.Context) {
|
||||
totalConnections.Add(1)
|
||||
if c.Bool("eager-auth") {
|
||||
khatru.RequestAuth(ctx)
|
||||
}
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
totalConnections.Add(-1)
|
||||
}()
|
||||
}
|
||||
|
||||
rl.OnAuth = func(ctx context.Context, pubkey nostr.PubKey) {
|
||||
log(" got %s %s\n", color.GreenString("authenticated"), pubkey.Hex())
|
||||
}
|
||||
|
||||
d := debounce.New(time.Second * 2)
|
||||
var printStatus func()
|
||||
printStatus = func() {
|
||||
@@ -228,23 +243,47 @@ var serve = &cli.Command{
|
||||
|
||||
// relay logging
|
||||
rl.OnRequest = func(ctx context.Context, filter nostr.Filter) (reject bool, msg string) {
|
||||
if c.Bool("auth") {
|
||||
if _, isAuthed := khatru.GetAuthed(ctx); !isAuthed {
|
||||
return true, "auth-required: subscribe"
|
||||
}
|
||||
}
|
||||
|
||||
negentropy := ""
|
||||
if khatru.IsNegentropySession(ctx) {
|
||||
negentropy = color.HiBlueString("negentropy ")
|
||||
}
|
||||
|
||||
log(" got %s%s %v\n", negentropy, color.HiYellowString("request"), colors.italic(filter))
|
||||
authedString := ""
|
||||
if pubkey, ok := khatru.GetAuthed(ctx); ok {
|
||||
authedString = fmt.Sprintf(" from %s", color.GreenString(pubkey.Hex()))
|
||||
}
|
||||
|
||||
log(" got %s%s %v%s\n",
|
||||
negentropy, color.HiYellowString("request"), colors.italic(filter), authedString)
|
||||
printStatus()
|
||||
return false, ""
|
||||
}
|
||||
|
||||
rl.OnCount = func(ctx context.Context, filter nostr.Filter) (reject bool, msg string) {
|
||||
if c.Bool("auth") {
|
||||
if _, isAuthed := khatru.GetAuthed(ctx); !isAuthed {
|
||||
return true, "auth-required: count"
|
||||
}
|
||||
}
|
||||
|
||||
log(" got %s %v\n", color.HiCyanString("count request"), colors.italic(filter))
|
||||
printStatus()
|
||||
return false, ""
|
||||
}
|
||||
|
||||
rl.OnEvent = func(ctx context.Context, event nostr.Event) (reject bool, msg string) {
|
||||
if c.Bool("auth") {
|
||||
if _, isAuthed := khatru.GetAuthed(ctx); !isAuthed {
|
||||
return true, "auth-required: event"
|
||||
}
|
||||
}
|
||||
|
||||
log(" got %s %v\n", color.BlueString("event"), colors.italic(event))
|
||||
printStatus()
|
||||
return false, ""
|
||||
|
||||
@@ -23,7 +23,7 @@ var spell = &cli.Command{
|
||||
Usage: "downloads a spell event and executes its REQ request",
|
||||
ArgsUsage: "[nevent_code]",
|
||||
Description: `fetches a spell event (kind 777) and executes REQ command encoded in its tags.`,
|
||||
Flags: append(defaultKeyFlags,
|
||||
Flags: combineFlags([][]cli.Flag{},
|
||||
&cli.StringFlag{
|
||||
Name: "pub",
|
||||
Usage: "public key to run spells in the context of (if you don't want to pass a --sec)",
|
||||
|
||||
@@ -124,9 +124,27 @@ func getBoundKey(b negentropy.Bound) boundKey {
|
||||
type RelayThirdPartyRemote struct {
|
||||
relay *nostr.Relay
|
||||
messages chan string
|
||||
mu sync.Mutex
|
||||
err error
|
||||
}
|
||||
|
||||
// fail records the error and closes the messages channel so a Receive()
|
||||
// that is already blocked on it wakes up instead of hanging forever.
|
||||
func (rtpr *RelayThirdPartyRemote) fail(err error) {
|
||||
rtpr.mu.Lock()
|
||||
if rtpr.err == nil {
|
||||
rtpr.err = err
|
||||
close(rtpr.messages)
|
||||
}
|
||||
rtpr.mu.Unlock()
|
||||
}
|
||||
|
||||
func (rtpr *RelayThirdPartyRemote) getErr() error {
|
||||
rtpr.mu.Lock()
|
||||
defer rtpr.mu.Unlock()
|
||||
return rtpr.err
|
||||
}
|
||||
|
||||
func NewRelayThirdPartyRemote(ctx context.Context, url string) (*RelayThirdPartyRemote, error) {
|
||||
rtpr := &RelayThirdPartyRemote{
|
||||
messages: make(chan string, 3),
|
||||
@@ -141,13 +159,18 @@ func NewRelayThirdPartyRemote(ctx context.Context, url string) (*RelayThirdParty
|
||||
}
|
||||
switch env := envelope.(type) {
|
||||
case *nip77.OpenEnvelope, *nip77.CloseEnvelope:
|
||||
rtpr.err = fmt.Errorf("unexpected %s received from relay", env.Label())
|
||||
rtpr.fail(fmt.Errorf("unexpected %s received from relay", env.Label()))
|
||||
return
|
||||
case *nip77.ErrorEnvelope:
|
||||
rtpr.err = fmt.Errorf("relay returned a %s: %s", env.Label(), env.Reason)
|
||||
rtpr.fail(fmt.Errorf("relay returned a %s: %s", env.Label(), env.Reason))
|
||||
return
|
||||
case *nip77.MessageEnvelope:
|
||||
rtpr.messages <- env.Message
|
||||
rtpr.mu.Lock()
|
||||
failed := rtpr.err != nil
|
||||
rtpr.mu.Unlock()
|
||||
if !failed {
|
||||
rtpr.messages <- env.Message
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -185,12 +208,13 @@ func (rtpr *RelayThirdPartyRemote) SendClose() error {
|
||||
var thirdPartyRemoteEndOfMessages = errors.New("the-end")
|
||||
|
||||
func (rtpr *RelayThirdPartyRemote) Receive() (string, error) {
|
||||
if rtpr.err != nil {
|
||||
return "", rtpr.err
|
||||
}
|
||||
if msg, ok := <-rtpr.messages; ok {
|
||||
return msg, nil
|
||||
}
|
||||
// channel closed by fail()
|
||||
if err := rtpr.getErr(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return "", thirdPartyRemoteEndOfMessages
|
||||
}
|
||||
|
||||
@@ -204,6 +228,9 @@ func NewThirdPartyNegentropy(peerA, peerB *RelayThirdPartyRemote, filter nostr.F
|
||||
}
|
||||
|
||||
func (n *ThirdPartyNegentropy) Run(ctx context.Context) error {
|
||||
// always close Deltas so the consumer goroutine doesn't hang when we return an error
|
||||
defer close(n.Deltas)
|
||||
|
||||
peerAIds := make(map[nostr.ID]struct{})
|
||||
peerBIds := make(map[nostr.ID]struct{})
|
||||
peerASkippedBounds := make(map[boundKey]struct{})
|
||||
@@ -341,7 +368,6 @@ func (n *ThirdPartyNegentropy) Run(ctx context.Context) error {
|
||||
|
||||
n.PeerA.SendClose()
|
||||
n.PeerB.SendClose()
|
||||
close(n.Deltas)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
+2
-1
@@ -51,7 +51,8 @@ nak event -k 1 -p not_a_pubkey | nak validate
|
||||
}
|
||||
|
||||
for stdinEvent := range getJsonsOrBlank() {
|
||||
if stdinEvent == "" {
|
||||
if stdinEvent == "{}" && !isPiped() {
|
||||
// blank sentinel from getJsonsOrBlank(), use the arguments instead
|
||||
for _, arg := range c.Args().Slice() {
|
||||
if err := handleEvent(arg); err != nil {
|
||||
ctx = lineProcessingError(ctx, "%s", err)
|
||||
|
||||
@@ -19,7 +19,8 @@ it outputs nothing if the verification is successful.`,
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
for stdinEvent := range getJsonsOrBlank() {
|
||||
evt := nostr.Event{}
|
||||
if stdinEvent == "" {
|
||||
if stdinEvent == "{}" && !isPiped() {
|
||||
// blank sentinel from getJsonsOrBlank(), use the argument instead
|
||||
stdinEvent = c.Args().First()
|
||||
if stdinEvent == "" {
|
||||
continue
|
||||
|
||||
@@ -105,7 +105,7 @@ var wallet = &cli.Command{
|
||||
Usage: "displays the current wallet balance",
|
||||
Description: "all wallet data is stored on Nostr relays, signed and encrypted with the given key, and reloaded again from relays on every call.\n\nthe same data can be accessed by other compatible nip60 clients.",
|
||||
DisableSliceFlagSeparator: true,
|
||||
Flags: append(defaultKeyFlags,
|
||||
Flags: combineFlags([][]cli.Flag{},
|
||||
&cli.BoolFlag{
|
||||
Name: "stream",
|
||||
Usage: "keep listening for wallet-related events and logging them",
|
||||
@@ -499,8 +499,13 @@ var wallet = &cli.Command{
|
||||
} else {
|
||||
return fmt.Errorf("missing --private-key")
|
||||
}
|
||||
} else if sk := c.String("private-key"); sk != "" && !c.Bool("force") {
|
||||
return fmt.Errorf("refusing to replace existing private key, use the --force flag")
|
||||
} else if sk := c.String("private-key"); sk != "" {
|
||||
if !c.Bool("force") {
|
||||
return fmt.Errorf("refusing to replace existing private key, use the --force flag")
|
||||
}
|
||||
if err := w.SetPrivateKey(ctx, sk); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
kr, _, _ := gatherKeyerFromArguments(ctx, c)
|
||||
@@ -517,13 +522,18 @@ var wallet = &cli.Command{
|
||||
info.ParseEvent(ie.Event)
|
||||
}
|
||||
|
||||
if mints := c.StringSlice("mints"); len(mints) == 0 && len(info.Mints) == 0 {
|
||||
if mints := c.StringSlice("mint"); len(mints) > 0 {
|
||||
info.Mints = mints
|
||||
} else if len(info.Mints) == 0 {
|
||||
info.Mints = w.Mints
|
||||
}
|
||||
if len(info.Mints) == 0 {
|
||||
return fmt.Errorf("missing --mint")
|
||||
}
|
||||
|
||||
// the pubkey tag is mandatory, without it no one can send us nutzaps
|
||||
info.PublicKey = nostr.PubKey(w.PublicKey.SerializeCompressed()[1:])
|
||||
|
||||
evt := nostr.Event{}
|
||||
if err := info.ToEvent(ctx, kr, &evt); err != nil {
|
||||
return err
|
||||
|
||||
Reference in New Issue
Block a user