mirror of
https://github.com/fiatjaf/nak.git
synced 2025-07-29 16:28:29 -04:00
fs: publishing new notes by writing to ./notes/new
This commit is contained in:
24
fs.go
24
fs.go
@@ -34,9 +34,15 @@ var fsCmd = &cli.Command{
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
&cli.DurationFlag{
|
&cli.DurationFlag{
|
||||||
Name: "auto-publish",
|
Name: "auto-publish-notes",
|
||||||
Usage: "delay after which edited articles will be auto-published.",
|
Usage: "delay after which new notes will be auto-published, set to -1 to not publish.",
|
||||||
Value: time.Hour * 24 * 365 * 2,
|
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,
|
||||||
@@ -53,6 +59,15 @@ var fsCmd = &cli.Command{
|
|||||||
kr = keyer.NewReadOnlyUser(c.String("pubkey"))
|
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(
|
||||||
context.WithValue(
|
context.WithValue(
|
||||||
@@ -65,7 +80,8 @@ var fsCmd = &cli.Command{
|
|||||||
kr,
|
kr,
|
||||||
mountpoint,
|
mountpoint,
|
||||||
nostrfs.Options{
|
nostrfs.Options{
|
||||||
AutoPublishTimeout: c.Duration("auto-publish"),
|
AutoPublishNotesTimeout: apnt,
|
||||||
|
AutoPublishArticlesTimeout: apat,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
4
go.mod
4
go.mod
@@ -17,7 +17,7 @@ require (
|
|||||||
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.3-0.20250312034958-cc23d81e8055
|
github.com/nbd-wtf/go-nostr v0.51.5
|
||||||
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-20250305212735-054e65f0b394
|
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/sys v0.31.0 // indirect
|
||||||
golang.org/x/text v0.23.0 // indirect
|
golang.org/x/text v0.23.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
replace github.com/nbd-wtf/go-nostr => ../go-nostr
|
|
||||||
|
2
go.sum
2
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/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.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/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=
|
||||||
|
@@ -269,17 +269,17 @@ func (e *EntityDir) handleWrite() {
|
|||||||
log := e.root.ctx.Value("log").(func(msg string, args ...any))
|
log := e.root.ctx.Value("log").(func(msg string, args ...any))
|
||||||
logverbose := e.root.ctx.Value("logverbose").(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() {
|
if e.publisher.IsRunning() {
|
||||||
log(", timer reset")
|
log(", timer reset")
|
||||||
}
|
}
|
||||||
log(", will publish the ")
|
log(", publishing the ")
|
||||||
if e.IsNew() {
|
if e.IsNew() {
|
||||||
log("new")
|
log("new")
|
||||||
} else {
|
} else {
|
||||||
log("updated")
|
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 {
|
} else {
|
||||||
log(".\n")
|
log(".\n")
|
||||||
}
|
}
|
||||||
@@ -339,9 +339,9 @@ func (e *EntityDir) handleWrite() {
|
|||||||
}
|
}
|
||||||
logverbose("%s\n", evt)
|
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 {
|
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))
|
log("publishing to %d relays... ", len(relays))
|
||||||
@@ -398,7 +398,7 @@ func (r *NostrRoot) CreateEntityDir(
|
|||||||
) *fs.Inode {
|
) *fs.Inode {
|
||||||
return parent.EmbeddedInode().NewPersistentInode(
|
return parent.EmbeddedInode().NewPersistentInode(
|
||||||
r.ctx,
|
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},
|
fs.StableAttr{Mode: syscall.S_IFDIR},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@@ -122,6 +122,7 @@ func (h *NpubDir) OnAdd(_ context.Context) {
|
|||||||
paginate: true,
|
paginate: true,
|
||||||
relays: relays,
|
relays: relays,
|
||||||
replaceable: false,
|
replaceable: false,
|
||||||
|
createable: true,
|
||||||
},
|
},
|
||||||
fs.StableAttr{Mode: syscall.S_IFDIR},
|
fs.StableAttr{Mode: syscall.S_IFDIR},
|
||||||
),
|
),
|
||||||
|
@@ -15,7 +15,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Options struct {
|
type Options struct {
|
||||||
AutoPublishTimeout time.Duration // a negative number means do not publish
|
AutoPublishNotesTimeout time.Duration
|
||||||
|
AutoPublishArticlesTimeout time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
type NostrRoot struct {
|
type NostrRoot struct {
|
||||||
|
@@ -2,12 +2,17 @@ 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"
|
||||||
|
"github.com/nbd-wtf/go-nostr/nip27"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ViewDir struct {
|
type ViewDir struct {
|
||||||
@@ -19,14 +24,187 @@ type ViewDir struct {
|
|||||||
relays []string
|
relays []string
|
||||||
replaceable bool
|
replaceable bool
|
||||||
createable 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.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 {
|
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 {
|
||||||
|
@@ -71,7 +71,10 @@ func (f *WriteableFile) Getattr(ctx context.Context, fh fs.FileHandle, out *fuse
|
|||||||
return fs.OK
|
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
|
return fs.OK
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user