mirror of
https://github.com/fiatjaf/nak.git
synced 2026-08-02 04:36: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 |
@@ -47,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))
|
||||
@@ -121,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
|
||||
@@ -131,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
|
||||
@@ -141,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")
|
||||
@@ -166,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}
|
||||
}
|
||||
@@ -177,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)
|
||||
}
|
||||
|
||||
+8
-3
@@ -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"
|
||||
@@ -153,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
|
||||
@@ -235,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)
|
||||
@@ -342,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
|
||||
}
|
||||
@@ -441,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)
|
||||
}
|
||||
|
||||
@@ -63,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",
|
||||
})
|
||||
|
||||
@@ -20,6 +20,9 @@ var curl = &cli.Command{
|
||||
Usage: "calls curl but with a nip98 header",
|
||||
Description: "accepts all flags and arguments exactly as they would be passed to curl.",
|
||||
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 {
|
||||
@@ -129,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...))
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -95,6 +95,11 @@ example:
|
||||
Usage: "filter returned events with jq expression",
|
||||
Category: CATEGORY_EXTRAS,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "jq-raw",
|
||||
Usage: "print --jq string results without JSON quoting, like `jq -r`",
|
||||
Category: CATEGORY_EXTRAS,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "nevent",
|
||||
Usage: "print the nevent code (to stderr) after the event is published",
|
||||
@@ -180,14 +185,14 @@ 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"))
|
||||
jq, err := jqPrepare(c.String("jq"), c.Bool("jq-raw"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -201,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{}
|
||||
@@ -378,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 {
|
||||
@@ -422,12 +430,11 @@ example:
|
||||
}
|
||||
stdout(result)
|
||||
} else {
|
||||
v, matches, err := jq(evt)
|
||||
out, matches, err := jq(evt)
|
||||
if err != nil {
|
||||
return fmt.Errorf("jq filter failed: %w", err)
|
||||
}
|
||||
if matches {
|
||||
out, _ := json.MarshalToString(v)
|
||||
stdout(out)
|
||||
}
|
||||
}
|
||||
@@ -536,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_
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -571,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
|
||||
|
||||
@@ -28,10 +28,14 @@ var fetch = &cli.Command{
|
||||
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"))
|
||||
jq, err := jqPrepare(c.String("jq"), c.Bool("jq-raw"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -131,7 +135,7 @@ var fetch = &cli.Command{
|
||||
if !matches {
|
||||
continue
|
||||
}
|
||||
out, _ = json.MarshalToString(v)
|
||||
out = v
|
||||
}
|
||||
stdout(out)
|
||||
}
|
||||
|
||||
@@ -24,6 +24,10 @@ example:
|
||||
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 {
|
||||
|
||||
@@ -332,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-20260620232658-8389bac80c5f
|
||||
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
|
||||
|
||||
@@ -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-20260620232658-8389bac80c5f h1:dl17ebu+HhtHTS9AdOc27TDUGfSjYVEO4qYLHEwjxOw=
|
||||
fiatjaf.com/nostr v0.0.0-20260620232658-8389bac80c5f/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=
|
||||
|
||||
@@ -29,7 +29,6 @@ var group = &cli.Command{
|
||||
ArgsUsage: "<subcommand> <relay>'<identifier> [flags]",
|
||||
Flags: combineFlags([][]cli.Flag{}),
|
||||
Before: func(ctx context.Context, c *cli.Command) (context.Context, error) {
|
||||
|
||||
return ctx, nil
|
||||
},
|
||||
Commands: []*cli.Command{
|
||||
@@ -244,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{
|
||||
@@ -317,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)
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -475,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)
|
||||
@@ -582,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)
|
||||
@@ -696,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)
|
||||
@@ -760,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})
|
||||
@@ -790,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
|
||||
})
|
||||
},
|
||||
@@ -877,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)
|
||||
|
||||
+69
-45
@@ -48,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 {
|
||||
@@ -219,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, colorizepreamble, logthis)
|
||||
if relay != nil {
|
||||
mu.Lock()
|
||||
relays = append(relays, relay)
|
||||
mu.Unlock()
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
@@ -261,63 +270,75 @@ func connectToSingleRelay(
|
||||
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 c.Bool("force-pre-auth") {
|
||||
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 authSigner(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) {
|
||||
@@ -500,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 {
|
||||
@@ -620,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
|
||||
}
|
||||
|
||||
|
||||
@@ -19,9 +19,11 @@ 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
|
||||
}
|
||||
@@ -36,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 {
|
||||
|
||||
@@ -57,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") {
|
||||
@@ -173,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)
|
||||
@@ -181,6 +196,7 @@ var decryptKey = &cli.Command{
|
||||
}
|
||||
stdout(sk.Hex())
|
||||
}
|
||||
exitIfLineProcessingError(ctx)
|
||||
return nil
|
||||
}
|
||||
default:
|
||||
@@ -212,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)
|
||||
@@ -310,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")
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"fiatjaf.com/nostr"
|
||||
"fiatjaf.com/nostr/nip42"
|
||||
@@ -42,6 +43,7 @@ var defaultKeyFlags = []cli.Flag{
|
||||
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",
|
||||
@@ -50,6 +52,7 @@ var defaultKeyFlags = []cli.Flag{
|
||||
Sources: cli.EnvVars("NOSTR_CLIENT_KEY"),
|
||||
Value: defaultKey(),
|
||||
DefaultText: "the default key (see `nak key default`)",
|
||||
Hidden: true,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -128,6 +131,7 @@ var app = &cli.Command{
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Hidden: true,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "verbose",
|
||||
@@ -141,6 +145,17 @@ var app = &cli.Command{
|
||||
}
|
||||
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,
|
||||
},
|
||||
},
|
||||
defaultKeyFlags,
|
||||
|
||||
@@ -155,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)
|
||||
@@ -239,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")
|
||||
|
||||
@@ -65,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 {
|
||||
@@ -147,7 +146,7 @@ examples:
|
||||
return fmt.Errorf("failed to render markdown: %w", err)
|
||||
}
|
||||
|
||||
fmt.Print(rendered)
|
||||
stdout(rendered)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
+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)
|
||||
}
|
||||
|
||||
|
||||
+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 == "" {
|
||||
|
||||
+181
-48
@@ -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: 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"},
|
||||
@@ -52,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{
|
||||
@@ -69,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)
|
||||
}
|
||||
@@ -148,7 +182,7 @@ 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")...)
|
||||
|
||||
relays := connectToAllRelays(ctx, c, relayUrls)
|
||||
|
||||
@@ -172,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
|
||||
},
|
||||
}
|
||||
|
||||
@@ -46,6 +46,10 @@ example:
|
||||
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",
|
||||
@@ -124,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
|
||||
}
|
||||
@@ -329,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
|
||||
@@ -369,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
|
||||
@@ -437,7 +425,7 @@ readevents:
|
||||
if !matches {
|
||||
continue
|
||||
}
|
||||
out, _ = json.MarshalToString(v)
|
||||
out = v
|
||||
}
|
||||
stdout(out)
|
||||
|
||||
@@ -641,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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