Compare commits

...

16 Commits

Author SHA1 Message Date
fiatjaf
6f48c29d0f fix go-nostr dependency. 2025-04-03 21:31:12 -03:00
fiatjaf
703c186958 much more colors everywhere and everything is prettier. 2025-04-03 14:50:25 -03:00
fiatjaf
7ae2e686cb more colors. 2025-04-03 11:57:18 -03:00
fiatjaf
9547711e8d nice dynamic UI when connecting to relays, and go much faster concurrently. 2025-04-03 11:42:33 -03:00
fiatjaf
50119e21e6 fix nip73.ExternalPointer reference 2025-04-02 22:38:12 -03:00
fiatjaf
33f4272dd0 update go-nostr to maybe fix nip-60 wallets? 2025-04-02 22:37:59 -03:00
fiatjaf
7b6f387aad tags GetFirst() => Find() 2025-03-29 17:12:31 -03:00
fiatjaf
b1a03800e6 add fake fs command that doesn't work when compiling for windows but at least compiles. 2025-03-19 15:12:39 -03:00
fiatjaf
db5dafb58a fix arm64 builds by removing base64x dependencies. 2025-03-19 15:05:27 -03:00
fiatjaf
4b15cdf625 fs: publishing new notes by writing to ./notes/new 2025-03-15 00:34:13 -03:00
fiatjaf
4b8c067e00 fs: creating articles (and presumably wikis); fixes and improvements to editing articles. 2025-03-13 01:13:34 -03:00
fiatjaf
931da4b0ae fs: editable articles and wiki. 2025-03-12 08:03:10 -03:00
fiatjaf
c87371208e fs: pass NostrRoot everywhere with a signer only if it can actually sign. 2025-03-11 13:18:33 -03:00
fiatjaf
bfe1e6ca94 fs: rename pictures -> photos. 2025-03-11 12:38:35 -03:00
fiatjaf
602e03a9a1 fs: do not paginate videos and highlights (should make this dynamic in the future). 2025-03-11 12:38:32 -03:00
fiatjaf
fe1f50f798 fs: logging and proper (?) handling of context passing (basically now we ignore the context given to us by the fuse library because they're weird). 2025-03-11 12:37:27 -03:00
20 changed files with 1389 additions and 558 deletions

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
nak nak
mnt mnt
nak.exe

View File

@@ -11,10 +11,10 @@ import (
"time" "time"
"github.com/fatih/color" "github.com/fatih/color"
"github.com/urfave/cli/v3"
"github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nip19" "github.com/nbd-wtf/go-nostr/nip19"
"github.com/nbd-wtf/go-nostr/nip46" "github.com/nbd-wtf/go-nostr/nip46"
"github.com/urfave/cli/v3"
) )
var bunker = &cli.Command{ var bunker = &cli.Command{
@@ -49,7 +49,7 @@ var bunker = &cli.Command{
qs := url.Values{} qs := url.Values{}
relayURLs := make([]string, 0, c.Args().Len()) relayURLs := make([]string, 0, c.Args().Len())
if relayUrls := c.Args().Slice(); len(relayUrls) > 0 { if relayUrls := c.Args().Slice(); len(relayUrls) > 0 {
relays := connectToAllRelays(ctx, relayUrls, false) relays := connectToAllRelays(ctx, c, relayUrls, nil)
if len(relays) == 0 { if len(relays) == 0 {
log("failed to connect to any of the given relays.\n") log("failed to connect to any of the given relays.\n")
os.Exit(3) os.Exit(3)

View File

@@ -6,10 +6,10 @@ import (
"os" "os"
"strings" "strings"
"github.com/urfave/cli/v3"
"github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nip45" "github.com/nbd-wtf/go-nostr/nip45"
"github.com/nbd-wtf/go-nostr/nip45/hyperloglog" "github.com/nbd-wtf/go-nostr/nip45/hyperloglog"
"github.com/urfave/cli/v3"
) )
var count = &cli.Command{ var count = &cli.Command{
@@ -70,10 +70,7 @@ var count = &cli.Command{
biggerUrlSize := 0 biggerUrlSize := 0
relayUrls := c.Args().Slice() relayUrls := c.Args().Slice()
if len(relayUrls) > 0 { if len(relayUrls) > 0 {
relays := connectToAllRelays(ctx, relays := connectToAllRelays(ctx, c, relayUrls, nil)
relayUrls,
false,
)
if len(relays) == 0 { if len(relays) == 0 {
log("failed to connect to any of the given relays.\n") log("failed to connect to any of the given relays.\n")
os.Exit(3) os.Exit(3)

12
dvm.go
View File

@@ -7,7 +7,6 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/fatih/color"
"github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nip90" "github.com/nbd-wtf/go-nostr/nip90"
"github.com/urfave/cli/v3" "github.com/urfave/cli/v3"
@@ -60,7 +59,7 @@ var dvm = &cli.Command{
Flags: flags, Flags: flags,
Action: func(ctx context.Context, c *cli.Command) error { Action: func(ctx context.Context, c *cli.Command) error {
relayUrls := c.StringSlice("relay") relayUrls := c.StringSlice("relay")
relays := connectToAllRelays(ctx, relayUrls, false) relays := connectToAllRelays(ctx, c, relayUrls, nil)
if len(relays) == 0 { if len(relays) == 0 {
log("failed to connect to any of the given relays.\n") log("failed to connect to any of the given relays.\n")
os.Exit(3) os.Exit(3)
@@ -103,10 +102,7 @@ var dvm = &cli.Command{
log("- publishing job request... ") log("- publishing job request... ")
first := true first := true
for res := range sys.Pool.PublishMany(ctx, relayUrls, evt) { for res := range sys.Pool.PublishMany(ctx, relayUrls, evt) {
cleanUrl, ok := strings.CutPrefix(res.RelayURL, "wss://") cleanUrl, _ := strings.CutPrefix(res.RelayURL, "wss://")
if !ok {
cleanUrl = res.RelayURL
}
if !first { if !first {
log(", ") log(", ")
@@ -114,9 +110,9 @@ var dvm = &cli.Command{
first = false first = false
if res.Error != nil { if res.Error != nil {
log("%s: %s", color.RedString(cleanUrl), res.Error) log("%s: %s", colors.errorf(cleanUrl), res.Error)
} else { } else {
log("%s: ok", color.GreenString(cleanUrl)) log("%s: ok", colors.successf(cleanUrl))
} }
} }

View File

@@ -8,6 +8,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/fatih/color"
"github.com/mailru/easyjson" "github.com/mailru/easyjson"
"github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nip13" "github.com/nbd-wtf/go-nostr/nip13"
@@ -133,8 +134,17 @@ example:
Action: func(ctx context.Context, c *cli.Command) error { Action: func(ctx context.Context, c *cli.Command) error {
// try to connect to the relays here // try to connect to the relays here
var relays []*nostr.Relay var relays []*nostr.Relay
// these are defaults, they will be replaced if we use the magic dynamic thing
logthis := func(relayUrl string, s string, args ...any) { log(s, args...) }
colorizethis := func(relayUrl string, colorize func(string, ...any) string) {}
if relayUrls := c.Args().Slice(); len(relayUrls) > 0 { if relayUrls := c.Args().Slice(); len(relayUrls) > 0 {
relays = connectToAllRelays(ctx, relayUrls, false) relays = connectToAllRelays(ctx, c, relayUrls, nil,
nostr.WithAuthHandler(func(ctx context.Context, authEvent nostr.RelayEvent) error {
return authSigner(ctx, c, func(s string, args ...any) {}, authEvent)
}),
)
if len(relays) == 0 { if len(relays) == 0 {
log("failed to connect to any of the given relays.\n") log("failed to connect to any of the given relays.\n")
os.Exit(3) os.Exit(3)
@@ -209,13 +219,19 @@ example:
} }
for _, etag := range c.StringSlice("e") { for _, etag := range c.StringSlice("e") {
tags = tags.AppendUnique([]string{"e", etag}) if tags.FindWithValue("e", etag) == nil {
tags = append(tags, nostr.Tag{"e", etag})
}
} }
for _, ptag := range c.StringSlice("p") { for _, ptag := range c.StringSlice("p") {
tags = tags.AppendUnique([]string{"p", ptag}) if tags.FindWithValue("p", ptag) == nil {
tags = append(tags, nostr.Tag{"p", ptag})
}
} }
for _, dtag := range c.StringSlice("d") { for _, dtag := range c.StringSlice("d") {
tags = tags.AppendUnique([]string{"d", dtag}) if tags.FindWithValue("d", dtag) == nil {
tags = append(tags, nostr.Tag{"d", dtag})
}
} }
if len(tags) > 0 { if len(tags) > 0 {
for _, tag := range tags { for _, tag := range tags {
@@ -295,9 +311,73 @@ example:
successRelays := make([]string, 0, len(relays)) successRelays := make([]string, 0, len(relays))
if len(relays) > 0 { if len(relays) > 0 {
os.Stdout.Sync() os.Stdout.Sync()
if supportsDynamicMultilineMagic() {
// overcomplicated multiline rendering magic
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
urls := make([]string, len(relays))
lines := make([][][]byte, len(urls))
flush := func() {
for _, line := range lines {
for _, part := range line {
os.Stderr.Write(part)
}
os.Stderr.Write([]byte{'\n'})
}
}
render := func() {
clearLines(len(lines))
flush()
}
flush()
logthis = func(relayUrl, s string, args ...any) {
idx := slices.Index(urls, relayUrl)
lines[idx] = append(lines[idx], []byte(fmt.Sprintf(s, args...)))
render()
}
colorizethis = func(relayUrl string, colorize func(string, ...any) string) {
cleanUrl, _ := strings.CutPrefix(relayUrl, "wss://")
idx := slices.Index(urls, relayUrl)
lines[idx][0] = []byte(fmt.Sprintf("publishing to %s... ", colorize(cleanUrl)))
render()
}
for i, relay := range relays {
urls[i] = relay.URL
lines[i] = make([][]byte, 1, 3)
colorizethis(relay.URL, color.CyanString)
}
render()
for res := range sys.Pool.PublishMany(ctx, urls, evt) {
if res.Error == nil {
colorizethis(res.RelayURL, colors.successf)
logthis(res.RelayURL, "success.")
successRelays = append(successRelays, res.RelayURL)
} else {
colorizethis(res.RelayURL, colors.errorf)
// in this case it's likely that the lowest-level error is the one that will be more helpful
low := unwrapAll(res.Error)
// hack for some messages such as from relay.westernbtc.com
msg := strings.ReplaceAll(low.Error(), evt.PubKey, "author")
// do not allow the message to overflow the term window
msg = clampMessage(msg, 20+len(res.RelayURL))
logthis(res.RelayURL, msg)
}
}
} else {
// normal dumb flow
for _, relay := range relays { for _, relay := range relays {
publish: publish:
log("publishing to %s... ", relay.URL) cleanUrl, _ := strings.CutPrefix(relay.URL, "wss://")
log("publishing to %s... ", color.CyanString(cleanUrl))
ctx, cancel := context.WithTimeout(ctx, 10*time.Second) ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel() defer cancel()
@@ -313,7 +393,8 @@ example:
if strings.HasPrefix(err.Error(), "msg: auth-required:") && kr != nil && doAuth { if strings.HasPrefix(err.Error(), "msg: auth-required:") && kr != nil && doAuth {
// if the relay is requesting auth and we can auth, let's do it // if the relay is requesting auth and we can auth, let's do it
pk, _ := kr.GetPublicKey(ctx) pk, _ := kr.GetPublicKey(ctx)
log("performing auth as %s... ", pk) npub, _ := nip19.EncodePublicKey(pk)
log("authenticating as %s... ", color.YellowString("%s…%s", npub[0:7], npub[58:]))
if err := relay.Auth(ctx, func(authEvent *nostr.Event) error { if err := relay.Auth(ctx, func(authEvent *nostr.Event) error {
return kr.SignEvent(ctx, authEvent) return kr.SignEvent(ctx, authEvent)
}); err == nil { }); err == nil {
@@ -326,6 +407,8 @@ example:
} }
log("failed: %s\n", err) log("failed: %s\n", err)
} }
}
if len(successRelays) > 0 && c.Bool("nevent") { if len(successRelays) > 0 && c.Bool("nevent") {
nevent, _ := nip19.EncodeEvent(evt.ID, successRelays, evt.PubKey) nevent, _ := nip19.EncodeEvent(evt.ID, successRelays, evt.PubKey)
log(nevent + "\n") log(nevent + "\n")

46
fs.go
View File

@@ -1,3 +1,5 @@
//go:build !windows
package main package main
import ( import (
@@ -22,7 +24,7 @@ var fsCmd = &cli.Command{
Usage: "mount a FUSE filesystem that exposes Nostr events as files.", Usage: "mount a FUSE filesystem that exposes Nostr events as files.",
Description: `(experimental)`, Description: `(experimental)`,
ArgsUsage: "<mountpoint>", ArgsUsage: "<mountpoint>",
Flags: []cli.Flag{ Flags: append(defaultKeyFlags,
&cli.StringFlag{ &cli.StringFlag{
Name: "pubkey", Name: "pubkey",
Usage: "public key from where to to prepopulate directories", Usage: "public key from where to to prepopulate directories",
@@ -33,7 +35,18 @@ var fsCmd = &cli.Command{
return fmt.Errorf("invalid public key '%s'", pk) return fmt.Errorf("invalid public key '%s'", pk)
}, },
}, },
&cli.DurationFlag{
Name: "auto-publish-notes",
Usage: "delay after which new notes will be auto-published, set to -1 to not publish.",
Value: time.Second * 30,
}, },
&cli.DurationFlag{
Name: "auto-publish-articles",
Usage: "delay after which edited articles will be auto-published.",
Value: time.Hour * 24 * 365 * 2,
DefaultText: "basically infinite",
},
),
DisableSliceFlagSeparator: true, DisableSliceFlagSeparator: true,
Action: func(ctx context.Context, c *cli.Command) error { Action: func(ctx context.Context, c *cli.Command) error {
mountpoint := c.Args().First() mountpoint := c.Args().First()
@@ -41,11 +54,37 @@ var fsCmd = &cli.Command{
return fmt.Errorf("must be called with a directory path to serve as the mountpoint as an argument") return fmt.Errorf("must be called with a directory path to serve as the mountpoint as an argument")
} }
var kr nostr.User
if signer, _, err := gatherKeyerFromArguments(ctx, c); err == nil {
kr = signer
} else {
kr = keyer.NewReadOnlyUser(c.String("pubkey"))
}
apnt := c.Duration("auto-publish-notes")
if apnt < 0 {
apnt = time.Hour * 24 * 365 * 3
}
apat := c.Duration("auto-publish-articles")
if apat < 0 {
apat = time.Hour * 24 * 365 * 3
}
root := nostrfs.NewNostrRoot( root := nostrfs.NewNostrRoot(
context.WithValue(
context.WithValue(
ctx, ctx,
"log", log,
),
"logverbose", logverbose,
),
sys, sys,
keyer.NewReadOnlyUser(c.String("pubkey")), kr,
mountpoint, mountpoint,
nostrfs.Options{
AutoPublishNotesTimeout: apnt,
AutoPublishArticlesTimeout: apat,
},
) )
// create the server // create the server
@@ -56,6 +95,7 @@ var fsCmd = &cli.Command{
Debug: isVerbose, Debug: isVerbose,
Name: "nak", Name: "nak",
FsName: "nak", FsName: "nak",
RememberInodes: true,
}, },
AttrTimeout: &timeout, AttrTimeout: &timeout,
EntryTimeout: &timeout, EntryTimeout: &timeout,
@@ -64,7 +104,7 @@ var fsCmd = &cli.Command{
if err != nil { if err != nil {
return fmt.Errorf("mount failed: %w", err) return fmt.Errorf("mount failed: %w", err)
} }
log("ok\n") log("ok.\n")
// setup signal handling for clean unmount // setup signal handling for clean unmount
ch := make(chan os.Signal, 1) ch := make(chan os.Signal, 1)

20
fs_windows.go Normal file
View File

@@ -0,0 +1,20 @@
//go:build windows
package main
import (
"context"
"fmt"
"github.com/urfave/cli/v3"
)
var fsCmd = &cli.Command{
Name: "fs",
Usage: "mount a FUSE filesystem that exposes Nostr events as files.",
Description: `doesn't work on Windows.`,
DisableSliceFlagSeparator: true,
Action: func(ctx context.Context, c *cli.Command) error {
return fmt.Errorf("this doesn't work on Windows.")
},
}

28
go.mod
View File

@@ -3,38 +3,39 @@ module github.com/fiatjaf/nak
go 1.24.1 go 1.24.1
require ( require (
fiatjaf.com/lib v0.3.1
github.com/bep/debounce v1.2.1 github.com/bep/debounce v1.2.1
github.com/btcsuite/btcd/btcec/v2 v2.3.4 github.com/btcsuite/btcd/btcec/v2 v2.3.4
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0
github.com/fatih/color v1.16.0 github.com/fatih/color v1.16.0
github.com/fiatjaf/eventstore v0.15.0 github.com/fiatjaf/eventstore v0.16.2
github.com/fiatjaf/khatru v0.16.0 github.com/fiatjaf/khatru v0.17.4
github.com/hanwen/go-fuse/v2 v2.7.2 github.com/hanwen/go-fuse/v2 v2.7.2
github.com/json-iterator/go v1.1.12 github.com/json-iterator/go v1.1.12
github.com/liamg/magic v0.0.1 github.com/liamg/magic v0.0.1
github.com/mailru/easyjson v0.9.0 github.com/mailru/easyjson v0.9.0
github.com/mark3labs/mcp-go v0.8.3 github.com/mark3labs/mcp-go v0.8.3
github.com/markusmobius/go-dateparser v1.2.3 github.com/markusmobius/go-dateparser v1.2.3
github.com/nbd-wtf/go-nostr v0.51.2 github.com/nbd-wtf/go-nostr v0.51.8
github.com/urfave/cli/v3 v3.0.0-beta1 github.com/urfave/cli/v3 v3.0.0-beta1
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac golang.org/x/exp v0.0.0-20250305212735-054e65f0b394
golang.org/x/term v0.30.0
) )
require ( require (
fiatjaf.com/lib v0.2.0 // indirect
github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3 // indirect github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3 // indirect
github.com/andybalholm/brotli v1.1.1 // indirect github.com/andybalholm/brotli v1.1.1 // indirect
github.com/btcsuite/btcd v0.24.2 // indirect github.com/btcsuite/btcd v0.24.2 // indirect
github.com/btcsuite/btcd/btcutil v1.1.5 // indirect github.com/btcsuite/btcd/btcutil v1.1.5 // indirect
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect
github.com/bytedance/sonic v1.13.1 // indirect github.com/bytedance/sonic v1.13.2 // indirect
github.com/bytedance/sonic/loader v0.2.4 // indirect github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/chzyer/logex v1.1.10 // indirect github.com/chzyer/logex v1.1.10 // indirect
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect github.com/cloudwego/base64x v0.1.5 // indirect
github.com/coder/websocket v1.8.12 // indirect github.com/coder/websocket v1.8.13 // indirect
github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect
github.com/dgraph-io/ristretto v1.0.0 // indirect github.com/dgraph-io/ristretto v1.0.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
@@ -43,7 +44,6 @@ require (
github.com/fasthttp/websocket v1.5.12 // indirect github.com/fasthttp/websocket v1.5.12 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/graph-gophers/dataloader/v7 v7.1.0 // indirect
github.com/hablullah/go-hijri v1.0.2 // indirect github.com/hablullah/go-hijri v1.0.2 // indirect
github.com/hablullah/go-juliandays v1.0.0 // indirect github.com/hablullah/go-juliandays v1.0.0 // indirect
github.com/jalaali/go-jalaali v0.0.0-20210801064154-80525e88d958 // indirect github.com/jalaali/go-jalaali v0.0.0-20210801064154-80525e88d958 // indirect
@@ -57,7 +57,7 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/puzpuzpuz/xsync/v3 v3.5.0 // indirect github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
github.com/rs/cors v1.11.1 // indirect github.com/rs/cors v1.11.1 // indirect
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 // indirect github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 // indirect
github.com/tetratelabs/wazero v1.8.0 // indirect github.com/tetratelabs/wazero v1.8.0 // indirect
@@ -66,12 +66,12 @@ require (
github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.58.0 // indirect github.com/valyala/fasthttp v1.59.0 // indirect
github.com/wasilibs/go-re2 v1.3.0 // indirect github.com/wasilibs/go-re2 v1.3.0 // indirect
github.com/x448/float16 v0.8.4 // indirect github.com/x448/float16 v0.8.4 // indirect
golang.org/x/arch v0.15.0 // indirect golang.org/x/arch v0.15.0 // indirect
golang.org/x/crypto v0.32.0 // indirect golang.org/x/crypto v0.36.0 // indirect
golang.org/x/net v0.34.0 // indirect golang.org/x/net v0.37.0 // indirect
golang.org/x/sys v0.31.0 // indirect golang.org/x/sys v0.31.0 // indirect
golang.org/x/text v0.21.0 // indirect golang.org/x/text v0.23.0 // indirect
) )

56
go.sum
View File

@@ -1,5 +1,5 @@
fiatjaf.com/lib v0.2.0 h1:TgIJESbbND6GjOgGHxF5jsO6EMjuAxIzZHPo5DXYexs= fiatjaf.com/lib v0.3.1 h1:/oFQwNtFRfV+ukmOCxfBEAuayoLwXp4wu2/fz5iHpwA=
fiatjaf.com/lib v0.2.0/go.mod h1:Ycqq3+mJ9jAWu7XjbQI1cVr+OFgnHn79dQR5oTII47g= fiatjaf.com/lib v0.3.1/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 h1:ClzzXMDDuUbWfNNZqGeYq4PnYOlwlOVIvSyNaIy0ykg=
github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3/go.mod h1:we0YA5CsBbH5+/NUzC/AlMmxaDtWlXeNsqrwXjTzmzA= 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/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
@@ -33,8 +33,8 @@ 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/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/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/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.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=
github.com/bytedance/sonic v1.13.1/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= 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 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
@@ -49,8 +49,8 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= 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/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.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE=
github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -59,8 +59,8 @@ github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn
github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8= github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8=
github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218=
github.com/dgraph-io/ristretto v1.0.0 h1:SYG07bONKMlFDUYu5pEu3DGAh8c2OFNzKm6G9J4Si84= github.com/dgraph-io/ristretto v1.0.0 h1:SYG07bONKMlFDUYu5pEu3DGAh8c2OFNzKm6G9J4Si84=
github.com/dgraph-io/ristretto v1.0.0/go.mod h1:jTi2FiYEhQ1NsMmA7DeBykizjOuY88NhKBkepyu1jPc= github.com/dgraph-io/ristretto v1.0.0/go.mod h1:jTi2FiYEhQ1NsMmA7DeBykizjOuY88NhKBkepyu1jPc=
@@ -77,10 +77,10 @@ github.com/fasthttp/websocket v1.5.12 h1:e4RGPpWW2HTbL3zV0Y/t7g0ub294LkiuXXUuTOU
github.com/fasthttp/websocket v1.5.12/go.mod h1:I+liyL7/4moHojiOgUOIKEWm9EIxHqxZChS+aMFltyg= 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 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/fiatjaf/eventstore v0.15.0 h1:5UXe0+vIb30/cYcOWipks8nR3g+X8W224TFy5yPzivk= github.com/fiatjaf/eventstore v0.16.2 h1:h4rHwSwPcqAKqWUsAbYWUhDeSgm2Kp+PBkJc3FgBYu4=
github.com/fiatjaf/eventstore v0.15.0/go.mod h1:KAsld5BhkmSck48aF11Txu8X+OGNmoabw4TlYVWqInc= github.com/fiatjaf/eventstore v0.16.2/go.mod h1:0gU8fzYO/bG+NQAVlHtJWOlt3JKKFefh5Xjj2d1dLIs=
github.com/fiatjaf/khatru v0.16.0 h1:xgGwnnOqE3989wEWm7c/z6Y6g4X92BFe/Xp1UWQ3Zmc= github.com/fiatjaf/khatru v0.17.4 h1:VzcLUyBKMlP/CAG4iHJbDJmnZgzhbGLKLxJAUuLRogg=
github.com/fiatjaf/khatru v0.16.0/go.mod h1:TLcMgPy3IAPh40VGYq6m+gxEMpDKHj+sumqcuvbSogc= github.com/fiatjaf/khatru v0.17.4/go.mod h1:VYQ7ZNhs3C1+E4gBnx+DtEgU0BrPdrl3XYF3H+mq6fg=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 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/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
@@ -102,8 +102,6 @@ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/graph-gophers/dataloader/v7 v7.1.0 h1:Wn8HGF/q7MNXcvfaBnLEPEFJttVHR8zuEqP1obys/oc=
github.com/graph-gophers/dataloader/v7 v7.1.0/go.mod h1:1bKE0Dm6OUcTB/OAuYVOZctgIz7Q3d0XrYtlIzTgg6Q=
github.com/hablullah/go-hijri v1.0.2 h1:drT/MZpSZJQXo7jftf5fthArShcaMtsal0Zf/dnmp6k= github.com/hablullah/go-hijri v1.0.2 h1:drT/MZpSZJQXo7jftf5fthArShcaMtsal0Zf/dnmp6k=
github.com/hablullah/go-hijri v1.0.2/go.mod h1:OS5qyYLDjORXzK4O1adFw9Q5WfhOcMdAKglDkcTxgWQ= github.com/hablullah/go-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 h1:A8YM7wIj16SzlKT0SRJc9CD29iiaUzpBLzh5hr0/5p0=
@@ -151,8 +149,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 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 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/nbd-wtf/go-nostr v0.51.2 h1:wQysG8omkF4LO7kcU6yoeCBBxD92SwUNab4TMeSuZZM= github.com/nbd-wtf/go-nostr v0.51.8 h1:CIoS+YqChcm4e1L1rfMZ3/mIwTz4CwApM2qx7MHNzmE=
github.com/nbd-wtf/go-nostr v0.51.2/go.mod h1:9PcGOZ+e1VOaLvcK0peT4dbip+/eS+eTWXR3HuexQrA= github.com/nbd-wtf/go-nostr v0.51.8/go.mod h1:d6+DfvMWYG5pA3dmNMBJd6WCHVDDhkXbHqvfljf0Gzg=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 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.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
@@ -166,8 +164,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/puzpuzpuz/xsync/v3 v3.5.0 h1:i+cMcpEDY1BkNm7lPDkCtE4oElsYLn+EKF8kAu2vXT4= github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
github.com/puzpuzpuz/xsync/v3 v3.5.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 h1:D0vL7YNisV2yqE55+q0lFuGse6U8lxlg7fYTctlT5Gc= github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 h1:D0vL7YNisV2yqE55+q0lFuGse6U8lxlg7fYTctlT5Gc=
@@ -199,8 +197,8 @@ github.com/urfave/cli/v3 v3.0.0-beta1 h1:6DTaaUarcM0wX7qj5Hcvs+5Dm3dyUTBbEwIWAjc
github.com/urfave/cli/v3 v3.0.0-beta1/go.mod h1:FnIeEMYu+ko8zP1F9Ypr3xkZMIDqW3DR92yUtY39q1Y= 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 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.58.0 h1:GGB2dWxSbEprU9j0iMJHgdKYJVDyjrOwF9RE59PbRuE= github.com/valyala/fasthttp v1.59.0 h1:Qu0qYHfXvPk1mSLNqcFtEk6DpxgA26hy6bmydotDpRI=
github.com/valyala/fasthttp v1.58.0/go.mod h1:SYXvHHaFp7QZHGKSHmoMipInhrI5StHrhDTYVEjK/Kw= github.com/valyala/fasthttp v1.59.0/go.mod h1:GTxNb9Bc6r2a9D0TWNSPwDz78UxnTGBViY3xZNEqyYU=
github.com/wasilibs/go-re2 v1.3.0 h1:LFhBNzoStM3wMie6rN2slD1cuYH2CGiHpvNL3UtcsMw= 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/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 h1:h1TJMihMC4neN6Zq+WKpLxgd9xCFMw7O9ETLwY2exJQ=
@@ -214,17 +212,17 @@ 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-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-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.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac h1:l5+whBCLH3iH2ZNHYLbAe58bo7yrN4mVcnkHDYz5vvs= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac/go.mod h1:hH+7mtFmImwwcMvScyxUhjuVHR3HGaDPMn9rMSUUbxo= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 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-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -239,11 +237,13 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 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.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@@ -3,6 +3,7 @@ package main
import ( import (
"bufio" "bufio"
"context" "context"
"errors"
"fmt" "fmt"
"iter" "iter"
"math/rand" "math/rand"
@@ -10,15 +11,19 @@ import (
"net/textproto" "net/textproto"
"net/url" "net/url"
"os" "os"
"runtime"
"slices" "slices"
"strings" "strings"
"sync"
"time" "time"
"github.com/fatih/color" "github.com/fatih/color"
jsoniter "github.com/json-iterator/go" jsoniter "github.com/json-iterator/go"
"github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nip19"
"github.com/nbd-wtf/go-nostr/sdk" "github.com/nbd-wtf/go-nostr/sdk"
"github.com/urfave/cli/v3" "github.com/urfave/cli/v3"
"golang.org/x/term"
) )
var sys *sdk.System var sys *sdk.System
@@ -148,8 +153,9 @@ func normalizeAndValidateRelayURLs(wsurls []string) error {
func connectToAllRelays( func connectToAllRelays(
ctx context.Context, ctx context.Context,
c *cli.Command,
relayUrls []string, relayUrls []string,
forcePreAuth bool, preAuthSigner func(ctx context.Context, c *cli.Command, log func(s string, args ...any), authEvent nostr.RelayEvent) (err error), // if this exists we will force preauth
opts ...nostr.PoolOption, opts ...nostr.PoolOption,
) []*nostr.Relay { ) []*nostr.Relay {
sys.Pool = nostr.NewSimplePool(context.Background(), sys.Pool = nostr.NewSimplePool(context.Background(),
@@ -163,50 +169,174 @@ func connectToAllRelays(
) )
relays := make([]*nostr.Relay, 0, len(relayUrls)) relays := make([]*nostr.Relay, 0, len(relayUrls))
relayLoop:
for _, url := range relayUrls { if supportsDynamicMultilineMagic() {
log("connecting to %s... ", url) // overcomplicated multiline rendering magic
if relay, err := sys.Pool.EnsureRelay(url); err == nil { lines := make([][][]byte, len(relayUrls))
if forcePreAuth { flush := func() {
log("waiting for auth challenge... ") for _, line := range lines {
signer := opts[0].(nostr.WithAuthHandler) for _, part := range line {
time.Sleep(time.Millisecond * 200) os.Stderr.Write(part)
challengeWaitLoop:
for {
// beginhack
// here starts the biggest and ugliest hack of this codebase
if err := relay.Auth(ctx, func(authEvent *nostr.Event) error {
challengeTag := authEvent.Tags.GetFirst([]string{"challenge", ""})
if (*challengeTag)[1] == "" {
return fmt.Errorf("auth not received yet *****")
} }
return signer(ctx, nostr.RelayEvent{Event: authEvent, Relay: relay}) os.Stderr.Write([]byte{'\n'})
}
}
render := func() {
clearLines(len(lines))
flush()
}
flush()
wg := sync.WaitGroup{}
wg.Add(len(relayUrls))
for i, url := range relayUrls {
lines[i] = make([][]byte, 1, 2)
logthis := func(s string, args ...any) {
lines[i] = append(lines[i], []byte(fmt.Sprintf(s, args...)))
render()
}
colorizepreamble := func(c func(string, ...any) string) {
lines[i][0] = []byte(fmt.Sprintf("%s... ", c(url)))
}
colorizepreamble(color.CyanString)
go func() {
relay := connectToSingleRelay(ctx, c, url, preAuthSigner, colorizepreamble, logthis)
if relay != nil {
relays = append(relays, relay)
}
wg.Done()
}()
}
wg.Wait()
} else {
// simple flow
for _, url := range relayUrls {
log("connecting to %s... ", color.CyanString(url))
relay := connectToSingleRelay(ctx, c, url, preAuthSigner, nil, log)
if relay != nil {
relays = append(relays, relay)
}
log("\n")
}
}
return relays
}
func connectToSingleRelay(
ctx context.Context,
c *cli.Command,
url string,
preAuthSigner func(ctx context.Context, c *cli.Command, log func(s string, args ...any), authEvent nostr.RelayEvent) (err error),
colorizepreamble func(c func(string, ...any) string),
logthis func(s string, args ...any),
) *nostr.Relay {
if relay, err := sys.Pool.EnsureRelay(url); err == nil {
if preAuthSigner != nil {
if colorizepreamble != nil {
colorizepreamble(color.YellowString)
}
logthis("waiting for auth challenge... ")
time.Sleep(time.Millisecond * 200)
for range 5 {
if err := relay.Auth(ctx, func(authEvent *nostr.Event) error {
challengeTag := authEvent.Tags.Find("challenge")
if challengeTag[1] == "" {
return fmt.Errorf("auth not received yet *****") // what a giant hack
}
return preAuthSigner(ctx, c, logthis, nostr.RelayEvent{Event: authEvent, Relay: relay})
}); err == nil { }); err == nil {
// auth succeeded // auth succeeded
break challengeWaitLoop goto preauthSuccess
} else { } else {
// auth failed // auth failed
if strings.HasSuffix(err.Error(), "auth not received yet *****") { if strings.HasSuffix(err.Error(), "auth not received yet *****") {
// it failed because we didn't receive the challenge yet, so keep waiting // it failed because we didn't receive the challenge yet, so keep waiting
time.Sleep(time.Second) time.Sleep(time.Second)
continue challengeWaitLoop continue
} else { } else {
// it failed for some other reason, so skip this relay // it failed for some other reason, so skip this relay
log(err.Error() + "\n") if colorizepreamble != nil {
continue relayLoop colorizepreamble(colors.errorf)
}
logthis(err.Error())
return nil
} }
} }
// endhack }
if colorizepreamble != nil {
colorizepreamble(colors.errorf)
}
logthis("failed to get an AUTH challenge in enough time.")
return nil
}
preauthSuccess:
if colorizepreamble != nil {
colorizepreamble(colors.successf)
}
logthis("ok.")
return relay
} else {
if colorizepreamble != nil {
colorizepreamble(colors.errorf)
}
// if we're here that means we've failed to connect, this may be a huge message
// but we're likely to only be interested in the lowest level error (although we can leave space)
logthis(clampError(err, len(url)+12))
return nil
} }
} }
relays = append(relays, relay) func clearLines(lineCount int) {
log("ok.\n") for i := 0; i < lineCount; i++ {
} else { os.Stderr.Write([]byte("\033[0A\033[2K\r"))
log(err.Error() + "\n")
} }
} }
return relays
func supportsDynamicMultilineMagic() bool {
if runtime.GOOS == "windows" {
return false
}
if !term.IsTerminal(0) {
return false
}
width, _, err := term.GetSize(0)
if err != nil {
return false
}
if width < 110 {
return false
}
return true
}
func authSigner(ctx context.Context, c *cli.Command, log func(s string, args ...any), authEvent nostr.RelayEvent) (err error) {
defer func() {
if err != nil {
cleanUrl, _ := strings.CutPrefix(authEvent.Relay.URL, "wss://")
log("%s auth failed: %s", colors.errorf(cleanUrl), err)
}
}()
if !c.Bool("auth") && !c.Bool("force-pre-auth") {
return fmt.Errorf("auth required, but --auth flag not given")
}
kr, _, err := gatherKeyerFromArguments(ctx, c)
if err != nil {
return err
}
pk, _ := kr.GetPublicKey(ctx)
npub, _ := nip19.EncodePublicKey(pk)
log("authenticating as %s... ", color.YellowString("%s…%s", npub[0:7], npub[58:]))
return kr.SignEvent(ctx, authEvent.Event)
} }
func lineProcessingError(ctx context.Context, msg string, args ...any) context.Context { func lineProcessingError(ctx context.Context, msg string, args ...any) context.Context {
@@ -234,16 +364,50 @@ func leftPadKey(k string) string {
return strings.Repeat("0", 64-len(k)) + k return strings.Repeat("0", 64-len(k)) + k
} }
func unwrapAll(err error) error {
low := err
for n := low; n != nil; n = errors.Unwrap(low) {
low = n
}
return low
}
func clampMessage(msg string, prefixAlreadyPrinted int) string {
termSize, _, _ := term.GetSize(0)
if len(msg) > termSize-prefixAlreadyPrinted {
msg = msg[0:termSize-prefixAlreadyPrinted-1] + "…"
}
return msg
}
func clampError(err error, prefixAlreadyPrinted int) string {
termSize, _, _ := term.GetSize(0)
msg := err.Error()
if len(msg) > termSize-prefixAlreadyPrinted {
err = unwrapAll(err)
msg = clampMessage(err.Error(), prefixAlreadyPrinted)
}
return msg
}
var colors = struct { var colors = struct {
reset func(...any) (int, error) reset func(...any) (int, error)
italic func(...any) string italic func(...any) string
italicf func(string, ...any) string italicf func(string, ...any) string
bold func(...any) string bold func(...any) string
boldf func(string, ...any) string boldf func(string, ...any) string
error func(...any) string
errorf func(string, ...any) string
success func(...any) string
successf func(string, ...any) string
}{ }{
color.New(color.Reset).Print, color.New(color.Reset).Print,
color.New(color.Italic).Sprint, color.New(color.Italic).Sprint,
color.New(color.Italic).Sprintf, color.New(color.Italic).Sprintf,
color.New(color.Bold).Sprint, color.New(color.Bold).Sprint,
color.New(color.Bold).Sprintf, color.New(color.Bold).Sprintf,
color.New(color.Bold, color.FgHiRed).Sprint,
color.New(color.Bold, color.FgHiRed).Sprintf,
color.New(color.Bold, color.FgHiGreen).Sprint,
color.New(color.Bold, color.FgHiGreen).Sprintf,
} }

View File

@@ -0,0 +1,50 @@
package nostrfs
import (
"context"
"syscall"
"unsafe"
"github.com/hanwen/go-fuse/v2/fs"
"github.com/hanwen/go-fuse/v2/fuse"
)
type DeterministicFile struct {
fs.Inode
get func() (ctime, mtime uint64, data string)
}
var (
_ = (fs.NodeOpener)((*DeterministicFile)(nil))
_ = (fs.NodeReader)((*DeterministicFile)(nil))
_ = (fs.NodeGetattrer)((*DeterministicFile)(nil))
)
func (r *NostrRoot) NewDeterministicFile(get func() (ctime, mtime uint64, data string)) *DeterministicFile {
return &DeterministicFile{
get: get,
}
}
func (f *DeterministicFile) Open(ctx context.Context, flags uint32) (fh fs.FileHandle, fuseFlags uint32, errno syscall.Errno) {
return nil, fuse.FOPEN_KEEP_CACHE, fs.OK
}
func (f *DeterministicFile) Getattr(ctx context.Context, fh fs.FileHandle, out *fuse.AttrOut) syscall.Errno {
var content string
out.Mode = 0444
out.Ctime, out.Mtime, content = f.get()
out.Size = uint64(len(content))
return fs.OK
}
func (f *DeterministicFile) Read(ctx context.Context, fh fs.FileHandle, dest []byte, off int64) (fuse.ReadResult, syscall.Errno) {
_, _, content := f.get()
data := unsafe.Slice(unsafe.StringData(content), len(content))
end := int(off) + len(dest)
if end > len(data) {
end = len(data)
}
return fuse.ReadResultData(data[off:end]), fs.OK
}

View File

@@ -9,9 +9,13 @@ import (
"net/http" "net/http"
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings"
"syscall" "syscall"
"time" "time"
"unsafe"
"fiatjaf.com/lib/debouncer"
"github.com/fatih/color"
"github.com/hanwen/go-fuse/v2/fs" "github.com/hanwen/go-fuse/v2/fs"
"github.com/hanwen/go-fuse/v2/fuse" "github.com/hanwen/go-fuse/v2/fuse"
"github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr"
@@ -23,141 +27,169 @@ import (
type EntityDir struct { type EntityDir struct {
fs.Inode fs.Inode
ctx context.Context root *NostrRoot
wd string
evt *nostr.Event publisher *debouncer.Debouncer
event *nostr.Event
updating struct {
title string
content string
publishedAt uint64
}
} }
var _ = (fs.NodeGetattrer)((*EntityDir)(nil)) var (
_ = (fs.NodeOnAdder)((*EntityDir)(nil))
_ = (fs.NodeGetattrer)((*EntityDir)(nil))
_ = (fs.NodeSetattrer)((*EntityDir)(nil))
_ = (fs.NodeCreater)((*EntityDir)(nil))
_ = (fs.NodeUnlinker)((*EntityDir)(nil))
)
func (e *EntityDir) Getattr(ctx context.Context, f fs.FileHandle, out *fuse.AttrOut) syscall.Errno { func (e *EntityDir) Getattr(_ context.Context, f fs.FileHandle, out *fuse.AttrOut) syscall.Errno {
publishedAt := uint64(e.evt.CreatedAt) out.Ctime = uint64(e.event.CreatedAt)
out.Ctime = publishedAt if e.updating.publishedAt != 0 {
out.Mtime = e.updating.publishedAt
if tag := e.evt.Tags.Find("published_at"); tag != nil { } else {
publishedAt, _ = strconv.ParseUint(tag[1], 10, 64) out.Mtime = e.PublishedAt()
} }
out.Mtime = publishedAt
return fs.OK return fs.OK
} }
func FetchAndCreateEntityDir( func (e *EntityDir) Create(
ctx context.Context, _ context.Context,
parent fs.InodeEmbedder, name string,
wd string, flags uint32,
extension string, mode uint32,
sys *sdk.System, out *fuse.EntryOut,
pointer nostr.EntityPointer, ) (node *fs.Inode, fh fs.FileHandle, fuseFlags uint32, errno syscall.Errno) {
) (*fs.Inode, error) { if name == "publish" && e.publisher.IsRunning() {
event, _, err := sys.FetchSpecificEvent(ctx, pointer, sdk.FetchSpecificEventParameters{ // this causes the publish process to be triggered faster
WithRelays: false, log := e.root.ctx.Value("log").(func(msg string, args ...any))
}) log("publishing now!\n")
if err != nil { e.publisher.Flush()
return nil, fmt.Errorf("failed to fetch: %w", err) return nil, nil, 0, syscall.ENOTDIR
} }
return CreateEntityDir(ctx, parent, wd, extension, event), nil return nil, nil, 0, syscall.ENOTSUP
} }
func CreateEntityDir( func (e *EntityDir) Unlink(ctx context.Context, name string) syscall.Errno {
ctx context.Context, switch name {
parent fs.InodeEmbedder, case "content" + kindToExtension(e.event.Kind):
wd string, e.updating.content = e.event.Content
extension string, return syscall.ENOTDIR
event *nostr.Event, case "title":
) *fs.Inode { e.updating.title = e.Title()
h := parent.EmbeddedInode().NewPersistentInode( return syscall.ENOTDIR
ctx, default:
&EntityDir{ctx: ctx, wd: wd, evt: event}, return syscall.EINTR
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) func (e *EntityDir) Setattr(_ context.Context, _ fs.FileHandle, in *fuse.SetAttrIn, _ *fuse.AttrOut) syscall.Errno {
h.AddChild("@author", h.NewPersistentInode( e.updating.publishedAt = in.Mtime
ctx, return fs.OK
}
func (e *EntityDir) OnAdd(_ context.Context) {
log := e.root.ctx.Value("log").(func(msg string, args ...any))
npub, _ := nip19.EncodePublicKey(e.event.PubKey)
e.AddChild("@author", e.NewPersistentInode(
e.root.ctx,
&fs.MemSymlink{ &fs.MemSymlink{
Data: []byte(wd + "/" + npub), Data: []byte(e.root.wd + "/" + npub),
}, },
fs.StableAttr{Mode: syscall.S_IFLNK}, fs.StableAttr{Mode: syscall.S_IFLNK},
), true) ), true)
eventj, _ := json.MarshalIndent(event, "", " ") e.AddChild("event.json", e.NewPersistentInode(
h.AddChild("event.json", h.NewPersistentInode( e.root.ctx,
ctx, &DeterministicFile{
&fs.MemRegularFile{ get: func() (ctime uint64, mtime uint64, data string) {
Data: eventj, eventj, _ := json.MarshalIndent(e.event, "", " ")
Attr: fuse.Attr{ return uint64(e.event.CreatedAt),
Mode: 0444, uint64(e.event.CreatedAt),
Ctime: uint64(event.CreatedAt), unsafe.String(unsafe.SliceData(eventj), len(eventj))
Mtime: uint64(publishedAt),
Size: uint64(len(event.Content)),
}, },
}, },
fs.StableAttr{}, fs.StableAttr{},
), true) ), true)
h.AddChild("identifier", h.NewPersistentInode( e.AddChild("identifier", e.NewPersistentInode(
ctx, e.root.ctx,
&fs.MemRegularFile{ &fs.MemRegularFile{
Data: []byte(event.Tags.GetD()), Data: []byte(e.event.Tags.GetD()),
Attr: fuse.Attr{ Attr: fuse.Attr{
Mode: 0444, Mode: 0444,
Ctime: uint64(event.CreatedAt), Ctime: uint64(e.event.CreatedAt),
Mtime: uint64(publishedAt), Mtime: uint64(e.event.CreatedAt),
Size: uint64(len(event.Tags.GetD())), Size: uint64(len(e.event.Tags.GetD())),
}, },
}, },
fs.StableAttr{}, fs.StableAttr{},
), true) ), true)
if tag := event.Tags.Find("title"); tag != nil { if e.root.signer == nil || e.root.rootPubKey != e.event.PubKey {
h.AddChild("title", h.NewPersistentInode( // read-only
ctx, e.AddChild("title", e.NewPersistentInode(
&fs.MemRegularFile{ e.root.ctx,
Data: []byte(tag[1]), &DeterministicFile{
Attr: fuse.Attr{ get: func() (ctime uint64, mtime uint64, data string) {
Mode: 0444, return uint64(e.event.CreatedAt), e.PublishedAt(), e.Title()
Ctime: uint64(event.CreatedAt),
Mtime: uint64(publishedAt),
Size: uint64(len(tag[1])),
}, },
}, },
fs.StableAttr{}, fs.StableAttr{},
), true) ), true)
e.AddChild("content."+kindToExtension(e.event.Kind), e.NewPersistentInode(
e.root.ctx,
&DeterministicFile{
get: func() (ctime uint64, mtime uint64, data string) {
return uint64(e.event.CreatedAt), e.PublishedAt(), e.event.Content
},
},
fs.StableAttr{},
), true)
} else {
// writeable
e.updating.title = e.Title()
e.updating.publishedAt = e.PublishedAt()
e.updating.content = e.event.Content
e.AddChild("title", e.NewPersistentInode(
e.root.ctx,
e.root.NewWriteableFile(e.updating.title, uint64(e.event.CreatedAt), e.updating.publishedAt, func(s string) {
log("title updated")
e.updating.title = strings.TrimSpace(s)
e.handleWrite()
}),
fs.StableAttr{},
), true)
e.AddChild("content."+kindToExtension(e.event.Kind), e.NewPersistentInode(
e.root.ctx,
e.root.NewWriteableFile(e.updating.content, uint64(e.event.CreatedAt), e.updating.publishedAt, func(s string) {
log("content updated")
e.updating.content = strings.TrimSpace(s)
e.handleWrite()
}),
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 var refsdir *fs.Inode
i := 0 i := 0
for ref := range nip27.ParseReferences(*event) { for ref := range nip27.ParseReferences(*e.event) {
i++ i++
if refsdir == nil { if refsdir == nil {
refsdir = h.NewPersistentInode(ctx, &fs.Inode{}, fs.StableAttr{Mode: syscall.S_IFDIR}) refsdir = e.NewPersistentInode(e.root.ctx, &fs.Inode{}, fs.StableAttr{Mode: syscall.S_IFDIR})
h.AddChild("references", refsdir, true) e.root.AddChild("references", refsdir, true)
} }
refsdir.AddChild(fmt.Sprintf("ref_%02d", i), refsdir.NewPersistentInode( refsdir.AddChild(fmt.Sprintf("ref_%02d", i), refsdir.NewPersistentInode(
ctx, e.root.ctx,
&fs.MemSymlink{ &fs.MemSymlink{
Data: []byte(wd + "/" + nip19.EncodePointer(ref.Pointer)), Data: []byte(e.root.wd + "/" + nip19.EncodePointer(ref.Pointer)),
}, },
fs.StableAttr{Mode: syscall.S_IFLNK}, fs.StableAttr{Mode: syscall.S_IFLNK},
), true) ), true)
@@ -167,26 +199,29 @@ func CreateEntityDir(
addImage := func(url string) { addImage := func(url string) {
if imagesdir == nil { if imagesdir == nil {
in := &fs.Inode{} in := &fs.Inode{}
imagesdir = h.NewPersistentInode(ctx, in, fs.StableAttr{Mode: syscall.S_IFDIR}) imagesdir = e.NewPersistentInode(e.root.ctx, in, fs.StableAttr{Mode: syscall.S_IFDIR})
h.AddChild("images", imagesdir, true) e.AddChild("images", imagesdir, true)
} }
imagesdir.AddChild(filepath.Base(url), imagesdir.NewPersistentInode( imagesdir.AddChild(filepath.Base(url), imagesdir.NewPersistentInode(
ctx, e.root.ctx,
&AsyncFile{ &AsyncFile{
ctx: ctx, ctx: e.root.ctx,
load: func() ([]byte, nostr.Timestamp) { load: func() ([]byte, nostr.Timestamp) {
ctx, cancel := context.WithTimeout(ctx, time.Second*20) ctx, cancel := context.WithTimeout(e.root.ctx, time.Second*20)
defer cancel() defer cancel()
r, err := http.NewRequestWithContext(ctx, "GET", url, nil) r, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil { if err != nil {
log("failed to load image %s: %s\n", url, err)
return nil, 0 return nil, 0
} }
resp, err := http.DefaultClient.Do(r) resp, err := http.DefaultClient.Do(r)
if err != nil { if err != nil {
log("failed to load image %s: %s\n", url, err)
return nil, 0 return nil, 0
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode >= 300 { if resp.StatusCode >= 300 {
log("failed to load image %s: %s\n", url, err)
return nil, 0 return nil, 0
} }
w := &bytes.Buffer{} w := &bytes.Buffer{}
@@ -198,7 +233,7 @@ func CreateEntityDir(
), true) ), true)
} }
images := nip92.ParseTags(event.Tags) images := nip92.ParseTags(e.event.Tags)
for _, imeta := range images { for _, imeta := range images {
if imeta.URL == "" { if imeta.URL == "" {
continue continue
@@ -206,9 +241,160 @@ func CreateEntityDir(
addImage(imeta.URL) addImage(imeta.URL)
} }
if tag := event.Tags.Find("image"); tag != nil { if tag := e.event.Tags.Find("image"); tag != nil {
addImage(tag[1]) addImage(tag[1])
} }
}
return h
func (e *EntityDir) IsNew() bool {
return e.event.CreatedAt == 0
}
func (e *EntityDir) PublishedAt() uint64 {
if tag := e.event.Tags.Find("published_at"); tag != nil {
publishedAt, _ := strconv.ParseUint(tag[1], 10, 64)
return publishedAt
}
return uint64(e.event.CreatedAt)
}
func (e *EntityDir) Title() string {
if tag := e.event.Tags.Find("title"); tag != nil {
return tag[1]
}
return ""
}
func (e *EntityDir) handleWrite() {
log := e.root.ctx.Value("log").(func(msg string, args ...any))
logverbose := e.root.ctx.Value("logverbose").(func(msg string, args ...any))
if e.root.opts.AutoPublishArticlesTimeout.Hours() < 24*365 {
if e.publisher.IsRunning() {
log(", timer reset")
}
log(", publishing the ")
if e.IsNew() {
log("new")
} else {
log("updated")
}
log(" event in %d seconds...\n", int(e.root.opts.AutoPublishArticlesTimeout.Seconds()))
} else {
log(".\n")
}
if !e.publisher.IsRunning() {
log("- `touch publish` to publish immediately\n")
log("- `rm title content." + kindToExtension(e.event.Kind) + "` to erase and cancel the edits\n")
}
e.publisher.Call(func() {
if e.Title() == e.updating.title && e.event.Content == e.updating.content {
log("not modified, publish canceled.\n")
return
}
evt := nostr.Event{
Kind: e.event.Kind,
Content: e.updating.content,
Tags: make(nostr.Tags, len(e.event.Tags)),
CreatedAt: nostr.Now(),
}
copy(evt.Tags, e.event.Tags) // copy tags because that's the rule
if e.updating.title != "" {
if titleTag := evt.Tags.Find("title"); titleTag != nil {
titleTag[1] = e.updating.title
} else {
evt.Tags = append(evt.Tags, nostr.Tag{"title", e.updating.title})
}
}
// "published_at" tag
publishedAtStr := strconv.FormatUint(e.updating.publishedAt, 10)
if publishedAtStr != "0" {
if publishedAtTag := evt.Tags.Find("published_at"); publishedAtTag != nil {
publishedAtTag[1] = publishedAtStr
} else {
evt.Tags = append(evt.Tags, nostr.Tag{"published_at", publishedAtStr})
}
}
// add "p" tags from people mentioned and "q" tags from events mentioned
for ref := range nip27.ParseReferences(evt) {
tag := ref.Pointer.AsTag()
key := tag[0]
val := tag[1]
if key == "e" || key == "a" {
key = "q"
}
if existing := evt.Tags.FindWithValue(key, val); existing == nil {
evt.Tags = append(evt.Tags, tag)
}
}
// sign and publish
if err := e.root.signer.SignEvent(e.root.ctx, &evt); err != nil {
log("failed to sign: '%s'.\n", err)
return
}
logverbose("%s\n", evt)
relays := e.root.sys.FetchWriteRelays(e.root.ctx, e.root.rootPubKey, 8)
if len(relays) == 0 {
relays = e.root.sys.FetchOutboxRelays(e.root.ctx, e.root.rootPubKey, 6)
}
log("publishing to %d relays... ", len(relays))
success := false
first := true
for res := range e.root.sys.Pool.PublishMany(e.root.ctx, relays, evt) {
cleanUrl, _ := strings.CutPrefix(res.RelayURL, "wss://")
if !first {
log(", ")
}
first = false
if res.Error != nil {
log("%s: %s", color.RedString(cleanUrl), res.Error)
} else {
success = true
log("%s: ok", color.GreenString(cleanUrl))
}
}
log("\n")
if success {
e.event = &evt
log("event updated locally.\n")
e.updating.publishedAt = uint64(evt.CreatedAt) // set this so subsequent edits get the correct value
} else {
log("failed.\n")
}
})
}
func (r *NostrRoot) FetchAndCreateEntityDir(
parent fs.InodeEmbedder,
extension string,
pointer nostr.EntityPointer,
) (*fs.Inode, error) {
event, _, err := r.sys.FetchSpecificEvent(r.ctx, pointer, sdk.FetchSpecificEventParameters{
WithRelays: false,
})
if err != nil {
return nil, fmt.Errorf("failed to fetch: %w", err)
}
return r.CreateEntityDir(parent, event), nil
}
func (r *NostrRoot) CreateEntityDir(
parent fs.InodeEmbedder,
event *nostr.Event,
) *fs.Inode {
return parent.EmbeddedInode().NewPersistentInode(
r.ctx,
&EntityDir{root: r, event: event, publisher: debouncer.New(r.opts.AutoPublishArticlesTimeout)},
fs.StableAttr{Mode: syscall.S_IFDIR},
)
} }

View File

@@ -18,6 +18,7 @@ import (
"github.com/nbd-wtf/go-nostr/nip19" "github.com/nbd-wtf/go-nostr/nip19"
"github.com/nbd-wtf/go-nostr/nip22" "github.com/nbd-wtf/go-nostr/nip22"
"github.com/nbd-wtf/go-nostr/nip27" "github.com/nbd-wtf/go-nostr/nip27"
"github.com/nbd-wtf/go-nostr/nip73"
"github.com/nbd-wtf/go-nostr/nip92" "github.com/nbd-wtf/go-nostr/nip92"
sdk "github.com/nbd-wtf/go-nostr/sdk" sdk "github.com/nbd-wtf/go-nostr/sdk"
) )
@@ -31,52 +32,47 @@ type EventDir struct {
var _ = (fs.NodeGetattrer)((*EventDir)(nil)) var _ = (fs.NodeGetattrer)((*EventDir)(nil))
func (e *EventDir) Getattr(ctx context.Context, f fs.FileHandle, out *fuse.AttrOut) syscall.Errno { func (e *EventDir) Getattr(_ context.Context, f fs.FileHandle, out *fuse.AttrOut) syscall.Errno {
out.Mtime = uint64(e.evt.CreatedAt) out.Mtime = uint64(e.evt.CreatedAt)
return fs.OK return fs.OK
} }
func FetchAndCreateEventDir( func (r *NostrRoot) FetchAndCreateEventDir(
ctx context.Context,
parent fs.InodeEmbedder, parent fs.InodeEmbedder,
wd string,
sys *sdk.System,
pointer nostr.EventPointer, pointer nostr.EventPointer,
) (*fs.Inode, error) { ) (*fs.Inode, error) {
event, _, err := sys.FetchSpecificEvent(ctx, pointer, sdk.FetchSpecificEventParameters{ event, _, err := r.sys.FetchSpecificEvent(r.ctx, pointer, sdk.FetchSpecificEventParameters{
WithRelays: false, WithRelays: false,
}) })
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to fetch: %w", err) return nil, fmt.Errorf("failed to fetch: %w", err)
} }
return CreateEventDir(ctx, parent, wd, event), nil return r.CreateEventDir(parent, event), nil
} }
func CreateEventDir( func (r *NostrRoot) CreateEventDir(
ctx context.Context,
parent fs.InodeEmbedder, parent fs.InodeEmbedder,
wd string,
event *nostr.Event, event *nostr.Event,
) *fs.Inode { ) *fs.Inode {
h := parent.EmbeddedInode().NewPersistentInode( h := parent.EmbeddedInode().NewPersistentInode(
ctx, r.ctx,
&EventDir{ctx: ctx, wd: wd, evt: event}, &EventDir{ctx: r.ctx, wd: r.wd, evt: event},
fs.StableAttr{Mode: syscall.S_IFDIR, Ino: hexToUint64(event.ID)}, fs.StableAttr{Mode: syscall.S_IFDIR, Ino: hexToUint64(event.ID)},
) )
npub, _ := nip19.EncodePublicKey(event.PubKey) npub, _ := nip19.EncodePublicKey(event.PubKey)
h.AddChild("@author", h.NewPersistentInode( h.AddChild("@author", h.NewPersistentInode(
ctx, r.ctx,
&fs.MemSymlink{ &fs.MemSymlink{
Data: []byte(wd + "/" + npub), Data: []byte(r.wd + "/" + npub),
}, },
fs.StableAttr{Mode: syscall.S_IFLNK}, fs.StableAttr{Mode: syscall.S_IFLNK},
), true) ), true)
eventj, _ := json.MarshalIndent(event, "", " ") eventj, _ := json.MarshalIndent(event, "", " ")
h.AddChild("event.json", h.NewPersistentInode( h.AddChild("event.json", h.NewPersistentInode(
ctx, r.ctx,
&fs.MemRegularFile{ &fs.MemRegularFile{
Data: eventj, Data: eventj,
Attr: fuse.Attr{ Attr: fuse.Attr{
@@ -90,7 +86,7 @@ func CreateEventDir(
), true) ), true)
h.AddChild("id", h.NewPersistentInode( h.AddChild("id", h.NewPersistentInode(
ctx, r.ctx,
&fs.MemRegularFile{ &fs.MemRegularFile{
Data: []byte(event.ID), Data: []byte(event.ID),
Attr: fuse.Attr{ Attr: fuse.Attr{
@@ -104,7 +100,7 @@ func CreateEventDir(
), true) ), true)
h.AddChild("content.txt", h.NewPersistentInode( h.AddChild("content.txt", h.NewPersistentInode(
ctx, r.ctx,
&fs.MemRegularFile{ &fs.MemRegularFile{
Data: []byte(event.Content), Data: []byte(event.Content),
Attr: fuse.Attr{ Attr: fuse.Attr{
@@ -122,13 +118,13 @@ func CreateEventDir(
for ref := range nip27.ParseReferences(*event) { for ref := range nip27.ParseReferences(*event) {
i++ i++
if refsdir == nil { if refsdir == nil {
refsdir = h.NewPersistentInode(ctx, &fs.Inode{}, fs.StableAttr{Mode: syscall.S_IFDIR}) refsdir = h.NewPersistentInode(r.ctx, &fs.Inode{}, fs.StableAttr{Mode: syscall.S_IFDIR})
h.AddChild("references", refsdir, true) h.AddChild("references", refsdir, true)
} }
refsdir.AddChild(fmt.Sprintf("ref_%02d", i), refsdir.NewPersistentInode( refsdir.AddChild(fmt.Sprintf("ref_%02d", i), refsdir.NewPersistentInode(
ctx, r.ctx,
&fs.MemSymlink{ &fs.MemSymlink{
Data: []byte(wd + "/" + nip19.EncodePointer(ref.Pointer)), Data: []byte(r.wd + "/" + nip19.EncodePointer(ref.Pointer)),
}, },
fs.StableAttr{Mode: syscall.S_IFLNK}, fs.StableAttr{Mode: syscall.S_IFLNK},
), true) ), true)
@@ -142,15 +138,15 @@ func CreateEventDir(
} }
if imagesdir == nil { if imagesdir == nil {
in := &fs.Inode{} in := &fs.Inode{}
imagesdir = h.NewPersistentInode(ctx, in, fs.StableAttr{Mode: syscall.S_IFDIR}) imagesdir = h.NewPersistentInode(r.ctx, in, fs.StableAttr{Mode: syscall.S_IFDIR})
h.AddChild("images", imagesdir, true) h.AddChild("images", imagesdir, true)
} }
imagesdir.AddChild(filepath.Base(imeta.URL), imagesdir.NewPersistentInode( imagesdir.AddChild(filepath.Base(imeta.URL), imagesdir.NewPersistentInode(
ctx, r.ctx,
&AsyncFile{ &AsyncFile{
ctx: ctx, ctx: r.ctx,
load: func() ([]byte, nostr.Timestamp) { load: func() ([]byte, nostr.Timestamp) {
ctx, cancel := context.WithTimeout(ctx, time.Second*20) ctx, cancel := context.WithTimeout(r.ctx, time.Second*20)
defer cancel() defer cancel()
r, err := http.NewRequestWithContext(ctx, "GET", imeta.URL, nil) r, err := http.NewRequestWithContext(ctx, "GET", imeta.URL, nil)
if err != nil { if err != nil {
@@ -177,9 +173,9 @@ func CreateEventDir(
if pointer := nip10.GetThreadRoot(event.Tags); pointer != nil { if pointer := nip10.GetThreadRoot(event.Tags); pointer != nil {
nevent := nip19.EncodePointer(*pointer) nevent := nip19.EncodePointer(*pointer)
h.AddChild("@root", h.NewPersistentInode( h.AddChild("@root", h.NewPersistentInode(
ctx, r.ctx,
&fs.MemSymlink{ &fs.MemSymlink{
Data: []byte(wd + "/" + nevent), Data: []byte(r.wd + "/" + nevent),
}, },
fs.StableAttr{Mode: syscall.S_IFLNK}, fs.StableAttr{Mode: syscall.S_IFLNK},
), true) ), true)
@@ -187,18 +183,18 @@ func CreateEventDir(
if pointer := nip10.GetImmediateParent(event.Tags); pointer != nil { if pointer := nip10.GetImmediateParent(event.Tags); pointer != nil {
nevent := nip19.EncodePointer(*pointer) nevent := nip19.EncodePointer(*pointer)
h.AddChild("@parent", h.NewPersistentInode( h.AddChild("@parent", h.NewPersistentInode(
ctx, r.ctx,
&fs.MemSymlink{ &fs.MemSymlink{
Data: []byte(wd + "/" + nevent), Data: []byte(r.wd + "/" + nevent),
}, },
fs.StableAttr{Mode: syscall.S_IFLNK}, fs.StableAttr{Mode: syscall.S_IFLNK},
), true) ), true)
} }
} else if event.Kind == 1111 { } else if event.Kind == 1111 {
if pointer := nip22.GetThreadRoot(event.Tags); pointer != nil { if pointer := nip22.GetThreadRoot(event.Tags); pointer != nil {
if xp, ok := pointer.(nostr.ExternalPointer); ok { if xp, ok := pointer.(nip73.ExternalPointer); ok {
h.AddChild("@root", h.NewPersistentInode( h.AddChild("@root", h.NewPersistentInode(
ctx, r.ctx,
&fs.MemRegularFile{ &fs.MemRegularFile{
Data: []byte(`<!doctype html><meta http-equiv="refresh" content="0; url=` + xp.Thing + `" />`), Data: []byte(`<!doctype html><meta http-equiv="refresh" content="0; url=` + xp.Thing + `" />`),
}, },
@@ -207,18 +203,18 @@ func CreateEventDir(
} else { } else {
nevent := nip19.EncodePointer(pointer) nevent := nip19.EncodePointer(pointer)
h.AddChild("@parent", h.NewPersistentInode( h.AddChild("@parent", h.NewPersistentInode(
ctx, r.ctx,
&fs.MemSymlink{ &fs.MemSymlink{
Data: []byte(wd + "/" + nevent), Data: []byte(r.wd + "/" + nevent),
}, },
fs.StableAttr{Mode: syscall.S_IFLNK}, fs.StableAttr{Mode: syscall.S_IFLNK},
), true) ), true)
} }
} }
if pointer := nip22.GetImmediateParent(event.Tags); pointer != nil { if pointer := nip22.GetImmediateParent(event.Tags); pointer != nil {
if xp, ok := pointer.(nostr.ExternalPointer); ok { if xp, ok := pointer.(nip73.ExternalPointer); ok {
h.AddChild("@parent", h.NewPersistentInode( h.AddChild("@parent", h.NewPersistentInode(
ctx, r.ctx,
&fs.MemRegularFile{ &fs.MemRegularFile{
Data: []byte(`<!doctype html><meta http-equiv="refresh" content="0; url=` + xp.Thing + `" />`), Data: []byte(`<!doctype html><meta http-equiv="refresh" content="0; url=` + xp.Thing + `" />`),
}, },
@@ -227,9 +223,9 @@ func CreateEventDir(
} else { } else {
nevent := nip19.EncodePointer(pointer) nevent := nip19.EncodePointer(pointer)
h.AddChild("@parent", h.NewPersistentInode( h.AddChild("@parent", h.NewPersistentInode(
ctx, r.ctx,
&fs.MemSymlink{ &fs.MemSymlink{
Data: []byte(wd + "/" + nevent), Data: []byte(r.wd + "/" + nevent),
}, },
fs.StableAttr{Mode: syscall.S_IFLNK}, fs.StableAttr{Mode: syscall.S_IFLNK},
), true) ), true)

View File

@@ -2,6 +2,17 @@ package nostrfs
import "strconv" import "strconv"
func kindToExtension(kind int) string {
switch kind {
case 30023:
return "md"
case 30818:
return "adoc"
default:
return "txt"
}
}
func hexToUint64(hexStr string) uint64 { func hexToUint64(hexStr string) uint64 {
v, _ := strconv.ParseUint(hexStr[16:32], 16, 64) v, _ := strconv.ParseUint(hexStr[16:32], 16, 64)
return v return v

View File

@@ -10,45 +10,51 @@ import (
"syscall" "syscall"
"time" "time"
"github.com/fatih/color"
"github.com/hanwen/go-fuse/v2/fs" "github.com/hanwen/go-fuse/v2/fs"
"github.com/hanwen/go-fuse/v2/fuse" "github.com/hanwen/go-fuse/v2/fuse"
"github.com/liamg/magic" "github.com/liamg/magic"
"github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr"
sdk "github.com/nbd-wtf/go-nostr/sdk" "github.com/nbd-wtf/go-nostr/nip19"
) )
type NpubDir struct { type NpubDir struct {
sys *sdk.System
fs.Inode fs.Inode
root *NostrRoot
pointer nostr.ProfilePointer pointer nostr.ProfilePointer
ctx context.Context
fetched atomic.Bool fetched atomic.Bool
} }
func CreateNpubDir( var _ = (fs.NodeOnAdder)((*NpubDir)(nil))
ctx context.Context,
sys *sdk.System, func (r *NostrRoot) CreateNpubDir(
parent fs.InodeEmbedder, parent fs.InodeEmbedder,
wd string,
pointer nostr.ProfilePointer, pointer nostr.ProfilePointer,
signer nostr.Signer,
) *fs.Inode { ) *fs.Inode {
npubdir := &NpubDir{ctx: ctx, sys: sys, pointer: pointer} npubdir := &NpubDir{root: r, pointer: pointer}
h := parent.EmbeddedInode().NewPersistentInode( return parent.EmbeddedInode().NewPersistentInode(
ctx, r.ctx,
npubdir, npubdir,
fs.StableAttr{Mode: syscall.S_IFDIR, Ino: hexToUint64(pointer.PublicKey)}, fs.StableAttr{Mode: syscall.S_IFDIR, Ino: hexToUint64(pointer.PublicKey)},
) )
}
relays := sys.FetchOutboxRelays(ctx, pointer.PublicKey, 2) func (h *NpubDir) OnAdd(_ context.Context) {
log := h.root.ctx.Value("log").(func(msg string, args ...any))
relays := h.root.sys.FetchOutboxRelays(h.root.ctx, h.pointer.PublicKey, 2)
log("- adding folder for %s with relays %s\n",
color.HiYellowString(nip19.EncodePointer(h.pointer)), color.HiGreenString("%v", relays))
h.AddChild("pubkey", h.NewPersistentInode( h.AddChild("pubkey", h.NewPersistentInode(
ctx, h.root.ctx,
&fs.MemRegularFile{Data: []byte(pointer.PublicKey + "\n"), Attr: fuse.Attr{Mode: 0444}}, &fs.MemRegularFile{Data: []byte(h.pointer.PublicKey + "\n"), Attr: fuse.Attr{Mode: 0444}},
fs.StableAttr{}, fs.StableAttr{},
), true) ), true)
go func() { go func() {
pm := sys.FetchProfileMetadata(ctx, pointer.PublicKey) pm := h.root.sys.FetchProfileMetadata(h.root.ctx, h.pointer.PublicKey)
if pm.Event == nil { if pm.Event == nil {
return return
} }
@@ -57,7 +63,7 @@ func CreateNpubDir(
h.AddChild( h.AddChild(
"metadata.json", "metadata.json",
h.NewPersistentInode( h.NewPersistentInode(
ctx, h.root.ctx,
&fs.MemRegularFile{ &fs.MemRegularFile{
Data: metadataj, Data: metadataj,
Attr: fuse.Attr{ Attr: fuse.Attr{
@@ -70,11 +76,11 @@ func CreateNpubDir(
true, true,
) )
ctx, cancel := context.WithTimeout(ctx, time.Second*20) ctx, cancel := context.WithTimeout(h.root.ctx, time.Second*20)
defer cancel() defer cancel()
r, err := http.NewRequestWithContext(ctx, "GET", pm.Picture, nil) req, err := http.NewRequestWithContext(ctx, "GET", pm.Picture, nil)
if err == nil { if err == nil {
resp, err := http.DefaultClient.Do(r) resp, err := http.DefaultClient.Do(req)
if err == nil { if err == nil {
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode < 300 { if resp.StatusCode < 300 {
@@ -102,174 +108,153 @@ func CreateNpubDir(
} }
}() }()
if h.GetChild("notes") == nil {
h.AddChild( h.AddChild(
"notes", "notes",
h.NewPersistentInode( h.NewPersistentInode(
ctx, h.root.ctx,
&ViewDir{ &ViewDir{
ctx: ctx, root: h.root,
sys: sys,
wd: wd,
filter: nostr.Filter{ filter: nostr.Filter{
Kinds: []int{1}, Kinds: []int{1},
Authors: []string{pointer.PublicKey}, Authors: []string{h.pointer.PublicKey},
}, },
paginate: true, paginate: true,
relays: relays, relays: relays,
create: func(ctx context.Context, n *ViewDir, event *nostr.Event) (string, *fs.Inode) { replaceable: false,
return event.ID, CreateEventDir(ctx, n, n.wd, event) createable: true,
},
}, },
fs.StableAttr{Mode: syscall.S_IFDIR}, fs.StableAttr{Mode: syscall.S_IFDIR},
), ),
true, true,
) )
}
if h.GetChild("comments") == nil {
h.AddChild( h.AddChild(
"comments", "comments",
h.NewPersistentInode( h.NewPersistentInode(
ctx, h.root.ctx,
&ViewDir{ &ViewDir{
ctx: ctx, root: h.root,
sys: sys,
wd: wd,
filter: nostr.Filter{ filter: nostr.Filter{
Kinds: []int{1111}, Kinds: []int{1111},
Authors: []string{pointer.PublicKey}, Authors: []string{h.pointer.PublicKey},
}, },
paginate: true, paginate: true,
relays: relays, relays: relays,
create: func(ctx context.Context, n *ViewDir, event *nostr.Event) (string, *fs.Inode) { replaceable: false,
return event.ID, CreateEventDir(ctx, n, n.wd, event)
},
}, },
fs.StableAttr{Mode: syscall.S_IFDIR}, fs.StableAttr{Mode: syscall.S_IFDIR},
), ),
true, true,
) )
}
if h.GetChild("photos") == nil {
h.AddChild( h.AddChild(
"pictures", "photos",
h.NewPersistentInode( h.NewPersistentInode(
ctx, h.root.ctx,
&ViewDir{ &ViewDir{
ctx: ctx, root: h.root,
sys: sys,
wd: wd,
filter: nostr.Filter{ filter: nostr.Filter{
Kinds: []int{20}, Kinds: []int{20},
Authors: []string{pointer.PublicKey}, Authors: []string{h.pointer.PublicKey},
}, },
paginate: true, paginate: true,
relays: relays, relays: relays,
create: func(ctx context.Context, n *ViewDir, event *nostr.Event) (string, *fs.Inode) { replaceable: false,
return event.ID, CreateEventDir(ctx, n, n.wd, event)
},
}, },
fs.StableAttr{Mode: syscall.S_IFDIR}, fs.StableAttr{Mode: syscall.S_IFDIR},
), ),
true, true,
) )
}
if h.GetChild("videos") == nil {
h.AddChild( h.AddChild(
"videos", "videos",
h.NewPersistentInode( h.NewPersistentInode(
ctx, h.root.ctx,
&ViewDir{ &ViewDir{
ctx: ctx, root: h.root,
sys: sys,
wd: wd,
filter: nostr.Filter{ filter: nostr.Filter{
Kinds: []int{21, 22}, Kinds: []int{21, 22},
Authors: []string{pointer.PublicKey}, Authors: []string{h.pointer.PublicKey},
}, },
paginate: true, paginate: false,
relays: relays, relays: relays,
create: func(ctx context.Context, n *ViewDir, event *nostr.Event) (string, *fs.Inode) { replaceable: false,
return event.ID, CreateEventDir(ctx, n, n.wd, event)
},
}, },
fs.StableAttr{Mode: syscall.S_IFDIR}, fs.StableAttr{Mode: syscall.S_IFDIR},
), ),
true, true,
) )
}
if h.GetChild("highlights") == nil {
h.AddChild( h.AddChild(
"highlights", "highlights",
h.NewPersistentInode( h.NewPersistentInode(
ctx, h.root.ctx,
&ViewDir{ &ViewDir{
ctx: ctx, root: h.root,
sys: sys,
wd: wd,
filter: nostr.Filter{ filter: nostr.Filter{
Kinds: []int{9802}, Kinds: []int{9802},
Authors: []string{pointer.PublicKey}, Authors: []string{h.pointer.PublicKey},
}, },
paginate: true, paginate: false,
relays: relays, relays: relays,
create: func(ctx context.Context, n *ViewDir, event *nostr.Event) (string, *fs.Inode) { replaceable: false,
return event.ID, CreateEventDir(ctx, n, n.wd, event)
},
}, },
fs.StableAttr{Mode: syscall.S_IFDIR}, fs.StableAttr{Mode: syscall.S_IFDIR},
), ),
true, true,
) )
}
if h.GetChild("articles") == nil {
h.AddChild( h.AddChild(
"articles", "articles",
h.NewPersistentInode( h.NewPersistentInode(
ctx, h.root.ctx,
&ViewDir{ &ViewDir{
ctx: ctx, root: h.root,
sys: sys,
wd: wd,
filter: nostr.Filter{ filter: nostr.Filter{
Kinds: []int{30023}, Kinds: []int{30023},
Authors: []string{pointer.PublicKey}, Authors: []string{h.pointer.PublicKey},
}, },
paginate: false, paginate: false,
relays: relays, relays: relays,
create: func(ctx context.Context, n *ViewDir, event *nostr.Event) (string, *fs.Inode) { replaceable: true,
d := event.Tags.GetD() createable: true,
if d == "" {
d = "_"
}
return d, CreateEntityDir(ctx, n, n.wd, ".md", event)
},
}, },
fs.StableAttr{Mode: syscall.S_IFDIR}, fs.StableAttr{Mode: syscall.S_IFDIR},
), ),
true, true,
) )
}
if h.GetChild("wiki") == nil {
h.AddChild( h.AddChild(
"wiki", "wiki",
h.NewPersistentInode( h.NewPersistentInode(
ctx, h.root.ctx,
&ViewDir{ &ViewDir{
ctx: ctx, root: h.root,
sys: sys,
wd: wd,
filter: nostr.Filter{ filter: nostr.Filter{
Kinds: []int{30818}, Kinds: []int{30818},
Authors: []string{pointer.PublicKey}, Authors: []string{h.pointer.PublicKey},
}, },
paginate: false, paginate: false,
relays: relays, relays: relays,
create: func(ctx context.Context, n *ViewDir, event *nostr.Event) (string, *fs.Inode) { replaceable: true,
d := event.Tags.GetD() createable: true,
if d == "" {
d = "_"
}
return d, CreateEntityDir(ctx, n, n.wd, ".adoc", event)
},
}, },
fs.StableAttr{Mode: syscall.S_IFDIR}, fs.StableAttr{Mode: syscall.S_IFDIR},
), ),
true, true,
) )
}
return h
} }

View File

@@ -14,6 +14,11 @@ import (
"github.com/nbd-wtf/go-nostr/sdk" "github.com/nbd-wtf/go-nostr/sdk"
) )
type Options struct {
AutoPublishNotesTimeout time.Duration
AutoPublishArticlesTimeout time.Duration
}
type NostrRoot struct { type NostrRoot struct {
fs.Inode fs.Inode
@@ -22,29 +27,40 @@ type NostrRoot struct {
sys *sdk.System sys *sdk.System
rootPubKey string rootPubKey string
signer nostr.Signer signer nostr.Signer
opts Options
} }
var _ = (fs.NodeOnAdder)((*NostrRoot)(nil)) var _ = (fs.NodeOnAdder)((*NostrRoot)(nil))
func NewNostrRoot(ctx context.Context, sys *sdk.System, user nostr.User, mountpoint string) *NostrRoot { func NewNostrRoot(ctx context.Context, sys *sdk.System, user nostr.User, mountpoint string, o Options) *NostrRoot {
pubkey, _ := user.GetPublicKey(ctx) pubkey, _ := user.GetPublicKey(ctx)
signer, _ := user.(nostr.Signer)
abs, _ := filepath.Abs(mountpoint) abs, _ := filepath.Abs(mountpoint)
var signer nostr.Signer
if user != nil {
signer, _ = user.(nostr.Signer)
}
return &NostrRoot{ return &NostrRoot{
ctx: ctx, ctx: ctx,
sys: sys, sys: sys,
rootPubKey: pubkey, rootPubKey: pubkey,
signer: signer, signer: signer,
wd: abs, wd: abs,
opts: o,
} }
} }
func (r *NostrRoot) OnAdd(context.Context) { func (r *NostrRoot) OnAdd(_ context.Context) {
if r.rootPubKey == "" { if r.rootPubKey == "" {
return return
} }
go func() {
time.Sleep(time.Millisecond * 100)
// add our contacts // add our contacts
fl := r.sys.FetchFollowList(r.ctx, r.rootPubKey) fl := r.sys.FetchFollowList(r.ctx, r.rootPubKey)
for _, f := range fl.Items { for _, f := range fl.Items {
@@ -52,7 +68,7 @@ func (r *NostrRoot) OnAdd(context.Context) {
npub, _ := nip19.EncodePublicKey(f.Pubkey) npub, _ := nip19.EncodePublicKey(f.Pubkey)
r.AddChild( r.AddChild(
npub, npub,
CreateNpubDir(r.ctx, r.sys, r, r.wd, pointer), r.CreateNpubDir(r, pointer, nil),
true, true,
) )
} }
@@ -61,9 +77,10 @@ func (r *NostrRoot) OnAdd(context.Context) {
npub, _ := nip19.EncodePublicKey(r.rootPubKey) npub, _ := nip19.EncodePublicKey(r.rootPubKey)
if r.GetChild(npub) == nil { if r.GetChild(npub) == nil {
pointer := nostr.ProfilePointer{PublicKey: r.rootPubKey} pointer := nostr.ProfilePointer{PublicKey: r.rootPubKey}
r.AddChild( r.AddChild(
npub, npub,
CreateNpubDir(r.ctx, r.sys, r, r.wd, pointer), r.CreateNpubDir(r, pointer, r.signer),
true, true,
) )
} }
@@ -74,9 +91,10 @@ func (r *NostrRoot) OnAdd(context.Context) {
&fs.MemSymlink{Data: []byte(r.wd + "/" + npub)}, &fs.MemSymlink{Data: []byte(r.wd + "/" + npub)},
fs.StableAttr{Mode: syscall.S_IFLNK}, fs.StableAttr{Mode: syscall.S_IFLNK},
), true) ), true)
}()
} }
func (r *NostrRoot) Lookup(ctx context.Context, name string, out *fuse.EntryOut) (*fs.Inode, syscall.Errno) { func (r *NostrRoot) Lookup(_ context.Context, name string, out *fuse.EntryOut) (*fs.Inode, syscall.Errno) {
out.SetEntryTimeout(time.Minute * 5) out.SetEntryTimeout(time.Minute * 5)
child := r.GetChild(name) child := r.GetChild(name)
@@ -84,9 +102,12 @@ func (r *NostrRoot) Lookup(ctx context.Context, name string, out *fuse.EntryOut)
return child, fs.OK return child, fs.OK
} }
if pp, err := nip05.QueryIdentifier(ctx, name); err == nil { if pp, err := nip05.QueryIdentifier(r.ctx, name); err == nil {
npubdir := CreateNpubDir(ctx, r.sys, r, r.wd, *pp) return r.NewPersistentInode(
return npubdir, fs.OK r.ctx,
&fs.MemSymlink{Data: []byte(r.wd + "/" + nip19.EncodePointer(*pp))},
fs.StableAttr{Mode: syscall.S_IFLNK},
), fs.OK
} }
pointer, err := nip19.ToPointer(name) pointer, err := nip19.ToPointer(name)
@@ -96,10 +117,10 @@ func (r *NostrRoot) Lookup(ctx context.Context, name string, out *fuse.EntryOut)
switch p := pointer.(type) { switch p := pointer.(type) {
case nostr.ProfilePointer: case nostr.ProfilePointer:
npubdir := CreateNpubDir(ctx, r.sys, r, r.wd, p) npubdir := r.CreateNpubDir(r, p, nil)
return npubdir, fs.OK return npubdir, fs.OK
case nostr.EventPointer: case nostr.EventPointer:
eventdir, err := FetchAndCreateEventDir(ctx, r, r.wd, r.sys, p) eventdir, err := r.FetchAndCreateEventDir(r, p)
if err != nil { if err != nil {
return nil, syscall.ENOENT return nil, syscall.ENOENT
} }

View File

@@ -2,33 +2,206 @@ package nostrfs
import ( import (
"context" "context"
"slices"
"strings"
"sync/atomic" "sync/atomic"
"syscall" "syscall"
"fiatjaf.com/lib/debouncer"
"github.com/fatih/color"
"github.com/hanwen/go-fuse/v2/fs" "github.com/hanwen/go-fuse/v2/fs"
"github.com/hanwen/go-fuse/v2/fuse" "github.com/hanwen/go-fuse/v2/fuse"
"github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr"
sdk "github.com/nbd-wtf/go-nostr/sdk" "github.com/nbd-wtf/go-nostr/nip27"
) )
type ViewDir struct { type ViewDir struct {
fs.Inode fs.Inode
ctx context.Context root *NostrRoot
sys *sdk.System
wd string
fetched atomic.Bool fetched atomic.Bool
filter nostr.Filter filter nostr.Filter
paginate bool paginate bool
relays []string relays []string
create func(context.Context, *ViewDir, *nostr.Event) (string, *fs.Inode) replaceable bool
createable bool
publisher *debouncer.Debouncer
publishing struct {
note string
}
} }
var ( var (
_ = (fs.NodeOpendirer)((*ViewDir)(nil)) _ = (fs.NodeOpendirer)((*ViewDir)(nil))
_ = (fs.NodeGetattrer)((*ViewDir)(nil)) _ = (fs.NodeGetattrer)((*ViewDir)(nil))
_ = (fs.NodeMkdirer)((*ViewDir)(nil))
_ = (fs.NodeSetattrer)((*ViewDir)(nil))
_ = (fs.NodeCreater)((*ViewDir)(nil))
_ = (fs.NodeUnlinker)((*ViewDir)(nil))
) )
func (n *ViewDir) Getattr(ctx context.Context, f fs.FileHandle, out *fuse.AttrOut) syscall.Errno { func (f *ViewDir) Setattr(_ context.Context, _ fs.FileHandle, _ *fuse.SetAttrIn, _ *fuse.AttrOut) syscall.Errno {
return fs.OK
}
func (n *ViewDir) Create(
_ context.Context,
name string,
flags uint32,
mode uint32,
out *fuse.EntryOut,
) (node *fs.Inode, fh fs.FileHandle, fuseFlags uint32, errno syscall.Errno) {
if !n.createable || n.root.rootPubKey != n.filter.Authors[0] {
return nil, nil, 0, syscall.EPERM
}
if n.publisher == nil {
n.publisher = debouncer.New(n.root.opts.AutoPublishNotesTimeout)
}
if n.filter.Kinds[0] != 1 {
return nil, nil, 0, syscall.ENOTSUP
}
switch name {
case "new":
log := n.root.ctx.Value("log").(func(msg string, args ...any))
if n.publisher.IsRunning() {
log("pending note updated, timer reset.")
} else {
log("new note detected")
if n.root.opts.AutoPublishNotesTimeout.Hours() < 24*365 {
log(", publishing it in %d seconds...\n", int(n.root.opts.AutoPublishNotesTimeout.Seconds()))
} else {
log(".\n")
}
log("- `touch publish` to publish immediately\n")
log("- `rm new` to erase and cancel the publication.\n")
}
n.publisher.Call(n.publishNote)
first := true
return n.NewPersistentInode(
n.root.ctx,
n.root.NewWriteableFile(n.publishing.note, uint64(nostr.Now()), uint64(nostr.Now()), func(s string) {
if !first {
log("pending note updated, timer reset.\n")
}
first = false
n.publishing.note = strings.TrimSpace(s)
n.publisher.Call(n.publishNote)
}),
fs.StableAttr{},
), nil, 0, fs.OK
case "publish":
if n.publisher.IsRunning() {
// this causes the publish process to be triggered faster
log := n.root.ctx.Value("log").(func(msg string, args ...any))
log("publishing now!\n")
n.publisher.Flush()
return nil, nil, 0, syscall.ENOTDIR
}
}
return nil, nil, 0, syscall.ENOTSUP
}
func (n *ViewDir) Unlink(ctx context.Context, name string) syscall.Errno {
if !n.createable || n.root.rootPubKey != n.filter.Authors[0] {
return syscall.EPERM
}
if n.publisher == nil {
n.publisher = debouncer.New(n.root.opts.AutoPublishNotesTimeout)
}
if n.filter.Kinds[0] != 1 {
return syscall.ENOTSUP
}
switch name {
case "new":
log := n.root.ctx.Value("log").(func(msg string, args ...any))
log("publishing canceled.\n")
n.publisher.Stop()
n.publishing.note = ""
return fs.OK
}
return syscall.ENOTSUP
}
func (n *ViewDir) publishNote() {
log := n.root.ctx.Value("log").(func(msg string, args ...any))
log("publishing note...\n")
evt := nostr.Event{
Kind: 1,
CreatedAt: nostr.Now(),
Content: n.publishing.note,
Tags: make(nostr.Tags, 0, 2),
}
// our write relays
relays := n.root.sys.FetchWriteRelays(n.root.ctx, n.root.rootPubKey, 8)
if len(relays) == 0 {
relays = n.root.sys.FetchOutboxRelays(n.root.ctx, n.root.rootPubKey, 6)
}
// add "p" tags from people mentioned and "q" tags from events mentioned
for ref := range nip27.ParseReferences(evt) {
tag := ref.Pointer.AsTag()
key := tag[0]
val := tag[1]
if key == "e" || key == "a" {
key = "q"
}
if existing := evt.Tags.FindWithValue(key, val); existing == nil {
evt.Tags = append(evt.Tags, tag)
// add their "read" relays
if key == "p" {
for _, r := range n.root.sys.FetchInboxRelays(n.root.ctx, val, 4) {
if !slices.Contains(relays, r) {
relays = append(relays, r)
}
}
}
}
}
// sign and publish
if err := n.root.signer.SignEvent(n.root.ctx, &evt); err != nil {
log("failed to sign: %s\n", err)
return
}
log(evt.String() + "\n")
log("publishing to %d relays... ", len(relays))
success := false
first := true
for res := range n.root.sys.Pool.PublishMany(n.root.ctx, relays, evt) {
cleanUrl, _ := strings.CutPrefix(res.RelayURL, "wss://")
if !first {
log(", ")
}
first = false
if res.Error != nil {
log("%s: %s", color.RedString(cleanUrl), res.Error)
} else {
success = true
log("%s: ok", color.GreenString(cleanUrl))
}
}
log("\n")
if success {
n.RmChild("new")
n.AddChild(evt.ID, n.root.CreateEventDir(n, &evt), true)
log("event published as %s and updated locally.\n", color.BlueString(evt.ID))
}
}
func (n *ViewDir) Getattr(_ context.Context, f fs.FileHandle, out *fuse.AttrOut) syscall.Errno {
now := nostr.Now() now := nostr.Now()
if n.filter.Until != nil { if n.filter.Until != nil {
now = *n.filter.Until now = *n.filter.Until
@@ -52,31 +225,62 @@ func (n *ViewDir) Opendir(ctx context.Context) syscall.Errno {
aMonthAgo := now - 30*24*60*60 aMonthAgo := now - 30*24*60*60
n.filter.Since = &aMonthAgo 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 := n.filter
filter.Until = &aMonthAgo filter.Until = &aMonthAgo
n.AddChild("@previous", n.NewPersistentInode( n.AddChild("@previous", n.NewPersistentInode(
ctx, n.root.ctx,
&ViewDir{ &ViewDir{
ctx: n.ctx, root: n.root,
sys: n.sys,
filter: filter, filter: filter,
wd: n.wd,
relays: n.relays, relays: n.relays,
replaceable: n.replaceable,
}, },
fs.StableAttr{Mode: syscall.S_IFDIR}, fs.StableAttr{Mode: syscall.S_IFDIR},
), true) ), true)
}
if n.replaceable {
for rkey, evt := range n.root.sys.Pool.FetchManyReplaceable(n.root.ctx, n.relays, n.filter,
nostr.WithLabel("nakfs"),
).Range {
name := rkey.D
if name == "" {
name = "_"
}
if n.GetChild(name) == nil {
n.AddChild(name, n.root.CreateEntityDir(n, evt), true)
}
}
} else { } else {
for ie := range n.sys.Pool.FetchMany(ctx, n.relays, n.filter, nostr.WithLabel("nakfs")) { for ie := range n.root.sys.Pool.FetchMany(n.root.ctx, n.relays, n.filter,
basename, inode := n.create(ctx, n, ie.Event) nostr.WithLabel("nakfs"),
n.AddChild(basename, inode, true) ) {
if n.GetChild(ie.Event.ID) == nil {
n.AddChild(ie.Event.ID, n.root.CreateEventDir(n, ie.Event), true)
}
} }
} }
return fs.OK return fs.OK
} }
func (n *ViewDir) Mkdir(ctx context.Context, name string, mode uint32, out *fuse.EntryOut) (*fs.Inode, syscall.Errno) {
if !n.createable || n.root.signer == nil || n.root.rootPubKey != n.filter.Authors[0] {
return nil, syscall.ENOTSUP
}
if n.replaceable {
// create a template event that can later be modified and published as new
return n.root.CreateEntityDir(n, &nostr.Event{
PubKey: n.root.rootPubKey,
CreatedAt: 0,
Kind: n.filter.Kinds[0],
Tags: nostr.Tags{
nostr.Tag{"d", name},
},
}), fs.OK
}
return nil, syscall.ENOTSUP
}

93
nostrfs/writeablefile.go Normal file
View File

@@ -0,0 +1,93 @@
package nostrfs
import (
"context"
"sync"
"syscall"
"github.com/hanwen/go-fuse/v2/fs"
"github.com/hanwen/go-fuse/v2/fuse"
)
type WriteableFile struct {
fs.Inode
root *NostrRoot
mu sync.Mutex
data []byte
attr fuse.Attr
onWrite func(string)
}
var (
_ = (fs.NodeOpener)((*WriteableFile)(nil))
_ = (fs.NodeReader)((*WriteableFile)(nil))
_ = (fs.NodeWriter)((*WriteableFile)(nil))
_ = (fs.NodeGetattrer)((*WriteableFile)(nil))
_ = (fs.NodeSetattrer)((*WriteableFile)(nil))
_ = (fs.NodeFlusher)((*WriteableFile)(nil))
)
func (r *NostrRoot) NewWriteableFile(data string, ctime, mtime uint64, onWrite func(string)) *WriteableFile {
return &WriteableFile{
root: r,
data: []byte(data),
attr: fuse.Attr{
Mode: 0666,
Ctime: ctime,
Mtime: mtime,
Size: uint64(len(data)),
},
onWrite: onWrite,
}
}
func (f *WriteableFile) Open(ctx context.Context, flags uint32) (fh fs.FileHandle, fuseFlags uint32, errno syscall.Errno) {
return nil, fuse.FOPEN_KEEP_CACHE, fs.OK
}
func (f *WriteableFile) Write(ctx context.Context, fh fs.FileHandle, data []byte, off int64) (uint32, syscall.Errno) {
f.mu.Lock()
defer f.mu.Unlock()
offset := int(off)
end := offset + len(data)
if len(f.data) < end {
newData := make([]byte, offset+len(data))
copy(newData, f.data)
f.data = newData
}
copy(f.data[offset:], data)
f.data = f.data[0:end]
f.onWrite(string(f.data))
return uint32(len(data)), fs.OK
}
func (f *WriteableFile) Getattr(ctx context.Context, fh fs.FileHandle, out *fuse.AttrOut) syscall.Errno {
f.mu.Lock()
defer f.mu.Unlock()
out.Attr = f.attr
out.Attr.Size = uint64(len(f.data))
return fs.OK
}
func (f *WriteableFile) Setattr(_ context.Context, _ fs.FileHandle, in *fuse.SetAttrIn, _ *fuse.AttrOut) syscall.Errno {
f.attr.Mtime = in.Mtime
f.attr.Atime = in.Atime
f.attr.Ctime = in.Ctime
return fs.OK
}
func (f *WriteableFile) Flush(ctx context.Context, fh fs.FileHandle) syscall.Errno {
return fs.OK
}
func (f *WriteableFile) Read(ctx context.Context, fh fs.FileHandle, dest []byte, off int64) (fuse.ReadResult, syscall.Errno) {
f.mu.Lock()
defer f.mu.Unlock()
end := int(off) + len(dest)
if end > len(f.data) {
end = len(f.data)
}
return fuse.ReadResultData(f.data[off:end]), fs.OK
}

48
req.go
View File

@@ -6,6 +6,7 @@ import (
"os" "os"
"strings" "strings"
"github.com/fatih/color"
"github.com/mailru/easyjson" "github.com/mailru/easyjson"
"github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nip77" "github.com/nbd-wtf/go-nostr/nip77"
@@ -76,35 +77,29 @@ example:
Action: func(ctx context.Context, c *cli.Command) error { Action: func(ctx context.Context, c *cli.Command) error {
relayUrls := c.Args().Slice() relayUrls := c.Args().Slice()
if len(relayUrls) > 0 { if len(relayUrls) > 0 {
relays := connectToAllRelays(ctx, // this is used both for the normal AUTH (after "auth-required:" is received) or forced pre-auth
// connect to all relays we expect to use in this call in parallel
forcePreAuthSigner := authSigner
if !c.Bool("force-pre-auth") {
forcePreAuthSigner = nil
}
relays := connectToAllRelays(
ctx,
c,
relayUrls, relayUrls,
c.Bool("force-pre-auth"), forcePreAuthSigner,
nostr.WithAuthHandler( nostr.WithAuthHandler(func(ctx context.Context, authEvent nostr.RelayEvent) error {
func(ctx context.Context, authEvent nostr.RelayEvent) (err error) { return authSigner(ctx, c, func(s string, args ...any) {
defer func() { if strings.HasPrefix(s, "authenticating as") {
if err != nil { cleanUrl, _ := strings.CutPrefix(authEvent.Relay.URL, "wss://")
log("auth to %s failed: %s\n", s = "authenticating to " + color.CyanString(cleanUrl) + " as" + s[len("authenticating as"):]
(*authEvent.Tags.GetFirst([]string{"relay", ""}))[1], }
err, log(s+"\n", args...)
}, authEvent)
}),
) )
}
}()
if !c.Bool("auth") && !c.Bool("force-pre-auth") { // stop here already if all connections failed
return fmt.Errorf("auth not authorized")
}
kr, _, err := gatherKeyerFromArguments(ctx, c)
if err != nil {
return err
}
pk, _ := kr.GetPublicKey(ctx)
log("performing auth as %s... ", pk)
return kr.SignEvent(ctx, authEvent.Event)
},
),
)
if len(relays) == 0 { if len(relays) == 0 {
log("failed to connect to any of the given relays.\n") log("failed to connect to any of the given relays.\n")
os.Exit(3) os.Exit(3)
@@ -121,6 +116,7 @@ example:
}() }()
} }
// go line by line from stdin or run once with input from flags
for stdinFilter := range getJsonsOrBlank() { for stdinFilter := range getJsonsOrBlank() {
filter := nostr.Filter{} filter := nostr.Filter{}
if stdinFilter != "" { if stdinFilter != "" {

View File

@@ -6,7 +6,6 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/fatih/color"
"github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nip60" "github.com/nbd-wtf/go-nostr/nip60"
"github.com/nbd-wtf/go-nostr/nip61" "github.com/nbd-wtf/go-nostr/nip61"
@@ -60,20 +59,16 @@ func prepareWallet(ctx context.Context, c *cli.Command) (*nip60.Wallet, func(),
log("- saving kind:%d event (%s)... ", event.Kind, desc) log("- saving kind:%d event (%s)... ", event.Kind, desc)
first := true first := true
for res := range sys.Pool.PublishMany(ctx, relays, event) { for res := range sys.Pool.PublishMany(ctx, relays, event) {
cleanUrl, ok := strings.CutPrefix(res.RelayURL, "wss://") cleanUrl, _ := strings.CutPrefix(res.RelayURL, "wss://")
if !ok {
cleanUrl = res.RelayURL
}
if !first { if !first {
log(", ") log(", ")
} }
first = false first = false
if res.Error != nil { if res.Error != nil {
log("%s: %s", color.RedString(cleanUrl), res.Error) log("%s: %s", colors.errorf(cleanUrl), res.Error)
} else { } else {
log("%s: ok", color.GreenString(cleanUrl)) log("%s: ok", colors.successf(cleanUrl))
} }
} }
log("\n") log("\n")
@@ -376,19 +371,15 @@ var wallet = &cli.Command{
log("- publishing nutzap... ") log("- publishing nutzap... ")
first := true first := true
for res := range results { for res := range results {
cleanUrl, ok := strings.CutPrefix(res.RelayURL, "wss://") cleanUrl, _ := strings.CutPrefix(res.RelayURL, "wss://")
if !ok {
cleanUrl = res.RelayURL
}
if !first { if !first {
log(", ") log(", ")
} }
first = false first = false
if res.Error != nil { if res.Error != nil {
log("%s: %s", color.RedString(cleanUrl), res.Error) log("%s: %s", colors.errorf(cleanUrl), res.Error)
} else { } else {
log("%s: ok", color.GreenString(cleanUrl)) log("%s: ok", colors.successf(cleanUrl))
} }
} }
@@ -463,10 +454,7 @@ var wallet = &cli.Command{
log("- saving kind:10019 event... ") log("- saving kind:10019 event... ")
first := true first := true
for res := range sys.Pool.PublishMany(ctx, relays, evt) { for res := range sys.Pool.PublishMany(ctx, relays, evt) {
cleanUrl, ok := strings.CutPrefix(res.RelayURL, "wss://") cleanUrl, _ := strings.CutPrefix(res.RelayURL, "wss://")
if !ok {
cleanUrl = res.RelayURL
}
if !first { if !first {
log(", ") log(", ")
@@ -474,9 +462,9 @@ var wallet = &cli.Command{
first = false first = false
if res.Error != nil { if res.Error != nil {
log("%s: %s", color.RedString(cleanUrl), res.Error) log("%s: %s", colors.errorf(cleanUrl), res.Error)
} else { } else {
log("%s: ok", color.GreenString(cleanUrl)) log("%s: ok", colors.successf(cleanUrl))
} }
} }