noirc launch

This commit is contained in:
jeremyd
2025-09-18 10:52:11 -07:00
committed by jeremyd
parent 68faf82787
commit 7c8784ecdc
28 changed files with 2327 additions and 680 deletions

4
.gitignore vendored
View File

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

View File

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

View File

@@ -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
View File

@@ -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
View File

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

View File

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

View File

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

View File

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

View File

@@ -399,7 +399,7 @@ func init() {
minParams: 1,
},
"WHOIS": {
handler: whoisHandler,
handler: whoHandler,
minParams: 1,
},
"WHOWAS": {

View File

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

View File

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

View File

@@ -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], ",")

View File

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

View File

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

View File

@@ -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.",

View File

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

View File

@@ -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).*

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -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)
```

View File

@@ -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
View File

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