diff --git a/go.mod b/go.mod index ebd3b44..4446283 100644 --- a/go.mod +++ b/go.mod @@ -5,15 +5,19 @@ go 1.25.4 require ( fiatjaf.com/lib v0.3.2 fiatjaf.com/nostr v0.0.0-20251126120447-7261a4b515ed - github.com/itchyny/gojq v0.12.17 + github.com/bep/debounce v1.2.1 + github.com/btcsuite/btcd/btcec/v2 v2.3.4 github.com/mappu/miqt v0.12.0 + github.com/puzpuzpuz/xsync/v3 v3.5.1 + golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 ) require ( github.com/FastFilter/xorfilter v0.2.1 // indirect github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3 // indirect + github.com/andybalholm/brotli v1.1.1 // indirect + github.com/bluekeyes/go-gitdiff v0.7.1 // indirect github.com/btcsuite/btcd v0.24.2 // indirect - github.com/btcsuite/btcd/btcec/v2 v2.3.4 // indirect github.com/btcsuite/btcd/btcutil v1.1.5 // indirect github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect @@ -23,22 +27,28 @@ require ( github.com/dgraph-io/ristretto/v2 v2.3.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/elnosh/gonuts v0.4.2 // indirect + github.com/fasthttp/websocket v1.5.12 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect - github.com/itchyny/timefmt-go v0.1.6 // indirect + github.com/go-git/go-git/v5 v5.16.3 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/liamg/magic v0.0.1 // indirect github.com/mailru/easyjson v0.9.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // 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/templexxx/cpu v0.0.1 // indirect github.com/templexxx/xhex v0.0.0-20200614015412-aed53437177b // indirect github.com/tidwall/gjson v1.18.0 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasthttp v1.59.0 // indirect github.com/x448/float16 v0.8.4 // indirect golang.org/x/crypto v0.39.0 // indirect - golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect + golang.org/x/net v0.41.0 // indirect golang.org/x/sync v0.15.0 // indirect golang.org/x/sys v0.35.0 // indirect golang.org/x/text v0.26.0 // indirect diff --git a/go.sum b/go.sum index 61ec00d..752613b 100644 --- a/go.sum +++ b/go.sum @@ -6,11 +6,15 @@ github.com/FastFilter/xorfilter v0.2.1 h1:lbdeLG9BdpquK64ZsleBS8B4xO/QW1IM0gMzF7 github.com/FastFilter/xorfilter v0.2.1/go.mod h1:aumvdkhscz6YBZF9ZA/6O4fIoNod4YR50kIVGGZ7l9I= 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/PowerDNS/lmdb-go v1.9.3 h1:AUMY2pZT8WRpkEv39I9Id3MuoHd+NZbTVpNhruVkPTg= +github.com/PowerDNS/lmdb-go v1.9.3/go.mod h1:TE0l+EZK8Z1B4dx070ZxkWTlp8RG1mjN0/+FkFRQMtU= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= +github.com/bluekeyes/go-gitdiff v0.7.1 h1:graP4ElLRshr8ecu0UtqfNTCHrtSyZd3DABQm/DWesQ= +github.com/bluekeyes/go-gitdiff v0.7.1/go.mod h1:QpfYYO1E0fTVHVZAZKiRjtSGY9823iCdvGXBcEzHGbM= github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M= github.com/btcsuite/btcd v0.23.5-0.20231215221805-96c9fd8078fd/go.mod h1:nm3Bko6zh6bWP60UxwoT5LzdGJsQJaPo6HjduXq9p6A= @@ -69,6 +73,8 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo 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/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/go-git/go-git/v5 v5.16.3 h1:Z8BtvxZ09bYm/yYNgPKCzgWtaRqDTgIKRgIRHBfU6Z8= +github.com/go-git/go-git/v5 v5.16.3/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= @@ -80,13 +86,11 @@ github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/itchyny/gojq v0.12.17 h1:8av8eGduDb5+rvEdaOO+zQUjA04MS0m3Ps8HiD+fceg= -github.com/itchyny/gojq v0.12.17/go.mod h1:WBrEMkgAfAGO1LUcGOckBl5O726KPp+OlkKug0I/FEY= -github.com/itchyny/timefmt-go v0.1.6 h1:ia3s54iciXDdzWzwaVKXZPbiXzxxnv1SPGFfM/myJ5Q= -github.com/itchyny/timefmt-go v0.1.6/go.mod h1:RRDZYC5s9ErkjQvTvvU7keJjxUYzIISJGxm9/mAERQg= github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= @@ -97,6 +101,12 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/liamg/magic v0.0.1 h1:Ru22ElY+sCh6RvRTWjQzKKCxsEco8hE0co8n1qe7TBM= +github.com/liamg/magic v0.0.1/go.mod h1:yQkOmZZI52EA+SQ2xyHpVw8fNvTBruF873Y+Vt6S+fk= github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mappu/miqt v0.12.0 h1:bBMBDeACmV8TbdLfoN51la7kF6QT3sNAcG+ZdRDgmxU= @@ -115,10 +125,14 @@ github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5 github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +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.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg= github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= 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= @@ -149,6 +163,8 @@ github.com/valyala/fasthttp v1.59.0 h1:Qu0qYHfXvPk1mSLNqcFtEk6DpxgA26hy6bmydotDp github.com/valyala/fasthttp v1.59.0/go.mod h1:GTxNb9Bc6r2a9D0TWNSPwDz78UxnTGBViY3xZNEqyYU= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= 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= @@ -192,6 +208,8 @@ google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miE google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -200,3 +218,5 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= +gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= diff --git a/main.go b/main.go index 40b9ee0..f78de2a 100644 --- a/main.go +++ b/main.go @@ -27,6 +27,7 @@ var ( event int req int paste int + serve int } statusLabel *qt.QLabel @@ -144,6 +145,7 @@ func main() { eventTab := setupEventTab() reqTab := setupReqTab() pasteTab := setupPasteTab() + serveTab := setupServeTab() tabWidget.AddTab(eventTab, "event") tabIndexes.event = 0 @@ -154,6 +156,9 @@ func main() { tabWidget.AddTab(pasteTab, "paste") tabIndexes.paste = 2 + tabWidget.AddTab(serveTab, "serve") + tabIndexes.serve = 3 + switch *initialTab { case "event": tabWidget.SetCurrentIndex(tabIndexes.event) @@ -161,6 +166,8 @@ func main() { tabWidget.SetCurrentIndex(tabIndexes.req) case "paste": tabWidget.SetCurrentIndex(tabIndexes.paste) + case "serve": + tabWidget.SetCurrentIndex(tabIndexes.serve) default: tabWidget.SetCurrentIndex(0) } diff --git a/serve.go b/serve.go new file mode 100644 index 0000000..2bf36de --- /dev/null +++ b/serve.go @@ -0,0 +1,293 @@ +package main + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/url" + "os" + "sync/atomic" + + "fiatjaf.com/nostr" + "fiatjaf.com/nostr/eventstore/slicestore" + "fiatjaf.com/nostr/khatru" + "fiatjaf.com/nostr/khatru/blossom" + "fiatjaf.com/nostr/khatru/grasp" + qt "github.com/mappu/miqt/qt6" + "github.com/mappu/miqt/qt6/mainthread" + "github.com/puzpuzpuz/xsync/v3" +) + +var version = "dev" + +func isPiped() bool { + stat, err := os.Stdin.Stat() + if err != nil { + return false + } + return (stat.Mode() & os.ModeCharDevice) == 0 +} + +type serveVars struct { + tab *qt.QWidget + + graspCheck *qt.QCheckBox + blossomCheck *qt.QCheckBox + negentropyCheck *qt.QCheckBox + + startButton *qt.QPushButton + stopButton *qt.QPushButton + + logsList *qt.QListWidget + eventsList *qt.QListWidget + + relay *khatru.Relay + db *slicestore.SliceStore + blobStore *xsync.MapOf[string, []byte] + repoDir string + + running bool +} + +var serve = &serveVars{} + +func setupServeTab() *qt.QWidget { + serve.tab = qt.NewQWidget(window.QWidget) + layout := qt.NewQVBoxLayout2() + serve.tab.SetLayout(layout.QLayout) + + // checkboxes + optionsHBox := qt.NewQHBoxLayout2() + layout.AddLayout(optionsHBox.QLayout) + + serve.negentropyCheck = qt.NewQCheckBox(serve.tab) + serve.negentropyCheck.SetText("negentropy") + optionsHBox.AddWidget(serve.negentropyCheck.QWidget) + + serve.graspCheck = qt.NewQCheckBox(serve.tab) + serve.graspCheck.SetText("grasp") + optionsHBox.AddWidget(serve.graspCheck.QWidget) + + serve.blossomCheck = qt.NewQCheckBox(serve.tab) + serve.blossomCheck.SetText("blossom") + optionsHBox.AddWidget(serve.blossomCheck.QWidget) + + // buttons + buttonsHBox := qt.NewQHBoxLayout2() + layout.AddLayout(buttonsHBox.QLayout) + + serve.startButton = qt.NewQPushButton5("start", serve.tab) + buttonsHBox.AddWidget(serve.startButton.QWidget) + + serve.stopButton = qt.NewQPushButton5("stop", serve.tab) + serve.stopButton.SetEnabled(false) + buttonsHBox.AddWidget(serve.stopButton.QWidget) + + // logs + logsLabel := qt.NewQLabel2() + logsLabel.SetText("logs:") + layout.AddWidget(logsLabel.QWidget) + serve.logsList = qt.NewQListWidget(serve.tab) + serve.logsList.SetMinimumHeight(150) + layout.AddWidget(serve.logsList.QWidget) + + // events + eventsLabel := qt.NewQLabel2() + eventsLabel.SetText("events:") + layout.AddWidget(eventsLabel.QWidget) + serve.eventsList = qt.NewQListWidget(serve.tab) + serve.eventsList.SetMinimumHeight(200) + layout.AddWidget(serve.eventsList.QWidget) + + // double-click events + serve.eventsList.OnItemDoubleClicked(func(item *qt.QListWidgetItem) { + var event nostr.Event + if err := json.Unmarshal([]byte(item.Text()), &event); err != nil { + return + } + pretty, _ := json.MarshalIndent(event, "", " ") + dialog := qt.NewQDialog(window.QWidget) + dialog.SetWindowTitle("event") + dialog.SetMinimumWidth(400) + dialog.SetMinimumHeight(500) + dlayout := qt.NewQVBoxLayout2() + dialog.SetLayout(dlayout.QLayout) + textEdit := qt.NewQTextEdit(dialog.QWidget) + textEdit.SetReadOnly(true) + textEdit.SetPlainText(string(pretty)) + dlayout.AddWidget(textEdit.QWidget) + closeButton := qt.NewQPushButton5("close", dialog.QWidget) + closeButton.OnClicked(func() { dialog.Close() }) + dlayout.AddWidget(closeButton.QWidget) + dialog.Exec() + }) + + serve.startButton.OnClicked(func() { + serve.startRelay() + }) + + serve.stopButton.OnClicked(func() { + serve.stopRelay() + }) + + return serve.tab +} + +func (serve *serveVars) startRelay() { + if serve.running { + return + } + serve.running = true + serve.startButton.SetEnabled(false) + serve.stopButton.SetEnabled(true) + + serve.db = &slicestore.SliceStore{} + serve.blobStore = xsync.NewMapOf[string, []byte]() + + rl := khatru.NewRelay() + serve.relay = rl + + rl.Info.Name = "nak serve" + rl.Info.Description = "a local relay for testing, debugging and development." + rl.Info.Software = "https://github.com/fiatjaf/nak" + rl.Info.Version = "dev" + + rl.UseEventstore(serve.db, 500) + + if serve.negentropyCheck.IsChecked() { + rl.Negentropy = true + } + + started := make(chan bool) + exited := make(chan error) + + hostname := "localhost" + port := 10547 + + if serve.blossomCheck.IsChecked() { + bs := blossom.New(rl, fmt.Sprintf("http://%s:%d", hostname, port)) + bs.Store = blossom.NewMemoryBlobIndex() + + bs.StoreBlob = func(ctx context.Context, sha256 string, ext string, body []byte) error { + serve.blobStore.Store(sha256+ext, body) + serve.log("blob stored: %s", sha256+ext) + return nil + } + bs.LoadBlob = func(ctx context.Context, sha256 string, ext string) (io.ReadSeeker, *url.URL, error) { + if body, ok := serve.blobStore.Load(sha256 + ext); ok { + serve.log("blob download: %s", sha256+ext) + return bytes.NewReader(body), nil, nil + } + return nil, nil, nil + } + bs.DeleteBlob = func(ctx context.Context, sha256 string, ext string) error { + serve.blobStore.Delete(sha256 + ext) + serve.log("blob delete: %s", sha256+ext) + return nil + } + } + + if serve.graspCheck.IsChecked() { + var err error + serve.repoDir, err = os.MkdirTemp("", "vnak-serve-grasp-repos-") + if err != nil { + serve.log("failed to create grasp repos directory: %w", err) + return + } + g := grasp.New(rl, serve.repoDir) + g.OnRead = func(ctx context.Context, pubkey nostr.PubKey, repo string) (reject bool, reason string) { + serve.log("git read by '%s' at '%s'", pubkey.Hex(), repo) + return false, "" + } + g.OnWrite = func(ctx context.Context, pubkey nostr.PubKey, repo string) (reject bool, reason string) { + serve.log("git write by '%s' at '%s'", pubkey.Hex(), repo) + return false, "" + } + } + + go func() { + err := rl.Start(hostname, port, started) + exited <- err + }() + + // relay logging + rl.OnRequest = func(ctx context.Context, filter nostr.Filter) (reject bool, msg string) { + negentropy := "" + if khatru.IsNegentropySession(ctx) { + negentropy = "negentropy " + } + + serve.log("%srequest: %s", negentropy, filter) + return false, "" + } + + rl.OnCount = func(ctx context.Context, filter nostr.Filter) (reject bool, msg string) { + serve.log("count request: %s", filter) + return false, "" + } + + rl.OnEvent = func(ctx context.Context, event nostr.Event) (reject bool, msg string) { + serve.log("event: %s", event) + serve.addEvent(event) + return false, "" + } + + totalConnections := atomic.Int32{} + rl.OnConnect = func(ctx context.Context) { + totalConnections.Add(1) + go func() { + <-ctx.Done() + totalConnections.Add(-1) + }() + } + + <-started + serve.log("relay running at %s", fmt.Sprintf("ws://%s:%d", hostname, port)) + if serve.graspCheck.IsChecked() { + serve.log("grasp repos at %s", serve.repoDir) + } + + go func() { + err := <-exited + if err != nil { + serve.log("relay exited with error: %s", err) + } + mainthread.Wait(func() { + serve.running = false + serve.startButton.SetEnabled(true) + serve.stopButton.SetEnabled(false) + }) + }() +} + +func (serve *serveVars) stopRelay() { + if !serve.running { + return + } + if serve.relay != nil { + serve.relay.Shutdown(ctx) + } + serve.running = false + serve.startButton.SetEnabled(true) + serve.stopButton.SetEnabled(false) + statusLabel.SetText("relay stopped") +} + +func (serve *serveVars) log(format string, args ...interface{}) { + msg := fmt.Sprintf(format, args...) + mainthread.Start(func() { + item := qt.NewQListWidgetItem2(msg) + serve.logsList.InsertItem(0, item) + serve.logsList.ScrollToTop() + }) +} + +func (serve *serveVars) addEvent(event nostr.Event) { + jsonBytes, _ := json.Marshal(event) + mainthread.Wait(func() { + item := qt.NewQListWidgetItem2(string(jsonBytes)) + serve.eventsList.AddItemWithItem(item) + }) +}