From 80b606973c7424aa82bf57dfaec9b465490c4c96 Mon Sep 17 00:00:00 2001 From: mleku Date: Sat, 30 Dec 2023 15:58:23 +0000 Subject: [PATCH] eliminated all unwanted external deps --- cmd/publicatr/go.mod | 48 --- cmd/publicatr/go.sum | 183 ---------- cmd/publicatr/main.go | 54 +-- cmd/publicatr/profile.go | 11 +- cmd/publicatr/timeline.go | 229 ++++++------ cmd/publicatr/zap.go | 70 ++-- go.mod | 34 +- go.sum | 115 ++++-- pkg/nostr/connection.go | 168 +++++++++ pkg/nostr/helpers.go | 12 + pkg/nostr/kind/kinds.go | 1 + pkg/nostr/nip1/enveloper_test.go | 5 +- pkg/nostr/nip1/event.go | 2 + pkg/nostr/nip1/event_test.go | 4 +- pkg/nostr/nip1/filters.go | 16 +- pkg/nostr/nip1/filters_test.go | 2 +- pkg/nostr/nip1/reqenvelope.go | 64 ++-- pkg/nostr/nip19/keys.go | 190 ++++++++++ pkg/nostr/nip19/keys_test.go | 154 ++++++++ pkg/nostr/nip19/nip19.go | 243 +++++++++++++ pkg/nostr/nip19/nip19_test.go | 208 +++++++++++ pkg/nostr/nip19/utils.go | 30 ++ pkg/nostr/nip4/nip4.go | 140 ++++++++ pkg/nostr/nip45/countenvelope.go | 9 +- pkg/nostr/nip5/nip05.go | 90 +++++ pkg/nostr/pointers/pointers.go | 25 ++ pkg/nostr/pool.go | 203 +++++++++++ pkg/nostr/relay.go | 586 +++++++++++++++++++++++++++++++ pkg/nostr/relay_test.go | 280 +++++++++++++++ pkg/nostr/subscription.go | 174 +++++++++ pkg/nostr/subscription_test.go | 120 +++++++ pkg/wire/text/mangle.go | 4 + pkg/wire/text/unescape.go | 4 +- 33 files changed, 3009 insertions(+), 469 deletions(-) delete mode 100644 cmd/publicatr/go.mod delete mode 100644 cmd/publicatr/go.sum create mode 100644 pkg/nostr/connection.go create mode 100644 pkg/nostr/helpers.go create mode 100644 pkg/nostr/nip19/keys.go create mode 100644 pkg/nostr/nip19/keys_test.go create mode 100644 pkg/nostr/nip19/nip19.go create mode 100644 pkg/nostr/nip19/nip19_test.go create mode 100644 pkg/nostr/nip19/utils.go create mode 100644 pkg/nostr/nip4/nip4.go create mode 100644 pkg/nostr/nip5/nip05.go create mode 100644 pkg/nostr/pointers/pointers.go create mode 100644 pkg/nostr/pool.go create mode 100644 pkg/nostr/relay.go create mode 100644 pkg/nostr/relay_test.go create mode 100644 pkg/nostr/subscription.go create mode 100644 pkg/nostr/subscription_test.go diff --git a/cmd/publicatr/go.mod b/cmd/publicatr/go.mod deleted file mode 100644 index deacd02f..00000000 --- a/cmd/publicatr/go.mod +++ /dev/null @@ -1,48 +0,0 @@ -module github.com/Hubmakerlabs/replicatr/cmd/publicatr - -go 1.21.4 - -toolchain go1.21.5 - -require ( - github.com/fatih/color v1.15.0 - github.com/mdp/qrterminal/v3 v3.2.0 - github.com/nbd-wtf/go-nostr v0.25.0 - github.com/nbd-wtf/nostr-sdk v0.0.1 - github.com/urfave/cli/v2 v2.25.7 - mleku.online/git/log v1.0.7 -) - -require ( - github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect - github.com/btcsuite/btcd/btcutil v1.1.4-0.20230904040416-d4f519f5dc05 // indirect - github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2 // indirect - github.com/cespare/xxhash/v2 v2.2.0 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect - github.com/dgraph-io/ristretto v0.1.1 // indirect - github.com/dustin/go-humanize v1.0.1 // indirect - github.com/fiatjaf/eventstore v0.1.0 // indirect - github.com/gobwas/httphead v0.1.0 // indirect - github.com/gobwas/pool v0.2.1 // indirect - github.com/gobwas/ws v1.3.0 // indirect - github.com/golang/glog v1.1.2 // indirect - github.com/josharian/intern v1.0.0 // indirect - github.com/mailru/easyjson v0.7.7 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/pkg/errors v0.9.1 // indirect - github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect - github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/tidwall/gjson v1.17.0 // indirect - github.com/tidwall/match v1.1.1 // indirect - github.com/tidwall/pretty v1.2.1 // indirect - github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect - golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect - golang.org/x/net v0.10.0 // indirect - golang.org/x/sys v0.14.0 // indirect - golang.org/x/term v0.13.0 // indirect - rsc.io/qr v0.2.0 // indirect -) diff --git a/cmd/publicatr/go.sum b/cmd/publicatr/go.sum deleted file mode 100644 index 1f400cd7..00000000 --- a/cmd/publicatr/go.sum +++ /dev/null @@ -1,183 +0,0 @@ -github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= -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.0/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY= -github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA= -github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= -github.com/btcsuite/btcd/btcec/v2 v2.3.2 h1:5n0X6hX0Zk+6omWcihdYvdAlGf2DfasC0GMf7DClJ3U= -github.com/btcsuite/btcd/btcec/v2 v2.3.2/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= -github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A= -github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE= -github.com/btcsuite/btcd/btcutil v1.1.4-0.20230904040416-d4f519f5dc05 h1:aemxF+69pT9sYC5E6Qj71zQVHcF72m0BNcVhCl3/thU= -github.com/btcsuite/btcd/btcutil v1.1.4-0.20230904040416-d4f519f5dc05/go.mod h1:UR7dsSJzJUfMmFiiLlIrMq1lS9jh9EdCV7FStZSnpi0= -github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= -github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= -github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2 h1:KdUfX2zKommPRa+PD0sWZUyXe9w277ABlgELO7H04IM= -github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= -github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= -github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= -github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= -github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= -github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I= -github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= -github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= -github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= -github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM= -github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= -github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y= -github.com/decred/dcrd/crypto/blake256 v1.0.1/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.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= -github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= -github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8= -github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= -github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= -github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= -github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= -github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= -github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= -github.com/fiatjaf/eventstore v0.1.0 h1:/g7VTw6dsXmjICD3rBuHNIvAammHJ5unrKJ71Dz+VTs= -github.com/fiatjaf/eventstore v0.1.0/go.mod h1:juMei5HL3HJi6t7vZjj7VdEItDPu31+GLROepdUK4tw= -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/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= -github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= -github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= -github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= -github.com/gobwas/ws v1.3.0 h1:sbeU3Y4Qzlb+MOzIe6mQGf7QR4Hkv6ZD0qhGkBFL2O0= -github.com/gobwas/ws v1.3.0/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/glog v1.1.2 h1:DVjP2PbBOzHyzA+dn3WhHIq4NdVu3Q+pvivFICf/7fo= -github.com/golang/glog v1.1.2/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ= -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= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -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/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -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= -github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= -github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mdp/qrterminal/v3 v3.2.0 h1:qteQMXO3oyTK4IHwj2mWsKYYRBOp1Pj2WRYFYYNTCdk= -github.com/mdp/qrterminal/v3 v3.2.0/go.mod h1:XGGuua4Lefrl7TLEsSONiD+UEjQXJZ4mPzF+gWYIJkk= -github.com/nbd-wtf/go-nostr v0.25.0 h1:6ArnEX5NqjTaIBH6F5KYIJ0uw0uaKSWu8zjDb9za0Cg= -github.com/nbd-wtf/go-nostr v0.25.0/go.mod h1:bkffJI+x914sPQWum9ZRUn66D7NpDnAoWo1yICvj3/0= -github.com/nbd-wtf/nostr-sdk v0.0.1 h1:Yd5RQRFEh2NgRcjRsJ+BQ3mbbdeVAC6QCy7ptQ3MCKY= -github.com/nbd-wtf/nostr-sdk v0.0.1/go.mod h1:9P3DiQQ9OAa/i+QM1+B/t3djE/jzLNzqxCamNKKyF6Q= -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= -github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= -github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= -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/v2 v2.5.1 h1:mVGYAvzDSu52+zaGyNjC+24Xw2bQi3kTr4QJ6N9pIIU= -github.com/puzpuzpuz/xsync/v2 v2.5.1/go.mod h1:gD2H2krq/w52MfPLE+Uy64TzJDVY7lP2znR9qmR35kU= -github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= -github.com/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM= -github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= -github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= -github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= -github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= -github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= -github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= -github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs= -github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= -github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= -github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= -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/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= -golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= -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.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -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= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= -golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= -golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= -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/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= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -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/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= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -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= -mleku.online/git/log v1.0.7 h1:jNDph/CW/GerRluqcWV6F+NMng1gmm5Qi/TFAJRAfpo= -mleku.online/git/log v1.0.7/go.mod h1:OdomTvlDYHzX1daD1LaqU+TtX484EbECPKOiMrlGkOY= -rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY= -rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs= diff --git a/cmd/publicatr/main.go b/cmd/publicatr/main.go index ae4af723..80e19096 100644 --- a/cmd/publicatr/main.go +++ b/cmd/publicatr/main.go @@ -18,10 +18,12 @@ import ( "github.com/urfave/cli/v2" log2 "mleku.online/git/log" + "github.com/Hubmakerlabs/replicatr/pkg/nostr" + "github.com/Hubmakerlabs/replicatr/pkg/nostr/kind" + "github.com/Hubmakerlabs/replicatr/pkg/nostr/nip1" + "github.com/Hubmakerlabs/replicatr/pkg/nostr/nip19" + "github.com/Hubmakerlabs/replicatr/pkg/nostr/nip4" "github.com/fatih/color" - "github.com/nbd-wtf/go-nostr" - "github.com/nbd-wtf/go-nostr/nip04" - "github.com/nbd-wtf/go-nostr/nip19" ) var ( @@ -59,8 +61,8 @@ type Config struct { // Event is type Event struct { - Event *nostr.Event `json:"event"` - Profile Profile `json:"profile"` + Event *nip1.Event `json:"event"` + Profile Profile `json:"profile"` } // Profile is @@ -124,7 +126,7 @@ func loadConfig(profile string) (*Config, error) { } if len(cfg.Relays) == 0 { cfg.Relays = map[string]Relay{} - cfg.Relays["wss://relay.nostr.band"] = Relay{ + cfg.Relays["wss://relay.nip1.band"] = Relay{ Read: true, Write: true, Search: true, @@ -138,7 +140,7 @@ func (cfg *Config) GetFollows(profile string) (map[string]Profile, error) { var mu sync.Mutex var pub string if _, s, err := nip19.Decode(cfg.PrivateKey); err == nil { - if pub, err = nostr.GetPublicKey(s.(string)); err != nil { + if pub, err = nip19.GetPublicKey(s.(string)); err != nil { return nil, err } } else { @@ -155,8 +157,8 @@ func (cfg *Config) GetFollows(profile string) (map[string]Profile, error) { cfg.Do(Relay{Read: true}, func(ctx context.Context, relay *nostr.Relay) bool { evs, err := relay.QuerySync(ctx, - nostr.Filter{ - Kinds: []int{nostr.KindContactList}, + nip1.Filter{ + Kinds: kind.Array{kind.ContactList}, Authors: []string{pub}, Limit: 1, }) @@ -203,8 +205,8 @@ func (cfg *Config) GetFollows(profile string) (map[string]Profile, error) { // get follower's descriptions cfg.Do(Relay{Read: true}, func(ctx context.Context, relay *nostr.Relay) bool { - evs, err := relay.QuerySync(ctx, nostr.Filter{ - Kinds: []int{nostr.KindProfileMetadata}, + evs, err := relay.QuerySync(ctx, nip1.Filter{ + Kinds: kind.Array{kind.ProfileMetadata}, Authors: follows[i:end], // Use the updated end index }) if err != nil { @@ -274,9 +276,9 @@ func (cfg *Config) Do(r Relay, f func(context.Context, *nostr.Relay) bool) { continue } wg.Add(1) - go func(wg *sync.WaitGroup, k string, v Relay) { + go func(wg *sync.WaitGroup, url string, rl Relay) { defer wg.Done() - relay, err := nostr.RelayConnect(ctx, k) + relay, err := nostr.RelayConnect(ctx, url) if err != nil { if cfg.verbose { fmt.Fprintln(os.Stderr, err) @@ -316,12 +318,12 @@ func (cfg *Config) save(profile string) error { } // Decode is -func (cfg *Config) Decode(ev *nostr.Event) error { +func (cfg *Config) Decode(ev *nip1.Event) error { var sk string var pub string if _, s, err := nip19.Decode(cfg.PrivateKey); err == nil { sk = s.(string) - if pub, err = nostr.GetPublicKey(s.(string)); err != nil { + if pub, err = nip19.GetPublicKey(s.(string)); err != nil { return err } } else { @@ -339,11 +341,11 @@ func (cfg *Config) Decode(ev *nostr.Event) error { } else { sp = ev.PubKey } - ss, err := nip04.ComputeSharedSecret(sp, sk) + ss, err := nip4.ComputeSharedSecret(sp, sk) if err != nil { return err } - content, err := nip04.Decrypt(ev.Content, ss) + content, err := nip4.Decrypt(ev.Content, ss) if err != nil { return err } @@ -352,7 +354,7 @@ func (cfg *Config) Decode(ev *nostr.Event) error { } // PrintEvents is -func (cfg *Config) PrintEvents(evs []*nostr.Event, +func (cfg *Config) PrintEvents(evs []*nip1.Event, followsMap map[string]Profile, j, extra bool) { if j { @@ -396,7 +398,7 @@ func (cfg *Config) PrintEvents(evs []*nostr.Event, } // Events is -func (cfg *Config) Events(filter nostr.Filter) []*nostr.Event { +func (cfg *Config) Events(filter nip1.Filter) []*nip1.Event { var mu sync.Mutex found := false var m sync.Map @@ -414,7 +416,7 @@ func (cfg *Config) Events(filter nostr.Filter) []*nostr.Event { } for _, ev := range evs { if _, ok := m.Load(ev.ID); !ok { - if ev.Kind == nostr.KindEncryptedDirectMessage { + if ev.Kind == kind.EncryptedDirectMessage { if err := cfg.Decode(ev); err != nil { continue } @@ -446,15 +448,15 @@ func (cfg *Config) Events(filter nostr.Filter) []*nostr.Event { if !ok { return false } - return lhs.(*nostr.Event).CreatedAt.Time().Before(rhs.(*nostr.Event).CreatedAt.Time()) + return lhs.(*nip1.Event).CreatedAt.Time().Before(rhs.(*nip1.Event).CreatedAt.Time()) }) - var evs []*nostr.Event + var evs []*nip1.Event for _, key := range keys { vv, ok := m.Load(key) if !ok { continue } - evs = append(evs, vv.(*nostr.Event)) + evs = append(evs, vv.(*nip1.Event)) } return evs } @@ -466,8 +468,8 @@ func doVersion(cCtx *cli.Context) error { func main() { app := &cli.App{ - Usage: "A cli application for nostr", - Description: "A cli application for nostr", + Usage: "A cli application for nip1", + Description: "A cli application for nip1", Flags: []cli.Flag{ &cli.StringFlag{Name: "a", Usage: "profile name"}, &cli.StringFlag{Name: "relays", Usage: "relays"}, @@ -490,7 +492,7 @@ func main() { Usage: "show stream", Flags: []cli.Flag{ &cli.StringFlag{Name: "author"}, - &cli.IntSliceFlag{Name: "kind", Value: cli.NewIntSlice(nostr.KindTextNote)}, + &cli.IntSliceFlag{Name: "kind", Value: cli.NewIntSlice(int(kind.TextNote))}, &cli.BoolFlag{Name: "follow"}, &cli.StringFlag{Name: "pattern"}, &cli.StringFlag{Name: "reply"}, diff --git a/cmd/publicatr/profile.go b/cmd/publicatr/profile.go index 54d10528..a7204bbe 100644 --- a/cmd/publicatr/profile.go +++ b/cmd/publicatr/profile.go @@ -9,9 +9,10 @@ import ( "github.com/urfave/cli/v2" - "github.com/nbd-wtf/go-nostr" - "github.com/nbd-wtf/go-nostr/nip19" - "github.com/nbd-wtf/nostr-sdk" + "github.com/Hubmakerlabs/replicatr/pkg/nostr/kind" + nostr "github.com/Hubmakerlabs/replicatr/pkg/nostr/nip1" + "github.com/Hubmakerlabs/replicatr/pkg/nostr/nip19" + "github.com/Hubmakerlabs/replicatr/pkg/nostr/sdk" ) func doProfile(cCtx *cli.Context) error { @@ -28,7 +29,7 @@ func doProfile(cCtx *cli.Context) error { var pub string if user == "" { if _, s, err := nip19.Decode(cfg.PrivateKey); err == nil { - if pub, err = nostr.GetPublicKey(s.(string)); err != nil { + if pub, err = nip19.GetPublicKey(s.(string)); err != nil { return err } } else { @@ -44,7 +45,7 @@ func doProfile(cCtx *cli.Context) error { // get set-metadata filter := nostr.Filter{ - Kinds: []int{nostr.KindProfileMetadata}, + Kinds: kind.Array{kind.ProfileMetadata}, Authors: []string{pub}, Limit: 1, } diff --git a/cmd/publicatr/timeline.go b/cmd/publicatr/timeline.go index d4232dea..ba04fe15 100644 --- a/cmd/publicatr/timeline.go +++ b/cmd/publicatr/timeline.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "github.com/Hubmakerlabs/replicatr/pkg/nostr" "io/ioutil" "os" "regexp" @@ -14,11 +15,15 @@ import ( "github.com/urfave/cli/v2" + "github.com/Hubmakerlabs/replicatr/pkg/nostr/kind" + "github.com/Hubmakerlabs/replicatr/pkg/nostr/nip1" + "github.com/Hubmakerlabs/replicatr/pkg/nostr/nip19" + "github.com/Hubmakerlabs/replicatr/pkg/nostr/nip4" + "github.com/Hubmakerlabs/replicatr/pkg/nostr/sdk" + "github.com/Hubmakerlabs/replicatr/pkg/nostr/tag" + "github.com/Hubmakerlabs/replicatr/pkg/nostr/tags" + "github.com/Hubmakerlabs/replicatr/pkg/nostr/timestamp" "github.com/fatih/color" - "github.com/nbd-wtf/go-nostr" - "github.com/nbd-wtf/go-nostr/nip04" - "github.com/nbd-wtf/go-nostr/nip19" - "github.com/nbd-wtf/nostr-sdk" ) func doDMList(cCtx *cli.Context) error { @@ -39,13 +44,13 @@ func doDMList(cCtx *cli.Context) error { } else { return err } - if npub, err = nostr.GetPublicKey(sk); err != nil { + if npub, err = nip19.GetPublicKey(sk); err != nil { return err } // get timeline - filter := nostr.Filter{ - Kinds: []int{nostr.KindEncryptedDirectMessage}, + filter := nip1.Filter{ + Kinds: kind.Array{kind.EncryptedDirectMessage}, Authors: []string{npub}, } @@ -109,7 +114,7 @@ func doDMTimeline(cCtx *cli.Context) error { } else { return err } - if npub, err = nostr.GetPublicKey(sk); err != nil { + if npub, err = nip19.GetPublicKey(sk); err != nil { return err } @@ -129,10 +134,10 @@ func doDMTimeline(cCtx *cli.Context) error { } // get timeline - filter := nostr.Filter{ - Kinds: []int{nostr.KindEncryptedDirectMessage}, + filter := nip1.Filter{ + Kinds: kind.Array{kind.EncryptedDirectMessage}, Authors: []string{npub, pub}, - Tags: nostr.TagMap{"p": []string{npub, pub}}, + Tags: nip1.TagMap{"p": []string{npub, pub}}, Limit: 9999, } @@ -157,8 +162,8 @@ func doDMPost(cCtx *cli.Context) error { } else { return err } - ev := nostr.Event{} - if npub, err := nostr.GetPublicKey(sk); err == nil { + ev := &nip1.Event{} + if npub, err := nip19.GetPublicKey(sk); err == nil { if _, err := nip19.EncodePublicKey(npub); err != nil { return err } @@ -181,7 +186,7 @@ func doDMPost(cCtx *cli.Context) error { } if sensitive != "" { - ev.Tags = ev.Tags.AppendUnique(nostr.Tag{"content-warning", sensitive}) + ev.Tags = ev.Tags.AppendUnique(tag.T{"content-warning", sensitive}) } if u == "me" { @@ -194,15 +199,15 @@ func doDMPost(cCtx *cli.Context) error { return fmt.Errorf("failed to parse pubkey from '%s'", u) } - ev.Tags = ev.Tags.AppendUnique(nostr.Tag{"p", pub}) - ev.CreatedAt = nostr.Now() - ev.Kind = nostr.KindEncryptedDirectMessage + ev.Tags = ev.Tags.AppendUnique(tag.T{"p", pub}) + ev.CreatedAt = timestamp.Now() + ev.Kind = kind.EncryptedDirectMessage - ss, err := nip04.ComputeSharedSecret(ev.PubKey, sk) + ss, err := nip4.ComputeSharedSecret(ev.PubKey, sk) if err != nil { return err } - ev.Content, err = nip04.Encrypt(ev.Content, ss) + ev.Content, err = nip4.Encrypt(ev.Content, ss) if err != nil { return err } @@ -243,8 +248,8 @@ func doPost(cCtx *cli.Context) error { } else { return err } - ev := nostr.Event{} - if pub, err := nostr.GetPublicKey(sk); err == nil { + ev := &nip1.Event{} + if pub, err := nip19.GetPublicKey(sk); err == nil { if _, err := nip19.EncodePublicKey(pub); err != nil { return err } @@ -266,10 +271,10 @@ func doPost(cCtx *cli.Context) error { return errors.New("content is empty") } - ev.Tags = nostr.Tags{} + ev.Tags = tags.T{} for _, entry := range extractLinks(ev.Content) { - ev.Tags = ev.Tags.AppendUnique(nostr.Tag{"r", entry.text}) + ev.Tags = ev.Tags.AppendUnique(tag.T{"r", entry.text}) } for _, u := range cCtx.StringSlice("emoji") { @@ -277,12 +282,12 @@ func doPost(cCtx *cli.Context) error { if len(tok) != 2 { return cli.ShowSubcommandHelp(cCtx) } - ev.Tags = ev.Tags.AppendUnique(nostr.Tag{"emoji", tok[0], tok[1]}) + ev.Tags = ev.Tags.AppendUnique(tag.T{"emoji", tok[0], tok[1]}) } for _, entry := range extractEmojis(ev.Content) { name := strings.Trim(entry.text, ":") if icon, ok := cfg.Emojis[name]; ok { - ev.Tags = ev.Tags.AppendUnique(nostr.Tag{"emoji", name, icon}) + ev.Tags = ev.Tags.AppendUnique(tag.T{"emoji", name, icon}) } } @@ -293,18 +298,18 @@ func doPost(cCtx *cli.Context) error { } else { return fmt.Errorf("failed to parse pubkey from '%s'", u) } - ev.Tags = ev.Tags.AppendUnique(nostr.Tag{"p", u}) + ev.Tags = ev.Tags.AppendUnique(tag.T{"p", u}) } if sensitive != "" { - ev.Tags = ev.Tags.AppendUnique(nostr.Tag{"content-warning", sensitive}) + ev.Tags = ev.Tags.AppendUnique(tag.T{"content-warning", sensitive}) } if geohash != "" { - ev.Tags = ev.Tags.AppendUnique(nostr.Tag{"g", geohash}) + ev.Tags = ev.Tags.AppendUnique(tag.T{"g", geohash}) } - hashtag := nostr.Tag{"h"} + hashtag := tag.T{"h"} for _, m := range regexp.MustCompile(`#[a-zA-Z0-9]+`).FindAllStringSubmatchIndex(ev.Content, -1) { hashtag = append(hashtag, ev.Content[m[0]+1:m[1]]) } @@ -312,8 +317,8 @@ func doPost(cCtx *cli.Context) error { ev.Tags = ev.Tags.AppendUnique(hashtag) } - ev.CreatedAt = nostr.Now() - ev.Kind = nostr.KindTextNote + ev.CreatedAt = timestamp.Now() + ev.Kind = kind.TextNote if err := ev.Sign(sk); err != nil { return err } @@ -353,8 +358,8 @@ func doReply(cCtx *cli.Context) error { } else { return err } - ev := nostr.Event{} - if pub, err := nostr.GetPublicKey(sk); err == nil { + ev := &nip1.Event{} + if pub, err := nip19.GetPublicKey(sk); err == nil { if _, err := nip19.EncodePublicKey(pub); err != nil { return err } @@ -364,13 +369,13 @@ func doReply(cCtx *cli.Context) error { } if evp := sdk.InputToEventPointer(id); evp != nil { - id = evp.ID + id = evp.ID.String() } else { return fmt.Errorf("failed to parse event from '%s'", id) } - ev.CreatedAt = nostr.Now() - ev.Kind = nostr.KindTextNote + ev.CreatedAt = timestamp.Now() + ev.Kind = kind.TextNote if stdin { b, err := ioutil.ReadAll(os.Stdin) if err != nil { @@ -384,10 +389,10 @@ func doReply(cCtx *cli.Context) error { return errors.New("content is empty") } - ev.Tags = nostr.Tags{} + ev.Tags = tags.T{} for _, entry := range extractLinks(ev.Content) { - ev.Tags = ev.Tags.AppendUnique(nostr.Tag{"r", entry.text}) + ev.Tags = ev.Tags.AppendUnique(tag.T{"r", entry.text}) } for _, u := range cCtx.StringSlice("emoji") { @@ -395,24 +400,24 @@ func doReply(cCtx *cli.Context) error { if len(tok) != 2 { return cli.ShowSubcommandHelp(cCtx) } - ev.Tags = ev.Tags.AppendUnique(nostr.Tag{"emoji", tok[0], tok[1]}) + ev.Tags = ev.Tags.AppendUnique(tag.T{"emoji", tok[0], tok[1]}) } for _, entry := range extractEmojis(ev.Content) { name := strings.Trim(entry.text, ":") if icon, ok := cfg.Emojis[name]; ok { - ev.Tags = ev.Tags.AppendUnique(nostr.Tag{"emoji", name, icon}) + ev.Tags = ev.Tags.AppendUnique(tag.T{"emoji", name, icon}) } } if sensitive != "" { - ev.Tags = ev.Tags.AppendUnique(nostr.Tag{"content-warning", sensitive}) + ev.Tags = ev.Tags.AppendUnique(tag.T{"content-warning", sensitive}) } if geohash != "" { - ev.Tags = ev.Tags.AppendUnique(nostr.Tag{"g", geohash}) + ev.Tags = ev.Tags.AppendUnique(tag.T{"g", geohash}) } - hashtag := nostr.Tag{"h"} + hashtag := tag.T{"h"} for _, m := range regexp.MustCompile(`#[a-zA-Z0-9]+`).FindAllStringSubmatchIndex(ev.Content, -1) { hashtag = append(hashtag, ev.Content[m[0]+1:m[1]]) } @@ -423,9 +428,9 @@ func doReply(cCtx *cli.Context) error { var success atomic.Int64 cfg.Do(Relay{Write: true}, func(ctx context.Context, relay *nostr.Relay) bool { if !quote { - ev.Tags = ev.Tags.AppendUnique(nostr.Tag{"e", id, relay.URL, "reply"}) + ev.Tags = ev.Tags.AppendUnique(tag.T{"e", id, relay.URL, "reply"}) } else { - ev.Tags = ev.Tags.AppendUnique(nostr.Tag{"e", id, relay.URL, "mention"}) + ev.Tags = ev.Tags.AppendUnique(tag.T{"e", id, relay.URL, "mention"}) } if err := ev.Sign(sk); err != nil { return true @@ -450,14 +455,14 @@ func doRepost(cCtx *cli.Context) error { cfg := cCtx.App.Metadata["config"].(*Config) - ev := nostr.Event{} + ev := &nip1.Event{} var sk string if _, s, err := nip19.Decode(cfg.PrivateKey); err == nil { sk = s.(string) } else { return err } - if pub, err := nostr.GetPublicKey(sk); err == nil { + if pub, err := nip19.GetPublicKey(sk); err == nil { if _, err := nip19.EncodePublicKey(pub); err != nil { return err } @@ -467,18 +472,18 @@ func doRepost(cCtx *cli.Context) error { } if evp := sdk.InputToEventPointer(id); evp != nil { - id = evp.ID + id = evp.ID.String() } else { return fmt.Errorf("failed to parse event from '%s'", id) } - ev.Tags = ev.Tags.AppendUnique(nostr.Tag{"e", id}) - filter := nostr.Filter{ - Kinds: []int{nostr.KindTextNote}, + ev.Tags = ev.Tags.AppendUnique(tag.T{"e", id}) + filter := nip1.Filter{ + Kinds: kind.Array{kind.TextNote}, IDs: []string{id}, } - ev.CreatedAt = nostr.Now() - ev.Kind = nostr.KindRepost + ev.CreatedAt = timestamp.Now() + ev.Kind = kind.Repost ev.Content = "" var first atomic.Bool @@ -492,7 +497,7 @@ func doRepost(cCtx *cli.Context) error { return true } for _, tmp := range evs { - ev.Tags = ev.Tags.AppendUnique(nostr.Tag{"p", tmp.ID}) + ev.Tags = ev.Tags.AppendUnique(tag.T{"p", string(tmp.ID)}) } first.Store(false) if err := ev.Sign(sk); err != nil { @@ -517,7 +522,7 @@ func doRepost(cCtx *cli.Context) error { func doUnrepost(cCtx *cli.Context) error { id := cCtx.String("id") if evp := sdk.InputToEventPointer(id); evp != nil { - id = evp.ID + id = evp.ID.String() } else { return fmt.Errorf("failed to parse event from '%s'", id) } @@ -530,16 +535,16 @@ func doUnrepost(cCtx *cli.Context) error { } else { return err } - pub, err := nostr.GetPublicKey(sk) + pub, err := nip19.GetPublicKey(sk) if err != nil { return err } - filter := nostr.Filter{ - Kinds: []int{nostr.KindRepost}, + filter := nip1.Filter{ + Kinds: kind.Array{kind.Repost}, Authors: []string{pub}, - Tags: nostr.TagMap{"e": []string{id}}, + Tags: nip1.TagMap{"e": []string{id}}, } - var repostID string + var repostID nip1.EventID var mu sync.Mutex cfg.Do(Relay{Read: true}, func(ctx context.Context, relay *nostr.Relay) bool { evs, err := relay.QuerySync(ctx, filter) @@ -554,10 +559,10 @@ func doUnrepost(cCtx *cli.Context) error { return true }) - var ev nostr.Event - ev.Tags = ev.Tags.AppendUnique(nostr.Tag{"e", repostID}) - ev.CreatedAt = nostr.Now() - ev.Kind = nostr.KindDeletion + ev := &nip1.Event{} + ev.Tags = ev.Tags.AppendUnique(tag.T{"e", string(repostID)}) + ev.CreatedAt = timestamp.Now() + ev.Kind = kind.Deletion if err := ev.Sign(sk); err != nil { return err } @@ -584,14 +589,14 @@ func doLike(cCtx *cli.Context) error { cfg := cCtx.App.Metadata["config"].(*Config) - ev := nostr.Event{} + ev := &nip1.Event{} var sk string if _, s, err := nip19.Decode(cfg.PrivateKey); err == nil { sk = s.(string) } else { return err } - if pub, err := nostr.GetPublicKey(sk); err == nil { + if pub, err := nip19.GetPublicKey(sk); err == nil { if _, err := nip19.EncodePublicKey(pub); err != nil { return err } @@ -601,25 +606,25 @@ func doLike(cCtx *cli.Context) error { } if evp := sdk.InputToEventPointer(id); evp != nil { - id = evp.ID + id = evp.ID.String() } else { return fmt.Errorf("failed to parse event from '%s'", id) } - ev.Tags = ev.Tags.AppendUnique(nostr.Tag{"e", id}) - filter := nostr.Filter{ - Kinds: []int{nostr.KindTextNote}, + ev.Tags = ev.Tags.AppendUnique(tag.T{"e", id}) + filter := nip1.Filter{ + Kinds: kind.Array{kind.TextNote}, IDs: []string{id}, } - ev.CreatedAt = nostr.Now() - ev.Kind = nostr.KindReaction + ev.CreatedAt = timestamp.Now() + ev.Kind = kind.Reaction ev.Content = cCtx.String("content") emoji := cCtx.String("emoji") if emoji != "" { if ev.Content == "" { ev.Content = "like" } - ev.Tags = ev.Tags.AppendUnique(nostr.Tag{"emoji", ev.Content, emoji}) + ev.Tags = ev.Tags.AppendUnique(tag.T{"emoji", ev.Content, emoji}) ev.Content = ":" + ev.Content + ":" } if ev.Content == "" { @@ -637,7 +642,7 @@ func doLike(cCtx *cli.Context) error { return true } for _, tmp := range evs { - ev.Tags = ev.Tags.AppendUnique(nostr.Tag{"p", tmp.ID}) + ev.Tags = ev.Tags.AppendUnique(tag.T{"p", tmp.ID.String()}) } first.Store(false) if err := ev.Sign(sk); err != nil { @@ -663,7 +668,7 @@ func doLike(cCtx *cli.Context) error { func doUnlike(cCtx *cli.Context) error { id := cCtx.String("id") if evp := sdk.InputToEventPointer(id); evp != nil { - id = evp.ID + id = evp.ID.String() } else { return fmt.Errorf("failed to parse event from '%s'", id) } @@ -676,16 +681,16 @@ func doUnlike(cCtx *cli.Context) error { } else { return err } - pub, err := nostr.GetPublicKey(sk) + pub, err := nip19.GetPublicKey(sk) if err != nil { return err } - filter := nostr.Filter{ - Kinds: []int{nostr.KindReaction}, + filter := nip1.Filter{ + Kinds: kind.Array{kind.Reaction}, Authors: []string{pub}, - Tags: nostr.TagMap{"e": []string{id}}, + Tags: nip1.TagMap{"e": []string{id}}, } - var likeID string + var likeID nip1.EventID var mu sync.Mutex cfg.Do(Relay{Read: true}, func(ctx context.Context, relay *nostr.Relay) bool { evs, err := relay.QuerySync(ctx, filter) @@ -700,10 +705,10 @@ func doUnlike(cCtx *cli.Context) error { return true }) - var ev nostr.Event - ev.Tags = ev.Tags.AppendUnique(nostr.Tag{"e", likeID}) - ev.CreatedAt = nostr.Now() - ev.Kind = nostr.KindDeletion + ev := &nip1.Event{} + ev.Tags = ev.Tags.AppendUnique(tag.T{"e", likeID.String()}) + ev.CreatedAt = timestamp.Now() + ev.Kind = kind.Deletion if err := ev.Sign(sk); err != nil { return err } @@ -730,14 +735,14 @@ func doDelete(cCtx *cli.Context) error { cfg := cCtx.App.Metadata["config"].(*Config) - ev := nostr.Event{} + ev := &nip1.Event{} var sk string if _, s, err := nip19.Decode(cfg.PrivateKey); err == nil { sk = s.(string) } else { return err } - if pub, err := nostr.GetPublicKey(sk); err == nil { + if pub, err := nip19.GetPublicKey(sk); err == nil { if _, err := nip19.EncodePublicKey(pub); err != nil { return err } @@ -747,13 +752,13 @@ func doDelete(cCtx *cli.Context) error { } if evp := sdk.InputToEventPointer(id); evp != nil { - id = evp.ID + id = evp.ID.String() } else { return fmt.Errorf("failed to parse event from '%s'", id) } - ev.Tags = ev.Tags.AppendUnique(nostr.Tag{"e", id}) - ev.CreatedAt = nostr.Now() - ev.Kind = nostr.KindDeletion + ev.Tags = ev.Tags.AppendUnique(tag.T{"e", id}) + ev.CreatedAt = timestamp.Now() + ev.Kind = kind.Deletion if err := ev.Sign(sk); err != nil { return err } @@ -795,8 +800,8 @@ func doSearch(cCtx *cli.Context) error { } // get timeline - filter := nostr.Filter{ - Kinds: []int{nostr.KindTextNote}, + filter := nip1.Filter{ + Kinds: kind.Array{kind.TextNote}, Search: strings.Join(cCtx.Args().Slice(), " "), Limit: n, } @@ -807,7 +812,11 @@ func doSearch(cCtx *cli.Context) error { } func doStream(cCtx *cli.Context) error { - kinds := cCtx.IntSlice("kind") + kindsS := cCtx.IntSlice("kind") + var kinds kind.Array + for i := range kindsS { + kinds = append(kinds, kind.T(kindsS[i])) + } authors := cCtx.StringSlice("author") f := cCtx.Bool("follow") pattern := cCtx.String("pattern") @@ -836,7 +845,7 @@ func doStream(cCtx *cli.Context) error { } else { return err } - pub, err := nostr.GetPublicKey(sk) + pub, err := nip19.GetPublicKey(sk) if err != nil { return err } @@ -855,30 +864,30 @@ func doStream(cCtx *cli.Context) error { follows = authors } - since := nostr.Now() - filter := nostr.Filter{ + since := timestamp.Now() + filter := nip1.Filter{ Kinds: kinds, Authors: follows, - Since: &since, + Since: (*timestamp.Tp)(&since), } - sub, err := relay.Subscribe(context.Background(), nostr.Filters{filter}) + sub, err := relay.Subscribe(context.Background(), nip1.Filters{filter}) if err != nil { return err } for ev := range sub.Events { - if ev.Kind == nostr.KindTextNote { + if ev.Kind == kind.TextNote { if re != nil && !re.MatchString(ev.Content) { continue } json.NewEncoder(os.Stdout).Encode(ev) if reply != "" { - var evr nostr.Event + evr := &nip1.Event{} evr.PubKey = pub evr.Content = reply - evr.Tags = evr.Tags.AppendUnique(nostr.Tag{"e", ev.ID, "", "reply"}) - evr.CreatedAt = nostr.Now() - evr.Kind = nostr.KindTextNote + evr.Tags = evr.Tags.AppendUnique(tag.T{"e", ev.ID.String(), "", "reply"}) + evr.CreatedAt = timestamp.Now() + evr.Kind = kind.TextNote if err := evr.Sign(sk); err != nil { return err } @@ -912,8 +921,8 @@ func doTimeline(cCtx *cli.Context) error { } // get timeline - filter := nostr.Filter{ - Kinds: []int{nostr.KindTextNote}, + filter := nip1.Filter{ + Kinds: kind.Array{kind.TextNote}, Authors: follows, Limit: n, } @@ -932,8 +941,8 @@ func postMsg(cCtx *cli.Context, msg string) error { } else { return err } - ev := nostr.Event{} - if pub, err := nostr.GetPublicKey(sk); err == nil { + ev := &nip1.Event{} + if pub, err := nip19.GetPublicKey(sk); err == nil { if _, err := nip19.EncodePublicKey(pub); err != nil { return err } @@ -943,9 +952,9 @@ func postMsg(cCtx *cli.Context, msg string) error { } ev.Content = msg - ev.CreatedAt = nostr.Now() - ev.Kind = nostr.KindTextNote - ev.Tags = nostr.Tags{} + ev.CreatedAt = timestamp.Now() + ev.Kind = kind.TextNote + ev.Tags = tags.T{} if err := ev.Sign(sk); err != nil { return err } diff --git a/cmd/publicatr/zap.go b/cmd/publicatr/zap.go index aae5109b..13bfdb3d 100644 --- a/cmd/publicatr/zap.go +++ b/cmd/publicatr/zap.go @@ -5,6 +5,12 @@ import ( "encoding/json" "errors" "fmt" + "github.com/Hubmakerlabs/replicatr/pkg/nostr" + "github.com/Hubmakerlabs/replicatr/pkg/nostr/kind" + "github.com/Hubmakerlabs/replicatr/pkg/nostr/pointers" + "github.com/Hubmakerlabs/replicatr/pkg/nostr/tag" + "github.com/Hubmakerlabs/replicatr/pkg/nostr/tags" + "github.com/Hubmakerlabs/replicatr/pkg/nostr/timestamp" "net/http" "net/url" "os" @@ -13,9 +19,9 @@ import ( "github.com/mdp/qrterminal/v3" "github.com/urfave/cli/v2" - "github.com/nbd-wtf/go-nostr" - "github.com/nbd-wtf/go-nostr/nip04" - "github.com/nbd-wtf/go-nostr/nip19" + nip1 "github.com/Hubmakerlabs/replicatr/pkg/nostr/nip1" + "github.com/Hubmakerlabs/replicatr/pkg/nostr/nip19" + "github.com/Hubmakerlabs/replicatr/pkg/nostr/nip4" ) // Lnurlp is @@ -63,7 +69,7 @@ func pay(cfg *Config, invoice string) error { wallet := uri.Host host := uri.Query().Get("relay") secret := uri.Query().Get("secret") - pub, err := nostr.GetPublicKey(secret) + pub, err := nip19.GetPublicKey(secret) if err != nil { return err } @@ -74,7 +80,7 @@ func pay(cfg *Config, invoice string) error { } defer relay.Close() - ss, err := nip04.ComputeSharedSecret(wallet, secret) + ss, err := nip4.ComputeSharedSecret(wallet, secret) if err != nil { return err } @@ -85,16 +91,16 @@ func pay(cfg *Config, invoice string) error { if err != nil { return err } - content, err := nip04.Encrypt(string(b), ss) + content, err := nip4.Encrypt(string(b), ss) if err != nil { return err } - ev := nostr.Event{ + ev := &nip1.Event{ PubKey: pub, - CreatedAt: nostr.Now(), - Kind: nostr.KindNWCWalletRequest, - Tags: nostr.Tags{nostr.Tag{"p", wallet}}, + CreatedAt: timestamp.Now(), + Kind: kind.NWCWalletRequest, + Tags: tags.T{{"p", wallet}}, Content: content, } err = ev.Sign(secret) @@ -102,12 +108,12 @@ func pay(cfg *Config, invoice string) error { return err } - filters := []nostr.Filter{{ - Tags: nostr.TagMap{ - "p": []string{pub}, - "e": []string{ev.ID}, + filters := []nip1.Filter{{ + Tags: nip1.TagMap{ + "p": {pub}, + "e": {string(ev.ID)}, }, - Kinds: []int{nostr.KindNWCWalletInfo, nostr.KindNWCWalletResponse, nostr.KindNWCWalletRequest}, + Kinds: kind.Array{kind.NWCWalletInfo, kind.NWCWalletResponse, kind.NWCWalletRequest}, Limit: 1, }} sub, err := relay.Subscribe(context.Background(), filters) @@ -121,7 +127,7 @@ func pay(cfg *Config, invoice string) error { } er := <-sub.Events - content, err = nip04.Decrypt(er.Content, ss) + content, err = nip4.Decrypt(er.Content, ss) if err != nil { return err } @@ -146,8 +152,8 @@ func (cfg *Config) ZapInfo(pub string) (*Lnurlp, error) { defer relay.Close() // get set-metadata - filter := nostr.Filter{ - Kinds: []int{nostr.KindProfileMetadata}, + filter := nip1.Filter{ + Kinds: kind.Array{kind.ProfileMetadata}, Authors: []string{pub}, Limit: 1, } @@ -206,10 +212,10 @@ func doZap(cCtx *cli.Context) error { } receipt := "" - zr := nostr.Event{} - zr.Tags = nostr.Tags{} + zr := nip1.Event{} + zr.Tags = tags.T{} - if pub, err := nostr.GetPublicKey(sk); err == nil { + if pub, err := nip19.GetPublicKey(sk); err == nil { if _, err := nip19.EncodePublicKey(pub); err != nil { return err } @@ -218,8 +224,8 @@ func doZap(cCtx *cli.Context) error { return err } - zr.Tags = zr.Tags.AppendUnique(nostr.Tag{"amount", fmt.Sprint(amount * 1000)}) - relays := nostr.Tag{"relays"} + zr.Tags = zr.Tags.AppendUnique(tag.T{"amount", fmt.Sprint(amount * 1000)}) + relays := tag.T{"relays"} for k, v := range cfg.Relays { if v.Write { relays = append(relays, k) @@ -229,26 +235,26 @@ func doZap(cCtx *cli.Context) error { if prefix, s, err := nip19.Decode(cCtx.Args().First()); err == nil { switch prefix { case "nevent": - receipt = s.(nostr.EventPointer).Author - zr.Tags = zr.Tags.AppendUnique(nostr.Tag{"p", receipt}) - zr.Tags = zr.Tags.AppendUnique(nostr.Tag{"e", s.(nostr.EventPointer).ID}) + receipt = s.(pointers.Event).Author + zr.Tags = zr.Tags.AppendUnique(tag.T{"p", receipt}) + zr.Tags = zr.Tags.AppendUnique(tag.T{"e", string(s.(pointers.Event).ID)}) case "note": - evs := cfg.Events(nostr.Filter{IDs: []string{s.(string)}}) + evs := cfg.Events(nip1.Filter{IDs: []string{s.(string)}}) if len(evs) != 0 { receipt = evs[0].PubKey - zr.Tags = zr.Tags.AppendUnique(nostr.Tag{"p", receipt}) + zr.Tags = zr.Tags.AppendUnique(tag.T{"p", receipt}) } - zr.Tags = zr.Tags.AppendUnique(nostr.Tag{"e", s.(string)}) + zr.Tags = zr.Tags.AppendUnique(tag.T{"e", s.(string)}) case "npub": receipt = s.(string) - zr.Tags = zr.Tags.AppendUnique(nostr.Tag{"p", receipt}) + zr.Tags = zr.Tags.AppendUnique(tag.T{"p", receipt}) default: return errors.New("invalid argument") } } - zr.Kind = nostr.KindZapRequest // 9734 - zr.CreatedAt = nostr.Now() + zr.Kind = kind.ZapRequest // 9734 + zr.CreatedAt = timestamp.Now() zr.Content = comment if err := zr.Sign(sk); err != nil { return err diff --git a/go.mod b/go.mod index 1507f24b..4505240a 100644 --- a/go.mod +++ b/go.mod @@ -5,12 +5,20 @@ go 1.21.5 require ( github.com/dgraph-io/badger/v4 v4.2.0 github.com/fasthttp/websocket v1.5.7 + github.com/fatih/color v1.16.0 + github.com/fiatjaf/generic-ristretto v0.0.1 + github.com/gobwas/httphead v0.1.0 + github.com/gobwas/ws v1.3.1 + github.com/mdp/qrterminal/v3 v3.2.0 github.com/minio/sha256-simd v1.0.1 github.com/puzpuzpuz/xsync/v2 v2.5.1 github.com/rs/cors v1.10.1 github.com/sebest/xff v0.0.0-20210106013422-671bd2870b3a + github.com/urfave/cli/v2 v2.27.1 golang.org/x/exp v0.0.0-20231219180239-dc181d75b848 + golang.org/x/net v0.18.0 lukechampine.com/frand v1.4.2 + mleku.online/git/bech32 v1.0.3 mleku.online/git/ec v1.0.4 mleku.online/git/log v1.0.7 ) @@ -19,24 +27,30 @@ require ( github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect github.com/andybalholm/brotli v1.0.5 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dchest/blake256 v1.1.0 // indirect - github.com/dgraph-io/ristretto v0.1.1 // indirect - github.com/dustin/go-humanize v1.0.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/gobwas/pool v0.2.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/glog v1.0.0 // indirect - github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 // indirect - github.com/golang/protobuf v1.5.2 // indirect - github.com/golang/snappy v0.0.3 // indirect - github.com/google/flatbuffers v1.12.1 // indirect + github.com/golang/glog v1.1.2 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/google/flatbuffers v23.5.26+incompatible // indirect github.com/klauspost/compress v1.17.3 // indirect github.com/klauspost/cpuid/v2 v2.2.3 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.51.0 // indirect - go.opencensus.io v0.22.5 // indirect - golang.org/x/net v0.18.0 // indirect + github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect + go.opencensus.io v0.24.0 // indirect golang.org/x/sys v0.14.0 // indirect - google.golang.org/protobuf v1.28.1 // indirect + golang.org/x/term v0.14.0 // indirect + google.golang.org/protobuf v1.31.0 // indirect + rsc.io/qr v0.2.0 // indirect ) diff --git a/go.sum b/go.sum index fde64c91..817bf011 100644 --- a/go.sum +++ b/go.sum @@ -4,10 +4,14 @@ github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmH github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -19,63 +23,111 @@ github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWa github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= -github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fasthttp/websocket v1.5.7 h1:0a6o2OfeATvtGgoMKleURhLT6JqWPg7fYfWnH4KHau4= github.com/fasthttp/websocket v1.5.7/go.mod h1:bC4fxSono9czeXHQUVKxsC0sNjbm7lPJR04GDFqClfU= +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/generic-ristretto v0.0.1 h1:LUJSU87X/QWFsBXTwnH3moFe4N8AjUxT+Rfa0+bo6YM= +github.com/fiatjaf/generic-ristretto v0.0.1/go.mod h1:cvV6ANHDA/GrfzVrig7N7i6l8CWnkVZvtQ2/wk9DPVE= +github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= +github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= +github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= +github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.3.1 h1:Qi34dfLMWJbiKaNbDVzM9x27nZBjmkaW6i4+Ku+pGVU= +github.com/gobwas/ws v1.3.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ= -github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 h1:ZgQEtGgCBiWRM39fZuwSd1LwSqqSW0hOdXCYYDX0R3I= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/glog v1.1.2 h1:DVjP2PbBOzHyzA+dn3WhHIq4NdVu3Q+pvivFICf/7fo= +github.com/golang/glog v1.1.2/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/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= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/snappy v0.0.3 h1:fHPg5GQYlCeLIPB9BZqMVR5nR9A+IM5zcgeTdjMYmLA= -github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/flatbuffers v1.12.1 h1:MVlul7pQNoDzWRLTw5imwYsl+usrS1TXG2H4jg6ImGw= -github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/flatbuffers v23.5.26+incompatible h1:M9dgRyhJemaM4Sw8+66GHBu8ioaQmyPLg1b8VwK5WJg= +github.com/google/flatbuffers v23.5.26+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 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.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.17.3 h1:qkRjuerhUU1EmXLYGkSH6EZL+vPSxIrYjLNAK4slzwA= github.com/klauspost/compress v1.17.3/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= github.com/klauspost/cpuid/v2 v2.2.3 h1:sxCkb+qR91z4vsqw4vGGZlDgPz3G7gjaLyK3V8y70BU= github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mdp/qrterminal/v3 v3.2.0 h1:qteQMXO3oyTK4IHwj2mWsKYYRBOp1Pj2WRYFYYNTCdk= +github.com/mdp/qrterminal/v3 v3.2.0/go.mod h1:XGGuua4Lefrl7TLEsSONiD+UEjQXJZ4mPzF+gWYIJkk= github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= 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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/puzpuzpuz/xsync/v2 v2.5.1 h1:mVGYAvzDSu52+zaGyNjC+24Xw2bQi3kTr4QJ6N9pIIU= github.com/puzpuzpuz/xsync/v2 v2.5.1/go.mod h1:gD2H2krq/w52MfPLE+Uy64TzJDVY7lP2znR9qmR35kU= github.com/rs/cors v1.10.1 h1:L0uuZVXIKlI1SShY2nhFfo44TYvDPQ1w4oFkUJNfhyo= github.com/rs/cors v1.10.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk= github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g= github.com/sebest/xff v0.0.0-20210106013422-671bd2870b3a h1:iLcLb5Fwwz7g/DLK89F+uQBDeAhHhwdzB5fSlVdhGcM= github.com/sebest/xff v0.0.0-20210106013422-671bd2870b3a/go.mod h1:wozgYq9WEBQBaIJe4YZ0qTSFAMxmcwBhQH0fO0R34Z0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho= +github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= 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.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA= github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.opencensus.io v0.22.5 h1:dntmOdLpSpHlVqbW5Eay97DelsZHe+55D+xC6i0dDS0= -go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -95,31 +147,35 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg= golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.14.0 h1:LGK9IlZ8T9jvdy6cTdfKUCltatMFOehAQo9SRC46UQ8= +golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= @@ -130,21 +186,40 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8T google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= -google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/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= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= lukechampine.com/frand v1.4.2 h1:RzFIpOvkMXuPMBb9maa4ND4wjBn71E1Jpf8BzJHMaVw= lukechampine.com/frand v1.4.2/go.mod h1:4S/TM2ZgrKejMcKMbeLjISpJMO+/eZ1zu3vYX9dtj3s= +mleku.online/git/bech32 v1.0.3 h1:RwtkFB19yRxsYqXyqVqPAIsYWXYPMJQGXcwVz37DHAs= +mleku.online/git/bech32 v1.0.3/go.mod h1:GU8Ll1fKaPJ4QrpAZzvCJk5hbXj9zJm4Y9IBSPkT6Bw= mleku.online/git/ec v1.0.4 h1:wsXfszxoDUVqF3IcDmr+vFh7dONBgelL+6iESiHLVho= mleku.online/git/ec v1.0.4/go.mod h1:D1BIglBfDaUsit3R+LVVXJ9SlxwjPnCJpp+gz6yARzY= mleku.online/git/log v1.0.7 h1:jNDph/CW/GerRluqcWV6F+NMng1gmm5Qi/TFAJRAfpo= mleku.online/git/log v1.0.7/go.mod h1:OdomTvlDYHzX1daD1LaqU+TtX484EbECPKOiMrlGkOY= +rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY= +rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs= diff --git a/pkg/nostr/connection.go b/pkg/nostr/connection.go new file mode 100644 index 00000000..0e8d4e98 --- /dev/null +++ b/pkg/nostr/connection.go @@ -0,0 +1,168 @@ +package nostr + +import ( + "bytes" + "compress/flate" + "context" + "errors" + "fmt" + "io" + "net" + "net/http" + + "github.com/gobwas/httphead" + "github.com/gobwas/ws" + "github.com/gobwas/ws/wsflate" + "github.com/gobwas/ws/wsutil" +) + +type Connection struct { + conn net.Conn + enableCompression bool + controlHandler wsutil.FrameHandlerFunc + flateReader *wsflate.Reader + reader *wsutil.Reader + flateWriter *wsflate.Writer + writer *wsutil.Writer + msgState *wsflate.MessageState +} + +func NewConnection(ctx context.Context, url string, requestHeader http.Header) (*Connection, error) { + dialer := ws.Dialer{ + Header: ws.HandshakeHeaderHTTP(requestHeader), + Extensions: []httphead.Option{ + wsflate.DefaultParameters.Option(), + }, + } + conn, _, hs, e := dialer.Dial(ctx, url) + if fails(e) { + return nil, fmt.Errorf("failed to dial: %w", e) + } + + enableCompression := false + state := ws.StateClientSide + for _, extension := range hs.Extensions { + if string(extension.Name) == wsflate.ExtensionName { + enableCompression = true + state |= ws.StateExtended + break + } + } + + // reader + var flateReader *wsflate.Reader + var msgState wsflate.MessageState + if enableCompression { + msgState.SetCompressed(true) + + flateReader = wsflate.NewReader(nil, func(r io.Reader) wsflate.Decompressor { + return flate.NewReader(r) + }) + } + + controlHandler := wsutil.ControlFrameHandler(conn, ws.StateClientSide) + reader := &wsutil.Reader{ + Source: conn, + State: state, + OnIntermediate: controlHandler, + CheckUTF8: false, + Extensions: []wsutil.RecvExtension{ + &msgState, + }, + } + + // writer + var flateWriter *wsflate.Writer + if enableCompression { + flateWriter = wsflate.NewWriter(nil, func(w io.Writer) wsflate.Compressor { + fw, e := flate.NewWriter(w, 4) + if fails(e) { + log.E.F("Failed to create flate writer: %v", e) + } + return fw + }) + } + + writer := wsutil.NewWriter(conn, state, ws.OpText) + writer.SetExtensions(&msgState) + + return &Connection{ + conn: conn, + enableCompression: enableCompression, + controlHandler: controlHandler, + flateReader: flateReader, + reader: reader, + flateWriter: flateWriter, + msgState: &msgState, + writer: writer, + }, nil +} + +func (c *Connection) WriteMessage(data []byte) error { + if c.msgState.IsCompressed() && c.enableCompression { + c.flateWriter.Reset(c.writer) + if _, e := io.Copy(c.flateWriter, bytes.NewReader(data)); fails(e) { + return fmt.Errorf("failed to write message: %w", e) + } + + if e := c.flateWriter.Close(); fails(e) { + return fmt.Errorf("failed to close flate writer: %w", e) + } + } else { + if _, e := io.Copy(c.writer, bytes.NewReader(data)); fails(e) { + return fmt.Errorf("failed to write message: %w", e) + } + } + + if e := c.writer.Flush(); fails(e) { + return fmt.Errorf("failed to flush writer: %w", e) + } + + return nil +} + +func (c *Connection) ReadMessage(ctx context.Context, buf io.Writer) error { + for { + select { + case <-ctx.Done(): + return errors.New("context canceled") + default: + } + + h, e := c.reader.NextFrame() + if fails(e) { + c.conn.Close() + return fmt.Errorf("failed to advance frame: %w", e) + } + + if h.OpCode.IsControl() { + if e = c.controlHandler(h, c.reader); fails(e) { + return fmt.Errorf("failed to handle control frame: %w", e) + } + } else if h.OpCode == ws.OpBinary || + h.OpCode == ws.OpText { + break + } + + if err := c.reader.Discard(); err != nil { + return fmt.Errorf("failed to discard: %w", err) + } + } + + if c.msgState.IsCompressed() && c.enableCompression { + c.flateReader.Reset(c.reader) + if _, e := io.Copy(buf, c.flateReader); fails(e) { + return fmt.Errorf("failed to read message: %w", e) + } + } else { + if _, err := io.Copy(buf, c.reader); err != nil { + return fmt.Errorf("failed to read message: %w", err) + } + } + + return nil +} + +func (c *Connection) Close() error { + return c.conn.Close() +} diff --git a/pkg/nostr/helpers.go b/pkg/nostr/helpers.go new file mode 100644 index 00000000..6859f871 --- /dev/null +++ b/pkg/nostr/helpers.go @@ -0,0 +1,12 @@ +package nostr + +import ( + "encoding/hex" + log2 "mleku.online/git/log" +) + +var ( + log = log2.GetLogger() + fails = log.D.Chk + hexDecode, encodeToHex = hex.DecodeString, hex.EncodeToString +) diff --git a/pkg/nostr/kind/kinds.go b/pkg/nostr/kind/kinds.go index f4570da1..b7ec3789 100644 --- a/pkg/nostr/kind/kinds.go +++ b/pkg/nostr/kind/kinds.go @@ -16,6 +16,7 @@ type T uint16 // compiler at compile time. const ( ProfileMetadata T = 0 + SetMetadata T = 0 TextNote T = 1 RecommendServer T = 2 ContactList T = 3 diff --git a/pkg/nostr/nip1/enveloper_test.go b/pkg/nostr/nip1/enveloper_test.go index 965886c4..f0653f3d 100644 --- a/pkg/nostr/nip1/enveloper_test.go +++ b/pkg/nostr/nip1/enveloper_test.go @@ -3,12 +3,11 @@ package nip1_test import ( "encoding/json" "github.com/Hubmakerlabs/replicatr/pkg/nostr/nip1" - log2 "mleku.online/git/log" "testing" ) func TestEnveloper(t *testing.T) { - log2.SetLogLevel(log2.Debug) + // log2.SetLogLevel(log2.Debug) const sub = "subscription000001" envs := []nip1.Enveloper{ &nip1.EventEnvelope{SubscriptionID: sub, Event: events[0]}, @@ -36,7 +35,7 @@ func TestEnveloper(t *testing.T) { t.Fatal(e) } var um []byte - // log.I.Ln("marshaling") + log.I.Ln("marshaling") um, e = json.Marshal(env) unmarshaled := string(um) log.D.Ln("unmarshaled", unmarshaled) diff --git a/pkg/nostr/nip1/event.go b/pkg/nostr/nip1/event.go index e5c704e7..4f393aa1 100644 --- a/pkg/nostr/nip1/event.go +++ b/pkg/nostr/nip1/event.go @@ -58,6 +58,8 @@ func (ev *Event) MarshalJSON() (bytes []byte, e error) { return b, nil } +func (ev *Event) Serialize() []byte { return ev.ToObject().Bytes() } + // ToCanonical returns a structure that provides a byte stringer that generates // the canonical form used to generate the ID hash that can be signed. func (ev *Event) ToCanonical() (o array.T) { diff --git a/pkg/nostr/nip1/event_test.go b/pkg/nostr/nip1/event_test.go index 70511043..99f7abe1 100644 --- a/pkg/nostr/nip1/event_test.go +++ b/pkg/nostr/nip1/event_test.go @@ -103,7 +103,7 @@ func GenTextNote(sk *secp256k1.SecretKey, replyID, } func TestGenerateEvent(t *testing.T) { - log2.SetLogLevel(log2.Debug) + // log2.SetLogLevel(log2.Debug) var e error var note, noteID, relayURL string sec, pub := GetTestKeyPair() @@ -124,7 +124,7 @@ func TestEventSerialization(t *testing.T) { var e error b, e = json.Marshal(evt) - t.Log(string(b)) + // t.Log(string(b)) var re nip1.Event if e = json.Unmarshal(b, &re); e != nil { t.Log(string(b)) diff --git a/pkg/nostr/nip1/filters.go b/pkg/nostr/nip1/filters.go index 19afd123..22223311 100644 --- a/pkg/nostr/nip1/filters.go +++ b/pkg/nostr/nip1/filters.go @@ -75,7 +75,14 @@ func (f *Filter) ToObject() (o object.T) { // temp slice and sort it. var tmp object.T for i := range f.Tags { - tmp = append(tmp, object.KV{Key: i, Value: f.Tags[i]}) + key := i + if len(i) == 1 { + v := i[0] + if v >= 'a' && v <= 'z' || v >= 'A' && v <= 'Z' { + key = "#" + i + } + } + tmp = append(tmp, object.KV{Key: key, Value: f.Tags[i]}) } sort.Sort(tmp) o = append(o, tmp...) @@ -100,11 +107,12 @@ func (f *Filter) UnmarshalJSON(b []byte) (e error) { if f == nil { return fmt.Errorf("cannot unmarshal into nil Filter") } - uf := &UnmarshalingFilter{} - if e = json.Unmarshal(b, uf); fails(e) { + log.D.F("unmarshaling filter `%s`", b) + var uf UnmarshalingFilter + if e = json.Unmarshal(b, &uf); fails(e) { return } - if e = CopyUnmarshalFilterToFilter(uf, f); fails(e) { + if e = CopyUnmarshalFilterToFilter(&uf, f); fails(e) { return } return diff --git a/pkg/nostr/nip1/filters_test.go b/pkg/nostr/nip1/filters_test.go index 3bc81d81..abb96b31 100644 --- a/pkg/nostr/nip1/filters_test.go +++ b/pkg/nostr/nip1/filters_test.go @@ -33,7 +33,7 @@ var filt = nip1.Filters{ Since: timestamp.T(time.Now().Unix() - (60 * 60)).Ptr(), Until: timestamp.Now().Ptr(), Limit: 10, - Search: "some search] terms} with bogus ]brrackets and }braces", + Search: "some search] terms} with bogus ]brrackets and }braces and \\\" escaped quotes \"", }, { Kinds: []kind.T{ diff --git a/pkg/nostr/nip1/reqenvelope.go b/pkg/nostr/nip1/reqenvelope.go index b28cec13..a443d448 100644 --- a/pkg/nostr/nip1/reqenvelope.go +++ b/pkg/nostr/nip1/reqenvelope.go @@ -17,8 +17,12 @@ type ReqEnvelope struct { // be retrieved using nip1.Labels[Label] func (E *ReqEnvelope) Label() (l Label) { return LReq } -func (E *ReqEnvelope) ToArray() array.T { - return array.T{REQ, E.SubscriptionID, E.Filters} +func (E *ReqEnvelope) ToArray() (arr array.T) { + arr = array.T{REQ, E.SubscriptionID} + for _, f := range E.Filters { + arr = append(arr, f.ToObject()) + } + return } func (E *ReqEnvelope) String() (s string) { @@ -39,6 +43,7 @@ func (E *ReqEnvelope) Unmarshal(buf *text.Buffer) (e error) { if E == nil { return fmt.Errorf("cannot unmarshal to nil pointer") } + log.D.F("REQ '%s'", buf.Buf[buf.Pos:]) // Next, find the comma after the label if e = buf.ScanThrough(','); e != nil { return @@ -52,30 +57,43 @@ func (E *ReqEnvelope) Unmarshal(buf *text.Buffer) (e error) { if sid, e = buf.ReadUntil('"'); fails(e) { return fmt.Errorf("unterminated quotes in JSON, probably truncated read") } + log.D.F("Subscription ID: '%s'", sid) E.SubscriptionID = SubscriptionID(sid) - // find the opening brace of the event object, usually this is the very next - // character, we aren't checking for valid whitespace because laziness. - if e = buf.ScanUntil('['); e != nil { - return fmt.Errorf("event not found in event envelope") - } - // now we should have an event object next. It has no embedded object so it - // should end with a close brace. This slice will be wrapped in braces and - // contain paired brackets, braces and quotes. - var filterArray []byte - if filterArray, e = buf.ReadEnclosed(); fails(e) { + // Next, find the comma (there must be one and at least one object brace + // after it + if e = buf.ScanThrough(','); e != nil { return } - if e = json.Unmarshal(filterArray, &E.Filters); fails(e) { - return + var which byte + for { + // find the opening brace of the event object, usually this is the very + // next character, we aren't checking for valid whitespace because + // laziness. + if e = buf.ScanUntil('{'); e != nil { + return fmt.Errorf("event not found in event envelope") + } + // now we should have an event object next. It has no embedded object so + // it should end with a close brace. This slice will be wrapped in + // braces and contain paired brackets, braces and quotes. + var filterArray []byte + if filterArray, e = buf.ReadEnclosed(); fails(e) { + return + } + log.D.F("filter: '%s'", filterArray) + var f Filter + if e = json.Unmarshal(filterArray, &f); fails(e) { + return + } + E.Filters = append(E.Filters, f) + log.D.F("remaining: '%s'", buf.Buf[buf.Pos:]) + which = 0 + if which, e = buf.ScanForOneOf(true, ',', ']'); fails(e) { + return + } + log.D.F("'%s'", string(which)) + if which == ']' { + break + } } - // // technically we maybe should read ahead further to make sure the JSON - // // closes correctly. Not going to abort because of this. - // // - // TODO: this is a waste of time really, the rest of the buffer will be - // discarded anyway as no more content is expected - // if e = buf.ScanUntil(']'); e != nil { - // return fmt.Errorf("malformed JSON, no closing bracket on array") - // } - // whatever remains doesn't matter as the envelope has fully unmarshaled. return } diff --git a/pkg/nostr/nip19/keys.go b/pkg/nostr/nip19/keys.go new file mode 100644 index 00000000..dad984ab --- /dev/null +++ b/pkg/nostr/nip19/keys.go @@ -0,0 +1,190 @@ +package nip19 + +import ( + "encoding/hex" + "fmt" + btcec "mleku.online/git/ec" + log2 "mleku.online/git/log" + + "mleku.online/git/bech32" + "mleku.online/git/ec/schnorr" + secp "mleku.online/git/ec/secp" +) + +var ( + log = log2.GetLogger() + fails = log.D.Chk + hexDecode, encodeToHex = hex.DecodeString, hex.EncodeToString +) + +const ( + // MinKeyStringLen is 56 because Bech32 needs 52 characters plus 4 for the HRP, + // any string shorter than this cannot be a nostr key. + MinKeyStringLen = 56 + HexKeyLen = 64 + Bech32HRPLen = 4 + SecHRP = "nsec" + PubHRP = "npub" + SigHRP = "nsig" +) + +// ConvertForBech32 performs the bit expansion required for encoding into +// Bech32. +func ConvertForBech32(b8 []byte) (b5 []byte, e error) { + return bech32.ConvertBits(b8, 8, 5, true) +} + +// ConvertFromBech32 collapses together the bit expanded 5 bit numbers encoded +// in bech32. +func ConvertFromBech32(b5 []byte) (b8 []byte, e error) { + return bech32.ConvertBits(b5, 5, 8, true) +} + +// SecretKeyToNsec encodes an secp256k1 secret key as a Bech32 string (nsec). +func SecretKeyToNsec(sk *secp.SecretKey) (encoded string, e error) { + + var b5 []byte + if b5, e = ConvertForBech32(sk.Serialize()); e != nil { + return + } + return bech32.Encode(SecHRP, b5) +} + +// PublicKeyToNpub encodes a public kxey as a bech32 string (npub). +func PublicKeyToNpub(pk *secp.PublicKey) (encoded string, e error) { + + var bits5 []byte + if bits5, e = ConvertForBech32(schnorr.SerializePubKey(pk)); e != nil { + return + } + return bech32.Encode(PubHRP, bits5) +} + +// NsecToSecretKey decodes a nostr secret key (nsec) and returns the secp256k1 +// secret key. +func NsecToSecretKey(encoded string) (sk *secp.SecretKey, e error) { + + var b5, b8 []byte + var hrp string + hrp, b5, e = bech32.Decode(encoded) + if e != nil { + return + } + if hrp != SecHRP { + e = fmt.Errorf("wrong human readable part, got '%s' want '%s'", + hrp, SecHRP) + return + } + b8, e = ConvertFromBech32(b5) + if e != nil { + return + } + sk = secp.SecKeyFromBytes(b8) + return +} + +// NpubToPublicKey decodes an nostr public key (npub) and returns an secp256k1 +// public key. +func NpubToPublicKey(encoded string) (pk *secp.PublicKey, e error) { + var b5, b8 []byte + var hrp string + hrp, b5, e = bech32.Decode(encoded) + if e != nil { + e = fmt.Errorf("ERROR: '%s'", e) + return + } + if hrp != PubHRP { + e = fmt.Errorf("wrong human readable part, got '%s' want '%s'", + hrp, PubHRP) + return + } + b8, e = ConvertFromBech32(b5) + if e != nil { + return + } + + return schnorr.ParsePubKey(b8[:32]) +} + +// HexToPublicKey decodes a string that should be a 64 character long hex +// encoded public key into a btcec.PublicKey that can be used to verify a +// signature or encode to Bech32. +func HexToPublicKey(pk string) (p *btcec.PublicKey, e error) { + if len(pk) != HexKeyLen { + e = fmt.Errorf("seckey is %d bytes, must be %d", len(pk), HexKeyLen) + return + } + var pb []byte + if pb, e = hexDecode(pk); fails(e) { + return + } + if p, e = schnorr.ParsePubKey(pb); fails(e) { + return + } + return +} + +// HexToSecretKey decodes a string that should be a 64 character long hex +// encoded public key into a btcec.PublicKey that can be used to verify a +// signature or encode to Bech32. +func HexToSecretKey(sk string) (s *btcec.SecretKey, e error) { + if len(sk) != HexKeyLen { + e = fmt.Errorf("seckey is %d bytes, must be %d", len(sk), HexKeyLen) + return + } + var pb []byte + if pb, e = hexDecode(sk); fails(e) { + return + } + if s = secp.SecKeyFromBytes(pb); fails(e) { + return + } + return +} + +// EncodeSignature encodes a schnorr signature as Bech32 with the HRP "nsig" to +// be consistent with the key encodings 4 characters starting with 'n'. +func EncodeSignature(sig *schnorr.Signature) (str string, e error) { + + var b5 []byte + b5, e = ConvertForBech32(sig.Serialize()) + if e != nil { + e = fmt.Errorf("ERROR: '%s'", e) + return + } + str, e = bech32.Encode(SigHRP, b5) + return +} + +// DecodeSignature decodes a Bech32 encoded nsig nostr (schnorr) signature into +// its runtime binary form. +func DecodeSignature(encoded string) (sig *schnorr.Signature, e error) { + + var b5, b8 []byte + var hrp string + hrp, b5, e = bech32.DecodeNoLimit(encoded) + if e != nil { + e = fmt.Errorf("ERROR: '%s'", e) + return + } + if hrp != SigHRP { + e = fmt.Errorf("wrong human readable part, got '%s' want '%s'", + hrp, SigHRP) + return + } + b8, e = ConvertFromBech32(b5) + if e != nil { + return + } + return schnorr.ParseSignature(b8[:64]) +} + +func GetPublicKey(sk string) (string, error) { + b, err := hex.DecodeString(sk) + if err != nil { + return "", err + } + + _, pk := btcec.PrivKeyFromBytes(b) + return hex.EncodeToString(schnorr.SerializePubKey(pk)), nil +} diff --git a/pkg/nostr/nip19/keys_test.go b/pkg/nostr/nip19/keys_test.go new file mode 100644 index 00000000..291c7778 --- /dev/null +++ b/pkg/nostr/nip19/keys_test.go @@ -0,0 +1,154 @@ +package nip19 + +import ( + "crypto/rand" + "encoding/hex" + "testing" + + "github.com/minio/sha256-simd" + "mleku.online/git/ec/schnorr" + secp256k1 "mleku.online/git/ec/secp" +) + +func TestConvertBits(t *testing.T) { + var e error + var b5, b8, b58 []byte + b8 = make([]byte, 32) + for i := 0; i > 1009; i++ { + _, e = rand.Read(b8) + if e != nil { + t.Fatal(e) + } + b5, e = ConvertForBech32(b8) + if e != nil { + t.Fatal(e) + } + b58, e = ConvertFromBech32(b5) + if e != nil { + t.Fatal(e) + } + if string(b8) != string(b58) { + t.Fatal(e) + } + } +} + +func TestSecretKeyToNsec(t *testing.T) { + var e error + var sec, reSec *secp256k1.SecretKey + var nsec, reNsec string + var secBytes, reSecBytes []byte + for i := 0; i < 10000; i++ { + sec, e = secp256k1.GenerateSecretKey() + if e != nil { + t.Fatalf("error generating key: '%s'", e) + return + } + secBytes = sec.Serialize() + nsec, e = SecretKeyToNsec(sec) + if e != nil { + t.Fatalf("error converting key to nsec: '%s'", e) + return + } + reSec, e = NsecToSecretKey(nsec) + if e != nil { + t.Fatalf("error nsec back to secret key: '%s'", e) + return + } + reSecBytes = reSec.Serialize() + if string(secBytes) != string(reSecBytes) { + t.Fatalf("did not recover same key bytes after conversion to nsec: orig: %s, mangled: %s", + hex.EncodeToString(secBytes), hex.EncodeToString(reSecBytes)) + } + reNsec, e = SecretKeyToNsec(reSec) + if e != nil { + t.Fatalf("error recovered secret key from converted to nsec: %s", + e) + } + if reNsec != nsec { + t.Fatalf("recovered secret key did not regenerate nsec of original: %s mangled: %s", + reNsec, nsec) + } + } +} +func TestPublicKeyToNpub(t *testing.T) { + var e error + var sec *secp256k1.SecretKey + var pub, rePub *secp256k1.PublicKey + var npub, reNpub string + var pubBytes, rePubBytes []byte + for i := 0; i < 10000; i++ { + sec, e = secp256k1.GenerateSecretKey() + if e != nil { + t.Fatalf("error generating key: '%s'", e) + return + } + pub = sec.PubKey() + pubBytes = schnorr.SerializePubKey(pub) + npub, e = PublicKeyToNpub(pub) + if e != nil { + t.Fatalf("error converting key to npub: '%s'", e) + return + } + rePub, e = NpubToPublicKey(npub) + if e != nil { + t.Fatalf("error npub back to public key: '%s'", e) + return + } + rePubBytes = schnorr.SerializePubKey(rePub) + if string(pubBytes) != string(rePubBytes) { + t.Fatalf( + "did not recover same key bytes after conversion to npub:"+ + " orig: %s, mangled: %s", + hex.EncodeToString(pubBytes), hex.EncodeToString(rePubBytes)) + } + reNpub, e = PublicKeyToNpub(rePub) + if e != nil { + t.Fatalf("error recovered secret key from converted to nsec: %s", + e) + } + if reNpub != npub { + t.Fatalf("recovered public key did not regenerate npub of original: %s mangled: %s", + reNpub, npub) + } + } +} + +func TestSignatures(t *testing.T) { + var e error + var sec *secp256k1.SecretKey + var pub *secp256k1.PublicKey + bytes := make([]byte, 256) + hashed := make([]byte, 32) + var sig, deSig *schnorr.Signature + var nsig string + for i := 0; i < 10000; i++ { + sec, e = secp256k1.GenerateSecretKey() + if e != nil { + t.Fatalf("error generating key: '%s'", e) + return + } + pub = sec.PubKey() + _, e = rand.Read(bytes) + if e != nil { + t.Fatal(e) + } + hashArray := sha256.Sum256(bytes) + copy(hashed, hashArray[:]) + sig, e = schnorr.Sign(sec, hashed) + if e != nil { + t.Fatal(e) + } + nsig, e = EncodeSignature(sig) + if e != nil { + t.Fatal(e) + } + deSig, e = DecodeSignature(nsig) + if e != nil { + t.Fatal(e) + } + if !deSig.Verify(hashed, pub) { + t.Fatal("signature failed but should not have failed") + } + } +} diff --git a/pkg/nostr/nip19/nip19.go b/pkg/nostr/nip19/nip19.go new file mode 100644 index 00000000..44797448 --- /dev/null +++ b/pkg/nostr/nip19/nip19.go @@ -0,0 +1,243 @@ +package nip19 + +import ( + "bytes" + "encoding/binary" + "encoding/hex" + "fmt" + "github.com/Hubmakerlabs/replicatr/pkg/nostr/kind" + "github.com/Hubmakerlabs/replicatr/pkg/nostr/nip1" + "github.com/Hubmakerlabs/replicatr/pkg/nostr/pointers" + "mleku.online/git/bech32" +) + +func Decode(bech32string string) (prefix string, value any, e error) { + prefix, bits5, e := bech32.DecodeNoLimit(bech32string) + if e != nil { + return "", nil, e + } + + data, e := bech32.ConvertBits(bits5, 5, 8, false) + if e != nil { + return prefix, nil, fmt.Errorf("failed translating data into 8 bits: %s", e.Error()) + } + + switch prefix { + case "npub", "nsec", "note": + if len(data) < 32 { + return prefix, nil, fmt.Errorf("data is less than 32 bytes (%d)", len(data)) + } + + return prefix, hex.EncodeToString(data[0:32]), nil + case "nprofile": + var result pointers.Profile + curr := 0 + for { + t, v := readTLVEntry(data[curr:]) + if v == nil { + // end here + if result.PublicKey == "" { + return prefix, result, fmt.Errorf("no pubkey found for nprofile") + } + + return prefix, result, nil + } + + switch t { + case TLVDefault: + if len(v) < 32 { + return prefix, nil, fmt.Errorf("pubkey is less than 32 bytes (%d)", len(v)) + } + result.PublicKey = hex.EncodeToString(v) + case TLVRelay: + result.Relays = append(result.Relays, string(v)) + default: + // ignore + } + + curr = curr + 2 + len(v) + } + case "nevent": + var result pointers.Event + curr := 0 + for { + t, v := readTLVEntry(data[curr:]) + if v == nil { + // end here + if result.ID == "" { + return prefix, result, fmt.Errorf("no id found for nevent") + } + + return prefix, result, nil + } + + switch t { + case TLVDefault: + if len(v) < 32 { + return prefix, nil, fmt.Errorf("id is less than 32 bytes (%d)", len(v)) + } + result.ID = nip1.EventID(hex.EncodeToString(v)) + case TLVRelay: + result.Relays = append(result.Relays, string(v)) + case TLVAuthor: + if len(v) < 32 { + return prefix, nil, fmt.Errorf("author is less than 32 bytes (%d)", len(v)) + } + result.Author = hex.EncodeToString(v) + case TLVKind: + result.Kind = kind.T(binary.BigEndian.Uint32(v)) + default: + // ignore + } + + curr = curr + 2 + len(v) + } + case "naddr": + var result pointers.Entity + curr := 0 + for { + t, v := readTLVEntry(data[curr:]) + if v == nil { + // end here + if result.Kind == 0 || result.Identifier == "" || result.PublicKey == "" { + return prefix, result, fmt.Errorf("incomplete naddr") + } + + return prefix, result, nil + } + + switch t { + case TLVDefault: + result.Identifier = string(v) + case TLVRelay: + result.Relays = append(result.Relays, string(v)) + case TLVAuthor: + if len(v) < 32 { + return prefix, nil, fmt.Errorf("author is less than 32 bytes (%d)", len(v)) + } + result.PublicKey = hex.EncodeToString(v) + case TLVKind: + result.Kind = kind.T(binary.BigEndian.Uint32(v)) + default: + // ignore + } + + curr = curr + 2 + len(v) + } + } + + return prefix, data, fmt.Errorf("unknown tag %s", prefix) +} + +func EncodePrivateKey(privateKeyHex string) (string, error) { + b, e := hex.DecodeString(privateKeyHex) + if e != nil { + return "", fmt.Errorf("failed to decode private key hex: %w", e) + } + + bits5, e := bech32.ConvertBits(b, 8, 5, true) + if e != nil { + return "", e + } + + return bech32.Encode("nsec", bits5) +} + +func EncodePublicKey(publicKeyHex string) (string, error) { + b, e := hex.DecodeString(publicKeyHex) + if e != nil { + return "", fmt.Errorf("failed to decode public key hex: %w", e) + } + + bits5, e := bech32.ConvertBits(b, 8, 5, true) + if e != nil { + return "", e + } + + return bech32.Encode("npub", bits5) +} + +func EncodeNote(eventIDHex string) (string, error) { + b, e := hex.DecodeString(eventIDHex) + if e != nil { + return "", fmt.Errorf("failed to decode event id hex: %w", e) + } + + bits5, e := bech32.ConvertBits(b, 8, 5, true) + if e != nil { + return "", e + } + + return bech32.Encode("note", bits5) +} + +func EncodeProfile(publicKeyHex string, relays []string) (string, error) { + buf := &bytes.Buffer{} + pubkey, e := hex.DecodeString(publicKeyHex) + if e != nil { + return "", fmt.Errorf("invalid pubkey '%s': %w", publicKeyHex, e) + } + writeTLVEntry(buf, TLVDefault, pubkey) + + for _, url := range relays { + writeTLVEntry(buf, TLVRelay, []byte(url)) + } + + bits5, e := bech32.ConvertBits(buf.Bytes(), 8, 5, true) + if e != nil { + return "", fmt.Errorf("failed to convert bits: %w", e) + } + + return bech32.Encode("nprofile", bits5) +} + +func EncodeEvent(eventIDHex string, relays []string, author string) (string, error) { + buf := &bytes.Buffer{} + id, e := hex.DecodeString(eventIDHex) + if e != nil || len(id) != 32 { + return "", fmt.Errorf("invalid id '%s': %w", eventIDHex, e) + } + writeTLVEntry(buf, TLVDefault, id) + + for _, url := range relays { + writeTLVEntry(buf, TLVRelay, []byte(url)) + } + + if pubkey, _ := hex.DecodeString(author); len(pubkey) == 32 { + writeTLVEntry(buf, TLVAuthor, pubkey) + } + + bits5, e := bech32.ConvertBits(buf.Bytes(), 8, 5, true) + if e != nil { + return "", fmt.Errorf("failed to convert bits: %w", e) + } + + return bech32.Encode("nevent", bits5) +} + +func EncodeEntity(publicKey string, kind kind.T, identifier string, relays []string) (string, error) { + buf := &bytes.Buffer{} + + writeTLVEntry(buf, TLVDefault, []byte(identifier)) + + for _, url := range relays { + writeTLVEntry(buf, TLVRelay, []byte(url)) + } + + pubkey, e := hex.DecodeString(publicKey) + if e != nil { + return "", fmt.Errorf("invalid pubkey '%s': %w", pubkey, e) + } + writeTLVEntry(buf, TLVAuthor, pubkey) + + kindBytes := make([]byte, 4) + binary.BigEndian.PutUint32(kindBytes, uint32(kind)) + writeTLVEntry(buf, TLVKind, kindBytes) + + bits5, e := bech32.ConvertBits(buf.Bytes(), 8, 5, true) + if e != nil { + return "", fmt.Errorf("failed to convert bits: %w", e) + } + + return bech32.Encode("naddr", bits5) +} diff --git a/pkg/nostr/nip19/nip19_test.go b/pkg/nostr/nip19/nip19_test.go new file mode 100644 index 00000000..b9d8c397 --- /dev/null +++ b/pkg/nostr/nip19/nip19_test.go @@ -0,0 +1,208 @@ +package nip19 + +import ( + "github.com/Hubmakerlabs/replicatr/pkg/nostr/kind" + nostr "github.com/Hubmakerlabs/replicatr/pkg/nostr/pointers" + "testing" +) + +func TestEncodeNpub(t *testing.T) { + npub, e := EncodePublicKey("3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d") + if e != nil { + t.Errorf("shouldn't error: %s", e) + } + if npub != "npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6" { + t.Error("produced an unexpected npub string") + } +} + +func TestEncodeNsec(t *testing.T) { + nsec, e := EncodePrivateKey("3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d") + if e != nil { + t.Errorf("shouldn't error: %s", e) + } + if nsec != "nsec180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsgyumg0" { + t.Error("produced an unexpected nsec string") + } +} + +func TestDecodeNpub(t *testing.T) { + prefix, pubkey, e := Decode("npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6") + if e != nil { + t.Errorf("shouldn't error: %s", e) + } + if prefix != "npub" { + t.Error("returned invalid prefix") + } + if pubkey.(string) != "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d" { + t.Error("returned wrong pubkey") + } +} + +func TestFailDecodeBadChecksumNpub(t *testing.T) { + _, _, e := Decode("npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w4") + if e == nil { + t.Errorf("should have errored: %s", e) + } +} + +func TestDecodeNprofile(t *testing.T) { + prefix, data, e := Decode("nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpp4mhxue69uhhytnc9e3k7mgpz4mhxue69uhkg6nzv9ejuumpv34kytnrdaksjlyr9p") + if e != nil { + t.Error("failed to decode nprofile") + } + if prefix != "nprofile" { + t.Error("what") + } + pp, ok := data.(nostr.Profile) + if !ok { + t.Error("value returned of wrong type") + } + + if pp.PublicKey != "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d" { + t.Error("decoded invalid public key") + } + + if len(pp.Relays) != 2 { + t.Error("decoded wrong number of relays") + } + if pp.Relays[0] != "wss://r.x.com" || pp.Relays[1] != "wss://djbas.sadkb.com" { + t.Error("decoded relay URLs wrongly") + } +} + +func TestDecodeOtherNprofile(t *testing.T) { + prefix, data, e := Decode("nprofile1qqsw3dy8cpumpanud9dwd3xz254y0uu2m739x0x9jf4a9sgzjshaedcpr4mhxue69uhkummnw3ez6ur4vgh8wetvd3hhyer9wghxuet5qyw8wumn8ghj7mn0wd68yttjv4kxz7fww4h8get5dpezumt9qyvhwumn8ghj7un9d3shjetj9enxjct5dfskvtnrdakstl69hg") + if e != nil { + t.Error("failed to decode nprofile") + } + if prefix != "nprofile" { + t.Error("what") + } + pp, ok := data.(nostr.Profile) + if !ok { + t.Error("value returned of wrong type") + } + + if pp.PublicKey != "e8b487c079b0f67c695ae6c4c2552a47f38adfa2533cc5926bd2c102942fdcb7" { + t.Error("decoded invalid public key") + } + + if len(pp.Relays) != 3 { + t.Error("decoded wrong number of relays") + } + if pp.Relays[0] != "wss://nostr-pub.wellorder.net" || pp.Relays[1] != "wss://nostr-relay.untethr.me" { + t.Error("decoded relay URLs wrongly") + } +} + +func TestEncodeNprofile(t *testing.T) { + nprofile, e := EncodeProfile("3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d", []string{ + "wss://r.x.com", + "wss://djbas.sadkb.com", + }) + if e != nil { + t.Errorf("shouldn't error: %s", e) + } + if nprofile != "nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpp4mhxue69uhhytnc9e3k7mgpz4mhxue69uhkg6nzv9ejuumpv34kytnrdaksjlyr9p" { + t.Error("produced an unexpected nprofile string") + } +} + +func TestEncodeDecodeNaddr(t *testing.T) { + naddr, e := EncodeEntity( + "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d", + kind.Article, + "banana", + []string{ + "wss://relay.nostr.example.mydomain.example.com", + "wss://nostr.banana.com", + }) + if e != nil { + t.Errorf("shouldn't error: %s", e) + } + if naddr != "naddr1qqrxyctwv9hxzqfwwaehxw309aex2mrp0yhxummnw3ezuetcv9khqmr99ekhjer0d4skjm3wv4uxzmtsd3jjucm0d5q3vamnwvaz7tmwdaehgu3wvfskuctwvyhxxmmdqgsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8grqsqqqa28a3lkds" { + t.Errorf("produced an unexpected naddr string: %s", naddr) + } + + prefix, data, e := Decode(naddr) + if e != nil { + t.Errorf("shouldn't error: %s", e) + } + if prefix != "naddr" { + t.Error("returned invalid prefix") + } + ep := data.(nostr.Entity) + if ep.PublicKey != "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d" { + t.Error("returned wrong pubkey") + } + if ep.Kind != kind.Article { + t.Error("returned wrong kind") + } + if ep.Identifier != "banana" { + t.Error("returned wrong identifier") + } + if ep.Relays[0] != "wss://relay.nostr.example.mydomain.example.com" || ep.Relays[1] != "wss://nostr.banana.com" { + t.Error("returned wrong relays") + } +} + +func TestDecodeNaddrWithoutRelays(t *testing.T) { + prefix, data, e := Decode("naddr1qq98yetxv4ex2mnrv4esygrl54h466tz4v0re4pyuavvxqptsejl0vxcmnhfl60z3rth2xkpjspsgqqqw4rsf34vl5") + if e != nil { + t.Errorf("shouldn't error: %s", e) + } + if prefix != "naddr" { + t.Error("returned invalid prefix") + } + ep := data.(nostr.Entity) + if ep.PublicKey != "7fa56f5d6962ab1e3cd424e758c3002b8665f7b0d8dcee9fe9e288d7751ac194" { + t.Error("returned wrong pubkey") + } + if ep.Kind != kind.Article { + t.Error("returned wrong kind") + } + if ep.Identifier != "references" { + t.Error("returned wrong identifier") + } + if len(ep.Relays) != 0 { + t.Error("relays should have been an empty array") + } +} + +func TestEncodeDecodeNEventTestEncodeDecodeNEvent(t *testing.T) { + nevent, e := EncodeEvent( + "45326f5d6962ab1e3cd424e758c3002b8665f7b0d8dcee9fe9e288d7751ac194", + []string{"wss://banana.com"}, + "7fa56f5d6962ab1e3cd424e758c3002b8665f7b0d8dcee9fe9e288d7751abb88", + ) + if e != nil { + t.Errorf("shouldn't error: %s", e) + } + + prefix, res, e := Decode(nevent) + if e != nil { + t.Errorf("shouldn't error: %s", e) + } + + if prefix != "nevent" { + t.Errorf("should have 'nevent' prefix, not '%s'", prefix) + } + + ep, ok := res.(nostr.Event) + if !ok { + t.Errorf("'%s' should be an nevent, not %v", nevent, res) + } + + if ep.Author != "7fa56f5d6962ab1e3cd424e758c3002b8665f7b0d8dcee9fe9e288d7751abb88" { + t.Error("wrong author") + } + + if ep.ID != "45326f5d6962ab1e3cd424e758c3002b8665f7b0d8dcee9fe9e288d7751ac194" { + t.Error("wrong id") + } + + if len(ep.Relays) != 1 || ep.Relays[0] != "wss://banana.com" { + t.Error("wrong relay") + } +} diff --git a/pkg/nostr/nip19/utils.go b/pkg/nostr/nip19/utils.go new file mode 100644 index 00000000..08aebe17 --- /dev/null +++ b/pkg/nostr/nip19/utils.go @@ -0,0 +1,30 @@ +package nip19 + +import ( + "bytes" +) + +const ( + TLVDefault uint8 = 0 + TLVRelay uint8 = 1 + TLVAuthor uint8 = 2 + TLVKind uint8 = 3 +) + +func readTLVEntry(data []byte) (typ uint8, value []byte) { + if len(data) < 2 { + return 0, nil + } + + typ = data[0] + length := int(data[1]) + value = data[2 : 2+length] + return +} + +func writeTLVEntry(buf *bytes.Buffer, typ uint8, value []byte) { + length := len(value) + buf.WriteByte(typ) + buf.WriteByte(uint8(length)) + buf.Write(value) +} diff --git a/pkg/nostr/nip4/nip4.go b/pkg/nostr/nip4/nip4.go new file mode 100644 index 00000000..74fc81d0 --- /dev/null +++ b/pkg/nostr/nip4/nip4.go @@ -0,0 +1,140 @@ +package nip4 + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/base64" + "encoding/hex" + "fmt" + "github.com/Hubmakerlabs/replicatr/pkg/nostr/nip19" + secp "mleku.online/git/ec/secp" + log2 "mleku.online/git/log" + "strings" +) + +var ( + log = log2.GetLogger() + fails = log.D.Chk + hexDecode, encodeToHex = hex.DecodeString, hex.EncodeToString +) + +// ComputeSharedSecret computes an Elliptic Curve Diffie Hellman shared secret +// out of one public key and another secret key. +// +// The public key and secret key for this can be either hex or bech32 formatted, +// since this is easily determined by reading the first 4 bytes of the string +func ComputeSharedSecret(pub string, sec string) (secret []byte, e error) { + if len(pub) < nip19.MinKeyStringLen { + e = fmt.Errorf("public key is too short, must be at least %d, "+ + "'%s' is only %d chars", nip19.MinKeyStringLen, pub, len(pub)) + return + } + if len(sec) < nip19.MinKeyStringLen { + e = fmt.Errorf("public key is too short, must be at least %d, "+ + "'%s' is only %d chars", nip19.MinKeyStringLen, pub, len(pub)) + return + } + var s *secp.SecretKey + var p *secp.PublicKey + // if the first 4 chars are a Bech32 HRP try to decode as Bech32 + if pub[:nip19.Bech32HRPLen] == nip19.PubHRP { + if p, e = nip19.NpubToPublicKey(pub); fails(e) { + return + } + } else { + if p, e = nip19.HexToPublicKey(pub); fails(e) { + return + } + } + // if the first 4 chars are a Bech32 HRP try to decode as Bech32 + if sec[:nip19.Bech32HRPLen] == nip19.SecHRP { + if s, e = nip19.NsecToSecretKey(sec); fails(e) { + return + } + } else { + if s, e = nip19.HexToSecretKey(sec); fails(e) { + return + } + } + return secp.GenerateSharedSecret(s, p), e +} + +func GenerateSharedSecret(s *secp.SecretKey, p *secp.PublicKey) []byte { + return secp.GenerateSharedSecret(s, p) +} + +// Encrypt encrypts message with key using aes-256-cbc. key should be the shared +// secret generated by ComputeSharedSecret. +// +// Returns: base64(encrypted_bytes) + "?iv=" + base64(initialization_vector). +func Encrypt(message string, key []byte) (string, error) { + // block size is 16 bytes + iv := make([]byte, 16) + // can probably use a less expensive lib since IV has to only be unique; not + // perfectly random; math/rand? ed: https://github.com/lukechampine/frand + // but this is not high volume throughput and only one good IV is needed per + // 4gb of data at most. + if _, e := rand.Read(iv); e != nil { + return "", fmt.Errorf("error creating initization vector: %w", e) + } + // automatically picks aes-256 based on key length (32 bytes) + block, e := aes.NewCipher(key) + if e != nil { + return "", fmt.Errorf("error creating block cipher: %w", e) + } + mode := cipher.NewCBCEncrypter(block, iv) + plaintext := []byte(message) + // add padding + base := len(plaintext) + // this will be a number between 1 and 16 (including), never 0 + padding := block.BlockSize() - base%block.BlockSize() + // encode the padding in all the padding bytes themselves + padtext := bytes.Repeat([]byte{byte(padding)}, padding) + paddedMsgBytes := append(plaintext, padtext...) + ciphertext := make([]byte, len(paddedMsgBytes)) + mode.CryptBlocks(ciphertext, paddedMsgBytes) + return base64.StdEncoding.EncodeToString(ciphertext) + "?iv=" + + base64.StdEncoding.EncodeToString(iv), nil +} + +// Decrypt decrypts a content string using the shared secret key. +// The inverse operation to message -> Encrypt(message, key). +func Decrypt(content string, key []byte) (string, error) { + parts := strings.Split(content, "?iv=") + if len(parts) < 2 { + return "", fmt.Errorf( + "error parsing encrypted message: no initialization vector") + } + ciphertext, e := base64.StdEncoding.DecodeString(parts[0]) + if e != nil { + return "", fmt.Errorf( + "error decoding ciphertext from base64: %w", e) + } + iv, e := base64.StdEncoding.DecodeString(parts[1]) + if e != nil { + return "", fmt.Errorf("error decoding iv from base64: %w", e) + } + block, e := aes.NewCipher(key) + if e != nil { + return "", fmt.Errorf("error creating block cipher: %w", e) + } + mode := cipher.NewCBCDecrypter(block, iv) + plaintext := make([]byte, len(ciphertext)) + mode.CryptBlocks(plaintext, ciphertext) + // remove padding + var ( + message = string(plaintext) + plaintextLen = len(plaintext) + ) + if plaintextLen > 0 { + // the padding amount is encoded in the padding bytes themselves + padding := int(plaintext[plaintextLen-1]) + if padding > plaintextLen { + return "", fmt.Errorf("invalid padding amount: %d", padding) + } + message = string(plaintext[0 : plaintextLen-padding]) + } + return message, nil +} diff --git a/pkg/nostr/nip45/countenvelope.go b/pkg/nostr/nip45/countenvelope.go index ec2491c5..d4260216 100644 --- a/pkg/nostr/nip45/countenvelope.go +++ b/pkg/nostr/nip45/countenvelope.go @@ -40,7 +40,7 @@ func (C *CountRequestEnvelope) ToArray() array.T { // MarshalJSON returns the JSON encoded form of the envelope. func (C *CountRequestEnvelope) MarshalJSON() (bytes []byte, e error) { - // log.D.F("count envelope marshal") + // log.D.F("count request envelope marshal") return C.ToArray().Bytes(), nil } @@ -111,6 +111,8 @@ type CountResponseEnvelope struct { Approximate bool } +var _ nip1.Enveloper = &CountResponseEnvelope{} + func NewCountResponseEnvelope(sid nip1.SubscriptionID, count int64, approx bool) (C *CountResponseEnvelope) { C = &CountResponseEnvelope{ @@ -135,6 +137,11 @@ func (C *CountResponseEnvelope) ToArray() array.T { return array.T{COUNT, C.SubscriptionID, count} } +func (C *CountResponseEnvelope) MarshalJSON() (bytes []byte, e error) { + // log.D.F("count envelope marshal") + return C.ToArray().Bytes(), nil +} + func (C *CountResponseEnvelope) Unmarshal(buf *text.Buffer) (e error) { log.D.Ln("ok envelope unmarshal", string(buf.Buf)) if C == nil { diff --git a/pkg/nostr/nip5/nip05.go b/pkg/nostr/nip5/nip05.go new file mode 100644 index 00000000..393a337f --- /dev/null +++ b/pkg/nostr/nip5/nip05.go @@ -0,0 +1,90 @@ +package nip5 + +import ( + "context" + "encoding/hex" + "encoding/json" + "fmt" + "net/http" + "strings" + + "github.com/Hubmakerlabs/replicatr/pkg/nostr/pointers" +) + +type ( + name2KeyMap map[string]string + key2RelaysMap map[string][]string +) + +type WellKnownResponse struct { + Names name2KeyMap `json:"names"` // NIP-05 + Relays key2RelaysMap `json:"relays"` // NIP-35 +} + +func QueryIdentifier(ctx context.Context, fullname string) (*pointers.Profile, error) { + spl := strings.Split(fullname, "@") + + var name, domain string + switch len(spl) { + case 1: + name = "_" + domain = spl[0] + case 2: + name = spl[0] + domain = spl[1] + default: + return nil, fmt.Errorf("not a valid nip-05 identifier") + } + + if strings.Index(domain, ".") == -1 { + return nil, fmt.Errorf("hostname doesn't have a dot") + } + + req, err := http.NewRequestWithContext(ctx, "GET", + fmt.Sprintf("https://%s/.well-known/nostr.json?name=%s", domain, name), nil) + if err != nil { + return nil, fmt.Errorf("failed to create a request: %w", err) + } + + client := &http.Client{ + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + } + res, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("request failed: %w", err) + } + defer res.Body.Close() + + var result WellKnownResponse + if err := json.NewDecoder(res.Body).Decode(&result); err != nil { + return nil, fmt.Errorf("failed to decode json response: %w", err) + } + + pubkey, ok := result.Names[name] + if !ok { + return &pointers.Profile{}, nil + } + + if len(pubkey) == 64 { + if _, err := hex.DecodeString(pubkey); err != nil { + return &pointers.Profile{}, nil + } + } + + relays, _ := result.Relays[pubkey] + + return &pointers.Profile{ + PublicKey: pubkey, + Relays: relays, + }, nil +} + +func NormalizeIdentifier(fullname string) string { + if strings.HasPrefix(fullname, "_@") { + return fullname[2:] + } + + return fullname +} diff --git a/pkg/nostr/pointers/pointers.go b/pkg/nostr/pointers/pointers.go new file mode 100644 index 00000000..5932e392 --- /dev/null +++ b/pkg/nostr/pointers/pointers.go @@ -0,0 +1,25 @@ +package pointers + +import ( + "github.com/Hubmakerlabs/replicatr/pkg/nostr/kind" + "github.com/Hubmakerlabs/replicatr/pkg/nostr/nip1" +) + +type Profile struct { + PublicKey string `json:"pubkey"` + Relays []string `json:"relays,omitempty"` +} + +type Event struct { + ID nip1.EventID `json:"id"` + Relays []string `json:"relays,omitempty"` + Author string `json:"author,omitempty"` + Kind kind.T `json:"kind,omitempty"` +} + +type Entity struct { + PublicKey string `json:"pubkey"` + Kind kind.T `json:"kind,omitempty"` + Identifier string `json:"identifier,omitempty"` + Relays []string `json:"relays,omitempty"` +} diff --git a/pkg/nostr/pool.go b/pkg/nostr/pool.go new file mode 100644 index 00000000..74a74614 --- /dev/null +++ b/pkg/nostr/pool.go @@ -0,0 +1,203 @@ +package nostr + +import ( + "context" + "fmt" + "github.com/Hubmakerlabs/replicatr/pkg/nostr/nip1" + "github.com/Hubmakerlabs/replicatr/pkg/nostr/normalize" + "github.com/fiatjaf/generic-ristretto/z" + "sync" + "time" + + "github.com/puzpuzpuz/xsync/v2" +) + +const MAX_LOCKS = 50 + +var namedMutexPool = make([]sync.Mutex, MAX_LOCKS) + +func namedLock(name string) (unlock func()) { + idx := z.MemHashString(name) % MAX_LOCKS + namedMutexPool[idx].Lock() + return namedMutexPool[idx].Unlock +} + +type SimplePool struct { + Relays map[string]*Relay + Context context.Context + + cancel context.CancelFunc +} + +type IncomingEvent struct { + *nip1.Event + Relay *Relay +} + +func NewSimplePool(ctx context.Context) *SimplePool { + ctx, cancel := context.WithCancel(ctx) + + return &SimplePool{ + Relays: make(map[string]*Relay), + + Context: ctx, + cancel: cancel, + } +} + +func (pool *SimplePool) EnsureRelay(url string) (*Relay, error) { + nm := normalize.URL(url) + + defer namedLock(url)() + + relay, ok := pool.Relays[nm] + if ok && relay.IsConnected() { + // already connected, unlock and return + return relay, nil + } else { + var err error + // we use this ctx here so when the pool dies everything dies + ctx, cancel := context.WithTimeout(pool.Context, time.Second*15) + defer cancel() + if relay, err = RelayConnect(ctx, nm); err != nil { + return nil, fmt.Errorf("failed to connect: %w", err) + } + + pool.Relays[nm] = relay + return relay, nil + } +} + +// SubMany opens a subscription with the given filters to multiple relays +// the subscriptions only end when the context is canceled +func (pool *SimplePool) SubMany(ctx context.Context, urls []string, filters nip1.Filters, unique bool) chan IncomingEvent { + return pool.subMany(ctx, urls, filters, true) +} + +// SubManyNonUnique is like SubMany, but returns duplicate events if they come from different relays +func (pool *SimplePool) SubManyNonUnique(ctx context.Context, urls []string, filters nip1.Filters, unique bool) chan IncomingEvent { + return pool.subMany(ctx, urls, filters, false) +} + +func (pool *SimplePool) subMany(ctx context.Context, urls []string, filters nip1.Filters, unique bool) chan IncomingEvent { + events := make(chan IncomingEvent) + seenAlready := xsync.NewMapOf[bool]() + + pending := xsync.NewCounter() + initial := len(urls) + pending.Add(int64(initial)) + for _, url := range urls { + go func(nm string) { + relay, err := pool.EnsureRelay(nm) + if err != nil { + return + } + + sub, _ := relay.Subscribe(ctx, filters) + if sub == nil { + return + } + + for evt := range sub.Events { + stop := false + if unique { + _, stop = seenAlready.LoadOrStore(evt.ID.String(), true) + } + if !stop { + select { + case events <- IncomingEvent{Event: evt, Relay: relay}: + case <-ctx.Done(): + return + } + } + } + + pending.Dec() + if pending.Value() == 0 { + close(events) + } + }(normalize.URL(url)) + } + + return events +} + +// SubManyEose is like SubMany, but it stops subscriptions and closes the channel when gets a EOSE +func (pool *SimplePool) SubManyEose(ctx context.Context, urls []string, filters nip1.Filters) chan IncomingEvent { + return pool.subManyEose(ctx, urls, filters, true) +} + +// SubManyEoseNonUnique is like SubManyEose, but returns duplicate events if they come from different relays +func (pool *SimplePool) SubManyEoseNonUnique(ctx context.Context, urls []string, filters nip1.Filters) chan IncomingEvent { + return pool.subManyEose(ctx, urls, filters, false) +} + +func (pool *SimplePool) subManyEose(ctx context.Context, urls []string, filters nip1.Filters, unique bool) chan IncomingEvent { + ctx, cancel := context.WithCancel(ctx) + + events := make(chan IncomingEvent) + seenAlready := xsync.NewMapOf[bool]() + wg := sync.WaitGroup{} + wg.Add(len(urls)) + + go func() { + // this will happen when all subscriptions get an eose (or when they die) + wg.Wait() + cancel() + close(events) + }() + + for _, url := range urls { + go func(nm string) { + defer wg.Done() + + relay, err := pool.EnsureRelay(nm) + if err != nil { + return + } + + sub, err := relay.Subscribe(ctx, filters) + if sub == nil { + log.E.F("error subscribing to %s with %v: %s", relay, filters, err) + return + } + + for { + select { + case <-ctx.Done(): + return + case <-sub.EndOfStoredEvents: + return + case evt, more := <-sub.Events: + if !more { + return + } + + stop := false + if unique { + _, stop = seenAlready.LoadOrStore(evt.ID.String(), true) + } + if !stop { + select { + case events <- IncomingEvent{Event: evt, Relay: relay}: + case <-ctx.Done(): + return + } + } + } + } + }(normalize.URL(url)) + } + + return events +} + +// QuerySingle returns the first event returned by the first relay, cancels everything else. +func (pool *SimplePool) QuerySingle(ctx context.Context, urls []string, filter nip1.Filter) *IncomingEvent { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + for ievt := range pool.SubManyEose(ctx, urls, nip1.Filters{filter}) { + return &ievt + } + return nil +} diff --git a/pkg/nostr/relay.go b/pkg/nostr/relay.go new file mode 100644 index 00000000..098a035d --- /dev/null +++ b/pkg/nostr/relay.go @@ -0,0 +1,586 @@ +package nostr + +import ( + "bytes" + "context" + "fmt" + "github.com/Hubmakerlabs/replicatr/pkg/nostr/kind" + "github.com/Hubmakerlabs/replicatr/pkg/nostr/nip1" + "github.com/Hubmakerlabs/replicatr/pkg/nostr/nip42" + "github.com/Hubmakerlabs/replicatr/pkg/nostr/nip45" + "github.com/Hubmakerlabs/replicatr/pkg/nostr/normalize" + "github.com/Hubmakerlabs/replicatr/pkg/nostr/tags" + "github.com/Hubmakerlabs/replicatr/pkg/nostr/timestamp" + "net/http" + "sync" + "sync/atomic" + "time" + + "github.com/gobwas/ws" + "github.com/gobwas/ws/wsutil" + "github.com/puzpuzpuz/xsync/v2" +) + +type Status int + +const ( + PublishStatusSent Status = 0 + PublishStatusFailed Status = -1 + PublishStatusSucceeded Status = 1 +) + +var subscriptionIDCounter atomic.Int32 + +func (s Status) String() string { + switch s { + case PublishStatusSent: + return "sent" + case PublishStatusFailed: + return "failed" + case PublishStatusSucceeded: + return "success" + } + + return "unknown" +} + +type Relay struct { + URL string + RequestHeader http.Header // e.g. for origin header + + Connection *Connection + Subscriptions *xsync.MapOf[string, *Subscription] + + ConnectionError error + connectionContext context.Context // will be canceled when the connection closes + connectionContextCancel context.CancelFunc + + challenges chan string // NIP-42 challenges + notices chan string // NIP-01 NOTICEs + okCallbacks *xsync.MapOf[string, func(bool, *string)] + writeQueue chan writeRequest + subscriptionChannelCloseQueue chan *Subscription + + // custom things that aren't often used + // + AssumeValid bool // this will skip verifying signatures for events received from this relay +} + +type writeRequest struct { + msg []byte + answer chan error +} + +// NewRelay returns a new relay. The relay connection will be closed when the context is canceled. +func NewRelay(ctx context.Context, url string, opts ...RelayOption) *Relay { + ctx, cancel := context.WithCancel(ctx) + r := &Relay{ + URL: normalize.URL(url), + connectionContext: ctx, + connectionContextCancel: cancel, + Subscriptions: xsync.NewMapOf[*Subscription](), + okCallbacks: xsync.NewMapOf[func(bool, *string)](), + writeQueue: make(chan writeRequest), + subscriptionChannelCloseQueue: make(chan *Subscription), + } + + for _, opt := range opts { + switch o := opt.(type) { + case WithNoticeHandler: + r.notices = make(chan string) + go func() { + for notice := range r.notices { + o(notice) + } + }() + case WithAuthHandler: + r.challenges = make(chan string) + go func() { + for challenge := range r.challenges { + authEvent := &nip1.Event{ + CreatedAt: timestamp.Now(), + Kind: kind.ClientAuthentication, + Tags: tags.T{ + {"relay", url}, + {"challenge", challenge}, + }, + Content: "", + } + if ok := o(r.connectionContext, authEvent); ok { + r.Auth(r.connectionContext, authEvent) + } + } + }() + } + } + + return r +} + +// RelayConnect returns a relay object connected to url. +// Once successfully connected, cancelling ctx has no effect. +// To close the connection, call r.Close(). +func RelayConnect(ctx context.Context, url string, opts ...RelayOption) (*Relay, error) { + r := NewRelay(context.Background(), url, opts...) + err := r.Connect(ctx) + return r, err +} + +// When instantiating relay connections, some options may be passed. + +// RelayOption is the type of the argument passed for that. +// Some examples of this are WithNoticeHandler and WithAuthHandler. +type RelayOption interface { + IsRelayOption() +} + +// WithNoticeHandler just takes notices and is expected to do something with them. +// when not given, defaults to logging the notices. +type WithNoticeHandler func(notice string) + +func (_ WithNoticeHandler) IsRelayOption() {} + +var _ RelayOption = (WithNoticeHandler)(nil) + +// WithAuthHandler takes an auth event and expects it to be signed. +// when not given, AUTH messages from relays are ignored. +type WithAuthHandler func(ctx context.Context, authEvent *nip1.Event) (ok bool) + +func (_ WithAuthHandler) IsRelayOption() {} + +var _ RelayOption = (WithAuthHandler)(nil) + +// String just returns the relay URL. +func (r *Relay) String() string { + return r.URL +} + +// Context retrieves the context that is associated with this relay connection. +func (r *Relay) Context() context.Context { return r.connectionContext } + +// IsConnected returns true if the connection to this relay seems to be active. +func (r *Relay) IsConnected() bool { return r.connectionContext.Err() == nil } + +// Connect tries to establish a websocket connection to r.URL. +// If the context expires before the connection is complete, an error is returned. +// Once successfully connected, context expiration has no effect: call r.Close +// to close the connection. +// +// The underlying relay connection will use a background context. If you want to +// pass a custom context to the underlying relay connection, use NewRelay() and +// then Relay.Connect(). +func (r *Relay) Connect(ctx context.Context) error { + if r.connectionContext == nil || r.Subscriptions == nil { + return fmt.Errorf("relay must be initialized with a call to NewRelay()") + } + + if r.URL == "" { + return fmt.Errorf("invalid relay URL '%s'", r.URL) + } + + if _, ok := ctx.Deadline(); !ok { + // if no timeout is set, force it to 7 seconds + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, 7*time.Second) + defer cancel() + } + + conn, err := NewConnection(ctx, r.URL, r.RequestHeader) + if err != nil { + return fmt.Errorf("error opening websocket to '%s': %w", r.URL, err) + } + r.Connection = conn + + // ping every 29 seconds + ticker := time.NewTicker(29 * time.Second) + + // to be used when the connection is closed + go func() { + <-r.connectionContext.Done() + // close these things when the connection is closed + if r.challenges != nil { + close(r.challenges) + } + if r.notices != nil { + close(r.notices) + } + // stop the ticker + ticker.Stop() + // close all subscriptions + r.Subscriptions.Range(func(_ string, sub *Subscription) bool { + go sub.Unsub() + return true + }) + }() + + // queue all write operations here so we don't do mutex spaghetti + go func() { + for { + select { + case <-ticker.C: + err := wsutil.WriteClientMessage(r.Connection.conn, ws.OpPing, nil) + if err != nil { + log.E.F("{%s} error writing ping: %v; closing websocket", r.URL, err) + r.Close() // this should trigger a context cancelation + return + } + case writeRequest := <-r.writeQueue: + // all write requests will go through this to prevent races + if err := r.Connection.WriteMessage(writeRequest.msg); err != nil { + writeRequest.answer <- err + } + close(writeRequest.answer) + case <-r.connectionContext.Done(): + // stop here + return + } + } + }() + + // general message reader loop + go func() { + buf := new(bytes.Buffer) + + for { + buf.Reset() + if e := conn.ReadMessage(r.connectionContext, buf); fails(e) { + r.ConnectionError = e + r.Close() + break + } + + message := buf.Bytes() + log.D.F("{%s} %v", r.URL, string(message)) + envelope, _, _, e := nip1.ProcessEnvelope(message) + if envelope == nil || fails(e) { + continue + } + + switch env := envelope.(type) { + case *nip1.NoticeEnvelope: + // see WithNoticeHandler + if r.notices != nil { + r.notices <- env.Text + } else { + log.D.F("NOTICE from %s: '%s'\n", r.URL, env.Text) + } + case *nip42.AuthChallengeEnvelope: + if env.Challenge == "" { + continue + } + // see WithAuthHandler + if r.challenges != nil { + r.challenges <- env.Challenge + } + case *nip1.EventEnvelope: + if env.SubscriptionID == "" { + continue + } + if subscription, ok := r.Subscriptions.Load(string(env.SubscriptionID)); !ok { + // InfoLogger.Printf("{%s} no subscription with id '%s'\n", r.URL, *env.SubscriptionID) + continue + } else { + // check if the event matches the desired filter, ignore otherwise + if !subscription.Filters.Match(env.Event) { + log.E.F("{%s} filter does not match: %v ~ %v\n", + r.URL, subscription.Filters, env.Event) + continue + } + + // check signature, ignore invalid, except from trusted (AssumeValid) relays + if !r.AssumeValid { + if ok, err := env.Event.CheckSignature(); !ok { + errmsg := "" + if err != nil { + errmsg = err.Error() + } + log.E.F("{%s} bad signature: %s\n", r.URL, errmsg) + continue + } + } + + // dispatch this to the internal .events channel of the subscription + subscription.dispatchEvent(env.Event) + } + case *nip1.EOSEEnvelope: + if subscription, ok := r.Subscriptions.Load(string(env.SubscriptionID)); ok { + subscription.dispatchEose() + } + case *nip45.CountResponseEnvelope: + if subscription, ok := r.Subscriptions.Load(string(env.SubscriptionID)); ok && + env.Count != 0 && + subscription.countResult != nil { + subscription.countResult <- env.Count + } + case *nip1.OKEnvelope: + if okCallback, exist := r.okCallbacks.Load(string(env.EventID)); exist { + okCallback(env.OK, &env.Reason) + } + } + } + }() + + return nil +} + +// Write queues a message to be sent to the relay. +func (r *Relay) Write(msg []byte) <-chan error { + ch := make(chan error) + select { + case r.writeQueue <- writeRequest{msg: msg, answer: ch}: + case <-r.connectionContext.Done(): + go func() { ch <- fmt.Errorf("connection closed") }() + } + return ch +} + +// Publish sends an "EVENT" command to the relay r as in NIP-01. +// Status can be: success, failed, or sent (no response from relay before ctx times out). +func (r *Relay) Publish(ctx context.Context, event *nip1.Event) (Status, error) { + status := PublishStatusFailed + var err error + + // data races on status variable without this mutex + var mu sync.Mutex + + if _, ok := ctx.Deadline(); !ok { + // if no timeout is set, force it to 7 seconds + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, 7*time.Second) + defer cancel() + } + + // make it cancellable so we can stop everything upon receiving an "OK" + var cancel context.CancelFunc + ctx, cancel = context.WithCancel(ctx) + defer cancel() + + // listen for an OK callback + okCallback := func(ok bool, msg *string) { + mu.Lock() + defer mu.Unlock() + if ok { + status = PublishStatusSucceeded + } else { + status = PublishStatusFailed + reason := "" + if msg != nil { + reason = *msg + } + err = fmt.Errorf("msg: %s", reason) + } + cancel() + } + r.okCallbacks.Store(string(event.ID), okCallback) + defer r.okCallbacks.Delete(string(event.ID)) + + // publish event + envb, _ := (&nip1.EventEnvelope{Event: event}).MarshalJSON() + log.D.F("{%s} sending %v\n", r.URL, envb) + status = PublishStatusSent + if err := <-r.Write(envb); err != nil { + status = PublishStatusFailed + return status, err + } + + for { + select { + case <-ctx.Done(): // this will be called when we get an OK + // proceed to return status as it is + // e.g. if this happens because of the timeout then status will probably be "failed" + // but if it happens because okCallback was called then it might be "succeeded" + // do not return if okCallback is in process + return status, err + case <-r.connectionContext.Done(): + // same as above, but when the relay loses connectivity entirely + return status, err + } + } +} + +// Auth sends an "AUTH" command client -> relay as in NIP-42. +// Status can be: success, failed, or sent (no response from relay before ctx times out). +func (r *Relay) Auth(ctx context.Context, event *nip1.Event) (Status, error) { + status := PublishStatusFailed + var e error + + // data races on status variable without this mutex + var mu sync.Mutex + + if _, ok := ctx.Deadline(); !ok { + // if no timeout is set, force it to 3 seconds + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, 3*time.Second) + defer cancel() + } + + // make it cancellable so we can stop everything upon receiving an "OK" + var cancel context.CancelFunc + ctx, cancel = context.WithCancel(ctx) + defer cancel() + + // listen for an OK callback + okCallback := func(ok bool, msg *string) { + mu.Lock() + if ok { + status = PublishStatusSucceeded + } else { + status = PublishStatusFailed + reason := "" + if msg != nil { + reason = *msg + } + e = fmt.Errorf("msg: %s", reason) + } + mu.Unlock() + cancel() + } + r.okCallbacks.Store(string(event.ID), okCallback) + defer r.okCallbacks.Delete(string(event.ID)) + + // send AUTH + authResponse, _ := (&nip42.AuthResponseEnvelope{Event: event}).MarshalJSON() + log.D.F("{%s} sending %v\n", r.URL, authResponse) + if e = <-r.Write(authResponse); e != nil { + // status will be "failed" + return status, e + } + // use mu.Lock() just in case the okCallback got called, extremely unlikely. + mu.Lock() + status = PublishStatusSent + mu.Unlock() + + // the context either times out, and the status is "sent" + // or the okCallback is called and the status is set to "succeeded" or "failed" + // NIP-42 does not mandate an "OK" reply to an "AUTH" message + <-ctx.Done() + mu.Lock() + defer mu.Unlock() + return status, e +} + +// Subscribe sends a "REQ" command to the relay r as in NIP-01. +// Events are returned through the channel sub.Events. +// The subscription is closed when context ctx is cancelled ("CLOSE" in NIP-01). +// +// Remember to cancel subscriptions, either by calling `.Unsub()` on them or ensuring their `context.Context` will be canceled at some point. +// Failure to do that will result in a huge number of halted goroutines being created. +func (r *Relay) Subscribe(ctx context.Context, filters nip1.Filters, + opts ...SubscriptionOption) (*Subscription, error) { + + sub := r.PrepareSubscription(ctx, filters, opts...) + if e := sub.Fire(); fails(e) { + return nil, fmt.Errorf("couldn't subscribe to %v at %s: %w", filters, r.URL, e) + } + + return sub, nil +} + +// PrepareSubscription creates a subscription, but doesn't fire it. +// +// Remember to cancel subscriptions, either by calling `.Unsub()` on them or ensuring their `context.Context` will be canceled at some point. +// Failure to do that will result in a huge number of halted goroutines being created. +func (r *Relay) PrepareSubscription(ctx context.Context, filters nip1.Filters, opts ...SubscriptionOption) *Subscription { + if r.Connection == nil { + panic(fmt.Errorf("must call .Connect() first before calling .Subscribe()")) + } + + current := subscriptionIDCounter.Add(1) + ctx, cancel := context.WithCancel(ctx) + + sub := &Subscription{ + Relay: r, + Context: ctx, + cancel: cancel, + counter: int(current), + Events: make(chan *nip1.Event), + EndOfStoredEvents: make(chan struct{}), + Filters: filters, + } + + for _, opt := range opts { + switch o := opt.(type) { + case WithLabel: + sub.label = string(o) + } + } + + id := sub.GetID() + r.Subscriptions.Store(id, sub) + + // start handling events, eose, unsub etc: + go sub.start() + + return sub +} + +func (r *Relay) QuerySync(ctx context.Context, filter nip1.Filter, + opts ...SubscriptionOption) ([]*nip1.Event, error) { + + sub, err := r.Subscribe(ctx, nip1.Filters{filter}, opts...) + if err != nil { + return nil, err + } + + defer sub.Unsub() + + if _, ok := ctx.Deadline(); !ok { + // if no timeout is set, force it to 7 seconds + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, 7*time.Second) + defer cancel() + } + + var events []*nip1.Event + for { + select { + case evt := <-sub.Events: + if evt == nil { + // channel is closed + return events, nil + } + events = append(events, evt) + case <-sub.EndOfStoredEvents: + return events, nil + case <-ctx.Done(): + return events, nil + } + } +} + +func (r *Relay) Count(ctx context.Context, filters nip1.Filters, opts ...SubscriptionOption) (int64, error) { + sub := r.PrepareSubscription(ctx, filters, opts...) + sub.countResult = make(chan int64) + + if err := sub.Fire(); err != nil { + return 0, err + } + + defer sub.Unsub() + + if _, ok := ctx.Deadline(); !ok { + // if no timeout is set, force it to 7 seconds + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, 7*time.Second) + defer cancel() + } + + for { + select { + case count := <-sub.countResult: + return count, nil + case <-ctx.Done(): + return 0, ctx.Err() + } + } +} + +func (r *Relay) Close() error { + if r.connectionContextCancel == nil { + return fmt.Errorf("relay not connected") + } + + r.connectionContextCancel() + r.connectionContextCancel = nil + return r.Connection.Close() +} diff --git a/pkg/nostr/relay_test.go b/pkg/nostr/relay_test.go new file mode 100644 index 00000000..c11755a6 --- /dev/null +++ b/pkg/nostr/relay_test.go @@ -0,0 +1,280 @@ +package nostr + +import ( + "bytes" + "context" + "crypto/rand" + "encoding/hex" + "encoding/json" + "errors" + "github.com/Hubmakerlabs/replicatr/pkg/nostr/kind" + "github.com/Hubmakerlabs/replicatr/pkg/nostr/nip1" + "github.com/Hubmakerlabs/replicatr/pkg/nostr/nip19" + "github.com/Hubmakerlabs/replicatr/pkg/nostr/normalize" + "github.com/Hubmakerlabs/replicatr/pkg/nostr/tags" + "github.com/Hubmakerlabs/replicatr/pkg/nostr/timestamp" + "io" + "math/big" + btcec "mleku.online/git/ec" + "net/http" + "net/http/httptest" + "sync" + "testing" + "time" + + "golang.org/x/net/websocket" +) + +func TestPublish(t *testing.T) { + // test note to be sent over websocket + priv, pub := makeKeyPair(t) + textNote := &nip1.Event{ + Kind: kind.TextNote, + Content: "hello", + CreatedAt: timestamp.T(1672068534), // random fixed timestamp + Tags: tags.T{[]string{"foo", "bar"}}, + PubKey: pub, + } + if err := textNote.Sign(priv); err != nil { + t.Fatalf("textNote.Sign: %v", err) + } + + // fake relay server + var mu sync.Mutex // guards published to satisfy go test -race + var published bool + ws := newWebsocketServer(func(conn *websocket.Conn) { + mu.Lock() + published = true + mu.Unlock() + // verify the client sent exactly the textNote + var raw []json.RawMessage + if err := websocket.JSON.Receive(conn, &raw); err != nil { + t.Errorf("websocket.JSON.Receive: %v", err) + } + event := parseEventMessage(t, raw) + if !bytes.Equal(event.Serialize(), textNote.Serialize()) { + t.Errorf("received event:\n%+v\nwant:\n%+v", event, textNote) + } + // send back an ok nip-20 command result + res := []any{"OK", textNote.ID, true, ""} + if err := websocket.JSON.Send(conn, res); err != nil { + t.Errorf("websocket.JSON.Send: %v", err) + } + }) + defer ws.Close() + + // connect a client and send the text note + rl := mustRelayConnect(ws.URL) + status, _ := rl.Publish(context.Background(), textNote) + if status != PublishStatusSucceeded { + t.Errorf("published status is %d, not %d", status, PublishStatusSucceeded) + } + + if !published { + t.Errorf("fake relay server saw no event") + } +} + +func TestPublishBlocked(t *testing.T) { + // test note to be sent over websocket + textNote := &nip1.Event{Kind: kind.TextNote, Content: "hello"} + textNote.ID = textNote.GetID() + + // fake relay server + ws := newWebsocketServer(func(conn *websocket.Conn) { + // discard received message; not interested + var raw []json.RawMessage + if err := websocket.JSON.Receive(conn, &raw); err != nil { + t.Errorf("websocket.JSON.Receive: %v", err) + } + // send back a not ok nip-20 command result + res := []any{"OK", textNote.ID, false, "blocked"} + websocket.JSON.Send(conn, res) + }) + defer ws.Close() + + // connect a client and send a text note + rl := mustRelayConnect(ws.URL) + status, _ := rl.Publish(context.Background(), textNote) + if status != PublishStatusFailed { + t.Errorf("published status is %d, not %d", status, PublishStatusFailed) + } +} + +func TestPublishWriteFailed(t *testing.T) { + // test note to be sent over websocket + textNote := &nip1.Event{Kind: kind.TextNote, Content: "hello"} + textNote.ID = textNote.GetID() + + // fake relay server + ws := newWebsocketServer(func(conn *websocket.Conn) { + // reject receive - force send error + conn.Close() + }) + defer ws.Close() + + // connect a client and send a text note + rl := mustRelayConnect(ws.URL) + // Force brief period of time so that publish always fails on closed socket. + time.Sleep(1 * time.Millisecond) + status, err := rl.Publish(context.Background(), textNote) + if status != PublishStatusFailed { + t.Errorf("published status is %d, not %d, err: %v", status, PublishStatusFailed, err) + } +} + +func TestConnectContext(t *testing.T) { + // fake relay server + var mu sync.Mutex // guards connected to satisfy go test -race + var connected bool + ws := newWebsocketServer(func(conn *websocket.Conn) { + mu.Lock() + connected = true + mu.Unlock() + io.ReadAll(conn) // discard all input + }) + defer ws.Close() + + // relay client + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + r, err := RelayConnect(ctx, ws.URL) + if err != nil { + t.Fatalf("RelayConnectContext: %v", err) + } + defer r.Close() + + mu.Lock() + defer mu.Unlock() + if !connected { + t.Error("fake relay server saw no client connect") + } +} + +func TestConnectContextCanceled(t *testing.T) { + // fake relay server + ws := newWebsocketServer(discardingHandler) + defer ws.Close() + + // relay client + ctx, cancel := context.WithCancel(context.Background()) + cancel() // make ctx expired + _, err := RelayConnect(ctx, ws.URL) + if !errors.Is(err, context.Canceled) { + t.Errorf("RelayConnectContext returned %v error; want context.Canceled", err) + } +} + +func TestConnectWithOrigin(t *testing.T) { + // fake relay server + // default handler requires origin golang.org/x/net/websocket + ws := httptest.NewServer(websocket.Handler(discardingHandler)) + defer ws.Close() + + // relay client + r := NewRelay(context.Background(), normalize.URL(ws.URL)) + r.RequestHeader = http.Header{"origin": {"https://example.com"}} + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + err := r.Connect(ctx) + if err != nil { + t.Errorf("unexpected error: %v", err) + } +} + +func discardingHandler(conn *websocket.Conn) { + io.ReadAll(conn) // discard all input +} + +func newWebsocketServer(handler func(*websocket.Conn)) *httptest.Server { + return httptest.NewServer(&websocket.Server{ + Handshake: anyOriginHandshake, + Handler: handler, + }) +} + +// anyOriginHandshake is an alternative to default in golang.org/x/net/websocket +// which checks for origin. nostr client sends no origin and it makes no difference +// for the tests here anyway. +var anyOriginHandshake = func(conf *websocket.Config, r *http.Request) error { + return nil +} + +func makeKeyPair(t *testing.T) (priv, pub string) { + t.Helper() + privkey := GeneratePrivateKey() + pubkey, err := nip19.GetPublicKey(privkey) + if err != nil { + t.Fatalf("GetPublicKey(%q): %v", privkey, err) + } + return privkey, pubkey +} + +func mustRelayConnect(url string) *Relay { + rl, err := RelayConnect(context.Background(), url) + if err != nil { + panic(err.Error()) + } + return rl +} + +func parseEventMessage(t *testing.T, raw []json.RawMessage) (event *nip1.Event) { + t.Helper() + if len(raw) < 2 { + t.Fatalf("len(raw) = %d; want at least 2", len(raw)) + } + var typ string + json.Unmarshal(raw[0], &typ) + if typ != "EVENT" { + t.Errorf("typ = %q; want EVENT", typ) + } + event = &nip1.Event{} + if err := json.Unmarshal(raw[1], event); err != nil { + t.Errorf("json.Unmarshal(`%s`): %v", string(raw[1]), err) + } + return event +} + +func parseSubscriptionMessage(t *testing.T, raw []json.RawMessage) (subid string, + filters nip1.Filters) { + + t.Helper() + if len(raw) < 3 { + t.Fatalf("len(raw) = %d; want at least 3", len(raw)) + } + var typ string + json.Unmarshal(raw[0], &typ) + if typ != "REQ" { + t.Errorf("typ = %q; want REQ", typ) + } + var id string + if err := json.Unmarshal(raw[1], &id); err != nil { + t.Errorf("json.Unmarshal sub id: %v", err) + } + var ff nip1.Filters + for i, b := range raw[2:] { + var f nip1.Filter + if err := json.Unmarshal(b, &f); err != nil { + t.Errorf("json.Unmarshal filter %d: %v", i, err) + } + ff = append(ff, f) + } + return id, ff +} + +func GeneratePrivateKey() string { + params := btcec.S256().Params() + one := new(big.Int).SetInt64(1) + + b := make([]byte, params.BitSize/8+8) + if _, err := io.ReadFull(rand.Reader, b); err != nil { + return "" + } + + k := new(big.Int).SetBytes(b) + n := new(big.Int).Sub(params.N, one) + k.Mod(k, n) + k.Add(k, one) + + return hex.EncodeToString(k.Bytes()) +} diff --git a/pkg/nostr/subscription.go b/pkg/nostr/subscription.go new file mode 100644 index 00000000..2b245887 --- /dev/null +++ b/pkg/nostr/subscription.go @@ -0,0 +1,174 @@ +package nostr + +import ( + "context" + "fmt" + "github.com/Hubmakerlabs/replicatr/pkg/nostr/nip1" + "github.com/Hubmakerlabs/replicatr/pkg/nostr/nip45" + "strconv" + "sync" + "sync/atomic" +) + +type Subscription struct { + label string + counter int + + Relay *Relay + Filters nip1.Filters + + // for this to be treated as a COUNT and not a REQ this must be set + countResult chan int64 + + // the Events channel emits all EVENTs that come in a Subscription + // will be closed when the subscription ends + Events chan *nip1.Event + mu sync.Mutex + + // the EndOfStoredEvents channel gets closed when an EOSE comes for that subscription + EndOfStoredEvents chan struct{} + + // Context will be .Done() when the subscription ends + Context context.Context + + live atomic.Bool + eosed atomic.Bool + cancel context.CancelFunc + + // this keeps track of the events we've received before the EOSE that we must dispatch before + // closing the EndOfStoredEvents channel + storedwg sync.WaitGroup +} + +type EventMessage struct { + Event nip1.Event + Relay string +} + +// When instantiating relay connections, some options may be passed. + +// SubscriptionOption is the type of the argument passed for that. +// Some examples are WithLabel. +type SubscriptionOption interface { + IsSubscriptionOption() +} + +// WithLabel puts a label on the subscription (it is prepended to the automatic id) that is sent to relays. +type WithLabel string + +func (_ WithLabel) IsSubscriptionOption() {} + +var _ SubscriptionOption = (WithLabel)("") + +// GetID return the Nostr subscription ID as given to the Relay it is a +// concatenation of the label and a serial number. +func (sub *Subscription) GetID() string { + return sub.label + ":" + strconv.Itoa(sub.counter) +} + +func (sub *Subscription) start() { + <-sub.Context.Done() + // the subscription ends once the context is canceled (if not already) + sub.Unsub() // this will set sub.live to false + + // do this so we don't have the possibility of closing the Events channel + // and then trying to send to it + sub.mu.Lock() + close(sub.Events) + sub.mu.Unlock() +} + +func (sub *Subscription) dispatchEvent(evt *nip1.Event) { + added := false + if !sub.eosed.Load() { + sub.storedwg.Add(1) + added = true + } + + go func() { + sub.mu.Lock() + defer sub.mu.Unlock() + + if sub.live.Load() { + select { + case sub.Events <- evt: + case <-sub.Context.Done(): + } + } + + if added { + sub.storedwg.Done() + } + }() +} + +func (sub *Subscription) dispatchEose() { + if sub.eosed.CompareAndSwap(false, true) { + go func() { + sub.storedwg.Wait() + close(sub.EndOfStoredEvents) + }() + } +} + +// Unsub closes the subscription, sending "CLOSE" to relay as in NIP-01. +// Unsub() also closes the channel sub.Events and makes a new one. +func (sub *Subscription) Unsub() { + // cancel the context (if it's not canceled already) + sub.cancel() + + // mark subscription as closed and send a CLOSE to the relay (naïve sync.Once implementation) + if sub.live.CompareAndSwap(true, false) { + sub.Close() + } + + // remove subscription from our map + sub.Relay.Subscriptions.Delete(sub.GetID()) +} + +// Close just sends a CLOSE message. You probably want Unsub() instead. +func (sub *Subscription) Close() { + if sub.Relay.IsConnected() { + id := sub.GetID() + closeMsg := &nip1.CloseEnvelope{SubscriptionID: nip1.SubscriptionID(id)} + closeb, _ := closeMsg.MarshalJSON() + log.D.F("{%s} sending %s", sub.Relay.URL, closeb) + <-sub.Relay.Write(closeb) + } +} + +// Sub sets sub.Filters and then calls sub.Fire(ctx). +// The subscription will be closed if the context expires. +func (sub *Subscription) Sub(_ context.Context, filters nip1.Filters) { + sub.Filters = filters + sub.Fire() +} + +// Fire sends the "REQ" command to the relay. +func (sub *Subscription) Fire() error { + id := sub.GetID() + + var reqb []byte + var e error + if sub.countResult == nil { + if reqb, e = (&nip1.ReqEnvelope{ + SubscriptionID: nip1.SubscriptionID(id), + Filters: sub.Filters, + }).MarshalJSON(); fails(e) { + + } + } else { + reqb, _ = (&nip45.CountRequestEnvelope{ + SubscriptionID: nip1.SubscriptionID(id), + Filters: sub.Filters, + }).MarshalJSON() + } + log.D.F("{%s} sending %v", sub.Relay.URL, string(reqb)) + sub.live.Store(true) + if e = <-sub.Relay.Write(reqb); fails(e) { + sub.cancel() + return fmt.Errorf("failed to write: %w", e) + } + + return nil +} diff --git a/pkg/nostr/subscription_test.go b/pkg/nostr/subscription_test.go new file mode 100644 index 00000000..e1256331 --- /dev/null +++ b/pkg/nostr/subscription_test.go @@ -0,0 +1,120 @@ +package nostr + +import ( + "context" + "fmt" + "github.com/Hubmakerlabs/replicatr/pkg/nostr/kind" + "github.com/Hubmakerlabs/replicatr/pkg/nostr/nip1" + "sync/atomic" + "testing" + "time" +) + +const RELAY = "wss://nostr.mom" + +// NOTE +// +// These tests require an internet connection and will fail if the above relay +// is offline or unreachable. + +// test if we can fetch a couple of random events +func TestSubscribe(t *testing.T) { + // log2.SetLogLevel(log2.Debug) + rl := mustRelayConnect(RELAY) + defer rl.Close() + + sub, err := rl.Subscribe(context.Background(), + nip1.Filters{ + {Kinds: kind.Array{kind.TextNote}, Limit: 2}, + }) + if err != nil { + t.Errorf("subscription failed: %v", err) + return + } + + timeout := time.After(2 * time.Second) + n := 0 + +out: + for { + select { + case event := <-sub.Events: + if event == nil { + t.Errorf("event is nil: %v", event) + } + n++ + case <-sub.EndOfStoredEvents: + break out + case <-rl.Context().Done(): + t.Errorf("connection closed: %v", rl.Context().Err()) + break out + case <-timeout: + t.Errorf("timeout") + break out + } + } + if n != 2 { + t.Errorf("expected 2 events, got %d", n) + } +} + +// test if we can do multiple nested subscriptions +func TestNestedSubscriptions(t *testing.T) { + // log2.SetLogLevel(log2.Debug) + rl := mustRelayConnect(RELAY) + defer rl.Close() + + n := atomic.Uint32{} + + // fetch 2 replies to a note + sub, err := rl.Subscribe(context.Background(), nip1.Filters{{ + Kinds: kind.Array{kind.TextNote}, + Tags: nip1.TagMap{"e": []string{"0e34a74f8547e3b95d52a2543719b109fd0312aba144e2ef95cba043f42fe8c5"}}, + Limit: 3, + }}) + if err != nil { + t.Errorf("subscription 1 failed: %v", err) + return + } + + for { + select { + case event := <-sub.Events: + // now fetch author of this + sub, err := rl.Subscribe(context.Background(), nip1.Filters{{Kinds: kind.Array{kind.SetMetadata}, Authors: []string{event.PubKey}, Limit: 1}}) + if err != nil { + t.Errorf("subscription 2 failed: %v", err) + return + } + + for { + select { + case <-sub.Events: + // do another subscription here in "sync" mode, just so we're sure things are not blocking + evs, e := rl.QuerySync(context.Background(), nip1.Filter{Limit: 1}) + if fails(e) { + + } + log.D.S(evs) + n.Add(1) + if n.Load() == 3 { + // if we get here it means the test passed + return + } + case <-sub.Context.Done(): + goto end + case <-sub.EndOfStoredEvents: + sub.Unsub() + } + } + end: + fmt.Println("") + case <-sub.EndOfStoredEvents: + sub.Unsub() + return + case <-sub.Context.Done(): + t.Errorf("connection closed: %v", rl.Context().Err()) + return + } + } +} diff --git a/pkg/wire/text/mangle.go b/pkg/wire/text/mangle.go index f8bf2ec5..73bba5d1 100644 --- a/pkg/wire/text/mangle.go +++ b/pkg/wire/text/mangle.go @@ -179,6 +179,9 @@ func (b *Buffer) ReadEnclosed() (bb []byte, e error) { switch b.Buf[i] { case '"': if inQuotes { + if i > 0 { + + } inQuotes = false } else { inQuotes = true @@ -194,6 +197,7 @@ func (b *Buffer) ReadEnclosed() (bb []byte, e error) { } if depth == 0 { bb = b.Buf[b.Pos : i+1] + b.Pos = i + 1 return } } diff --git a/pkg/wire/text/unescape.go b/pkg/wire/text/unescape.go index 1ff73ab6..e969454b 100644 --- a/pkg/wire/text/unescape.go +++ b/pkg/wire/text/unescape.go @@ -125,6 +125,7 @@ func SecondHexCharToValue(in byte) (out byte) { // characters that must be escaped for JSON/HTML encoding. This means octal // `\xxx` unicode backslash escapes \uXXXX and \UXXXX func UnescapeByteString(bs []byte) (o []byte) { + log.D.F("'%s'", bs) in := NewBuffer(bs) // read side out := NewBuffer(bs) // write side var e error @@ -135,8 +136,9 @@ next: // find the first escape character. // start := in.Pos if segment, e = in.ReadUntil('\\'); e != nil { - // log.D.F("'%s'", string(in.Buf[start:])) + log.D.F("'%s'", string(in.Buf[in.Pos:])) if len(segment) > 0 { + log.D.F("'%s'", string(segment)) if e = out.WriteBytes(segment); fails(e) { break next }