1
0
mirror of https://github.com/fiatjaf/nak.git synced 2025-07-29 08:18:28 -04:00

fs: editable articles and wiki.

This commit is contained in:
fiatjaf
2025-03-12 08:03:10 -03:00
parent c87371208e
commit 931da4b0ae
7 changed files with 466 additions and 182 deletions

24
go.mod

@@ -3,26 +3,26 @@ 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.3-0.20250312035319-596bca93c3ff
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.3-0.20250312034958-cc23d81e8055
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
) )
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
@@ -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,14 @@ 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
) )
replace github.com/nbd-wtf/go-nostr => ../go-nostr

42
go.sum

@@ -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=
@@ -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.3-0.20250312035319-596bca93c3ff h1:b6LYwWlc8zAW6aoZpXYC3Gx/zkP4XW5amDx0VwyeREs=
github.com/fiatjaf/khatru v0.16.0/go.mod h1:TLcMgPy3IAPh40VGYq6m+gxEMpDKHj+sumqcuvbSogc= 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.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=
@@ -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/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.2/go.mod h1:9PcGOZ+e1VOaLvcK0peT4dbip+/eS+eTWXR3HuexQrA=
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=
@@ -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.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=

@@ -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
}

@@ -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,18 +27,29 @@ import (
type EntityDir struct { type EntityDir struct {
fs.Inode fs.Inode
ctx context.Context root *NostrRoot
wd string
evt *nostr.Event 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 { 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 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) publishedAt, _ = strconv.ParseUint(tag[1], 10, 64)
} }
out.Mtime = publishedAt out.Mtime = publishedAt
@@ -42,119 +57,147 @@ func (e *EntityDir) Getattr(_ context.Context, f fs.FileHandle, out *fuse.AttrOu
return fs.OK return fs.OK
} }
func (r *NostrRoot) FetchAndCreateEntityDir( func (e *EntityDir) Create(
parent fs.InodeEmbedder, _ context.Context,
extension string, name string,
pointer nostr.EntityPointer, flags uint32,
) (*fs.Inode, error) { mode uint32,
event, _, err := r.sys.FetchSpecificEvent(r.ctx, pointer, sdk.FetchSpecificEventParameters{ out *fuse.EntryOut,
WithRelays: false, ) (node *fs.Inode, fh fs.FileHandle, fuseFlags uint32, errno syscall.Errno) {
}) if name == "publish" {
if err != nil { // this causes the publish process to be triggered faster
return nil, fmt.Errorf("failed to fetch: %w", err) 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( func (e *EntityDir) Unlink(ctx context.Context, name string) syscall.Errno {
parent fs.InodeEmbedder, switch name {
extension string, case "content" + e.extension:
event *nostr.Event, e.updating.content = e.event.Content
) *fs.Inode { return syscall.ENOTDIR
log := r.ctx.Value("log").(func(msg string, args ...any)) 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( func (e *EntityDir) OnAdd(_ context.Context) {
r.ctx, log := e.root.ctx.Value("log").(func(msg string, args ...any))
&EntityDir{ctx: r.ctx, wd: r.wd, evt: event}, publishedAt := uint64(e.event.CreatedAt)
fs.StableAttr{Mode: syscall.S_IFDIR, Ino: hexToUint64(event.ID)}, if tag := e.event.Tags.Find("published_at"); tag != nil {
)
var publishedAt uint64
if tag := event.Tags.Find("published_at"); tag != nil {
publishedAt, _ = strconv.ParseUint(tag[1], 10, 64) publishedAt, _ = strconv.ParseUint(tag[1], 10, 64)
} }
npub, _ := nip19.EncodePublicKey(event.PubKey) npub, _ := nip19.EncodePublicKey(e.event.PubKey)
h.AddChild("@author", h.NewPersistentInode( e.AddChild("@author", e.NewPersistentInode(
r.ctx, e.root.ctx,
&fs.MemSymlink{ &fs.MemSymlink{
Data: []byte(r.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,
r.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(
r.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 {
h.AddChild("title", h.NewPersistentInode( // read-only
r.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, var title string
Ctime: uint64(event.CreatedAt), if tag := e.event.Tags.Find("title"); tag != nil {
Mtime: uint64(publishedAt), title = tag[1]
Size: uint64(len(tag[1])), } else {
title = e.event.Tags.GetD()
}
return uint64(e.event.CreatedAt), publishedAt, title
}, },
}, },
fs.StableAttr{}, fs.StableAttr{},
), true) ), true)
} e.AddChild("content."+e.extension, e.NewPersistentInode(
e.root.ctx,
h.AddChild("content"+extension, h.NewPersistentInode( &DeterministicFile{
r.ctx, get: func() (ctime uint64, mtime uint64, data string) {
&fs.MemRegularFile{ return uint64(e.event.CreatedAt), publishedAt, e.event.Content
Data: []byte(event.Content), },
Attr: fuse.Attr{
Mode: 0444,
Ctime: uint64(event.CreatedAt),
Mtime: uint64(publishedAt),
Size: uint64(len(event.Content)),
}, },
}, fs.StableAttr{},
fs.StableAttr{}, ), true)
), 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 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(r.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(
r.ctx, e.root.ctx,
&fs.MemSymlink{ &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}, fs.StableAttr{Mode: syscall.S_IFLNK},
), true) ), true)
@@ -164,15 +207,15 @@ func (r *NostrRoot) CreateEntityDir(
addImage := func(url string) { addImage := func(url string) {
if imagesdir == nil { if imagesdir == nil {
in := &fs.Inode{} in := &fs.Inode{}
imagesdir = h.NewPersistentInode(r.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(
r.ctx, e.root.ctx,
&AsyncFile{ &AsyncFile{
ctx: r.ctx, ctx: e.root.ctx,
load: func() ([]byte, nostr.Timestamp) { 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() defer cancel()
r, err := http.NewRequestWithContext(ctx, "GET", url, nil) r, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil { if err != nil {
@@ -198,7 +241,7 @@ func (r *NostrRoot) 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 +249,116 @@ func (r *NostrRoot) 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) 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)},
)
} }

@@ -108,11 +108,10 @@ func (r *NostrRoot) CreateNpubDir(
Kinds: []int{1}, Kinds: []int{1},
Authors: []string{pointer.PublicKey}, Authors: []string{pointer.PublicKey},
}, },
paginate: true, paginate: true,
relays: relays, relays: relays,
create: func(n *ViewDir, event *nostr.Event) (string, *fs.Inode) { replaceable: false,
return event.ID, r.CreateEventDir(n, event) extension: "txt",
},
}, },
fs.StableAttr{Mode: syscall.S_IFDIR}, fs.StableAttr{Mode: syscall.S_IFDIR},
), ),
@@ -129,11 +128,10 @@ func (r *NostrRoot) CreateNpubDir(
Kinds: []int{1111}, Kinds: []int{1111},
Authors: []string{pointer.PublicKey}, Authors: []string{pointer.PublicKey},
}, },
paginate: true, paginate: true,
relays: relays, relays: relays,
create: func(n *ViewDir, event *nostr.Event) (string, *fs.Inode) { replaceable: false,
return event.ID, r.CreateEventDir(n, event) extension: "txt",
},
}, },
fs.StableAttr{Mode: syscall.S_IFDIR}, fs.StableAttr{Mode: syscall.S_IFDIR},
), ),
@@ -150,11 +148,10 @@ func (r *NostrRoot) CreateNpubDir(
Kinds: []int{20}, Kinds: []int{20},
Authors: []string{pointer.PublicKey}, Authors: []string{pointer.PublicKey},
}, },
paginate: true, paginate: true,
relays: relays, relays: relays,
create: func(n *ViewDir, event *nostr.Event) (string, *fs.Inode) { replaceable: false,
return event.ID, r.CreateEventDir(n, event) extension: "txt",
},
}, },
fs.StableAttr{Mode: syscall.S_IFDIR}, fs.StableAttr{Mode: syscall.S_IFDIR},
), ),
@@ -171,11 +168,10 @@ func (r *NostrRoot) CreateNpubDir(
Kinds: []int{21, 22}, Kinds: []int{21, 22},
Authors: []string{pointer.PublicKey}, Authors: []string{pointer.PublicKey},
}, },
paginate: false, paginate: false,
relays: relays, relays: relays,
create: func(n *ViewDir, event *nostr.Event) (string, *fs.Inode) { replaceable: false,
return event.ID, r.CreateEventDir(n, event) extension: "txt",
},
}, },
fs.StableAttr{Mode: syscall.S_IFDIR}, fs.StableAttr{Mode: syscall.S_IFDIR},
), ),
@@ -192,11 +188,10 @@ func (r *NostrRoot) CreateNpubDir(
Kinds: []int{9802}, Kinds: []int{9802},
Authors: []string{pointer.PublicKey}, Authors: []string{pointer.PublicKey},
}, },
paginate: false, paginate: false,
relays: relays, relays: relays,
create: func(n *ViewDir, event *nostr.Event) (string, *fs.Inode) { replaceable: false,
return event.ID, r.CreateEventDir(n, event) extension: "txt",
},
}, },
fs.StableAttr{Mode: syscall.S_IFDIR}, fs.StableAttr{Mode: syscall.S_IFDIR},
), ),
@@ -213,15 +208,10 @@ func (r *NostrRoot) CreateNpubDir(
Kinds: []int{30023}, Kinds: []int{30023},
Authors: []string{pointer.PublicKey}, Authors: []string{pointer.PublicKey},
}, },
paginate: false, paginate: false,
relays: relays, relays: relays,
create: func(n *ViewDir, event *nostr.Event) (string, *fs.Inode) { replaceable: true,
d := event.Tags.GetD() extension: "md",
if d == "" {
d = "_"
}
return d, r.CreateEntityDir(n, ".md", event)
},
}, },
fs.StableAttr{Mode: syscall.S_IFDIR}, fs.StableAttr{Mode: syscall.S_IFDIR},
), ),
@@ -238,15 +228,10 @@ func (r *NostrRoot) CreateNpubDir(
Kinds: []int{30818}, Kinds: []int{30818},
Authors: []string{pointer.PublicKey}, Authors: []string{pointer.PublicKey},
}, },
paginate: false, paginate: false,
relays: relays, relays: relays,
create: func(n *ViewDir, event *nostr.Event) (string, *fs.Inode) { replaceable: true,
d := event.Tags.GetD() extension: "adoc",
if d == "" {
d = "_"
}
return d, r.CreateEntityDir(n, ".adoc", event)
},
}, },
fs.StableAttr{Mode: syscall.S_IFDIR}, fs.StableAttr{Mode: syscall.S_IFDIR},
), ),

