diff --git a/go.mod b/go.mod index f95f2b2..10cdd41 100644 --- a/go.mod +++ b/go.mod @@ -3,26 +3,26 @@ module github.com/fiatjaf/nak go 1.24.1 require ( + fiatjaf.com/lib v0.3.1 github.com/bep/debounce v1.2.1 github.com/btcsuite/btcd/btcec/v2 v2.3.4 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/fiatjaf/eventstore v0.15.0 - github.com/fiatjaf/khatru v0.16.0 + github.com/fiatjaf/eventstore v0.16.2 + github.com/fiatjaf/khatru v0.17.3-0.20250312035319-596bca93c3ff github.com/hanwen/go-fuse/v2 v2.7.2 github.com/json-iterator/go v1.1.12 github.com/liamg/magic v0.0.1 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.2 + github.com/nbd-wtf/go-nostr v0.51.3-0.20250312034958-cc23d81e8055 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 ) require ( - fiatjaf.com/lib v0.2.0 // indirect github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3 // indirect github.com/andybalholm/brotli v1.1.1 // indirect github.com/btcsuite/btcd v0.24.2 // indirect @@ -57,7 +57,7 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/puzpuzpuz/xsync/v3 v3.5.0 // indirect + github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect github.com/rs/cors v1.11.1 // indirect github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 // indirect github.com/tetratelabs/wazero v1.8.0 // indirect @@ -66,12 +66,14 @@ require ( github.com/tidwall/pretty v1.2.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/valyala/fasthttp v1.58.0 // indirect + github.com/valyala/fasthttp v1.59.0 // indirect github.com/wasilibs/go-re2 v1.3.0 // indirect github.com/x448/float16 v0.8.4 // indirect golang.org/x/arch v0.15.0 // indirect - golang.org/x/crypto v0.32.0 // indirect - golang.org/x/net v0.34.0 // indirect + golang.org/x/crypto v0.36.0 // indirect + golang.org/x/net v0.37.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 ) + +replace github.com/nbd-wtf/go-nostr => ../go-nostr diff --git a/go.sum b/go.sum index dc27658..2885144 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -fiatjaf.com/lib v0.2.0 h1:TgIJESbbND6GjOgGHxF5jsO6EMjuAxIzZHPo5DXYexs= -fiatjaf.com/lib v0.2.0/go.mod h1:Ycqq3+mJ9jAWu7XjbQI1cVr+OFgnHn79dQR5oTII47g= +fiatjaf.com/lib v0.3.1 h1:/oFQwNtFRfV+ukmOCxfBEAuayoLwXp4wu2/fz5iHpwA= +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/go.mod h1:we0YA5CsBbH5+/NUzC/AlMmxaDtWlXeNsqrwXjTzmzA= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= @@ -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/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.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg= -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 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= github.com/dgraph-io/ristretto v1.0.0 h1:SYG07bONKMlFDUYu5pEu3DGAh8c2OFNzKm6G9J4Si84= 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/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= 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.15.0/go.mod h1:KAsld5BhkmSck48aF11Txu8X+OGNmoabw4TlYVWqInc= -github.com/fiatjaf/khatru v0.16.0 h1:xgGwnnOqE3989wEWm7c/z6Y6g4X92BFe/Xp1UWQ3Zmc= -github.com/fiatjaf/khatru v0.16.0/go.mod h1:TLcMgPy3IAPh40VGYq6m+gxEMpDKHj+sumqcuvbSogc= +github.com/fiatjaf/eventstore v0.16.2 h1:h4rHwSwPcqAKqWUsAbYWUhDeSgm2Kp+PBkJc3FgBYu4= +github.com/fiatjaf/eventstore v0.16.2/go.mod h1:0gU8fzYO/bG+NQAVlHtJWOlt3JKKFefh5Xjj2d1dLIs= +github.com/fiatjaf/khatru v0.17.3-0.20250312035319-596bca93c3ff h1:b6LYwWlc8zAW6aoZpXYC3Gx/zkP4XW5amDx0VwyeREs= +github.com/fiatjaf/khatru v0.17.3-0.20250312035319-596bca93c3ff/go.mod h1:dAaXV6QZwuMVYlXQigp/0Uyl/m1nKOhtRssjQYsgMu0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= @@ -151,8 +151,6 @@ 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.2 h1:wQysG8omkF4LO7kcU6yoeCBBxD92SwUNab4TMeSuZZM= -github.com/nbd-wtf/go-nostr v0.51.2/go.mod h1:9PcGOZ+e1VOaLvcK0peT4dbip+/eS+eTWXR3HuexQrA= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/puzpuzpuz/xsync/v3 v3.5.0 h1:i+cMcpEDY1BkNm7lPDkCtE4oElsYLn+EKF8kAu2vXT4= -github.com/puzpuzpuz/xsync/v3 v3.5.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= +github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg= +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/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= 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/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasthttp v1.58.0 h1:GGB2dWxSbEprU9j0iMJHgdKYJVDyjrOwF9RE59PbRuE= -github.com/valyala/fasthttp v1.58.0/go.mod h1:SYXvHHaFp7QZHGKSHmoMipInhrI5StHrhDTYVEjK/Kw= +github.com/valyala/fasthttp v1.59.0 h1:Qu0qYHfXvPk1mSLNqcFtEk6DpxgA26hy6bmydotDpRI= +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/go.mod h1:AafrCXVvGRJJOImMajgJ2M7rVmWyisVK7sFshbxnVrg= 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-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= -golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= -golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac h1:l5+whBCLH3iH2ZNHYLbAe58bo7yrN4mVcnkHDYz5vvs= -golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac/go.mod h1:hH+7mtFmImwwcMvScyxUhjuVHR3HGaDPMn9rMSUUbxo= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= +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-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-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.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= -golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= +golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= +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/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= @@ -242,8 +240,8 @@ golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +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/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= diff --git a/nostrfs/deterministicfile.go b/nostrfs/deterministicfile.go new file mode 100644 index 0000000..95fe030 --- /dev/null +++ b/nostrfs/deterministicfile.go @@ -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 +} diff --git a/nostrfs/entitydir.go b/nostrfs/entitydir.go index c9411bf..9a242aa 100644 --- a/nostrfs/entitydir.go +++ b/nostrfs/entitydir.go @@ -9,9 +9,13 @@ import ( "net/http" "path/filepath" "strconv" + "strings" "syscall" "time" + "unsafe" + "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" @@ -23,18 +27,29 @@ import ( type EntityDir struct { fs.Inode - ctx context.Context - wd string - evt *nostr.Event + root *NostrRoot + + publisher *debouncer.Debouncer + extension string + event *nostr.Event + updating struct { + title string + content string + } } -var _ = (fs.NodeGetattrer)((*EntityDir)(nil)) +var ( + _ = (fs.NodeOnAdder)((*EntityDir)(nil)) + _ = (fs.NodeGetattrer)((*EntityDir)(nil)) + _ = (fs.NodeCreater)((*EntityDir)(nil)) + _ = (fs.NodeUnlinker)((*EntityDir)(nil)) +) func (e *EntityDir) Getattr(_ context.Context, f fs.FileHandle, out *fuse.AttrOut) syscall.Errno { - publishedAt := uint64(e.evt.CreatedAt) + publishedAt := uint64(e.event.CreatedAt) out.Ctime = publishedAt - if tag := e.evt.Tags.Find("published_at"); tag != nil { + if tag := e.event.Tags.Find("published_at"); tag != nil { publishedAt, _ = strconv.ParseUint(tag[1], 10, 64) } out.Mtime = publishedAt @@ -42,119 +57,147 @@ func (e *EntityDir) Getattr(_ context.Context, f fs.FileHandle, out *fuse.AttrOu return fs.OK } -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) +func (e *EntityDir) Create( + _ context.Context, + name string, + flags uint32, + mode uint32, + out *fuse.EntryOut, +) (node *fs.Inode, fh fs.FileHandle, fuseFlags uint32, errno syscall.Errno) { + if name == "publish" { + // this causes the publish process to be triggered faster + e.publisher.Flush() + return nil, nil, 0, syscall.ENOTDIR } - return r.CreateEntityDir(parent, extension, event), nil + return nil, nil, 0, syscall.ENOTSUP } -func (r *NostrRoot) CreateEntityDir( - parent fs.InodeEmbedder, - extension string, - event *nostr.Event, -) *fs.Inode { - log := r.ctx.Value("log").(func(msg string, args ...any)) +func (e *EntityDir) Unlink(ctx context.Context, name string) syscall.Errno { + switch name { + case "content" + e.extension: + e.updating.content = e.event.Content + return syscall.ENOTDIR + case "title": + e.updating.title = "" + if titleTag := e.event.Tags.Find("title"); titleTag != nil { + e.updating.title = titleTag[1] + } + return syscall.ENOTDIR + default: + return syscall.EINTR + } +} - h := parent.EmbeddedInode().NewPersistentInode( - r.ctx, - &EntityDir{ctx: r.ctx, wd: r.wd, evt: event}, - fs.StableAttr{Mode: syscall.S_IFDIR, Ino: hexToUint64(event.ID)}, - ) - - var publishedAt uint64 - if tag := event.Tags.Find("published_at"); tag != nil { +func (e *EntityDir) OnAdd(_ context.Context) { + log := e.root.ctx.Value("log").(func(msg string, args ...any)) + publishedAt := uint64(e.event.CreatedAt) + if tag := e.event.Tags.Find("published_at"); tag != nil { publishedAt, _ = strconv.ParseUint(tag[1], 10, 64) } - npub, _ := nip19.EncodePublicKey(event.PubKey) - h.AddChild("@author", h.NewPersistentInode( - r.ctx, + npub, _ := nip19.EncodePublicKey(e.event.PubKey) + e.AddChild("@author", e.NewPersistentInode( + e.root.ctx, &fs.MemSymlink{ - Data: []byte(r.wd + "/" + npub), + Data: []byte(e.root.wd + "/" + npub), }, fs.StableAttr{Mode: syscall.S_IFLNK}, ), true) - eventj, _ := json.MarshalIndent(event, "", " ") - h.AddChild("event.json", h.NewPersistentInode( - r.ctx, - &fs.MemRegularFile{ - Data: eventj, - Attr: fuse.Attr{ - Mode: 0444, - Ctime: uint64(event.CreatedAt), - Mtime: uint64(publishedAt), - Size: uint64(len(event.Content)), + e.AddChild("event.json", e.NewPersistentInode( + e.root.ctx, + &DeterministicFile{ + get: func() (ctime uint64, mtime uint64, data string) { + eventj, _ := json.MarshalIndent(e.event, "", " ") + return uint64(e.event.CreatedAt), + uint64(e.event.CreatedAt), + unsafe.String(unsafe.SliceData(eventj), len(eventj)) }, }, fs.StableAttr{}, ), true) - h.AddChild("identifier", h.NewPersistentInode( - r.ctx, + e.AddChild("identifier", e.NewPersistentInode( + e.root.ctx, &fs.MemRegularFile{ - Data: []byte(event.Tags.GetD()), + Data: []byte(e.event.Tags.GetD()), Attr: fuse.Attr{ Mode: 0444, - Ctime: uint64(event.CreatedAt), - Mtime: uint64(publishedAt), - Size: uint64(len(event.Tags.GetD())), + Ctime: uint64(e.event.CreatedAt), + Mtime: uint64(e.event.CreatedAt), + Size: uint64(len(e.event.Tags.GetD())), }, }, fs.StableAttr{}, ), true) - if tag := event.Tags.Find("title"); tag != nil { - h.AddChild("title", h.NewPersistentInode( - r.ctx, - &fs.MemRegularFile{ - Data: []byte(tag[1]), - Attr: fuse.Attr{ - Mode: 0444, - Ctime: uint64(event.CreatedAt), - Mtime: uint64(publishedAt), - Size: uint64(len(tag[1])), + if e.root.signer == nil { + // read-only + e.AddChild("title", e.NewPersistentInode( + e.root.ctx, + &DeterministicFile{ + get: func() (ctime uint64, mtime uint64, data string) { + var title string + if tag := e.event.Tags.Find("title"); tag != nil { + title = tag[1] + } else { + title = e.event.Tags.GetD() + } + return uint64(e.event.CreatedAt), publishedAt, title }, }, fs.StableAttr{}, ), true) - } - - h.AddChild("content"+extension, h.NewPersistentInode( - r.ctx, - &fs.MemRegularFile{ - Data: []byte(event.Content), - Attr: fuse.Attr{ - Mode: 0444, - Ctime: uint64(event.CreatedAt), - Mtime: uint64(publishedAt), - Size: uint64(len(event.Content)), + e.AddChild("content."+e.extension, e.NewPersistentInode( + e.root.ctx, + &DeterministicFile{ + get: func() (ctime uint64, mtime uint64, data string) { + return uint64(e.event.CreatedAt), publishedAt, e.event.Content + }, }, - }, - fs.StableAttr{}, - ), true) + fs.StableAttr{}, + ), true) + } else { + // writeable + if tag := e.event.Tags.Find("title"); tag != nil { + e.updating.title = tag[1] + } + e.updating.content = e.event.Content + + e.AddChild("title", e.NewPersistentInode( + e.root.ctx, + e.root.NewWriteableFile(e.updating.title, uint64(e.event.CreatedAt), publishedAt, func(s string) { + log("title updated") + e.updating.title = strings.TrimSpace(s) + e.handleWrite() + }), + fs.StableAttr{}, + ), true) + + e.AddChild("content."+e.extension, e.NewPersistentInode( + e.root.ctx, + e.root.NewWriteableFile(e.updating.content, uint64(e.event.CreatedAt), publishedAt, func(s string) { + log("content updated") + e.updating.content = strings.TrimSpace(s) + e.handleWrite() + }), + fs.StableAttr{}, + ), true) + } var refsdir *fs.Inode i := 0 - for ref := range nip27.ParseReferences(*event) { + for ref := range nip27.ParseReferences(*e.event) { i++ if refsdir == nil { - refsdir = h.NewPersistentInode(r.ctx, &fs.Inode{}, fs.StableAttr{Mode: syscall.S_IFDIR}) - h.AddChild("references", refsdir, true) + refsdir = e.NewPersistentInode(e.root.ctx, &fs.Inode{}, fs.StableAttr{Mode: syscall.S_IFDIR}) + e.root.AddChild("references", refsdir, true) } refsdir.AddChild(fmt.Sprintf("ref_%02d", i), refsdir.NewPersistentInode( - r.ctx, + e.root.ctx, &fs.MemSymlink{ - Data: []byte(r.wd + "/" + nip19.EncodePointer(ref.Pointer)), + Data: []byte(e.root.wd + "/" + nip19.EncodePointer(ref.Pointer)), }, fs.StableAttr{Mode: syscall.S_IFLNK}, ), true) @@ -164,15 +207,15 @@ func (r *NostrRoot) CreateEntityDir( addImage := func(url string) { if imagesdir == nil { in := &fs.Inode{} - imagesdir = h.NewPersistentInode(r.ctx, in, fs.StableAttr{Mode: syscall.S_IFDIR}) - h.AddChild("images", imagesdir, true) + imagesdir = e.NewPersistentInode(e.root.ctx, in, fs.StableAttr{Mode: syscall.S_IFDIR}) + e.AddChild("images", imagesdir, true) } imagesdir.AddChild(filepath.Base(url), imagesdir.NewPersistentInode( - r.ctx, + e.root.ctx, &AsyncFile{ - ctx: r.ctx, + ctx: e.root.ctx, load: func() ([]byte, nostr.Timestamp) { - ctx, cancel := context.WithTimeout(r.ctx, time.Second*20) + ctx, cancel := context.WithTimeout(e.root.ctx, time.Second*20) defer cancel() r, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { @@ -198,7 +241,7 @@ func (r *NostrRoot) CreateEntityDir( ), true) } - images := nip92.ParseTags(event.Tags) + images := nip92.ParseTags(e.event.Tags) for _, imeta := range images { if imeta.URL == "" { continue @@ -206,9 +249,116 @@ func (r *NostrRoot) CreateEntityDir( addImage(imeta.URL) } - if tag := event.Tags.Find("image"); tag != nil { + if tag := e.event.Tags.Find("image"); tag != nil { addImage(tag[1]) } - - return h +} + +func (e *EntityDir) handleWrite() { + log := e.root.ctx.Value("log").(func(msg string, args ...any)) + + if e.publisher.IsRunning() { + log(", timer reset") + } + log(", will publish the updated event in 30 seconds...\n") + if !e.publisher.IsRunning() { + log("- `touch publish` to publish immediately\n") + log("- `rm title content." + e.extension + "` to erase and cancel the edits\n") + } + + e.publisher.Call(func() { + if currentTitle := e.event.Tags.Find("title"); (currentTitle != nil && currentTitle[1] == e.updating.title) || (currentTitle == nil && e.updating.title == "") && e.updating.content == e.event.Content { + log("back into the previous state, not publishing.\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) + 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}) + } + } + if publishedAtTag := evt.Tags.Find("published_at"); publishedAtTag == nil { + evt.Tags = append(evt.Tags, nostr.Tag{ + "published_at", + strconv.FormatInt(int64(e.event.CreatedAt), 10), + }) + } + for ref := range nip27.ParseReferences(evt) { + tag := ref.Pointer.AsTag() + if existing := evt.Tags.FindWithValue(tag[0], tag[1]); existing == nil { + evt.Tags = append(evt.Tags, tag) + } + } + if err := e.root.signer.SignEvent(e.root.ctx, &evt); err != nil { + log("failed to sign: '%s'.\n", err) + return + } + + relays := e.root.sys.FetchWriteRelays(e.root.ctx, evt.PubKey, 8) + 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, 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 { + e.event = &evt + log("event updated locally.\n") + } 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, extension, event), nil +} + +func (r *NostrRoot) CreateEntityDir( + parent fs.InodeEmbedder, + extension string, + event *nostr.Event, +) *fs.Inode { + return parent.EmbeddedInode().NewPersistentInode( + r.ctx, + &EntityDir{root: r, event: event, publisher: debouncer.New(time.Second * 30), extension: extension}, + fs.StableAttr{Mode: syscall.S_IFDIR, Ino: hexToUint64(event.ID)}, + ) } diff --git a/nostrfs/npubdir.go b/nostrfs/npubdir.go index 20df761..633137e 100644 --- a/nostrfs/npubdir.go +++ b/nostrfs/npubdir.go @@ -108,11 +108,10 @@ func (r *NostrRoot) CreateNpubDir( Kinds: []int{1}, Authors: []string{pointer.PublicKey}, }, - paginate: true, - relays: relays, - create: func(n *ViewDir, event *nostr.Event) (string, *fs.Inode) { - return event.ID, r.CreateEventDir(n, event) - }, + paginate: true, + relays: relays, + replaceable: false, + extension: "txt", }, fs.StableAttr{Mode: syscall.S_IFDIR}, ), @@ -129,11 +128,10 @@ func (r *NostrRoot) CreateNpubDir( Kinds: []int{1111}, Authors: []string{pointer.PublicKey}, }, - paginate: true, - relays: relays, - create: func(n *ViewDir, event *nostr.Event) (string, *fs.Inode) { - return event.ID, r.CreateEventDir(n, event) - }, + paginate: true, + relays: relays, + replaceable: false, + extension: "txt", }, fs.StableAttr{Mode: syscall.S_IFDIR}, ), @@ -150,11 +148,10 @@ func (r *NostrRoot) CreateNpubDir( Kinds: []int{20}, Authors: []string{pointer.PublicKey}, }, - paginate: true, - relays: relays, - create: func(n *ViewDir, event *nostr.Event) (string, *fs.Inode) { - return event.ID, r.CreateEventDir(n, event) - }, + paginate: true, + relays: relays, + replaceable: false, + extension: "txt", }, fs.StableAttr{Mode: syscall.S_IFDIR}, ), @@ -171,11 +168,10 @@ func (r *NostrRoot) CreateNpubDir( Kinds: []int{21, 22}, Authors: []string{pointer.PublicKey}, }, - paginate: false, - relays: relays, - create: func(n *ViewDir, event *nostr.Event) (string, *fs.Inode) { - return event.ID, r.CreateEventDir(n, event) - }, + paginate: false, + relays: relays, + replaceable: false, + extension: "txt", }, fs.StableAttr{Mode: syscall.S_IFDIR}, ), @@ -192,11 +188,10 @@ func (r *NostrRoot) CreateNpubDir( Kinds: []int{9802}, Authors: []string{pointer.PublicKey}, }, - paginate: false, - relays: relays, - create: func(n *ViewDir, event *nostr.Event) (string, *fs.Inode) { - return event.ID, r.CreateEventDir(n, event) - }, + paginate: false, + relays: relays, + replaceable: false, + extension: "txt", }, fs.StableAttr{Mode: syscall.S_IFDIR}, ), @@ -213,15 +208,10 @@ func (r *NostrRoot) CreateNpubDir( Kinds: []int{30023}, Authors: []string{pointer.PublicKey}, }, - paginate: false, - relays: relays, - create: func(n *ViewDir, event *nostr.Event) (string, *fs.Inode) { - d := event.Tags.GetD() - if d == "" { - d = "_" - } - return d, r.CreateEntityDir(n, ".md", event) - }, + paginate: false, + relays: relays, + replaceable: true, + extension: "md", }, fs.StableAttr{Mode: syscall.S_IFDIR}, ), @@ -238,15 +228,10 @@ func (r *NostrRoot) CreateNpubDir( Kinds: []int{30818}, Authors: []string{pointer.PublicKey}, }, - paginate: false, - relays: relays, - create: func(n *ViewDir, event *nostr.Event) (string, *fs.Inode) { - d := event.Tags.GetD() - if d == "" { - d = "_" - } - return d, r.CreateEntityDir(n, ".adoc", event) - }, + paginate: false, + relays: relays, + replaceable: true, + extension: "adoc", }, fs.StableAttr{Mode: syscall.S_IFDIR}, ), diff --git a/nostrfs/viewdir.go b/nostrfs/viewdir.go index 1a45e91..b4b94a1 100644 --- a/nostrfs/viewdir.go +++ b/nostrfs/viewdir.go @@ -12,12 +12,13 @@ import ( type ViewDir struct { fs.Inode - root *NostrRoot - fetched atomic.Bool - filter nostr.Filter - paginate bool - relays []string - create func(*ViewDir, *nostr.Event) (string, *fs.Inode) + root *NostrRoot + fetched atomic.Bool + filter nostr.Filter + paginate bool + relays []string + replaceable bool + extension string } var ( @@ -49,27 +50,37 @@ func (n *ViewDir) Opendir(_ context.Context) syscall.Errno { aMonthAgo := now - 30*24*60*60 n.filter.Since = &aMonthAgo - for ie := range n.root.sys.Pool.FetchMany(n.root.ctx, n.relays, n.filter, nostr.WithLabel("nakfs")) { - basename, inode := n.create(n, ie.Event) - n.AddChild(basename, inode, true) - } - filter := n.filter filter.Until = &aMonthAgo n.AddChild("@previous", n.NewPersistentInode( n.root.ctx, &ViewDir{ - root: n.root, - filter: filter, - relays: n.relays, + root: n.root, + filter: filter, + relays: n.relays, + extension: n.extension, + replaceable: n.replaceable, }, fs.StableAttr{Mode: syscall.S_IFDIR}, ), 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 = "_" + } + n.AddChild(name, n.root.CreateEntityDir(n, n.extension, evt), true) + } } else { - for ie := range n.root.sys.Pool.FetchMany(n.root.ctx, n.relays, n.filter, nostr.WithLabel("nakfs")) { - basename, inode := n.create(n, ie.Event) - n.AddChild(basename, inode, true) + for ie := range n.root.sys.Pool.FetchMany(n.root.ctx, n.relays, n.filter, + nostr.WithLabel("nakfs"), + ) { + n.AddChild(ie.Event.ID, n.root.CreateEventDir(n, ie.Event), true) } } diff --git a/nostrfs/writeablefile.go b/nostrfs/writeablefile.go new file mode 100644 index 0000000..2c897fb --- /dev/null +++ b/nostrfs/writeablefile.go @@ -0,0 +1,88 @@ +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() + end := int64(len(data)) + off + if int64(len(f.data)) < end { + n := make([]byte, end) + copy(n, f.data) + f.data = n + } + copy(f.data[off:off+int64(len(data))], data) + + 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(ctx context.Context, fh fs.FileHandle, in *fuse.SetAttrIn, out *fuse.AttrOut) syscall.Errno { + 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 +}