diff --git a/fs.go b/fs.go index 1d8d048..6750a93 100644 --- a/fs.go +++ b/fs.go @@ -34,9 +34,15 @@ var fsCmd = &cli.Command{ }, }, &cli.DurationFlag{ - Name: "auto-publish", - Usage: "delay after which edited articles will be auto-published.", - Value: time.Hour * 24 * 365 * 2, + 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, @@ -53,6 +59,15 @@ var fsCmd = &cli.Command{ 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( context.WithValue( context.WithValue( @@ -65,7 +80,8 @@ var fsCmd = &cli.Command{ kr, mountpoint, nostrfs.Options{ - AutoPublishTimeout: c.Duration("auto-publish"), + AutoPublishNotesTimeout: apnt, + AutoPublishArticlesTimeout: apat, }, ) diff --git a/go.mod b/go.mod index 10cdd41..7a5cb32 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,7 @@ require ( github.com/mailru/easyjson v0.9.0 github.com/mark3labs/mcp-go v0.8.3 github.com/markusmobius/go-dateparser v1.2.3 - github.com/nbd-wtf/go-nostr v0.51.3-0.20250312034958-cc23d81e8055 + github.com/nbd-wtf/go-nostr v0.51.5 github.com/urfave/cli/v3 v3.0.0-beta1 golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 ) @@ -75,5 +75,3 @@ require ( golang.org/x/sys v0.31.0 // indirect golang.org/x/text v0.23.0 // indirect ) - -replace github.com/nbd-wtf/go-nostr => ../go-nostr diff --git a/go.sum b/go.sum index 2885144..c39a094 100644 --- a/go.sum +++ b/go.sum @@ -151,6 +151,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/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/nbd-wtf/go-nostr v0.51.5 h1:kztpm/JuavVefyuEjG0QaCgDtzHIW9K/Hzq+y9Ph2DY= +github.com/nbd-wtf/go-nostr v0.51.5/go.mod h1:raIUNOilCdhiVIqgwe+9enCtdXu1iuPjbLh1hO7wTqI= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= diff --git a/nostrfs/entitydir.go b/nostrfs/entitydir.go index bfdd275..852a53f 100644 --- a/nostrfs/entitydir.go +++ b/nostrfs/entitydir.go @@ -269,17 +269,17 @@ 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.AutoPublishTimeout.Hours() < 24*365 { + if e.root.opts.AutoPublishArticlesTimeout.Hours() < 24*365 { if e.publisher.IsRunning() { log(", timer reset") } - log(", will publish the ") + log(", publishing the ") if e.IsNew() { log("new") } else { log("updated") } - log(" event in %d seconds...\n", e.root.opts.AutoPublishTimeout.Seconds()) + log(" event in %d seconds...\n", int(e.root.opts.AutoPublishArticlesTimeout.Seconds())) } else { log(".\n") } @@ -339,9 +339,9 @@ func (e *EntityDir) handleWrite() { } logverbose("%s\n", evt) - relays := e.root.sys.FetchWriteRelays(e.root.ctx, evt.PubKey, 8) + relays := e.root.sys.FetchWriteRelays(e.root.ctx, e.root.rootPubKey, 8) if len(relays) == 0 { - relays = e.root.sys.FetchOutboxRelays(e.root.ctx, evt.PubKey, 6) + relays = e.root.sys.FetchOutboxRelays(e.root.ctx, e.root.rootPubKey, 6) } log("publishing to %d relays... ", len(relays)) @@ -398,7 +398,7 @@ func (r *NostrRoot) CreateEntityDir( ) *fs.Inode { return parent.EmbeddedInode().NewPersistentInode( r.ctx, - &EntityDir{root: r, event: event, publisher: debouncer.New(r.opts.AutoPublishTimeout)}, + &EntityDir{root: r, event: event, publisher: debouncer.New(r.opts.AutoPublishArticlesTimeout)}, fs.StableAttr{Mode: syscall.S_IFDIR}, ) } diff --git a/nostrfs/npubdir.go b/nostrfs/npubdir.go index b721002..d34a226 100644 --- a/nostrfs/npubdir.go +++ b/nostrfs/npubdir.go @@ -122,6 +122,7 @@ func (h *NpubDir) OnAdd(_ context.Context) { paginate: true, relays: relays, replaceable: false, + createable: true, }, fs.StableAttr{Mode: syscall.S_IFDIR}, ), diff --git a/nostrfs/root.go b/nostrfs/root.go index 510969c..d821bc8 100644 --- a/nostrfs/root.go +++ b/nostrfs/root.go @@ -15,7 +15,8 @@ import ( ) type Options struct { - AutoPublishTimeout time.Duration // a negative number means do not publish + AutoPublishNotesTimeout time.Duration + AutoPublishArticlesTimeout time.Duration } type NostrRoot struct { diff --git a/nostrfs/viewdir.go b/nostrfs/viewdir.go index 838ab05..b861290 100644 --- a/nostrfs/viewdir.go +++ b/nostrfs/viewdir.go @@ -2,12 +2,17 @@ package nostrfs import ( "context" + "slices" + "strings" "sync/atomic" "syscall" + "fiatjaf.com/lib/debouncer" + "github.com/fatih/color" "github.com/hanwen/go-fuse/v2/fs" "github.com/hanwen/go-fuse/v2/fuse" "github.com/nbd-wtf/go-nostr" + "github.com/nbd-wtf/go-nostr/nip27" ) type ViewDir struct { @@ -19,14 +24,187 @@ type ViewDir struct { relays []string replaceable bool createable bool + publisher *debouncer.Debouncer + publishing struct { + note string + } } var ( _ = (fs.NodeOpendirer)((*ViewDir)(nil)) _ = (fs.NodeGetattrer)((*ViewDir)(nil)) _ = (fs.NodeMkdirer)((*ViewDir)(nil)) + _ = (fs.NodeSetattrer)((*ViewDir)(nil)) + _ = (fs.NodeCreater)((*ViewDir)(nil)) + _ = (fs.NodeUnlinker)((*ViewDir)(nil)) ) +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, ok := strings.CutPrefix(res.RelayURL, "wss://") + if !ok { + cleanUrl = res.RelayURL + } + + if !first { + log(", ") + } + first = false + + if res.Error != nil { + log("%s: %s", color.RedString(cleanUrl), res.Error) + } else { + 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() if n.filter.Until != nil { diff --git a/nostrfs/writeablefile.go b/nostrfs/writeablefile.go index c37291a..b6ca0a9 100644 --- a/nostrfs/writeablefile.go +++ b/nostrfs/writeablefile.go @@ -71,7 +71,10 @@ func (f *WriteableFile) Getattr(ctx context.Context, fh fs.FileHandle, out *fuse return fs.OK } -func (f *WriteableFile) Setattr(_ context.Context, _ fs.FileHandle, _ *fuse.SetAttrIn, _ *fuse.AttrOut) syscall.Errno { +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 }