@@ -12,12 +12,13 @@ import (
type ViewDir struct { type ViewDir struct {
fs.Inode fs.Inode
root *NostrRoot root *NostrRoot
fetched atomic.Bool fetched atomic.Bool
filter nostr.Filter filter nostr.Filter
paginate bool paginate bool
relays []string relays []string
create func(*ViewDir, *nostr.Event) (string, *fs.Inode) replaceable bool
extension string
} }
var ( var (
@@ -49,27 +50,37 @@ func (n *ViewDir) Opendir(_ 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.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 := n.filter
filter.Until = &aMonthAgo filter.Until = &aMonthAgo
n.AddChild("@previous", n.NewPersistentInode( n.AddChild("@previous", n.NewPersistentInode(
n.root.ctx, n.root.ctx,
&ViewDir{ &ViewDir{
root: n.root, root: n.root,
filter: filter, filter: filter,
relays: n.relays, relays: n.relays,
extension: n.extension,
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 = "_"
}
n.AddChild(name, n.root.CreateEntityDir(n, n.extension, evt), true)
}
} else { } else {
for ie := range n.root.sys.Pool.FetchMany(n.root.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(n, ie.Event) nostr.WithLabel("nakfs"),
n.AddChild(basename, inode, true) ) {
n.AddChild(ie.Event.ID, n.root.CreateEventDir(n, ie.Event), true)
} }
} }

88
nostrfs/writeablefile.go Normal file

@@ -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
}