mirror of
https://github.com/fiatjaf/nak.git
synced 2026-08-03 21:16:15 +00:00
Compare commits
23
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
62fa610a06 | ||
|
|
5afdce2672 | ||
|
|
4fd7400272 | ||
|
|
40a934e3f9 | ||
|
|
12000232a7 | ||
|
|
cc879b6bf6 | ||
|
|
483bf94ff4 | ||
|
|
814059fcec | ||
|
|
b3e009730c | ||
|
|
7bbff0a3ad | ||
|
|
c97e1815b4 | ||
|
|
29113783b3 | ||
|
|
59c691f4d5 | ||
|
|
0c8e643609 | ||
|
|
7872c61a4d | ||
|
|
97950d1203 | ||
|
|
0960ceb6a6 | ||
|
|
6a95407939 | ||
|
|
ac988a792d | ||
|
|
fa41c22c12 | ||
|
|
c2902bfb5d | ||
|
|
0f7268c071 | ||
|
|
f7a00cec10 |
@@ -107,7 +107,7 @@ ok.
|
||||
|
||||
### sign an event collaboratively with multiple parties using musig2
|
||||
```shell
|
||||
~> nak event --sec 1234 -k 1 -c 'hello from a combined key' --musig 2
|
||||
~> nak event --sec 1234 -k 'text note' -c 'hello from a combined key' --musig 2
|
||||
the following code should be saved secretly until the next step an included with --musig-nonce-secret:
|
||||
QebOT03ERmV7km22CqEqBPFmzAkgxQzGGbR7Si8yIZCBrd1N9A3LKwGLO71kbgXZ9EYFKpjiwun4u0mj5Tq6vwM3pK7x+EI8oHbkt9majKv/QN24Ix8qnwEIHxXX+mXBug==
|
||||
|
||||
@@ -145,6 +145,33 @@ type the password to decrypt your secret key: **********
|
||||
985d66d2644dfa7676e26046914470d66ebc7fa783a3f57f139fde32d0d631d7
|
||||
```
|
||||
|
||||
### display profile information for any npub, nip05 or hex key
|
||||
|
||||
```shell
|
||||
~> nak profile f7z.io
|
||||
pubkey (hex): fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52
|
||||
npub: npub1l2vyh47mk2p0qlsku7hg0vn29faehy9hy34ygaclpn66ukqp3afqutajft
|
||||
profile uri: nostr:nprofile1qqs04xzt6ldm9qhs0ctw0t58kf4z57umjzmjg6jywu0seadwtqqc75spr9mhxue69uhhq7tjv9kkjepwve5kzar2v9nzucm0d5q3qamnwvaz7te3xsczue3h0ghxjmcppemhxue69uhhytnxxaazu6t0axr23s
|
||||
name: PABLOF7z
|
||||
display_name: PABLOF7z
|
||||
about: Magical Other Stuff Maximalist.
|
||||
picture: https://m.primal.net/KwlG.jpg
|
||||
banner: https://24242.io/1dc3a4a8bb2626551d4cc91e93f5b2e6ef87b1931b9db9bf4ac72dfe61c7a45b.png
|
||||
website: https://pablof7z.com
|
||||
nip05: _@f7z.io (verified)
|
||||
lud16: pablof7z@primal.net
|
||||
relays:
|
||||
wss://relay.damus.io (read)
|
||||
wss://relay.primal.net (read)
|
||||
wss://pyramid.fiatjaf.com (write)
|
||||
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)
|
||||
```shell
|
||||
~> export NOSTR_CLIENT_KEY="$(nak key generate)"
|
||||
@@ -284,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
|
||||
@@ -343,7 +392,7 @@ echo "#surely you're joking, mr npub1l2vyh47mk2p0qlsku7hg0vn29faehy9hy34ygaclpn6
|
||||
|
||||
### record and publish an audio note (yakbak, nostur etc) signed from a bunker
|
||||
```shell
|
||||
ffmpeg -f alsa -i default -f webm -t 00:00:03 pipe:1 | nak blossom --server blossom.primal.net upload | jq -rc '{content: .url}' | nak event -k 1222 --sec 'bunker://urlgoeshere' pyramid.fiatjaf.com nostr.wine
|
||||
ffmpeg -f alsa -i default -f webm -t 00:00:03 pipe:1 | nak blossom --server blossom.primal.net upload | jq -rc '{content: .url}' | nak event -k 'voice message' --sec 'bunker://urlgoeshere' pyramid.fiatjaf.com nostr.wine
|
||||
```
|
||||
|
||||
### gift-wrap an event to a recipient and publish it somewhere
|
||||
@@ -353,7 +402,7 @@ ffmpeg -f alsa -i default -f webm -t 00:00:03 pipe:1 | nak blossom --server blos
|
||||
|
||||
### download a gift-wrap event and unwrap it
|
||||
```shell
|
||||
~> nak req -p <my-public-key> -k 1059 relay.com | nak gift unwrap --sec <my-secret-key> --from <sender-public-key>
|
||||
~> nak req -p <my-public-key> -k 'giftwrap' relay.com | nak gift unwrap --sec <my-secret-key> --from <sender-public-key>
|
||||
```
|
||||
|
||||
### sync events between two relays using negentropy
|
||||
@@ -472,3 +521,40 @@ gitnostr.com... ok.
|
||||
~> nak group chat "<relay>'<id>"
|
||||
~> nak group chat send "<relay>'<id>" "<message>"
|
||||
```
|
||||
|
||||
### figure out what is a given kind
|
||||
```shell
|
||||
~> nak kind 10050 | jq .description
|
||||
"Relay list to receive DMs"
|
||||
~> nak kind 'fav relays' | jq
|
||||
{
|
||||
"kind": 10012,
|
||||
"description": "Favorite relays list",
|
||||
"in_use": true,
|
||||
"content": {
|
||||
"type": "free"
|
||||
},
|
||||
"multiple": [
|
||||
"relay"
|
||||
],
|
||||
"tags": [
|
||||
{
|
||||
"name": "relay",
|
||||
"next": {
|
||||
"type": "relay",
|
||||
"required": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "a",
|
||||
"next": {
|
||||
"type": "addr",
|
||||
"required": true,
|
||||
"next": {
|
||||
"type": "relay"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
@@ -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
|
||||
|
||||
+1
-1
@@ -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"},
|
||||
|
||||
@@ -44,15 +44,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"},
|
||||
@@ -185,7 +176,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 +216,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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -19,7 +19,6 @@ var curl = &cli.Command{
|
||||
Name: "curl",
|
||||
Usage: "calls curl but with a nip98 header",
|
||||
Description: "accepts all flags and arguments exactly as they would be passed to curl.",
|
||||
Flags: defaultKeyFlags,
|
||||
DisableSliceFlagSeparator: true,
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
kr, _, err := gatherKeyerFromArguments(ctx, c)
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"fiatjaf.com/nostr"
|
||||
"fiatjaf.com/nostr/nip19"
|
||||
"fiatjaf.com/nostr/schema"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
@@ -290,7 +291,7 @@ var encode = &cli.Command{
|
||||
Usage: "pubkey of the naddr author",
|
||||
Aliases: []string{"author", "a", "p"},
|
||||
},
|
||||
&cli.IntFlag{
|
||||
&KindFlag{
|
||||
Name: "kind",
|
||||
Aliases: []string{"k"},
|
||||
Usage: "kind of referred replaceable event",
|
||||
@@ -305,12 +306,22 @@ var encode = &cli.Command{
|
||||
Usage: "automatically appends outbox relays to the code",
|
||||
Value: 3,
|
||||
},
|
||||
|
||||
// hidden
|
||||
&cli.StringFlag{
|
||||
Name: "schema",
|
||||
Usage: "url to download the YAML schema from, or path to the file",
|
||||
Value: schema.DefaultSchemaURL,
|
||||
TakesFile: true,
|
||||
Destination: &schemaURI,
|
||||
Hidden: true,
|
||||
},
|
||||
},
|
||||
DisableSliceFlagSeparator: true,
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
for target := range getEncodeSubcommandInput(c.Args(), true) {
|
||||
pubkey := getPubKey(c, "pubkey")
|
||||
kind := nostr.Kind(c.Int("kind"))
|
||||
kind := getKind(c, "kind")
|
||||
d := c.String("identifier")
|
||||
relays := c.StringSlice("relay")
|
||||
|
||||
|
||||
+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")
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"fiatjaf.com/nostr/keyer"
|
||||
"fiatjaf.com/nostr/nip13"
|
||||
"fiatjaf.com/nostr/nip19"
|
||||
"fiatjaf.com/nostr/schema"
|
||||
"github.com/fatih/color"
|
||||
"github.com/mailru/easyjson"
|
||||
"github.com/urfave/cli/v3"
|
||||
@@ -22,6 +23,7 @@ const (
|
||||
CATEGORY_EVENT_FIELDS = "EVENT FIELDS"
|
||||
CATEGORY_SIGNER = "SIGNER OPTIONS"
|
||||
CATEGORY_EXTRAS = "EXTRAS"
|
||||
CATEGORY_AUTH = "AUTH"
|
||||
)
|
||||
|
||||
var eventCmd = &cli.Command{
|
||||
@@ -39,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{
|
||||
@@ -88,9 +90,9 @@ example:
|
||||
Usage: "print the event enveloped in a [\"EVENT\", ...] message ready to be sent to a relay",
|
||||
Category: CATEGORY_EXTRAS,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "auth",
|
||||
Usage: "always perform nip42 \"AUTH\" when facing an \"auth-required: \" rejection and try again",
|
||||
&cli.StringFlag{
|
||||
Name: "jq",
|
||||
Usage: "filter returned events with jq expression",
|
||||
Category: CATEGORY_EXTRAS,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
@@ -104,10 +106,10 @@ example:
|
||||
DefaultText: "false, will only use manually-specified relays",
|
||||
Category: CATEGORY_EXTRAS,
|
||||
},
|
||||
&cli.UintFlag{
|
||||
&KindFlag{
|
||||
Name: "kind",
|
||||
Aliases: []string{"k"},
|
||||
Usage: "event kind",
|
||||
Usage: "event kind number or name",
|
||||
DefaultText: "1",
|
||||
Value: 0,
|
||||
Category: CATEGORY_EVENT_FIELDS,
|
||||
@@ -165,6 +167,16 @@ example:
|
||||
Usage: "ask before publishing the event",
|
||||
Category: CATEGORY_EXTRAS,
|
||||
},
|
||||
|
||||
// hidden
|
||||
&cli.StringFlag{
|
||||
Name: "schema",
|
||||
Usage: "url to download the YAML schema from, or path to the file",
|
||||
Value: schema.DefaultSchemaURL,
|
||||
TakesFile: true,
|
||||
Destination: &schemaURI,
|
||||
Hidden: true,
|
||||
},
|
||||
),
|
||||
ArgsUsage: "[relay...]",
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
@@ -175,6 +187,11 @@ example:
|
||||
return err
|
||||
}
|
||||
|
||||
jq, err := jqPrepare(c.String("jq"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// then process input and generate events:
|
||||
|
||||
// will reuse this
|
||||
@@ -197,8 +214,8 @@ example:
|
||||
return fmt.Errorf("invalid event received from stdin: %s", err)
|
||||
}
|
||||
|
||||
if kind := c.Uint("kind"); slices.Contains(c.FlagNames(), "kind") {
|
||||
evt.Kind = nostr.Kind(kind)
|
||||
if c.IsSet("kind") {
|
||||
evt.Kind = getKind(c, "kind")
|
||||
mustRehashAndResign = true
|
||||
} else if !kindWasSupplied {
|
||||
evt.Kind = 1
|
||||
@@ -385,10 +402,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)
|
||||
@@ -397,15 +411,26 @@ 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)
|
||||
v, matches, err := jq(evt)
|
||||
if err != nil {
|
||||
return fmt.Errorf("jq filter failed: %w", err)
|
||||
}
|
||||
if matches {
|
||||
out, _ := json.MarshalToString(v)
|
||||
stdout(out)
|
||||
}
|
||||
}
|
||||
stdout(result)
|
||||
|
||||
return publishFlow(ctx, c, kr, evt, relays)
|
||||
}
|
||||
|
||||
@@ -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,24 @@ 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",
|
||||
},
|
||||
),
|
||||
ArgsUsage: "[nip05_or_nip19_code]",
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
jq, err := jqPrepare(c.String("jq"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for code := range getStdinLinesOrArguments(c.Args()) {
|
||||
filter := nostr.Filter{}
|
||||
var authorHint nostr.PubKey
|
||||
@@ -115,25 +115,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, _ = json.MarshalToString(v)
|
||||
}
|
||||
stdout(out)
|
||||
}
|
||||
|
||||
if !found {
|
||||
|
||||
@@ -19,7 +19,7 @@ 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",
|
||||
|
||||
@@ -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]
|
||||
)
|
||||
@@ -289,3 +328,55 @@ type (
|
||||
func getIDSlice(cmd *cli.Command, name string) []nostr.ID {
|
||||
return cmd.Value(name).([]nostr.ID)
|
||||
}
|
||||
|
||||
//
|
||||
//
|
||||
//
|
||||
|
||||
type (
|
||||
KindFlag = cli.FlagBase[nostr.Kind, struct{}, kindValue]
|
||||
)
|
||||
|
||||
type kindValue struct {
|
||||
kind nostr.Kind
|
||||
hasBeenSet bool
|
||||
}
|
||||
|
||||
var _ cli.ValueCreator[nostr.Kind, struct{}] = kindValue{}
|
||||
|
||||
func (t kindValue) Create(val nostr.Kind, p *nostr.Kind, c struct{}) cli.Value {
|
||||
*p = val
|
||||
return &kindValue{
|
||||
kind: val,
|
||||
}
|
||||
}
|
||||
|
||||
func (t kindValue) ToString(b nostr.Kind) string { return fmt.Sprintf("%d", b) }
|
||||
|
||||
func (t *kindValue) Set(value string) error {
|
||||
k, err := stringToKind(value)
|
||||
t.kind = k
|
||||
t.hasBeenSet = true
|
||||
return err
|
||||
}
|
||||
|
||||
func (t *kindValue) String() string { return fmt.Sprintf("%#v", t.kind) }
|
||||
func (t *kindValue) Value() nostr.Kind { return t.kind }
|
||||
func (t *kindValue) Get() any { return t.kind }
|
||||
|
||||
func getKind(cmd *cli.Command, name string) nostr.Kind {
|
||||
return cmd.Value(name).(nostr.Kind)
|
||||
}
|
||||
|
||||
//
|
||||
//
|
||||
//
|
||||
|
||||
type (
|
||||
kindSlice = cli.SliceBase[nostr.Kind, struct{}, kindValue]
|
||||
KindSliceFlag = cli.FlagBase[[]nostr.Kind, struct{}, kindSlice]
|
||||
)
|
||||
|
||||
func getKindSlice(cmd *cli.Command, name string) []nostr.Kind {
|
||||
return cmd.Value(name).([]nostr.Kind)
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -33,7 +33,6 @@ aside from those, there is also:
|
||||
- 'nak git init' for setting up nip34 repository metadata; and
|
||||
- 'nak git sync' for getting the latest metadata update from nostr relays (called automatically by other commands)
|
||||
`,
|
||||
Flags: defaultKeyFlags,
|
||||
Commands: []*cli.Command{
|
||||
{
|
||||
Name: "init",
|
||||
|
||||
@@ -3,7 +3,7 @@ module github.com/fiatjaf/nak
|
||||
go 1.25
|
||||
|
||||
require (
|
||||
fiatjaf.com/nostr v0.0.0-20260602223326-015842e96d86
|
||||
fiatjaf.com/nostr v0.0.0-20260620232658-8389bac80c5f
|
||||
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
|
||||
)
|
||||
|
||||
|
||||
@@ -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-20260602223326-015842e96d86 h1:p3HnX1UDT/CfiMvTc4yTcxHQm08ri7DM32P1uKkFNKg=
|
||||
fiatjaf.com/nostr v0.0.0-20260602223326-015842e96d86/go.mod h1:b1EIUDnd133Ie8Pg8O/biaKdFyCMz28aD4n64g1GqvM=
|
||||
fiatjaf.com/nostr v0.0.0-20260620232658-8389bac80c5f h1:dl17ebu+HhtHTS9AdOc27TDUGfSjYVEO4qYLHEwjxOw=
|
||||
fiatjaf.com/nostr v0.0.0-20260620232658-8389bac80c5f/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=
|
||||
@@ -181,6 +183,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/liamg/magic v0.0.1 h1:Ru22ElY+sCh6RvRTWjQzKKCxsEco8hE0co8n1qe7TBM=
|
||||
github.com/liamg/magic v0.0.1/go.mod h1:yQkOmZZI52EA+SQ2xyHpVw8fNvTBruF873Y+Vt6S+fk=
|
||||
github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
|
||||
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo=
|
||||
@@ -300,6 +304,7 @@ golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632
|
||||
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 h1:zfMcR1Cs4KNuomFFgGefv5N0czO2XZpUbxGUy8i8ug0=
|
||||
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
@@ -308,11 +313,13 @@ golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/
|
||||
golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -329,11 +336,13 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
|
||||
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -341,11 +350,14 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
||||
@@ -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,25 +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
|
||||
},
|
||||
@@ -276,68 +258,32 @@ var group = &cli.Command{
|
||||
return err
|
||||
}
|
||||
|
||||
// stored events arrive newest-first before EOSE, so we buffer them
|
||||
// from the end and print them in chronological order once EOSE
|
||||
// arrives; live events are printed as they come.
|
||||
eosed := false
|
||||
var mu sync.Mutex
|
||||
messages := make([]struct {
|
||||
message string
|
||||
rendered bool
|
||||
}, 200)
|
||||
messages := make([]string, 200)
|
||||
base := len(messages)
|
||||
|
||||
// must be called with mu held
|
||||
tryRender := func(i int) {
|
||||
// if all messages before these are loaded we can render this,
|
||||
// otherwise we render whatever we can and stop
|
||||
for m, msg := range messages[base:] {
|
||||
if msg.rendered {
|
||||
continue
|
||||
}
|
||||
if msg.message == "" {
|
||||
break
|
||||
}
|
||||
messages[base+m].rendered = true
|
||||
stdout(msg.message)
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case evt := <-sub.Events:
|
||||
mu.Lock()
|
||||
var i int
|
||||
meta := sys.FetchProfileMetadata(ctx, evt.PubKey)
|
||||
line := color.HiBlueString(meta.ShortName()) + " " + color.HiCyanString(evt.CreatedAt.Time().Format(time.DateTime)) + ": " + evt.Content
|
||||
if eosed {
|
||||
i = len(messages)
|
||||
messages = append(messages, struct {
|
||||
message string
|
||||
rendered bool
|
||||
}{})
|
||||
stdout(line)
|
||||
} else if base > 0 {
|
||||
base--
|
||||
i = base
|
||||
} else {
|
||||
// pre-EOSE buffer is full (relay returned more than the limit); drop.
|
||||
mu.Unlock()
|
||||
continue
|
||||
messages[base] = line
|
||||
}
|
||||
mu.Unlock()
|
||||
|
||||
go func() {
|
||||
meta := sys.FetchProfileMetadata(ctx, evt.PubKey)
|
||||
line := color.HiBlueString(meta.ShortName()) + " " + color.HiCyanString(evt.CreatedAt.Time().Format(time.DateTime)) + ": " + evt.Content
|
||||
mu.Lock()
|
||||
messages[i].message = line
|
||||
if eosed {
|
||||
tryRender(i)
|
||||
}
|
||||
mu.Unlock()
|
||||
}()
|
||||
// else: pre-EOSE buffer is full (relay returned more than the limit); drop.
|
||||
case reason := <-sub.ClosedReason:
|
||||
stdout("closed:" + color.YellowString(reason))
|
||||
case <-sub.EndOfStoredEvents:
|
||||
mu.Lock()
|
||||
eosed = true
|
||||
tryRender(len(messages) - 1)
|
||||
mu.Unlock()
|
||||
for _, msg := range messages[base:] {
|
||||
stdout(msg)
|
||||
}
|
||||
case <-sub.Context.Done():
|
||||
return fmt.Errorf("subscription ended: %w", context.Cause(sub.Context))
|
||||
}
|
||||
|
||||
+93
-30
@@ -13,6 +13,7 @@ import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -21,11 +22,12 @@ 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"
|
||||
"github.com/fatih/color"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/lithammer/fuzzysearch/fuzzy"
|
||||
"github.com/mattn/go-isatty"
|
||||
"github.com/mattn/go-tty/v2"
|
||||
"github.com/urfave/cli/v3"
|
||||
@@ -185,7 +187,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 {
|
||||
@@ -230,7 +231,7 @@ func connectToAllRelays(
|
||||
colorizepreamble(color.CyanString)
|
||||
|
||||
go func() {
|
||||
relay := connectToSingleRelay(ctx, c, url, preAuthSigner, colorizepreamble, logthis)
|
||||
relay := connectToSingleRelay(ctx, c, url, colorizepreamble, logthis)
|
||||
if relay != nil {
|
||||
relays = append(relays, relay)
|
||||
}
|
||||
@@ -242,7 +243,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)
|
||||
}
|
||||
@@ -257,12 +258,11 @@ 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 c.Bool("force-pre-auth") {
|
||||
if colorizepreamble != nil {
|
||||
colorizepreamble(color.YellowString)
|
||||
}
|
||||
@@ -275,7 +275,7 @@ func connectToSingleRelay(
|
||||
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)
|
||||
return authSigner(ctx, c, logthis, authEvent)
|
||||
}); err == nil {
|
||||
// auth succeeded
|
||||
goto preauthSuccess
|
||||
@@ -349,29 +349,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)
|
||||
@@ -501,6 +478,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 {
|
||||
@@ -610,6 +600,65 @@ func clampWithEllipsis(s string, size int) string {
|
||||
return s[0:size-1] + "…"
|
||||
}
|
||||
|
||||
var (
|
||||
schemaURI string
|
||||
fetchSchemaOnce sync.Once
|
||||
schemaCache schema.Schema
|
||||
schemaErrCache error
|
||||
)
|
||||
|
||||
func getSchema() (schema.Schema, error) {
|
||||
fetchSchemaOnce.Do(func() {
|
||||
if strings.HasPrefix(schemaURI, "http") {
|
||||
schemaCache, schemaErrCache = schema.FetchSchemaFromURL(schemaURI)
|
||||
} else {
|
||||
schemaCache, schemaErrCache = schema.NewSchemaFromFile(schemaURI)
|
||||
}
|
||||
})
|
||||
return schemaCache, schemaErrCache
|
||||
}
|
||||
|
||||
func stringToKind(value string) (nostr.Kind, error) {
|
||||
if n, err := strconv.Atoi(value); err == nil && n >= 0 {
|
||||
return nostr.Kind(n), nil
|
||||
}
|
||||
|
||||
// find kind from name
|
||||
sch, err := getSchema()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
fuzzyWords := make([]string, 0, len(sch.Kinds))
|
||||
fuzzyIndexes := make([]string, 0, len(sch.Kinds))
|
||||
|
||||
// exact match
|
||||
for k, ks := range sch.Kinds {
|
||||
fuzzyWords = append(fuzzyWords, ks.Description)
|
||||
fuzzyIndexes = append(fuzzyIndexes, k)
|
||||
|
||||
if strings.EqualFold(ks.Description, value) {
|
||||
return ks.Kind, nil
|
||||
}
|
||||
}
|
||||
|
||||
// fuzzy match
|
||||
result := fuzzy.RankFindNormalizedFold(value, fuzzyWords)
|
||||
bestDesc := "<none>"
|
||||
bestDist := "-"
|
||||
|
||||
if len(result) > 0 {
|
||||
if bd := result[0].Distance; bd < 26 {
|
||||
return sch.Kinds[fuzzyIndexes[result[0].OriginalIndex]].Kind, nil
|
||||
} else {
|
||||
bestDesc = sch.Kinds[fuzzyIndexes[result[0].OriginalIndex]].Description
|
||||
bestDist = strconv.Itoa(bd)
|
||||
}
|
||||
}
|
||||
|
||||
return 0, fmt.Errorf("unknown kind: %q (closest: %q, distance: %s)", value, bestDesc, bestDist)
|
||||
}
|
||||
|
||||
var colors = struct {
|
||||
reset func(...any) (int, error)
|
||||
italic func(...any) string
|
||||
@@ -635,3 +684,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,6 +14,9 @@ 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)
|
||||
|
||||
@@ -28,6 +28,7 @@ var key = &cli.Command{
|
||||
decryptKey,
|
||||
combine,
|
||||
validate,
|
||||
defaultCommand,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -235,6 +236,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",
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"fiatjaf.com/nostr"
|
||||
"fiatjaf.com/nostr/schema"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
var kindCmd = &cli.Command{
|
||||
Name: "kind",
|
||||
Usage: "look up information about a Nostr event kind",
|
||||
Description: `takes a kind number or a kind name and prints information about that kind from the registry of kinds schema.
|
||||
|
||||
example:
|
||||
nak kind 1
|
||||
nak kind "text note"
|
||||
nak kind "git meta"
|
||||
nak kind "fav relays"
|
||||
nak kind 30023`,
|
||||
DisableSliceFlagSeparator: true,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "schema",
|
||||
Usage: "url to download the YAML schema from, or path to the file",
|
||||
Value: schema.DefaultSchemaURL,
|
||||
TakesFile: true,
|
||||
Destination: &schemaURI,
|
||||
},
|
||||
},
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
if c.Args().Len() != 1 {
|
||||
return fmt.Errorf("requires exactly one argument: kind number or name")
|
||||
}
|
||||
|
||||
input := c.Args().First()
|
||||
|
||||
// resolve input to a kind number
|
||||
var k nostr.Kind
|
||||
if n, err := strconv.ParseUint(input, 10, 16); err == nil && n >= 0 {
|
||||
k = nostr.Kind(n)
|
||||
} else {
|
||||
resolved, err := stringToKind(input)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to resolve kind: %w", err)
|
||||
}
|
||||
k = resolved
|
||||
}
|
||||
|
||||
sch, err := getSchema()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ks := sch.Kinds[strconv.Itoa(int(k.Num()))]
|
||||
|
||||
j, _ := json.MarshalIndent(ks, "", " ")
|
||||
stdout(string(j))
|
||||
return nil
|
||||
},
|
||||
}
|
||||
@@ -6,13 +6,53 @@ import (
|
||||
"net/textproto"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"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,
|
||||
},
|
||||
&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`)",
|
||||
},
|
||||
}
|
||||
|
||||
var (
|
||||
version string = "debug"
|
||||
isVerbose bool = false
|
||||
@@ -33,6 +73,7 @@ var app = &cli.Command{
|
||||
decode,
|
||||
encode,
|
||||
key,
|
||||
kindCmd,
|
||||
verify,
|
||||
relay,
|
||||
admin,
|
||||
@@ -52,6 +93,7 @@ var app = &cli.Command{
|
||||
nsite,
|
||||
git,
|
||||
group,
|
||||
podcast,
|
||||
nip,
|
||||
syncCmd,
|
||||
spell,
|
||||
@@ -59,47 +101,51 @@ 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
|
||||
},
|
||||
},
|
||||
&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
|
||||
},
|
||||
},
|
||||
},
|
||||
&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()
|
||||
|
||||
@@ -110,6 +156,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",
|
||||
|
||||
@@ -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 {
|
||||
@@ -129,7 +131,18 @@ 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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,28 +193,43 @@ 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)
|
||||
}
|
||||
|
||||
stdout(evt.String())
|
||||
if identifier == "" {
|
||||
stdout(nip19.EncodeNpub(pk))
|
||||
log(nip19.EncodeNpub(pk) + "\n")
|
||||
} else {
|
||||
stdout(nip5a.PubKeyToBase36(pk) + identifier)
|
||||
log(nip5a.PubKeyToBase36(pk) + identifier + "\n")
|
||||
}
|
||||
|
||||
return publishFlow(ctx, c, kr, evt, relays)
|
||||
|
||||
+626
@@ -0,0 +1,626 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"fiatjaf.com/nostr"
|
||||
"fiatjaf.com/nostr/nip05"
|
||||
"fiatjaf.com/nostr/nip19"
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/fatih/color"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
type podcastInfo struct {
|
||||
PubKey nostr.PubKey
|
||||
Title string
|
||||
Image string
|
||||
Relays []string
|
||||
Metadata nostr.Event
|
||||
}
|
||||
|
||||
var podcast = &cli.Command{
|
||||
Name: "podcast",
|
||||
Usage: "play podcasts from Nostr",
|
||||
Commands: []*cli.Command{
|
||||
{
|
||||
Name: "play",
|
||||
Usage: "play latest episode from podcast or specific episode",
|
||||
ArgsUsage: "<pubkey|nip05|npub|nprofile|nevent>",
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringSliceFlag{
|
||||
Name: "relay",
|
||||
Aliases: []string{"r"},
|
||||
Usage: "also use these relays to fetch from",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "player",
|
||||
Usage: "player command to use, defaults to auto-detect",
|
||||
},
|
||||
},
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
for target := range getStdinLinesOrArguments(c.Args()) {
|
||||
if target == "" {
|
||||
return fmt.Errorf("missing podcast or episode target")
|
||||
}
|
||||
|
||||
relays := c.StringSlice("relay")
|
||||
if err := normalizeAndValidateRelayURLs(relays); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
episode, err := resolvePodcastEpisode(ctx, target, relays)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := playPodcastEpisode(c.String("player"), episode); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "info",
|
||||
Usage: "display podcast metadata",
|
||||
ArgsUsage: "<pubkey|nip05|npub|nprofile>",
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringSliceFlag{
|
||||
Name: "relay",
|
||||
Aliases: []string{"r"},
|
||||
Usage: "also use these relays to fetch from",
|
||||
},
|
||||
},
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
for target := range getStdinLinesOrArguments(c.Args()) {
|
||||
if target == "" {
|
||||
return fmt.Errorf("missing podcast target")
|
||||
}
|
||||
|
||||
relays := c.StringSlice("relay")
|
||||
if err := normalizeAndValidateRelayURLs(relays); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
podcasts, err := resolvePodcastsForPubkey(ctx, target, relays)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, p := range podcasts {
|
||||
printPodcastInfo(p)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "list",
|
||||
Usage: "list podcast episodes",
|
||||
ArgsUsage: "<pubkey|nip05|npub|nprofile>",
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringSliceFlag{
|
||||
Name: "relay",
|
||||
Aliases: []string{"r"},
|
||||
Usage: "also use these relays to fetch from",
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: "limit",
|
||||
Aliases: []string{"l"},
|
||||
Usage: "maximum number of episodes to fetch",
|
||||
Value: 50,
|
||||
},
|
||||
},
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
for target := range getStdinLinesOrArguments(c.Args()) {
|
||||
if target == "" {
|
||||
return fmt.Errorf("missing podcast target")
|
||||
}
|
||||
|
||||
relays := c.StringSlice("relay")
|
||||
if err := normalizeAndValidateRelayURLs(relays); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
podcasts, err := resolvePodcastsForPubkey(ctx, target, relays)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, p := range podcasts {
|
||||
if err := printPodcastEpisodeList(ctx, p, int(c.Int("limit"))); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func resolvePodcastEpisode(ctx context.Context, target string, relayHints []string) (*nostr.Event, error) {
|
||||
target = strings.TrimSpace(strings.TrimPrefix(target, "nostr:"))
|
||||
|
||||
if prefix, value, err := nip19.Decode(target); err == nil {
|
||||
switch prefix {
|
||||
case "nevent":
|
||||
pointer := value.(nostr.EventPointer)
|
||||
return fetchSpecificPodcastEpisode(ctx, pointer, relayHints)
|
||||
case "note":
|
||||
pointer := nostr.EventPointer{ID: value.(nostr.ID)}
|
||||
return fetchSpecificPodcastEpisode(ctx, pointer, relayHints)
|
||||
}
|
||||
}
|
||||
|
||||
if pk, extraRelays, err := resolvePodcastPubKeyTarget(ctx, target); err == nil {
|
||||
return fetchLatestEpisodeForPodcastSelection(ctx, pk, nostr.AppendUnique(relayHints, extraRelays...))
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("invalid podcast target %q", target)
|
||||
}
|
||||
|
||||
func resolvePodcastPubKeyTarget(ctx context.Context, target string) (nostr.PubKey, []string, error) {
|
||||
if strings.TrimSpace(target) == "" {
|
||||
return nostr.ZeroPK, nil, fmt.Errorf("missing pubkey target")
|
||||
}
|
||||
|
||||
if strings.HasPrefix(target, "nostr:") {
|
||||
target = strings.TrimPrefix(target, "nostr:")
|
||||
}
|
||||
|
||||
if prefix, value, err := nip19.Decode(target); err == nil {
|
||||
switch prefix {
|
||||
case "npub":
|
||||
return value.(nostr.PubKey), nil, nil
|
||||
case "nprofile":
|
||||
profile := value.(nostr.ProfilePointer)
|
||||
return profile.PublicKey, profile.Relays, nil
|
||||
}
|
||||
}
|
||||
|
||||
if strings.Contains(target, "@") {
|
||||
pp, err := nip05.QueryIdentifier(ctx, target)
|
||||
if err != nil {
|
||||
return nostr.ZeroPK, nil, err
|
||||
}
|
||||
return pp.PublicKey, pp.Relays, nil
|
||||
}
|
||||
|
||||
pk, err := nostr.PubKeyFromHex(target)
|
||||
if err != nil {
|
||||
return nostr.ZeroPK, nil, err
|
||||
}
|
||||
return pk, nil, nil
|
||||
}
|
||||
|
||||
func fetchSpecificPodcastEpisode(ctx context.Context, pointer nostr.EventPointer, relayHints []string) (*nostr.Event, error) {
|
||||
relays := slices.Clone(relayHints)
|
||||
relays = nostr.AppendUnique(relays, pointer.Relays...)
|
||||
if pointer.Author != nostr.ZeroPK {
|
||||
relays = nostr.AppendUnique(relays, sys.FetchOutboxRelays(ctx, pointer.Author, 3)...)
|
||||
}
|
||||
if len(relays) == 0 {
|
||||
return nil, fmt.Errorf("no relay hints found for episode")
|
||||
}
|
||||
|
||||
res := sys.Pool.QuerySingle(ctx, relays, nostr.Filter{IDs: []nostr.ID{pointer.ID}}, nostr.SubscriptionOptions{Label: "nak-podcast"})
|
||||
if res == nil {
|
||||
return nil, fmt.Errorf("podcast episode not found")
|
||||
}
|
||||
if res.Event.Kind != 54 {
|
||||
return nil, fmt.Errorf("event is not podcast episode: expected kind 54, got %d", res.Event.Kind)
|
||||
}
|
||||
|
||||
return &res.Event, nil
|
||||
}
|
||||
|
||||
func fetchLatestEpisodeForPodcastSelection(ctx context.Context, pk nostr.PubKey, relayHints []string) (*nostr.Event, error) {
|
||||
choices, err := discoverPodcasts(ctx, pk, relayHints)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
chosen := choices[0]
|
||||
if len(choices) > 1 {
|
||||
labels := make([]string, len(choices))
|
||||
for i, item := range choices {
|
||||
labels[i] = podcastLabel(item)
|
||||
}
|
||||
|
||||
var selected string
|
||||
if err := survey.AskOne(&survey.Select{
|
||||
Message: "choose podcast",
|
||||
Options: labels,
|
||||
PageSize: 12,
|
||||
}, &selected); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for i, label := range labels {
|
||||
if label == selected {
|
||||
chosen = choices[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
episode, err := fetchLatestPodcastEpisode(ctx, chosen)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return episode, nil
|
||||
}
|
||||
|
||||
func discoverPodcasts(ctx context.Context, pk nostr.PubKey, relayHints []string) ([]podcastInfo, error) {
|
||||
choices := make([]podcastInfo, 0, 4)
|
||||
seen := make(map[nostr.PubKey]struct{}, 4)
|
||||
|
||||
if direct, ok := fetchPodcastMetadata(ctx, pk, relayHints); ok {
|
||||
choices = append(choices, direct)
|
||||
seen[pk] = struct{}{}
|
||||
}
|
||||
|
||||
authoredRelays := nostr.AppendUnique(slices.Clone(relayHints), sys.FetchOutboxRelays(ctx, pk, 3)...)
|
||||
for _, candidatePubKey := range fetchAuthoredPodcastPubKeys(ctx, pk, authoredRelays) {
|
||||
if _, ok := seen[candidatePubKey]; ok {
|
||||
continue
|
||||
}
|
||||
meta, ok := fetchPodcastMetadata(ctx, candidatePubKey, relayHints)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if !podcastClaimsAuthor(meta.Metadata, pk) {
|
||||
continue
|
||||
}
|
||||
choices = append(choices, meta)
|
||||
seen[candidatePubKey] = struct{}{}
|
||||
}
|
||||
|
||||
if len(choices) == 0 {
|
||||
return nil, fmt.Errorf("no podcasts found for %s", nip19.EncodeNpub(pk))
|
||||
}
|
||||
|
||||
return choices, nil
|
||||
}
|
||||
|
||||
func fetchPodcastMetadata(ctx context.Context, pk nostr.PubKey, relayHints []string) (podcastInfo, bool) {
|
||||
relays := nostr.AppendUnique(slices.Clone(relayHints), sys.FetchOutboxRelays(ctx, pk, 3)...)
|
||||
if len(relays) == 0 {
|
||||
return podcastInfo{}, false
|
||||
}
|
||||
|
||||
res := sys.Pool.FetchManyReplaceable(ctx, relays, nostr.Filter{
|
||||
Kinds: []nostr.Kind{10154},
|
||||
Authors: []nostr.PubKey{pk},
|
||||
}, nostr.SubscriptionOptions{Label: "nak-podcast"})
|
||||
|
||||
evt, ok := res.Load(nostr.ReplaceableKey{PubKey: pk, D: ""})
|
||||
if !ok {
|
||||
return podcastInfo{}, false
|
||||
}
|
||||
|
||||
return podcastInfo{
|
||||
PubKey: pk,
|
||||
Title: podcastTagValue(evt, "title"),
|
||||
Image: podcastTagValue(evt, "image"),
|
||||
Relays: relays,
|
||||
Metadata: evt,
|
||||
}, true
|
||||
}
|
||||
|
||||
func fetchAuthoredPodcastPubKeys(ctx context.Context, pk nostr.PubKey, relays []string) []nostr.PubKey {
|
||||
if len(relays) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
found := make([]nostr.PubKey, 0, 4)
|
||||
seen := make(map[nostr.PubKey]struct{}, 4)
|
||||
for _, kind := range []nostr.Kind{10064, 10164} {
|
||||
res := sys.Pool.FetchManyReplaceable(ctx, relays, nostr.Filter{
|
||||
Kinds: []nostr.Kind{kind},
|
||||
Authors: []nostr.PubKey{pk},
|
||||
}, nostr.SubscriptionOptions{Label: "nak-podcast"})
|
||||
|
||||
evt, ok := res.Load(nostr.ReplaceableKey{PubKey: pk, D: ""})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, tag := range evt.Tags {
|
||||
if len(tag) < 2 || tag[0] != "p" {
|
||||
continue
|
||||
}
|
||||
podcastPK, err := nostr.PubKeyFromHex(tag[1])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[podcastPK]; ok {
|
||||
continue
|
||||
}
|
||||
seen[podcastPK] = struct{}{}
|
||||
found = append(found, podcastPK)
|
||||
}
|
||||
}
|
||||
|
||||
return found
|
||||
}
|
||||
|
||||
func podcastClaimsAuthor(metadata nostr.Event, author nostr.PubKey) bool {
|
||||
for _, tag := range metadata.Tags {
|
||||
if len(tag) >= 2 && tag[0] == "p" && tag[1] == author.Hex() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func fetchLatestPodcastEpisode(ctx context.Context, podcast podcastInfo) (*nostr.Event, error) {
|
||||
relays := podcast.Relays
|
||||
if len(relays) == 0 {
|
||||
relays = sys.FetchOutboxRelays(ctx, podcast.PubKey, 3)
|
||||
}
|
||||
if len(relays) == 0 {
|
||||
return nil, fmt.Errorf("no relay hints found for podcast %s", nip19.EncodeNpub(podcast.PubKey))
|
||||
}
|
||||
|
||||
var latest *nostr.Event
|
||||
for ie := range sys.Pool.FetchMany(ctx, relays, nostr.Filter{
|
||||
Kinds: []nostr.Kind{54},
|
||||
Authors: []nostr.PubKey{podcast.PubKey},
|
||||
Limit: 20,
|
||||
}, nostr.SubscriptionOptions{Label: "nak-podcast"}) {
|
||||
if latest == nil || ie.Event.CreatedAt > latest.CreatedAt {
|
||||
event := ie.Event
|
||||
latest = &event
|
||||
}
|
||||
}
|
||||
|
||||
if latest == nil {
|
||||
return nil, fmt.Errorf("no episodes found for %s", podcastLabel(podcast))
|
||||
}
|
||||
|
||||
return latest, nil
|
||||
}
|
||||
|
||||
func playPodcastEpisode(player string, episode *nostr.Event) error {
|
||||
audioURL := firstPodcastAudioURL(*episode)
|
||||
if audioURL == "" {
|
||||
return fmt.Errorf("podcast episode has no audio tag")
|
||||
}
|
||||
|
||||
title := podcastTagValue(*episode, "title")
|
||||
if title == "" {
|
||||
title = episode.ID.Hex()
|
||||
}
|
||||
|
||||
stdout(colors.bold("playing:"), color.HiBlueString(title))
|
||||
stdout(colors.bold("audio:"), color.HiCyanString(audioURL))
|
||||
|
||||
cmd, err := buildPodcastPlayerCommand(player, audioURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd.Stdin = nil
|
||||
cmd.Stdout = color.Output
|
||||
cmd.Stderr = color.Error
|
||||
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func buildPodcastPlayerCommand(player string, audioURL string) (*exec.Cmd, error) {
|
||||
if strings.TrimSpace(player) != "" {
|
||||
parts := strings.Fields(player)
|
||||
if len(parts) == 0 {
|
||||
return nil, fmt.Errorf("invalid player command %q", player)
|
||||
}
|
||||
return exec.Command(parts[0], append(parts[1:], audioURL)...), nil
|
||||
}
|
||||
|
||||
for _, candidate := range podcastPlayerCandidates() {
|
||||
if _, err := exec.LookPath(candidate.name); err == nil {
|
||||
return exec.Command(candidate.name, append(candidate.args, audioURL)...), nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("no media player found; install mpv, vlc, ffplay or pass --player")
|
||||
}
|
||||
|
||||
func podcastPlayerCandidates() []struct {
|
||||
name string
|
||||
args []string
|
||||
} {
|
||||
if runtime.GOOS == "darwin" {
|
||||
return []struct {
|
||||
name string
|
||||
args []string
|
||||
}{
|
||||
{"mpv", nil},
|
||||
{"vlc", nil},
|
||||
{"ffplay", []string{"-nodisp", "-autoexit"}},
|
||||
{"open", nil},
|
||||
}
|
||||
}
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
return []struct {
|
||||
name string
|
||||
args []string
|
||||
}{
|
||||
{"mpv.exe", nil},
|
||||
{"vlc.exe", nil},
|
||||
{"ffplay.exe", []string{"-nodisp", "-autoexit"}},
|
||||
{"cmd", []string{"/c", "start"}},
|
||||
}
|
||||
}
|
||||
|
||||
return []struct {
|
||||
name string
|
||||
args []string
|
||||
}{
|
||||
{"mpv", nil},
|
||||
{"vlc", nil},
|
||||
{"ffplay", []string{"-nodisp", "-autoexit"}},
|
||||
{"xdg-open", nil},
|
||||
}
|
||||
}
|
||||
|
||||
func firstPodcastAudioURL(evt nostr.Event) string {
|
||||
for _, tag := range evt.Tags {
|
||||
if len(tag) >= 2 && tag[0] == "audio" {
|
||||
return tag[1]
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func podcastTagValue(evt nostr.Event, name string) string {
|
||||
if tag := evt.Tags.Find(name); len(tag) >= 2 {
|
||||
return tag[1]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func podcastLabel(info podcastInfo) string {
|
||||
title := strings.TrimSpace(info.Title)
|
||||
if title == "" {
|
||||
title = nip19.EncodeNpub(info.PubKey)
|
||||
} else {
|
||||
title += " (" + nip19.EncodeNpub(info.PubKey) + ")"
|
||||
}
|
||||
return title
|
||||
}
|
||||
|
||||
func resolvePodcastsForPubkey(ctx context.Context, target string, relayHints []string) ([]podcastInfo, error) {
|
||||
pk, extraRelays, err := resolvePodcastPubKeyTarget(ctx, target)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
allRelays := nostr.AppendUnique(relayHints, extraRelays...)
|
||||
choices, err := discoverPodcasts(ctx, pk, allRelays)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(choices) > 1 {
|
||||
labels := make([]string, len(choices))
|
||||
for i, item := range choices {
|
||||
labels[i] = podcastLabel(item)
|
||||
}
|
||||
|
||||
var selected string
|
||||
if err := survey.AskOne(&survey.Select{
|
||||
Message: "choose podcast",
|
||||
Options: labels,
|
||||
PageSize: 12,
|
||||
}, &selected); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
filtered := make([]podcastInfo, 0, 1)
|
||||
for i, label := range labels {
|
||||
if label == selected {
|
||||
filtered = append(filtered, choices[i])
|
||||
break
|
||||
}
|
||||
}
|
||||
return filtered, nil
|
||||
}
|
||||
|
||||
return choices, nil
|
||||
}
|
||||
|
||||
func printPodcastInfo(p podcastInfo) {
|
||||
stdout(colors.bold("title:"), color.HiBlueString(p.Title))
|
||||
stdout(colors.bold("pubkey:"), color.HiCyanString(nip19.EncodeNpub(p.PubKey)))
|
||||
|
||||
if desc := podcastTagValue(p.Metadata, "description"); desc != "" {
|
||||
stdout(colors.bold("description:"), desc)
|
||||
}
|
||||
if image := podcastTagValue(p.Metadata, "image"); image != "" {
|
||||
stdout(colors.bold("image:"), color.HiCyanString(image))
|
||||
}
|
||||
if website := podcastTagValue(p.Metadata, "website"); website != "" {
|
||||
stdout(colors.bold("website:"), color.HiCyanString(website))
|
||||
}
|
||||
|
||||
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]
|
||||
}
|
||||
pk, err := nostr.PubKeyFromHex(tag[1])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
authors = append(authors, nip19.EncodeNpub(pk)+" "+role)
|
||||
}
|
||||
}
|
||||
if len(authors) > 0 {
|
||||
stdout(colors.bold("authors:"), strings.Join(authors, ", "))
|
||||
}
|
||||
|
||||
stdout("")
|
||||
}
|
||||
|
||||
func printPodcastEpisodeList(ctx context.Context, p podcastInfo, limit int) error {
|
||||
episodes, err := fetchPodcastEpisodes(ctx, p, limit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(episodes) == 0 {
|
||||
stdout(color.YellowString("no episodes found for %s"), p.Title)
|
||||
return nil
|
||||
}
|
||||
|
||||
stdout(color.CyanString("episodes for %s:"), p.Title)
|
||||
for _, ep := range episodes {
|
||||
title := podcastTagValue(ep, "title")
|
||||
if title == "" {
|
||||
title = ep.ID.Hex()[:16]
|
||||
}
|
||||
date := ep.CreatedAt.Time().Format("2006-01-02")
|
||||
desc := ep.Content
|
||||
if len(desc) > 100 {
|
||||
desc = desc[:100] + "..."
|
||||
}
|
||||
stdout(fmt.Sprintf("%s %s %s", date, title, desc))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func fetchPodcastEpisodes(ctx context.Context, p podcastInfo, limit int) ([]nostr.Event, error) {
|
||||
relays := p.Relays
|
||||
if len(relays) == 0 {
|
||||
relays = sys.FetchOutboxRelays(ctx, p.PubKey, 3)
|
||||
}
|
||||
if len(relays) == 0 {
|
||||
return nil, fmt.Errorf("no relay hints found for podcast %s", nip19.EncodeNpub(p.PubKey))
|
||||
}
|
||||
|
||||
episodes := make([]nostr.Event, 0, limit)
|
||||
for ie := range sys.Pool.FetchMany(ctx, relays, nostr.Filter{
|
||||
Kinds: []nostr.Kind{54},
|
||||
Authors: []nostr.PubKey{p.PubKey},
|
||||
Limit: limit,
|
||||
}, nostr.SubscriptionOptions{Label: "nak-podcast-list"}) {
|
||||
episodes = append(episodes, ie.Event)
|
||||
}
|
||||
|
||||
return episodes, nil
|
||||
}
|
||||
+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))
|
||||
|
||||
+2
-10
@@ -23,7 +23,7 @@ example:
|
||||
echo "I agree!" | nak publish --reply nevent1...
|
||||
echo "tagged post" | nak publish -t t=mytag -t e=someeventid`,
|
||||
DisableSliceFlagSeparator: true,
|
||||
Flags: append(defaultKeyFlags,
|
||||
Flags: combineFlags([][]cli.Flag{},
|
||||
&cli.StringFlag{
|
||||
Name: "reply",
|
||||
Usage: "event id, naddr1 or nevent1 code to reply to",
|
||||
@@ -40,11 +40,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",
|
||||
@@ -155,10 +150,7 @@ example:
|
||||
relayUrls = nostr.AppendUnique(relayUrls, replyRelays...)
|
||||
relayUrls = nostr.AppendUnique(relayUrls, c.Args().Slice()...)
|
||||
|
||||
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 {
|
||||
|
||||
@@ -15,9 +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"
|
||||
"github.com/fatih/color"
|
||||
"fiatjaf.com/nostr/schema"
|
||||
"github.com/mailru/easyjson"
|
||||
"github.com/urfave/cli/v3"
|
||||
"golang.org/x/sync/errgroup"
|
||||
@@ -42,69 +41,57 @@ 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: "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 {
|
||||
@@ -146,30 +133,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
|
||||
@@ -503,10 +470,10 @@ var reqFilterFlags = []cli.Flag{
|
||||
Usage: "only accept events with these ids",
|
||||
Category: CATEGORY_FILTER_ATTRIBUTES,
|
||||
},
|
||||
&cli.IntSliceFlag{
|
||||
&KindSliceFlag{
|
||||
Name: "kind",
|
||||
Aliases: []string{"k"},
|
||||
Usage: "only accept events with these kind numbers",
|
||||
Usage: "only accept events with these kind numbers or kind names",
|
||||
Category: CATEGORY_FILTER_ATTRIBUTES,
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
@@ -558,6 +525,16 @@ var reqFilterFlags = []cli.Flag{
|
||||
Usage: "a nip50 search query, use it only with relays that explicitly support it",
|
||||
Category: CATEGORY_FILTER_ATTRIBUTES,
|
||||
},
|
||||
|
||||
// hidden
|
||||
&cli.StringFlag{
|
||||
Name: "schema",
|
||||
Usage: "url to download the YAML schema from, or path to the file",
|
||||
Value: schema.DefaultSchemaURL,
|
||||
TakesFile: true,
|
||||
Destination: &schemaURI,
|
||||
Hidden: true,
|
||||
},
|
||||
}
|
||||
|
||||
type flagTag struct {
|
||||
@@ -587,8 +564,8 @@ func applyFlagsToFilter(c *cli.Command, filter *nostr.Filter) error {
|
||||
if ids := getIDSlice(c, "id"); len(ids) > 0 {
|
||||
filter.IDs = append(filter.IDs, ids...)
|
||||
}
|
||||
for _, kind64 := range c.IntSlice("kind") {
|
||||
filter.Kinds = append(filter.Kinds, nostr.Kind(kind64))
|
||||
if kinds := getKindSlice(c, "kind"); len(kinds) > 0 {
|
||||
filter.Kinds = append(filter.Kinds, kinds...)
|
||||
}
|
||||
if search := c.String("search"); search != "" {
|
||||
filter.Search = search
|
||||
|
||||
@@ -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)",
|
||||
|
||||
+10
-20
@@ -3,7 +3,6 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"fiatjaf.com/nostr"
|
||||
"fiatjaf.com/nostr/schema"
|
||||
@@ -22,29 +21,21 @@ nak event -k 1 -p not_a_pubkey | nak validate
|
||||
DisableSliceFlagSeparator: true,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "schema",
|
||||
Usage: "url to download the YAML schema from, or path to the file",
|
||||
Value: "https://raw.githubusercontent.com/nostr-protocol/registry-of-kinds/refs/heads/master/schema.yaml",
|
||||
TakesFile: true,
|
||||
Name: "schema",
|
||||
Usage: "url to download the YAML schema from, or path to the file",
|
||||
Value: schema.DefaultSchemaURL,
|
||||
TakesFile: true,
|
||||
Destination: &schemaURI,
|
||||
},
|
||||
},
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
var validator schema.Validator
|
||||
|
||||
if schemaURL := c.String("schema"); strings.HasPrefix(schemaURL, "http") {
|
||||
var err error
|
||||
validator, err = schema.NewValidatorFromURL(schemaURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to instantiate validator from '%s': %w", schemaURL, err)
|
||||
}
|
||||
} else {
|
||||
var err error
|
||||
validator, err = schema.NewValidatorFromFile(schemaURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to instantiate validator from %s: %w", schemaURL, err)
|
||||
}
|
||||
sch, err := getSchema()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
validator := schema.NewValidatorFromSchema(sch)
|
||||
|
||||
handleEvent := func(stdinEvent string) error {
|
||||
evt := nostr.Event{}
|
||||
if err := json.Unmarshal([]byte(stdinEvent), &evt); err != nil {
|
||||
@@ -56,7 +47,6 @@ nak event -k 1 -p not_a_pubkey | nak validate
|
||||
}
|
||||
|
||||
stdout(evt)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user