mirror of
https://github.com/nbd-wtf/go-nostr.git
synced 2025-06-30 02:20:37 +02:00
move nostr-sdk repository into here because why not?
This commit is contained in:
29
go.mod
29
go.mod
@ -1,37 +1,46 @@
|
|||||||
module github.com/nbd-wtf/go-nostr
|
module github.com/nbd-wtf/go-nostr
|
||||||
|
|
||||||
go 1.22.5
|
go 1.23.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/bluekeyes/go-gitdiff v0.7.1
|
github.com/bluekeyes/go-gitdiff v0.7.1
|
||||||
github.com/btcsuite/btcd/btcec/v2 v2.3.2
|
github.com/btcsuite/btcd/btcec/v2 v2.3.2
|
||||||
github.com/btcsuite/btcd/btcutil v1.1.3
|
github.com/btcsuite/btcd/btcutil v1.1.3
|
||||||
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0
|
||||||
|
github.com/fiatjaf/eventstore v0.8.1
|
||||||
|
github.com/fiatjaf/generic-ristretto v0.0.1
|
||||||
github.com/gobwas/httphead v0.1.0
|
github.com/gobwas/httphead v0.1.0
|
||||||
github.com/gobwas/ws v1.2.0
|
github.com/gobwas/ws v1.3.1
|
||||||
|
github.com/graph-gophers/dataloader/v7 v7.1.0
|
||||||
github.com/mailru/easyjson v0.7.7
|
github.com/mailru/easyjson v0.7.7
|
||||||
github.com/puzpuzpuz/xsync/v3 v3.0.2
|
github.com/puzpuzpuz/xsync/v3 v3.0.2
|
||||||
github.com/stretchr/testify v1.9.0
|
github.com/stretchr/testify v1.9.0
|
||||||
github.com/tidwall/gjson v1.14.4
|
github.com/tidwall/gjson v1.17.0
|
||||||
github.com/tyler-smith/go-bip32 v1.0.0
|
github.com/tyler-smith/go-bip32 v1.0.0
|
||||||
github.com/tyler-smith/go-bip39 v1.1.0
|
github.com/tyler-smith/go-bip39 v1.1.0
|
||||||
golang.org/x/crypto v0.7.0
|
golang.org/x/crypto v0.14.0
|
||||||
golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d
|
||||||
golang.org/x/net v0.8.0
|
golang.org/x/net v0.17.0
|
||||||
golang.org/x/text v0.8.0
|
golang.org/x/text v0.15.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/FactomProject/basen v0.0.0-20150613233007-fe3947df716e // indirect
|
github.com/FactomProject/basen v0.0.0-20150613233007-fe3947df716e // indirect
|
||||||
github.com/FactomProject/btcutilecc v0.0.0-20130527213604-d3a63a5752ec // indirect
|
github.com/FactomProject/btcutilecc v0.0.0-20130527213604-d3a63a5752ec // indirect
|
||||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2 // indirect
|
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/decred/dcrd/crypto/blake256 v1.0.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/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/gobwas/pool v0.2.1 // indirect
|
github.com/gobwas/pool v0.2.1 // indirect
|
||||||
|
github.com/golang/glog v1.1.2 // indirect
|
||||||
github.com/josharian/intern v1.0.0 // indirect
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
|
github.com/kr/pretty v0.3.1 // indirect
|
||||||
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/rogpeppe/go-internal v1.12.0 // indirect
|
||||||
github.com/tidwall/match v1.1.1 // indirect
|
github.com/tidwall/match v1.1.1 // indirect
|
||||||
github.com/tidwall/pretty v1.2.0 // indirect
|
github.com/tidwall/pretty v1.2.1 // indirect
|
||||||
golang.org/x/sys v0.8.0 // indirect
|
golang.org/x/sys v0.20.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
65
go.sum
65
go.sum
@ -29,8 +29,11 @@ github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku
|
|||||||
github.com/btcsuite/snappy-go v1.0.0/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/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/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
|
||||||
|
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/cmars/basen v0.0.0-20150613233007-fe3947df716e h1:0XBUw73chJ1VYSsfvcPvVT7auykAJce9FpRr10L6Qhw=
|
github.com/cmars/basen v0.0.0-20150613233007-fe3947df716e h1:0XBUw73chJ1VYSsfvcPvVT7auykAJce9FpRr10L6Qhw=
|
||||||
github.com/cmars/basen v0.0.0-20150613233007-fe3947df716e/go.mod h1:P13beTBKr5Q18lJe1rIoLUqjM+CB1zYrRg44ZqGuQSA=
|
github.com/cmars/basen v0.0.0-20150613233007-fe3947df716e/go.mod h1:P13beTBKr5Q18lJe1rIoLUqjM+CB1zYrRg44ZqGuQSA=
|
||||||
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
@ -42,14 +45,24 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeC
|
|||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs=
|
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/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/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218=
|
||||||
|
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.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
|
github.com/fiatjaf/eventstore v0.8.1 h1:51LchQNy0Hpb0YQHwqYR5pKBpfDs/KjySlWCbbz2pkc=
|
||||||
|
github.com/fiatjaf/eventstore v0.8.1/go.mod h1:bsp0Ibv0CIcVuFcoM2AEerMWmXRhF8uWXMf+dClhuow=
|
||||||
|
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/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||||
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
|
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/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 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
||||||
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||||
github.com/gobwas/ws v1.2.0 h1:u0p9s3xLYpZCA1z5JgCkMeB34CKCMMQbM+G8Ii7YD0I=
|
github.com/gobwas/ws v1.3.1 h1:Qi34dfLMWJbiKaNbDVzM9x27nZBjmkaW6i4+Ku+pGVU=
|
||||||
github.com/gobwas/ws v1.2.0/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
|
github.com/gobwas/ws v1.3.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
|
||||||
|
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.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/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.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||||
@ -61,6 +74,8 @@ github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW
|
|||||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
github.com/google/go-cmp v0.3.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.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.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/graph-gophers/dataloader/v7 v7.1.0 h1:Wn8HGF/q7MNXcvfaBnLEPEFJttVHR8zuEqP1obys/oc=
|
||||||
|
github.com/graph-gophers/dataloader/v7 v7.1.0/go.mod h1:1bKE0Dm6OUcTB/OAuYVOZctgIz7Q3d0XrYtlIzTgg6Q=
|
||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
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 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/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||||
@ -68,6 +83,10 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm
|
|||||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
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/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/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||||
@ -79,28 +98,29 @@ github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5
|
|||||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
github.com/onsi/gomega v1.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.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||||
|
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||||
|
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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/puzpuzpuz/xsync/v3 v3.0.2 h1:3yESHrRFYr6xzkz61LLkvNiPFXxJEAABanTQpKbAaew=
|
github.com/puzpuzpuz/xsync/v3 v3.0.2 h1:3yESHrRFYr6xzkz61LLkvNiPFXxJEAABanTQpKbAaew=
|
||||||
github.com/puzpuzpuz/xsync/v3 v3.0.2/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
github.com/puzpuzpuz/xsync/v3 v3.0.2/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||||
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||||
|
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||||
|
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
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.1.5-0.20170601210322-f6abca593680/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.1.5-0.20170601210322-f6abca593680/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
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.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
|
|
||||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
|
||||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
|
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
|
||||||
github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
|
github.com/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM=
|
||||||
github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
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 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||||
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
|
||||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
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/tyler-smith/go-bip32 v1.0.0 h1:sDR9juArbUgX+bO/iblgZnMPeWY1KZMUC2AFUJdv5KE=
|
github.com/tyler-smith/go-bip32 v1.0.0 h1:sDR9juArbUgX+bO/iblgZnMPeWY1KZMUC2AFUJdv5KE=
|
||||||
github.com/tyler-smith/go-bip32 v1.0.0/go.mod h1:onot+eHknzV4BVPwrzqY5OoVpyCvnwD7lMawL5aQupE=
|
github.com/tyler-smith/go-bip32 v1.0.0/go.mod h1:onot+eHknzV4BVPwrzqY5OoVpyCvnwD7lMawL5aQupE=
|
||||||
github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8=
|
github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8=
|
||||||
@ -109,17 +129,17 @@ golang.org/x/crypto v0.0.0-20170613210332-850760c427c5/go.mod h1:6SG95UA2DQfeDnf
|
|||||||
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
|
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
|
||||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||||
golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 h1:5llv2sWeaMSnA3w2kS57ouQQ4pudlXrR0dCgw51QK9o=
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
||||||
golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
|
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-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||||
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
|
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
||||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
@ -131,13 +151,13 @@ golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||||||
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/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-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
|
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
|
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
||||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
@ -147,8 +167,9 @@ google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQ
|
|||||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
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.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
gopkg.in/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/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.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
39
keyring/bunker.go
Normal file
39
keyring/bunker.go
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
package keyring
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/nbd-wtf/go-nostr"
|
||||||
|
"github.com/nbd-wtf/go-nostr/nip46"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BunkerSigner is a signer that asks a bunker using NIP-46 every time it needs to do an operation.
|
||||||
|
type BunkerSigner struct {
|
||||||
|
bunker *nip46.BunkerClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBunkerSignerFromBunkerClient(bc *nip46.BunkerClient) BunkerSigner {
|
||||||
|
return BunkerSigner{bc}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bs BunkerSigner) GetPublicKey(ctx context.Context) string {
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, time.Second*30)
|
||||||
|
defer cancel()
|
||||||
|
pk, _ := bs.bunker.GetPublicKey(ctx)
|
||||||
|
return pk
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bs BunkerSigner) SignEvent(ctx context.Context, evt *nostr.Event) error {
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, time.Second*30)
|
||||||
|
defer cancel()
|
||||||
|
return bs.bunker.SignEvent(ctx, evt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bs BunkerSigner) Encrypt(ctx context.Context, plaintext string, recipient string) (string, error) {
|
||||||
|
return bs.bunker.NIP44Encrypt(ctx, recipient, plaintext)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bs BunkerSigner) Decrypt(ctx context.Context, base64ciphertext string, sender string) (plaintext string, err error) {
|
||||||
|
return bs.bunker.NIP44Encrypt(ctx, sender, base64ciphertext)
|
||||||
|
}
|
67
keyring/encrypted.go
Normal file
67
keyring/encrypted.go
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
package keyring
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/nbd-wtf/go-nostr"
|
||||||
|
"github.com/nbd-wtf/go-nostr/nip44"
|
||||||
|
"github.com/nbd-wtf/go-nostr/nip49"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EncryptedKeySigner is a signer that must always ask the user for a password before every operation.
|
||||||
|
type EncryptedKeySigner struct {
|
||||||
|
ncryptsec string
|
||||||
|
pk string
|
||||||
|
callback func(context.Context) string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (es *EncryptedKeySigner) GetPublicKey(ctx context.Context) string {
|
||||||
|
if es.pk != "" {
|
||||||
|
return es.pk
|
||||||
|
}
|
||||||
|
password := es.callback(ctx)
|
||||||
|
key, err := nip49.Decrypt(es.ncryptsec, password)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
pk, _ := nostr.GetPublicKey(key)
|
||||||
|
es.pk = pk
|
||||||
|
return pk
|
||||||
|
}
|
||||||
|
|
||||||
|
func (es *EncryptedKeySigner) SignEvent(ctx context.Context, evt *nostr.Event) error {
|
||||||
|
password := es.callback(ctx)
|
||||||
|
sk, err := nip49.Decrypt(es.ncryptsec, password)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid password: %w", err)
|
||||||
|
}
|
||||||
|
es.pk = evt.PubKey
|
||||||
|
return evt.Sign(sk)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (es EncryptedKeySigner) Encrypt(ctx context.Context, plaintext string, recipient string) (c64 string, err error) {
|
||||||
|
password := es.callback(ctx)
|
||||||
|
sk, err := nip49.Decrypt(es.ncryptsec, password)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("invalid password: %w", err)
|
||||||
|
}
|
||||||
|
ck, err := nip44.GenerateConversationKey(recipient, sk)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return nip44.Encrypt(plaintext, ck)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (es EncryptedKeySigner) Decrypt(ctx context.Context, base64ciphertext string, sender string) (plaintext string, err error) {
|
||||||
|
password := es.callback(ctx)
|
||||||
|
sk, err := nip49.Decrypt(es.ncryptsec, password)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("invalid password: %w", err)
|
||||||
|
}
|
||||||
|
ck, err := nip44.GenerateConversationKey(sender, sk)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return nip44.Encrypt(plaintext, ck)
|
||||||
|
}
|
90
keyring/lib.go
Normal file
90
keyring/lib.go
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
package keyring
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/nbd-wtf/go-nostr"
|
||||||
|
"github.com/nbd-wtf/go-nostr/nip05"
|
||||||
|
"github.com/nbd-wtf/go-nostr/nip19"
|
||||||
|
"github.com/nbd-wtf/go-nostr/nip46"
|
||||||
|
"github.com/nbd-wtf/go-nostr/nip49"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Keyring interface {
|
||||||
|
Signer
|
||||||
|
Cipher
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Signer provides basic public key signing methods.
|
||||||
|
type Signer interface {
|
||||||
|
GetPublicKey(context.Context) string
|
||||||
|
SignEvent(context.Context, *nostr.Event) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Cipher provides NIP-44 encryption and decryption methods.
|
||||||
|
type Cipher interface {
|
||||||
|
Encrypt(ctx context.Context, plaintext string, recipientPublicKey string) (base64ciphertext string, err error)
|
||||||
|
Decrypt(ctx context.Context, base64ciphertext string, senderPublicKey string) (plaintext string, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type SignerOptions struct {
|
||||||
|
BunkerClientSecretKey string
|
||||||
|
BunkerSignTimeout time.Duration
|
||||||
|
BunkerAuthHandler func(string)
|
||||||
|
|
||||||
|
// if a PasswordHandler is provided the key will be stored encrypted and this function will be called
|
||||||
|
// every time an operation needs access to the key so the user can be prompted.
|
||||||
|
PasswordHandler func(context.Context) string
|
||||||
|
|
||||||
|
// if instead a Password is provided along with a ncryptsec, then the key will be decrypted and stored in plaintext.
|
||||||
|
Password string
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(ctx context.Context, pool *nostr.SimplePool, input string, opts *SignerOptions) (Keyring, error) {
|
||||||
|
if opts == nil {
|
||||||
|
opts = &SignerOptions{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(input, "ncryptsec") {
|
||||||
|
if opts.PasswordHandler != nil {
|
||||||
|
return &EncryptedKeySigner{input, "", opts.PasswordHandler}, nil
|
||||||
|
}
|
||||||
|
sec, err := nip49.Decrypt(input, opts.Password)
|
||||||
|
if err != nil {
|
||||||
|
if opts.Password == "" {
|
||||||
|
return nil, fmt.Errorf("failed to decrypt with blank password: %w", err)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("failed to decrypt with given password: %w", err)
|
||||||
|
}
|
||||||
|
pk, _ := nostr.GetPublicKey(sec)
|
||||||
|
return KeySigner{sec, pk, make(map[string][32]byte)}, nil
|
||||||
|
} else if nip46.IsValidBunkerURL(input) || nip05.IsValidIdentifier(input) {
|
||||||
|
bcsk := nostr.GeneratePrivateKey()
|
||||||
|
oa := func(url string) { println("auth_url received but not handled") }
|
||||||
|
|
||||||
|
if opts.BunkerClientSecretKey != "" {
|
||||||
|
bcsk = opts.BunkerClientSecretKey
|
||||||
|
}
|
||||||
|
if opts.BunkerAuthHandler != nil {
|
||||||
|
oa = opts.BunkerAuthHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
bunker, err := nip46.ConnectBunker(ctx, bcsk, input, pool, oa)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return BunkerSigner{bunker}, nil
|
||||||
|
} else if prefix, parsed, err := nip19.Decode(input); err == nil && prefix == "nsec" {
|
||||||
|
sec := parsed.(string)
|
||||||
|
pk, _ := nostr.GetPublicKey(sec)
|
||||||
|
return KeySigner{sec, pk, make(map[string][32]byte)}, nil
|
||||||
|
} else if nostr.IsValid32ByteHex(input) {
|
||||||
|
pk, _ := nostr.GetPublicKey(input)
|
||||||
|
return KeySigner{input, pk, make(map[string][32]byte)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("unsupported input '%s'", input)
|
||||||
|
}
|
33
keyring/manual.go
Normal file
33
keyring/manual.go
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
package keyring
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/nbd-wtf/go-nostr"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ManualSigner is a signer that doesn't really do anything, it just calls the functions given to it.
|
||||||
|
// It can be used when an app for some reason wants to ask the user to manually provide a signed event
|
||||||
|
// by copy-and-paste, for example.
|
||||||
|
type ManualSigner struct {
|
||||||
|
ManualGetPublicKey func(context.Context) string
|
||||||
|
ManualSignEvent func(context.Context, *nostr.Event) error
|
||||||
|
ManualEncrypt func(ctx context.Context, plaintext string, recipientPublicKey string) (base64ciphertext string, err error)
|
||||||
|
ManualDecrypt func(ctx context.Context, base64ciphertext string, senderPublicKey string) (plaintext string, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ms ManualSigner) SignEvent(ctx context.Context, evt *nostr.Event) error {
|
||||||
|
return ms.ManualSignEvent(ctx, evt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ms ManualSigner) GetPublicKey(ctx context.Context) string {
|
||||||
|
return ms.ManualGetPublicKey(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ms ManualSigner) Encrypt(ctx context.Context, plaintext string, recipient string) (c64 string, err error) {
|
||||||
|
return ms.ManualEncrypt(ctx, plaintext, recipient)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ms ManualSigner) Decrypt(ctx context.Context, base64ciphertext string, sender string) (plaintext string, err error) {
|
||||||
|
return ms.ManualDecrypt(ctx, base64ciphertext, sender)
|
||||||
|
}
|
44
keyring/plain.go
Normal file
44
keyring/plain.go
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
package keyring
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/nbd-wtf/go-nostr"
|
||||||
|
"github.com/nbd-wtf/go-nostr/nip44"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Keysigner is a signer that holds the private key in memory and can do all the operations instantly and easily.
|
||||||
|
type KeySigner struct {
|
||||||
|
sk string
|
||||||
|
pk string
|
||||||
|
|
||||||
|
conversationKeys map[string][32]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ks KeySigner) SignEvent(ctx context.Context, evt *nostr.Event) error { return evt.Sign(ks.sk) }
|
||||||
|
func (ks KeySigner) GetPublicKey(ctx context.Context) string { return ks.pk }
|
||||||
|
|
||||||
|
func (ks KeySigner) Encrypt(ctx context.Context, plaintext string, recipient string) (c64 string, err error) {
|
||||||
|
ck, ok := ks.conversationKeys[recipient]
|
||||||
|
if !ok {
|
||||||
|
ck, err = nip44.GenerateConversationKey(recipient, ks.sk)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
ks.conversationKeys[recipient] = ck
|
||||||
|
}
|
||||||
|
return nip44.Encrypt(plaintext, ck)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ks KeySigner) Decrypt(ctx context.Context, base64ciphertext string, sender string) (plaintext string, err error) {
|
||||||
|
ck, ok := ks.conversationKeys[sender]
|
||||||
|
if !ok {
|
||||||
|
var err error
|
||||||
|
ck, err = nip44.GenerateConversationKey(sender, ks.sk)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
ks.conversationKeys[sender] = ck
|
||||||
|
}
|
||||||
|
return nip44.Encrypt(plaintext, ck)
|
||||||
|
}
|
@ -225,6 +225,7 @@ func calcPadding(sLen int) int {
|
|||||||
return chunk * int(math.Floor(float64((sLen-1)/chunk))+1)
|
return chunk * int(math.Floor(float64((sLen-1)/chunk))+1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// code adapted from nip04.ComputeSharedSecret()
|
||||||
func computeSharedSecret(pub string, sk string) (sharedSecret [32]byte, err error) {
|
func computeSharedSecret(pub string, sk string) (sharedSecret [32]byte, err error) {
|
||||||
privKeyBytes, err := hex.DecodeString(sk)
|
privKeyBytes, err := hex.DecodeString(sk)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -232,7 +233,6 @@ func computeSharedSecret(pub string, sk string) (sharedSecret [32]byte, err erro
|
|||||||
}
|
}
|
||||||
privKey, _ := btcec.PrivKeyFromBytes(privKeyBytes)
|
privKey, _ := btcec.PrivKeyFromBytes(privKeyBytes)
|
||||||
|
|
||||||
// adding 02 to signal that this is a compressed public key (33 bytes)
|
|
||||||
pubKeyBytes, err := hex.DecodeString("02" + pub)
|
pubKeyBytes, err := hex.DecodeString("02" + pub)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return sharedSecret, fmt.Errorf("error decoding hex string of receiver public key '%s': %w", "02"+pub, err)
|
return sharedSecret, fmt.Errorf("error decoding hex string of receiver public key '%s': %w", "02"+pub, err)
|
||||||
|
10
sdk/cache/interface.go
vendored
Normal file
10
sdk/cache/interface.go
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
package cache
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type Cache32[V any] interface {
|
||||||
|
Get(k string) (v V, ok bool)
|
||||||
|
Delete(k string)
|
||||||
|
Set(k string, v V) bool
|
||||||
|
SetWithTTL(k string, v V, d time.Duration) bool
|
||||||
|
}
|
48
sdk/cache/memory/cache.go
vendored
Normal file
48
sdk/cache/memory/cache.go
vendored
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
package cache_memory
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"encoding/hex"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
ristretto "github.com/fiatjaf/generic-ristretto"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RistrettoCache[V any] struct {
|
||||||
|
Cache *ristretto.Cache[string, V]
|
||||||
|
}
|
||||||
|
|
||||||
|
func New32[V any](max int64) *RistrettoCache[V] {
|
||||||
|
cache, _ := ristretto.NewCache(&ristretto.Config[string, V]{
|
||||||
|
NumCounters: max * 10,
|
||||||
|
MaxCost: max,
|
||||||
|
BufferItems: 64,
|
||||||
|
KeyToHash: func(key string) (uint64, uint64) { return h32(key), 0 },
|
||||||
|
})
|
||||||
|
return &RistrettoCache[V]{Cache: cache}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s RistrettoCache[V]) Get(k string) (v V, ok bool) { return s.Cache.Get(k) }
|
||||||
|
func (s RistrettoCache[V]) Delete(k string) { s.Cache.Del(k) }
|
||||||
|
func (s RistrettoCache[V]) Set(k string, v V) bool { return s.Cache.Set(k, v, 1) }
|
||||||
|
func (s RistrettoCache[V]) SetWithTTL(k string, v V, d time.Duration) bool {
|
||||||
|
return s.Cache.SetWithTTL(k, v, 1, d)
|
||||||
|
}
|
||||||
|
|
||||||
|
func h32(key string) uint64 {
|
||||||
|
// we get an event id or pubkey as hex,
|
||||||
|
// so just extract the last 8 bytes from it and turn them into a uint64
|
||||||
|
return shortUint64(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func shortUint64(idOrPubkey string) uint64 {
|
||||||
|
length := len(idOrPubkey)
|
||||||
|
if length < 8 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
b, err := hex.DecodeString(idOrPubkey[length-8:])
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return uint64(binary.BigEndian.Uint32(b))
|
||||||
|
}
|
50
sdk/follows.go
Normal file
50
sdk/follows.go
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
package sdk
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/nbd-wtf/go-nostr"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FollowList = GenericList[Follow]
|
||||||
|
|
||||||
|
type Follow struct {
|
||||||
|
Pubkey string
|
||||||
|
Relay string
|
||||||
|
Petname string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f Follow) Value() string { return f.Pubkey }
|
||||||
|
|
||||||
|
func (sys *System) FetchFollowList(ctx context.Context, pubkey string) FollowList {
|
||||||
|
fl, _ := fetchGenericList[Follow](sys, ctx, pubkey, 3, parseFollow, sys.FollowListCache, false)
|
||||||
|
return fl
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseFollow(tag nostr.Tag) (fw Follow, ok bool) {
|
||||||
|
if len(tag) < 2 {
|
||||||
|
return fw, false
|
||||||
|
}
|
||||||
|
if tag[0] != "p" {
|
||||||
|
return fw, false
|
||||||
|
}
|
||||||
|
|
||||||
|
fw.Pubkey = tag[1]
|
||||||
|
if !nostr.IsValidPublicKey(fw.Pubkey) {
|
||||||
|
return fw, false
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(tag) > 2 {
|
||||||
|
if _, err := url.Parse(tag[2]); err == nil {
|
||||||
|
fw.Relay = nostr.NormalizeURL(tag[2])
|
||||||
|
}
|
||||||
|
if len(tag) > 3 {
|
||||||
|
fw.Petname = strings.TrimSpace(tag[3])
|
||||||
|
}
|
||||||
|
return fw, true
|
||||||
|
}
|
||||||
|
|
||||||
|
return fw, false
|
||||||
|
}
|
21
sdk/helpers.go
Normal file
21
sdk/helpers.go
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
package sdk
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IsVirtualRelay returns true if the given normalized relay URL shouldn't be considered for outbox-model calculations.
|
||||||
|
func IsVirtualRelay(url string) bool {
|
||||||
|
if len(url) < 6 {
|
||||||
|
// this is just invalid
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(url, "wss://feeds.nostr.band") ||
|
||||||
|
strings.HasPrefix(url, "wss://filter.nostr.wine") ||
|
||||||
|
strings.HasPrefix(url, "wss://cache") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
8
sdk/hints/interface.go
Normal file
8
sdk/hints/interface.go
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
package hints
|
||||||
|
|
||||||
|
import "github.com/nbd-wtf/go-nostr"
|
||||||
|
|
||||||
|
type HintsDB interface {
|
||||||
|
TopN(pubkey string, n int) []string
|
||||||
|
Save(pubkey string, relay string, key HintKey, score nostr.Timestamp)
|
||||||
|
}
|
49
sdk/hints/keys.go
Normal file
49
sdk/hints/keys.go
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
package hints
|
||||||
|
|
||||||
|
import "github.com/nbd-wtf/go-nostr"
|
||||||
|
|
||||||
|
const END_OF_WORLD nostr.Timestamp = 2208999600 // 2040-01-01
|
||||||
|
|
||||||
|
type HintKey int
|
||||||
|
|
||||||
|
const (
|
||||||
|
LastFetchAttempt HintKey = iota
|
||||||
|
MostRecentEventFetched
|
||||||
|
LastInRelayList
|
||||||
|
LastInTag
|
||||||
|
LastInNprofile
|
||||||
|
LastInNevent
|
||||||
|
LastInNIP05
|
||||||
|
)
|
||||||
|
|
||||||
|
var KeyBasePoints = [7]int64{
|
||||||
|
-500, // attempting has negative power because it may fail
|
||||||
|
700, // when it succeeds that should cancel the negative effect of trying
|
||||||
|
350, // a relay list is a very strong indicator
|
||||||
|
5, // tag hints are often autogenerated so we don't care very much about them (that may change)
|
||||||
|
22, // it feels like people take nprofiles slightly more seriously so we value these a bit more
|
||||||
|
8, // these are also not often too bad
|
||||||
|
7, // nip05 hints should be a strong indicator, although in practice they're kinda bad
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hk HintKey) BasePoints() int64 { return KeyBasePoints[hk] }
|
||||||
|
|
||||||
|
func (hk HintKey) String() string {
|
||||||
|
switch hk {
|
||||||
|
case LastFetchAttempt:
|
||||||
|
return "last_fetch_attempt"
|
||||||
|
case MostRecentEventFetched:
|
||||||
|
return "most_recent_event_fetched"
|
||||||
|
case LastInRelayList:
|
||||||
|
return "last_in_relay_list"
|
||||||
|
case LastInTag:
|
||||||
|
return "last_in_tag"
|
||||||
|
case LastInNprofile:
|
||||||
|
return "last_in_nprofile"
|
||||||
|
case LastInNevent:
|
||||||
|
return "last_in_nevent"
|
||||||
|
case LastInNIP05:
|
||||||
|
return "last_in_nip05"
|
||||||
|
}
|
||||||
|
return "<unexpected>"
|
||||||
|
}
|
142
sdk/hints/memory/memory_hints.go
Normal file
142
sdk/hints/memory/memory_hints.go
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
package memory
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"slices"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/nbd-wtf/go-nostr"
|
||||||
|
"github.com/nbd-wtf/go-nostr/sdk/hints"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ hints.HintsDB = (*HintDB)(nil)
|
||||||
|
|
||||||
|
type HintDB struct {
|
||||||
|
RelayBySerial []string
|
||||||
|
OrderedRelaysByPubKey map[string]RelaysForPubKey
|
||||||
|
|
||||||
|
sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHintDB() *HintDB {
|
||||||
|
return &HintDB{
|
||||||
|
RelayBySerial: make([]string, 0, 100),
|
||||||
|
OrderedRelaysByPubKey: make(map[string]RelaysForPubKey, 100),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *HintDB) Save(pubkey string, relay string, key hints.HintKey, ts nostr.Timestamp) {
|
||||||
|
now := nostr.Now()
|
||||||
|
// this is used for calculating what counts as a usable hint
|
||||||
|
threshold := (now - 60*60*24*180)
|
||||||
|
if threshold < 0 {
|
||||||
|
threshold = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
relayIndex := slices.Index(db.RelayBySerial, relay)
|
||||||
|
if relayIndex == -1 {
|
||||||
|
relayIndex = len(db.RelayBySerial)
|
||||||
|
db.RelayBySerial = append(db.RelayBySerial, relay)
|
||||||
|
}
|
||||||
|
|
||||||
|
db.Lock()
|
||||||
|
defer db.Unlock()
|
||||||
|
// fmt.Println(" ", relay, "index", relayIndex, "--", "adding", hints.HintKey(key).String(), ts)
|
||||||
|
|
||||||
|
rfpk, _ := db.OrderedRelaysByPubKey[pubkey]
|
||||||
|
|
||||||
|
entries := rfpk.Entries
|
||||||
|
|
||||||
|
entryIndex := slices.IndexFunc(entries, func(re RelayEntry) bool { return re.Relay == relayIndex })
|
||||||
|
if entryIndex == -1 {
|
||||||
|
// we don't have an entry for this relay, so add one
|
||||||
|
entryIndex = len(entries)
|
||||||
|
|
||||||
|
entry := RelayEntry{
|
||||||
|
Relay: relayIndex,
|
||||||
|
}
|
||||||
|
entry.Timestamps[key] = ts
|
||||||
|
|
||||||
|
entries = append(entries, entry)
|
||||||
|
} else {
|
||||||
|
// just update this entry
|
||||||
|
if entries[entryIndex].Timestamps[key] < ts {
|
||||||
|
entries[entryIndex].Timestamps[key] = ts
|
||||||
|
} else {
|
||||||
|
// no need to update anything
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rfpk.Entries = entries
|
||||||
|
|
||||||
|
db.OrderedRelaysByPubKey[pubkey] = rfpk
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *HintDB) TopN(pubkey string, n int) []string {
|
||||||
|
db.Lock()
|
||||||
|
defer db.Unlock()
|
||||||
|
|
||||||
|
urls := make([]string, 0, n)
|
||||||
|
if rfpk, ok := db.OrderedRelaysByPubKey[pubkey]; ok {
|
||||||
|
// sort everything from scratch
|
||||||
|
slices.SortFunc(rfpk.Entries, func(a, b RelayEntry) int {
|
||||||
|
return int(b.Sum() - a.Sum())
|
||||||
|
})
|
||||||
|
|
||||||
|
for i, re := range rfpk.Entries {
|
||||||
|
urls = append(urls, db.RelayBySerial[re.Relay])
|
||||||
|
if i+1 == n {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return urls
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *HintDB) PrintScores() {
|
||||||
|
db.Lock()
|
||||||
|
defer db.Unlock()
|
||||||
|
|
||||||
|
fmt.Println("= print scores")
|
||||||
|
for pubkey, rfpk := range db.OrderedRelaysByPubKey {
|
||||||
|
fmt.Println("== relay scores for", pubkey)
|
||||||
|
for i, re := range rfpk.Entries {
|
||||||
|
fmt.Printf(" %3d :: %30s (%3d) ::> %12d\n", i, db.RelayBySerial[re.Relay], re.Relay, re.Sum())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type RelaysForPubKey struct {
|
||||||
|
Entries []RelayEntry
|
||||||
|
}
|
||||||
|
|
||||||
|
type RelayEntry struct {
|
||||||
|
Relay int
|
||||||
|
Timestamps [8]nostr.Timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
func (re RelayEntry) Sum() int64 {
|
||||||
|
now := nostr.Now() + 24*60*60
|
||||||
|
var sum int64
|
||||||
|
for i, ts := range re.Timestamps {
|
||||||
|
if ts == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
hk := hints.HintKey(i)
|
||||||
|
divisor := int64(now - ts)
|
||||||
|
if divisor == 0 {
|
||||||
|
divisor = 1
|
||||||
|
} else {
|
||||||
|
divisor = int64(math.Pow(float64(divisor), 1.3))
|
||||||
|
}
|
||||||
|
|
||||||
|
multiplier := hk.BasePoints()
|
||||||
|
value := multiplier * 10000000000 / divisor
|
||||||
|
// fmt.Println(" ", i, "value:", value)
|
||||||
|
sum += value
|
||||||
|
}
|
||||||
|
return sum
|
||||||
|
}
|
143
sdk/hints/memory/memory_test.go
Normal file
143
sdk/hints/memory/memory_test.go
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
package memory
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/nbd-wtf/go-nostr"
|
||||||
|
"github.com/nbd-wtf/go-nostr/sdk/hints"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRelayPicking(t *testing.T) {
|
||||||
|
hdb := NewHintDB()
|
||||||
|
|
||||||
|
const key1 = "0000000000000000000000000000000000000000000000000000000000000001"
|
||||||
|
const key2 = "0000000000000000000000000000000000000000000000000000000000000002"
|
||||||
|
const key3 = "0000000000000000000000000000000000000000000000000000000000000003"
|
||||||
|
const key4 = "0000000000000000000000000000000000000000000000000000000000000004"
|
||||||
|
const relayA = "wss://aaa.com"
|
||||||
|
const relayB = "wss://bbb.online"
|
||||||
|
const relayC = "wss://ccc.technology"
|
||||||
|
|
||||||
|
hour := nostr.Timestamp((time.Hour).Seconds())
|
||||||
|
day := hour * 24
|
||||||
|
|
||||||
|
// key1: finding out
|
||||||
|
// add some random parameters things and see what we get
|
||||||
|
hdb.Save(key1, relayA, hints.LastInTag, nostr.Now()-60*hour)
|
||||||
|
hdb.Save(key1, relayB, hints.LastInRelayList, nostr.Now()-day*10)
|
||||||
|
hdb.Save(key1, relayB, hints.LastInNevent, nostr.Now()-day*30)
|
||||||
|
hdb.Save(key1, relayA, hints.LastInNprofile, nostr.Now()-hour*10)
|
||||||
|
hdb.PrintScores()
|
||||||
|
|
||||||
|
require.Equal(t, []string{relayB, relayA}, hdb.TopN(key1, 3))
|
||||||
|
|
||||||
|
hdb.Save(key1, relayA, hints.LastFetchAttempt, nostr.Now()-5*hour)
|
||||||
|
hdb.Save(key1, relayC, hints.LastInNIP05, nostr.Now()-5*hour)
|
||||||
|
hdb.PrintScores()
|
||||||
|
|
||||||
|
require.Equal(t, []string{relayB, relayC, relayA}, hdb.TopN(key1, 3))
|
||||||
|
|
||||||
|
hdb.Save(key1, relayC, hints.LastInTag, nostr.Now()-5*hour)
|
||||||
|
hdb.Save(key1, relayC, hints.LastFetchAttempt, nostr.Now()-5*hour)
|
||||||
|
hdb.PrintScores()
|
||||||
|
|
||||||
|
require.Equal(t, []string{relayB, relayA, relayC}, hdb.TopN(key1, 3))
|
||||||
|
|
||||||
|
hdb.Save(key1, relayA, hints.MostRecentEventFetched, nostr.Now()-day*60)
|
||||||
|
hdb.PrintScores()
|
||||||
|
|
||||||
|
require.Equal(t, []string{relayB, relayA, relayC}, hdb.TopN(key1, 3))
|
||||||
|
|
||||||
|
// now let's try a different thing for key2
|
||||||
|
// key2 has a relay list with A and B
|
||||||
|
hdb.Save(key2, relayA, hints.LastInRelayList, nostr.Now()-day*25)
|
||||||
|
hdb.Save(key2, relayB, hints.LastInRelayList, nostr.Now()-day*25)
|
||||||
|
|
||||||
|
// but it's old, recently we only see hints for relay C
|
||||||
|
hdb.Save(key2, relayC, hints.LastInTag, nostr.Now()-5*hour)
|
||||||
|
hdb.Save(key2, relayC, hints.LastInNIP05, nostr.Now()-5*hour)
|
||||||
|
hdb.Save(key2, relayC, hints.LastInNevent, nostr.Now()-5*hour)
|
||||||
|
hdb.Save(key2, relayC, hints.LastInNprofile, nostr.Now()-5*hour)
|
||||||
|
|
||||||
|
// at this point we just barely see C coming first
|
||||||
|
hdb.PrintScores()
|
||||||
|
require.Equal(t, []string{relayC, relayA, relayB}, hdb.TopN(key2, 3))
|
||||||
|
|
||||||
|
// yet a different thing for key3
|
||||||
|
// it doesn't have relay lists published because it's banned everywhere
|
||||||
|
// all it has are references to its posts from others
|
||||||
|
hdb.Save(key3, relayA, hints.LastInTag, nostr.Now()-day*2)
|
||||||
|
hdb.Save(key3, relayB, hints.LastInNevent, nostr.Now()-day)
|
||||||
|
hdb.Save(key3, relayB, hints.LastInTag, nostr.Now()-day)
|
||||||
|
hdb.PrintScores()
|
||||||
|
require.Equal(t, []string{relayB, relayA}, hdb.TopN(key3, 3))
|
||||||
|
|
||||||
|
// we try to fetch events for key3 and we get a very recent one for relay A, an older for relay B
|
||||||
|
hdb.Save(key3, relayA, hints.LastFetchAttempt, nostr.Now()-5*hour)
|
||||||
|
hdb.Save(key3, relayA, hints.MostRecentEventFetched, nostr.Now()-day)
|
||||||
|
hdb.Save(key3, relayB, hints.LastFetchAttempt, nostr.Now()-5*hour)
|
||||||
|
hdb.Save(key3, relayB, hints.MostRecentEventFetched, nostr.Now()-day*30)
|
||||||
|
hdb.PrintScores()
|
||||||
|
require.Equal(t, []string{relayA, relayB}, hdb.TopN(key3, 3))
|
||||||
|
|
||||||
|
// for key4 we'll try the alex jones case
|
||||||
|
// key4 used to publish normally to a bunch of big relays until it got banned
|
||||||
|
// then it started publishing only to its personal relay
|
||||||
|
// how long until clients realize that?
|
||||||
|
banDate := nostr.Now() - day*10
|
||||||
|
hdb.Save(key4, relayA, hints.LastInRelayList, banDate)
|
||||||
|
hdb.Save(key4, relayA, hints.LastFetchAttempt, banDate)
|
||||||
|
hdb.Save(key4, relayA, hints.MostRecentEventFetched, banDate)
|
||||||
|
hdb.Save(key4, relayA, hints.LastInNprofile, banDate+8*day)
|
||||||
|
hdb.Save(key4, relayA, hints.LastInNIP05, banDate+5*day)
|
||||||
|
hdb.Save(key4, relayB, hints.LastInRelayList, banDate)
|
||||||
|
hdb.Save(key4, relayB, hints.LastFetchAttempt, banDate)
|
||||||
|
hdb.Save(key4, relayB, hints.MostRecentEventFetched, banDate)
|
||||||
|
hdb.Save(key4, relayB, hints.LastInNevent, banDate+5*day)
|
||||||
|
hdb.Save(key4, relayB, hints.LastInNIP05, banDate+8*day)
|
||||||
|
hdb.Save(key4, relayB, hints.LastInNprofile, banDate+5*day)
|
||||||
|
hdb.PrintScores()
|
||||||
|
require.Equal(t, []string{relayA, relayB}, hdb.TopN(key4, 3))
|
||||||
|
|
||||||
|
// information about the new relay starts to spread through relay hints in tags only
|
||||||
|
hdb.Save(key4, relayC, hints.LastInTag, nostr.Now()-5*day)
|
||||||
|
hdb.Save(key4, relayC, hints.LastInTag, nostr.Now()-5*day)
|
||||||
|
hdb.Save(key4, relayC, hints.LastInNevent, nostr.Now()-5*day)
|
||||||
|
hdb.Save(key4, relayC, hints.LastInNIP05, nostr.Now()-5*day)
|
||||||
|
|
||||||
|
// as long as we see one tag hint the new relay will already be in our map
|
||||||
|
hdb.PrintScores()
|
||||||
|
require.Equal(t, []string{relayA, relayB, relayC}, hdb.TopN(key4, 3))
|
||||||
|
|
||||||
|
// client tries to fetch stuff from the old relays, but gets nothing new
|
||||||
|
hdb.Save(key4, relayA, hints.LastFetchAttempt, nostr.Now()-5*hour)
|
||||||
|
hdb.Save(key4, relayB, hints.LastFetchAttempt, nostr.Now()-5*hour)
|
||||||
|
|
||||||
|
// which is enough for us to transition to the new relay as the toppermost of the uppermost
|
||||||
|
hdb.PrintScores()
|
||||||
|
require.Equal(t, []string{relayC, relayA, relayB}, hdb.TopN(key4, 3))
|
||||||
|
|
||||||
|
// what if the big relays are attempting to game this algorithm by allowing some of our
|
||||||
|
// events from time to time while still shadowbanning us?
|
||||||
|
hdb.Save(key4, relayA, hints.MostRecentEventFetched, nostr.Now()-5*hour)
|
||||||
|
hdb.Save(key4, relayB, hints.MostRecentEventFetched, nostr.Now()-5*hour)
|
||||||
|
hdb.PrintScores()
|
||||||
|
require.Equal(t, []string{relayA, relayB, relayC}, hdb.TopN(key4, 3))
|
||||||
|
|
||||||
|
// we'll need overwhelming force from the third relay
|
||||||
|
// (actually just a relay list with just its name in it will be enough)
|
||||||
|
hdb.Save(key4, relayC, hints.LastFetchAttempt, nostr.Now()-5*hour)
|
||||||
|
hdb.Save(key4, relayC, hints.MostRecentEventFetched, nostr.Now()-6*hour)
|
||||||
|
hdb.Save(key4, relayC, hints.LastInRelayList, nostr.Now()-6*hour)
|
||||||
|
hdb.PrintScores()
|
||||||
|
require.Equal(t, []string{relayC, relayA, relayB}, hdb.TopN(key4, 3))
|
||||||
|
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// things remain the same for key1, key2 and key3
|
||||||
|
require.Equal(t, []string{relayC, relayA}, hdb.TopN(key2, 2))
|
||||||
|
require.Equal(t, []string{relayB, relayA, relayC}, hdb.TopN(key1, 3))
|
||||||
|
require.Equal(t, []string{relayA, relayB}, hdb.TopN(key3, 3))
|
||||||
|
}
|
65
sdk/input.go
Normal file
65
sdk/input.go
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
package sdk
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/hex"
|
||||||
|
|
||||||
|
"github.com/nbd-wtf/go-nostr"
|
||||||
|
"github.com/nbd-wtf/go-nostr/nip05"
|
||||||
|
"github.com/nbd-wtf/go-nostr/nip19"
|
||||||
|
)
|
||||||
|
|
||||||
|
// InputToProfile turns any npub/nprofile/hex/nip05 input into a ProfilePointer (or nil).
|
||||||
|
func InputToProfile(ctx context.Context, input string) *nostr.ProfilePointer {
|
||||||
|
// handle if it is a hex string
|
||||||
|
if len(input) == 64 {
|
||||||
|
if _, err := hex.DecodeString(input); err == nil {
|
||||||
|
return &nostr.ProfilePointer{PublicKey: input}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle nip19 codes, if that's the case
|
||||||
|
prefix, data, _ := nip19.Decode(input)
|
||||||
|
switch prefix {
|
||||||
|
case "npub":
|
||||||
|
input = data.(string)
|
||||||
|
return &nostr.ProfilePointer{PublicKey: input}
|
||||||
|
case "nprofile":
|
||||||
|
pp := data.(nostr.ProfilePointer)
|
||||||
|
return &pp
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle nip05 ids, if that's the case
|
||||||
|
pp, _ := nip05.QueryIdentifier(ctx, input)
|
||||||
|
if pp != nil {
|
||||||
|
return pp
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// InputToEventPointer turns any note/nevent/hex input into a EventPointer (or nil).
|
||||||
|
func InputToEventPointer(input string) *nostr.EventPointer {
|
||||||
|
// handle if it is a hex string
|
||||||
|
if len(input) == 64 {
|
||||||
|
if _, err := hex.DecodeString(input); err == nil {
|
||||||
|
return &nostr.EventPointer{ID: input}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle nip19 codes, if that's the case
|
||||||
|
prefix, data, _ := nip19.Decode(input)
|
||||||
|
switch prefix {
|
||||||
|
case "note":
|
||||||
|
if input, ok := data.(string); ok {
|
||||||
|
return &nostr.EventPointer{ID: input}
|
||||||
|
}
|
||||||
|
case "nevent":
|
||||||
|
if ep, ok := data.(nostr.EventPointer); ok {
|
||||||
|
return &ep
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle nip05 ids, if that's the case
|
||||||
|
return nil
|
||||||
|
}
|
82
sdk/list.go
Normal file
82
sdk/list.go
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
package sdk
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"slices"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/nbd-wtf/go-nostr"
|
||||||
|
"github.com/nbd-wtf/go-nostr/sdk/cache"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GenericList[I TagItemWithValue] struct {
|
||||||
|
PubKey string `json:"-"` // must always be set otherwise things will break
|
||||||
|
Event *nostr.Event `json:"-"` // may be empty if a contact list event wasn't found
|
||||||
|
|
||||||
|
Items []I
|
||||||
|
}
|
||||||
|
|
||||||
|
type TagItemWithValue interface {
|
||||||
|
Value() string
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchGenericList[I TagItemWithValue](
|
||||||
|
sys *System,
|
||||||
|
ctx context.Context,
|
||||||
|
pubkey string,
|
||||||
|
kind int,
|
||||||
|
parseTag func(nostr.Tag) (I, bool),
|
||||||
|
cache cache.Cache32[GenericList[I]],
|
||||||
|
skipFetch bool,
|
||||||
|
) (fl GenericList[I], fromInternal bool) {
|
||||||
|
if cache != nil {
|
||||||
|
if v, ok := cache.Get(pubkey); ok {
|
||||||
|
return v, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
events, _ := sys.StoreRelay.QuerySync(ctx, nostr.Filter{Kinds: []int{kind}, Authors: []string{pubkey}})
|
||||||
|
if len(events) != 0 {
|
||||||
|
items := parseItemsFromEventTags(events[0], parseTag)
|
||||||
|
v := GenericList[I]{
|
||||||
|
PubKey: pubkey,
|
||||||
|
Event: events[0],
|
||||||
|
Items: items,
|
||||||
|
}
|
||||||
|
cache.SetWithTTL(pubkey, v, time.Hour*6)
|
||||||
|
return v, true
|
||||||
|
}
|
||||||
|
|
||||||
|
v := GenericList[I]{PubKey: pubkey}
|
||||||
|
if !skipFetch {
|
||||||
|
thunk := sys.replaceableLoaders[kind].Load(ctx, pubkey)
|
||||||
|
evt, err := thunk()
|
||||||
|
if err == nil {
|
||||||
|
items := parseItemsFromEventTags(evt, parseTag)
|
||||||
|
v.Items = items
|
||||||
|
if cache != nil {
|
||||||
|
cache.SetWithTTL(pubkey, v, time.Hour*6)
|
||||||
|
}
|
||||||
|
sys.StoreRelay.Publish(ctx, *evt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return v, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseItemsFromEventTags[I TagItemWithValue](
|
||||||
|
evt *nostr.Event,
|
||||||
|
parseTag func(nostr.Tag) (I, bool),
|
||||||
|
) []I {
|
||||||
|
result := make([]I, 0, len(evt.Tags))
|
||||||
|
for _, tag := range evt.Tags {
|
||||||
|
item, ok := parseTag(tag)
|
||||||
|
if ok {
|
||||||
|
// check if this already exists before adding
|
||||||
|
if slices.IndexFunc(result, func(i I) bool { return i.Value() == item.Value() }) == -1 {
|
||||||
|
result = append(result, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
155
sdk/metadata.go
Normal file
155
sdk/metadata.go
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
package sdk
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/nbd-wtf/go-nostr"
|
||||||
|
"github.com/nbd-wtf/go-nostr/nip19"
|
||||||
|
"github.com/nbd-wtf/go-nostr/sdk/hints"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProfileMetadata struct {
|
||||||
|
PubKey string `json:"-"` // must always be set otherwise things will break
|
||||||
|
Event *nostr.Event `json:"-"` // may be empty if a profile metadata event wasn't found
|
||||||
|
|
||||||
|
// every one of these may be empty
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
DisplayName string `json:"display_name,omitempty"`
|
||||||
|
About string `json:"about,omitempty"`
|
||||||
|
Website string `json:"website,omitempty"`
|
||||||
|
Picture string `json:"picture,omitempty"`
|
||||||
|
Banner string `json:"banner,omitempty"`
|
||||||
|
NIP05 string `json:"nip05,omitempty"`
|
||||||
|
LUD16 string `json:"lud16,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p ProfileMetadata) Npub() string {
|
||||||
|
v, _ := nip19.EncodePublicKey(p.PubKey)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p ProfileMetadata) NpubShort() string {
|
||||||
|
npub := p.Npub()
|
||||||
|
return npub[0:7] + "…" + npub[58:]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p ProfileMetadata) Nprofile(ctx context.Context, sys *System, nrelays int) string {
|
||||||
|
v, _ := nip19.EncodeProfile(p.PubKey, sys.FetchOutboxRelays(ctx, p.PubKey, 2))
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p ProfileMetadata) ShortName() string {
|
||||||
|
if p.Name != "" {
|
||||||
|
return p.Name
|
||||||
|
}
|
||||||
|
if p.DisplayName != "" {
|
||||||
|
return p.DisplayName
|
||||||
|
}
|
||||||
|
return p.NpubShort()
|
||||||
|
}
|
||||||
|
|
||||||
|
// FetchProfileFromInput takes an nprofile, npub, nip05 or hex pubkey and returns a ProfileMetadata,
|
||||||
|
// updating the hintsDB in the process with any eventual relay hints
|
||||||
|
func (sys System) FetchProfileFromInput(ctx context.Context, nip19OrNip05Code string) (ProfileMetadata, error) {
|
||||||
|
p := InputToProfile(ctx, nip19OrNip05Code)
|
||||||
|
if p == nil {
|
||||||
|
return ProfileMetadata{}, fmt.Errorf("couldn't decode profile reference")
|
||||||
|
}
|
||||||
|
|
||||||
|
hintType := hints.LastInNIP05
|
||||||
|
if strings.HasPrefix(nip19OrNip05Code, "nprofile") {
|
||||||
|
hintType = hints.LastInNprofile
|
||||||
|
}
|
||||||
|
for _, r := range p.Relays {
|
||||||
|
nm := nostr.NormalizeURL(r)
|
||||||
|
if !IsVirtualRelay(nm) {
|
||||||
|
sys.Hints.Save(p.PublicKey, nm, hintType, nostr.Now())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pm := sys.FetchProfileMetadata(ctx, p.PublicKey)
|
||||||
|
return pm, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FetchProfileMetadata fetches metadata for a given user from the local cache, or from the local store,
|
||||||
|
// or, failing these, from the target user's defined outbox relays -- then caches the result.
|
||||||
|
func (sys *System) FetchProfileMetadata(ctx context.Context, pubkey string) (pm ProfileMetadata) {
|
||||||
|
if v, ok := sys.MetadataCache.Get(pubkey); ok {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
res, _ := sys.StoreRelay.QuerySync(ctx, nostr.Filter{Kinds: []int{0}, Authors: []string{pubkey}})
|
||||||
|
if len(res) != 0 {
|
||||||
|
if m, err := ParseMetadata(res[0]); err == nil {
|
||||||
|
m.PubKey = pubkey
|
||||||
|
m.Event = res[0]
|
||||||
|
sys.MetadataCache.SetWithTTL(pubkey, m, time.Hour*6)
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pm.PubKey = pubkey
|
||||||
|
|
||||||
|
thunk0 := sys.replaceableLoaders[0].Load(ctx, pubkey)
|
||||||
|
evt, err := thunk0()
|
||||||
|
if err == nil {
|
||||||
|
pm, _ = ParseMetadata(evt)
|
||||||
|
|
||||||
|
// save on store even if the metadata json is malformed
|
||||||
|
if sys.StoreRelay != nil && pm.Event != nil {
|
||||||
|
sys.StoreRelay.Publish(ctx, *pm.Event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// save on cache even if the metadata isn't found (unless the context was canceled)
|
||||||
|
if err == nil || err != context.Canceled {
|
||||||
|
sys.MetadataCache.SetWithTTL(pubkey, pm, time.Hour*6)
|
||||||
|
}
|
||||||
|
|
||||||
|
return pm
|
||||||
|
}
|
||||||
|
|
||||||
|
// FetchUserEvents fetches events from each users' outbox relays, grouping queries when possible.
|
||||||
|
func (sys *System) FetchUserEvents(ctx context.Context, filter nostr.Filter) (map[string][]*nostr.Event, error) {
|
||||||
|
filters, err := sys.ExpandQueriesByAuthorAndRelays(ctx, filter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to expand queries: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
results := make(map[string][]*nostr.Event)
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
wg.Add(len(filters))
|
||||||
|
for relayURL, filter := range filters {
|
||||||
|
go func(relayURL string, filter nostr.Filter) {
|
||||||
|
defer wg.Done()
|
||||||
|
filter.Limit = filter.Limit * len(filter.Authors) // hack
|
||||||
|
for ie := range sys.Pool.SubManyEose(ctx, []string{relayURL}, nostr.Filters{filter}) {
|
||||||
|
results[ie.PubKey] = append(results[ie.PubKey], ie.Event)
|
||||||
|
}
|
||||||
|
}(relayURL, filter)
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseMetadata(event *nostr.Event) (meta ProfileMetadata, err error) {
|
||||||
|
if event.Kind != 0 {
|
||||||
|
err = fmt.Errorf("event %s is kind %d, not 0", event.ID, event.Kind)
|
||||||
|
} else if er := json.Unmarshal([]byte(event.Content), &meta); er != nil {
|
||||||
|
cont := event.Content
|
||||||
|
if len(cont) > 100 {
|
||||||
|
cont = cont[0:99]
|
||||||
|
}
|
||||||
|
err = fmt.Errorf("failed to parse metadata (%s) from event %s: %w", cont, event.ID, er)
|
||||||
|
}
|
||||||
|
|
||||||
|
meta.PubKey = event.PubKey
|
||||||
|
meta.Event = event
|
||||||
|
return meta, err
|
||||||
|
}
|
10
sdk/mutes.go
Normal file
10
sdk/mutes.go
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
package sdk
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
type MuteList = GenericList[Follow]
|
||||||
|
|
||||||
|
func (sys *System) FetchMuteList(ctx context.Context, pubkey string) MuteList {
|
||||||
|
ml, _ := fetchGenericList[Follow](sys, ctx, pubkey, 10000, parseFollow, nil, false)
|
||||||
|
return ml
|
||||||
|
}
|
89
sdk/outbox.go
Normal file
89
sdk/outbox.go
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
package sdk
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/nbd-wtf/go-nostr"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (sys *System) FetchOutboxRelays(ctx context.Context, pubkey string, n int) []string {
|
||||||
|
if relays, ok := sys.outboxShortTermCache.Get(pubkey); ok {
|
||||||
|
if len(relays) > n {
|
||||||
|
relays = relays[0:n]
|
||||||
|
}
|
||||||
|
return relays
|
||||||
|
}
|
||||||
|
|
||||||
|
if rl, ok := sys.RelayListCache.Get(pubkey); !ok || (rl.Event != nil && rl.Event.CreatedAt < nostr.Now()-60*60*24*7) {
|
||||||
|
// try to fetch relays list again if we don't have one or if ours is a week old
|
||||||
|
fetchGenericList(sys, ctx, pubkey, 10002, parseRelayFromKind10002, sys.RelayListCache, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
relays := sys.Hints.TopN(pubkey, 6)
|
||||||
|
|
||||||
|
if len(relays) == 0 {
|
||||||
|
return []string{"wss://relay.damus.io", "wss://nos.lol"}
|
||||||
|
}
|
||||||
|
|
||||||
|
sys.outboxShortTermCache.SetWithTTL(pubkey, relays, time.Minute*2)
|
||||||
|
|
||||||
|
if len(relays) > n {
|
||||||
|
relays = relays[0:n]
|
||||||
|
}
|
||||||
|
|
||||||
|
return relays
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sys *System) ExpandQueriesByAuthorAndRelays(
|
||||||
|
ctx context.Context,
|
||||||
|
filter nostr.Filter,
|
||||||
|
) (map[string]nostr.Filter, error) {
|
||||||
|
n := len(filter.Authors)
|
||||||
|
if n == 0 {
|
||||||
|
return nil, fmt.Errorf("no authors in filter")
|
||||||
|
}
|
||||||
|
|
||||||
|
relaysForPubkey := make(map[string][]string, n)
|
||||||
|
mu := sync.Mutex{}
|
||||||
|
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
wg.Add(n)
|
||||||
|
for _, pubkey := range filter.Authors {
|
||||||
|
go func(pubkey string) {
|
||||||
|
defer wg.Done()
|
||||||
|
relayURLs := sys.FetchOutboxRelays(ctx, pubkey, 3)
|
||||||
|
c := 0
|
||||||
|
for _, r := range relayURLs {
|
||||||
|
relay, err := sys.Pool.EnsureRelay(r)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
mu.Lock()
|
||||||
|
relaysForPubkey[pubkey] = append(relaysForPubkey[pubkey], relay.URL)
|
||||||
|
mu.Unlock()
|
||||||
|
c++
|
||||||
|
if c == 3 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}(pubkey)
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
filterForRelay := make(map[string]nostr.Filter, n) // { [relay]: filter }
|
||||||
|
for pubkey, relays := range relaysForPubkey {
|
||||||
|
for _, relay := range relays {
|
||||||
|
flt, ok := filterForRelay[relay]
|
||||||
|
if !ok {
|
||||||
|
flt = filter.Clone()
|
||||||
|
filterForRelay[relay] = flt
|
||||||
|
}
|
||||||
|
flt.Authors = append(flt.Authors, pubkey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filterForRelay, nil
|
||||||
|
}
|
108
sdk/references.go
Normal file
108
sdk/references.go
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
package sdk
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/nbd-wtf/go-nostr"
|
||||||
|
"github.com/nbd-wtf/go-nostr/nip19"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Reference struct {
|
||||||
|
Text string
|
||||||
|
Start int
|
||||||
|
End int
|
||||||
|
Profile *nostr.ProfilePointer
|
||||||
|
Event *nostr.EventPointer
|
||||||
|
Entity *nostr.EntityPointer
|
||||||
|
}
|
||||||
|
|
||||||
|
var mentionRegex = regexp.MustCompile(`\bnostr:((note|npub|naddr|nevent|nprofile)1\w+)\b|#\[(\d+)\]`)
|
||||||
|
|
||||||
|
// ParseReferences parses both NIP-08 and NIP-27 references in a single unifying interface.
|
||||||
|
func ParseReferences(evt *nostr.Event) []*Reference {
|
||||||
|
var references []*Reference
|
||||||
|
content := evt.Content
|
||||||
|
|
||||||
|
for _, ref := range mentionRegex.FindAllStringSubmatchIndex(evt.Content, -1) {
|
||||||
|
reference := &Reference{
|
||||||
|
Text: content[ref[0]:ref[1]],
|
||||||
|
Start: ref[0],
|
||||||
|
End: ref[1],
|
||||||
|
}
|
||||||
|
|
||||||
|
if ref[6] == -1 {
|
||||||
|
// didn't find a NIP-10 #[0] reference, so it's a NIP-27 mention
|
||||||
|
nip19code := content[ref[2]:ref[3]]
|
||||||
|
|
||||||
|
if prefix, data, err := nip19.Decode(nip19code); err == nil {
|
||||||
|
switch prefix {
|
||||||
|
case "npub":
|
||||||
|
reference.Profile = &nostr.ProfilePointer{
|
||||||
|
PublicKey: data.(string), Relays: []string{},
|
||||||
|
}
|
||||||
|
case "nprofile":
|
||||||
|
pp := data.(nostr.ProfilePointer)
|
||||||
|
reference.Profile = &pp
|
||||||
|
case "note":
|
||||||
|
reference.Event = &nostr.EventPointer{ID: data.(string), Relays: []string{}}
|
||||||
|
case "nevent":
|
||||||
|
evp := data.(nostr.EventPointer)
|
||||||
|
reference.Event = &evp
|
||||||
|
case "naddr":
|
||||||
|
addr := data.(nostr.EntityPointer)
|
||||||
|
reference.Entity = &addr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// it's a NIP-10 mention.
|
||||||
|
// parse the number, get data from event tags.
|
||||||
|
n := content[ref[6]:ref[7]]
|
||||||
|
idx, err := strconv.Atoi(n)
|
||||||
|
if err != nil || len(evt.Tags) <= idx {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if tag := evt.Tags[idx]; tag != nil && len(tag) >= 2 {
|
||||||
|
switch tag[0] {
|
||||||
|
case "p":
|
||||||
|
relays := make([]string, 0, 1)
|
||||||
|
if len(tag) > 2 && tag[2] != "" {
|
||||||
|
relays = append(relays, tag[2])
|
||||||
|
}
|
||||||
|
reference.Profile = &nostr.ProfilePointer{
|
||||||
|
PublicKey: tag[1],
|
||||||
|
Relays: relays,
|
||||||
|
}
|
||||||
|
case "e":
|
||||||
|
relays := make([]string, 0, 1)
|
||||||
|
if len(tag) > 2 && tag[2] != "" {
|
||||||
|
relays = append(relays, tag[2])
|
||||||
|
}
|
||||||
|
reference.Event = &nostr.EventPointer{
|
||||||
|
ID: tag[1],
|
||||||
|
Relays: relays,
|
||||||
|
}
|
||||||
|
case "a":
|
||||||
|
if parts := strings.Split(tag[1], ":"); len(parts) == 3 {
|
||||||
|
kind, _ := strconv.Atoi(parts[0])
|
||||||
|
relays := make([]string, 0, 1)
|
||||||
|
if len(tag) > 2 && tag[2] != "" {
|
||||||
|
relays = append(relays, tag[2])
|
||||||
|
}
|
||||||
|
reference.Entity = &nostr.EntityPointer{
|
||||||
|
Identifier: parts[2],
|
||||||
|
PublicKey: parts[1],
|
||||||
|
Kind: kind,
|
||||||
|
Relays: relays,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
references = append(references, reference)
|
||||||
|
}
|
||||||
|
|
||||||
|
return references
|
||||||
|
}
|
108
sdk/references_test.go
Normal file
108
sdk/references_test.go
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
package sdk
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/nbd-wtf/go-nostr"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseReferences(t *testing.T) {
|
||||||
|
evt := nostr.Event{
|
||||||
|
Tags: nostr.Tags{
|
||||||
|
{"p", "c9d556c6d3978d112d30616d0d20aaa81410e3653911dd67787b5aaf9b36ade8", "wss://nostr.com"},
|
||||||
|
{"e", "a84c5de86efc2ec2cff7bad077c4171e09146b633b7ad117fffe088d9579ac33", "wss://other.com", "reply"},
|
||||||
|
{"e", "31d7c2875b5fc8e6f9c8f9dc1f84de1b6b91d1947ea4c59225e55c325d330fa8", ""},
|
||||||
|
},
|
||||||
|
Content: "hello #[0], have you seen #[2]? it was made by nostr:nprofile1qqsvc6ulagpn7kwrcwdqgp797xl7usumqa6s3kgcelwq6m75x8fe8yc5usxdg on nostr:nevent1qqsvc6ulagpn7kwrcwdqgp797xl7usumqa6s3kgcelwq6m75x8fe8ychxp5v4! broken #[3]",
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := []Reference{
|
||||||
|
{
|
||||||
|
Text: "#[0]",
|
||||||
|
Start: 6,
|
||||||
|
End: 10,
|
||||||
|
Profile: &nostr.ProfilePointer{
|
||||||
|
PublicKey: "c9d556c6d3978d112d30616d0d20aaa81410e3653911dd67787b5aaf9b36ade8",
|
||||||
|
Relays: []string{"wss://nostr.com"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Text: "#[2]",
|
||||||
|
Start: 26,
|
||||||
|
End: 30,
|
||||||
|
Event: &nostr.EventPointer{
|
||||||
|
ID: "31d7c2875b5fc8e6f9c8f9dc1f84de1b6b91d1947ea4c59225e55c325d330fa8",
|
||||||
|
Relays: []string{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Text: "nostr:nprofile1qqsvc6ulagpn7kwrcwdqgp797xl7usumqa6s3kgcelwq6m75x8fe8yc5usxdg",
|
||||||
|
Start: 47,
|
||||||
|
End: 123,
|
||||||
|
Profile: &nostr.ProfilePointer{
|
||||||
|
PublicKey: "cc6b9fea033f59c3c39a0407c5f1bfee439b077508d918cfdc0d6fd431d39393",
|
||||||
|
Relays: []string{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Text: "nostr:nevent1qqsvc6ulagpn7kwrcwdqgp797xl7usumqa6s3kgcelwq6m75x8fe8ychxp5v4",
|
||||||
|
Start: 127,
|
||||||
|
End: 201,
|
||||||
|
Event: &nostr.EventPointer{
|
||||||
|
ID: "cc6b9fea033f59c3c39a0407c5f1bfee439b077508d918cfdc0d6fd431d39393",
|
||||||
|
Relays: []string{},
|
||||||
|
Author: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
got := ParseReferences(&evt)
|
||||||
|
|
||||||
|
if len(got) != len(expected) {
|
||||||
|
t.Errorf("got %d references, expected %d", len(got), len(expected))
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, g := range got {
|
||||||
|
e := expected[i]
|
||||||
|
if g.Text != e.Text {
|
||||||
|
t.Errorf("%d: got text %s, expected %s", i, g.Text, e.Text)
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.Start != e.Start {
|
||||||
|
t.Errorf("%d: got start %d, expected %d", i, g.Start, e.Start)
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.End != e.End {
|
||||||
|
t.Errorf("%d: got end %d, expected %d", i, g.End, e.End)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (g.Entity == nil && e.Entity != nil) ||
|
||||||
|
(g.Event == nil && e.Event != nil) ||
|
||||||
|
(g.Profile == nil && e.Profile != nil) {
|
||||||
|
t.Errorf("%d: got some unexpected nil", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.Profile != nil && (g.Profile.PublicKey != e.Profile.PublicKey ||
|
||||||
|
len(g.Profile.Relays) != len(e.Profile.Relays) ||
|
||||||
|
(len(g.Profile.Relays) > 0 && g.Profile.Relays[0] != e.Profile.Relays[0])) {
|
||||||
|
t.Errorf("%d: profile value is wrong", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.Event != nil && (g.Event.ID != e.Event.ID ||
|
||||||
|
g.Event.Author != e.Event.Author ||
|
||||||
|
len(g.Event.Relays) != len(e.Event.Relays) ||
|
||||||
|
(len(g.Event.Relays) > 0 && g.Event.Relays[0] != e.Event.Relays[0])) {
|
||||||
|
fmt.Println(g.Event.ID, g.Event.Relays, len(g.Event.Relays), g.Event.Relays[0] == "")
|
||||||
|
fmt.Println(e.Event.Relays, len(e.Event.Relays))
|
||||||
|
t.Errorf("%d: event value is wrong", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.Entity != nil && (g.Entity.PublicKey != e.Entity.PublicKey ||
|
||||||
|
g.Entity.Identifier != e.Entity.Identifier ||
|
||||||
|
g.Entity.Kind != e.Entity.Kind ||
|
||||||
|
len(g.Entity.Relays) != len(g.Entity.Relays)) {
|
||||||
|
t.Errorf("%d: entity value is wrong", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
41
sdk/relays.go
Normal file
41
sdk/relays.go
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package sdk
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/nbd-wtf/go-nostr"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RelayList = GenericList[Relay]
|
||||||
|
|
||||||
|
type Relay struct {
|
||||||
|
URL string
|
||||||
|
Inbox bool
|
||||||
|
Outbox bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r Relay) Value() string { return r.URL }
|
||||||
|
|
||||||
|
func parseRelayFromKind10002(tag nostr.Tag) (rl Relay, ok bool) {
|
||||||
|
if u := tag.Value(); u != "" && tag[0] == "r" {
|
||||||
|
if !nostr.IsValidRelayURL(u) {
|
||||||
|
return rl, false
|
||||||
|
}
|
||||||
|
u := nostr.NormalizeURL(u)
|
||||||
|
|
||||||
|
relay := Relay{
|
||||||
|
URL: u,
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(tag) == 2 {
|
||||||
|
relay.Inbox = true
|
||||||
|
relay.Outbox = true
|
||||||
|
} else if tag[2] == "write" {
|
||||||
|
relay.Outbox = true
|
||||||
|
} else if tag[2] == "read" {
|
||||||
|
relay.Inbox = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return relay, true
|
||||||
|
}
|
||||||
|
|
||||||
|
return rl, false
|
||||||
|
}
|
194
sdk/replaceable_loader.go
Normal file
194
sdk/replaceable_loader.go
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
package sdk
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/graph-gophers/dataloader/v7"
|
||||||
|
"github.com/nbd-wtf/go-nostr"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EventResult dataloader.Result[*nostr.Event]
|
||||||
|
|
||||||
|
func (sys *System) initializeDataloaders() {
|
||||||
|
sys.replaceableLoaders = make(map[int]*dataloader.Loader[string, *nostr.Event])
|
||||||
|
for _, kind := range []int{0, 3, 10000, 10001, 10002, 10003, 10004, 10005, 10006, 10007, 10015, 10030} {
|
||||||
|
sys.replaceableLoaders[kind] = sys.createReplaceableDataloader(kind)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sys *System) createReplaceableDataloader(kind int) *dataloader.Loader[string, *nostr.Event] {
|
||||||
|
return dataloader.NewBatchedLoader(
|
||||||
|
func(
|
||||||
|
ctx context.Context,
|
||||||
|
pubkeys []string,
|
||||||
|
) []*dataloader.Result[*nostr.Event] {
|
||||||
|
return sys.batchLoadReplaceableEvents(ctx, kind, pubkeys)
|
||||||
|
},
|
||||||
|
dataloader.WithBatchCapacity[string, *nostr.Event](60),
|
||||||
|
dataloader.WithClearCacheOnBatch[string, *nostr.Event](),
|
||||||
|
dataloader.WithWait[string, *nostr.Event](time.Millisecond*350),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sys *System) batchLoadReplaceableEvents(
|
||||||
|
ctx context.Context,
|
||||||
|
kind int,
|
||||||
|
pubkeys []string,
|
||||||
|
) []*dataloader.Result[*nostr.Event] {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*4)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
batchSize := len(pubkeys)
|
||||||
|
results := make([]*dataloader.Result[*nostr.Event], batchSize)
|
||||||
|
keyPositions := make(map[string]int) // { [pubkey]: slice_index }
|
||||||
|
relayFilters := make(map[string]nostr.Filter) // { [relayUrl]: filter }
|
||||||
|
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
wg.Add(len(pubkeys))
|
||||||
|
cm := sync.Mutex{}
|
||||||
|
|
||||||
|
for i, pubkey := range pubkeys {
|
||||||
|
// build batched queries for the external relays
|
||||||
|
keyPositions[pubkey] = i // this is to help us know where to save the result later
|
||||||
|
|
||||||
|
go func(i int, pubkey string) {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
// if we're attempting this query with a short key (last 8 characters), stop here
|
||||||
|
if len(pubkey) != 64 {
|
||||||
|
results[i] = &dataloader.Result[*nostr.Event]{
|
||||||
|
Error: fmt.Errorf("won't proceed to query relays with a shortened key (%d)", kind),
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// save attempts here so we don't try the same failed query over and over
|
||||||
|
if doItNow := DoThisNotMoreThanOnceAnHour("repl:" + strconv.Itoa(kind) + pubkey); !doItNow {
|
||||||
|
results[i] = &dataloader.Result[*nostr.Event]{
|
||||||
|
Error: fmt.Errorf("last attempt failed, waiting more to try again"),
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// gather relays we'll use for this pubkey
|
||||||
|
relays := sys.determineRelaysToQuery(ctx, pubkey, kind)
|
||||||
|
|
||||||
|
// by default we will return an error (this will be overwritten when we find an event)
|
||||||
|
results[i] = &dataloader.Result[*nostr.Event]{
|
||||||
|
Error: fmt.Errorf("couldn't find a kind %d event anywhere %v", kind, relays),
|
||||||
|
}
|
||||||
|
|
||||||
|
cm.Lock()
|
||||||
|
for _, relay := range relays {
|
||||||
|
// each relay will have a custom filter
|
||||||
|
filter, ok := relayFilters[relay]
|
||||||
|
if !ok {
|
||||||
|
filter = nostr.Filter{
|
||||||
|
Kinds: []int{kind},
|
||||||
|
Authors: make([]string, 0, batchSize-i /* this and all pubkeys after this can be added */),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
filter.Authors = append(filter.Authors, pubkey)
|
||||||
|
relayFilters[relay] = filter
|
||||||
|
}
|
||||||
|
cm.Unlock()
|
||||||
|
}(i, pubkey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// query all relays with the prepared filters
|
||||||
|
wg.Wait()
|
||||||
|
multiSubs := sys.batchReplaceableRelayQueries(ctx, relayFilters)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case evt, more := <-multiSubs:
|
||||||
|
if !more {
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
// insert this event at the desired position
|
||||||
|
pos := keyPositions[evt.PubKey] // @unchecked: it must succeed because it must be a key we passed
|
||||||
|
if results[pos].Data == nil || results[pos].Data.CreatedAt < evt.CreatedAt {
|
||||||
|
results[pos] = &dataloader.Result[*nostr.Event]{Data: evt}
|
||||||
|
}
|
||||||
|
case <-ctx.Done():
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sys *System) determineRelaysToQuery(ctx context.Context, pubkey string, kind int) []string {
|
||||||
|
relays := make([]string, 0, 10)
|
||||||
|
|
||||||
|
// search in specific relays for user
|
||||||
|
if kind == 10002 {
|
||||||
|
// prevent infinite loops by jumping directly to this
|
||||||
|
relays = sys.Hints.TopN(pubkey, 3)
|
||||||
|
} else if kind == 0 {
|
||||||
|
// leave room for one hardcoded relay because people are stupid
|
||||||
|
relays = sys.FetchOutboxRelays(ctx, pubkey, 2)
|
||||||
|
} else {
|
||||||
|
relays = sys.FetchOutboxRelays(ctx, pubkey, 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
// use a different set of extra relays depending on the kind
|
||||||
|
for len(relays) < 3 {
|
||||||
|
switch kind {
|
||||||
|
case 0:
|
||||||
|
relays = append(relays, pickNext(sys.MetadataRelays))
|
||||||
|
case 3:
|
||||||
|
relays = append(relays, pickNext(sys.FollowListRelays))
|
||||||
|
case 10002:
|
||||||
|
relays = append(relays, pickNext(sys.RelayListRelays))
|
||||||
|
default:
|
||||||
|
relays = append(relays, pickNext(sys.FallbackRelays))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return relays
|
||||||
|
}
|
||||||
|
|
||||||
|
// batchReplaceableRelayQueries subscribes to multiple relays using a different filter for each and returns
|
||||||
|
// a single channel with all results. it closes on EOSE or when all the expected events were returned.
|
||||||
|
//
|
||||||
|
// the number of expected events is given by the number of pubkeys in the .Authors filter field.
|
||||||
|
// because of that, batchReplaceableRelayQueries is only suitable for querying replaceable events -- and
|
||||||
|
// care must be taken to not include the same pubkey more than once in the filter .Authors array.
|
||||||
|
func (sys *System) batchReplaceableRelayQueries(
|
||||||
|
ctx context.Context,
|
||||||
|
relayFilters map[string]nostr.Filter,
|
||||||
|
) <-chan *nostr.Event {
|
||||||
|
all := make(chan *nostr.Event)
|
||||||
|
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
wg.Add(len(relayFilters))
|
||||||
|
for url, filter := range relayFilters {
|
||||||
|
go func(url string, filter nostr.Filter) {
|
||||||
|
defer wg.Done()
|
||||||
|
n := len(filter.Authors)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, time.Millisecond*450+time.Millisecond*50*time.Duration(n))
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
received := 0
|
||||||
|
for ie := range sys.Pool.SubManyEose(ctx, []string{url}, nostr.Filters{filter}) {
|
||||||
|
all <- ie.Event
|
||||||
|
received++
|
||||||
|
if received >= n {
|
||||||
|
// we got all events we asked for, unless the relay is shitty and sent us two from the same
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}(url, filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
wg.Wait()
|
||||||
|
close(all)
|
||||||
|
}()
|
||||||
|
|
||||||
|
return all
|
||||||
|
}
|
24
sdk/search.go
Normal file
24
sdk/search.go
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package sdk
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/nbd-wtf/go-nostr"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (sys *System) SearchUsers(ctx context.Context, query string) []ProfileMetadata {
|
||||||
|
limit := 10
|
||||||
|
profiles := make([]ProfileMetadata, 0, limit*len(sys.UserSearchRelays))
|
||||||
|
|
||||||
|
for ie := range sys.Pool.SubManyEose(ctx, sys.UserSearchRelays, nostr.Filters{
|
||||||
|
{
|
||||||
|
Search: query,
|
||||||
|
Limit: limit,
|
||||||
|
},
|
||||||
|
}) {
|
||||||
|
m, _ := ParseMetadata(ie.Event)
|
||||||
|
profiles = append(profiles, m)
|
||||||
|
}
|
||||||
|
|
||||||
|
return profiles
|
||||||
|
}
|
158
sdk/system.go
Normal file
158
sdk/system.go
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
package sdk
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/fiatjaf/eventstore"
|
||||||
|
"github.com/fiatjaf/eventstore/slicestore"
|
||||||
|
"github.com/graph-gophers/dataloader/v7"
|
||||||
|
"github.com/nbd-wtf/go-nostr"
|
||||||
|
"github.com/nbd-wtf/go-nostr/sdk/cache"
|
||||||
|
cache_memory "github.com/nbd-wtf/go-nostr/sdk/cache/memory"
|
||||||
|
"github.com/nbd-wtf/go-nostr/sdk/hints"
|
||||||
|
memory_hints "github.com/nbd-wtf/go-nostr/sdk/hints/memory"
|
||||||
|
)
|
||||||
|
|
||||||
|
type System struct {
|
||||||
|
RelayListCache cache.Cache32[RelayList]
|
||||||
|
FollowListCache cache.Cache32[FollowList]
|
||||||
|
MetadataCache cache.Cache32[ProfileMetadata]
|
||||||
|
Hints hints.HintsDB
|
||||||
|
Pool *nostr.SimplePool
|
||||||
|
RelayListRelays []string
|
||||||
|
FollowListRelays []string
|
||||||
|
MetadataRelays []string
|
||||||
|
FallbackRelays []string
|
||||||
|
UserSearchRelays []string
|
||||||
|
NoteSearchRelays []string
|
||||||
|
Store eventstore.Store
|
||||||
|
|
||||||
|
StoreRelay nostr.RelayStore
|
||||||
|
|
||||||
|
replaceableLoaders map[int]*dataloader.Loader[string, *nostr.Event]
|
||||||
|
outboxShortTermCache cache.Cache32[[]string]
|
||||||
|
}
|
||||||
|
|
||||||
|
type SystemModifier func(sys *System)
|
||||||
|
|
||||||
|
func NewSystem(mods ...SystemModifier) *System {
|
||||||
|
sys := &System{
|
||||||
|
RelayListCache: cache_memory.New32[RelayList](1000),
|
||||||
|
FollowListCache: cache_memory.New32[FollowList](1000),
|
||||||
|
MetadataCache: cache_memory.New32[ProfileMetadata](1000),
|
||||||
|
RelayListRelays: []string{"wss://purplepag.es", "wss://user.kindpag.es", "wss://relay.nos.social"},
|
||||||
|
FollowListRelays: []string{"wss://purplepag.es", "wss://user.kindpag.es", "wss://relay.nos.social"},
|
||||||
|
MetadataRelays: []string{"wss://purplepag.es", "wss://user.kindpag.es", "wss://relay.nos.social"},
|
||||||
|
FallbackRelays: []string{
|
||||||
|
"wss://relay.primal.net",
|
||||||
|
"wss://relay.damus.io",
|
||||||
|
"wss://nostr.wine",
|
||||||
|
"wss://nostr.mom",
|
||||||
|
"wss://offchain.pub",
|
||||||
|
"wss://nos.lol",
|
||||||
|
"wss://mostr.pub",
|
||||||
|
"wss://relay.nostr.band",
|
||||||
|
"wss://nostr21.com",
|
||||||
|
},
|
||||||
|
UserSearchRelays: []string{
|
||||||
|
"wss://nostr.wine",
|
||||||
|
"wss://relay.nostr.band",
|
||||||
|
"wss://relay.noswhere.com",
|
||||||
|
},
|
||||||
|
NoteSearchRelays: []string{
|
||||||
|
"wss://nostr.wine",
|
||||||
|
"wss://relay.nostr.band",
|
||||||
|
"wss://relay.noswhere.com",
|
||||||
|
},
|
||||||
|
Hints: memory_hints.NewHintDB(),
|
||||||
|
|
||||||
|
outboxShortTermCache: cache_memory.New32[[]string](1000),
|
||||||
|
}
|
||||||
|
|
||||||
|
sys.Pool = nostr.NewSimplePool(context.Background(),
|
||||||
|
nostr.WithEventMiddleware(sys.trackEventHints),
|
||||||
|
nostr.WithPenaltyBox(),
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, mod := range mods {
|
||||||
|
mod(sys)
|
||||||
|
}
|
||||||
|
|
||||||
|
if sys.Store == nil {
|
||||||
|
sys.Store = &slicestore.SliceStore{}
|
||||||
|
sys.Store.Init()
|
||||||
|
}
|
||||||
|
sys.StoreRelay = eventstore.RelayWrapper{Store: sys.Store}
|
||||||
|
|
||||||
|
sys.initializeDataloaders()
|
||||||
|
|
||||||
|
return sys
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sys *System) Close() {}
|
||||||
|
|
||||||
|
func WithHintsDB(hdb hints.HintsDB) SystemModifier {
|
||||||
|
return func(sys *System) {
|
||||||
|
sys.Hints = hdb
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithRelayListRelays(list []string) SystemModifier {
|
||||||
|
return func(sys *System) {
|
||||||
|
sys.RelayListRelays = list
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithMetadataRelays(list []string) SystemModifier {
|
||||||
|
return func(sys *System) {
|
||||||
|
sys.MetadataRelays = list
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithFollowListRelays(list []string) SystemModifier {
|
||||||
|
return func(sys *System) {
|
||||||
|
sys.FollowListRelays = list
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithFallbackRelays(list []string) SystemModifier {
|
||||||
|
return func(sys *System) {
|
||||||
|
sys.FallbackRelays = list
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithUserSearchRelays(list []string) SystemModifier {
|
||||||
|
return func(sys *System) {
|
||||||
|
sys.UserSearchRelays = list
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithNoteSearchRelays(list []string) SystemModifier {
|
||||||
|
return func(sys *System) {
|
||||||
|
sys.NoteSearchRelays = list
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithStore(store eventstore.Store) SystemModifier {
|
||||||
|
return func(sys *System) {
|
||||||
|
sys.Store = store
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithRelayListCache(cache cache.Cache32[RelayList]) SystemModifier {
|
||||||
|
return func(sys *System) {
|
||||||
|
sys.RelayListCache = cache
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithFollowListCache(cache cache.Cache32[FollowList]) SystemModifier {
|
||||||
|
return func(sys *System) {
|
||||||
|
sys.FollowListCache = cache
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithMetadataCache(cache cache.Cache32[ProfileMetadata]) SystemModifier {
|
||||||
|
return func(sys *System) {
|
||||||
|
sys.MetadataCache = cache
|
||||||
|
}
|
||||||
|
}
|
86
sdk/tracker.go
Normal file
86
sdk/tracker.go
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
package sdk
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/nbd-wtf/go-nostr"
|
||||||
|
"github.com/nbd-wtf/go-nostr/sdk/hints"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (sys *System) trackEventHints(ie nostr.IncomingEvent) {
|
||||||
|
if IsVirtualRelay(ie.Relay.URL) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ie.Kind {
|
||||||
|
case nostr.KindRelayListMetadata:
|
||||||
|
for _, tag := range ie.Tags {
|
||||||
|
if len(tag) < 2 || tag[0] != "r" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(tag) == 2 || (tag[2] == "" || tag[2] == "write") {
|
||||||
|
sys.Hints.Save(ie.PubKey, tag[1], hints.LastInRelayList, ie.CreatedAt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case nostr.KindContactList:
|
||||||
|
sys.Hints.Save(ie.PubKey, ie.Relay.URL, hints.MostRecentEventFetched, ie.CreatedAt)
|
||||||
|
|
||||||
|
for _, tag := range ie.Tags {
|
||||||
|
if len(tag) < 3 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if IsVirtualRelay(tag[2]) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if p, err := url.Parse(tag[2]); err != nil || (p.Scheme != "wss" && p.Scheme != "ws") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if tag[0] == "p" && nostr.IsValidPublicKey(tag[1]) {
|
||||||
|
sys.Hints.Save(tag[1], tag[2], hints.LastInTag, ie.CreatedAt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case nostr.KindTextNote:
|
||||||
|
sys.Hints.Save(ie.PubKey, ie.Relay.URL, hints.MostRecentEventFetched, ie.CreatedAt)
|
||||||
|
|
||||||
|
for _, tag := range ie.Tags {
|
||||||
|
if len(tag) < 3 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if IsVirtualRelay(tag[2]) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if p, err := url.Parse(tag[2]); err != nil || (p.Scheme != "wss" && p.Scheme != "ws") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if tag[0] == "p" && nostr.IsValidPublicKey(tag[1]) {
|
||||||
|
sys.Hints.Save(tag[1], tag[2], hints.LastInTag, ie.CreatedAt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ref := range ParseReferences(ie.Event) {
|
||||||
|
if ref.Profile != nil {
|
||||||
|
for _, relay := range ref.Profile.Relays {
|
||||||
|
if IsVirtualRelay(relay) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if p, err := url.Parse(relay); err != nil || (p.Scheme != "wss" && p.Scheme != "ws") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if nostr.IsValidPublicKey(ref.Profile.PublicKey) {
|
||||||
|
sys.Hints.Save(ref.Profile.PublicKey, relay, hints.LastInNprofile, ie.CreatedAt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if ref.Event != nil && nostr.IsValidPublicKey(ref.Event.Author) {
|
||||||
|
for _, relay := range ref.Event.Relays {
|
||||||
|
if IsVirtualRelay(relay) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if p, err := url.Parse(relay); err != nil || (p.Scheme != "wss" && p.Scheme != "ws") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
sys.Hints.Save(ref.Event.Author, relay, hints.LastInNevent, ie.CreatedAt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
43
sdk/utils.go
Normal file
43
sdk/utils.go
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
package sdk
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_dtnmtoah map[string]time.Time
|
||||||
|
_dtnmtoahLock sync.Mutex
|
||||||
|
)
|
||||||
|
|
||||||
|
func DoThisNotMoreThanOnceAnHour(key string) (doItNow bool) {
|
||||||
|
if _dtnmtoah == nil {
|
||||||
|
go func() {
|
||||||
|
_dtnmtoah = make(map[string]time.Time)
|
||||||
|
for {
|
||||||
|
time.Sleep(time.Minute * 10)
|
||||||
|
_dtnmtoahLock.Lock()
|
||||||
|
now := time.Now()
|
||||||
|
for k, v := range _dtnmtoah {
|
||||||
|
if v.Before(now) {
|
||||||
|
delete(_dtnmtoah, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_dtnmtoahLock.Unlock()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
_dtnmtoahLock.Lock()
|
||||||
|
defer _dtnmtoahLock.Unlock()
|
||||||
|
|
||||||
|
_, exists := _dtnmtoah[key]
|
||||||
|
return !exists
|
||||||
|
}
|
||||||
|
|
||||||
|
var serial = 0
|
||||||
|
|
||||||
|
func pickNext(list []string) string {
|
||||||
|
serial++
|
||||||
|
return list[serial%len(list)]
|
||||||
|
}
|
Reference in New Issue
Block a user