mirror of
https://github.com/jeremyd/ergo.git
synced 2026-04-23 22:29:58 -07:00
noirc launch
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,5 +1,7 @@
|
||||
# Created by https://www.gitignore.io/api/windows,osx,linux,go
|
||||
|
||||
vendor
|
||||
|
||||
### Windows ###
|
||||
# Windows image file caches
|
||||
Thumbs.db
|
||||
@@ -110,4 +112,4 @@ ergo.prof
|
||||
ergo.mprof
|
||||
/dist
|
||||
*.pem
|
||||
.dccache
|
||||
.dccache
|
||||
|
||||
17
default.yaml
17
default.yaml
@@ -360,6 +360,10 @@ server:
|
||||
# all users will receive simply `netname` as their cloaked hostname.
|
||||
num-bits: 64
|
||||
|
||||
# enable nostr-based hostnames for accounts registered with nostr verification
|
||||
# (e.g., alice@example.com -> alice.example.com, npub1abc123... -> npub1abc123.nostr)
|
||||
nostr-hostnames: true
|
||||
|
||||
# secure-nets identifies IPs and CIDRs which are secure at layer 3,
|
||||
# for example, because they are on a trusted internal LAN or a VPN.
|
||||
# plaintext connections from these IPs and CIDRs will be considered
|
||||
@@ -471,6 +475,19 @@ accounts:
|
||||
# time for which a password reset code is valid
|
||||
timeout: 1d
|
||||
|
||||
# options for nostr-based verification of account registrations
|
||||
nostr-verification:
|
||||
enabled: true
|
||||
# hex-encoded private key for signing DMs (generate with: openssl rand -hex 32)
|
||||
private-key: ""
|
||||
# default relays to use for sending DMs and relay discovery
|
||||
default-relays:
|
||||
- "wss://relay.damus.io"
|
||||
- "wss://nos.lol"
|
||||
- "wss://profiles.nostr1.com"
|
||||
# timeout for nostr operations (NIP-05 resolution, relay connections)
|
||||
timeout: 30s
|
||||
|
||||
# throttle account login attempts (to prevent either password guessing, or DoS
|
||||
# attacks on the server aimed at forcing repeated expensive bcrypt computations)
|
||||
login-throttling:
|
||||
|
||||
46
ergo.motd
46
ergo.motd
@@ -1,35 +1,15 @@
|
||||
__ __ ______ ___ ______ ___
|
||||
__/ // /_/ ____/ __ \/ ____/ __ \
|
||||
/_ // __/ __/ / /_/ / / __/ / / /
|
||||
/_ // __/ /___/ _, _/ /_/ / /_/ /
|
||||
/_//_/ /_____/_/ |_|\____/\____/
|
||||
|
||||
░▒▓███████▓▒░ ░▒▓██████▓▒░░▒▓█▓▒░▒▓███████▓▒░ ░▒▓██████▓▒░
|
||||
░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░
|
||||
░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░
|
||||
░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░▒▓███████▓▒░░▒▓█▓▒░
|
||||
░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░
|
||||
░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░
|
||||
░▒▓█▓▒░░▒▓█▓▒░░▒▓██████▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░░▒▓██████▓▒░
|
||||
|
||||
$iAn IRC server for $bNOSTR$r communities.$r
|
||||
|
||||
$c1,82 To register /msg Nickserv register <new password> <your NIP05 -or- npub>
|
||||
$c1,82 Nickserv will send you a Nostr DM with verification instructions!
|
||||
|
||||
This is the default Ergo MOTD.
|
||||
|
||||
|
||||
If motd-formatting is enabled in the config file, you can use the dollarsign character to
|
||||
create special formatting such as bold, italics and color codes.
|
||||
|
||||
For example, here are a few formatted lines (enable motd-formatting to see these in action):
|
||||
|
||||
- this is $bbold text$r.
|
||||
- this is $iitalics text$r.
|
||||
- this is $c[red]red$c and $c[blue]blue$c text.
|
||||
- this is $c[red,light blue]red text with a light blue background$c.
|
||||
- this is a normal escaped dollarsign: $$
|
||||
|
||||
And now a few fun colour charts!
|
||||
|
||||
$c1,0 00 $c0,1 01 $c0,2 02 $c0,3 03 $c1,4 04 $c0,5 05 $c0,6 06 $c1,7 07
|
||||
$c1,8 08 $c1,9 09 $c0,10 10 $c1,11 11 $c0,12 12 $c1,13 13 $c1,14 14 $c1,15 15
|
||||
|
||||
$c0,16 16 $c0,17 17 $c0,18 18 $c0,19 19 $c0,20 20 $c0,21 21 $c0,22 22 $c0,23 23 $c0,24 24 $c0,25 25 $c0,26 26 $c0,27 27
|
||||
$c0,28 28 $c0,29 29 $c0,30 30 $c0,31 31 $c0,32 32 $c0,33 33 $c0,34 34 $c0,35 35 $c0,36 36 $c0,37 37 $c0,38 38 $c0,39 39
|
||||
$c0,40 40 $c0,41 41 $c0,42 42 $c0,43 43 $c0,44 44 $c0,45 45 $c0,46 46 $c0,47 47 $c0,48 48 $c0,49 49 $c0,50 50 $c0,51 51
|
||||
$c0,52 52 $c0,53 53 $c1,54 54 $c1,55 55 $c1,56 56 $c1,57 57 $c1,58 58 $c0,59 59 $c0,60 60 $c0,61 61 $c0,62 62 $c0,63 63
|
||||
$c0,64 64 $c1,65 65 $c1,66 66 $c1,67 67 $c1,68 68 $c1,69 69 $c1,70 70 $c1,71 71 $c0,72 72 $c0,73 73 $c0,74 74 $c0,75 75
|
||||
$c1,76 76 $c1,77 77 $c1,78 78 $c1,79 79 $c1,80 80 $c1,81 81 $c1,82 82 $c1,83 83 $c1,84 84 $c1,85 85 $c1,86 86 $c1,87 87
|
||||
$c0,88 88 $c0,89 89 $c0,90 90 $c0,91 91 $c0,92 92 $c0,93 93 $c0,94 94 $c0,95 95 $c1,96 96 $c1,97 97 $c1,98 98 $c99,99 99
|
||||
|
||||
For more information on using these, see MOTDFORMATTING.md
|
||||
Join #help channel for questions.
|
||||
|
||||
44
go.mod
44
go.mod
@@ -11,14 +11,20 @@ require (
|
||||
github.com/ergochat/irc-go v0.5.0-rc2
|
||||
github.com/go-sql-driver/mysql v1.7.0
|
||||
github.com/gofrs/flock v0.8.1
|
||||
github.com/gorilla/websocket v1.4.2
|
||||
github.com/nbd-wtf/go-nostr v0.52.0
|
||||
github.com/okzk/sdnotify v0.0.0-20180710141335-d9becc38acbd
|
||||
github.com/onsi/ginkgo v1.12.0 // indirect
|
||||
github.com/onsi/gomega v1.9.0 // indirect
|
||||
github.com/stretchr/testify v1.4.0 // indirect
|
||||
github.com/tidwall/btree v1.4.2 // indirect
|
||||
github.com/tidwall/buntdb v1.3.2
|
||||
github.com/tidwall/gjson v1.18.0 // indirect
|
||||
github.com/tidwall/grect v0.1.4 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/rtred v0.1.2 // indirect
|
||||
github.com/tidwall/tinyqueue v0.1.1 // indirect
|
||||
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||
github.com/xdg-go/scram v1.0.2
|
||||
golang.org/x/crypto v0.38.0
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/term v0.32.0
|
||||
golang.org/x/text v0.25.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
@@ -28,18 +34,30 @@ require (
|
||||
github.com/emersion/go-msgauth v0.7.0
|
||||
github.com/ergochat/webpush-go/v2 v2.0.0
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/tidwall/btree v1.4.2 // indirect
|
||||
github.com/tidwall/gjson v1.14.3 // indirect
|
||||
github.com/tidwall/grect v0.1.4 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
github.com/tidwall/rtred v0.1.2 // indirect
|
||||
github.com/tidwall/tinyqueue v0.1.1 // indirect
|
||||
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3 // indirect
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.3.4 // indirect
|
||||
github.com/btcsuite/btcd/btcutil v1.1.5 // indirect
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect
|
||||
github.com/bytedance/sonic v1.13.1 // indirect
|
||||
github.com/bytedance/sonic/loader v0.2.4 // indirect
|
||||
github.com/cloudwego/base64x v0.1.5 // indirect
|
||||
github.com/coder/websocket v1.8.12 // indirect
|
||||
github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
||||
github.com/mailru/easyjson v0.9.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
golang.org/x/arch v0.15.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
|
||||
)
|
||||
|
||||
replace github.com/gorilla/websocket => github.com/ergochat/websocket v1.4.2-oragono1
|
||||
|
||||
175
go.sum
175
go.sum
@@ -2,12 +2,57 @@ code.cloudfoundry.org/bytefmt v0.0.0-20200131002437-cf55d5288a48 h1:/EMHruHCFXR9
|
||||
code.cloudfoundry.org/bytefmt v0.0.0-20200131002437-cf55d5288a48/go.mod h1:wN/zk7mhREp/oviagqUXY3EwuHhWyOvAdsn5Y4CzOrc=
|
||||
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962 h1:KeNholpO2xKjgaaSyd+DyQRrsQjhbSeS7qe4nEw8aQw=
|
||||
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962/go.mod h1:kC29dT1vFpj7py2OvG1khBdQpo3kInWP+6QipLbdngo=
|
||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||
github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3 h1:ClzzXMDDuUbWfNNZqGeYq4PnYOlwlOVIvSyNaIy0ykg=
|
||||
github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3/go.mod h1:we0YA5CsBbH5+/NUzC/AlMmxaDtWlXeNsqrwXjTzmzA=
|
||||
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
|
||||
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
|
||||
github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M=
|
||||
github.com/btcsuite/btcd v0.23.5-0.20231215221805-96c9fd8078fd/go.mod h1:nm3Bko6zh6bWP60UxwoT5LzdGJsQJaPo6HjduXq9p6A=
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA=
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE=
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.3.4 h1:3EJjcN70HCu/mwqlUsGK8GcNVyLVxFDlWurTXGPFfiQ=
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.3.4/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04=
|
||||
github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A=
|
||||
github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE=
|
||||
github.com/btcsuite/btcd/btcutil v1.1.5 h1:+wER79R5670vs/ZusMTF1yTcRYE5GUsFbdjdisflzM8=
|
||||
github.com/btcsuite/btcd/btcutil v1.1.5/go.mod h1:PSZZ4UitpLBWzxGd5VGOrLnmOjtPP/a6HaFo12zMs00=
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 h1:59Kx4K6lzOW5w6nFlA0v5+lk/6sjybR934QNHSJZPTQ=
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
|
||||
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
|
||||
github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
|
||||
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg=
|
||||
github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY=
|
||||
github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I=
|
||||
github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
|
||||
github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
|
||||
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
|
||||
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
|
||||
github.com/bytedance/sonic v1.13.1 h1:Jyd5CIvdFnkOWuKXr+wm4Nyk2h0yAFsr8ucJgEasO3g=
|
||||
github.com/bytedance/sonic v1.13.1/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
|
||||
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
|
||||
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
|
||||
github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
||||
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
|
||||
github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218=
|
||||
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 h1:bWDMxwH3px2JBh6AyO7hdCn/PkvCZXii8TGj7sbtEbQ=
|
||||
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
|
||||
github.com/emersion/go-msgauth v0.6.8 h1:kW/0E9E8Zx5CdKsERC/WnAvnXvX7q9wTHia1OA4944A=
|
||||
github.com/emersion/go-msgauth v0.6.8/go.mod h1:YDwuyTCUHu9xxmAeVj0eW4INnwB6NNZoPdLerpSxRrc=
|
||||
github.com/dvyukov/go-fuzz v0.0.0-20200318091601-be3528f3a813/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
|
||||
github.com/emersion/go-msgauth v0.7.0 h1:vj2hMn6KhFtW41kshIBTXvp6KgYSqpA/ZN9Pv4g1INc=
|
||||
github.com/emersion/go-msgauth v0.7.0/go.mod h1:mmS9I6HkSovrNgq0HNXTeu8l3sRAAuQ9RMvbM4KU7Ck=
|
||||
github.com/ergochat/confusables v0.0.0-20201108231250-4ab98ab61fb1 h1:WLHTOodthVyv5NvYLIvWl112kSFv5IInKKrRN2qpons=
|
||||
@@ -23,6 +68,8 @@ github.com/ergochat/webpush-go/v2 v2.0.0/go.mod h1:OQlhnq8JeHDzRzAy6bdDObr19uqbH
|
||||
github.com/ergochat/websocket v1.4.2-oragono1 h1:plMUunFBM6UoSCIYCKKclTdy/TkkHfUslhOfJQzfueM=
|
||||
github.com/ergochat/websocket v1.4.2-oragono1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
|
||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
|
||||
@@ -30,21 +77,69 @@ github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14j
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
|
||||
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/nbd-wtf/go-nostr v0.52.0 h1:9gtz0VOUPOb0PC2kugr2WJAxThlCSSM62t5VC3tvk1g=
|
||||
github.com/nbd-wtf/go-nostr v0.52.0/go.mod h1:4avYoc9mDGZ9wHsvCOhHH9vPzKucCfuYBtJUSpHTfNk=
|
||||
github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/okzk/sdnotify v0.0.0-20180710141335-d9becc38acbd h1:+iAPaTbi1gZpcpDwe/BW1fx7Xoesv69hLNGPheoyhBs=
|
||||
github.com/okzk/sdnotify v0.0.0-20180710141335-d9becc38acbd/go.mod h1:4soZNh0zW0LtYGdQ416i0jO0EIqMGcbtaspRS4BDvRQ=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.12.0 h1:Iw5WCbBcaAAd0fpRb1c9r5YCylv4XDoCSigm1zLevwU=
|
||||
github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg=
|
||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||
github.com/onsi/ginkgo v1.14.0 h1:2mOpI4JVVPBN+WQRa0WKH2eXR+Ey+uK4n7Zj0aYpIQA=
|
||||
github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
|
||||
github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
|
||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.9.0 h1:R1uwffexN6Pr340GtYRIdZmAiN4J+iw6WG4wog1DUXg=
|
||||
github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA=
|
||||
github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.6.1/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.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.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/tidwall/assert v0.1.0 h1:aWcKyRBUAdLoVebxo95N7+YZVTFF/ASTr7BN4sLP6XI=
|
||||
github.com/tidwall/assert v0.1.0/go.mod h1:QLYtGyeqse53vuELQheYl9dngGCJQ+mTtlxcktb+Kj8=
|
||||
github.com/tidwall/btree v1.4.2 h1:PpkaieETJMUxYNADsjgtNRcERX7mGc/GP2zp/r5FM3g=
|
||||
@@ -52,58 +147,84 @@ github.com/tidwall/btree v1.4.2/go.mod h1:LGm8L/DZjPLmeWGjv5kFrY8dL4uVhMmzmmLYms
|
||||
github.com/tidwall/buntdb v1.3.2 h1:qd+IpdEGs0pZci37G4jF51+fSKlkuUTMXuHhXL1AkKg=
|
||||
github.com/tidwall/buntdb v1.3.2/go.mod h1:lZZrZUWzlyDJKlLQ6DKAy53LnG7m5kHyrEHvvcDmBpU=
|
||||
github.com/tidwall/gjson v1.12.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.14.3 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw=
|
||||
github.com/tidwall/gjson v1.14.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/grect v0.1.4 h1:dA3oIgNgWdSspFzn1kS4S/RDpZFLrIxAZOdJKjYapOg=
|
||||
github.com/tidwall/grect v0.1.4/go.mod h1:9FBsaYRaR0Tcy4UwefBX/UDcDcDy9V5jUcxHzv2jd5Q=
|
||||
github.com/tidwall/lotsa v1.0.2 h1:dNVBH5MErdaQ/xd9s769R31/n2dXavsQ0Yf4TMEHHw8=
|
||||
github.com/tidwall/lotsa v1.0.2/go.mod h1:X6NiU+4yHA3fE3Puvpnn1XMDrFZrE9JO2/w+UMuqgR8=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||
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/tidwall/rtred v0.1.2 h1:exmoQtOLvDoO8ud++6LwVsAMTu0KPzLTUrMln8u1yu8=
|
||||
github.com/tidwall/rtred v0.1.2/go.mod h1:hd69WNXQ5RP9vHd7dqekAz+RIdtfBogmglkZSRxCHFQ=
|
||||
github.com/tidwall/tinyqueue v0.1.1 h1:SpNEvEggbpyN5DIReaJ2/1ndroY8iyEGxPYxoSaymYE=
|
||||
github.com/tidwall/tinyqueue v0.1.1/go.mod h1:O/QNHwrnjqr6IHItYrzoHAKYhBkLI67Q096fQP5zMYw=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
|
||||
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||
github.com/xdg-go/stringprep v1.0.2 h1:6iq84/ryjjeRmMJwxutI51F2GIPlP5BfTvXHeYjyhBc=
|
||||
github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM=
|
||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||
golang.org/x/arch v0.15.0 h1:QtOrQd0bTUnhNVNndMpLHNWrDmYzZ2KDqSrEymqInZw=
|
||||
golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
|
||||
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
||||
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
|
||||
golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
|
||||
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
|
||||
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
||||
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
|
||||
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/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 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
|
||||
584
irc/accounts.go
584
irc/accounts.go
@@ -24,6 +24,7 @@ import (
|
||||
"github.com/ergochat/ergo/irc/email"
|
||||
"github.com/ergochat/ergo/irc/migrations"
|
||||
"github.com/ergochat/ergo/irc/modes"
|
||||
"github.com/ergochat/ergo/irc/nostr"
|
||||
"github.com/ergochat/ergo/irc/oauth2"
|
||||
"github.com/ergochat/ergo/irc/passwd"
|
||||
"github.com/ergochat/ergo/irc/utils"
|
||||
@@ -48,6 +49,7 @@ const (
|
||||
keyAccountSuspended = "account.suspended %s" // client realname stored as string
|
||||
keyAccountPwReset = "account.pwreset %s"
|
||||
keyAccountEmailChange = "account.emailchange %s"
|
||||
keyAccountNostrIdentifier = "account.nostridentifier %s" // stores the nostr identifier used during registration
|
||||
// for an always-on client, a map of channel names they're in to their current modes
|
||||
// (not to be confused with their amodes, which a non-always-on client can have):
|
||||
keyAccountChannelToModes = "account.channeltomodes %s"
|
||||
@@ -400,7 +402,6 @@ func (am *AccountManager) Register(client *Client, account string, callbackNames
|
||||
credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount)
|
||||
verificationCodeKey := fmt.Sprintf(keyAccountVerificationCode, casefoldedAccount)
|
||||
settingsKey := fmt.Sprintf(keyAccountSettings, casefoldedAccount)
|
||||
certFPKey := fmt.Sprintf(keyCertToAccount, certfp)
|
||||
|
||||
var creds AccountCredentials
|
||||
creds.Version = 1
|
||||
@@ -446,13 +447,13 @@ func (am *AccountManager) Register(client *Client, account string, callbackNames
|
||||
}
|
||||
|
||||
_, err = am.loadRawAccount(tx, casefoldedAccount)
|
||||
if err != errAccountDoesNotExist {
|
||||
return errAccountAlreadyRegistered
|
||||
if err != nil && err != errAccountDoesNotExist {
|
||||
return err
|
||||
}
|
||||
|
||||
if certfp != "" {
|
||||
// make sure certfp doesn't already exist because that'd be silly
|
||||
_, err := tx.Get(certFPKey)
|
||||
_, err := tx.Get(fmt.Sprintf(keyCertToAccount, certfp))
|
||||
if err != buntdb.ErrNotFound {
|
||||
return errCertfpAlreadyExists
|
||||
}
|
||||
@@ -463,9 +464,6 @@ func (am *AccountManager) Register(client *Client, account string, callbackNames
|
||||
tx.Set(registeredTimeKey, registeredTimeStr, setOptions)
|
||||
tx.Set(credentialsKey, credStr, setOptions)
|
||||
tx.Set(settingsKey, settingsStr, setOptions)
|
||||
if certfp != "" {
|
||||
tx.Set(certFPKey, casefoldedAccount, setOptions)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}()
|
||||
@@ -751,6 +749,26 @@ func (am *AccountManager) loadPushSubscriptions(account string) (result []stored
|
||||
}
|
||||
}
|
||||
|
||||
func (am *AccountManager) saveNostrIdentifier(account string, nostrIdentifier string) {
|
||||
key := fmt.Sprintf(keyAccountNostrIdentifier, account)
|
||||
am.server.logger.Info("nostr-hostname", "Saving nostr identifier:", nostrIdentifier, "for account:", account, "with key:", key)
|
||||
am.server.store.Update(func(tx *buntdb.Tx) error {
|
||||
tx.Set(key, nostrIdentifier, nil)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (am *AccountManager) loadNostrIdentifier(account string) (nostrIdentifier string) {
|
||||
key := fmt.Sprintf(keyAccountNostrIdentifier, account)
|
||||
am.server.logger.Info("nostr-hostname", "Loading nostr identifier for account:", account, "with key:", key)
|
||||
am.server.store.View(func(tx *buntdb.Tx) error {
|
||||
nostrIdentifier, _ = tx.Get(key)
|
||||
return nil
|
||||
})
|
||||
am.server.logger.Info("nostr-hostname", "Loaded nostr identifier:", nostrIdentifier, "for account:", account)
|
||||
return
|
||||
}
|
||||
|
||||
func (am *AccountManager) addRemoveCertfp(account, certfp string, add bool, hasPrivs bool) (err error) {
|
||||
certfp, err = utils.NormalizeCertfp(certfp)
|
||||
if err != nil {
|
||||
@@ -801,20 +819,18 @@ func (am *AccountManager) addRemoveCertfp(account, certfp string, add bool, hasP
|
||||
return err
|
||||
}
|
||||
|
||||
certfpKey := fmt.Sprintf(keyCertToAccount, certfp)
|
||||
err = am.server.store.Update(func(tx *buntdb.Tx) error {
|
||||
curCredStr, err := tx.Get(credKey)
|
||||
if credStr != curCredStr {
|
||||
return errCASFailed
|
||||
}
|
||||
if add {
|
||||
_, err = tx.Get(certfpKey)
|
||||
_, err = tx.Get(fmt.Sprintf(keyCertToAccount, certfp))
|
||||
if err != buntdb.ErrNotFound {
|
||||
return errCertfpAlreadyExists
|
||||
}
|
||||
tx.Set(certfpKey, cfAccount, nil)
|
||||
} else {
|
||||
tx.Delete(certfpKey)
|
||||
tx.Delete(fmt.Sprintf(keyCertToAccount, certfp))
|
||||
}
|
||||
_, _, err = tx.Set(credKey, newCredStr, nil)
|
||||
return err
|
||||
@@ -828,6 +844,8 @@ func (am *AccountManager) dispatchCallback(client *Client, account string, callb
|
||||
return "", nil
|
||||
} else if callbackNamespace == "mailto" {
|
||||
return am.dispatchMailtoCallback(client, account, callbackValue)
|
||||
} else if callbackNamespace == "nostr" {
|
||||
return am.dispatchNostrCallback(client, account, callbackValue)
|
||||
} else {
|
||||
return "", fmt.Errorf("Callback not implemented: %s", callbackNamespace)
|
||||
}
|
||||
@@ -839,7 +857,7 @@ func (am *AccountManager) dispatchMailtoCallback(client *Client, account string,
|
||||
|
||||
subject := config.VerifyMessageSubject
|
||||
if subject == "" {
|
||||
subject = fmt.Sprintf(client.t("Verify your account on %s"), am.server.name)
|
||||
subject = fmt.Sprintf(client.t("Verify your account on %s"), am.server.Config().Server.Name)
|
||||
}
|
||||
|
||||
message := email.ComposeMail(config, callbackValue, subject)
|
||||
@@ -859,6 +877,35 @@ func (am *AccountManager) dispatchMailtoCallback(client *Client, account string,
|
||||
return
|
||||
}
|
||||
|
||||
func (am *AccountManager) dispatchNostrCallback(_ *Client, account string, callbackValue string) (code string, err error) {
|
||||
config := am.server.Config().Accounts.Registration.NostrVerification
|
||||
if !config.Enabled {
|
||||
return "", fmt.Errorf("Nostr verification is not enabled")
|
||||
}
|
||||
|
||||
code = utils.GenerateSecretToken()
|
||||
|
||||
// Create DM config from server config
|
||||
dmConfig := nostr.DMConfig{
|
||||
PrivateKey: config.PrivateKey,
|
||||
DefaultRelays: config.DefaultRelays,
|
||||
Timeout: time.Duration(config.Timeout),
|
||||
UserAgent: fmt.Sprintf("Ergo IRC Server %s", am.server.Config().Server.Name),
|
||||
}
|
||||
|
||||
// Send the verification DM
|
||||
err = nostr.SendVerificationDM(callbackValue, account, code, am.server.Config().Server.Name, dmConfig)
|
||||
if err != nil {
|
||||
am.server.logger.Error("internal", "Failed to dispatch nostr DM to", callbackValue, err.Error())
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Save the nostr identifier for hostname generation
|
||||
am.saveNostrIdentifier(account, callbackValue)
|
||||
|
||||
return code, nil
|
||||
}
|
||||
|
||||
func (am *AccountManager) Verify(client *Client, account string, code string, admin bool) error {
|
||||
casefoldedAccount, err := CasefoldName(account)
|
||||
var skeleton string
|
||||
@@ -949,15 +996,6 @@ func (am *AccountManager) Verify(client *Client, account string, code string, ad
|
||||
tx.Set(credentialsKey, raw.Credentials, nil)
|
||||
tx.Set(settingsKey, raw.Settings, nil)
|
||||
|
||||
var creds AccountCredentials
|
||||
// XXX we shouldn't do (de)serialization inside the txn,
|
||||
// but this is like 2 usec on my system
|
||||
json.Unmarshal([]byte(raw.Credentials), &creds)
|
||||
for _, cert := range creds.Certfps {
|
||||
certFPKey := fmt.Sprintf(keyCertToAccount, cert)
|
||||
tx.Set(certFPKey, casefoldedAccount, nil)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -993,7 +1031,7 @@ func (am *AccountManager) Verify(client *Client, account string, code string, ad
|
||||
_, method := am.EnforcementStatus(casefoldedAccount, skeleton)
|
||||
if method == NickEnforcementStrict {
|
||||
currentClient := am.server.clients.Get(casefoldedAccount)
|
||||
if currentClient != nil && currentClient != client && currentClient.Account() != casefoldedAccount {
|
||||
if currentClient != nil && currentClient.AlwaysOn() {
|
||||
am.server.RandomlyRename(currentClient)
|
||||
}
|
||||
}
|
||||
@@ -1038,6 +1076,7 @@ func (am *AccountManager) NsSetEmail(client *Client, emailAddr string) (err erro
|
||||
recordKey := fmt.Sprintf(keyAccountEmailChange, casefoldedAccount)
|
||||
recordBytes, _ := json.Marshal(record)
|
||||
recordVal := string(recordBytes)
|
||||
|
||||
am.server.store.Update(func(tx *buntdb.Tx) error {
|
||||
tx.Set(recordKey, recordVal, nil)
|
||||
return nil
|
||||
@@ -1049,8 +1088,8 @@ func (am *AccountManager) NsSetEmail(client *Client, emailAddr string) (err erro
|
||||
|
||||
message := email.ComposeMail(config.Accounts.Registration.EmailVerification,
|
||||
emailAddr,
|
||||
fmt.Sprintf(client.t("Verify your change of e-mail address on %s"), am.server.name))
|
||||
message.WriteString(fmt.Sprintf(client.t("To confirm your change of e-mail address on %s, issue the following command:"), am.server.name))
|
||||
fmt.Sprintf(client.t("Verify your change of e-mail address on %s"), am.server.Config().Server.Name))
|
||||
message.WriteString(fmt.Sprintf(client.t("To confirm your change of e-mail address on %s, issue the following command:"), am.server.Config().Server.Name))
|
||||
message.WriteString("\r\n")
|
||||
fmt.Fprintf(&message, "/MSG NickServ VERIFYEMAIL %s\r\n", record.Code)
|
||||
|
||||
@@ -1153,9 +1192,9 @@ func (am *AccountManager) NsSendpass(client *Client, accountName string) (err er
|
||||
return
|
||||
}
|
||||
|
||||
subject := fmt.Sprintf(client.t("Reset your password on %s"), am.server.name)
|
||||
subject := fmt.Sprintf(client.t("Reset your password on %s"), am.server.Config().Server.Name)
|
||||
message := email.ComposeMail(config.Accounts.Registration.EmailVerification, account.Settings.Email, subject)
|
||||
fmt.Fprintf(&message, client.t("We received a request to reset your password on %[1]s for account: %[2]s"), am.server.name, account.Name)
|
||||
fmt.Fprintf(&message, client.t("We received a request to reset your password on %[1]s for account: %[2]s"), am.server.Config().Server.Name, account.Name)
|
||||
message.WriteString("\r\n")
|
||||
message.WriteString(client.t("If you did not initiate this request, you can safely ignore this message."))
|
||||
message.WriteString("\r\n")
|
||||
@@ -1181,7 +1220,7 @@ func (am *AccountManager) NsResetpass(client *Client, accountName, code, passwor
|
||||
}
|
||||
account, err := am.LoadAccount(accountName)
|
||||
if err != nil {
|
||||
return
|
||||
return err
|
||||
}
|
||||
if !account.Verified {
|
||||
return errAccountUnverified
|
||||
@@ -1842,6 +1881,7 @@ func (am *AccountManager) Rename(oldName, newName string) (err error) {
|
||||
tx.Set(key, newName, nil)
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1880,6 +1920,7 @@ func (am *AccountManager) Unregister(account string, erase bool) error {
|
||||
pwResetKey := fmt.Sprintf(keyAccountPwReset, casefoldedAccount)
|
||||
emailChangeKey := fmt.Sprintf(keyAccountEmailChange, casefoldedAccount)
|
||||
pushSubscriptionsKey := fmt.Sprintf(keyAccountPushSubscriptions, casefoldedAccount)
|
||||
nostrIdentifierKey := fmt.Sprintf(keyAccountNostrIdentifier, casefoldedAccount)
|
||||
|
||||
var clients []*Client
|
||||
defer func() {
|
||||
@@ -1939,22 +1980,25 @@ func (am *AccountManager) Unregister(account string, erase bool) error {
|
||||
tx.Delete(pwResetKey)
|
||||
tx.Delete(emailChangeKey)
|
||||
tx.Delete(pushSubscriptionsKey)
|
||||
tx.Delete(nostrIdentifierKey)
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err == nil {
|
||||
var creds AccountCredentials
|
||||
if err := json.Unmarshal([]byte(credText), &creds); err == nil {
|
||||
for _, cert := range creds.Certfps {
|
||||
certFPKey := fmt.Sprintf(keyCertToAccount, cert)
|
||||
am.server.store.Update(func(tx *buntdb.Tx) error {
|
||||
if account, err := tx.Get(certFPKey); err == nil && account == casefoldedAccount {
|
||||
tx.Delete(certFPKey)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var creds AccountCredentials
|
||||
if err := json.Unmarshal([]byte(credText), &creds); err == nil {
|
||||
for _, cert := range creds.Certfps {
|
||||
certFPKey := fmt.Sprintf(keyCertToAccount, cert)
|
||||
am.server.store.Update(func(tx *buntdb.Tx) error {
|
||||
if account, err := tx.Get(certFPKey); err == nil && account == casefoldedAccount {
|
||||
tx.Delete(certFPKey)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1991,229 +2035,40 @@ func unmarshalRegisteredChannels(channelsStr string) (result []string) {
|
||||
return
|
||||
}
|
||||
|
||||
func (am *AccountManager) AuthenticateByCertificate(client *Client, certfp string, peerCerts []*x509.Certificate, authzid string) (err error) {
|
||||
if certfp == "" {
|
||||
return errAccountInvalidCredentials
|
||||
}
|
||||
|
||||
var clientAccount ClientAccount
|
||||
|
||||
defer func() {
|
||||
if err != nil {
|
||||
return
|
||||
} else if !clientAccount.Verified {
|
||||
err = errAccountUnverified
|
||||
return
|
||||
} else if clientAccount.Suspended != nil {
|
||||
err = errAccountSuspended
|
||||
return
|
||||
}
|
||||
// TODO(#1109) clean this check up?
|
||||
if client.registered {
|
||||
if clientAlready := am.server.clients.Get(clientAccount.Name); clientAlready != nil && clientAlready.AlwaysOn() {
|
||||
err = errNickAccountMismatch
|
||||
return
|
||||
}
|
||||
}
|
||||
am.Login(client, clientAccount)
|
||||
return
|
||||
}()
|
||||
|
||||
config := am.server.Config()
|
||||
if config.Accounts.AuthScript.Enabled {
|
||||
var output AuthScriptOutput
|
||||
output, err = CheckAuthScript(am.server.semaphores.AuthScript, config.Accounts.AuthScript.ScriptConfig,
|
||||
AuthScriptInput{Certfp: certfp, IP: client.IP().String(), peerCerts: peerCerts})
|
||||
if err != nil {
|
||||
am.server.logger.Error("internal", "failed shell auth invocation", err.Error())
|
||||
} else if output.Success && output.AccountName != "" {
|
||||
clientAccount, err = am.loadWithAutocreation(output.AccountName, config.Accounts.AuthScript.Autocreate)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var account string
|
||||
certFPKey := fmt.Sprintf(keyCertToAccount, certfp)
|
||||
|
||||
err = am.server.store.View(func(tx *buntdb.Tx) error {
|
||||
account, _ = tx.Get(certFPKey)
|
||||
if account == "" {
|
||||
return errAccountInvalidCredentials
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if authzid != "" {
|
||||
if cfAuthzid, err := CasefoldName(authzid); err != nil || cfAuthzid != account {
|
||||
return errAuthzidAuthcidMismatch
|
||||
}
|
||||
}
|
||||
|
||||
// ok, we found an account corresponding to their certificate
|
||||
clientAccount, err = am.LoadAccount(account)
|
||||
return err
|
||||
}
|
||||
|
||||
type settingsMunger func(input AccountSettings) (output AccountSettings, err error)
|
||||
|
||||
func (am *AccountManager) ModifyAccountSettings(account string, munger settingsMunger) (newSettings AccountSettings, err error) {
|
||||
casefoldedAccount, err := CasefoldName(account)
|
||||
if err != nil {
|
||||
return newSettings, errAccountDoesNotExist
|
||||
}
|
||||
// TODO implement this in general via a compare-and-swap API
|
||||
accountData, err := am.LoadAccount(casefoldedAccount)
|
||||
if err != nil {
|
||||
return
|
||||
} else if !accountData.Verified {
|
||||
return newSettings, errAccountUnverified
|
||||
}
|
||||
newSettings, err = munger(accountData.Settings)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
text, err := json.Marshal(newSettings)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
key := fmt.Sprintf(keyAccountSettings, casefoldedAccount)
|
||||
serializedValue := string(text)
|
||||
err = am.server.store.Update(func(tx *buntdb.Tx) (err error) {
|
||||
_, _, err = tx.Set(key, serializedValue, nil)
|
||||
return
|
||||
})
|
||||
if err != nil {
|
||||
err = errAccountUpdateFailed
|
||||
return
|
||||
}
|
||||
// success, push new settings into the client objects
|
||||
am.Lock()
|
||||
defer am.Unlock()
|
||||
for _, client := range am.accountToClients[casefoldedAccount] {
|
||||
client.SetAccountSettings(newSettings)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// represents someone's status in hostserv
|
||||
type VHostInfo struct {
|
||||
ApprovedVHost string
|
||||
Enabled bool
|
||||
}
|
||||
|
||||
// callback type implementing the actual business logic of vhost operations
|
||||
type vhostMunger func(input VHostInfo) (output VHostInfo, err error)
|
||||
|
||||
func (am *AccountManager) VHostSet(account string, vhost string) (result VHostInfo, err error) {
|
||||
munger := func(input VHostInfo) (output VHostInfo, err error) {
|
||||
output = input
|
||||
output.Enabled = true
|
||||
output.ApprovedVHost = vhost
|
||||
return
|
||||
}
|
||||
|
||||
return am.performVHostChange(account, munger)
|
||||
}
|
||||
|
||||
func (am *AccountManager) VHostSetEnabled(client *Client, enabled bool) (result VHostInfo, err error) {
|
||||
munger := func(input VHostInfo) (output VHostInfo, err error) {
|
||||
if input.ApprovedVHost == "" {
|
||||
err = errNoVhost
|
||||
return
|
||||
}
|
||||
output = input
|
||||
output.Enabled = enabled
|
||||
return
|
||||
}
|
||||
|
||||
return am.performVHostChange(client.Account(), munger)
|
||||
}
|
||||
|
||||
func (am *AccountManager) performVHostChange(account string, munger vhostMunger) (result VHostInfo, err error) {
|
||||
account, err = CasefoldName(account)
|
||||
if err != nil || account == "" {
|
||||
err = errAccountDoesNotExist
|
||||
return
|
||||
}
|
||||
|
||||
if am.server.Defcon() <= 3 {
|
||||
err = errFeatureDisabled
|
||||
return
|
||||
}
|
||||
|
||||
clientAccount, err := am.LoadAccount(account)
|
||||
if err != nil {
|
||||
err = errAccountDoesNotExist
|
||||
return
|
||||
} else if !clientAccount.Verified {
|
||||
err = errAccountUnverified
|
||||
return
|
||||
}
|
||||
|
||||
result, err = munger(clientAccount.VHost)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
vhtext, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
err = errAccountUpdateFailed
|
||||
return
|
||||
}
|
||||
vhstr := string(vhtext)
|
||||
|
||||
key := fmt.Sprintf(keyAccountVHost, account)
|
||||
err = am.server.store.Update(func(tx *buntdb.Tx) error {
|
||||
_, _, err := tx.Set(key, vhstr, nil)
|
||||
return err
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
err = errAccountUpdateFailed
|
||||
return
|
||||
}
|
||||
|
||||
am.applyVhostToClients(account, result)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (am *AccountManager) applyVHostInfo(client *Client, info VHostInfo) {
|
||||
// if hostserv is disabled in config, then don't grant vhosts
|
||||
// that were previously approved while it was enabled
|
||||
if !am.server.Config().Accounts.VHosts.Enabled {
|
||||
return
|
||||
}
|
||||
func (am *AccountManager) Login(client *Client, account ClientAccount) {
|
||||
client.Login(account)
|
||||
|
||||
vhost := ""
|
||||
if info.Enabled {
|
||||
vhost = info.ApprovedVHost
|
||||
if account.VHost.Enabled {
|
||||
vhost = account.VHost.ApprovedVHost
|
||||
}
|
||||
oldNickmask := client.NickMaskString()
|
||||
updated := client.SetVHost(vhost)
|
||||
|
||||
// Set nostr hostname if enabled and no vhost is set
|
||||
config := am.server.Config()
|
||||
if vhost == "" && config.Server.Cloaks.EnabledForAlwaysOn {
|
||||
am.server.logger.Info("nostr-hostname", "Login - checking nostr hostname for account:", account.Name)
|
||||
var cloakedHostname string
|
||||
if config.Server.Cloaks.NostrHostnames {
|
||||
am.server.logger.Info("nostr-hostname", "NostrHostnames enabled, computing nostr hostname")
|
||||
cloakedHostname = am.ComputeNostrHostname(account.Name)
|
||||
} else {
|
||||
am.server.logger.Info("nostr-hostname", "NostrHostnames disabled in config")
|
||||
}
|
||||
if cloakedHostname == "" {
|
||||
am.server.logger.Info("nostr-hostname", "No nostr hostname, using regular account cloak")
|
||||
cloakedHostname = config.Server.Cloaks.ComputeAccountCloak(account.Name)
|
||||
}
|
||||
am.server.logger.Info("nostr-hostname", "Setting cloaked hostname:", cloakedHostname, "for account:", account.Name)
|
||||
client.setCloakedHostname(cloakedHostname)
|
||||
updated = true
|
||||
}
|
||||
|
||||
if updated && client.Registered() {
|
||||
// TODO: doing I/O here is kind of a kludge
|
||||
client.sendChghost(oldNickmask, client.Hostname())
|
||||
}
|
||||
}
|
||||
|
||||
func (am *AccountManager) applyVhostToClients(account string, result VHostInfo) {
|
||||
am.RLock()
|
||||
clients := am.accountToClients[account]
|
||||
am.RUnlock()
|
||||
|
||||
for _, client := range clients {
|
||||
am.applyVHostInfo(client, result)
|
||||
}
|
||||
}
|
||||
|
||||
func (am *AccountManager) Login(client *Client, account ClientAccount) {
|
||||
client.Login(account)
|
||||
|
||||
am.applyVHostInfo(client, account.VHost)
|
||||
|
||||
casefoldedAccount := client.Account()
|
||||
am.Lock()
|
||||
@@ -2461,6 +2316,12 @@ type ClientAccount struct {
|
||||
Settings AccountSettings
|
||||
}
|
||||
|
||||
// represents someone's status in hostserv
|
||||
type VHostInfo struct {
|
||||
ApprovedVHost string
|
||||
Enabled bool
|
||||
}
|
||||
|
||||
// convenience for passing around raw serialized account data
|
||||
type rawClientAccount struct {
|
||||
Name string
|
||||
@@ -2472,3 +2333,220 @@ type rawClientAccount struct {
|
||||
Settings string
|
||||
Suspended string
|
||||
}
|
||||
|
||||
// ComputeNostrHostname generates a nostr-based hostname for an account if available
|
||||
func (am *AccountManager) ComputeNostrHostname(accountName string) string {
|
||||
config := am.server.Config()
|
||||
am.server.logger.Info("nostr-hostname", "ComputeNostrHostname called for account:", accountName)
|
||||
|
||||
if !config.Server.Cloaks.NostrHostnames {
|
||||
am.server.logger.Info("nostr-hostname", "NostrHostnames disabled in config")
|
||||
return ""
|
||||
}
|
||||
|
||||
nostrIdentifier := am.loadNostrIdentifier(accountName)
|
||||
am.server.logger.Info("nostr-hostname", "Loaded nostr identifier:", nostrIdentifier, "for account:", accountName)
|
||||
|
||||
if nostrIdentifier == "" {
|
||||
am.server.logger.Info("nostr-hostname", "No nostr identifier found for account:", accountName)
|
||||
return ""
|
||||
}
|
||||
|
||||
hostname := config.Server.Cloaks.ComputeNostrHostname(nostrIdentifier)
|
||||
am.server.logger.Info("nostr-hostname", "Generated hostname:", hostname, "from identifier:", nostrIdentifier)
|
||||
return hostname
|
||||
}
|
||||
|
||||
func (am *AccountManager) AuthenticateByCertificate(client *Client, certfp string, peerCerts []*x509.Certificate, authzid string) (err error) {
|
||||
if certfp == "" {
|
||||
return errAccountInvalidCredentials
|
||||
}
|
||||
|
||||
var clientAccount ClientAccount
|
||||
|
||||
defer func() {
|
||||
if err == nil {
|
||||
am.Login(client, clientAccount)
|
||||
}
|
||||
}()
|
||||
|
||||
config := am.server.Config()
|
||||
if config.Accounts.AuthScript.Enabled {
|
||||
var output AuthScriptOutput
|
||||
output, err = CheckAuthScript(am.server.semaphores.AuthScript, config.Accounts.AuthScript.ScriptConfig,
|
||||
AuthScriptInput{Certfp: certfp, IP: client.IP().String(), peerCerts: peerCerts})
|
||||
if err != nil {
|
||||
am.server.logger.Error("internal", "failed shell auth invocation", err.Error())
|
||||
} else if output.Success && output.AccountName != "" {
|
||||
clientAccount, err = am.loadWithAutocreation(output.AccountName, config.Accounts.AuthScript.Autocreate)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var account string
|
||||
certFPKey := fmt.Sprintf(keyCertToAccount, certfp)
|
||||
|
||||
err = am.server.store.View(func(tx *buntdb.Tx) error {
|
||||
account, _ = tx.Get(certFPKey)
|
||||
if account == "" {
|
||||
return errAccountInvalidCredentials
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if authzid != "" {
|
||||
if cfAuthzid, err := CasefoldName(authzid); err != nil || cfAuthzid != account {
|
||||
return errAuthzidAuthcidMismatch
|
||||
}
|
||||
}
|
||||
|
||||
// ok, we found an account corresponding to their certificate
|
||||
clientAccount, err = am.LoadAccount(account)
|
||||
return err
|
||||
}
|
||||
|
||||
type settingsMunger func(input AccountSettings) (output AccountSettings, err error)
|
||||
|
||||
func (am *AccountManager) ModifyAccountSettings(account string, munger settingsMunger) (newSettings AccountSettings, err error) {
|
||||
casefoldedAccount, err := CasefoldName(account)
|
||||
if err != nil {
|
||||
return newSettings, errAccountDoesNotExist
|
||||
}
|
||||
// TODO implement this in general via a compare-and-swap API
|
||||
accountData, err := am.LoadAccount(casefoldedAccount)
|
||||
if err != nil {
|
||||
return
|
||||
} else if !accountData.Verified {
|
||||
return newSettings, errAccountUnverified
|
||||
}
|
||||
newSettings, err = munger(accountData.Settings)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
text, err := json.Marshal(newSettings)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
key := fmt.Sprintf(keyAccountSettings, casefoldedAccount)
|
||||
serializedValue := string(text)
|
||||
err = am.server.store.Update(func(tx *buntdb.Tx) (err error) {
|
||||
_, _, err = tx.Set(key, serializedValue, nil)
|
||||
return
|
||||
})
|
||||
if err != nil {
|
||||
err = errAccountUpdateFailed
|
||||
return
|
||||
}
|
||||
// success, push new settings into the client objects
|
||||
am.Lock()
|
||||
defer am.Unlock()
|
||||
for _, client := range am.accountToClients[casefoldedAccount] {
|
||||
client.SetAccountSettings(newSettings)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// callback type implementing the actual business logic of vhost operations
|
||||
type vhostMunger func(input VHostInfo) (output VHostInfo, err error)
|
||||
|
||||
func (am *AccountManager) VHostSet(account string, vhost string) (result VHostInfo, err error) {
|
||||
munger := func(input VHostInfo) (output VHostInfo, err error) {
|
||||
output = input
|
||||
output.Enabled = true
|
||||
output.ApprovedVHost = vhost
|
||||
return
|
||||
}
|
||||
|
||||
return am.performVHostChange(account, munger)
|
||||
}
|
||||
|
||||
func (am *AccountManager) VHostSetEnabled(client *Client, enabled bool) (result VHostInfo, err error) {
|
||||
munger := func(input VHostInfo) (output VHostInfo, err error) {
|
||||
if input.ApprovedVHost == "" {
|
||||
err = errNoVhost
|
||||
return
|
||||
}
|
||||
output = input
|
||||
output.Enabled = enabled
|
||||
return
|
||||
}
|
||||
|
||||
return am.performVHostChange(client.Account(), munger)
|
||||
}
|
||||
|
||||
func (am *AccountManager) performVHostChange(account string, munger vhostMunger) (result VHostInfo, err error) {
|
||||
account, err = CasefoldName(account)
|
||||
if err != nil || account == "" {
|
||||
err = errAccountDoesNotExist
|
||||
return
|
||||
}
|
||||
|
||||
if am.server.Defcon() <= 3 {
|
||||
err = errFeatureDisabled
|
||||
return
|
||||
}
|
||||
|
||||
clientAccount, err := am.LoadAccount(account)
|
||||
if err != nil {
|
||||
err = errAccountDoesNotExist
|
||||
return
|
||||
} else if !clientAccount.Verified {
|
||||
err = errAccountUnverified
|
||||
return
|
||||
}
|
||||
|
||||
result, err = munger(clientAccount.VHost)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
vhtext, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
err = errAccountUpdateFailed
|
||||
return
|
||||
}
|
||||
vhstr := string(vhtext)
|
||||
|
||||
key := fmt.Sprintf(keyAccountVHost, account)
|
||||
err = am.server.store.Update(func(tx *buntdb.Tx) error {
|
||||
_, _, err = tx.Set(key, vhstr, nil)
|
||||
return err
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
err = errAccountUpdateFailed
|
||||
return
|
||||
}
|
||||
|
||||
am.applyVhostToClients(account, result)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (am *AccountManager) applyVhostToClients(account string, result VHostInfo) {
|
||||
// if hostserv is disabled in config, then don't grant vhosts
|
||||
// that were previously approved while it was enabled
|
||||
if !am.server.Config().Accounts.VHosts.Enabled {
|
||||
return
|
||||
}
|
||||
|
||||
vhost := ""
|
||||
if result.Enabled {
|
||||
vhost = result.ApprovedVHost
|
||||
}
|
||||
am.RLock()
|
||||
clients := am.accountToClients[account]
|
||||
am.RUnlock()
|
||||
|
||||
for _, client := range clients {
|
||||
oldNickmask := client.NickMaskString()
|
||||
updated := client.SetVHost(vhost)
|
||||
if updated && client.Registered() {
|
||||
// TODO: doing I/O here is kind of a kludge
|
||||
client.sendChghost(oldNickmask, client.Hostname())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -436,7 +436,19 @@ func (server *Server) AddAlwaysOnClient(account ClientAccount, channelToStatus m
|
||||
|
||||
rawHostname, cloakedHostname := server.name, ""
|
||||
if config.Server.Cloaks.EnabledForAlwaysOn {
|
||||
cloakedHostname = config.Server.Cloaks.ComputeAccountCloak(account.Name)
|
||||
// Try nostr hostname first, fallback to regular account cloak
|
||||
server.logger.Info("nostr-hostname", "AddAlwaysOnClient - checking nostr hostname for account:", account.Name)
|
||||
if config.Server.Cloaks.NostrHostnames {
|
||||
server.logger.Info("nostr-hostname", "NostrHostnames enabled, computing nostr hostname")
|
||||
cloakedHostname = server.accounts.ComputeNostrHostname(account.Name)
|
||||
} else {
|
||||
server.logger.Info("nostr-hostname", "NostrHostnames disabled in config")
|
||||
}
|
||||
if cloakedHostname == "" {
|
||||
server.logger.Info("nostr-hostname", "No nostr hostname, using regular account cloak")
|
||||
cloakedHostname = config.Server.Cloaks.ComputeAccountCloak(account.Name)
|
||||
}
|
||||
server.logger.Info("nostr-hostname", "Setting cloaked hostname:", cloakedHostname, "for always-on account:", account.Name)
|
||||
}
|
||||
|
||||
username := "~u"
|
||||
|
||||
@@ -5,9 +5,11 @@ package cloaks
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"crypto/sha3"
|
||||
|
||||
"github.com/ergochat/ergo/irc/nostr"
|
||||
"github.com/ergochat/ergo/irc/utils"
|
||||
)
|
||||
|
||||
@@ -19,6 +21,7 @@ type CloakConfig struct {
|
||||
CidrLenIPv6 int `yaml:"cidr-len-ipv6"`
|
||||
NumBits int `yaml:"num-bits"`
|
||||
LegacySecretValue string `yaml:"secret"`
|
||||
NostrHostnames bool `yaml:"nostr-hostnames"` // enable nostr-based hostnames for accounts registered with nostr
|
||||
|
||||
secret string
|
||||
numBytes int
|
||||
@@ -93,3 +96,54 @@ func (config *CloakConfig) ComputeAccountCloak(accountName string) string {
|
||||
copy(paddedAccountName[16:], accountName[:])
|
||||
return config.macAndCompose(paddedAccountName)
|
||||
}
|
||||
|
||||
// ComputeNostrHostname generates a readable hostname from a nostr identifier
|
||||
func (config *CloakConfig) ComputeNostrHostname(nostrIdentifier string) string {
|
||||
fmt.Printf("DEBUG: ComputeNostrHostname called with identifier: '%s'\n", nostrIdentifier)
|
||||
|
||||
if nostrIdentifier == "" {
|
||||
fmt.Printf("DEBUG: Empty nostr identifier, returning netname: %s\n", config.Netname)
|
||||
return config.Netname
|
||||
}
|
||||
|
||||
// Handle NIP-05 addresses (alice@example.com -> alice@example.com)
|
||||
if strings.Contains(nostrIdentifier, "@") {
|
||||
parts := strings.SplitN(nostrIdentifier, "@", 2)
|
||||
if len(parts) == 2 {
|
||||
// Return the full NIP-05 address as the hostname
|
||||
hostname := nostrIdentifier
|
||||
fmt.Printf("DEBUG: NIP-05 hostname generated: %s\n", hostname)
|
||||
return hostname
|
||||
}
|
||||
}
|
||||
|
||||
// Handle npub format (npub1abc123... -> npub1abc123....nostr)
|
||||
if strings.HasPrefix(nostrIdentifier, "npub1") {
|
||||
// Use the full npub as hostname
|
||||
hostname := fmt.Sprintf("%s.nostr", nostrIdentifier)
|
||||
fmt.Printf("DEBUG: npub hostname generated: %s\n", hostname)
|
||||
return hostname
|
||||
}
|
||||
|
||||
// Handle hex pubkey (3bf0c63f... -> convert to npub and use full npub.nostr)
|
||||
if len(nostrIdentifier) == 64 {
|
||||
// Convert hex pubkey to npub format
|
||||
npub, err := nostr.HexToNpub(nostrIdentifier)
|
||||
if err != nil {
|
||||
fmt.Printf("DEBUG: Failed to convert hex to npub: %v, using fallback\n", err)
|
||||
// Fallback to truncated hex if conversion fails
|
||||
truncated := nostrIdentifier[:8]
|
||||
hostname := fmt.Sprintf("%s.nostr", truncated)
|
||||
fmt.Printf("DEBUG: hex pubkey fallback hostname generated: %s\n", hostname)
|
||||
return hostname
|
||||
}
|
||||
hostname := fmt.Sprintf("%s.nostr", npub)
|
||||
fmt.Printf("DEBUG: hex pubkey converted to npub hostname: %s\n", hostname)
|
||||
return hostname
|
||||
}
|
||||
|
||||
// Fallback to regular account cloak
|
||||
fallback := config.ComputeAccountCloak(nostrIdentifier)
|
||||
fmt.Printf("DEBUG: Using fallback account cloak: %s\n", fallback)
|
||||
return fallback
|
||||
}
|
||||
|
||||
@@ -399,7 +399,7 @@ func init() {
|
||||
minParams: 1,
|
||||
},
|
||||
"WHOIS": {
|
||||
handler: whoisHandler,
|
||||
handler: whoHandler,
|
||||
minParams: 1,
|
||||
},
|
||||
"WHOWAS": {
|
||||
|
||||
@@ -369,6 +369,13 @@ type AccountRegistrationConfig struct {
|
||||
Throttling ThrottleConfig
|
||||
// new-style (v2.4 email verification config):
|
||||
EmailVerification email.MailtoConfig `yaml:"email-verification"`
|
||||
// nostr-based account verification, where we send a DM with verification code
|
||||
NostrVerification struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
PrivateKey string `yaml:"private-key"`
|
||||
DefaultRelays []string `yaml:"default-relays"`
|
||||
Timeout time.Duration `yaml:"timeout"`
|
||||
} `yaml:"nostr-verification"`
|
||||
// old-style email verification config, with "callbacks":
|
||||
LegacyEnabledCallbacks []string `yaml:"enabled-callbacks"`
|
||||
LegacyCallbacks struct {
|
||||
@@ -665,6 +672,14 @@ type Config struct {
|
||||
|
||||
Accounts AccountConfig
|
||||
|
||||
NostrVerification struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
PrivateKey string `yaml:"private-key"`
|
||||
DefaultRelays []string `yaml:"default-relays"`
|
||||
Timeout time.Duration `yaml:"timeout"`
|
||||
}
|
||||
|
||||
|
||||
Channels struct {
|
||||
DefaultModes *string `yaml:"default-modes"`
|
||||
defaultModes modes.Modes
|
||||
@@ -1974,3 +1989,14 @@ func normalizeCommandAliases(aliases map[string]string) (normalizedAliases map[s
|
||||
}
|
||||
return normalizedAliases, nil
|
||||
}
|
||||
|
||||
type CloakConfig struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
EnabledForAlwaysOn bool `yaml:"enabled-for-always-on"`
|
||||
Netname string `yaml:"netname"`
|
||||
CidrLenIPv4 int `yaml:"cidr-len-ipv4"`
|
||||
CidrLenIPv6 int `yaml:"cidr-len-ipv6"`
|
||||
NumBits int `yaml:"num-bits"`
|
||||
SecretValue string `yaml:"secret"`
|
||||
NostrHostnames bool `yaml:"nostr-hostnames"` // enable nostr-based hostnames for accounts registered with nostr
|
||||
}
|
||||
|
||||
131
irc/errors.go
131
irc/errors.go
@@ -15,69 +15,74 @@ import (
|
||||
|
||||
// Runtime Errors
|
||||
var (
|
||||
errAccountAlreadyRegistered = errors.New(`Account already exists`)
|
||||
errAccountAlreadyUnregistered = errors.New(`That account name was registered previously and can't be reused`)
|
||||
errAccountAlreadyVerified = errors.New(`Account is already verified`)
|
||||
errAccountCantDropPrimaryNick = errors.New("Can't unreserve primary nickname")
|
||||
errAccountCreation = errors.New("Account could not be created")
|
||||
errAccountDoesNotExist = errors.New("Account does not exist")
|
||||
errAccountInvalidCredentials = errors.New("Invalid account credentials")
|
||||
errAccountBadPassphrase = errors.New(`Passphrase contains forbidden characters or is otherwise invalid`)
|
||||
errAccountNickReservationFailed = errors.New("Could not (un)reserve nick")
|
||||
errAccountNotLoggedIn = errors.New("You're not logged into an account")
|
||||
errAccountAlreadyLoggedIn = errors.New("You're already logged into an account")
|
||||
errAccountTooManyNicks = errors.New("Account has too many reserved nicks")
|
||||
errAccountUnverified = errors.New(`Account is not yet verified`)
|
||||
errAccountSuspended = errors.New(`Account has been suspended`)
|
||||
errAccountVerificationFailed = errors.New("Account verification failed")
|
||||
errAccountVerificationInvalidCode = errors.New("Invalid account verification code")
|
||||
errAccountUpdateFailed = errors.New(`Error while updating your account information`)
|
||||
errAccountMustHoldNick = errors.New(`You must hold that nickname in order to register it`)
|
||||
errAuthRequired = errors.New("You must be logged into an account to do this")
|
||||
errAuthzidAuthcidMismatch = errors.New(`authcid and authzid must be the same`)
|
||||
errCertfpAlreadyExists = errors.New(`An account already exists for your certificate fingerprint`)
|
||||
errChannelNotOwnedByAccount = errors.New("Channel not owned by the specified account")
|
||||
errChannelTransferNotOffered = errors.New(`You weren't offered ownership of that channel`)
|
||||
errChannelAlreadyRegistered = errors.New("Channel is already registered")
|
||||
errChannelNotRegistered = errors.New("Channel is not registered")
|
||||
errChannelNameInUse = errors.New(`Channel name in use`)
|
||||
errInvalidChannelName = errors.New(`Invalid channel name`)
|
||||
errMonitorLimitExceeded = errors.New("Monitor limit exceeded")
|
||||
errNickMissing = errors.New("nick missing")
|
||||
errNicknameInvalid = errors.New("invalid nickname")
|
||||
errNicknameInUse = errors.New("nickname in use")
|
||||
errInsecureReattach = errors.New("insecure reattach")
|
||||
errNicknameReserved = errors.New("nickname is reserved")
|
||||
errNickAccountMismatch = errors.New(`Your nickname must match your account name; try logging out and logging back in with SASL`)
|
||||
errNoExistingBan = errors.New("Ban does not exist")
|
||||
errNoSuchChannel = errors.New(`No such channel`)
|
||||
errChannelPurged = errors.New(`This channel was purged by the server operators and cannot be used`)
|
||||
errChannelPurgedAlready = errors.New(`This channel was already purged and cannot be purged again`)
|
||||
errConfusableIdentifier = errors.New("This identifier is confusable with one already in use")
|
||||
errInsufficientPrivs = errors.New("Insufficient privileges")
|
||||
errInvalidUsername = errors.New("Invalid username")
|
||||
errFeatureDisabled = errors.New(`That feature is disabled`)
|
||||
errBanned = errors.New("IP or nickmask banned")
|
||||
errInvalidParams = utils.ErrInvalidParams
|
||||
errNoVhost = errors.New(`You do not have an approved vhost`)
|
||||
errLimitExceeded = errors.New("Limit exceeded")
|
||||
errNoop = errors.New("Action was a no-op")
|
||||
errCASFailed = errors.New("Compare-and-swap update of database value failed")
|
||||
errEmptyCredentials = errors.New("No more credentials are approved")
|
||||
errCredsExternallyManaged = errors.New("Credentials are externally managed and cannot be changed here")
|
||||
errNoSCRAMCredentials = errors.New("SCRAM credentials are not initialized for this account; consult the user guide")
|
||||
errInvalidMultilineBatch = errors.New("Invalid multiline batch")
|
||||
errTimedOut = errors.New("Operation timed out")
|
||||
errInvalidUtf8 = errors.New("Message rejected for invalid utf8")
|
||||
errClientDestroyed = errors.New("Client was already destroyed")
|
||||
errTooManyChannels = errors.New("You have joined too many channels")
|
||||
errWrongChannelKey = errors.New("Cannot join password-protected channel without the password")
|
||||
errInviteOnly = errors.New("Cannot join invite-only channel without an invite")
|
||||
errRegisteredOnly = errors.New("Cannot join registered-only channel without an account")
|
||||
errValidEmailRequired = errors.New("A valid email address is required for account registration")
|
||||
errInvalidAccountRename = errors.New("Account renames can only change the casefolding of the account name")
|
||||
errNameReserved = errors.New(`Name reserved due to a prior registration`)
|
||||
errInvalidBearerTokenType = errors.New("invalid bearer token type")
|
||||
errAccountAlreadyRegistered = errors.New("Account already exists")
|
||||
errAccountAlreadyUnregistered = errors.New("Account was already unregistered")
|
||||
errAccountAlreadyVerified = errors.New("Account is already verified")
|
||||
errAccountCantDropPrimaryNick = errors.New("Can't unreserve primary nickname")
|
||||
errAccountCreation = errors.New("Account could not be created")
|
||||
errAccountDoesNotExist = errors.New("Account does not exist")
|
||||
errAccountInvalidCredentials = errors.New("Invalid account credentials")
|
||||
errAccountBadPassphrase = errors.New("Passphrase contains forbidden characters or is otherwise invalid")
|
||||
errAccountNickReservationFailed = errors.New("Could not (un)reserve nick")
|
||||
errAccountNotLoggedIn = errors.New("You're not logged into an account")
|
||||
errAccountAlreadyLoggedIn = errors.New("You're already logged into an account")
|
||||
errAccountPasswordInvalid = errors.New("Password incorrect")
|
||||
errAccountTooManyNicks = errors.New("Account has too many reserved nicks")
|
||||
errAccountUnverified = errors.New("Account is not yet verified")
|
||||
errAccountSuspended = errors.New("Account has been suspended")
|
||||
errAccountVerificationFailed = errors.New("Account verification failed")
|
||||
errAccountVerificationInvalidCode = errors.New("Invalid verification code")
|
||||
errAccountUpdateFailed = errors.New("Error while updating your account information")
|
||||
errAccountMustHoldNick = errors.New("You must hold that nickname in order to register it")
|
||||
errAccountBadUnregisterCredentials = errors.New("Invalid credentials for unregistering account")
|
||||
errAccountBadSetting = errors.New("Invalid account setting")
|
||||
errAuthRequired = errors.New("You must be logged into an account to do this")
|
||||
errAuthzidAuthcidMismatch = errors.New("authcid and authzid must be the same")
|
||||
errCertfpAlreadyExists = errors.New("An account already exists for your certificate fingerprint")
|
||||
errChannelNotOwnedByAccount = errors.New("Channel not owned by the specified account")
|
||||
errChannelTransferNotOffered = errors.New("You weren't offered ownership of that channel")
|
||||
errChannelAlreadyRegistered = errors.New("Channel is already registered")
|
||||
errChannelNotRegistered = errors.New("Channel is not registered")
|
||||
errChannelNameInUse = errors.New("Channel name in use")
|
||||
errInvalidChannelName = errors.New("Invalid channel name")
|
||||
errMonitorLimitExceeded = errors.New("Monitor limit exceeded")
|
||||
errNickMissing = errors.New("nick missing")
|
||||
errNicknameInvalid = errors.New("invalid nickname")
|
||||
errNicknameInUse = errors.New("nickname in use")
|
||||
errInsecureReattach = errors.New("insecure reattach")
|
||||
errNicknameReserved = errors.New("nickname is reserved")
|
||||
errNickAccountMismatch = errors.New("Your nickname must match your account name; try logging out and logging back in with SASL")
|
||||
errNoExistingBan = errors.New("Ban does not exist")
|
||||
errNoSuchChannel = errors.New("No such channel")
|
||||
errChannelPurged = errors.New("This channel was purged by the server operators and cannot be used")
|
||||
errChannelPurgedAlready = errors.New("This channel was already purged and cannot be purged again")
|
||||
errConfusableIdentifier = errors.New("This identifier is confusable with one already in use")
|
||||
errInsufficientPrivs = errors.New("Insufficient privileges")
|
||||
errInvalidUsername = errors.New("Invalid username")
|
||||
errFeatureDisabled = errors.New("That feature is disabled")
|
||||
errBanned = errors.New("IP or nickmask banned")
|
||||
errInvalidParams = utils.ErrInvalidParams
|
||||
errNoVhost = errors.New("You do not have an approved vhost")
|
||||
errLimitExceeded = errors.New("Limit exceeded")
|
||||
errNoop = errors.New("Action was a no-op")
|
||||
errCASFailed = errors.New("Compare-and-swap update of database value failed")
|
||||
errEmptyCredentials = errors.New("No more credentials are approved")
|
||||
errCredsExternallyManaged = errors.New("Credentials are externally managed and cannot be changed here")
|
||||
errNoSCRAMCredentials = errors.New("SCRAM credentials are not initialized for this account; consult the user guide")
|
||||
errInvalidMultilineBatch = errors.New("Invalid multiline batch")
|
||||
errTimedOut = errors.New("Operation timed out")
|
||||
errInvalidUtf8 = errors.New("Message rejected for invalid utf8")
|
||||
errClientDestroyed = errors.New("Client was already destroyed")
|
||||
errTooManyChannels = errors.New("You have joined too many channels")
|
||||
errWrongChannelKey = errors.New("Cannot join password-protected channel without the password")
|
||||
errInviteOnly = errors.New("Cannot join invite-only channel without an invite")
|
||||
errRegisteredOnly = errors.New("Cannot join registered-only channel without an account")
|
||||
errValidEmailRequired = errors.New("A valid e-mail address is required")
|
||||
errValidNostrIdentifierRequired = errors.New("A valid nostr identifier is required")
|
||||
errUnsupportedCallbackNamespace = errors.New("Unsupported callback namespace")
|
||||
errInvalidAccountRename = errors.New("Account renames can only change the casefolding of the account name")
|
||||
errNameReserved = errors.New("Name reserved due to a prior registration")
|
||||
errInvalidBearerTokenType = errors.New("invalid bearer token type")
|
||||
)
|
||||
|
||||
// String Errors
|
||||
|
||||
167
irc/handlers.go
167
irc/handlers.go
@@ -32,6 +32,7 @@ import (
|
||||
"github.com/ergochat/ergo/irc/history"
|
||||
"github.com/ergochat/ergo/irc/jwt"
|
||||
"github.com/ergochat/ergo/irc/modes"
|
||||
"github.com/ergochat/ergo/irc/nostr"
|
||||
"github.com/ergochat/ergo/irc/oauth2"
|
||||
"github.com/ergochat/ergo/irc/sno"
|
||||
"github.com/ergochat/ergo/irc/utils"
|
||||
@@ -42,25 +43,42 @@ import (
|
||||
func parseCallback(spec string, config *Config) (callbackNamespace string, callbackValue string, err error) {
|
||||
// XXX if we don't require verification, ignore any callback that was passed here
|
||||
// (to avoid confusion in the case where the ircd has no mail server configured)
|
||||
if !config.Accounts.Registration.EmailVerification.Enabled {
|
||||
if !config.Accounts.Registration.EmailVerification.Enabled && !config.Accounts.Registration.NostrVerification.Enabled {
|
||||
callbackNamespace = "*"
|
||||
return
|
||||
}
|
||||
callback := strings.ToLower(spec)
|
||||
if colonIndex := strings.IndexByte(callback, ':'); colonIndex != -1 {
|
||||
callbackNamespace, callbackValue = callback[:colonIndex], callback[colonIndex+1:]
|
||||
if colonIndex := strings.IndexByte(spec, ':'); colonIndex != -1 {
|
||||
callbackNamespace, callbackValue = strings.ToLower(spec[:colonIndex]), spec[colonIndex+1:]
|
||||
} else {
|
||||
// "If a callback namespace is not ... provided, the IRC server MUST use mailto""
|
||||
callbackNamespace = "mailto"
|
||||
callbackValue = callback
|
||||
// Auto-detect callback type based on format
|
||||
if nostr.IsNostrIdentifier(spec) {
|
||||
callbackNamespace = "nostr"
|
||||
callbackValue = spec
|
||||
} else {
|
||||
// "If a callback namespace is not ... provided, the IRC server MUST use mailto""
|
||||
callbackNamespace = "mailto"
|
||||
callbackValue = strings.ToLower(spec)
|
||||
}
|
||||
}
|
||||
|
||||
if config.Accounts.Registration.EmailVerification.Enabled {
|
||||
if callbackNamespace != "mailto" {
|
||||
err = errValidEmailRequired
|
||||
} else if strings.IndexByte(callbackValue, '@') < 1 {
|
||||
err = errValidEmailRequired
|
||||
if callbackNamespace == "mailto" {
|
||||
if config.Accounts.Registration.EmailVerification.Enabled {
|
||||
if strings.IndexByte(callbackValue, '@') < 1 {
|
||||
err = errValidEmailRequired
|
||||
}
|
||||
} else {
|
||||
err = errUnsupportedCallbackNamespace
|
||||
}
|
||||
} else if callbackNamespace == "nostr" {
|
||||
if config.Accounts.Registration.NostrVerification.Enabled {
|
||||
if !nostr.IsValidNostrIdentifier(callbackValue) {
|
||||
err = errValidNostrIdentifierRequired
|
||||
}
|
||||
} else {
|
||||
err = errUnsupportedCallbackNamespace
|
||||
}
|
||||
} else if callbackNamespace != "admin" && callbackNamespace != "none" && callbackNamespace != "*" {
|
||||
err = errUnsupportedCallbackNamespace
|
||||
}
|
||||
|
||||
return
|
||||
@@ -131,7 +149,20 @@ func sendSuccessfulAccountAuth(service *ircService, client *Client, rb *Response
|
||||
if rb.session.isTor {
|
||||
config := client.server.Config()
|
||||
if config.Server.Cloaks.EnabledForAlwaysOn {
|
||||
cloakedHostname := config.Server.Cloaks.ComputeAccountCloak(details.accountName)
|
||||
// Try nostr hostname first, fallback to regular account cloak
|
||||
var cloakedHostname string
|
||||
client.server.logger.Info("nostr-hostname", "Authentication success - checking nostr hostname for account:", details.accountName)
|
||||
if config.Server.Cloaks.NostrHostnames {
|
||||
client.server.logger.Info("nostr-hostname", "NostrHostnames enabled, computing nostr hostname")
|
||||
cloakedHostname = client.server.accounts.ComputeNostrHostname(details.accountName)
|
||||
} else {
|
||||
client.server.logger.Info("nostr-hostname", "NostrHostnames disabled in config")
|
||||
}
|
||||
if cloakedHostname == "" {
|
||||
client.server.logger.Info("nostr-hostname", "No nostr hostname, using regular account cloak")
|
||||
cloakedHostname = config.Server.Cloaks.ComputeAccountCloak(details.accountName)
|
||||
}
|
||||
client.server.logger.Info("nostr-hostname", "Setting cloaked hostname:", cloakedHostname, "for account:", details.accountName)
|
||||
client.setCloakedHostname(cloakedHostname)
|
||||
if client.registered {
|
||||
client.sendChghost(details.nickMask, client.Hostname())
|
||||
@@ -216,7 +247,7 @@ func authenticateHandler(server *Server, client *Client, msg ircmsg.Message, rb
|
||||
// and I don't want to break working clients that use PLAIN or EXTERNAL
|
||||
// and violate this MUST (e.g. by sending CAP END too early).
|
||||
if client.registered && !(mechanism == "PLAIN" || mechanism == "EXTERNAL") {
|
||||
rb.Add(nil, server.name, ERR_SASLFAIL, details.nick, client.t("SASL is only allowed before connection registration"))
|
||||
rb.Add(nil, server.name, ERR_SASLFAIL, client.Nick(), client.t("SASL is only allowed before connection registration"))
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -275,12 +306,12 @@ func authPlainHandler(server *Server, client *Client, session *Session, value []
|
||||
if len(splitValue) == 3 {
|
||||
authzid, authcid = string(splitValue[0]), string(splitValue[1])
|
||||
|
||||
if authzid != "" && authcid != authzid {
|
||||
rb.Add(nil, server.name, ERR_SASLFAIL, client.Nick(), client.t("SASL authentication failed: authcid and authzid should be the same"))
|
||||
if authzid != "" && authzid != authcid {
|
||||
rb.Add(nil, server.name, ERR_SASLFAIL, client.nick, client.t("SASL authentication failed: authcid and authzid should be the same"))
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
rb.Add(nil, server.name, ERR_SASLFAIL, client.Nick(), client.t("SASL authentication failed: Invalid auth blob"))
|
||||
rb.Add(nil, server.name, ERR_SASLFAIL, client.nick, client.t("SASL authentication failed: Invalid auth blob"))
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -979,7 +1010,7 @@ func dlineHandler(server *Server, client *Client, msg ircmsg.Message, rb *Respon
|
||||
// check oper permissions
|
||||
oper := client.Oper()
|
||||
if !oper.HasRoleCapab("ban") {
|
||||
rb.Add(nil, server.name, ERR_NOPRIVS, client.nick, msg.Command, client.t("Insufficient oper privs"))
|
||||
rb.Add(nil, server.name, ERR_NOPRIVS, client.Nick(), msg.Command, client.t("Insufficient oper privs"))
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1024,10 +1055,6 @@ func dlineHandler(server *Server, client *Client, msg ircmsg.Message, rb *Respon
|
||||
}
|
||||
|
||||
// get host
|
||||
if len(msg.Params) < currentArg+1 {
|
||||
rb.Add(nil, server.name, ERR_NEEDMOREPARAMS, client.nick, msg.Command, client.t("Not enough parameters"))
|
||||
return false
|
||||
}
|
||||
hostString := msg.Params[currentArg]
|
||||
currentArg++
|
||||
|
||||
@@ -1367,6 +1394,10 @@ func joinHandler(server *Server, client *Client, msg ircmsg.Message, rb *Respons
|
||||
if len(keys) > i {
|
||||
key = keys[i]
|
||||
}
|
||||
|
||||
// Check if this is a nostr feed channel
|
||||
// (nostr relay feeds removed)
|
||||
|
||||
err, forward := server.channels.Join(client, name, key, false, rb)
|
||||
if err != nil {
|
||||
if forward != "" {
|
||||
@@ -1529,7 +1560,7 @@ func killHandler(server *Server, client *Client, msg ircmsg.Message, rb *Respons
|
||||
rb.Add(nil, client.server.name, ERR_NOSUCHNICK, client.Nick(), utils.SafeErrorParam(nickname), client.t("No such nick"))
|
||||
return false
|
||||
} else if target.AlwaysOn() {
|
||||
rb.Add(nil, client.server.name, ERR_UNKNOWNERROR, client.Nick(), "KILL", fmt.Sprintf(client.t("Client %s is always-on and cannot be fully removed by /KILL; consider /UBAN ADD instead"), target.Nick()))
|
||||
rb.Add(nil, client.server.name, ERR_UNKNOWNERROR, client.Nick(), client.t("Client %s is always-on and cannot be fully removed by /KILL; consider /UBAN ADD instead"), target.Nick())
|
||||
}
|
||||
|
||||
quitMsg := "Killed"
|
||||
@@ -1812,6 +1843,8 @@ func listHandler(server *Server, client *Client, msg ircmsg.Message, rb *Respons
|
||||
rplList(channel)
|
||||
}
|
||||
}
|
||||
|
||||
// (nostr relay feeds removed)
|
||||
} else {
|
||||
// limit regular users to only listing one channel
|
||||
if !clientIsOp {
|
||||
@@ -2224,7 +2257,7 @@ func absorbBatchedMessage(server *Server, client *Client, msg ircmsg.Message, ba
|
||||
var failParams []string
|
||||
defer func() {
|
||||
if failParams != nil {
|
||||
if histType != history.Notice {
|
||||
if histType != history.Privmsg {
|
||||
params := make([]string, 1+len(failParams))
|
||||
params[0] = "BATCH"
|
||||
copy(params[1:], failParams)
|
||||
@@ -2342,6 +2375,8 @@ func dispatchMessageToTarget(client *Client, tags map[string]string, histType hi
|
||||
if len(target) == 0 {
|
||||
return
|
||||
} else if target[0] == '#' {
|
||||
// (nostr relay feeds removed)
|
||||
|
||||
channel := server.channels.Get(target)
|
||||
if channel == nil {
|
||||
if histType != history.Notice {
|
||||
@@ -2367,7 +2402,7 @@ func dispatchMessageToTarget(client *Client, tags map[string]string, histType hi
|
||||
|
||||
tnick := tClient.Nick()
|
||||
for _, session := range tClient.Sessions() {
|
||||
session.sendSplitMsgFromClientInternal(false, nickMaskString, accountName, isBot, nil, command, tnick, message)
|
||||
session.sendFromClientInternal(false, time.Time{}, "", nickMaskString, accountName, isBot, tags, command, tnick)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2484,7 +2519,7 @@ func dispatchMessageToTarget(client *Client, tags map[string]string, histType hi
|
||||
time: message.Time,
|
||||
})
|
||||
} else {
|
||||
server.logger.Error("internal", "can't serialize push message", err.Error())
|
||||
server.logger.Error("internal", "couldn't serialize push message", err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2541,7 +2576,7 @@ func npcaHandler(server *Server, client *Client, msg ircmsg.Message, rb *Respons
|
||||
// OPER <name> [password]
|
||||
func operHandler(server *Server, client *Client, msg ircmsg.Message, rb *ResponseBuffer) bool {
|
||||
if client.HasMode(modes.Operator) {
|
||||
rb.Add(nil, server.name, ERR_UNKNOWNERROR, client.Nick(), "OPER", client.t("You're already opered-up!"))
|
||||
rb.Add(nil, server.name, ERR_UNKNOWNERROR, client.Nick(), client.t("You're already opered-up!"))
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -2666,6 +2701,9 @@ func partHandler(server *Server, client *Client, msg ircmsg.Message, rb *Respons
|
||||
if chname == "" {
|
||||
continue // #679
|
||||
}
|
||||
|
||||
// (nostr relay feeds removed)
|
||||
|
||||
err := server.channels.Part(client, chname, reason, rb)
|
||||
if err == errNoSuchChannel {
|
||||
rb.Add(nil, server.name, ERR_NOSUCHCHANNEL, client.nick, utils.SafeErrorParam(chname), client.t("No such channel"))
|
||||
@@ -2801,6 +2839,7 @@ func redactHandler(server *Server, client *Client, msg ircmsg.Message, rb *Respo
|
||||
isBot := client.HasMode(modes.Bot)
|
||||
|
||||
if target[0] == '#' {
|
||||
// (nostr relay feeds removed)
|
||||
channel := server.channels.Get(target)
|
||||
if channel == nil {
|
||||
rb.Add(nil, server.name, ERR_NOSUCHCHANNEL, client.Nick(), utils.SafeErrorParam(target), client.t("No such channel"))
|
||||
@@ -2852,7 +2891,7 @@ func redactHandler(server *Server, client *Client, msg ircmsg.Message, rb *Respo
|
||||
|
||||
if err != nil {
|
||||
client.server.logger.Error("internal", fmt.Sprintf("Private message %s is not deletable by %s from their own buffer's even though we just deleted it from %s's. This is a bug, please report it in details.", targetmsgid, client.Nick(), target), client.Nick())
|
||||
isOper := client.HasRoleCapabs("history")
|
||||
isOper := client.HasMode(modes.Operator)
|
||||
if isOper {
|
||||
rb.Add(nil, server.name, "FAIL", "REDACT", "REDACT_FORBIDDEN", utils.SafeErrorParam(target), utils.SafeErrorParam(targetmsgid), fmt.Sprintf(client.t("Error deleting message: %v"), err))
|
||||
} else {
|
||||
@@ -3232,8 +3271,8 @@ func metadataRegisteredHandler(client *Client, config *Config, subcommand string
|
||||
return
|
||||
}
|
||||
|
||||
batchId := rb.StartNestedBatch("metadata", target)
|
||||
defer rb.EndNestedBatch(batchId)
|
||||
batchID := rb.StartNestedBatch("metadata", target)
|
||||
defer rb.EndNestedBatch(batchID)
|
||||
|
||||
for _, key := range params[2:] {
|
||||
if metadataKeyIsEvil(key) {
|
||||
@@ -3255,11 +3294,6 @@ func metadataRegisteredHandler(client *Client, config *Config, subcommand string
|
||||
playMetadataList(rb, client.Nick(), target, targetObj.ListMetadata())
|
||||
|
||||
case "clear":
|
||||
if !metadataCanIEditThisTarget(client, targetObj) {
|
||||
noKeyPerms("*")
|
||||
return
|
||||
}
|
||||
|
||||
values := targetObj.ClearMetadata()
|
||||
|
||||
playMetadataList(rb, client.Nick(), target, values)
|
||||
@@ -4240,7 +4274,7 @@ func whoHandler(server *Server, client *Client, msg ircmsg.Message, rb *Response
|
||||
|
||||
// successfully parsed query, ensure we send the success response:
|
||||
defer func() {
|
||||
rb.Add(nil, server.name, RPL_ENDOFWHO, client.Nick(), origMask, client.t("End of WHO list"))
|
||||
rb.Add(nil, server.name, RPL_ENDOFWHO, client.Nick(), origMask, client.t("End of /WHO list"))
|
||||
}()
|
||||
|
||||
// XXX #1730: https://datatracker.ietf.org/doc/html/rfc1459#section-4.5.1
|
||||
@@ -4312,67 +4346,6 @@ func whoHandler(server *Server, client *Client, msg ircmsg.Message, rb *Response
|
||||
return false
|
||||
}
|
||||
|
||||
// WHOIS [<target>] <mask>{,<mask>}
|
||||
func whoisHandler(server *Server, client *Client, msg ircmsg.Message, rb *ResponseBuffer) bool {
|
||||
var masksString string
|
||||
//var target string
|
||||
|
||||
if len(msg.Params) > 1 {
|
||||
//target = msg.Params[0]
|
||||
masksString = msg.Params[1]
|
||||
} else {
|
||||
masksString = msg.Params[0]
|
||||
}
|
||||
|
||||
handleService := func(nick string) bool {
|
||||
cfnick, _ := CasefoldName(nick)
|
||||
service, ok := ErgoServices[cfnick]
|
||||
hostname := "localhost"
|
||||
config := server.Config()
|
||||
if config.Server.OverrideServicesHostname != "" {
|
||||
hostname = config.Server.OverrideServicesHostname
|
||||
}
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
clientNick := client.Nick()
|
||||
rb.Add(nil, client.server.name, RPL_WHOISUSER, clientNick, service.Name, service.Name, hostname, "*", service.Realname(client))
|
||||
// #1080:
|
||||
rb.Add(nil, client.server.name, RPL_WHOISOPERATOR, clientNick, service.Name, client.t("is a network service"))
|
||||
// hehe
|
||||
if client.HasMode(modes.TLS) {
|
||||
rb.Add(nil, client.server.name, RPL_WHOISSECURE, clientNick, service.Name, client.t("is using a secure connection"))
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
hasPrivs := client.HasRoleCapabs("samode")
|
||||
if hasPrivs {
|
||||
for _, mask := range strings.Split(masksString, ",") {
|
||||
matches := server.clients.FindAll(mask)
|
||||
if len(matches) == 0 && !handleService(mask) {
|
||||
rb.Add(nil, client.server.name, ERR_NOSUCHNICK, client.Nick(), utils.SafeErrorParam(mask), client.t("No such nick"))
|
||||
continue
|
||||
}
|
||||
for mclient := range matches {
|
||||
client.getWhoisOf(mclient, hasPrivs, rb)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// only get the first request; also require a nick, not a mask
|
||||
nick := strings.Split(masksString, ",")[0]
|
||||
mclient := server.clients.Get(nick)
|
||||
if mclient != nil {
|
||||
client.getWhoisOf(mclient, hasPrivs, rb)
|
||||
} else if !handleService(nick) {
|
||||
rb.Add(nil, client.server.name, ERR_NOSUCHNICK, client.Nick(), utils.SafeErrorParam(masksString), client.t("No such nick"))
|
||||
}
|
||||
// fall through, ENDOFWHOIS is always sent
|
||||
}
|
||||
rb.Add(nil, server.name, RPL_ENDOFWHOIS, client.nick, utils.SafeErrorParam(masksString), client.t("End of /WHOIS list"))
|
||||
return false
|
||||
}
|
||||
|
||||
// WHOWAS <nickname> [<count> [<server>]]
|
||||
func whowasHandler(server *Server, client *Client, msg ircmsg.Message, rb *ResponseBuffer) bool {
|
||||
nicknames := strings.Split(msg.Params[0], ",")
|
||||
|
||||
@@ -135,11 +135,18 @@ INFO gives you information about the given (or your own) user account.`,
|
||||
handler: nsRegisterHandler,
|
||||
// TODO: "email" is an oversimplification here; it's actually any callback, e.g.,
|
||||
// person@example.com, mailto:person@example.com, tel:16505551234.
|
||||
help: `Syntax: $bREGISTER <password> [email]$b
|
||||
help: `Syntax: $bREGISTER <password> [nostr-identifier]$b
|
||||
|
||||
REGISTER lets you register your current nickname as a user account. If the
|
||||
server allows anonymous registration, you can omit the e-mail address.
|
||||
REGISTER lets you register your current nickname as a user account using nostr
|
||||
verification. You can provide a nostr identifier in one of these formats:
|
||||
|
||||
• NIP-05 address: alice@example.com
|
||||
• npub key: npub1abc123def456...
|
||||
• hex pubkey: 3bf0c63fcb93c5ef2f068d70b8d70d963b649d75...
|
||||
|
||||
The server will contact you via Nostr DM (nip17 or nip04) with verification code.
|
||||
|
||||
If the server allows anonymous registration, you can omit the nostr identifier.
|
||||
If you are currently logged in with a TLS client certificate and wish to use
|
||||
it instead of a password to log in, send * as the password.`,
|
||||
helpShort: `$bREGISTER$b lets you register a user account.`,
|
||||
@@ -1016,7 +1023,7 @@ func nsRegisterHandler(service *ircService, server *Server, client *Client, comm
|
||||
|
||||
callbackNamespace, callbackValue, validationErr := parseCallback(email, config)
|
||||
if validationErr != nil {
|
||||
service.Notice(rb, client.t("Registration requires a valid e-mail address"))
|
||||
service.Notice(rb, client.t("Registration invalid, see help"))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
380
irc/nostr/dm.go
Normal file
380
irc/nostr/dm.go
Normal file
@@ -0,0 +1,380 @@
|
||||
// Copyright (c) 2024 Ergo Contributors
|
||||
// released under the MIT license
|
||||
|
||||
package nostr
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/nbd-wtf/go-nostr/nip04"
|
||||
"github.com/nbd-wtf/go-nostr/nip17"
|
||||
"github.com/nbd-wtf/go-nostr/nip44"
|
||||
)
|
||||
|
||||
// DMConfig holds configuration for DM operations
|
||||
type DMConfig struct {
|
||||
PrivateKey string // Hex-encoded private key for signing
|
||||
DefaultRelays []string // Fallback relays if none discovered
|
||||
Timeout time.Duration // Timeout for relay operations
|
||||
UserAgent string // User agent for HTTP requests
|
||||
}
|
||||
|
||||
// SimpleKeyer implements nostr.Keyer interface for NIP-17
|
||||
type SimpleKeyer struct {
|
||||
privateKey string
|
||||
}
|
||||
|
||||
func (k *SimpleKeyer) GetPublicKey(ctx context.Context) (string, error) {
|
||||
return nostr.GetPublicKey(k.privateKey)
|
||||
}
|
||||
|
||||
func (k *SimpleKeyer) SignEvent(ctx context.Context, event *nostr.Event) error {
|
||||
return event.Sign(k.privateKey)
|
||||
}
|
||||
|
||||
func (k *SimpleKeyer) Encrypt(ctx context.Context, plaintext, recipientPubkey string) (string, error) {
|
||||
// Generate conversation key using NIP-44
|
||||
conversationKey, err := nip44.GenerateConversationKey(recipientPubkey, k.privateKey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return nip44.Encrypt(plaintext, conversationKey)
|
||||
}
|
||||
|
||||
func (k *SimpleKeyer) Decrypt(ctx context.Context, ciphertext, senderPubkey string) (string, error) {
|
||||
// Generate conversation key using NIP-44
|
||||
conversationKey, err := nip44.GenerateConversationKey(senderPubkey, k.privateKey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return nip44.Decrypt(ciphertext, conversationKey)
|
||||
}
|
||||
|
||||
// CreateNIP04DM creates a NIP-04 DM event with verification code
|
||||
func CreateNIP04DM(recipientPubkey, senderPrivkey, account, code, serverName string) (*nostr.Event, error) {
|
||||
// Create the message content
|
||||
message := fmt.Sprintf("Account verification for %s\n\nAccount: %s\nVerification code: %s\n\nTo verify your account, issue the following command:\n/MSG NickServ VERIFY %s %s",
|
||||
serverName, account, code, account, code)
|
||||
|
||||
// Compute shared secret for NIP-04 encryption
|
||||
sharedSecret, err := nip04.ComputeSharedSecret(recipientPubkey, senderPrivkey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: failed to compute shared secret: %v", ErrDMEncryptionFailed, err)
|
||||
}
|
||||
|
||||
// Encrypt the message using NIP-04
|
||||
encryptedContent, err := nip04.Encrypt(message, sharedSecret)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %v", ErrDMEncryptionFailed, err)
|
||||
}
|
||||
|
||||
// Get sender pubkey from private key
|
||||
senderPubkey, err := nostr.GetPublicKey(senderPrivkey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to derive public key: %w", err)
|
||||
}
|
||||
|
||||
// Create the event
|
||||
event := &nostr.Event{
|
||||
PubKey: senderPubkey,
|
||||
CreatedAt: nostr.Timestamp(time.Now().Unix()),
|
||||
Kind: 4, // NIP-04 DM event kind
|
||||
Tags: nostr.Tags{
|
||||
{"p", recipientPubkey},
|
||||
},
|
||||
Content: encryptedContent,
|
||||
}
|
||||
|
||||
// Sign the event
|
||||
if err := event.Sign(senderPrivkey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Detailed debug logging for comparison
|
||||
log.Printf("[NOSTR DEBUG] Created NIP-04 DM event: %+v", event)
|
||||
jsonEvent, err := json.MarshalIndent(event, "", " ")
|
||||
if err != nil {
|
||||
log.Printf("[NOSTR DEBUG] Failed to marshal event to JSON: %v", err)
|
||||
} else {
|
||||
log.Printf("[NOSTR DEBUG] NIP-04 DM event JSON: %s", jsonEvent)
|
||||
}
|
||||
|
||||
return event, nil
|
||||
}
|
||||
|
||||
// CreateNIP17DM creates a NIP-17 DM event with verification code
|
||||
func CreateNIP17DM(recipientPubkey, senderPrivkey, account, code, serverName string) (*nostr.Event, error) {
|
||||
// Create the message content
|
||||
message := fmt.Sprintf("Account verification for %s\n\nAccount: %s\nVerification code: %s\n\nTo verify your account, issue the following command:\n/MSG NickServ VERIFY %s %s",
|
||||
serverName, account, code, account, code)
|
||||
|
||||
log.Printf("[NOSTR DEBUG] Creating NIP-17 DM")
|
||||
log.Printf("[NOSTR DEBUG] Recipient pubkey: %s", recipientPubkey)
|
||||
log.Printf("[NOSTR DEBUG] Message content: %s", message)
|
||||
|
||||
// Create a SimpleKeyer instance
|
||||
keyer := &SimpleKeyer{privateKey: senderPrivkey}
|
||||
|
||||
// Get sender pubkey for logging
|
||||
senderPubkey, err := nostr.GetPublicKey(senderPrivkey)
|
||||
if err != nil {
|
||||
log.Printf("[NOSTR DEBUG] Failed to derive sender pubkey: %v", err)
|
||||
return nil, fmt.Errorf("failed to derive public key: %w", err)
|
||||
}
|
||||
log.Printf("[NOSTR DEBUG] Sender pubkey: %s", senderPubkey)
|
||||
|
||||
// Use nip17.PrepareMessage to create properly gift-wrapped events
|
||||
ctx := context.Background()
|
||||
toUs, toThem, err := nip17.PrepareMessage(
|
||||
ctx,
|
||||
message,
|
||||
nostr.Tags{}, // empty tags, the function will add the "p" tag
|
||||
keyer,
|
||||
recipientPubkey,
|
||||
nil, // no modify function
|
||||
)
|
||||
if err != nil {
|
||||
log.Printf("[NOSTR DEBUG] Failed to prepare NIP-17 message: %v", err)
|
||||
return nil, fmt.Errorf("%w: %v", ErrDMEncryptionFailed, err)
|
||||
}
|
||||
|
||||
log.Printf("[NOSTR DEBUG] NIP-17 events prepared successfully")
|
||||
log.Printf("[NOSTR DEBUG] toUs event - Kind: %d, ID: %s", toUs.Kind, toUs.ID)
|
||||
log.Printf("[NOSTR DEBUG] toThem event - Kind: %d, ID: %s", toThem.Kind, toThem.ID)
|
||||
|
||||
// Marshal complete event to JSON for detailed inspection
|
||||
jsonEvent, err := json.MarshalIndent(toThem, "", " ")
|
||||
if err != nil {
|
||||
log.Printf("[NOSTR DEBUG] Failed to marshal NIP-17 event to JSON: %v", err)
|
||||
} else {
|
||||
log.Printf("[NOSTR DEBUG] Complete NIP-17 event JSON: %s", jsonEvent)
|
||||
}
|
||||
|
||||
// Return the toThem event (the one that goes to the recipient)
|
||||
return &toThem, nil
|
||||
}
|
||||
|
||||
// SendVerificationDM sends a verification DM to a user
|
||||
func SendVerificationDM(identifier, account, code, serverName string, config DMConfig) error {
|
||||
if config.PrivateKey == "" {
|
||||
return ErrNostrKeyNotConfigured
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), config.Timeout)
|
||||
defer cancel()
|
||||
|
||||
// Resolve the identifier to a pubkey
|
||||
nip05Config := NIP05Config{
|
||||
Timeout: config.Timeout,
|
||||
UserAgent: config.UserAgent,
|
||||
}
|
||||
|
||||
pubkey, nip05Relays, err := ResolvePubkey(identifier, nip05Config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to resolve pubkey: %w", err)
|
||||
}
|
||||
|
||||
// Discover inbox relays
|
||||
relayConfig := RelayConfig{
|
||||
DefaultRelays: config.DefaultRelays,
|
||||
Timeout: config.Timeout,
|
||||
MaxRelays: 10,
|
||||
}
|
||||
|
||||
// Combine NIP-05 relays with discovered relays
|
||||
allRelays := append(nip05Relays, config.DefaultRelays...)
|
||||
if len(allRelays) > 0 {
|
||||
relayConfig.DefaultRelays = allRelays
|
||||
}
|
||||
|
||||
// Check if user has private relays (NIP-50 kind 10050)
|
||||
privateRelays, hasPrivateRelays, err := CheckPrivateRelays(ctx, pubkey, relayConfig)
|
||||
if err != nil {
|
||||
// Continue with regular flow if private relay check fails
|
||||
hasPrivateRelays = false
|
||||
}
|
||||
|
||||
var dmEvent *nostr.Event
|
||||
var targetRelays []string
|
||||
|
||||
if hasPrivateRelays && len(privateRelays) > 0 {
|
||||
log.Printf("Using NIP-17 DM for user with private relays")
|
||||
// Use NIP-17 DMs for users with private relays
|
||||
dmEvent, err = CreateNIP17DM(pubkey, config.PrivateKey, account, code, serverName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create NIP-17 DM: %w", err)
|
||||
}
|
||||
targetRelays = privateRelays
|
||||
log.Printf("Targeting private relays: %v", targetRelays)
|
||||
} else {
|
||||
log.Printf("Using NIP-04 DM for regular user")
|
||||
// Use NIP-04 DMs for regular users
|
||||
dmEvent, err = CreateNIP04DM(pubkey, config.PrivateKey, account, code, serverName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create NIP-04 DM: %w", err)
|
||||
}
|
||||
|
||||
// Discover inbox relays for NIP-04
|
||||
inboxRelays, err := DiscoverInboxRelays(pubkey, relayConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to discover inbox relays: %w", err)
|
||||
}
|
||||
targetRelays = inboxRelays
|
||||
log.Printf("Targeting inbox relays: %v", targetRelays)
|
||||
}
|
||||
|
||||
// Send to target relays with retry logic
|
||||
var lastErr error
|
||||
successCount := 0
|
||||
|
||||
for _, relayURL := range targetRelays {
|
||||
// Try connecting and sending
|
||||
err := sendToRelayWithRetry(ctx, relayURL, *dmEvent, config.PrivateKey)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
|
||||
successCount++
|
||||
}
|
||||
|
||||
if successCount == 0 {
|
||||
if lastErr != nil {
|
||||
return fmt.Errorf("%w: %v", ErrDMSendFailed, lastErr)
|
||||
}
|
||||
return ErrDMSendFailed
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// sendToRelayWithRetry attempts to send a DM to a relay with retry logic for auth failures
|
||||
func sendToRelayWithRetry(ctx context.Context, relayURL string, event nostr.Event, privkey string) error {
|
||||
log.Printf("[NOSTR DEBUG] Connecting to relay: %s", relayURL)
|
||||
|
||||
// Create a separate timeout context for this relay connection (15 seconds)
|
||||
relayCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// First attempt without auth
|
||||
relay, err := nostr.RelayConnect(relayCtx, relayURL)
|
||||
if err != nil {
|
||||
log.Printf("[NOSTR DEBUG] Failed to connect to %s: %v", relayURL, err)
|
||||
return fmt.Errorf("failed to connect to relay %s: %w", relayURL, err)
|
||||
}
|
||||
defer relay.Close()
|
||||
|
||||
log.Printf("[NOSTR DEBUG] Attempting to publish to %s without auth", relayURL)
|
||||
// Try sending without auth first
|
||||
err = relay.Publish(relayCtx, event)
|
||||
if err == nil {
|
||||
log.Printf("[NOSTR DEBUG] Successfully published to %s without auth", relayURL)
|
||||
// Log the event that was successfully published
|
||||
eventJSON, jsonErr := json.MarshalIndent(event, "", " ")
|
||||
if jsonErr != nil {
|
||||
log.Printf("[NOSTR DEBUG] Failed to marshal published event: %v", jsonErr)
|
||||
} else {
|
||||
log.Printf("[NOSTR DEBUG] Published event JSON: %s", eventJSON)
|
||||
}
|
||||
return nil // Success!
|
||||
}
|
||||
|
||||
log.Printf("[NOSTR DEBUG] Publish failed on %s: %v", relayURL, err)
|
||||
// If we get an auth error, try with authentication
|
||||
if strings.Contains(err.Error(), "auth-required") || strings.Contains(err.Error(), "you must auth") {
|
||||
log.Printf("[NOSTR DEBUG] Auth required for %s, reconnecting with authentication", relayURL)
|
||||
// Close and reconnect with auth
|
||||
relay.Close()
|
||||
|
||||
// Create a new timeout context for the auth connection
|
||||
authCtx, authCancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer authCancel()
|
||||
|
||||
relay, err = ConnectToRelay(authCtx, relayURL, privkey)
|
||||
if err != nil {
|
||||
log.Printf("[NOSTR DEBUG] Failed to connect with auth to %s: %v", relayURL, err)
|
||||
return fmt.Errorf("failed to connect with auth to relay %s: %w", relayURL, err)
|
||||
}
|
||||
defer relay.Close()
|
||||
|
||||
log.Printf("[NOSTR DEBUG] Attempting to publish to %s with auth", relayURL)
|
||||
// Try sending again after auth
|
||||
err = relay.Publish(authCtx, event)
|
||||
if err != nil {
|
||||
log.Printf("[NOSTR DEBUG] Failed to publish to authenticated %s: %v", relayURL, err)
|
||||
return fmt.Errorf("failed to publish to authenticated relay %s: %w", relayURL, err)
|
||||
}
|
||||
|
||||
log.Printf("[NOSTR DEBUG] Successfully published to %s with auth", relayURL)
|
||||
// Log the event that was successfully published
|
||||
eventJSON, jsonErr := json.MarshalIndent(event, "", " ")
|
||||
if jsonErr != nil {
|
||||
log.Printf("[NOSTR DEBUG] Failed to marshal published event: %v", jsonErr)
|
||||
} else {
|
||||
log.Printf("[NOSTR DEBUG] Published event JSON: %s", eventJSON)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Other error, return as-is
|
||||
log.Printf("[NOSTR DEBUG] Non-auth error on %s: %v", relayURL, err)
|
||||
return fmt.Errorf("failed to publish to relay %s: %w", relayURL, err)
|
||||
}
|
||||
|
||||
// SendNIP04DM sends a NIP-04 DM to specific relays
|
||||
func SendNIP04DM(ctx context.Context, event *nostr.Event, relays []string, privkey string) error {
|
||||
var lastErr error
|
||||
successCount := 0
|
||||
|
||||
for _, relayURL := range relays {
|
||||
// Try connecting and sending
|
||||
err := sendToRelayWithRetry(ctx, relayURL, *event, privkey)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
|
||||
successCount++
|
||||
}
|
||||
|
||||
if successCount == 0 {
|
||||
if lastErr != nil {
|
||||
return fmt.Errorf("%w: %v", ErrDMSendFailed, lastErr)
|
||||
}
|
||||
return ErrDMSendFailed
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendNIP17DM sends a NIP-17 DM to private relays
|
||||
func SendNIP17DM(ctx context.Context, event *nostr.Event, privateRelays []string, privkey string) error {
|
||||
var lastErr error
|
||||
successCount := 0
|
||||
|
||||
for _, relayURL := range privateRelays {
|
||||
// Try connecting and sending
|
||||
err := sendToRelayWithRetry(ctx, relayURL, *event, privkey)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
|
||||
successCount++
|
||||
}
|
||||
|
||||
if successCount == 0 {
|
||||
if lastErr != nil {
|
||||
return fmt.Errorf("%w: %v", ErrDMSendFailed, lastErr)
|
||||
}
|
||||
return ErrDMSendFailed
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
37
irc/nostr/errors.go
Normal file
37
irc/nostr/errors.go
Normal file
@@ -0,0 +1,37 @@
|
||||
// Copyright (c) 2024 Ergo Contributors
|
||||
// released under the MIT license
|
||||
|
||||
package nostr
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
// Identifier validation errors
|
||||
ErrInvalidNostrIdentifier = errors.New("invalid nostr identifier format")
|
||||
ErrInvalidPubkeyFormat = errors.New("invalid pubkey format")
|
||||
ErrInvalidPubkeyLength = errors.New("invalid pubkey length")
|
||||
ErrInvalidHexFormat = errors.New("invalid hex format")
|
||||
ErrInvalidPrivkeyLength = errors.New("invalid private key length")
|
||||
ErrInvalidNpubPrefix = errors.New("invalid npub prefix")
|
||||
|
||||
// NIP-05 resolution errors
|
||||
ErrNIP05ResolutionFailed = errors.New("NIP-05 resolution failed")
|
||||
ErrNIP05NotFound = errors.New("NIP-05 address not found")
|
||||
ErrNIP05InvalidResponse = errors.New("invalid NIP-05 response")
|
||||
ErrNIP05HTTPError = errors.New("HTTP error during NIP-05 resolution")
|
||||
ErrNIP05PubkeyNotFound = errors.New("pubkey not found in NIP-05 response")
|
||||
|
||||
// Relay discovery errors
|
||||
ErrRelayDiscoveryFailed = errors.New("relay discovery failed")
|
||||
ErrNoInboxRelaysFound = errors.New("no inbox relays found")
|
||||
ErrRelayConnectionFailed = errors.New("relay connection failed")
|
||||
ErrRelayAuthFailed = errors.New("relay authentication failed")
|
||||
|
||||
// DM sending errors
|
||||
ErrDMSendFailed = errors.New("DM send failed")
|
||||
ErrDMEncryptionFailed = errors.New("DM encryption failed")
|
||||
ErrNostrKeyNotConfigured = errors.New("nostr private key not configured")
|
||||
|
||||
// Decoding errors
|
||||
ErrNpubDecodingFailed = errors.New("npub decoding failed")
|
||||
)
|
||||
115
irc/nostr/nip05.go
Normal file
115
irc/nostr/nip05.go
Normal file
@@ -0,0 +1,115 @@
|
||||
// Copyright (c) 2024 Ergo Contributors
|
||||
// released under the MIT license
|
||||
|
||||
package nostr
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// NIP05Response represents the JSON response from a .well-known/nostr.json endpoint
|
||||
type NIP05Response struct {
|
||||
Names map[string]string `json:"names"`
|
||||
Relays map[string][]string `json:"relays,omitempty"`
|
||||
}
|
||||
|
||||
// NIP05Config holds configuration for NIP-05 resolution
|
||||
type NIP05Config struct {
|
||||
Timeout time.Duration
|
||||
UserAgent string
|
||||
}
|
||||
|
||||
// ResolveNIP05 resolves a NIP-05 identifier to a pubkey
|
||||
func ResolveNIP05(identifier string, config NIP05Config) (pubkey string, relays []string, err error) {
|
||||
if !IsNIP05(identifier) {
|
||||
return "", nil, ErrInvalidPubkeyFormat
|
||||
}
|
||||
|
||||
parts := strings.Split(identifier, "@")
|
||||
if len(parts) != 2 {
|
||||
return "", nil, ErrInvalidPubkeyFormat
|
||||
}
|
||||
|
||||
name, domain := parts[0], parts[1]
|
||||
|
||||
// Construct the well-known URL
|
||||
url := fmt.Sprintf("https://%s/.well-known/nostr.json?name=%s", domain, name)
|
||||
|
||||
// Create HTTP client with timeout
|
||||
client := &http.Client{
|
||||
Timeout: config.Timeout,
|
||||
}
|
||||
|
||||
// Make the request
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("%w: %v", ErrNIP05HTTPError, err)
|
||||
}
|
||||
|
||||
if config.UserAgent != "" {
|
||||
req.Header.Set("User-Agent", config.UserAgent)
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("%w: %v", ErrNIP05HTTPError, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", nil, fmt.Errorf("%w: HTTP %d", ErrNIP05HTTPError, resp.StatusCode)
|
||||
}
|
||||
|
||||
// Parse the JSON response
|
||||
var nip05Resp NIP05Response
|
||||
if err := json.NewDecoder(resp.Body).Decode(&nip05Resp); err != nil {
|
||||
return "", nil, fmt.Errorf("%w: %v", ErrNIP05InvalidResponse, err)
|
||||
}
|
||||
|
||||
// Look up the pubkey for this name
|
||||
pubkey, exists := nip05Resp.Names[name]
|
||||
if !exists {
|
||||
return "", nil, ErrNIP05PubkeyNotFound
|
||||
}
|
||||
|
||||
// Validate the returned pubkey
|
||||
if err := ValidateHexPubkey(pubkey); err != nil {
|
||||
return "", nil, fmt.Errorf("%w: invalid pubkey in response", ErrNIP05InvalidResponse)
|
||||
}
|
||||
|
||||
// Get relays if available
|
||||
if nip05Resp.Relays != nil {
|
||||
relays = nip05Resp.Relays[pubkey]
|
||||
}
|
||||
|
||||
return pubkey, relays, nil
|
||||
}
|
||||
|
||||
// ResolvePubkey resolves any nostr identifier to a hex pubkey
|
||||
func ResolvePubkey(identifier string, config NIP05Config) (pubkey string, relays []string, err error) {
|
||||
// Debug logging to see what identifier we're trying to resolve
|
||||
fmt.Printf("[DEBUG] ResolvePubkey called with identifier: '%s'\n", identifier)
|
||||
fmt.Printf("[DEBUG] IsNIP05: %v, IsPubkey: %v, IsNpub: %v, IsHexPubkey: %v\n",
|
||||
IsNIP05(identifier), IsPubkey(identifier), IsNpub(identifier), IsHexPubkey(identifier))
|
||||
|
||||
if IsNIP05(identifier) {
|
||||
fmt.Printf("[DEBUG] Resolving as NIP-05 identifier\n")
|
||||
return ResolveNIP05(identifier, config)
|
||||
} else if IsPubkey(identifier) {
|
||||
fmt.Printf("[DEBUG] Resolving as pubkey, normalizing...\n")
|
||||
normalizedPubkey, err := NormalizePubkey(identifier)
|
||||
if err != nil {
|
||||
fmt.Printf("[DEBUG] NormalizePubkey failed: %v\n", err)
|
||||
return "", nil, err
|
||||
}
|
||||
fmt.Printf("[DEBUG] Normalized pubkey: %s\n", normalizedPubkey)
|
||||
return normalizedPubkey, nil, nil
|
||||
}
|
||||
|
||||
fmt.Printf("[DEBUG] Identifier doesn't match any known format\n")
|
||||
return "", nil, ErrInvalidPubkeyFormat
|
||||
}
|
||||
160
irc/nostr/nostr.go
Normal file
160
irc/nostr/nostr.go
Normal file
@@ -0,0 +1,160 @@
|
||||
// Copyright (c) 2024 Ergo Contributors
|
||||
// released under the MIT license
|
||||
|
||||
package nostr
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/nbd-wtf/go-nostr/nip19"
|
||||
)
|
||||
|
||||
var (
|
||||
// NIP-05 identifier format: name@domain.tld
|
||||
nip05Regex = regexp.MustCompile(`^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
|
||||
|
||||
// npub format (bech32 encoded pubkey)
|
||||
npubRegex = regexp.MustCompile(`^npub1[a-zA-Z0-9]{58}$`)
|
||||
|
||||
// hex pubkey format (64 hex characters)
|
||||
hexPubkeyRegex = regexp.MustCompile(`^[a-fA-F0-9]{64}$`)
|
||||
)
|
||||
|
||||
// IsNostrIdentifier checks if a string could be a nostr identifier (NIP-05 or pubkey)
|
||||
func IsNostrIdentifier(identifier string) bool {
|
||||
return IsNIP05(identifier) || IsPubkey(identifier)
|
||||
}
|
||||
|
||||
// IsValidNostrIdentifier validates a nostr identifier format
|
||||
func IsValidNostrIdentifier(identifier string) bool {
|
||||
return IsNostrIdentifier(identifier)
|
||||
}
|
||||
|
||||
// IsNIP05 checks if the identifier is a NIP-05 address
|
||||
func IsNIP05(identifier string) bool {
|
||||
return nip05Regex.MatchString(identifier)
|
||||
}
|
||||
|
||||
// IsPubkey checks if the identifier is a pubkey (npub or hex)
|
||||
func IsPubkey(identifier string) bool {
|
||||
return IsNpub(identifier) || IsHexPubkey(identifier)
|
||||
}
|
||||
|
||||
// IsNpub checks if the identifier is an npub (bech32) format
|
||||
func IsNpub(identifier string) bool {
|
||||
return npubRegex.MatchString(identifier)
|
||||
}
|
||||
|
||||
// IsHexPubkey checks if the identifier is a hex pubkey
|
||||
func IsHexPubkey(identifier string) bool {
|
||||
return hexPubkeyRegex.MatchString(identifier)
|
||||
}
|
||||
|
||||
// NormalizePubkey converts npub to hex format, returns hex pubkey as-is
|
||||
func NormalizePubkey(pubkey string) (string, error) {
|
||||
fmt.Printf("[DEBUG] NormalizePubkey called with: '%s' (len=%d)\n", pubkey, len(pubkey))
|
||||
fmt.Printf("[DEBUG] IsHexPubkey: %v, IsNpub: %v\n", IsHexPubkey(pubkey), IsNpub(pubkey))
|
||||
|
||||
if IsHexPubkey(pubkey) {
|
||||
fmt.Printf("[DEBUG] Treating as hex pubkey\n")
|
||||
return strings.ToLower(pubkey), nil
|
||||
}
|
||||
|
||||
if IsNpub(pubkey) {
|
||||
fmt.Printf("[DEBUG] Treating as npub, decoding...\n")
|
||||
// Use go-nostr to decode npub
|
||||
prefix, data, err := nip19.Decode(pubkey)
|
||||
if err != nil {
|
||||
fmt.Printf("[DEBUG] nip19.Decode failed: %v\n", err)
|
||||
return "", ErrInvalidPubkeyFormat
|
||||
}
|
||||
fmt.Printf("[DEBUG] nip19.Decode success - prefix: %s, data type: %T\n", prefix, data)
|
||||
|
||||
// Handle both string and []byte return types from nip19.Decode
|
||||
switch v := data.(type) {
|
||||
case []byte:
|
||||
if len(v) == 32 {
|
||||
hexResult := hex.EncodeToString(v)
|
||||
fmt.Printf("[DEBUG] Successfully converted npub bytes to hex: %s\n", hexResult)
|
||||
return hexResult, nil
|
||||
}
|
||||
case string:
|
||||
if len(v) == 64 {
|
||||
// Already a hex string, validate it
|
||||
if err := ValidateHexPubkey(v); err != nil {
|
||||
fmt.Printf("[DEBUG] Invalid hex pubkey from npub: %v\n", err)
|
||||
return "", ErrInvalidPubkeyFormat
|
||||
}
|
||||
fmt.Printf("[DEBUG] Successfully got hex string from npub: %s\n", v)
|
||||
return strings.ToLower(v), nil
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("[DEBUG] Unexpected data format from nip19.Decode: %T, value: %v\n", data, data)
|
||||
return "", ErrInvalidPubkeyFormat
|
||||
}
|
||||
|
||||
fmt.Printf("[DEBUG] Pubkey doesn't match hex or npub format\n")
|
||||
return "", ErrInvalidPubkeyFormat
|
||||
}
|
||||
|
||||
// ValidateHexPubkey ensures a hex string is a valid pubkey
|
||||
func ValidateHexPubkey(hexStr string) error {
|
||||
if len(hexStr) != 64 {
|
||||
return ErrInvalidPubkeyLength
|
||||
}
|
||||
|
||||
_, err := hex.DecodeString(hexStr)
|
||||
if err != nil {
|
||||
return ErrInvalidHexFormat
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidatePrivateKey validates a hex private key
|
||||
func ValidatePrivateKey(privkeyHex string) error {
|
||||
if len(privkeyHex) != 64 {
|
||||
return ErrInvalidPubkeyLength
|
||||
}
|
||||
|
||||
_, err := hex.DecodeString(privkeyHex)
|
||||
if err != nil {
|
||||
return ErrInvalidHexFormat
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPubkeyFromPrivkey derives public key from private key using go-nostr
|
||||
func GetPubkeyFromPrivkey(privkeyHex string) (string, error) {
|
||||
if len(privkeyHex) != 64 {
|
||||
return "", ErrInvalidPrivkeyLength
|
||||
}
|
||||
|
||||
pubkey, err := nostr.GetPublicKey(privkeyHex)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return pubkey, nil
|
||||
}
|
||||
|
||||
// HexToNpub converts a hex pubkey to npub format
|
||||
func HexToNpub(hexPubkey string) (string, error) {
|
||||
if !IsHexPubkey(hexPubkey) {
|
||||
return "", ErrInvalidPubkeyFormat
|
||||
}
|
||||
|
||||
// Encode as npub using nip19 (it expects hex string, not bytes)
|
||||
npub, err := nip19.EncodePublicKey(hexPubkey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return npub, nil
|
||||
}
|
||||
271
irc/nostr/relay.go
Normal file
271
irc/nostr/relay.go
Normal file
@@ -0,0 +1,271 @@
|
||||
// Copyright (c) 2024 Ergo Contributors
|
||||
// released under the MIT license
|
||||
|
||||
package nostr
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
)
|
||||
|
||||
// RelayConfig holds configuration for relay operations
|
||||
type RelayConfig struct {
|
||||
DefaultRelays []string
|
||||
Timeout time.Duration
|
||||
MaxRelays int
|
||||
}
|
||||
|
||||
// RelayInfo represents the NIP-11 relay information document
|
||||
type RelayInfo struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
PubKey string `json:"pubkey"`
|
||||
Contact string `json:"contact"`
|
||||
Limitation struct {
|
||||
AuthRequired bool `json:"auth_required"`
|
||||
} `json:"limitation"`
|
||||
}
|
||||
|
||||
// DiscoverInboxRelays discovers a user's inbox relays using NIP-65
|
||||
func DiscoverInboxRelays(pubkey string, config RelayConfig) ([]string, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), config.Timeout)
|
||||
defer cancel()
|
||||
|
||||
// Try to get relays from NIP-65 (kind 10002 events)
|
||||
inboxRelays, err := queryRelayListMetadata(ctx, pubkey, config)
|
||||
if err == nil && len(inboxRelays) > 0 {
|
||||
return inboxRelays, nil
|
||||
}
|
||||
|
||||
// Fallback to default relays if no inbox relays found
|
||||
if len(config.DefaultRelays) > 0 {
|
||||
return config.DefaultRelays, nil
|
||||
}
|
||||
|
||||
return nil, ErrNoInboxRelaysFound
|
||||
}
|
||||
|
||||
// queryRelayListMetadata queries for NIP-65 relay list metadata events
|
||||
func queryRelayListMetadata(ctx context.Context, pubkey string, config RelayConfig) ([]string, error) {
|
||||
var inboxRelays []string
|
||||
|
||||
// Connect to default relays to query for relay list metadata
|
||||
for _, relayURL := range config.DefaultRelays {
|
||||
relay, err := nostr.RelayConnect(ctx, relayURL)
|
||||
if err != nil {
|
||||
continue // Try next relay
|
||||
}
|
||||
|
||||
// Query for kind 10002 events (NIP-65 relay list metadata)
|
||||
filters := []nostr.Filter{{
|
||||
Authors: []string{pubkey},
|
||||
Kinds: []int{10002},
|
||||
Limit: 1,
|
||||
}}
|
||||
|
||||
sub, err := relay.Subscribe(ctx, filters)
|
||||
if err != nil {
|
||||
relay.Close()
|
||||
continue
|
||||
}
|
||||
|
||||
// Wait for events with timeout
|
||||
done := false
|
||||
select {
|
||||
case event := <-sub.Events:
|
||||
relays := parseRelayListEvent(*event)
|
||||
inboxRelays = append(inboxRelays, relays...)
|
||||
case <-time.After(5 * time.Second):
|
||||
// Timeout waiting for event
|
||||
case <-ctx.Done():
|
||||
done = true
|
||||
}
|
||||
|
||||
sub.Unsub()
|
||||
relay.Close()
|
||||
|
||||
if done {
|
||||
break
|
||||
}
|
||||
|
||||
if len(inboxRelays) > 0 {
|
||||
break // Found relays, no need to query more
|
||||
}
|
||||
}
|
||||
|
||||
if len(inboxRelays) == 0 {
|
||||
return nil, ErrRelayDiscoveryFailed
|
||||
}
|
||||
|
||||
return inboxRelays, nil
|
||||
}
|
||||
|
||||
// parseRelayListEvent parses a NIP-65 relay list metadata event
|
||||
func parseRelayListEvent(event nostr.Event) []string {
|
||||
var inboxRelays []string
|
||||
|
||||
for _, tag := range event.Tags {
|
||||
if len(tag) >= 2 && tag[0] == "r" {
|
||||
relayURL := tag[1]
|
||||
|
||||
// Check if this is an inbox relay (read capability)
|
||||
// If no marker is specified, assume both read and write
|
||||
isInbox := true
|
||||
if len(tag) >= 3 {
|
||||
marker := tag[2]
|
||||
isInbox = marker == "read" || marker == ""
|
||||
}
|
||||
|
||||
if isInbox {
|
||||
inboxRelays = append(inboxRelays, relayURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return inboxRelays
|
||||
}
|
||||
|
||||
// checkRelayRequiresAuth checks if a relay requires authentication via NIP-11
|
||||
func checkRelayRequiresAuth(url string) bool {
|
||||
httpURL := strings.Replace(strings.Replace(url, "ws://", "http://", 1), "wss://", "https://", 1)
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: time.Second * 5,
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", httpURL, nil)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/nostr+json")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var info RelayInfo
|
||||
if err := json.NewDecoder(resp.Body).Decode(&info); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return info.Limitation.AuthRequired
|
||||
}
|
||||
|
||||
// ConnectToRelay establishes a connection to a nostr relay with optional NIP-42 auth
|
||||
func ConnectToRelay(ctx context.Context, url string, privkey string) (*nostr.Relay, error) {
|
||||
log.Printf("Connecting to relay: %s\n", url)
|
||||
|
||||
relay, err := nostr.RelayConnect(ctx, url)
|
||||
if err != nil {
|
||||
log.Printf("Failed to connect to relay: %s, error: %v\n", url, err)
|
||||
return nil, fmt.Errorf("%w: %v", ErrRelayConnectionFailed, err)
|
||||
}
|
||||
|
||||
log.Printf("Connected to relay: %s\n", url)
|
||||
|
||||
// Check if relay requires auth before attempting authentication
|
||||
if privkey != "" && checkRelayRequiresAuth(url) {
|
||||
log.Printf("Relay requires authentication: %s\n", url)
|
||||
|
||||
err = relay.Auth(ctx, func(authEvent *nostr.Event) error {
|
||||
// Validate challenge tag is present and not empty
|
||||
challengeTag := authEvent.Tags.Find("challenge")
|
||||
if len(challengeTag) < 2 || challengeTag[1] == "" || challengeTag[1] == " " {
|
||||
return fmt.Errorf("invalid or missing challenge in auth event")
|
||||
}
|
||||
return authEvent.Sign(privkey)
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("Failed to authenticate with relay: %s, error: %v\n", url, err)
|
||||
// Don't fail connection on auth error - some operations might still work
|
||||
// The relay will reject operations that require auth if needed
|
||||
} else {
|
||||
log.Printf("Authenticated with relay: %s\n", url)
|
||||
}
|
||||
}
|
||||
|
||||
return relay, nil
|
||||
}
|
||||
|
||||
// SendEventToRelay sends a nostr event to a relay
|
||||
func SendEventToRelay(ctx context.Context, relay *nostr.Relay, event nostr.Event) error {
|
||||
err := relay.Publish(ctx, event)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %v", ErrDMSendFailed, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckPrivateRelays checks if a user has private relays (NIP-50 kind 10050)
|
||||
func CheckPrivateRelays(ctx context.Context, pubkey string, config RelayConfig) ([]string, bool, error) {
|
||||
var privateRelays []string
|
||||
|
||||
// Connect to default relays to query for private relay list
|
||||
for _, relayURL := range config.DefaultRelays {
|
||||
relay, err := nostr.RelayConnect(ctx, relayURL)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Query for kind 10050 events (private relay list)
|
||||
filters := []nostr.Filter{{
|
||||
Authors: []string{pubkey},
|
||||
Kinds: []int{10050},
|
||||
Limit: 1,
|
||||
}}
|
||||
|
||||
sub, err := relay.Subscribe(ctx, filters)
|
||||
if err != nil {
|
||||
relay.Close()
|
||||
continue
|
||||
}
|
||||
|
||||
// Wait for events
|
||||
done := false
|
||||
select {
|
||||
case event := <-sub.Events:
|
||||
privateRelays = parsePrivateRelayList(*event)
|
||||
case <-time.After(5 * time.Second):
|
||||
// Timeout
|
||||
case <-ctx.Done():
|
||||
done = true
|
||||
}
|
||||
|
||||
sub.Unsub()
|
||||
relay.Close()
|
||||
|
||||
if done {
|
||||
break
|
||||
}
|
||||
|
||||
if len(privateRelays) > 0 {
|
||||
return privateRelays, true, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
// parsePrivateRelayList parses a NIP-50 private relay list event
|
||||
func parsePrivateRelayList(event nostr.Event) []string {
|
||||
var privateRelays []string
|
||||
|
||||
for _, tag := range event.Tags {
|
||||
if len(tag) >= 2 && tag[0] == "relay" {
|
||||
privateRelays = append(privateRelays, tag[1])
|
||||
}
|
||||
}
|
||||
|
||||
return privateRelays
|
||||
}
|
||||
@@ -103,6 +103,7 @@ type Server struct {
|
||||
apiHandler http.Handler // always initialized
|
||||
apiListener *utils.ReloadableListener
|
||||
apiServer *http.Server // nil if API is not enabled
|
||||
|
||||
}
|
||||
|
||||
// NewServer returns a new Oragono server.
|
||||
@@ -135,6 +136,7 @@ func NewServer(config *Config, logger *logger.Manager) (*Server, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
// Attempt to clean up when receiving these signals.
|
||||
signal.Notify(server.exitSignals, utils.ServerExitSignals...)
|
||||
signal.Notify(server.rehashSignal, syscall.SIGHUP)
|
||||
@@ -161,6 +163,7 @@ func (server *Server) Shutdown() {
|
||||
// flush data associated with always-on clients:
|
||||
server.performAlwaysOnMaintenance(false, true)
|
||||
|
||||
|
||||
if err := server.store.Close(); err != nil {
|
||||
server.logger.Error("shutdown", fmt.Sprintln("Could not close datastore:", err))
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
"PONG <args>...\n\nReplies to a PING. Used to check link connectivity.": "PONG <args>...\n\nReplies to a PING. Used to check link connectivity.",
|
||||
"PRIVMSG <target>{,<target>} <text to be sent>\n\nSends the text to the given targets as a PRIVMSG.": "PRIVMSG <target>{,<target>} <text to be sent>\n\nSends the text to the given targets as a PRIVMSG.",
|
||||
"QUIT [reason]\n\nIndicates that you're leaving the server, and shows everyone the given reason.": "QUIT [reason]\n\nIndicates that you're leaving the server, and shows everyone the given reason.",
|
||||
"REGISTER <email | *> <password>\n\nRegisters an account in accordance with the draft/register capability.": "REGISTER <email | *> <password>\n\nRegisters an account in accordance with the draft/register capability.",
|
||||
"REGISTER <nostr-identifier | *> <password>\n\nRegisters an account using nostr verification. The nostr-identifier can be:\n• NIP-05 address: alice@example.com\n• npub key: npub1abc123def456...\n• hex pubkey: 3bf0c63fcb93c5ef2f068d70b8d70d963b649d75...\n\nUse * if you want to register without nostr verification.": "REGISTER <nostr-identifier | *> <password>\n\nRegisters an account using nostr verification. The nostr-identifier can be:\n• NIP-05 address: alice@example.com\n• npub key: npub1abc123def456...\n• hex pubkey: 3bf0c63fcb93c5ef2f068d70b8d70d963b649d75...\n\nUse * if you want to register without nostr verification.",
|
||||
"REHASH\n\nReloads the config file and updates TLS certificates on listeners": "REHASH\n\nReloads the config file and updates TLS certificates on listeners",
|
||||
"RELAYMSG <channel> <spoofed nick> :<message>\n\nThis command lets channel operators relay messages to their\nchannel from other messaging systems using relay bots. The\nspoofed nickname MUST contain a forwardslash.\n\nFor example:\n\tRELAYMSG #ircv3 Mallory/D :Welp, we linked Discord...": "RELAYMSG <channel> <spoofed nick> :<message>\n\nThis command lets channel operators relay messages to their\nchannel from other messaging systems using relay bots. The\nspoofed nickname MUST contain a forwardslash.\n\nFor example:\n\tRELAYMSG #ircv3 Mallory/D :Welp, we linked Discord...",
|
||||
"RENAME <channel> <newname> [<reason>]\n\nRenames the given channel with the given reason, if possible.\n\nFor example:\n\tRENAME #ircv2 #ircv3 :Protocol upgrades!": "RENAME <channel> <newname> [<reason>]\n\nRenames the given channel with the given reason, if possible.\n\nFor example:\n\tRENAME #ircv2 #ircv3 :Protocol upgrades!",
|
||||
@@ -64,7 +64,7 @@
|
||||
"USER <username> 0 * <realname>\n\nUsed in connection registration, sets your username and realname to the given\nvalues (though your username may also be looked up with Ident).": "USER <username> 0 * <realname>\n\nUsed in connection registration, sets your username and realname to the given\nvalues (though your username may also be looked up with Ident).",
|
||||
"USERHOST <nickname>{ <nickname>}\n\t\t\nShows information about the given users. Takes up to 10 nicknames.": "USERHOST <nickname>{ <nickname>}\n\t\t\nShows information about the given users. Takes up to 10 nicknames.",
|
||||
"USERS [parameters]\n\nThe USERS command is not implemented.": "USERS [parameters]\n\nThe USERS command is not implemented.",
|
||||
"VERIFY <account> <password>\n\nVerifies an account in accordance with the draft/register capability.": "VERIFY <account> <password>\n\nVerifies an account in accordance with the draft/register capability.",
|
||||
"VERIFY <account> <code>\n\nVerifies an account using the verification code sent via nostr DM.": "VERIFY <account> <code>\n\nVerifies an account using the verification code sent via nostr DM.",
|
||||
"VERSION [server]\n\nViews the version of software and the RPL_ISUPPORT tokens for the given server.": "VERSION [server]\n\nViews the version of software and the RPL_ISUPPORT tokens for the given server.",
|
||||
"WEBIRC <password> <gateway> <hostname> <ip> [:<flags>]\n\nUsed by web<->IRC gateways and bouncers, the WEBIRC command allows gateways to\npass-through the real IP addresses of clients:\nircv3.net/specs/extensions/webirc.html\n\n<flags> is a list of space-separated strings indicating various details about\nthe connection from the client to the gateway, such as:\n\n- tls: this flag indicates that the client->gateway connection is secure": "WEBIRC <password> <gateway> <hostname> <ip> [:<flags>]\n\nUsed by web<->IRC gateways and bouncers, the WEBIRC command allows gateways to\npass-through the real IP addresses of clients:\nircv3.net/specs/extensions/webirc.html\n\n<flags> is a list of space-separated strings indicating various details about\nthe connection from the client to the gateway, such as:\n\n- tls: this flag indicates that the client->gateway connection is secure",
|
||||
"WHO <name> [o]\n\nReturns information for the given user.": "WHO <name> [o]\n\nReturns information for the given user.",
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"$bCLIENTS$b can list and logout the sessions attached to a nickname.": "$bCLIENTS$b can list and logout the sessions attached to a nickname.",
|
||||
"$bDM-HISTORY$b\n'dm-history' is only effective for always-on clients. It lets you control\nhow the history of your direct messages is stored. Your options are:\n1. 'off' [no history]\n2. 'ephemeral' [a limited amount of temporary history, not stored on disk]\n3. 'on' [history stored in a permanent database, if available]\n4. 'default' [use the server default]": "$bDM-HISTORY$b\n'dm-history' is only effective for always-on clients. It lets you control\nhow the history of your direct messages is stored. Your options are:\n1. 'off' [no history]\n2. 'ephemeral' [a limited amount of temporary history, not stored on disk]\n3. 'on' [history stored in a permanent database, if available]\n4. 'default' [use the server default]",
|
||||
"$bDROP$b de-links your current (or the given) nickname from your user account.": "$bDROP$b de-links your current (or the given) nickname from your user account.",
|
||||
"$bEMAIL$b\n'email' controls the e-mail address associated with your account (if the\nserver operator allows it, this address can be used for password resets).\nAs an additional security measure, if you have a password set, you must\nprovide it as an additional argument to $bSET$b, for example,\nSET EMAIL test@example.com hunter2": "$bEMAIL$b\n'email' controls the e-mail address associated with your account (if the\nserver operator allows it, this address can be used for password resets).\nAs an additional security measure, if you have a password set, you must\nprovide it as an additional argument to $bSET$b, for example,\nSET EMAIL test@example.com hunter2",
|
||||
"$bNOSTR$b\n'nostr' controls the nostr identifier associated with your account.\nThis can be a NIP-05 address, npub key, or hex pubkey. As an additional\nsecurity measure, if you have a password set, you must provide it as an\nadditional argument to $bSET$b, for example,\nSET NOSTR alice@example.com hunter2": "$bNOSTR$b\n'nostr' controls the nostr identifier associated with your account.\nThis can be a NIP-05 address, npub key, or hex pubkey. As an additional\nsecurity measure, if you have a password set, you must provide it as an\nadditional argument to $bSET$b, for example,\nSET NOSTR alice@example.com hunter2",
|
||||
"$bENFORCE$b\n'enforce' lets you specify a custom enforcement mechanism for your registered\nnicknames. Your options are:\n1. 'none' [no enforcement, overriding the server default]\n2. 'strict' [you must already be authenticated to use the nick]\n3. 'default' [use the server default]": "$bENFORCE$b\n'enforce' lets you specify a custom enforcement mechanism for your registered\nnicknames. Your options are:\n1. 'none' [no enforcement, overriding the server default]\n2. 'strict' [you must already be authenticated to use the nick]\n3. 'default' [use the server default]",
|
||||
"$bERASE$b erases all records of an account, allowing reuse.": "$bERASE$b erases all records of an account, allowing reuse.",
|
||||
"$bGET$b queries the current values of your account settings": "$bGET$b queries the current values of your account settings",
|
||||
@@ -18,15 +18,15 @@
|
||||
"$bLIST$b searches the list of registered nicknames.": "$bLIST$b searches the list of registered nicknames.",
|
||||
"$bMULTICLIENT$b\nIf 'multiclient' is enabled and you are already logged in and using a nick, a\nsecond client of yours that authenticates with SASL and requests the same nick\nis allowed to attach to the nick as well (this is comparable to the behavior\nof IRC \"bouncers\" like ZNC). Your options are 'on' (allow this behavior),\n'off' (disallow it), and 'default' (use the server default value).": "$bMULTICLIENT$b\nIf 'multiclient' is enabled and you are already logged in and using a nick, a\nsecond client of yours that authenticates with SASL and requests the same nick\nis allowed to attach to the nick as well (this is comparable to the behavior\nof IRC \"bouncers\" like ZNC). Your options are 'on' (allow this behavior),\n'off' (disallow it), and 'default' (use the server default value).",
|
||||
"$bPASSWD$b lets you change your password.": "$bPASSWD$b lets you change your password.",
|
||||
"$bREGISTER$b lets you register a user account.": "$bREGISTER$b lets you register a user account.",
|
||||
"$bREGISTER$b lets you register a user account.": "$bREGISTER$b lets you register a user account with nostr verification.",
|
||||
"$bRENAME$b renames an account": "$bRENAME$b renames an account",
|
||||
"$bREPLAY-JOINS$b\n'replay-joins' controls whether replayed channel history will include\nlines for join and part. This provides more information about the context of\nmessages, but may be spammy. Your options are 'always' and the default of\n'commands-only' (the messages will be replayed in CHATHISTORY output, but not\nduring autoreplay).": "$bREPLAY-JOINS$b\n'replay-joins' controls whether replayed channel history will include\nlines for join and part. This provides more information about the context of\nmessages, but may be spammy. Your options are 'always' and the default of\n'commands-only' (the messages will be replayed in CHATHISTORY output, but not\nduring autoreplay).",
|
||||
"$bRESETPASS$b completes an email-based password reset": "$bRESETPASS$b completes an email-based password reset",
|
||||
"$bRESETPASS$b completes a nostr-based password reset": "$bRESETPASS$b completes a nostr-based password reset",
|
||||
"$bSADROP$b forcibly de-links the given nickname from its user account.": "$bSADROP$b forcibly de-links the given nickname from its user account.",
|
||||
"$bSAGET$b queries the current values of another user's account settings": "$bSAGET$b queries the current values of another user's account settings",
|
||||
"$bSAREGISTER$b registers an account on someone else's behalf.": "$bSAREGISTER$b registers an account on someone else's behalf.",
|
||||
"$bSASET$b modifies another user's account settings": "$bSASET$b modifies another user's account settings",
|
||||
"$bSENDPASS$b initiates an email-based password reset": "$bSENDPASS$b initiates an email-based password reset",
|
||||
"$bSENDPASS$b initiates a nostr-based password reset": "$bSENDPASS$b initiates a nostr-based password reset",
|
||||
"$bSET$b modifies your account settings": "$bSET$b modifies your account settings",
|
||||
"$bSUSPEND$b manages account suspensions": "$bSUSPEND$b manages account suspensions",
|
||||
"$bUNREGISTER$b lets you delete your user account.": "$bUNREGISTER$b lets you delete your user account.",
|
||||
@@ -39,7 +39,7 @@
|
||||
"Password incorrect": "Password incorrect",
|
||||
"Passwords do not match": "Passwords do not match",
|
||||
"Successfully reset account password": "Successfully reset account password",
|
||||
"Successfully sent password reset email": "Successfully sent password reset email",
|
||||
"Successfully sent password reset via nostr DM": "Successfully sent password reset via nostr DM",
|
||||
"Syntax $bSET <setting> <value>$b\n\nSET modifies your account settings. The following settings are available:": "Syntax $bSET <setting> <value>$b\n\nSET modifies your account settings. The following settings are available:",
|
||||
"Syntax: $bCERT <LIST | ADD | DEL> [account] [certfp]$b\n\nCERT examines or modifies the SHA-256 TLS certificate fingerprints that can\nbe used to log into an account. Specifically, $bCERT LIST$b lists the\nauthorized fingerprints, $bCERT ADD <fingerprint>$b adds a new fingerprint, and\n$bCERT DEL <fingerprint>$b removes a fingerprint. If you're an IRC operator\nwith the correct permissions, you can act on another user's account, for\nexample with $bCERT ADD <account> <fingerprint>$b. See the operator manual\nfor instructions on how to compute the fingerprint.": "Syntax: $bCERT <LIST | ADD | DEL> [account] [certfp]$b\n\nCERT examines or modifies the SHA-256 TLS certificate fingerprints that can\nbe used to log into an account. Specifically, $bCERT LIST$b lists the\nauthorized fingerprints, $bCERT ADD <fingerprint>$b adds a new fingerprint, and\n$bCERT DEL <fingerprint>$b removes a fingerprint. If you're an IRC operator\nwith the correct permissions, you can act on another user's account, for\nexample with $bCERT ADD <account> <fingerprint>$b. See the operator manual\nfor instructions on how to compute the fingerprint.",
|
||||
"Syntax: $bCLIENTS LIST [nickname]$b\n\nCLIENTS LIST shows information about the clients currently attached, via\nthe server's multiclient functionality, to your nickname. An administrator\ncan use this command to list another user's clients.\n\nSyntax: $bCLIENTS LOGOUT [nickname] [client_id/all]$b\n\nCLIENTS LOGOUT detaches a single client, or all clients currently attached\nto your nickname. An administrator can use this command to logout another\nuser's clients.": "Syntax: $bCLIENTS LIST [nickname]$b\n\nCLIENTS LIST shows information about the clients currently attached, via\nthe server's multiclient functionality, to your nickname. An administrator\ncan use this command to list another user's clients.\n\nSyntax: $bCLIENTS LOGOUT [nickname] [client_id/all]$b\n\nCLIENTS LOGOUT detaches a single client, or all clients currently attached\nto your nickname. An administrator can use this command to logout another\nuser's clients.",
|
||||
@@ -53,19 +53,19 @@
|
||||
"Syntax: $bINFO [username]$b\n\nINFO gives you information about the given (or your own) user account.": "Syntax: $bINFO [username]$b\n\nINFO gives you information about the given (or your own) user account.",
|
||||
"Syntax: $bLIST [regex]$b\n\nLIST returns the list of registered nicknames, which match the given regex.\nIf no regex is provided, all registered nicknames are returned.": "Syntax: $bLIST [regex]$b\n\nLIST returns the list of registered nicknames, which match the given regex.\nIf no regex is provided, all registered nicknames are returned.",
|
||||
"Syntax: $bPASSWD <current> <new> <new_again>$b\nOr: $bPASSWD <username> <new>$b\n\nPASSWD lets you change your account password. You must supply your current\npassword and confirm the new one by typing it twice. If you're an IRC operator\nwith the correct permissions, you can use PASSWD to reset someone else's\npassword by supplying their username and then the desired password. To\nindicate an empty password, use * instead.": "Syntax: $bPASSWD <current> <new> <new_again>$b\nOr: $bPASSWD <username> <new>$b\n\nPASSWD lets you change your account password. You must supply your current\npassword and confirm the new one by typing it twice. If you're an IRC operator\nwith the correct permissions, you can use PASSWD to reset someone else's\npassword by supplying their username and then the desired password. To\nindicate an empty password, use * instead.",
|
||||
"Syntax: $bREGISTER <password> [email]$b\n\nREGISTER lets you register your current nickname as a user account. If the\nserver allows anonymous registration, you can omit the e-mail address.\n\nIf you are currently logged in with a TLS client certificate and wish to use\nit instead of a password to log in, send * as the password.": "Syntax: $bREGISTER <password> [email]$b\n\nREGISTER lets you register your current nickname as a user account. If the\nserver allows anonymous registration, you can omit the e-mail address.\n\nIf you are currently logged in with a TLS client certificate and wish to use\nit instead of a password to log in, send * as the password.",
|
||||
"Syntax: $bREGISTER <password> [nostr-identifier]$b\n\nREGISTER lets you register your current nickname as a user account using nostr\nverification. You can provide a nostr identifier in one of these formats:\n\n• NIP-05 address: alice@example.com\n• npub key: npub1abc123def456...\n• hex pubkey: 3bf0c63fcb93c5ef2f068d70b8d70d963b649d75...\n\nIf the server allows anonymous registration, you can omit the nostr identifier.\nIf you are currently logged in with a TLS client certificate and wish to use\nit instead of a password to log in, send * as the password.": "Syntax: $bREGISTER <password> [nostr-identifier]$b\n\nREGISTER lets you register your current nickname as a user account using nostr\nverification. You can provide a nostr identifier in one of these formats:\n\n• NIP-05 address: alice@example.com\n• npub key: npub1abc123def456...\n• hex pubkey: 3bf0c63fcb93c5ef2f068d70b8d70d963b649d75...\n\nIf the server allows anonymous registration, you can omit the nostr identifier.\nIf you are currently logged in with a TLS client certificate and wish to use\nit instead of a password to log in, send * as the password.",
|
||||
"Syntax: $bRENAME <account> <newname>$b\n\nRENAME allows a server administrator to change the name of an account.\nCurrently, you can only change the canonical casefolding of an account\n(e.g., you can change \"Alice\" to \"alice\", but not \"Alice\" to \"Amanda\").": "Syntax: $bRENAME <account> <newname>$b\n\nRENAME allows a server administrator to change the name of an account.\nCurrently, you can only change the canonical casefolding of an account\n(e.g., you can change \"Alice\" to \"alice\", but not \"Alice\" to \"Amanda\").",
|
||||
"Syntax: $bRESETPASS <account> <code> <password>$b\n\nRESETPASS resets an account password, using a reset code that was emailed as\nthe result of a previous $bSENDPASS$b command.": "Syntax: $bRESETPASS <account> <code> <password>$b\n\nRESETPASS resets an account password, using a reset code that was emailed as\nthe result of a previous $bSENDPASS$b command.",
|
||||
"Syntax: $bRESETPASS <account> <code> <password>$b\n\nRESETPASS resets an account password, using a reset code that was sent via\nnostr DM as the result of a previous $bSENDPASS$b command.": "Syntax: $bRESETPASS <account> <code> <password>$b\n\nRESETPASS resets an account password, using a reset code that was sent via\nnostr DM as the result of a previous $bSENDPASS$b command.",
|
||||
"Syntax: $bSADROP <nickname>$b\n\nSADROP forcibly de-links the given nickname from the attached user account.": "Syntax: $bSADROP <nickname>$b\n\nSADROP forcibly de-links the given nickname from the attached user account.",
|
||||
"Syntax: $bSAGET <account> <setting>$b\n\nSAGET queries the values of someone else's account settings. For more\ninformation on the settings and their possible values, see HELP SET.": "Syntax: $bSAGET <account> <setting>$b\n\nSAGET queries the values of someone else's account settings. For more\ninformation on the settings and their possible values, see HELP SET.",
|
||||
"Syntax: $bSAREGISTER <username> [password]$b\n\nSAREGISTER registers an account on someone else's behalf.\nThis is for use in configurations that require SASL for all connections;\nan administrator can set use this command to set up user accounts.": "Syntax: $bSAREGISTER <username> [password]$b\n\nSAREGISTER registers an account on someone else's behalf.\nThis is for use in configurations that require SASL for all connections;\nan administrator can set use this command to set up user accounts.",
|
||||
"Syntax: $bSASET <account> <setting> <value>$b\n\nSASET modifies the values of someone else's account settings. For more\ninformation on the settings and their possible values, see HELP SET.": "Syntax: $bSASET <account> <setting> <value>$b\n\nSASET modifies the values of someone else's account settings. For more\ninformation on the settings and their possible values, see HELP SET.",
|
||||
"Syntax: $bSENDPASS <account>$b\n\nSENDPASS sends a password reset email to the email address associated with\nthe target account. The reset code in the email can then be used with the\n$bRESETPASS$b command.": "Syntax: $bSENDPASS <account>$b\n\nSENDPASS sends a password reset email to the email address associated with\nthe target account. The reset code in the email can then be used with the\n$bRESETPASS$b command.",
|
||||
"Syntax: $bSENDPASS <account>$b\n\nSENDPASS sends a password reset code via nostr DM to the nostr identifier\nassociated with the target account. The reset code can then be used with the\n$bRESETPASS$b command.": "Syntax: $bSENDPASS <account>$b\n\nSENDPASS sends a password reset code via nostr DM to the nostr identifier\nassociated with the target account. The reset code can then be used with the\n$bRESETPASS$b command.",
|
||||
"Syntax: $bSESSIONS [nickname]$b\n\nSESSIONS is an alias for $bCLIENTS LIST$b. See the help entry for $bCLIENTS$b\nfor more information.": "Syntax: $bSESSIONS [nickname]$b\n\nSESSIONS is an alias for $bCLIENTS LIST$b. See the help entry for $bCLIENTS$b\nfor more information.",
|
||||
"Syntax: $bSUSPEND ADD <nickname> [DURATION duration] [reason]$b\n $bSUSPEND DEL <nickname>$b\n $bSUSPEND LIST$b\n\nSuspending an account disables it (preventing new logins) and disconnects\nall associated clients. You can specify a time limit or a reason for\nthe suspension. The $bDEL$b subcommand reverses a suspension, and the $bLIST$b\ncommand lists all current suspensions.": "Syntax: $bSUSPEND ADD <nickname> [DURATION duration] [reason]$b\n $bSUSPEND DEL <nickname>$b\n $bSUSPEND LIST$b\n\nSuspending an account disables it (preventing new logins) and disconnects\nall associated clients. You can specify a time limit or a reason for\nthe suspension. The $bDEL$b subcommand reverses a suspension, and the $bLIST$b\ncommand lists all current suspensions.",
|
||||
"Syntax: $bUNREGISTER <username> [code]$b\n\nUNREGISTER lets you delete your user account (or someone else's, if you're an\nIRC operator with the correct permissions). To prevent accidental\nunregistrations, a verification code is required; invoking the command without\na code will display the necessary code.": "Syntax: $bUNREGISTER <username> [code]$b\n\nUNREGISTER lets you delete your user account (or someone else's, if you're an\nIRC operator with the correct permissions). To prevent accidental\nunregistrations, a verification code is required; invoking the command without\na code will display the necessary code.",
|
||||
"Syntax: $bVERIFY <username> <code>$b\n\nVERIFY lets you complete an account registration, if the server requires email\nor other verification.": "Syntax: $bVERIFY <username> <code>$b\n\nVERIFY lets you complete an account registration, if the server requires email\nor other verification.",
|
||||
"That account is not associated with an email address": "That account is not associated with an email address",
|
||||
"Syntax: $bVERIFY <username> <code>$b\n\nVERIFY lets you complete an account registration, if the server requires nostr\nverification. You will receive a verification code via nostr DM.": "Syntax: $bVERIFY <username> <code>$b\n\nVERIFY lets you complete an account registration, if the server requires nostr\nverification. You will receive a verification code via nostr DM.",
|
||||
"That account is not associated with a nostr identifier": "That account is not associated with a nostr identifier",
|
||||
"Try again later": "Try again later",
|
||||
"You must supply a password": "You must supply a password",
|
||||
"You're not logged into an account": "You're not logged into an account"
|
||||
|
||||
43
vendor/github.com/tidwall/gjson/README.md
generated
vendored
43
vendor/github.com/tidwall/gjson/README.md
generated
vendored
@@ -1,7 +1,9 @@
|
||||
<p align="center">
|
||||
<img
|
||||
src="logo.png"
|
||||
width="240" height="78" border="0" alt="GJSON">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="/.github/images/logo-dark.png">
|
||||
<source media="(prefers-color-scheme: light)" srcset="/.github/images/logo-light.png">
|
||||
<img src="/.github/images/logo-light.png" width="240" alt="GJSON" >
|
||||
</picture>
|
||||
<br>
|
||||
<a href="https://godoc.org/github.com/tidwall/gjson"><img src="https://img.shields.io/badge/api-reference-blue.svg?style=flat-square" alt="GoDoc"></a>
|
||||
<a href="https://tidwall.com/gjson-play"><img src="https://img.shields.io/badge/%F0%9F%8F%90-playground-9900cc.svg?style=flat-square" alt="GJSON Playground"></a>
|
||||
@@ -176,7 +178,7 @@ The `result.Int()` and `result.Uint()` calls are capable of reading all 64 bits,
|
||||
|
||||
```go
|
||||
result.Int() int64 // -9223372036854775808 to 9223372036854775807
|
||||
result.Uint() int64 // 0 to 18446744073709551615
|
||||
result.Uint() uint64 // 0 to 18446744073709551615
|
||||
```
|
||||
|
||||
## Modifiers and path chaining
|
||||
@@ -211,6 +213,7 @@ There are currently the following built-in modifiers:
|
||||
- `@tostr`: Converts json to a string. Wraps a json string.
|
||||
- `@fromstr`: Converts a string from json. Unwraps a json string.
|
||||
- `@group`: Groups arrays of objects. See [e4fc67c](https://github.com/tidwall/gjson/commit/e4fc67c92aeebf2089fabc7872f010e340d105db).
|
||||
- `@dig`: Search for a value without providing its entire path. See [e8e87f2](https://github.com/tidwall/gjson/commit/e8e87f2a00dc41f3aba5631094e21f59a8cf8cbf).
|
||||
|
||||
### Modifier arguments
|
||||
|
||||
@@ -426,16 +429,6 @@ if result.Index > 0 {
|
||||
|
||||
This is a best-effort no allocation sub slice of the original json. This method utilizes the `result.Index` field, which is the position of the raw data in the original json. It's possible that the value of `result.Index` equals zero, in which case the `result.Raw` is converted to a `[]byte`.
|
||||
|
||||
## Get multiple values at once
|
||||
|
||||
The `GetMany` function can be used to get multiple values at the same time.
|
||||
|
||||
```go
|
||||
results := gjson.GetMany(json, "name.first", "name.last", "age")
|
||||
```
|
||||
|
||||
The return value is a `[]Result`, which will always contain exactly the same number of items as the input paths.
|
||||
|
||||
## Performance
|
||||
|
||||
Benchmarks of GJSON alongside [encoding/json](https://golang.org/pkg/encoding/json/),
|
||||
@@ -445,15 +438,15 @@ Benchmarks of GJSON alongside [encoding/json](https://golang.org/pkg/encoding/js
|
||||
and [json-iterator](https://github.com/json-iterator/go)
|
||||
|
||||
```
|
||||
BenchmarkGJSONGet-16 11644512 311 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkGJSONUnmarshalMap-16 1122678 3094 ns/op 1920 B/op 26 allocs/op
|
||||
BenchmarkJSONUnmarshalMap-16 516681 6810 ns/op 2944 B/op 69 allocs/op
|
||||
BenchmarkJSONUnmarshalStruct-16 697053 5400 ns/op 928 B/op 13 allocs/op
|
||||
BenchmarkJSONDecoder-16 330450 10217 ns/op 3845 B/op 160 allocs/op
|
||||
BenchmarkFFJSONLexer-16 1424979 2585 ns/op 880 B/op 8 allocs/op
|
||||
BenchmarkEasyJSONLexer-16 3000000 729 ns/op 501 B/op 5 allocs/op
|
||||
BenchmarkJSONParserGet-16 3000000 366 ns/op 21 B/op 0 allocs/op
|
||||
BenchmarkJSONIterator-16 3000000 869 ns/op 693 B/op 14 allocs/op
|
||||
BenchmarkGJSONGet-10 17893731 202.1 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkGJSONUnmarshalMap-10 1663548 2157 ns/op 1920 B/op 26 allocs/op
|
||||
BenchmarkJSONUnmarshalMap-10 832236 4279 ns/op 2920 B/op 68 allocs/op
|
||||
BenchmarkJSONUnmarshalStruct-10 1076475 3219 ns/op 920 B/op 12 allocs/op
|
||||
BenchmarkJSONDecoder-10 585729 6126 ns/op 3845 B/op 160 allocs/op
|
||||
BenchmarkFFJSONLexer-10 2508573 1391 ns/op 880 B/op 8 allocs/op
|
||||
BenchmarkEasyJSONLexer-10 3000000 537.9 ns/op 501 B/op 5 allocs/op
|
||||
BenchmarkJSONParserGet-10 13707510 263.9 ns/op 21 B/op 0 allocs/op
|
||||
BenchmarkJSONIterator-10 3000000 561.2 ns/op 693 B/op 14 allocs/op
|
||||
```
|
||||
|
||||
JSON document used:
|
||||
@@ -494,4 +487,6 @@ widget.image.hOffset
|
||||
widget.text.onMouseUp
|
||||
```
|
||||
|
||||
*These benchmarks were run on a MacBook Pro 16" 2.4 GHz Intel Core i9 using Go 1.17 and can be found [here](https://github.com/tidwall/gjson-benchmarks).*
|
||||
**
|
||||
|
||||
*These benchmarks were run on a MacBook Pro M1 Max using Go 1.22 and can be found [here](https://github.com/tidwall/gjson-benchmarks).*
|
||||
|
||||
32
vendor/github.com/tidwall/gjson/SYNTAX.md
generated
vendored
32
vendor/github.com/tidwall/gjson/SYNTAX.md
generated
vendored
@@ -1,6 +1,6 @@
|
||||
# GJSON Path Syntax
|
||||
|
||||
A GJSON Path is a text string syntax that describes a search pattern for quickly retreiving values from a JSON payload.
|
||||
A GJSON Path is a text string syntax that describes a search pattern for quickly retrieving values from a JSON payload.
|
||||
|
||||
This document is designed to explain the structure of a GJSON Path through examples.
|
||||
|
||||
@@ -15,12 +15,12 @@ This document is designed to explain the structure of a GJSON Path through examp
|
||||
- [Multipaths](#multipaths)
|
||||
- [Literals](#literals)
|
||||
|
||||
The definitive implemenation is [github.com/tidwall/gjson](https://github.com/tidwall/gjson).
|
||||
The definitive implementation is [github.com/tidwall/gjson](https://github.com/tidwall/gjson).
|
||||
Use the [GJSON Playground](https://gjson.dev) to experiment with the syntax online.
|
||||
|
||||
## Path structure
|
||||
|
||||
A GJSON Path is intended to be easily expressed as a series of components seperated by a `.` character.
|
||||
A GJSON Path is intended to be easily expressed as a series of components separated by a `.` character.
|
||||
|
||||
Along with `.` character, there are a few more that have special meaning, including `|`, `#`, `@`, `\`, `*`, `!`, and `?`.
|
||||
|
||||
@@ -46,7 +46,7 @@ The following GJSON Paths evaluate to the accompanying values.
|
||||
|
||||
### Basic
|
||||
|
||||
In many cases you'll just want to retreive values by object name or array index.
|
||||
In many cases you'll just want to retrieve values by object name or array index.
|
||||
|
||||
```go
|
||||
name.last "Anderson"
|
||||
@@ -137,12 +137,21 @@ next major release.*
|
||||
|
||||
The `~` (tilde) operator will convert a value to a boolean before comparison.
|
||||
|
||||
Supported tilde comparison type are:
|
||||
|
||||
```
|
||||
~true Converts true-ish values to true
|
||||
~false Converts false-ish and non-existent values to true
|
||||
~null Converts null and non-existent values to true
|
||||
~* Converts any existing value to true
|
||||
```
|
||||
|
||||
For example, using the following JSON:
|
||||
|
||||
```json
|
||||
{
|
||||
"vals": [
|
||||
{ "a": 1, "b": true },
|
||||
{ "a": 1, "b": "data" },
|
||||
{ "a": 2, "b": true },
|
||||
{ "a": 3, "b": false },
|
||||
{ "a": 4, "b": "0" },
|
||||
@@ -157,15 +166,23 @@ For example, using the following JSON:
|
||||
}
|
||||
```
|
||||
|
||||
You can now query for all true(ish) or false(ish) values:
|
||||
To query for all true-ish or false-ish values:
|
||||
|
||||
```
|
||||
vals.#(b==~true)#.a >> [1,2,6,7,8]
|
||||
vals.#(b==~true)#.a >> [2,6,7,8]
|
||||
vals.#(b==~false)#.a >> [3,4,5,9,10,11]
|
||||
```
|
||||
|
||||
The last value which was non-existent is treated as `false`
|
||||
|
||||
To query for null and explicit value existence:
|
||||
|
||||
```
|
||||
vals.#(b==~null)#.a >> [10,11]
|
||||
vals.#(b==~*)#.a >> [1,2,3,4,5,6,7,8,9,10]
|
||||
vals.#(b!=~*)#.a >> [11]
|
||||
```
|
||||
|
||||
### Dot vs Pipe
|
||||
|
||||
The `.` is standard separator, but it's also possible to use a `|`.
|
||||
@@ -241,6 +258,7 @@ There are currently the following built-in modifiers:
|
||||
- `@tostr`: Converts json to a string. Wraps a json string.
|
||||
- `@fromstr`: Converts a string from json. Unwraps a json string.
|
||||
- `@group`: Groups arrays of objects. See [e4fc67c](https://github.com/tidwall/gjson/commit/e4fc67c92aeebf2089fabc7872f010e340d105db).
|
||||
- `@dig`: Search for a value without providing its entire path. See [e8e87f2](https://github.com/tidwall/gjson/commit/e8e87f2a00dc41f3aba5631094e21f59a8cf8cbf).
|
||||
|
||||
#### Modifier arguments
|
||||
|
||||
|
||||
498
vendor/github.com/tidwall/gjson/gjson.go
generated
vendored
498
vendor/github.com/tidwall/gjson/gjson.go
generated
vendored
@@ -645,9 +645,9 @@ func tostr(json string) (raw string, str string) {
|
||||
|
||||
// Exists returns true if value exists.
|
||||
//
|
||||
// if gjson.Get(json, "name.last").Exists(){
|
||||
// println("value exists")
|
||||
// }
|
||||
// if gjson.Get(json, "name.last").Exists(){
|
||||
// println("value exists")
|
||||
// }
|
||||
func (t Result) Exists() bool {
|
||||
return t.Type != Null || len(t.Raw) != 0
|
||||
}
|
||||
@@ -661,7 +661,6 @@ func (t Result) Exists() bool {
|
||||
// nil, for JSON null
|
||||
// map[string]interface{}, for JSON objects
|
||||
// []interface{}, for JSON arrays
|
||||
//
|
||||
func (t Result) Value() interface{} {
|
||||
if t.Type == String {
|
||||
return t.Str
|
||||
@@ -826,19 +825,28 @@ func parseArrayPath(path string) (r arrayPathResult) {
|
||||
}
|
||||
|
||||
// splitQuery takes a query and splits it into three parts:
|
||||
// path, op, middle, and right.
|
||||
//
|
||||
// path, op, middle, and right.
|
||||
//
|
||||
// So for this query:
|
||||
// #(first_name=="Murphy").last
|
||||
//
|
||||
// #(first_name=="Murphy").last
|
||||
//
|
||||
// Becomes
|
||||
// first_name # path
|
||||
// =="Murphy" # middle
|
||||
// .last # right
|
||||
//
|
||||
// first_name # path
|
||||
// =="Murphy" # middle
|
||||
// .last # right
|
||||
//
|
||||
// Or,
|
||||
// #(service_roles.#(=="one")).cap
|
||||
//
|
||||
// #(service_roles.#(=="one")).cap
|
||||
//
|
||||
// Becomes
|
||||
// service_roles.#(=="one") # path
|
||||
// # middle
|
||||
// .cap # right
|
||||
//
|
||||
// service_roles.#(=="one") # path
|
||||
// # middle
|
||||
// .cap # right
|
||||
func parseQuery(query string) (
|
||||
path, op, value, remain string, i int, vesc, ok bool,
|
||||
) {
|
||||
@@ -1009,8 +1017,8 @@ func parseObjectPath(path string) (r objectPathResult) {
|
||||
r.piped = true
|
||||
} else {
|
||||
r.path = path[i+1:]
|
||||
r.more = true
|
||||
}
|
||||
r.more = true
|
||||
return
|
||||
} else if path[i] == '|' {
|
||||
r.part = string(epart)
|
||||
@@ -1032,6 +1040,10 @@ func parseObjectPath(path string) (r objectPathResult) {
|
||||
return
|
||||
}
|
||||
|
||||
var vchars = [256]byte{
|
||||
'"': 2, '{': 3, '(': 3, '[': 3, '}': 1, ')': 1, ']': 1,
|
||||
}
|
||||
|
||||
func parseSquash(json string, i int) (int, string) {
|
||||
// expects that the lead character is a '[' or '{' or '('
|
||||
// squash the value, ignoring all nested arrays and objects.
|
||||
@@ -1039,43 +1051,137 @@ func parseSquash(json string, i int) (int, string) {
|
||||
s := i
|
||||
i++
|
||||
depth := 1
|
||||
for ; i < len(json); i++ {
|
||||
if json[i] >= '"' && json[i] <= '}' {
|
||||
switch json[i] {
|
||||
case '"':
|
||||
var c byte
|
||||
for i < len(json) {
|
||||
for i < len(json)-8 {
|
||||
jslice := json[i : i+8]
|
||||
c = vchars[jslice[0]]
|
||||
if c != 0 {
|
||||
i += 0
|
||||
goto token
|
||||
}
|
||||
c = vchars[jslice[1]]
|
||||
if c != 0 {
|
||||
i += 1
|
||||
goto token
|
||||
}
|
||||
c = vchars[jslice[2]]
|
||||
if c != 0 {
|
||||
i += 2
|
||||
goto token
|
||||
}
|
||||
c = vchars[jslice[3]]
|
||||
if c != 0 {
|
||||
i += 3
|
||||
goto token
|
||||
}
|
||||
c = vchars[jslice[4]]
|
||||
if c != 0 {
|
||||
i += 4
|
||||
goto token
|
||||
}
|
||||
c = vchars[jslice[5]]
|
||||
if c != 0 {
|
||||
i += 5
|
||||
goto token
|
||||
}
|
||||
c = vchars[jslice[6]]
|
||||
if c != 0 {
|
||||
i += 6
|
||||
goto token
|
||||
}
|
||||
c = vchars[jslice[7]]
|
||||
if c != 0 {
|
||||
i += 7
|
||||
goto token
|
||||
}
|
||||
i += 8
|
||||
}
|
||||
c = vchars[json[i]]
|
||||
if c == 0 {
|
||||
i++
|
||||
continue
|
||||
}
|
||||
token:
|
||||
if c == 2 {
|
||||
// '"' string
|
||||
i++
|
||||
s2 := i
|
||||
nextquote:
|
||||
for i < len(json)-8 {
|
||||
jslice := json[i : i+8]
|
||||
if jslice[0] == '"' {
|
||||
i += 0
|
||||
goto strchkesc
|
||||
}
|
||||
if jslice[1] == '"' {
|
||||
i += 1
|
||||
goto strchkesc
|
||||
}
|
||||
if jslice[2] == '"' {
|
||||
i += 2
|
||||
goto strchkesc
|
||||
}
|
||||
if jslice[3] == '"' {
|
||||
i += 3
|
||||
goto strchkesc
|
||||
}
|
||||
if jslice[4] == '"' {
|
||||
i += 4
|
||||
goto strchkesc
|
||||
}
|
||||
if jslice[5] == '"' {
|
||||
i += 5
|
||||
goto strchkesc
|
||||
}
|
||||
if jslice[6] == '"' {
|
||||
i += 6
|
||||
goto strchkesc
|
||||
}
|
||||
if jslice[7] == '"' {
|
||||
i += 7
|
||||
goto strchkesc
|
||||
}
|
||||
i += 8
|
||||
}
|
||||
goto strchkstd
|
||||
strchkesc:
|
||||
if json[i-1] != '\\' {
|
||||
i++
|
||||
s2 := i
|
||||
for ; i < len(json); i++ {
|
||||
if json[i] > '\\' {
|
||||
continue
|
||||
}
|
||||
if json[i] == '"' {
|
||||
// look for an escaped slash
|
||||
if json[i-1] == '\\' {
|
||||
n := 0
|
||||
for j := i - 2; j > s2-1; j-- {
|
||||
if json[j] != '\\' {
|
||||
break
|
||||
}
|
||||
n++
|
||||
}
|
||||
if n%2 == 0 {
|
||||
continue
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
case '{', '[', '(':
|
||||
depth++
|
||||
case '}', ']', ')':
|
||||
depth--
|
||||
if depth == 0 {
|
||||
continue
|
||||
}
|
||||
strchkstd:
|
||||
for i < len(json) {
|
||||
if json[i] > '\\' || json[i] != '"' {
|
||||
i++
|
||||
return i, json[s:i]
|
||||
continue
|
||||
}
|
||||
// look for an escaped slash
|
||||
if json[i-1] == '\\' {
|
||||
n := 0
|
||||
for j := i - 2; j > s2-1; j-- {
|
||||
if json[j] != '\\' {
|
||||
break
|
||||
}
|
||||
n++
|
||||
}
|
||||
if n%2 == 0 {
|
||||
i++
|
||||
goto nextquote
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
} else {
|
||||
// '{', '[', '(', '}', ']', ')'
|
||||
// open close tokens
|
||||
depth += int(c) - 2
|
||||
if depth == 0 {
|
||||
i++
|
||||
return i, json[s:i]
|
||||
}
|
||||
}
|
||||
i++
|
||||
}
|
||||
return i, json[s:]
|
||||
}
|
||||
@@ -1244,22 +1350,81 @@ func parseObject(c *parseContext, i int, path string) (int, bool) {
|
||||
}
|
||||
|
||||
// matchLimit will limit the complexity of the match operation to avoid ReDos
|
||||
// attacks from arbritary inputs.
|
||||
// attacks from arbitrary inputs.
|
||||
// See the github.com/tidwall/match.MatchLimit function for more information.
|
||||
func matchLimit(str, pattern string) bool {
|
||||
matched, _ := match.MatchLimit(str, pattern, 10000)
|
||||
return matched
|
||||
}
|
||||
|
||||
func falseish(t Result) bool {
|
||||
switch t.Type {
|
||||
case Null:
|
||||
return true
|
||||
case False:
|
||||
return true
|
||||
case String:
|
||||
b, err := strconv.ParseBool(strings.ToLower(t.Str))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return !b
|
||||
case Number:
|
||||
return t.Num == 0
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func trueish(t Result) bool {
|
||||
switch t.Type {
|
||||
case True:
|
||||
return true
|
||||
case String:
|
||||
b, err := strconv.ParseBool(strings.ToLower(t.Str))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return b
|
||||
case Number:
|
||||
return t.Num != 0
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func nullish(t Result) bool {
|
||||
return t.Type == Null
|
||||
}
|
||||
|
||||
func queryMatches(rp *arrayPathResult, value Result) bool {
|
||||
rpv := rp.query.value
|
||||
if len(rpv) > 0 && rpv[0] == '~' {
|
||||
// convert to bool
|
||||
rpv = rpv[1:]
|
||||
if value.Bool() {
|
||||
value = Result{Type: True}
|
||||
} else {
|
||||
value = Result{Type: False}
|
||||
if len(rpv) > 0 {
|
||||
if rpv[0] == '~' {
|
||||
// convert to bool
|
||||
rpv = rpv[1:]
|
||||
var ish, ok bool
|
||||
switch rpv {
|
||||
case "*":
|
||||
ish, ok = value.Exists(), true
|
||||
case "null":
|
||||
ish, ok = nullish(value), true
|
||||
case "true":
|
||||
ish, ok = trueish(value), true
|
||||
case "false":
|
||||
ish, ok = falseish(value), true
|
||||
}
|
||||
if ok {
|
||||
rpv = "true"
|
||||
if ish {
|
||||
value = Result{Type: True}
|
||||
} else {
|
||||
value = Result{Type: False}
|
||||
}
|
||||
} else {
|
||||
rpv = ""
|
||||
value = Result{}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !value.Exists() {
|
||||
@@ -1850,6 +2015,16 @@ func appendHex16(dst []byte, x uint16) []byte {
|
||||
)
|
||||
}
|
||||
|
||||
// DisableEscapeHTML will disable the automatic escaping of certain
|
||||
// "problamatic" HTML characters when encoding to JSON.
|
||||
// These character include '>', '<' and '&', which get escaped to \u003e,
|
||||
// \u0026, and \u003c respectively.
|
||||
//
|
||||
// This is a global flag and will affect all further gjson operations.
|
||||
// Ideally, if used, it should be set one time before other gjson functions
|
||||
// are called.
|
||||
var DisableEscapeHTML = false
|
||||
|
||||
// AppendJSONString is a convenience function that converts the provided string
|
||||
// to a valid JSON string and appends it to dst.
|
||||
func AppendJSONString(dst []byte, s string) []byte {
|
||||
@@ -1859,6 +2034,10 @@ func AppendJSONString(dst []byte, s string) []byte {
|
||||
if s[i] < ' ' {
|
||||
dst = append(dst, '\\')
|
||||
switch s[i] {
|
||||
case '\b':
|
||||
dst = append(dst, 'b')
|
||||
case '\f':
|
||||
dst = append(dst, 'f')
|
||||
case '\n':
|
||||
dst = append(dst, 'n')
|
||||
case '\r':
|
||||
@@ -1869,7 +2048,8 @@ func AppendJSONString(dst []byte, s string) []byte {
|
||||
dst = append(dst, 'u')
|
||||
dst = appendHex16(dst, uint16(s[i]))
|
||||
}
|
||||
} else if s[i] == '>' || s[i] == '<' || s[i] == '&' {
|
||||
} else if !DisableEscapeHTML &&
|
||||
(s[i] == '>' || s[i] == '<' || s[i] == '&') {
|
||||
dst = append(dst, '\\', 'u')
|
||||
dst = appendHex16(dst, uint16(s[i]))
|
||||
} else if s[i] == '\\' {
|
||||
@@ -1918,23 +2098,23 @@ type parseContext struct {
|
||||
// the '#' character.
|
||||
// The dot and wildcard character can be escaped with '\'.
|
||||
//
|
||||
// {
|
||||
// "name": {"first": "Tom", "last": "Anderson"},
|
||||
// "age":37,
|
||||
// "children": ["Sara","Alex","Jack"],
|
||||
// "friends": [
|
||||
// {"first": "James", "last": "Murphy"},
|
||||
// {"first": "Roger", "last": "Craig"}
|
||||
// ]
|
||||
// }
|
||||
// "name.last" >> "Anderson"
|
||||
// "age" >> 37
|
||||
// "children" >> ["Sara","Alex","Jack"]
|
||||
// "children.#" >> 3
|
||||
// "children.1" >> "Alex"
|
||||
// "child*.2" >> "Jack"
|
||||
// "c?ildren.0" >> "Sara"
|
||||
// "friends.#.first" >> ["James","Roger"]
|
||||
// {
|
||||
// "name": {"first": "Tom", "last": "Anderson"},
|
||||
// "age":37,
|
||||
// "children": ["Sara","Alex","Jack"],
|
||||
// "friends": [
|
||||
// {"first": "James", "last": "Murphy"},
|
||||
// {"first": "Roger", "last": "Craig"}
|
||||
// ]
|
||||
// }
|
||||
// "name.last" >> "Anderson"
|
||||
// "age" >> 37
|
||||
// "children" >> ["Sara","Alex","Jack"]
|
||||
// "children.#" >> 3
|
||||
// "children.1" >> "Alex"
|
||||
// "child*.2" >> "Jack"
|
||||
// "c?ildren.0" >> "Sara"
|
||||
// "friends.#.first" >> ["James","Roger"]
|
||||
//
|
||||
// This function expects that the json is well-formed, and does not validate.
|
||||
// Invalid json will not panic, but it may return back unexpected results.
|
||||
@@ -2123,11 +2303,10 @@ func unescape(json string) string {
|
||||
}
|
||||
|
||||
// Less return true if a token is less than another token.
|
||||
// The caseSensitive paramater is used when the tokens are Strings.
|
||||
// The caseSensitive parameter is used when the tokens are Strings.
|
||||
// The order when comparing two different type is:
|
||||
//
|
||||
// Null < False < Number < String < True < JSON
|
||||
//
|
||||
// Null < False < Number < String < True < JSON
|
||||
func (t Result) Less(token Result, caseSensitive bool) bool {
|
||||
if t.Type < token.Type {
|
||||
return true
|
||||
@@ -2556,11 +2735,10 @@ func validnull(data []byte, i int) (outi int, ok bool) {
|
||||
|
||||
// Valid returns true if the input is valid json.
|
||||
//
|
||||
// if !gjson.Valid(json) {
|
||||
// return errors.New("invalid json")
|
||||
// }
|
||||
// value := gjson.Get(json, "name.last")
|
||||
//
|
||||
// if !gjson.Valid(json) {
|
||||
// return errors.New("invalid json")
|
||||
// }
|
||||
// value := gjson.Get(json, "name.last")
|
||||
func Valid(json string) bool {
|
||||
_, ok := validpayload(stringBytes(json), 0)
|
||||
return ok
|
||||
@@ -2568,13 +2746,12 @@ func Valid(json string) bool {
|
||||
|
||||
// ValidBytes returns true if the input is valid json.
|
||||
//
|
||||
// if !gjson.Valid(json) {
|
||||
// return errors.New("invalid json")
|
||||
// }
|
||||
// value := gjson.Get(json, "name.last")
|
||||
// if !gjson.Valid(json) {
|
||||
// return errors.New("invalid json")
|
||||
// }
|
||||
// value := gjson.Get(json, "name.last")
|
||||
//
|
||||
// If working with bytes, this method preferred over ValidBytes(string(data))
|
||||
//
|
||||
func ValidBytes(json []byte) bool {
|
||||
_, ok := validpayload(json, 0)
|
||||
return ok
|
||||
@@ -2690,6 +2867,7 @@ func execModifier(json, path string) (pathOut, res string, ok bool) {
|
||||
var parsedArgs bool
|
||||
switch pathOut[0] {
|
||||
case '{', '[', '"':
|
||||
// json arg
|
||||
res := Parse(pathOut)
|
||||
if res.Exists() {
|
||||
args = squash(pathOut)
|
||||
@@ -2698,14 +2876,20 @@ func execModifier(json, path string) (pathOut, res string, ok bool) {
|
||||
}
|
||||
}
|
||||
if !parsedArgs {
|
||||
idx := strings.IndexByte(pathOut, '|')
|
||||
if idx == -1 {
|
||||
args = pathOut
|
||||
pathOut = ""
|
||||
} else {
|
||||
args = pathOut[:idx]
|
||||
pathOut = pathOut[idx:]
|
||||
// simple arg
|
||||
i := 0
|
||||
for ; i < len(pathOut); i++ {
|
||||
if pathOut[i] == '|' {
|
||||
break
|
||||
}
|
||||
switch pathOut[i] {
|
||||
case '{', '[', '"', '(':
|
||||
s := squash(pathOut[i:])
|
||||
i += len(s) - 1
|
||||
}
|
||||
}
|
||||
args = pathOut[:i]
|
||||
pathOut = pathOut[i:]
|
||||
}
|
||||
}
|
||||
return pathOut, fn(json, args), true
|
||||
@@ -2725,19 +2909,24 @@ func unwrap(json string) string {
|
||||
// DisableModifiers will disable the modifier syntax
|
||||
var DisableModifiers = false
|
||||
|
||||
var modifiers = map[string]func(json, arg string) string{
|
||||
"pretty": modPretty,
|
||||
"ugly": modUgly,
|
||||
"reverse": modReverse,
|
||||
"this": modThis,
|
||||
"flatten": modFlatten,
|
||||
"join": modJoin,
|
||||
"valid": modValid,
|
||||
"keys": modKeys,
|
||||
"values": modValues,
|
||||
"tostr": modToStr,
|
||||
"fromstr": modFromStr,
|
||||
"group": modGroup,
|
||||
var modifiers map[string]func(json, arg string) string
|
||||
|
||||
func init() {
|
||||
modifiers = map[string]func(json, arg string) string{
|
||||
"pretty": modPretty,
|
||||
"ugly": modUgly,
|
||||
"reverse": modReverse,
|
||||
"this": modThis,
|
||||
"flatten": modFlatten,
|
||||
"join": modJoin,
|
||||
"valid": modValid,
|
||||
"keys": modKeys,
|
||||
"values": modValues,
|
||||
"tostr": modToStr,
|
||||
"fromstr": modFromStr,
|
||||
"group": modGroup,
|
||||
"dig": modDig,
|
||||
}
|
||||
}
|
||||
|
||||
// AddModifier binds a custom modifier command to the GJSON syntax.
|
||||
@@ -2848,9 +3037,13 @@ func modReverse(json, arg string) string {
|
||||
}
|
||||
|
||||
// @flatten an array with child arrays.
|
||||
// [1,[2],[3,4],[5,[6,7]]] -> [1,2,3,4,5,[6,7]]
|
||||
//
|
||||
// [1,[2],[3,4],[5,[6,7]]] -> [1,2,3,4,5,[6,7]]
|
||||
//
|
||||
// The {"deep":true} arg can be provide for deep flattening.
|
||||
// [1,[2],[3,4],[5,[6,7]]] -> [1,2,3,4,5,6,7]
|
||||
//
|
||||
// [1,[2],[3,4],[5,[6,7]]] -> [1,2,3,4,5,6,7]
|
||||
//
|
||||
// The original json is returned when the json is not an array.
|
||||
func modFlatten(json, arg string) string {
|
||||
res := Parse(json)
|
||||
@@ -2895,7 +3088,8 @@ func modFlatten(json, arg string) string {
|
||||
}
|
||||
|
||||
// @keys extracts the keys from an object.
|
||||
// {"first":"Tom","last":"Smith"} -> ["first","last"]
|
||||
//
|
||||
// {"first":"Tom","last":"Smith"} -> ["first","last"]
|
||||
func modKeys(json, arg string) string {
|
||||
v := Parse(json)
|
||||
if !v.Exists() {
|
||||
@@ -2922,7 +3116,8 @@ func modKeys(json, arg string) string {
|
||||
}
|
||||
|
||||
// @values extracts the values from an object.
|
||||
// {"first":"Tom","last":"Smith"} -> ["Tom","Smith"]
|
||||
//
|
||||
// {"first":"Tom","last":"Smith"} -> ["Tom","Smith"]
|
||||
func modValues(json, arg string) string {
|
||||
v := Parse(json)
|
||||
if !v.Exists() {
|
||||
@@ -2947,11 +3142,17 @@ func modValues(json, arg string) string {
|
||||
}
|
||||
|
||||
// @join multiple objects into a single object.
|
||||
// [{"first":"Tom"},{"last":"Smith"}] -> {"first","Tom","last":"Smith"}
|
||||
//
|
||||
// [{"first":"Tom"},{"last":"Smith"}] -> {"first","Tom","last":"Smith"}
|
||||
//
|
||||
// The arg can be "true" to specify that duplicate keys should be preserved.
|
||||
// [{"first":"Tom","age":37},{"age":41}] -> {"first","Tom","age":37,"age":41}
|
||||
//
|
||||
// [{"first":"Tom","age":37},{"age":41}] -> {"first","Tom","age":37,"age":41}
|
||||
//
|
||||
// Without preserved keys:
|
||||
// [{"first":"Tom","age":37},{"age":41}] -> {"first","Tom","age":41}
|
||||
//
|
||||
// [{"first":"Tom","age":37},{"age":41}] -> {"first","Tom","age":41}
|
||||
//
|
||||
// The original json is returned when the json is not an object.
|
||||
func modJoin(json, arg string) string {
|
||||
res := Parse(json)
|
||||
@@ -3024,7 +3225,8 @@ func modValid(json, arg string) string {
|
||||
}
|
||||
|
||||
// @fromstr converts a string to json
|
||||
// "{\"id\":1023,\"name\":\"alert\"}" -> {"id":1023,"name":"alert"}
|
||||
//
|
||||
// "{\"id\":1023,\"name\":\"alert\"}" -> {"id":1023,"name":"alert"}
|
||||
func modFromStr(json, arg string) string {
|
||||
if !Valid(json) {
|
||||
return ""
|
||||
@@ -3033,7 +3235,8 @@ func modFromStr(json, arg string) string {
|
||||
}
|
||||
|
||||
// @tostr converts a string to json
|
||||
// {"id":1023,"name":"alert"} -> "{\"id\":1023,\"name\":\"alert\"}"
|
||||
//
|
||||
// {"id":1023,"name":"alert"} -> "{\"id\":1023,\"name\":\"alert\"}"
|
||||
func modToStr(str, arg string) string {
|
||||
return string(AppendJSONString(nil, str))
|
||||
}
|
||||
@@ -3210,11 +3413,11 @@ func revSquash(json string) string {
|
||||
// Paths returns the original GJSON paths for a Result where the Result came
|
||||
// from a simple query path that returns an array, like:
|
||||
//
|
||||
// gjson.Get(json, "friends.#.first")
|
||||
// gjson.Get(json, "friends.#.first")
|
||||
//
|
||||
// The returned value will be in the form of a JSON array:
|
||||
//
|
||||
// ["friends.0.first","friends.1.first","friends.2.first"]
|
||||
// ["friends.0.first","friends.1.first","friends.2.first"]
|
||||
//
|
||||
// The param 'json' must be the original JSON used when calling Get.
|
||||
//
|
||||
@@ -3239,11 +3442,11 @@ func (t Result) Paths(json string) []string {
|
||||
// Path returns the original GJSON path for a Result where the Result came
|
||||
// from a simple path that returns a single value, like:
|
||||
//
|
||||
// gjson.Get(json, "friends.#(last=Murphy)")
|
||||
// gjson.Get(json, "friends.#(last=Murphy)")
|
||||
//
|
||||
// The returned value will be in the form of a JSON string:
|
||||
//
|
||||
// "friends.0"
|
||||
// "friends.0"
|
||||
//
|
||||
// The param 'json' must be the original JSON used when calling Get.
|
||||
//
|
||||
@@ -3259,7 +3462,7 @@ func (t Result) Path(json string) string {
|
||||
goto fail
|
||||
}
|
||||
if !strings.HasPrefix(json[t.Index:], t.Raw) {
|
||||
// Result is not at the JSON index as exepcted.
|
||||
// Result is not at the JSON index as expected.
|
||||
goto fail
|
||||
}
|
||||
for ; i >= 0; i-- {
|
||||
@@ -3320,7 +3523,7 @@ func (t Result) Path(json string) string {
|
||||
if !rcomp.Exists() {
|
||||
goto fail
|
||||
}
|
||||
comp := escapeComp(rcomp.String())
|
||||
comp := Escape(rcomp.String())
|
||||
path = append(path, '.')
|
||||
path = append(path, comp...)
|
||||
}
|
||||
@@ -3335,17 +3538,31 @@ fail:
|
||||
// isSafePathKeyChar returns true if the input character is safe for not
|
||||
// needing escaping.
|
||||
func isSafePathKeyChar(c byte) bool {
|
||||
return c <= ' ' || c > '~' || c == '_' || c == '-' || c == ':' ||
|
||||
(c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
|
||||
(c >= '0' && c <= '9')
|
||||
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
|
||||
(c >= '0' && c <= '9') || c <= ' ' || c > '~' || c == '_' ||
|
||||
c == '-' || c == ':'
|
||||
}
|
||||
|
||||
// escapeComp escaped a path compontent, making it safe for generating a
|
||||
// path for later use.
|
||||
func escapeComp(comp string) string {
|
||||
// Escape returns an escaped path component.
|
||||
//
|
||||
// json := `{
|
||||
// "user":{
|
||||
// "first.name": "Janet",
|
||||
// "last.name": "Prichard"
|
||||
// }
|
||||
// }`
|
||||
// user := gjson.Get(json, "user")
|
||||
// println(user.Get(gjson.Escape("first.name"))
|
||||
// println(user.Get(gjson.Escape("last.name"))
|
||||
// // Output:
|
||||
// // Janet
|
||||
// // Prichard
|
||||
func Escape(comp string) string {
|
||||
for i := 0; i < len(comp); i++ {
|
||||
if !isSafePathKeyChar(comp[i]) {
|
||||
ncomp := []byte(comp[:i])
|
||||
ncomp := make([]byte, len(comp)+1)
|
||||
copy(ncomp, comp[:i])
|
||||
ncomp = ncomp[:i]
|
||||
for ; i < len(comp); i++ {
|
||||
if !isSafePathKeyChar(comp[i]) {
|
||||
ncomp = append(ncomp, '\\')
|
||||
@@ -3357,3 +3574,30 @@ func escapeComp(comp string) string {
|
||||
}
|
||||
return comp
|
||||
}
|
||||
|
||||
func parseRecursiveDescent(all []Result, parent Result, path string) []Result {
|
||||
if res := parent.Get(path); res.Exists() {
|
||||
all = append(all, res)
|
||||
}
|
||||
if parent.IsArray() || parent.IsObject() {
|
||||
parent.ForEach(func(_, val Result) bool {
|
||||
all = parseRecursiveDescent(all, val, path)
|
||||
return true
|
||||
})
|
||||
}
|
||||
return all
|
||||
}
|
||||
|
||||
func modDig(json, arg string) string {
|
||||
all := parseRecursiveDescent(nil, Parse(json), arg)
|
||||
var out []byte
|
||||
out = append(out, '[')
|
||||
for i, res := range all {
|
||||
if i > 0 {
|
||||
out = append(out, ',')
|
||||
}
|
||||
out = append(out, res.Raw...)
|
||||
}
|
||||
out = append(out, ']')
|
||||
return string(out)
|
||||
}
|
||||
|
||||
BIN
vendor/github.com/tidwall/gjson/logo.png
generated
vendored
BIN
vendor/github.com/tidwall/gjson/logo.png
generated
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 16 KiB |
2
vendor/github.com/tidwall/pretty/README.md
generated
vendored
2
vendor/github.com/tidwall/pretty/README.md
generated
vendored
@@ -59,7 +59,7 @@ Will format the json to:
|
||||
|
||||
Color will colorize the json for outputing to the screen.
|
||||
|
||||
```json
|
||||
```go
|
||||
result = pretty.Color(json, nil)
|
||||
```
|
||||
|
||||
|
||||
22
vendor/github.com/tidwall/pretty/pretty.go
generated
vendored
22
vendor/github.com/tidwall/pretty/pretty.go
generated
vendored
@@ -422,6 +422,7 @@ type Style struct {
|
||||
Key, String, Number [2]string
|
||||
True, False, Null [2]string
|
||||
Escape [2]string
|
||||
Brackets [2]string
|
||||
Append func(dst []byte, c byte) []byte
|
||||
}
|
||||
|
||||
@@ -439,13 +440,14 @@ var TerminalStyle *Style
|
||||
|
||||
func init() {
|
||||
TerminalStyle = &Style{
|
||||
Key: [2]string{"\x1B[94m", "\x1B[0m"},
|
||||
String: [2]string{"\x1B[92m", "\x1B[0m"},
|
||||
Number: [2]string{"\x1B[93m", "\x1B[0m"},
|
||||
True: [2]string{"\x1B[96m", "\x1B[0m"},
|
||||
False: [2]string{"\x1B[96m", "\x1B[0m"},
|
||||
Null: [2]string{"\x1B[91m", "\x1B[0m"},
|
||||
Escape: [2]string{"\x1B[35m", "\x1B[0m"},
|
||||
Key: [2]string{"\x1B[1m\x1B[94m", "\x1B[0m"},
|
||||
String: [2]string{"\x1B[32m", "\x1B[0m"},
|
||||
Number: [2]string{"\x1B[33m", "\x1B[0m"},
|
||||
True: [2]string{"\x1B[36m", "\x1B[0m"},
|
||||
False: [2]string{"\x1B[36m", "\x1B[0m"},
|
||||
Null: [2]string{"\x1B[2m", "\x1B[0m"},
|
||||
Escape: [2]string{"\x1B[35m", "\x1B[0m"},
|
||||
Brackets: [2]string{"\x1B[1m", "\x1B[0m"},
|
||||
Append: func(dst []byte, c byte) []byte {
|
||||
if c < ' ' && (c != '\r' && c != '\n' && c != '\t' && c != '\v') {
|
||||
dst = append(dst, "\\u00"...)
|
||||
@@ -539,13 +541,19 @@ func Color(src []byte, style *Style) []byte {
|
||||
}
|
||||
} else if src[i] == '{' || src[i] == '[' {
|
||||
stack = append(stack, stackt{src[i], src[i] == '{'})
|
||||
dst = append(dst, style.Brackets[0]...)
|
||||
dst = apnd(dst, src[i])
|
||||
dst = append(dst, style.Brackets[1]...)
|
||||
} else if (src[i] == '}' || src[i] == ']') && len(stack) > 0 {
|
||||
stack = stack[:len(stack)-1]
|
||||
dst = append(dst, style.Brackets[0]...)
|
||||
dst = apnd(dst, src[i])
|
||||
dst = append(dst, style.Brackets[1]...)
|
||||
} else if (src[i] == ':' || src[i] == ',') && len(stack) > 0 && stack[len(stack)-1].kind == '{' {
|
||||
stack[len(stack)-1].key = !stack[len(stack)-1].key
|
||||
dst = append(dst, style.Brackets[0]...)
|
||||
dst = apnd(dst, src[i])
|
||||
dst = append(dst, style.Brackets[1]...)
|
||||
} else {
|
||||
var kind byte
|
||||
if (src[i] >= '0' && src[i] <= '9') || src[i] == '-' || isNaNOrInf(src[i:]) {
|
||||
|
||||
141
vendor/modules.txt
vendored
141
vendor/modules.txt
vendored
@@ -7,6 +7,74 @@ github.com/GehirnInc/crypt
|
||||
github.com/GehirnInc/crypt/common
|
||||
github.com/GehirnInc/crypt/internal
|
||||
github.com/GehirnInc/crypt/md5_crypt
|
||||
# github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3
|
||||
## explicit; go 1.14
|
||||
github.com/ImVexed/fasturl
|
||||
# github.com/btcsuite/btcd/btcec/v2 v2.3.4
|
||||
## explicit; go 1.17
|
||||
github.com/btcsuite/btcd/btcec/v2
|
||||
github.com/btcsuite/btcd/btcec/v2/schnorr
|
||||
# github.com/btcsuite/btcd/btcutil v1.1.5
|
||||
## explicit; go 1.16
|
||||
github.com/btcsuite/btcd/btcutil/bech32
|
||||
# github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0
|
||||
## explicit; go 1.17
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash
|
||||
# github.com/bytedance/sonic v1.13.1
|
||||
## explicit; go 1.17
|
||||
github.com/bytedance/sonic/ast
|
||||
github.com/bytedance/sonic/encoder
|
||||
github.com/bytedance/sonic/internal/caching
|
||||
github.com/bytedance/sonic/internal/compat
|
||||
github.com/bytedance/sonic/internal/cpu
|
||||
github.com/bytedance/sonic/internal/encoder
|
||||
github.com/bytedance/sonic/internal/encoder/alg
|
||||
github.com/bytedance/sonic/internal/encoder/ir
|
||||
github.com/bytedance/sonic/internal/encoder/vars
|
||||
github.com/bytedance/sonic/internal/encoder/vm
|
||||
github.com/bytedance/sonic/internal/encoder/x86
|
||||
github.com/bytedance/sonic/internal/jit
|
||||
github.com/bytedance/sonic/internal/native
|
||||
github.com/bytedance/sonic/internal/native/avx2
|
||||
github.com/bytedance/sonic/internal/native/neon
|
||||
github.com/bytedance/sonic/internal/native/sse
|
||||
github.com/bytedance/sonic/internal/native/types
|
||||
github.com/bytedance/sonic/internal/resolver
|
||||
github.com/bytedance/sonic/internal/rt
|
||||
github.com/bytedance/sonic/internal/utils
|
||||
github.com/bytedance/sonic/option
|
||||
github.com/bytedance/sonic/unquote
|
||||
github.com/bytedance/sonic/utf8
|
||||
# github.com/bytedance/sonic/loader v0.2.4
|
||||
## explicit; go 1.16
|
||||
github.com/bytedance/sonic/loader
|
||||
github.com/bytedance/sonic/loader/internal/abi
|
||||
github.com/bytedance/sonic/loader/internal/iasm/expr
|
||||
github.com/bytedance/sonic/loader/internal/iasm/x86_64
|
||||
github.com/bytedance/sonic/loader/internal/rt
|
||||
# github.com/cloudwego/base64x v0.1.5
|
||||
## explicit; go 1.16
|
||||
github.com/cloudwego/base64x
|
||||
github.com/cloudwego/base64x/internal/native
|
||||
github.com/cloudwego/base64x/internal/native/avx2
|
||||
github.com/cloudwego/base64x/internal/native/sse
|
||||
github.com/cloudwego/base64x/internal/rt
|
||||
# github.com/coder/websocket v1.8.12
|
||||
## explicit; go 1.19
|
||||
github.com/coder/websocket
|
||||
github.com/coder/websocket/internal/bpool
|
||||
github.com/coder/websocket/internal/errd
|
||||
github.com/coder/websocket/internal/util
|
||||
github.com/coder/websocket/internal/wsjs
|
||||
github.com/coder/websocket/internal/xsync
|
||||
# github.com/decred/dcrd/crypto/blake256 v1.1.0
|
||||
## explicit; go 1.17
|
||||
github.com/decred/dcrd/crypto/blake256
|
||||
github.com/decred/dcrd/crypto/blake256/internal/compress
|
||||
# github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0
|
||||
## explicit; go 1.17
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4/schnorr
|
||||
# github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815
|
||||
## explicit
|
||||
github.com/docopt/docopt-go
|
||||
@@ -37,25 +105,52 @@ github.com/gofrs/flock
|
||||
# github.com/golang-jwt/jwt/v5 v5.2.2
|
||||
## explicit; go 1.18
|
||||
github.com/golang-jwt/jwt/v5
|
||||
# github.com/gorilla/websocket v1.4.2 => github.com/ergochat/websocket v1.4.2-oragono1
|
||||
# github.com/gorilla/websocket v1.5.3 => github.com/ergochat/websocket v1.4.2-oragono1
|
||||
## explicit; go 1.12
|
||||
github.com/gorilla/websocket
|
||||
# github.com/josharian/intern v1.0.0
|
||||
## explicit; go 1.5
|
||||
github.com/josharian/intern
|
||||
# github.com/json-iterator/go v1.1.12
|
||||
## explicit; go 1.12
|
||||
github.com/json-iterator/go
|
||||
# github.com/klauspost/cpuid/v2 v2.2.10
|
||||
## explicit; go 1.22
|
||||
github.com/klauspost/cpuid/v2
|
||||
# github.com/mailru/easyjson v0.9.0
|
||||
## explicit; go 1.20
|
||||
github.com/mailru/easyjson
|
||||
github.com/mailru/easyjson/buffer
|
||||
github.com/mailru/easyjson/jlexer
|
||||
github.com/mailru/easyjson/jwriter
|
||||
# github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd
|
||||
## explicit
|
||||
github.com/modern-go/concurrent
|
||||
# github.com/modern-go/reflect2 v1.0.2
|
||||
## explicit; go 1.12
|
||||
github.com/modern-go/reflect2
|
||||
# github.com/nbd-wtf/go-nostr v0.52.0
|
||||
## explicit; go 1.24.1
|
||||
github.com/nbd-wtf/go-nostr
|
||||
github.com/nbd-wtf/go-nostr/nip04
|
||||
github.com/nbd-wtf/go-nostr/nip17
|
||||
github.com/nbd-wtf/go-nostr/nip19
|
||||
github.com/nbd-wtf/go-nostr/nip44
|
||||
github.com/nbd-wtf/go-nostr/nip45/hyperloglog
|
||||
github.com/nbd-wtf/go-nostr/nip59
|
||||
# github.com/okzk/sdnotify v0.0.0-20180710141335-d9becc38acbd
|
||||
## explicit
|
||||
github.com/okzk/sdnotify
|
||||
# github.com/onsi/ginkgo v1.12.0
|
||||
## explicit; go 1.12
|
||||
# github.com/onsi/gomega v1.9.0
|
||||
## explicit
|
||||
# github.com/stretchr/testify v1.4.0
|
||||
## explicit
|
||||
# github.com/puzpuzpuz/xsync/v3 v3.5.1
|
||||
## explicit; go 1.18
|
||||
github.com/puzpuzpuz/xsync/v3
|
||||
# github.com/tidwall/btree v1.4.2
|
||||
## explicit; go 1.18
|
||||
github.com/tidwall/btree
|
||||
# github.com/tidwall/buntdb v1.3.2
|
||||
## explicit; go 1.18
|
||||
github.com/tidwall/buntdb
|
||||
# github.com/tidwall/gjson v1.14.3
|
||||
# github.com/tidwall/gjson v1.18.0
|
||||
## explicit; go 1.12
|
||||
github.com/tidwall/gjson
|
||||
# github.com/tidwall/grect v0.1.4
|
||||
@@ -64,7 +159,7 @@ github.com/tidwall/grect
|
||||
# github.com/tidwall/match v1.1.1
|
||||
## explicit; go 1.15
|
||||
github.com/tidwall/match
|
||||
# github.com/tidwall/pretty v1.2.0
|
||||
# github.com/tidwall/pretty v1.2.1
|
||||
## explicit; go 1.16
|
||||
github.com/tidwall/pretty
|
||||
# github.com/tidwall/rtred v0.1.2
|
||||
@@ -74,21 +169,49 @@ github.com/tidwall/rtred/base
|
||||
# github.com/tidwall/tinyqueue v0.1.1
|
||||
## explicit; go 1.15
|
||||
github.com/tidwall/tinyqueue
|
||||
# github.com/twitchyliquid64/golang-asm v0.15.1
|
||||
## explicit; go 1.13
|
||||
github.com/twitchyliquid64/golang-asm/asm/arch
|
||||
github.com/twitchyliquid64/golang-asm/bio
|
||||
github.com/twitchyliquid64/golang-asm/dwarf
|
||||
github.com/twitchyliquid64/golang-asm/goobj
|
||||
github.com/twitchyliquid64/golang-asm/obj
|
||||
github.com/twitchyliquid64/golang-asm/obj/arm
|
||||
github.com/twitchyliquid64/golang-asm/obj/arm64
|
||||
github.com/twitchyliquid64/golang-asm/obj/mips
|
||||
github.com/twitchyliquid64/golang-asm/obj/ppc64
|
||||
github.com/twitchyliquid64/golang-asm/obj/riscv
|
||||
github.com/twitchyliquid64/golang-asm/obj/s390x
|
||||
github.com/twitchyliquid64/golang-asm/obj/wasm
|
||||
github.com/twitchyliquid64/golang-asm/obj/x86
|
||||
github.com/twitchyliquid64/golang-asm/objabi
|
||||
github.com/twitchyliquid64/golang-asm/src
|
||||
github.com/twitchyliquid64/golang-asm/sys
|
||||
github.com/twitchyliquid64/golang-asm/unsafeheader
|
||||
# github.com/xdg-go/pbkdf2 v1.0.0
|
||||
## explicit; go 1.9
|
||||
github.com/xdg-go/pbkdf2
|
||||
# github.com/xdg-go/scram v1.0.2 => github.com/ergochat/scram v1.0.2-ergo1
|
||||
## explicit; go 1.11
|
||||
github.com/xdg-go/scram
|
||||
# golang.org/x/arch v0.15.0
|
||||
## explicit; go 1.23.0
|
||||
golang.org/x/arch/x86/x86asm
|
||||
# golang.org/x/crypto v0.38.0
|
||||
## explicit; go 1.23.0
|
||||
golang.org/x/crypto/bcrypt
|
||||
golang.org/x/crypto/blowfish
|
||||
golang.org/x/crypto/chacha20
|
||||
golang.org/x/crypto/ed25519
|
||||
golang.org/x/crypto/hkdf
|
||||
golang.org/x/crypto/internal/alias
|
||||
golang.org/x/crypto/pbkdf2
|
||||
# golang.org/x/exp v0.0.0-20250305212735-054e65f0b394
|
||||
## explicit; go 1.23.0
|
||||
golang.org/x/exp/constraints
|
||||
# golang.org/x/sys v0.33.0
|
||||
## explicit; go 1.23.0
|
||||
golang.org/x/sys/cpu
|
||||
golang.org/x/sys/plan9
|
||||
golang.org/x/sys/unix
|
||||
golang.org/x/sys/windows
|
||||
|
||||
Reference in New Issue
Block a user