mirror of
https://github.com/fiatjaf/nak.git
synced 2025-12-08 16:48:51 +00:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3031568266 | ||
|
|
a828ee3793 | ||
|
|
186948db9a | ||
|
|
5fe354f642 | ||
|
|
3d961d4bec | ||
|
|
d6a23bd00c | ||
|
|
c1248eb37b | ||
|
|
c60bb82be8 | ||
|
|
f5316a0f35 | ||
|
|
e6448debf2 | ||
|
|
7bb7543ef7 | ||
|
|
43a3e5f40d | ||
|
|
707e5b3918 | ||
|
|
faca2e50f0 | ||
|
|
26930d40bc | ||
|
|
17920d8aef | ||
|
|
95bed5d5a8 | ||
|
|
2e30dfe2eb | ||
|
|
55c6f75b8a | ||
|
|
1f2492c9b1 | ||
|
|
d00976a669 | ||
|
|
4392293ed6 | ||
|
|
60d1292f80 | ||
|
|
6c634d8081 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
||||
nak
|
||||
mnt
|
||||
|
||||
222
blossom.go
Normal file
222
blossom.go
Normal file
@@ -0,0 +1,222 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/nbd-wtf/go-nostr/keyer"
|
||||
"github.com/nbd-wtf/go-nostr/nipb0/blossom"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
var blossomCmd = &cli.Command{
|
||||
Name: "blossom",
|
||||
Suggest: true,
|
||||
UseShortOptionHandling: true,
|
||||
Usage: "an army knife for blossom things",
|
||||
DisableSliceFlagSeparator: true,
|
||||
Flags: append(defaultKeyFlags,
|
||||
&cli.StringFlag{
|
||||
Name: "server",
|
||||
Aliases: []string{"s"},
|
||||
Usage: "the hostname of the target mediaserver",
|
||||
Required: true,
|
||||
},
|
||||
),
|
||||
Commands: []*cli.Command{
|
||||
{
|
||||
Name: "list",
|
||||
Usage: "lists blobs from a pubkey",
|
||||
Description: `takes one pubkey passed as an argument or derives one from the --sec supplied. if that is given then it will also pre-authorize the list, which some servers may require.`,
|
||||
DisableSliceFlagSeparator: true,
|
||||
ArgsUsage: "[pubkey]",
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
var client *blossom.Client
|
||||
pubkey := c.Args().First()
|
||||
if pubkey != "" {
|
||||
client = blossom.NewClient(client.GetMediaServer(), keyer.NewReadOnlySigner(pubkey))
|
||||
} else {
|
||||
var err error
|
||||
client, err = getBlossomClient(ctx, c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
bds, err := client.List(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, bd := range bds {
|
||||
stdout(bd)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "upload",
|
||||
Usage: "uploads a file to a specific mediaserver.",
|
||||
Description: `takes any number of local file paths and uploads them to a mediaserver, printing the resulting blob descriptions when successful.`,
|
||||
DisableSliceFlagSeparator: true,
|
||||
ArgsUsage: "[files...]",
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
client, err := getBlossomClient(ctx, c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hasError := false
|
||||
for _, fpath := range c.Args().Slice() {
|
||||
bd, err := client.UploadFile(ctx, fpath)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%s\n", err)
|
||||
hasError = true
|
||||
continue
|
||||
}
|
||||
|
||||
j, _ := json.Marshal(bd)
|
||||
stdout(string(j))
|
||||
}
|
||||
|
||||
if hasError {
|
||||
os.Exit(3)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "download",
|
||||
Usage: "downloads files from mediaservers",
|
||||
Description: `takes any number of sha256 hashes as hex, downloads them and prints them to stdout (unless --output is specified).`,
|
||||
DisableSliceFlagSeparator: true,
|
||||
ArgsUsage: "[sha256...]",
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringSliceFlag{
|
||||
Name: "output",
|
||||
Aliases: []string{"o"},
|
||||
Usage: "file name to save downloaded file to, can be passed multiple times when downloading multiple hashes",
|
||||
},
|
||||
},
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
client, err := getBlossomClient(ctx, c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
outputs := c.StringSlice("output")
|
||||
|
||||
hasError := false
|
||||
for i, hash := range c.Args().Slice() {
|
||||
if len(outputs)-1 >= i && outputs[i] != "--" {
|
||||
// save to this file
|
||||
err := client.DownloadToFile(ctx, hash, outputs[i])
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%s\n", err)
|
||||
hasError = true
|
||||
}
|
||||
} else {
|
||||
// if output wasn't specified, print to stdout
|
||||
data, err := client.Download(ctx, hash)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%s\n", err)
|
||||
hasError = true
|
||||
continue
|
||||
}
|
||||
os.Stdout.Write(data)
|
||||
}
|
||||
}
|
||||
|
||||
if hasError {
|
||||
os.Exit(2)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "del",
|
||||
Aliases: []string{"delete"},
|
||||
Usage: "deletes a file from a mediaserver",
|
||||
Description: `takes any number of sha256 hashes, signs authorizations and deletes them from the current mediaserver.
|
||||
|
||||
if any of the files are not deleted command will fail, otherwise it will succeed. it will also print error messages to stderr and the hashes it successfully deletes to stdout.`,
|
||||
DisableSliceFlagSeparator: true,
|
||||
ArgsUsage: "[sha256...]",
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
client, err := getBlossomClient(ctx, c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hasError := false
|
||||
for _, hash := range c.Args().Slice() {
|
||||
err := client.Delete(ctx, hash)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%s\n", err)
|
||||
hasError = true
|
||||
continue
|
||||
}
|
||||
|
||||
stdout(hash)
|
||||
}
|
||||
|
||||
if hasError {
|
||||
os.Exit(3)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "check",
|
||||
Usage: "asks the mediaserver if it has the specified hashes.",
|
||||
Description: `uses the HEAD request to succintly check if the server has the specified sha256 hash.
|
||||
|
||||
if any of the files are not found the command will fail, otherwise it will succeed. it will also print error messages to stderr and the hashes it finds to stdout.`,
|
||||
DisableSliceFlagSeparator: true,
|
||||
ArgsUsage: "[sha256...]",
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
client, err := getBlossomClient(ctx, c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hasError := false
|
||||
for _, hash := range c.Args().Slice() {
|
||||
err := client.Check(ctx, hash)
|
||||
if err != nil {
|
||||
hasError = true
|
||||
fmt.Fprintf(os.Stderr, "%s\n", err)
|
||||
continue
|
||||
}
|
||||
|
||||
stdout(hash)
|
||||
}
|
||||
|
||||
if hasError {
|
||||
os.Exit(2)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "mirror",
|
||||
Usage: "",
|
||||
Description: ``,
|
||||
DisableSliceFlagSeparator: true,
|
||||
ArgsUsage: "",
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func getBlossomClient(ctx context.Context, c *cli.Command) (*blossom.Client, error) {
|
||||
keyer, _, err := gatherKeyerFromArguments(ctx, c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return blossom.NewClient(c.String("server"), keyer), nil
|
||||
}
|
||||
33
bunker.go
33
bunker.go
@@ -11,7 +11,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/fiatjaf/cli/v3"
|
||||
"github.com/urfave/cli/v3"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/nbd-wtf/go-nostr/nip19"
|
||||
"github.com/nbd-wtf/go-nostr/nip46"
|
||||
@@ -141,13 +141,11 @@ var bunker = &cli.Command{
|
||||
|
||||
// subscribe to relays
|
||||
now := nostr.Now()
|
||||
events := sys.Pool.SubMany(ctx, relayURLs, nostr.Filters{
|
||||
{
|
||||
Kinds: []int{nostr.KindNostrConnect},
|
||||
Tags: nostr.TagMap{"p": []string{pubkey}},
|
||||
Since: &now,
|
||||
LimitZero: true,
|
||||
},
|
||||
events := sys.Pool.SubscribeMany(ctx, relayURLs, nostr.Filter{
|
||||
Kinds: []int{nostr.KindNostrConnect},
|
||||
Tags: nostr.TagMap{"p": []string{pubkey}},
|
||||
Since: &now,
|
||||
LimitZero: true,
|
||||
})
|
||||
|
||||
signer := nip46.NewStaticKeySigner(sec)
|
||||
@@ -227,4 +225,23 @@ var bunker = &cli.Command{
|
||||
|
||||
return nil
|
||||
},
|
||||
Commands: []*cli.Command{
|
||||
{
|
||||
Name: "connect",
|
||||
Usage: "use the client-initiated NostrConnect flow of NIP46",
|
||||
ArgsUsage: "<nostrconnect-uri>",
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
if c.Args().Len() != 1 {
|
||||
return fmt.Errorf("must be called with a nostrconnect://... uri")
|
||||
}
|
||||
|
||||
uri, err := url.Parse(c.Args().First())
|
||||
if err != nil || uri.Scheme != "nostrconnect" || !nostr.IsValidPublicKey(uri.Host) {
|
||||
return fmt.Errorf("invalid uri")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
2
count.go
2
count.go
@@ -6,7 +6,7 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/fiatjaf/cli/v3"
|
||||
"github.com/urfave/cli/v3"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/nbd-wtf/go-nostr/nip45"
|
||||
"github.com/nbd-wtf/go-nostr/nip45/hyperloglog"
|
||||
|
||||
132
curl.go
Normal file
132
curl.go
Normal file
@@ -0,0 +1,132 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/urfave/cli/v3"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
var curlFlags []string
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// cowboy parsing of curl flags to get the data we need for nip98
|
||||
var url string
|
||||
var method string
|
||||
var presumedMethod string
|
||||
|
||||
curlBodyBuildingFlags := []string{
|
||||
"-d",
|
||||
"--data",
|
||||
"--data-binary",
|
||||
"--data-ascii",
|
||||
"--data-raw",
|
||||
"--data-urlencode",
|
||||
"-F",
|
||||
"--form",
|
||||
"--form-string",
|
||||
"--form-escape",
|
||||
"--upload-file",
|
||||
}
|
||||
|
||||
nextIsMethod := false
|
||||
for _, f := range curlFlags {
|
||||
if nextIsMethod {
|
||||
method = f
|
||||
method, _ = strings.CutPrefix(method, `"`)
|
||||
method, _ = strings.CutSuffix(method, `"`)
|
||||
method = strings.ToUpper(method)
|
||||
} else if strings.HasPrefix(f, "https://") || strings.HasPrefix(f, "http://") {
|
||||
url = f
|
||||
} else if f == "--request" || f == "-X" {
|
||||
nextIsMethod = true
|
||||
continue
|
||||
} else if slices.Contains(curlBodyBuildingFlags, f) ||
|
||||
slices.ContainsFunc(curlBodyBuildingFlags, func(s string) bool {
|
||||
return strings.HasPrefix(f, s)
|
||||
}) {
|
||||
presumedMethod = "POST"
|
||||
}
|
||||
nextIsMethod = false
|
||||
}
|
||||
|
||||
if url == "" {
|
||||
return fmt.Errorf("can't create nip98 event: target url is empty")
|
||||
}
|
||||
|
||||
if method == "" {
|
||||
if presumedMethod != "" {
|
||||
method = presumedMethod
|
||||
} else {
|
||||
method = "GET"
|
||||
}
|
||||
}
|
||||
|
||||
// make and sign event
|
||||
evt := nostr.Event{
|
||||
Kind: 27235,
|
||||
CreatedAt: nostr.Now(),
|
||||
Tags: nostr.Tags{
|
||||
{"u", url},
|
||||
{"method", method},
|
||||
},
|
||||
}
|
||||
if err := kr.SignEvent(ctx, &evt); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// the first 2 indexes of curlFlags were reserved for this
|
||||
curlFlags[0] = "-H"
|
||||
curlFlags[1] = fmt.Sprintf("Authorization: Nostr %s", base64.StdEncoding.EncodeToString([]byte(evt.String())))
|
||||
|
||||
// call curl
|
||||
cmd := exec.Command("curl", curlFlags...)
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Run()
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func realCurl() error {
|
||||
curlFlags = make([]string, 2, max(len(os.Args)-4, 2))
|
||||
keyFlags := make([]string, 0, 5)
|
||||
|
||||
for i := 0; i < len(os.Args[2:]); i++ {
|
||||
arg := os.Args[i+2]
|
||||
if slices.ContainsFunc(defaultKeyFlags, func(f cli.Flag) bool {
|
||||
bareArg, _ := strings.CutPrefix(arg, "-")
|
||||
bareArg, _ = strings.CutPrefix(bareArg, "-")
|
||||
return slices.Contains(f.Names(), bareArg)
|
||||
}) {
|
||||
keyFlags = append(keyFlags, arg)
|
||||
if arg != "--prompt-sec" {
|
||||
i++
|
||||
val := os.Args[i+2]
|
||||
keyFlags = append(keyFlags, val)
|
||||
}
|
||||
} else {
|
||||
curlFlags = append(curlFlags, arg)
|
||||
}
|
||||
}
|
||||
|
||||
return curl.Run(context.Background(), keyFlags)
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"encoding/hex"
|
||||
"strings"
|
||||
|
||||
"github.com/fiatjaf/cli/v3"
|
||||
"github.com/urfave/cli/v3"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/nbd-wtf/go-nostr/nip19"
|
||||
"github.com/nbd-wtf/go-nostr/sdk"
|
||||
|
||||
137
dvm.go
Normal file
137
dvm.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/nbd-wtf/go-nostr/nip90"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
var dvm = &cli.Command{
|
||||
Name: "dvm",
|
||||
Usage: "deal with nip90 data-vending-machine things (experimental)",
|
||||
DisableSliceFlagSeparator: true,
|
||||
Flags: append(defaultKeyFlags,
|
||||
&cli.StringSliceFlag{
|
||||
Name: "relay",
|
||||
Aliases: []string{"r"},
|
||||
},
|
||||
),
|
||||
Commands: append([]*cli.Command{
|
||||
{
|
||||
Name: "list",
|
||||
Usage: "find DVMs that have announced themselves for a specific kind",
|
||||
DisableSliceFlagSeparator: true,
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
return fmt.Errorf("we don't know how to do this yet")
|
||||
},
|
||||
},
|
||||
}, (func() []*cli.Command {
|
||||
commands := make([]*cli.Command, len(nip90.Jobs))
|
||||
for i, job := range nip90.Jobs {
|
||||
flags := make([]cli.Flag, 0, 2+len(job.Params))
|
||||
|
||||
if job.InputType != "" {
|
||||
flags = append(flags, &cli.StringSliceFlag{
|
||||
Name: "input",
|
||||
Aliases: []string{"i"},
|
||||
Category: "INPUT",
|
||||
})
|
||||
}
|
||||
|
||||
for _, param := range job.Params {
|
||||
flags = append(flags, &cli.StringSliceFlag{
|
||||
Name: param,
|
||||
Category: "PARAMETER",
|
||||
})
|
||||
}
|
||||
|
||||
commands[i] = &cli.Command{
|
||||
Name: strconv.Itoa(job.InputKind),
|
||||
Usage: job.Name,
|
||||
Description: job.Description,
|
||||
DisableSliceFlagSeparator: true,
|
||||
Flags: flags,
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
relayUrls := c.StringSlice("relay")
|
||||
relays := connectToAllRelays(ctx, relayUrls, false)
|
||||
if len(relays) == 0 {
|
||||
log("failed to connect to any of the given relays.\n")
|
||||
os.Exit(3)
|
||||
}
|
||||
defer func() {
|
||||
for _, relay := range relays {
|
||||
relay.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
evt := nostr.Event{
|
||||
Kind: job.InputKind,
|
||||
Tags: make(nostr.Tags, 0, 2+len(job.Params)),
|
||||
CreatedAt: nostr.Now(),
|
||||
}
|
||||
|
||||
for _, input := range c.StringSlice("input") {
|
||||
evt.Tags = append(evt.Tags, nostr.Tag{"i", input, job.InputType})
|
||||
}
|
||||
for _, paramN := range job.Params {
|
||||
for _, paramV := range c.StringSlice(paramN) {
|
||||
tag := nostr.Tag{"param", paramN, "", ""}[0:2]
|
||||
for _, v := range strings.Split(paramV, ";") {
|
||||
tag = append(tag, v)
|
||||
}
|
||||
evt.Tags = append(evt.Tags, tag)
|
||||
}
|
||||
}
|
||||
|
||||
kr, _, err := gatherKeyerFromArguments(ctx, c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := kr.SignEvent(ctx, &evt); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logverbose("%s", evt)
|
||||
|
||||
log("- publishing job request... ")
|
||||
first := true
|
||||
for res := range sys.Pool.PublishMany(ctx, relayUrls, evt) {
|
||||
cleanUrl, ok := strings.CutPrefix(res.RelayURL, "wss://")
|
||||
if !ok {
|
||||
cleanUrl = res.RelayURL
|
||||
}
|
||||
|
||||
if !first {
|
||||
log(", ")
|
||||
}
|
||||
first = false
|
||||
|
||||
if res.Error != nil {
|
||||
log("%s: %s", color.RedString(cleanUrl), res.Error)
|
||||
} else {
|
||||
log("%s: ok", color.GreenString(cleanUrl))
|
||||
}
|
||||
}
|
||||
|
||||
log("\n- waiting for response...\n")
|
||||
for ie := range sys.Pool.SubscribeMany(ctx, relayUrls, nostr.Filter{
|
||||
Kinds: []int{7000, job.OutputKind},
|
||||
Tags: nostr.TagMap{"e": []string{evt.ID}},
|
||||
}) {
|
||||
stdout(ie.Event)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
return commands
|
||||
})()...),
|
||||
}
|
||||
@@ -4,9 +4,9 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/fiatjaf/cli/v3"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/nbd-wtf/go-nostr/nip19"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
var encode = &cli.Command{
|
||||
@@ -19,11 +19,11 @@ var encode = &cli.Command{
|
||||
nak encode nevent <event-id>
|
||||
nak encode nevent --author <pubkey-hex> --relay <relay-url> --relay <other-relay> <event-id>
|
||||
nak encode nsec <privkey-hex>`,
|
||||
Before: func(ctx context.Context, c *cli.Command) error {
|
||||
Before: func(ctx context.Context, c *cli.Command) (context.Context, error) {
|
||||
if c.Args().Len() < 1 {
|
||||
return fmt.Errorf("expected more than 1 argument.")
|
||||
return ctx, fmt.Errorf("expected more than 1 argument.")
|
||||
}
|
||||
return nil
|
||||
return ctx, nil
|
||||
},
|
||||
DisableSliceFlagSeparator: true,
|
||||
Commands: []*cli.Command{
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/fiatjaf/cli/v3"
|
||||
"github.com/urfave/cli/v3"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/nbd-wtf/go-nostr/nip04"
|
||||
)
|
||||
|
||||
47
event.go
47
event.go
@@ -8,11 +8,11 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fiatjaf/cli/v3"
|
||||
"github.com/mailru/easyjson"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/nbd-wtf/go-nostr/nip13"
|
||||
"github.com/nbd-wtf/go-nostr/nip19"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -94,7 +94,7 @@ example:
|
||||
&cli.StringFlag{
|
||||
Name: "content",
|
||||
Aliases: []string{"c"},
|
||||
Usage: "event content",
|
||||
Usage: "event content (if it starts with an '@' will read from a file)",
|
||||
DefaultText: "hello from the nostr army knife",
|
||||
Value: "",
|
||||
Category: CATEGORY_EVENT_FIELDS,
|
||||
@@ -140,7 +140,6 @@ example:
|
||||
os.Exit(3)
|
||||
}
|
||||
}
|
||||
|
||||
defer func() {
|
||||
for _, relay := range relays {
|
||||
relay.Close()
|
||||
@@ -154,21 +153,20 @@ example:
|
||||
|
||||
doAuth := c.Bool("auth")
|
||||
|
||||
// then process input and generate events
|
||||
for stdinEvent := range getStdinLinesOrBlank() {
|
||||
evt := nostr.Event{
|
||||
Tags: make(nostr.Tags, 0, 3),
|
||||
}
|
||||
// then process input and generate events:
|
||||
|
||||
kindWasSupplied := false
|
||||
// will reuse this
|
||||
var evt nostr.Event
|
||||
|
||||
// this is called when we have a valid json from stdin
|
||||
handleEvent := func(stdinEvent string) error {
|
||||
evt.Content = ""
|
||||
|
||||
kindWasSupplied := strings.Contains(stdinEvent, `"kind"`)
|
||||
mustRehashAndResign := false
|
||||
|
||||
if stdinEvent != "" {
|
||||
if err := easyjson.Unmarshal([]byte(stdinEvent), &evt); err != nil {
|
||||
ctx = lineProcessingError(ctx, "invalid event received from stdin: %s", err)
|
||||
continue
|
||||
}
|
||||
kindWasSupplied = strings.Contains(stdinEvent, `"kind"`)
|
||||
if err := easyjson.Unmarshal([]byte(stdinEvent), &evt); err != nil {
|
||||
return fmt.Errorf("invalid event received from stdin: %s", err)
|
||||
}
|
||||
|
||||
if kind := c.Uint("kind"); slices.Contains(c.FlagNames(), "kind") {
|
||||
@@ -180,7 +178,16 @@ example:
|
||||
}
|
||||
|
||||
if c.IsSet("content") {
|
||||
evt.Content = c.String("content")
|
||||
content := c.String("content")
|
||||
if strings.HasPrefix(content, "@") {
|
||||
filedata, err := os.ReadFile(content[1:])
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read file '%s' for content: %w", content[1:], err)
|
||||
}
|
||||
evt.Content = string(filedata)
|
||||
} else {
|
||||
evt.Content = content
|
||||
}
|
||||
mustRehashAndResign = true
|
||||
} else if evt.Content == "" && evt.Kind == 1 {
|
||||
evt.Content = "hello from the nostr army knife"
|
||||
@@ -324,6 +331,14 @@ example:
|
||||
log(nevent + "\n")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
for stdinEvent := range getJsonsOrBlank() {
|
||||
if err := handleEvent(stdinEvent); err != nil {
|
||||
ctx = lineProcessingError(ctx, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
exitIfLineProcessingError(ctx)
|
||||
|
||||
4
fetch.go
4
fetch.go
@@ -4,7 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/fiatjaf/cli/v3"
|
||||
"github.com/urfave/cli/v3"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/nbd-wtf/go-nostr/nip05"
|
||||
"github.com/nbd-wtf/go-nostr/nip19"
|
||||
@@ -113,7 +113,7 @@ var fetch = &cli.Command{
|
||||
continue
|
||||
}
|
||||
|
||||
for ie := range sys.Pool.SubManyEose(ctx, relays, nostr.Filters{filter}) {
|
||||
for ie := range sys.Pool.FetchMany(ctx, relays, filter) {
|
||||
stdout(ie.Event)
|
||||
}
|
||||
}
|
||||
|
||||
2
flags.go
2
flags.go
@@ -6,7 +6,7 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/fiatjaf/cli/v3"
|
||||
"github.com/urfave/cli/v3"
|
||||
"github.com/markusmobius/go-dateparser"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
)
|
||||
|
||||
89
fs.go
Normal file
89
fs.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/fiatjaf/nak/nostrfs"
|
||||
"github.com/hanwen/go-fuse/v2/fs"
|
||||
"github.com/hanwen/go-fuse/v2/fuse"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/nbd-wtf/go-nostr/keyer"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
var fsCmd = &cli.Command{
|
||||
Name: "fs",
|
||||
Usage: "mount a FUSE filesystem that exposes Nostr events as files.",
|
||||
Description: `(experimental)`,
|
||||
ArgsUsage: "<mountpoint>",
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "pubkey",
|
||||
Usage: "public key from where to to prepopulate directories",
|
||||
Validator: func(pk string) error {
|
||||
if nostr.IsValidPublicKey(pk) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("invalid public key '%s'", pk)
|
||||
},
|
||||
},
|
||||
},
|
||||
DisableSliceFlagSeparator: true,
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
mountpoint := c.Args().First()
|
||||
if mountpoint == "" {
|
||||
return fmt.Errorf("must be called with a directory path to serve as the mountpoint as an argument")
|
||||
}
|
||||
|
||||
root := nostrfs.NewNostrRoot(
|
||||
ctx,
|
||||
sys,
|
||||
keyer.NewReadOnlyUser(c.String("pubkey")),
|
||||
mountpoint,
|
||||
)
|
||||
|
||||
// create the server
|
||||
log("- mounting at %s... ", color.HiCyanString(mountpoint))
|
||||
timeout := time.Second * 120
|
||||
server, err := fs.Mount(mountpoint, root, &fs.Options{
|
||||
MountOptions: fuse.MountOptions{
|
||||
Debug: isVerbose,
|
||||
Name: "nak",
|
||||
FsName: "nak",
|
||||
},
|
||||
AttrTimeout: &timeout,
|
||||
EntryTimeout: &timeout,
|
||||
Logger: nostr.DebugLogger,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("mount failed: %w", err)
|
||||
}
|
||||
log("ok\n")
|
||||
|
||||
// setup signal handling for clean unmount
|
||||
ch := make(chan os.Signal, 1)
|
||||
chErr := make(chan error)
|
||||
signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
|
||||
go func() {
|
||||
<-ch
|
||||
log("- unmounting... ")
|
||||
err := server.Unmount()
|
||||
if err != nil {
|
||||
chErr <- fmt.Errorf("unmount failed: %w", err)
|
||||
} else {
|
||||
log("ok\n")
|
||||
chErr <- nil
|
||||
}
|
||||
}()
|
||||
|
||||
// serve the filesystem until unmounted
|
||||
server.Wait()
|
||||
return <-chErr
|
||||
},
|
||||
}
|
||||
31
go.mod
31
go.mod
@@ -10,32 +10,38 @@ require (
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0
|
||||
github.com/fatih/color v1.16.0
|
||||
github.com/fiatjaf/cli/v3 v3.0.0-20240723181502-e7dd498b16ae
|
||||
github.com/fiatjaf/eventstore v0.15.0
|
||||
github.com/fiatjaf/khatru v0.15.0
|
||||
github.com/fiatjaf/khatru v0.16.0
|
||||
github.com/hanwen/go-fuse/v2 v2.7.2
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/mailru/easyjson v0.9.0
|
||||
github.com/mark3labs/mcp-go v0.8.3
|
||||
github.com/markusmobius/go-dateparser v1.2.3
|
||||
github.com/nbd-wtf/go-nostr v0.49.3
|
||||
github.com/nbd-wtf/go-nostr v0.51.0
|
||||
github.com/urfave/cli/v3 v3.0.0-beta1
|
||||
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac
|
||||
)
|
||||
|
||||
require (
|
||||
fiatjaf.com/lib v0.2.0 // indirect
|
||||
github.com/andybalholm/brotli v1.0.5 // indirect
|
||||
github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3 // indirect
|
||||
github.com/andybalholm/brotli v1.1.1 // indirect
|
||||
github.com/btcsuite/btcd v0.24.2 // indirect
|
||||
github.com/btcsuite/btcd/btcutil v1.1.5 // indirect
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect
|
||||
github.com/bytedance/sonic v1.13.1 // indirect
|
||||
github.com/bytedance/sonic/loader v0.2.4 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/chzyer/logex v1.1.10 // indirect
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect
|
||||
github.com/cloudwego/base64x v0.1.5 // indirect
|
||||
github.com/coder/websocket v1.8.12 // indirect
|
||||
github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect
|
||||
github.com/dgraph-io/ristretto v1.0.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/elliotchance/pie/v2 v2.7.0 // indirect
|
||||
github.com/elnosh/gonuts v0.3.1-0.20250123162555-7c0381a585e3 // indirect
|
||||
github.com/fasthttp/websocket v1.5.7 // indirect
|
||||
github.com/fasthttp/websocket v1.5.12 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/graph-gophers/dataloader/v7 v7.1.0 // indirect
|
||||
@@ -43,27 +49,30 @@ require (
|
||||
github.com/hablullah/go-juliandays v1.0.0 // indirect
|
||||
github.com/jalaali/go-jalaali v0.0.0-20210801064154-80525e88d958 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/klauspost/compress v1.17.11 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/magefile/mage v1.14.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/puzpuzpuz/xsync/v3 v3.4.0 // indirect
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.0 // indirect
|
||||
github.com/rs/cors v1.11.1 // indirect
|
||||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect
|
||||
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 // indirect
|
||||
github.com/tetratelabs/wazero v1.8.0 // indirect
|
||||
github.com/tidwall/gjson v1.18.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.51.0 // indirect
|
||||
github.com/valyala/fasthttp v1.58.0 // indirect
|
||||
github.com/wasilibs/go-re2 v1.3.0 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
golang.org/x/arch v0.15.0 // indirect
|
||||
golang.org/x/crypto v0.32.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect
|
||||
golang.org/x/net v0.34.0 // indirect
|
||||
golang.org/x/sys v0.29.0 // indirect
|
||||
golang.org/x/sys v0.31.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
)
|
||||
|
||||
78
go.sum
78
go.sum
@@ -1,8 +1,10 @@
|
||||
fiatjaf.com/lib v0.2.0 h1:TgIJESbbND6GjOgGHxF5jsO6EMjuAxIzZHPo5DXYexs=
|
||||
fiatjaf.com/lib v0.2.0/go.mod h1:Ycqq3+mJ9jAWu7XjbQI1cVr+OFgnHn79dQR5oTII47g=
|
||||
github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3 h1:ClzzXMDDuUbWfNNZqGeYq4PnYOlwlOVIvSyNaIy0ykg=
|
||||
github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3/go.mod h1:we0YA5CsBbH5+/NUzC/AlMmxaDtWlXeNsqrwXjTzmzA=
|
||||
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
|
||||
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
|
||||
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
||||
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
||||
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
|
||||
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
|
||||
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
|
||||
@@ -31,6 +33,11 @@ github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku
|
||||
github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
|
||||
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
|
||||
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
|
||||
github.com/bytedance/sonic v1.13.1 h1:Jyd5CIvdFnkOWuKXr+wm4Nyk2h0yAFsr8ucJgEasO3g=
|
||||
github.com/bytedance/sonic v1.13.1/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
|
||||
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
|
||||
@@ -39,6 +46,9 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5O
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
|
||||
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
|
||||
github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
||||
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -58,20 +68,19 @@ github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WA
|
||||
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/dvyukov/go-fuzz v0.0.0-20200318091601-be3528f3a813/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
|
||||
github.com/elliotchance/pie/v2 v2.7.0 h1:FqoIKg4uj0G/CrLGuMS9ejnFKa92lxE1dEgBD3pShXg=
|
||||
github.com/elliotchance/pie/v2 v2.7.0/go.mod h1:18t0dgGFH006g4eVdDtWfgFZPQEgl10IoEO8YWEq3Og=
|
||||
github.com/elnosh/gonuts v0.3.1-0.20250123162555-7c0381a585e3 h1:k7evIqJ2BtFn191DgY/b03N2bMYA/iQwzr4f/uHYn20=
|
||||
github.com/elnosh/gonuts v0.3.1-0.20250123162555-7c0381a585e3/go.mod h1:vgZomh4YQk7R3w4ltZc0sHwCmndfHkuX6V4sga/8oNs=
|
||||
github.com/fasthttp/websocket v1.5.7 h1:0a6o2OfeATvtGgoMKleURhLT6JqWPg7fYfWnH4KHau4=
|
||||
github.com/fasthttp/websocket v1.5.7/go.mod h1:bC4fxSono9czeXHQUVKxsC0sNjbm7lPJR04GDFqClfU=
|
||||
github.com/fasthttp/websocket v1.5.12 h1:e4RGPpWW2HTbL3zV0Y/t7g0ub294LkiuXXUuTOUInlE=
|
||||
github.com/fasthttp/websocket v1.5.12/go.mod h1:I+liyL7/4moHojiOgUOIKEWm9EIxHqxZChS+aMFltyg=
|
||||
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
||||
github.com/fiatjaf/cli/v3 v3.0.0-20240723181502-e7dd498b16ae h1:0B/1dU3YECIbPoBIRTQ4c0scZCNz9TVHtQpiODGrTTo=
|
||||
github.com/fiatjaf/cli/v3 v3.0.0-20240723181502-e7dd498b16ae/go.mod h1:aAWPO4bixZZxPtOnH6K3q4GbQ0jftUNDW9Oa861IRew=
|
||||
github.com/fiatjaf/eventstore v0.15.0 h1:5UXe0+vIb30/cYcOWipks8nR3g+X8W224TFy5yPzivk=
|
||||
github.com/fiatjaf/eventstore v0.15.0/go.mod h1:KAsld5BhkmSck48aF11Txu8X+OGNmoabw4TlYVWqInc=
|
||||
github.com/fiatjaf/khatru v0.15.0 h1:0aLWiTrdzoKD4WmW35GWL/Jsn4dACCUw325JKZg/AmI=
|
||||
github.com/fiatjaf/khatru v0.15.0/go.mod h1:GBQJXZpitDatXF9RookRXcWB5zCJclCE4ufDK3jk80g=
|
||||
github.com/fiatjaf/khatru v0.16.0 h1:xgGwnnOqE3989wEWm7c/z6Y6g4X92BFe/Xp1UWQ3Zmc=
|
||||
github.com/fiatjaf/khatru v0.16.0/go.mod h1:TLcMgPy3IAPh40VGYq6m+gxEMpDKHj+sumqcuvbSogc=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
|
||||
@@ -97,6 +106,8 @@ github.com/hablullah/go-hijri v1.0.2 h1:drT/MZpSZJQXo7jftf5fthArShcaMtsal0Zf/dnm
|
||||
github.com/hablullah/go-hijri v1.0.2/go.mod h1:OS5qyYLDjORXzK4O1adFw9Q5WfhOcMdAKglDkcTxgWQ=
|
||||
github.com/hablullah/go-juliandays v1.0.0 h1:A8YM7wIj16SzlKT0SRJc9CD29iiaUzpBLzh5hr0/5p0=
|
||||
github.com/hablullah/go-juliandays v1.0.0/go.mod h1:0JOYq4oFOuDja+oospuc61YoX+uNEn7Z6uHYTbBzdGc=
|
||||
github.com/hanwen/go-fuse/v2 v2.7.2 h1:SbJP1sUP+n1UF8NXBA14BuojmTez+mDgOk0bC057HQw=
|
||||
github.com/hanwen/go-fuse/v2 v2.7.2/go.mod h1:ugNaD/iv5JYyS1Rcvi57Wz7/vrLQJo10mmketmoef48=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/jalaali/go-jalaali v0.0.0-20210801064154-80525e88d958 h1:qxLoi6CAcXVzjfvu+KXIXJOAsQB62LXjsfbOaErsVzE=
|
||||
github.com/jalaali/go-jalaali v0.0.0-20210801064154-80525e88d958/go.mod h1:Wqfu7mjUHj9WDzSSPI5KfBclTTEnLveRUFr/ujWnTgE=
|
||||
@@ -108,8 +119,14 @@ github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlT
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
|
||||
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
||||
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo=
|
||||
github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
|
||||
@@ -123,13 +140,15 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78=
|
||||
github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/nbd-wtf/go-nostr v0.49.3 h1:7tsEdMZOtJ764JuMLffkbhVUi4yyf688dbqArLvItPs=
|
||||
github.com/nbd-wtf/go-nostr v0.49.3/go.mod h1:M50QnhkraC5Ol93v3jqxSMm1aGxUQm5mlmkYw5DJzh8=
|
||||
github.com/nbd-wtf/go-nostr v0.51.0 h1:Z6gir3lQmlbQGYkccEPbvHlfCydMWXD6bIqukR4DZqU=
|
||||
github.com/nbd-wtf/go-nostr v0.51.0/go.mod h1:9PcGOZ+e1VOaLvcK0peT4dbip+/eS+eTWXR3HuexQrA=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
@@ -143,15 +162,21 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.4.0 h1:DuVBAdXuGFHv8adVXjWWZ63pJq+NRXOWVXlKDBZ+mJ4=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.4.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.0 h1:i+cMcpEDY1BkNm7lPDkCtE4oElsYLn+EKF8kAu2vXT4=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
|
||||
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
|
||||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk=
|
||||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g=
|
||||
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 h1:D0vL7YNisV2yqE55+q0lFuGse6U8lxlg7fYTctlT5Gc=
|
||||
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38/go.mod h1:sM7Mt7uEoCeFSCBM+qBrqvEo+/9vdmj19wzp3yzUhmg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
|
||||
@@ -164,23 +189,31 @@ github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JT
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/urfave/cli/v3 v3.0.0-beta1 h1:6DTaaUarcM0wX7qj5Hcvs+5Dm3dyUTBbEwIWAjcw9Zg=
|
||||
github.com/urfave/cli/v3 v3.0.0-beta1/go.mod h1:FnIeEMYu+ko8zP1F9Ypr3xkZMIDqW3DR92yUtY39q1Y=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
|
||||
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
|
||||
github.com/valyala/fasthttp v1.58.0 h1:GGB2dWxSbEprU9j0iMJHgdKYJVDyjrOwF9RE59PbRuE=
|
||||
github.com/valyala/fasthttp v1.58.0/go.mod h1:SYXvHHaFp7QZHGKSHmoMipInhrI5StHrhDTYVEjK/Kw=
|
||||
github.com/wasilibs/go-re2 v1.3.0 h1:LFhBNzoStM3wMie6rN2slD1cuYH2CGiHpvNL3UtcsMw=
|
||||
github.com/wasilibs/go-re2 v1.3.0/go.mod h1:AafrCXVvGRJJOImMajgJ2M7rVmWyisVK7sFshbxnVrg=
|
||||
github.com/wasilibs/nottinygc v0.4.0 h1:h1TJMihMC4neN6Zq+WKpLxgd9xCFMw7O9ETLwY2exJQ=
|
||||
github.com/wasilibs/nottinygc v0.4.0/go.mod h1:oDcIotskuYNMpqMF23l7Z8uzD4TC0WXHK8jetlB3HIo=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
golang.org/x/arch v0.15.0 h1:QtOrQd0bTUnhNVNndMpLHNWrDmYzZ2KDqSrEymqInZw=
|
||||
golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
|
||||
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA=
|
||||
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
|
||||
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac h1:l5+whBCLH3iH2ZNHYLbAe58bo7yrN4mVcnkHDYz5vvs=
|
||||
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac/go.mod h1:hH+7mtFmImwwcMvScyxUhjuVHR3HGaDPMn9rMSUUbxo=
|
||||
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=
|
||||
@@ -200,8 +233,8 @@ golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
@@ -225,3 +258,4 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
|
||||
97
helpers.go
97
helpers.go
@@ -4,19 +4,21 @@ import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"iter"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/textproto"
|
||||
"net/url"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/fiatjaf/cli/v3"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/nbd-wtf/go-nostr/sdk"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
var sys *sdk.System
|
||||
@@ -43,60 +45,79 @@ func isPiped() bool {
|
||||
return stat.Mode()&os.ModeCharDevice == 0
|
||||
}
|
||||
|
||||
func getStdinLinesOrBlank() chan string {
|
||||
multi := make(chan string)
|
||||
if hasStdinLines := writeStdinLinesOrNothing(multi); !hasStdinLines {
|
||||
single := make(chan string, 1)
|
||||
single <- ""
|
||||
close(single)
|
||||
return single
|
||||
} else {
|
||||
return multi
|
||||
func getJsonsOrBlank() iter.Seq[string] {
|
||||
var curr strings.Builder
|
||||
|
||||
return func(yield func(string) bool) {
|
||||
hasStdin := writeStdinLinesOrNothing(func(stdinLine string) bool {
|
||||
// we're look for an event, but it may be in multiple lines, so if json parsing fails
|
||||
// we'll try the next line until we're successful
|
||||
curr.WriteString(stdinLine)
|
||||
stdinEvent := curr.String()
|
||||
|
||||
var dummy any
|
||||
if err := json.Unmarshal([]byte(stdinEvent), &dummy); err != nil {
|
||||
return true
|
||||
}
|
||||
|
||||
if !yield(stdinEvent) {
|
||||
return false
|
||||
}
|
||||
|
||||
curr.Reset()
|
||||
return true
|
||||
})
|
||||
|
||||
if !hasStdin {
|
||||
yield("{}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getStdinLinesOrArguments(args cli.Args) chan string {
|
||||
func getStdinLinesOrBlank() iter.Seq[string] {
|
||||
return func(yield func(string) bool) {
|
||||
hasStdin := writeStdinLinesOrNothing(func(stdinLine string) bool {
|
||||
if !yield(stdinLine) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
if !hasStdin {
|
||||
yield("")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getStdinLinesOrArguments(args cli.Args) iter.Seq[string] {
|
||||
return getStdinLinesOrArgumentsFromSlice(args.Slice())
|
||||
}
|
||||
|
||||
func getStdinLinesOrArgumentsFromSlice(args []string) chan string {
|
||||
func getStdinLinesOrArgumentsFromSlice(args []string) iter.Seq[string] {
|
||||
// try the first argument
|
||||
if len(args) > 0 {
|
||||
argsCh := make(chan string, 1)
|
||||
go func() {
|
||||
for _, arg := range args {
|
||||
argsCh <- arg
|
||||
}
|
||||
close(argsCh)
|
||||
}()
|
||||
return argsCh
|
||||
return slices.Values(args)
|
||||
}
|
||||
|
||||
// try the stdin
|
||||
multi := make(chan string)
|
||||
if !writeStdinLinesOrNothing(multi) {
|
||||
close(multi)
|
||||
return func(yield func(string) bool) {
|
||||
writeStdinLinesOrNothing(yield)
|
||||
}
|
||||
return multi
|
||||
}
|
||||
|
||||
func writeStdinLinesOrNothing(ch chan string) (hasStdinLines bool) {
|
||||
func writeStdinLinesOrNothing(yield func(string) bool) (hasStdinLines bool) {
|
||||
if isPiped() {
|
||||
// piped
|
||||
go func() {
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
scanner.Buffer(make([]byte, 16*1024*1024), 256*1024*1024)
|
||||
hasEmittedAtLeastOne := false
|
||||
for scanner.Scan() {
|
||||
ch <- strings.TrimSpace(scanner.Text())
|
||||
hasEmittedAtLeastOne = true
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
scanner.Buffer(make([]byte, 16*1024*1024), 256*1024*1024)
|
||||
hasEmittedAtLeastOne := false
|
||||
for scanner.Scan() {
|
||||
if !yield(strings.TrimSpace(scanner.Text())) {
|
||||
return
|
||||
}
|
||||
if !hasEmittedAtLeastOne {
|
||||
ch <- ""
|
||||
}
|
||||
close(ch)
|
||||
}()
|
||||
return true
|
||||
hasEmittedAtLeastOne = true
|
||||
}
|
||||
return hasEmittedAtLeastOne
|
||||
} else {
|
||||
// not piped
|
||||
return false
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
|
||||
"github.com/chzyer/readline"
|
||||
"github.com/fatih/color"
|
||||
"github.com/fiatjaf/cli/v3"
|
||||
"github.com/urfave/cli/v3"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/nbd-wtf/go-nostr/keyer"
|
||||
"github.com/nbd-wtf/go-nostr/nip19"
|
||||
|
||||
2
key.go
2
key.go
@@ -9,7 +9,7 @@ import (
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
"github.com/fiatjaf/cli/v3"
|
||||
"github.com/urfave/cli/v3"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/nbd-wtf/go-nostr/nip19"
|
||||
"github.com/nbd-wtf/go-nostr/nip49"
|
||||
|
||||
52
main.go
52
main.go
@@ -7,26 +7,28 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/fiatjaf/cli/v3"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/nbd-wtf/go-nostr/sdk"
|
||||
"github.com/nbd-wtf/go-nostr/sdk/hints/memoryh"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
var version string = "debug"
|
||||
var (
|
||||
version string = "debug"
|
||||
isVerbose bool = false
|
||||
)
|
||||
|
||||
var app = &cli.Command{
|
||||
Name: "nak",
|
||||
Suggest: true,
|
||||
UseShortOptionHandling: true,
|
||||
AllowFlagsAfterArguments: true,
|
||||
Usage: "the nostr army knife command-line tool",
|
||||
DisableSliceFlagSeparator: true,
|
||||
Commands: []*cli.Command{
|
||||
req,
|
||||
count,
|
||||
fetch,
|
||||
event,
|
||||
req,
|
||||
fetch,
|
||||
count,
|
||||
decode,
|
||||
encode,
|
||||
key,
|
||||
@@ -34,24 +36,26 @@ var app = &cli.Command{
|
||||
relay,
|
||||
bunker,
|
||||
serve,
|
||||
blossomCmd,
|
||||
encrypt,
|
||||
decrypt,
|
||||
outbox,
|
||||
wallet,
|
||||
mcpServer,
|
||||
curl,
|
||||
dvm,
|
||||
fsCmd,
|
||||
},
|
||||
Version: version,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "config-path",
|
||||
Hidden: true,
|
||||
Persistent: true,
|
||||
Name: "config-path",
|
||||
Hidden: true,
|
||||
},
|
||||
&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"},
|
||||
Persistent: true,
|
||||
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 {
|
||||
@@ -64,20 +68,20 @@ var app = &cli.Command{
|
||||
},
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "verbose",
|
||||
Usage: "print more stuff than normally",
|
||||
Aliases: []string{"v"},
|
||||
Persistent: true,
|
||||
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
|
||||
},
|
||||
},
|
||||
},
|
||||
Before: func(ctx context.Context, c *cli.Command) error {
|
||||
Before: func(ctx context.Context, c *cli.Command) (context.Context, error) {
|
||||
configPath := c.String("config-path")
|
||||
if configPath == "" {
|
||||
if home, err := os.UserHomeDir(); err == nil {
|
||||
@@ -116,7 +120,7 @@ var app = &cli.Command{
|
||||
),
|
||||
)
|
||||
|
||||
return nil
|
||||
return ctx, nil
|
||||
},
|
||||
After: func(ctx context.Context, c *cli.Command) error {
|
||||
// save hints database on exit
|
||||
@@ -140,6 +144,16 @@ func main() {
|
||||
Usage: "prints the version",
|
||||
}
|
||||
|
||||
// a megahack to enable this curl command proxy
|
||||
if len(os.Args) > 2 && os.Args[1] == "curl" {
|
||||
if err := realCurl(); err != nil {
|
||||
stdout(err)
|
||||
colors.reset()
|
||||
os.Exit(1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err := app.Run(context.Background(), os.Args); err != nil {
|
||||
stdout(err)
|
||||
colors.reset()
|
||||
|
||||
61
mcp.go
61
mcp.go
@@ -6,11 +6,12 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/fiatjaf/cli/v3"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/nbd-wtf/go-nostr/nip19"
|
||||
"github.com/nbd-wtf/go-nostr/sdk"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
var mcpServer = &cli.Command{
|
||||
@@ -27,6 +28,9 @@ var mcpServer = &cli.Command{
|
||||
|
||||
s.AddTool(mcp.NewTool("publish_note",
|
||||
mcp.WithDescription("Publish a short note event to Nostr with the given text content"),
|
||||
mcp.WithString("relay",
|
||||
mcp.Description("Relay to publish the note to"),
|
||||
),
|
||||
mcp.WithString("content",
|
||||
mcp.Required(),
|
||||
mcp.Description("Arbitrary string to be published"),
|
||||
@@ -38,6 +42,11 @@ var mcpServer = &cli.Command{
|
||||
), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
content, _ := request.Params.Arguments["content"].(string)
|
||||
mention, _ := request.Params.Arguments["mention"].(string)
|
||||
relayI, ok := request.Params.Arguments["relay"]
|
||||
var relay string
|
||||
if ok {
|
||||
relay, _ = relayI.(string)
|
||||
}
|
||||
|
||||
if mention != "" && !nostr.IsValidPublicKey(mention) {
|
||||
return mcp.NewToolResultError("the given mention isn't a valid public key, it must be 32 bytes hex, like the ones returned by search_profile"), nil
|
||||
@@ -71,16 +80,33 @@ var mcpServer = &cli.Command{
|
||||
relays = []string{"nos.lol", "relay.damus.io"}
|
||||
}
|
||||
|
||||
for res := range sys.Pool.PublishMany(ctx, []string{"nos.lol"}, evt) {
|
||||
// extra relay specified
|
||||
relays = append(relays, relay)
|
||||
|
||||
result := strings.Builder{}
|
||||
result.WriteString(
|
||||
fmt.Sprintf("the event we generated has id '%s', kind '%d' and is signed by pubkey '%s'. ",
|
||||
evt.ID,
|
||||
evt.Kind,
|
||||
evt.PubKey,
|
||||
),
|
||||
)
|
||||
|
||||
for res := range sys.Pool.PublishMany(ctx, relays, evt) {
|
||||
if res.Error != nil {
|
||||
return mcp.NewToolResultError(
|
||||
fmt.Sprintf("there was an error publishing the event to the relay %s",
|
||||
result.WriteString(
|
||||
fmt.Sprintf("there was an error publishing the event to the relay %s. ",
|
||||
res.RelayURL),
|
||||
), nil
|
||||
)
|
||||
} else {
|
||||
result.WriteString(
|
||||
fmt.Sprintf("the event was successfully published to the relay %s. ",
|
||||
res.RelayURL),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return mcp.NewToolResultText("event was successfully published with id " + evt.ID), nil
|
||||
return mcp.NewToolResultText(result.String()), nil
|
||||
})
|
||||
|
||||
s.AddTool(mcp.NewTool("resolve_nostr_uri",
|
||||
@@ -114,7 +140,9 @@ var mcpServer = &cli.Command{
|
||||
pm.ShortName(), pm.PubKey),
|
||||
), nil
|
||||
case "nevent":
|
||||
event, _, err := sys.FetchSpecificEventFromInput(ctx, uri, false)
|
||||
event, _, err := sys.FetchSpecificEventFromInput(ctx, uri, sdk.FetchSpecificEventParameters{
|
||||
WithRelays: false,
|
||||
})
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError("Couldn't find this event anywhere"), nil
|
||||
}
|
||||
@@ -152,13 +180,9 @@ var mcpServer = &cli.Command{
|
||||
mcp.Description("Public key of Nostr user we want to know the relay from where to read"),
|
||||
),
|
||||
), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
name, _ := request.Params.Arguments["name"].(string)
|
||||
re := sys.Pool.QuerySingle(ctx, []string{"relay.nostr.band", "nostr.wine"}, nostr.Filter{Search: name, Kinds: []int{0}})
|
||||
if re == nil {
|
||||
return mcp.NewToolResultError("couldn't find anyone with that name"), nil
|
||||
}
|
||||
|
||||
return mcp.NewToolResultText(re.PubKey), nil
|
||||
pubkey, _ := request.Params.Arguments["pubkey"].(string)
|
||||
res := sys.FetchOutboxRelays(ctx, pubkey, 1)
|
||||
return mcp.NewToolResultText(res[0]), nil
|
||||
})
|
||||
|
||||
s.AddTool(mcp.NewTool("read_events_from_relay",
|
||||
@@ -182,7 +206,11 @@ var mcpServer = &cli.Command{
|
||||
relay, _ := request.Params.Arguments["relay"].(string)
|
||||
limit, _ := request.Params.Arguments["limit"].(int)
|
||||
kind, _ := request.Params.Arguments["kind"].(int)
|
||||
pubkey, _ := request.Params.Arguments["pubkey"].(string)
|
||||
pubkeyI, ok := request.Params.Arguments["pubkey"]
|
||||
var pubkey string
|
||||
if ok {
|
||||
pubkey, _ = pubkeyI.(string)
|
||||
}
|
||||
|
||||
if pubkey != "" && !nostr.IsValidPublicKey(pubkey) {
|
||||
return mcp.NewToolResultError("the given pubkey isn't a valid public key, it must be 32 bytes hex, like the ones returned by search_profile"), nil
|
||||
@@ -196,8 +224,7 @@ var mcpServer = &cli.Command{
|
||||
filter.Authors = []string{pubkey}
|
||||
}
|
||||
|
||||
events := sys.Pool.SubManyEose(ctx, []string{relay}, nostr.Filters{filter})
|
||||
|
||||
events := sys.Pool.FetchMany(ctx, []string{relay}, filter)
|
||||
|
||||
result := strings.Builder{}
|
||||
for ie := range events {
|
||||
|
||||
56
nostrfs/asyncfile.go
Normal file
56
nostrfs/asyncfile.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package nostrfs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
|
||||
"github.com/hanwen/go-fuse/v2/fs"
|
||||
"github.com/hanwen/go-fuse/v2/fuse"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
)
|
||||
|
||||
type AsyncFile struct {
|
||||
fs.Inode
|
||||
ctx context.Context
|
||||
fetched atomic.Bool
|
||||
data []byte
|
||||
ts nostr.Timestamp
|
||||
load func() ([]byte, nostr.Timestamp)
|
||||
}
|
||||
|
||||
var (
|
||||
_ = (fs.NodeOpener)((*AsyncFile)(nil))
|
||||
_ = (fs.NodeGetattrer)((*AsyncFile)(nil))
|
||||
)
|
||||
|
||||
func (af *AsyncFile) Getattr(ctx context.Context, f fs.FileHandle, out *fuse.AttrOut) syscall.Errno {
|
||||
if af.fetched.CompareAndSwap(false, true) {
|
||||
af.data, af.ts = af.load()
|
||||
}
|
||||
|
||||
out.Size = uint64(len(af.data))
|
||||
out.Mtime = uint64(af.ts)
|
||||
return fs.OK
|
||||
}
|
||||
|
||||
func (af *AsyncFile) Open(ctx context.Context, flags uint32) (fs.FileHandle, uint32, syscall.Errno) {
|
||||
if af.fetched.CompareAndSwap(false, true) {
|
||||
af.data, af.ts = af.load()
|
||||
}
|
||||
|
||||
return nil, fuse.FOPEN_KEEP_CACHE, 0
|
||||
}
|
||||
|
||||
func (af *AsyncFile) Read(
|
||||
ctx context.Context,
|
||||
f fs.FileHandle,
|
||||
dest []byte,
|
||||
off int64,
|
||||
) (fuse.ReadResult, syscall.Errno) {
|
||||
end := int(off) + len(dest)
|
||||
if end > len(af.data) {
|
||||
end = len(af.data)
|
||||
}
|
||||
return fuse.ReadResultData(af.data[off:end]), 0
|
||||
}
|
||||
214
nostrfs/entitydir.go
Normal file
214
nostrfs/entitydir.go
Normal file
@@ -0,0 +1,214 @@
|
||||
package nostrfs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/hanwen/go-fuse/v2/fs"
|
||||
"github.com/hanwen/go-fuse/v2/fuse"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/nbd-wtf/go-nostr/nip19"
|
||||
"github.com/nbd-wtf/go-nostr/nip27"
|
||||
"github.com/nbd-wtf/go-nostr/nip92"
|
||||
sdk "github.com/nbd-wtf/go-nostr/sdk"
|
||||
)
|
||||
|
||||
type EntityDir struct {
|
||||
fs.Inode
|
||||
ctx context.Context
|
||||
wd string
|
||||
evt *nostr.Event
|
||||
}
|
||||
|
||||
var _ = (fs.NodeGetattrer)((*EntityDir)(nil))
|
||||
|
||||
func (e *EntityDir) Getattr(ctx context.Context, f fs.FileHandle, out *fuse.AttrOut) syscall.Errno {
|
||||
publishedAt := uint64(e.evt.CreatedAt)
|
||||
out.Ctime = publishedAt
|
||||
|
||||
if tag := e.evt.Tags.Find("published_at"); tag != nil {
|
||||
publishedAt, _ = strconv.ParseUint(tag[1], 10, 64)
|
||||
}
|
||||
out.Mtime = publishedAt
|
||||
|
||||
return fs.OK
|
||||
}
|
||||
|
||||
func FetchAndCreateEntityDir(
|
||||
ctx context.Context,
|
||||
parent fs.InodeEmbedder,
|
||||
wd string,
|
||||
extension string,
|
||||
sys *sdk.System,
|
||||
pointer nostr.EntityPointer,
|
||||
) (*fs.Inode, error) {
|
||||
event, _, err := sys.FetchSpecificEvent(ctx, pointer, sdk.FetchSpecificEventParameters{
|
||||
WithRelays: false,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch: %w", err)
|
||||
}
|
||||
|
||||
return CreateEntityDir(ctx, parent, wd, extension, event), nil
|
||||
}
|
||||
|
||||
func CreateEntityDir(
|
||||
ctx context.Context,
|
||||
parent fs.InodeEmbedder,
|
||||
wd string,
|
||||
extension string,
|
||||
event *nostr.Event,
|
||||
) *fs.Inode {
|
||||
h := parent.EmbeddedInode().NewPersistentInode(
|
||||
ctx,
|
||||
&EntityDir{ctx: ctx, wd: wd, evt: event},
|
||||
fs.StableAttr{Mode: syscall.S_IFDIR, Ino: hexToUint64(event.ID)},
|
||||
)
|
||||
|
||||
var publishedAt uint64
|
||||
if tag := event.Tags.Find("published_at"); tag != nil {
|
||||
publishedAt, _ = strconv.ParseUint(tag[1], 10, 64)
|
||||
}
|
||||
|
||||
npub, _ := nip19.EncodePublicKey(event.PubKey)
|
||||
h.AddChild("@author", h.NewPersistentInode(
|
||||
ctx,
|
||||
&fs.MemSymlink{
|
||||
Data: []byte(wd + "/" + npub),
|
||||
},
|
||||
fs.StableAttr{Mode: syscall.S_IFLNK},
|
||||
), true)
|
||||
|
||||
eventj, _ := json.MarshalIndent(event, "", " ")
|
||||
h.AddChild("event.json", h.NewPersistentInode(
|
||||
ctx,
|
||||
&fs.MemRegularFile{
|
||||
Data: eventj,
|
||||
Attr: fuse.Attr{
|
||||
Mode: 0444,
|
||||
Ctime: uint64(event.CreatedAt),
|
||||
Mtime: uint64(publishedAt),
|
||||
Size: uint64(len(event.Content)),
|
||||
},
|
||||
},
|
||||
fs.StableAttr{},
|
||||
), true)
|
||||
|
||||
h.AddChild("identifier", h.NewPersistentInode(
|
||||
ctx,
|
||||
&fs.MemRegularFile{
|
||||
Data: []byte(event.Tags.GetD()),
|
||||
Attr: fuse.Attr{
|
||||
Mode: 0444,
|
||||
Ctime: uint64(event.CreatedAt),
|
||||
Mtime: uint64(publishedAt),
|
||||
Size: uint64(len(event.Tags.GetD())),
|
||||
},
|
||||
},
|
||||
fs.StableAttr{},
|
||||
), true)
|
||||
|
||||
if tag := event.Tags.Find("title"); tag != nil {
|
||||
h.AddChild("title", h.NewPersistentInode(
|
||||
ctx,
|
||||
&fs.MemRegularFile{
|
||||
Data: []byte(tag[1]),
|
||||
Attr: fuse.Attr{
|
||||
Mode: 0444,
|
||||
Ctime: uint64(event.CreatedAt),
|
||||
Mtime: uint64(publishedAt),
|
||||
Size: uint64(len(tag[1])),
|
||||
},
|
||||
},
|
||||
fs.StableAttr{},
|
||||
), true)
|
||||
}
|
||||
|
||||
h.AddChild("content"+extension, h.NewPersistentInode(
|
||||
ctx,
|
||||
&fs.MemRegularFile{
|
||||
Data: []byte(event.Content),
|
||||
Attr: fuse.Attr{
|
||||
Mode: 0444,
|
||||
Ctime: uint64(event.CreatedAt),
|
||||
Mtime: uint64(publishedAt),
|
||||
Size: uint64(len(event.Content)),
|
||||
},
|
||||
},
|
||||
fs.StableAttr{},
|
||||
), true)
|
||||
|
||||
var refsdir *fs.Inode
|
||||
i := 0
|
||||
for ref := range nip27.ParseReferences(*event) {
|
||||
i++
|
||||
if refsdir == nil {
|
||||
refsdir = h.NewPersistentInode(ctx, &fs.Inode{}, fs.StableAttr{Mode: syscall.S_IFDIR})
|
||||
h.AddChild("references", refsdir, true)
|
||||
}
|
||||
refsdir.AddChild(fmt.Sprintf("ref_%02d", i), refsdir.NewPersistentInode(
|
||||
ctx,
|
||||
&fs.MemSymlink{
|
||||
Data: []byte(wd + "/" + nip19.EncodePointer(ref.Pointer)),
|
||||
},
|
||||
fs.StableAttr{Mode: syscall.S_IFLNK},
|
||||
), true)
|
||||
}
|
||||
|
||||
var imagesdir *fs.Inode
|
||||
addImage := func(url string) {
|
||||
if imagesdir == nil {
|
||||
in := &fs.Inode{}
|
||||
imagesdir = h.NewPersistentInode(ctx, in, fs.StableAttr{Mode: syscall.S_IFDIR})
|
||||
h.AddChild("images", imagesdir, true)
|
||||
}
|
||||
imagesdir.AddChild(filepath.Base(url), imagesdir.NewPersistentInode(
|
||||
ctx,
|
||||
&AsyncFile{
|
||||
ctx: ctx,
|
||||
load: func() ([]byte, nostr.Timestamp) {
|
||||
ctx, cancel := context.WithTimeout(ctx, time.Second*20)
|
||||
defer cancel()
|
||||
r, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, 0
|
||||
}
|
||||
resp, err := http.DefaultClient.Do(r)
|
||||
if err != nil {
|
||||
return nil, 0
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode >= 300 {
|
||||
return nil, 0
|
||||
}
|
||||
w := &bytes.Buffer{}
|
||||
io.Copy(w, resp.Body)
|
||||
return w.Bytes(), 0
|
||||
},
|
||||
},
|
||||
fs.StableAttr{},
|
||||
), true)
|
||||
}
|
||||
|
||||
images := nip92.ParseTags(event.Tags)
|
||||
for _, imeta := range images {
|
||||
if imeta.URL == "" {
|
||||
continue
|
||||
}
|
||||
addImage(imeta.URL)
|
||||
}
|
||||
|
||||
if tag := event.Tags.Find("image"); tag != nil {
|
||||
addImage(tag[1])
|
||||
}
|
||||
|
||||
return h
|
||||
}
|
||||
241
nostrfs/eventdir.go
Normal file
241
nostrfs/eventdir.go
Normal file
@@ -0,0 +1,241 @@
|
||||
package nostrfs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/hanwen/go-fuse/v2/fs"
|
||||
"github.com/hanwen/go-fuse/v2/fuse"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/nbd-wtf/go-nostr/nip10"
|
||||
"github.com/nbd-wtf/go-nostr/nip19"
|
||||
"github.com/nbd-wtf/go-nostr/nip22"
|
||||
"github.com/nbd-wtf/go-nostr/nip27"
|
||||
"github.com/nbd-wtf/go-nostr/nip92"
|
||||
sdk "github.com/nbd-wtf/go-nostr/sdk"
|
||||
)
|
||||
|
||||
type EventDir struct {
|
||||
fs.Inode
|
||||
ctx context.Context
|
||||
wd string
|
||||
evt *nostr.Event
|
||||
}
|
||||
|
||||
var _ = (fs.NodeGetattrer)((*EventDir)(nil))
|
||||
|
||||
func (e *EventDir) Getattr(ctx context.Context, f fs.FileHandle, out *fuse.AttrOut) syscall.Errno {
|
||||
out.Mtime = uint64(e.evt.CreatedAt)
|
||||
return fs.OK
|
||||
}
|
||||
|
||||
func FetchAndCreateEventDir(
|
||||
ctx context.Context,
|
||||
parent fs.InodeEmbedder,
|
||||
wd string,
|
||||
sys *sdk.System,
|
||||
pointer nostr.EventPointer,
|
||||
) (*fs.Inode, error) {
|
||||
event, _, err := sys.FetchSpecificEvent(ctx, pointer, sdk.FetchSpecificEventParameters{
|
||||
WithRelays: false,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch: %w", err)
|
||||
}
|
||||
|
||||
return CreateEventDir(ctx, parent, wd, event), nil
|
||||
}
|
||||
|
||||
func CreateEventDir(
|
||||
ctx context.Context,
|
||||
parent fs.InodeEmbedder,
|
||||
wd string,
|
||||
event *nostr.Event,
|
||||
) *fs.Inode {
|
||||
h := parent.EmbeddedInode().NewPersistentInode(
|
||||
ctx,
|
||||
&EventDir{ctx: ctx, wd: wd, evt: event},
|
||||
fs.StableAttr{Mode: syscall.S_IFDIR, Ino: hexToUint64(event.ID)},
|
||||
)
|
||||
|
||||
npub, _ := nip19.EncodePublicKey(event.PubKey)
|
||||
h.AddChild("@author", h.NewPersistentInode(
|
||||
ctx,
|
||||
&fs.MemSymlink{
|
||||
Data: []byte(wd + "/" + npub),
|
||||
},
|
||||
fs.StableAttr{Mode: syscall.S_IFLNK},
|
||||
), true)
|
||||
|
||||
eventj, _ := json.MarshalIndent(event, "", " ")
|
||||
h.AddChild("event.json", h.NewPersistentInode(
|
||||
ctx,
|
||||
&fs.MemRegularFile{
|
||||
Data: eventj,
|
||||
Attr: fuse.Attr{
|
||||
Mode: 0444,
|
||||
Ctime: uint64(event.CreatedAt),
|
||||
Mtime: uint64(event.CreatedAt),
|
||||
Size: uint64(len(event.Content)),
|
||||
},
|
||||
},
|
||||
fs.StableAttr{},
|
||||
), true)
|
||||
|
||||
h.AddChild("id", h.NewPersistentInode(
|
||||
ctx,
|
||||
&fs.MemRegularFile{
|
||||
Data: []byte(event.ID),
|
||||
Attr: fuse.Attr{
|
||||
Mode: 0444,
|
||||
Ctime: uint64(event.CreatedAt),
|
||||
Mtime: uint64(event.CreatedAt),
|
||||
Size: uint64(64),
|
||||
},
|
||||
},
|
||||
fs.StableAttr{},
|
||||
), true)
|
||||
|
||||
h.AddChild("content.txt", h.NewPersistentInode(
|
||||
ctx,
|
||||
&fs.MemRegularFile{
|
||||
Data: []byte(event.Content),
|
||||
Attr: fuse.Attr{
|
||||
Mode: 0444,
|
||||
Ctime: uint64(event.CreatedAt),
|
||||
Mtime: uint64(event.CreatedAt),
|
||||
Size: uint64(len(event.Content)),
|
||||
},
|
||||
},
|
||||
fs.StableAttr{},
|
||||
), true)
|
||||
|
||||
var refsdir *fs.Inode
|
||||
i := 0
|
||||
for ref := range nip27.ParseReferences(*event) {
|
||||
i++
|
||||
if refsdir == nil {
|
||||
refsdir = h.NewPersistentInode(ctx, &fs.Inode{}, fs.StableAttr{Mode: syscall.S_IFDIR})
|
||||
h.AddChild("references", refsdir, true)
|
||||
}
|
||||
refsdir.AddChild(fmt.Sprintf("ref_%02d", i), refsdir.NewPersistentInode(
|
||||
ctx,
|
||||
&fs.MemSymlink{
|
||||
Data: []byte(wd + "/" + nip19.EncodePointer(ref.Pointer)),
|
||||
},
|
||||
fs.StableAttr{Mode: syscall.S_IFLNK},
|
||||
), true)
|
||||
}
|
||||
|
||||
var imagesdir *fs.Inode
|
||||
images := nip92.ParseTags(event.Tags)
|
||||
for _, imeta := range images {
|
||||
if imeta.URL == "" {
|
||||
continue
|
||||
}
|
||||
if imagesdir == nil {
|
||||
in := &fs.Inode{}
|
||||
imagesdir = h.NewPersistentInode(ctx, in, fs.StableAttr{Mode: syscall.S_IFDIR})
|
||||
h.AddChild("images", imagesdir, true)
|
||||
}
|
||||
imagesdir.AddChild(filepath.Base(imeta.URL), imagesdir.NewPersistentInode(
|
||||
ctx,
|
||||
&AsyncFile{
|
||||
ctx: ctx,
|
||||
load: func() ([]byte, nostr.Timestamp) {
|
||||
ctx, cancel := context.WithTimeout(ctx, time.Second*20)
|
||||
defer cancel()
|
||||
r, err := http.NewRequestWithContext(ctx, "GET", imeta.URL, nil)
|
||||
if err != nil {
|
||||
return nil, 0
|
||||
}
|
||||
resp, err := http.DefaultClient.Do(r)
|
||||
if err != nil {
|
||||
return nil, 0
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode >= 300 {
|
||||
return nil, 0
|
||||
}
|
||||
w := &bytes.Buffer{}
|
||||
io.Copy(w, resp.Body)
|
||||
return w.Bytes(), 0
|
||||
},
|
||||
},
|
||||
fs.StableAttr{},
|
||||
), true)
|
||||
}
|
||||
|
||||
if event.Kind == 1 {
|
||||
if pointer := nip10.GetThreadRoot(event.Tags); pointer != nil {
|
||||
nevent := nip19.EncodePointer(*pointer)
|
||||
h.AddChild("@root", h.NewPersistentInode(
|
||||
ctx,
|
||||
&fs.MemSymlink{
|
||||
Data: []byte(wd + "/" + nevent),
|
||||
},
|
||||
fs.StableAttr{Mode: syscall.S_IFLNK},
|
||||
), true)
|
||||
}
|
||||
if pointer := nip10.GetImmediateParent(event.Tags); pointer != nil {
|
||||
nevent := nip19.EncodePointer(*pointer)
|
||||
h.AddChild("@parent", h.NewPersistentInode(
|
||||
ctx,
|
||||
&fs.MemSymlink{
|
||||
Data: []byte(wd + "/" + nevent),
|
||||
},
|
||||
fs.StableAttr{Mode: syscall.S_IFLNK},
|
||||
), true)
|
||||
}
|
||||
} else if event.Kind == 1111 {
|
||||
if pointer := nip22.GetThreadRoot(event.Tags); pointer != nil {
|
||||
if xp, ok := pointer.(nostr.ExternalPointer); ok {
|
||||
h.AddChild("@root", h.NewPersistentInode(
|
||||
ctx,
|
||||
&fs.MemRegularFile{
|
||||
Data: []byte(`<!doctype html><meta http-equiv="refresh" content="0; url=` + xp.Thing + `" />`),
|
||||
},
|
||||
fs.StableAttr{},
|
||||
), true)
|
||||
} else {
|
||||
nevent := nip19.EncodePointer(pointer)
|
||||
h.AddChild("@parent", h.NewPersistentInode(
|
||||
ctx,
|
||||
&fs.MemSymlink{
|
||||
Data: []byte(wd + "/" + nevent),
|
||||
},
|
||||
fs.StableAttr{Mode: syscall.S_IFLNK},
|
||||
), true)
|
||||
}
|
||||
}
|
||||
if pointer := nip22.GetImmediateParent(event.Tags); pointer != nil {
|
||||
if xp, ok := pointer.(nostr.ExternalPointer); ok {
|
||||
h.AddChild("@parent", h.NewPersistentInode(
|
||||
ctx,
|
||||
&fs.MemRegularFile{
|
||||
Data: []byte(`<!doctype html><meta http-equiv="refresh" content="0; url=` + xp.Thing + `" />`),
|
||||
},
|
||||
fs.StableAttr{},
|
||||
), true)
|
||||
} else {
|
||||
nevent := nip19.EncodePointer(pointer)
|
||||
h.AddChild("@parent", h.NewPersistentInode(
|
||||
ctx,
|
||||
&fs.MemSymlink{
|
||||
Data: []byte(wd + "/" + nevent),
|
||||
},
|
||||
fs.StableAttr{Mode: syscall.S_IFLNK},
|
||||
), true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return h
|
||||
}
|
||||
8
nostrfs/helpers.go
Normal file
8
nostrfs/helpers.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package nostrfs
|
||||
|
||||
import "strconv"
|
||||
|
||||
func hexToUint64(hexStr string) uint64 {
|
||||
v, _ := strconv.ParseUint(hexStr[16:32], 16, 64)
|
||||
return v
|
||||
}
|
||||
228
nostrfs/npubdir.go
Normal file
228
nostrfs/npubdir.go
Normal file
@@ -0,0 +1,228 @@
|
||||
package nostrfs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/hanwen/go-fuse/v2/fs"
|
||||
"github.com/hanwen/go-fuse/v2/fuse"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
sdk "github.com/nbd-wtf/go-nostr/sdk"
|
||||
)
|
||||
|
||||
type NpubDir struct {
|
||||
sys *sdk.System
|
||||
fs.Inode
|
||||
pointer nostr.ProfilePointer
|
||||
ctx context.Context
|
||||
fetched atomic.Bool
|
||||
}
|
||||
|
||||
func CreateNpubDir(
|
||||
ctx context.Context,
|
||||
sys *sdk.System,
|
||||
parent fs.InodeEmbedder,
|
||||
wd string,
|
||||
pointer nostr.ProfilePointer,
|
||||
) *fs.Inode {
|
||||
npubdir := &NpubDir{ctx: ctx, sys: sys, pointer: pointer}
|
||||
h := parent.EmbeddedInode().NewPersistentInode(
|
||||
ctx,
|
||||
npubdir,
|
||||
fs.StableAttr{Mode: syscall.S_IFDIR, Ino: hexToUint64(pointer.PublicKey)},
|
||||
)
|
||||
|
||||
relays := sys.FetchOutboxRelays(ctx, pointer.PublicKey, 2)
|
||||
|
||||
h.AddChild("pubkey", h.NewPersistentInode(
|
||||
ctx,
|
||||
&fs.MemRegularFile{Data: []byte(pointer.PublicKey + "\n"), Attr: fuse.Attr{Mode: 0444}},
|
||||
fs.StableAttr{},
|
||||
), true)
|
||||
|
||||
h.AddChild(
|
||||
"metadata.json",
|
||||
h.NewPersistentInode(
|
||||
ctx,
|
||||
&AsyncFile{
|
||||
ctx: ctx,
|
||||
load: func() ([]byte, nostr.Timestamp) {
|
||||
pm := sys.FetchProfileMetadata(ctx, pointer.PublicKey)
|
||||
jsonb, _ := sonic.ConfigFastest.MarshalIndent(pm, "", " ")
|
||||
var ts nostr.Timestamp
|
||||
if pm.Event != nil {
|
||||
ts = pm.Event.CreatedAt
|
||||
}
|
||||
return jsonb, ts
|
||||
},
|
||||
},
|
||||
fs.StableAttr{},
|
||||
),
|
||||
true,
|
||||
)
|
||||
|
||||
h.AddChild(
|
||||
"notes",
|
||||
h.NewPersistentInode(
|
||||
ctx,
|
||||
&ViewDir{
|
||||
ctx: ctx,
|
||||
sys: sys,
|
||||
wd: wd,
|
||||
filter: nostr.Filter{
|
||||
Kinds: []int{1},
|
||||
Authors: []string{pointer.PublicKey},
|
||||
},
|
||||
paginate: true,
|
||||
relays: relays,
|
||||
create: func(ctx context.Context, n *ViewDir, event *nostr.Event) (string, *fs.Inode) {
|
||||
return event.ID, CreateEventDir(ctx, n, n.wd, event)
|
||||
},
|
||||
},
|
||||
fs.StableAttr{Mode: syscall.S_IFDIR},
|
||||
),
|
||||
true,
|
||||
)
|
||||
|
||||
h.AddChild(
|
||||
"comments",
|
||||
h.NewPersistentInode(
|
||||
ctx,
|
||||
&ViewDir{
|
||||
ctx: ctx,
|
||||
sys: sys,
|
||||
wd: wd,
|
||||
filter: nostr.Filter{
|
||||
Kinds: []int{1111},
|
||||
Authors: []string{pointer.PublicKey},
|
||||
},
|
||||
paginate: true,
|
||||
relays: relays,
|
||||
create: func(ctx context.Context, n *ViewDir, event *nostr.Event) (string, *fs.Inode) {
|
||||
return event.ID, CreateEventDir(ctx, n, n.wd, event)
|
||||
},
|
||||
},
|
||||
fs.StableAttr{Mode: syscall.S_IFDIR},
|
||||
),
|
||||
true,
|
||||
)
|
||||
|
||||
h.AddChild(
|
||||
"pictures",
|
||||
h.NewPersistentInode(
|
||||
ctx,
|
||||
&ViewDir{
|
||||
ctx: ctx,
|
||||
sys: sys,
|
||||
wd: wd,
|
||||
filter: nostr.Filter{
|
||||
Kinds: []int{20},
|
||||
Authors: []string{pointer.PublicKey},
|
||||
},
|
||||
paginate: true,
|
||||
relays: relays,
|
||||
create: func(ctx context.Context, n *ViewDir, event *nostr.Event) (string, *fs.Inode) {
|
||||
return event.ID, CreateEventDir(ctx, n, n.wd, event)
|
||||
},
|
||||
},
|
||||
fs.StableAttr{Mode: syscall.S_IFDIR},
|
||||
),
|
||||
true,
|
||||
)
|
||||
|
||||
h.AddChild(
|
||||
"videos",
|
||||
h.NewPersistentInode(
|
||||
ctx,
|
||||
&ViewDir{
|
||||
ctx: ctx,
|
||||
sys: sys,
|
||||
wd: wd,
|
||||
filter: nostr.Filter{
|
||||
Kinds: []int{21, 22},
|
||||
Authors: []string{pointer.PublicKey},
|
||||
},
|
||||
paginate: true,
|
||||
relays: relays,
|
||||
create: func(ctx context.Context, n *ViewDir, event *nostr.Event) (string, *fs.Inode) {
|
||||
return event.ID, CreateEventDir(ctx, n, n.wd, event)
|
||||
},
|
||||
},
|
||||
fs.StableAttr{Mode: syscall.S_IFDIR},
|
||||
),
|
||||
true,
|
||||
)
|
||||
|
||||
h.AddChild(
|
||||
"highlights",
|
||||
h.NewPersistentInode(
|
||||
ctx,
|
||||
&ViewDir{
|
||||
ctx: ctx,
|
||||
sys: sys,
|
||||
wd: wd,
|
||||
filter: nostr.Filter{
|
||||
Kinds: []int{9802},
|
||||
Authors: []string{pointer.PublicKey},
|
||||
},
|
||||
paginate: true,
|
||||
relays: relays,
|
||||
create: func(ctx context.Context, n *ViewDir, event *nostr.Event) (string, *fs.Inode) {
|
||||
return event.ID, CreateEventDir(ctx, n, n.wd, event)
|
||||
},
|
||||
},
|
||||
fs.StableAttr{Mode: syscall.S_IFDIR},
|
||||
),
|
||||
true,
|
||||
)
|
||||
|
||||
h.AddChild(
|
||||
"articles",
|
||||
h.NewPersistentInode(
|
||||
ctx,
|
||||
&ViewDir{
|
||||
ctx: ctx,
|
||||
sys: sys,
|
||||
wd: wd,
|
||||
filter: nostr.Filter{
|
||||
Kinds: []int{30023},
|
||||
Authors: []string{pointer.PublicKey},
|
||||
},
|
||||
paginate: false,
|
||||
relays: relays,
|
||||
create: func(ctx context.Context, n *ViewDir, event *nostr.Event) (string, *fs.Inode) {
|
||||
return event.Tags.GetD(), CreateEntityDir(ctx, n, n.wd, ".md", event)
|
||||
},
|
||||
},
|
||||
fs.StableAttr{Mode: syscall.S_IFDIR},
|
||||
),
|
||||
true,
|
||||
)
|
||||
|
||||
h.AddChild(
|
||||
"wiki",
|
||||
h.NewPersistentInode(
|
||||
ctx,
|
||||
&ViewDir{
|
||||
ctx: ctx,
|
||||
sys: sys,
|
||||
wd: wd,
|
||||
filter: nostr.Filter{
|
||||
Kinds: []int{30818},
|
||||
Authors: []string{pointer.PublicKey},
|
||||
},
|
||||
paginate: false,
|
||||
relays: relays,
|
||||
create: func(ctx context.Context, n *ViewDir, event *nostr.Event) (string, *fs.Inode) {
|
||||
return event.Tags.GetD(), CreateEntityDir(ctx, n, n.wd, ".adoc", event)
|
||||
},
|
||||
},
|
||||
fs.StableAttr{Mode: syscall.S_IFDIR},
|
||||
),
|
||||
true,
|
||||
)
|
||||
|
||||
return h
|
||||
}
|
||||
110
nostrfs/root.go
Normal file
110
nostrfs/root.go
Normal file
@@ -0,0 +1,110 @@
|
||||
package nostrfs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/hanwen/go-fuse/v2/fs"
|
||||
"github.com/hanwen/go-fuse/v2/fuse"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/nbd-wtf/go-nostr/nip05"
|
||||
"github.com/nbd-wtf/go-nostr/nip19"
|
||||
"github.com/nbd-wtf/go-nostr/sdk"
|
||||
)
|
||||
|
||||
type NostrRoot struct {
|
||||
fs.Inode
|
||||
|
||||
ctx context.Context
|
||||
wd string
|
||||
sys *sdk.System
|
||||
rootPubKey string
|
||||
signer nostr.Signer
|
||||
}
|
||||
|
||||
var _ = (fs.NodeOnAdder)((*NostrRoot)(nil))
|
||||
|
||||
func NewNostrRoot(ctx context.Context, sys *sdk.System, user nostr.User, mountpoint string) *NostrRoot {
|
||||
pubkey, _ := user.GetPublicKey(ctx)
|
||||
signer, _ := user.(nostr.Signer)
|
||||
abs, _ := filepath.Abs(mountpoint)
|
||||
|
||||
return &NostrRoot{
|
||||
ctx: ctx,
|
||||
sys: sys,
|
||||
rootPubKey: pubkey,
|
||||
signer: signer,
|
||||
wd: abs,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *NostrRoot) OnAdd(context.Context) {
|
||||
if r.rootPubKey == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// add our contacts
|
||||
fl := r.sys.FetchFollowList(r.ctx, r.rootPubKey)
|
||||
for _, f := range fl.Items {
|
||||
pointer := nostr.ProfilePointer{PublicKey: f.Pubkey, Relays: []string{f.Relay}}
|
||||
npub, _ := nip19.EncodePublicKey(f.Pubkey)
|
||||
r.AddChild(
|
||||
npub,
|
||||
CreateNpubDir(r.ctx, r.sys, r, r.wd, pointer),
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
// add ourselves
|
||||
npub, _ := nip19.EncodePublicKey(r.rootPubKey)
|
||||
if r.GetChild(npub) == nil {
|
||||
pointer := nostr.ProfilePointer{PublicKey: r.rootPubKey}
|
||||
r.AddChild(
|
||||
npub,
|
||||
CreateNpubDir(r.ctx, r.sys, r, r.wd, pointer),
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
// add a link to ourselves
|
||||
r.AddChild("@me", r.NewPersistentInode(
|
||||
r.ctx,
|
||||
&fs.MemSymlink{Data: []byte(r.wd + "/" + npub)},
|
||||
fs.StableAttr{Mode: syscall.S_IFLNK},
|
||||
), true)
|
||||
}
|
||||
|
||||
func (r *NostrRoot) Lookup(ctx context.Context, name string, out *fuse.EntryOut) (*fs.Inode, syscall.Errno) {
|
||||
out.SetEntryTimeout(time.Minute * 5)
|
||||
|
||||
child := r.GetChild(name)
|
||||
if child != nil {
|
||||
return child, fs.OK
|
||||
}
|
||||
|
||||
if pp, err := nip05.QueryIdentifier(ctx, name); err == nil {
|
||||
npubdir := CreateNpubDir(ctx, r.sys, r, r.wd, *pp)
|
||||
return npubdir, fs.OK
|
||||
}
|
||||
|
||||
pointer, err := nip19.ToPointer(name)
|
||||
if err != nil {
|
||||
return nil, syscall.ENOENT
|
||||
}
|
||||
|
||||
switch p := pointer.(type) {
|
||||
case nostr.ProfilePointer:
|
||||
npubdir := CreateNpubDir(ctx, r.sys, r, r.wd, p)
|
||||
return npubdir, fs.OK
|
||||
case nostr.EventPointer:
|
||||
eventdir, err := FetchAndCreateEventDir(ctx, r, r.wd, r.sys, p)
|
||||
if err != nil {
|
||||
return nil, syscall.ENOENT
|
||||
}
|
||||
return eventdir, fs.OK
|
||||
default:
|
||||
return nil, syscall.ENOENT
|
||||
}
|
||||
}
|
||||
82
nostrfs/viewdir.go
Normal file
82
nostrfs/viewdir.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package nostrfs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
|
||||
"github.com/hanwen/go-fuse/v2/fs"
|
||||
"github.com/hanwen/go-fuse/v2/fuse"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
sdk "github.com/nbd-wtf/go-nostr/sdk"
|
||||
)
|
||||
|
||||
type ViewDir struct {
|
||||
fs.Inode
|
||||
ctx context.Context
|
||||
sys *sdk.System
|
||||
wd string
|
||||
fetched atomic.Bool
|
||||
filter nostr.Filter
|
||||
paginate bool
|
||||
relays []string
|
||||
create func(context.Context, *ViewDir, *nostr.Event) (string, *fs.Inode)
|
||||
}
|
||||
|
||||
var (
|
||||
_ = (fs.NodeOpendirer)((*ViewDir)(nil))
|
||||
_ = (fs.NodeGetattrer)((*ViewDir)(nil))
|
||||
)
|
||||
|
||||
func (n *ViewDir) Getattr(ctx context.Context, f fs.FileHandle, out *fuse.AttrOut) syscall.Errno {
|
||||
now := nostr.Now()
|
||||
if n.filter.Until != nil {
|
||||
now = *n.filter.Until
|
||||
}
|
||||
aMonthAgo := now - 30*24*60*60
|
||||
out.Mtime = uint64(aMonthAgo)
|
||||
|
||||
return fs.OK
|
||||
}
|
||||
|
||||
func (n *ViewDir) Opendir(ctx context.Context) syscall.Errno {
|
||||
if n.fetched.CompareAndSwap(true, true) {
|
||||
return fs.OK
|
||||
}
|
||||
|
||||
if n.paginate {
|
||||
now := nostr.Now()
|
||||
if n.filter.Until != nil {
|
||||
now = *n.filter.Until
|
||||
}
|
||||
aMonthAgo := now - 30*24*60*60
|
||||
n.filter.Since = &aMonthAgo
|
||||
|
||||
for ie := range n.sys.Pool.FetchMany(ctx, n.relays, n.filter, nostr.WithLabel("nakfs")) {
|
||||
basename, inode := n.create(ctx, n, ie.Event)
|
||||
n.AddChild(basename, inode, true)
|
||||
}
|
||||
|
||||
filter := n.filter
|
||||
filter.Until = &aMonthAgo
|
||||
|
||||
n.AddChild("@previous", n.NewPersistentInode(
|
||||
ctx,
|
||||
&ViewDir{
|
||||
ctx: n.ctx,
|
||||
sys: n.sys,
|
||||
filter: filter,
|
||||
wd: n.wd,
|
||||
relays: n.relays,
|
||||
},
|
||||
fs.StableAttr{Mode: syscall.S_IFDIR},
|
||||
), true)
|
||||
} else {
|
||||
for ie := range n.sys.Pool.FetchMany(ctx, n.relays, n.filter, nostr.WithLabel("nakfs")) {
|
||||
basename, inode := n.create(ctx, n, ie.Event)
|
||||
n.AddChild(basename, inode, true)
|
||||
}
|
||||
}
|
||||
|
||||
return fs.OK
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/fiatjaf/cli/v3"
|
||||
"github.com/urfave/cli/v3"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
)
|
||||
|
||||
|
||||
76
paginate.go
76
paginate.go
@@ -1,76 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
)
|
||||
|
||||
func paginateWithParams(
|
||||
interval time.Duration,
|
||||
globalLimit uint64,
|
||||
) func(ctx context.Context, urls []string, filters nostr.Filters, opts ...nostr.SubscriptionOption) chan nostr.RelayEvent {
|
||||
return func(ctx context.Context, urls []string, filters nostr.Filters, opts ...nostr.SubscriptionOption) chan nostr.RelayEvent {
|
||||
// filters will always be just one
|
||||
filter := filters[0]
|
||||
|
||||
nextUntil := nostr.Now()
|
||||
if filter.Until != nil {
|
||||
nextUntil = *filter.Until
|
||||
}
|
||||
|
||||
if globalLimit == 0 {
|
||||
globalLimit = uint64(filter.Limit)
|
||||
if globalLimit == 0 && !filter.LimitZero {
|
||||
globalLimit = math.MaxUint64
|
||||
}
|
||||
}
|
||||
var globalCount uint64 = 0
|
||||
globalCh := make(chan nostr.RelayEvent)
|
||||
|
||||
repeatedCache := make([]string, 0, 300)
|
||||
nextRepeatedCache := make([]string, 0, 300)
|
||||
|
||||
go func() {
|
||||
defer close(globalCh)
|
||||
|
||||
for {
|
||||
filter.Until = &nextUntil
|
||||
time.Sleep(interval)
|
||||
|
||||
keepGoing := false
|
||||
for evt := range sys.Pool.SubManyEose(ctx, urls, nostr.Filters{filter}, opts...) {
|
||||
if slices.Contains(repeatedCache, evt.ID) {
|
||||
continue
|
||||
}
|
||||
|
||||
keepGoing = true // if we get one that isn't repeated, then keep trying to get more
|
||||
nextRepeatedCache = append(nextRepeatedCache, evt.ID)
|
||||
|
||||
globalCh <- evt
|
||||
|
||||
globalCount++
|
||||
if globalCount >= globalLimit {
|
||||
return
|
||||
}
|
||||
|
||||
if evt.CreatedAt < *filter.Until {
|
||||
nextUntil = evt.CreatedAt
|
||||
}
|
||||
}
|
||||
|
||||
if !keepGoing {
|
||||
return
|
||||
}
|
||||
|
||||
repeatedCache = nextRepeatedCache
|
||||
nextRepeatedCache = nextRepeatedCache[:0]
|
||||
}
|
||||
}()
|
||||
|
||||
return globalCh
|
||||
}
|
||||
}
|
||||
14
relay.go
14
relay.go
@@ -10,10 +10,10 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/fiatjaf/cli/v3"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/nbd-wtf/go-nostr/nip11"
|
||||
"github.com/nbd-wtf/go-nostr/nip86"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
var relay = &cli.Command{
|
||||
@@ -45,9 +45,7 @@ var relay = &cli.Command{
|
||||
return nil
|
||||
},
|
||||
Commands: (func() []*cli.Command {
|
||||
commands := make([]*cli.Command, 0, 12)
|
||||
|
||||
for _, def := range []struct {
|
||||
methods := []struct {
|
||||
method string
|
||||
args []string
|
||||
}{
|
||||
@@ -69,7 +67,10 @@ var relay = &cli.Command{
|
||||
{"blockip", []string{"ip", "reason"}},
|
||||
{"unblockip", []string{"ip", "reason"}},
|
||||
{"listblockedips", nil},
|
||||
} {
|
||||
}
|
||||
|
||||
commands := make([]*cli.Command, 0, len(methods))
|
||||
for _, def := range methods {
|
||||
def := def
|
||||
|
||||
flags := make([]cli.Flag, len(def.args), len(def.args)+4)
|
||||
@@ -84,6 +85,8 @@ var relay = &cli.Command{
|
||||
Usage: fmt.Sprintf(`the "%s" relay management RPC call`, def.method),
|
||||
Description: fmt.Sprintf(
|
||||
`the "%s" management RPC call, see https://nips.nostr.com/86 for more information`, def.method),
|
||||
Flags: flags,
|
||||
DisableSliceFlagSeparator: true,
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
params := make([]any, len(def.args))
|
||||
for i, argName := range def.args {
|
||||
@@ -173,7 +176,6 @@ var relay = &cli.Command{
|
||||
|
||||
return nil
|
||||
},
|
||||
Flags: flags,
|
||||
}
|
||||
|
||||
commands = append(commands, cmd)
|
||||
|
||||
54
req.go
54
req.go
@@ -6,9 +6,10 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/fiatjaf/cli/v3"
|
||||
"github.com/mailru/easyjson"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/nbd-wtf/go-nostr/nip77"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -32,6 +33,10 @@ example:
|
||||
DisableSliceFlagSeparator: true,
|
||||
Flags: append(defaultKeyFlags,
|
||||
append(reqFilterFlags,
|
||||
&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",
|
||||
@@ -75,7 +80,16 @@ example:
|
||||
relayUrls,
|
||||
c.Bool("force-pre-auth"),
|
||||
nostr.WithAuthHandler(
|
||||
func(ctx context.Context, authEvent nostr.RelayEvent) error {
|
||||
func(ctx context.Context, authEvent nostr.RelayEvent) (err error) {
|
||||
defer func() {
|
||||
if err != nil {
|
||||
log("auth to %s failed: %s\n",
|
||||
(*authEvent.Tags.GetFirst([]string{"relay", ""}))[1],
|
||||
err,
|
||||
)
|
||||
}
|
||||
}()
|
||||
|
||||
if !c.Bool("auth") && !c.Bool("force-pre-auth") {
|
||||
return fmt.Errorf("auth not authorized")
|
||||
}
|
||||
@@ -107,7 +121,7 @@ example:
|
||||
}()
|
||||
}
|
||||
|
||||
for stdinFilter := range getStdinLinesOrBlank() {
|
||||
for stdinFilter := range getJsonsOrBlank() {
|
||||
filter := nostr.Filter{}
|
||||
if stdinFilter != "" {
|
||||
if err := easyjson.Unmarshal([]byte(stdinFilter), &filter); err != nil {
|
||||
@@ -121,15 +135,33 @@ example:
|
||||
}
|
||||
|
||||
if len(relayUrls) > 0 {
|
||||
fn := sys.Pool.SubManyEose
|
||||
if c.Bool("paginate") {
|
||||
fn = paginateWithParams(c.Duration("paginate-interval"), c.Uint("paginate-global-limit"))
|
||||
} else if c.Bool("stream") {
|
||||
fn = sys.Pool.SubMany
|
||||
}
|
||||
if c.Bool("ids-only") {
|
||||
seen := make(map[string]struct{}, max(500, filter.Limit))
|
||||
for _, url := range relayUrls {
|
||||
ch, err := nip77.FetchIDsOnly(ctx, url, filter)
|
||||
if err != nil {
|
||||
log("negentropy call to %s failed: %s", url, err)
|
||||
continue
|
||||
}
|
||||
for id := range ch {
|
||||
if _, ok := seen[id]; ok {
|
||||
continue
|
||||
}
|
||||
seen[id] = struct{}{}
|
||||
stdout(id)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fn := sys.Pool.FetchMany
|
||||
if c.Bool("paginate") {
|
||||
fn = sys.Pool.PaginatorWithInterval(c.Duration("paginate-interval"))
|
||||
} else if c.Bool("stream") {
|
||||
fn = sys.Pool.SubscribeMany
|
||||
}
|
||||
|
||||
for ie := range fn(ctx, relayUrls, nostr.Filters{filter}) {
|
||||
stdout(ie.Event)
|
||||
for ie := range fn(ctx, relayUrls, filter) {
|
||||
stdout(ie.Event)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// no relays given, will just print the filter
|
||||
|
||||
8
serve.go
8
serve.go
@@ -10,10 +10,10 @@ import (
|
||||
|
||||
"github.com/bep/debounce"
|
||||
"github.com/fatih/color"
|
||||
"github.com/fiatjaf/cli/v3"
|
||||
"github.com/fiatjaf/eventstore/slicestore"
|
||||
"github.com/fiatjaf/khatru"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
var serve = &cli.Command{
|
||||
@@ -65,6 +65,12 @@ var serve = &cli.Command{
|
||||
}
|
||||
|
||||
rl := khatru.NewRelay()
|
||||
|
||||
rl.Info.Name = "nak serve"
|
||||
rl.Info.Description = "a local relay for testing, debugging and development."
|
||||
rl.Info.Software = "https://github.com/fiatjaf/nak"
|
||||
rl.Info.Version = version
|
||||
|
||||
rl.QueryEvents = append(rl.QueryEvents, db.QueryEvents)
|
||||
rl.CountEvents = append(rl.CountEvents, db.CountEvents)
|
||||
rl.DeleteEvent = append(rl.DeleteEvent, db.DeleteEvent)
|
||||
|
||||
@@ -3,7 +3,7 @@ package main
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/fiatjaf/cli/v3"
|
||||
"github.com/urfave/cli/v3"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
)
|
||||
|
||||
|
||||
11
wallet.go
11
wallet.go
@@ -7,10 +7,11 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/fiatjaf/cli/v3"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/nbd-wtf/go-nostr/nip60"
|
||||
"github.com/nbd-wtf/go-nostr/nip61"
|
||||
"github.com/nbd-wtf/go-nostr/sdk"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func prepareWallet(ctx context.Context, c *cli.Command) (*nip60.Wallet, func(), error) {
|
||||
@@ -316,8 +317,8 @@ var wallet = &cli.Command{
|
||||
},
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
args := c.Args().Slice()
|
||||
if len(args) >= 2 {
|
||||
return fmt.Errorf("must be called as `nak wallet send <amount> <target>...")
|
||||
if len(args) < 2 {
|
||||
return fmt.Errorf("must be called as `nak wallet nutzap <amount> <target>...")
|
||||
}
|
||||
|
||||
w, closew, err := prepareWallet(ctx, c)
|
||||
@@ -332,7 +333,9 @@ var wallet = &cli.Command{
|
||||
var eventId string
|
||||
|
||||
if strings.HasPrefix(target, "nevent1") {
|
||||
evt, _, err = sys.FetchSpecificEventFromInput(ctx, target, false)
|
||||
evt, _, err = sys.FetchSpecificEventFromInput(ctx, target, sdk.FetchSpecificEventParameters{
|
||||
WithRelays: false,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user