Compare commits

...

7 Commits

Author SHA1 Message Date
fiatjaf
6f48c29d0f fix go-nostr dependency. 2025-04-03 21:31:12 -03:00
fiatjaf
703c186958 much more colors everywhere and everything is prettier. 2025-04-03 14:50:25 -03:00
fiatjaf
7ae2e686cb more colors. 2025-04-03 11:57:18 -03:00
fiatjaf
9547711e8d nice dynamic UI when connecting to relays, and go much faster concurrently. 2025-04-03 11:42:33 -03:00
fiatjaf
50119e21e6 fix nip73.ExternalPointer reference 2025-04-02 22:38:12 -03:00
fiatjaf
33f4272dd0 update go-nostr to maybe fix nip-60 wallets? 2025-04-02 22:37:59 -03:00
fiatjaf
7b6f387aad tags GetFirst() => Find() 2025-03-29 17:12:31 -03:00
13 changed files with 379 additions and 161 deletions

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
nak nak
mnt mnt
nak.exe

View File

@@ -11,10 +11,10 @@ import (
"time" "time"
"github.com/fatih/color" "github.com/fatih/color"
"github.com/urfave/cli/v3"
"github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nip19" "github.com/nbd-wtf/go-nostr/nip19"
"github.com/nbd-wtf/go-nostr/nip46" "github.com/nbd-wtf/go-nostr/nip46"
"github.com/urfave/cli/v3"
) )
var bunker = &cli.Command{ var bunker = &cli.Command{
@@ -49,7 +49,7 @@ var bunker = &cli.Command{
qs := url.Values{} qs := url.Values{}
relayURLs := make([]string, 0, c.Args().Len()) relayURLs := make([]string, 0, c.Args().Len())
if relayUrls := c.Args().Slice(); len(relayUrls) > 0 { if relayUrls := c.Args().Slice(); len(relayUrls) > 0 {
relays := connectToAllRelays(ctx, relayUrls, false) relays := connectToAllRelays(ctx, c, relayUrls, nil)
if len(relays) == 0 { if len(relays) == 0 {
log("failed to connect to any of the given relays.\n") log("failed to connect to any of the given relays.\n")
os.Exit(3) os.Exit(3)

View File

@@ -6,10 +6,10 @@ import (
"os" "os"
"strings" "strings"
"github.com/urfave/cli/v3"
"github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nip45" "github.com/nbd-wtf/go-nostr/nip45"
"github.com/nbd-wtf/go-nostr/nip45/hyperloglog" "github.com/nbd-wtf/go-nostr/nip45/hyperloglog"
"github.com/urfave/cli/v3"
) )
var count = &cli.Command{ var count = &cli.Command{
@@ -70,10 +70,7 @@ var count = &cli.Command{
biggerUrlSize := 0 biggerUrlSize := 0
relayUrls := c.Args().Slice() relayUrls := c.Args().Slice()
if len(relayUrls) > 0 { if len(relayUrls) > 0 {
relays := connectToAllRelays(ctx, relays := connectToAllRelays(ctx, c, relayUrls, nil)
relayUrls,
false,
)
if len(relays) == 0 { if len(relays) == 0 {
log("failed to connect to any of the given relays.\n") log("failed to connect to any of the given relays.\n")
os.Exit(3) os.Exit(3)

12
dvm.go
View File

@@ -7,7 +7,6 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/fatih/color"
"github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nip90" "github.com/nbd-wtf/go-nostr/nip90"
"github.com/urfave/cli/v3" "github.com/urfave/cli/v3"
@@ -60,7 +59,7 @@ var dvm = &cli.Command{
Flags: flags, Flags: flags,
Action: func(ctx context.Context, c *cli.Command) error { Action: func(ctx context.Context, c *cli.Command) error {
relayUrls := c.StringSlice("relay") relayUrls := c.StringSlice("relay")
relays := connectToAllRelays(ctx, relayUrls, false) relays := connectToAllRelays(ctx, c, relayUrls, nil)
if len(relays) == 0 { if len(relays) == 0 {
log("failed to connect to any of the given relays.\n") log("failed to connect to any of the given relays.\n")
os.Exit(3) os.Exit(3)
@@ -103,10 +102,7 @@ var dvm = &cli.Command{
log("- publishing job request... ") log("- publishing job request... ")
first := true first := true
for res := range sys.Pool.PublishMany(ctx, relayUrls, evt) { for res := range sys.Pool.PublishMany(ctx, relayUrls, evt) {
cleanUrl, ok := strings.CutPrefix(res.RelayURL, "wss://") cleanUrl, _ := strings.CutPrefix(res.RelayURL, "wss://")
if !ok {
cleanUrl = res.RelayURL
}
if !first { if !first {
log(", ") log(", ")
@@ -114,9 +110,9 @@ var dvm = &cli.Command{
first = false first = false
if res.Error != nil { if res.Error != nil {
log("%s: %s", color.RedString(cleanUrl), res.Error) log("%s: %s", colors.errorf(cleanUrl), res.Error)
} else { } else {
log("%s: ok", color.GreenString(cleanUrl)) log("%s: ok", colors.successf(cleanUrl))
} }
} }

View File

@@ -8,6 +8,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/fatih/color"
"github.com/mailru/easyjson" "github.com/mailru/easyjson"
"github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nip13" "github.com/nbd-wtf/go-nostr/nip13"
@@ -133,8 +134,17 @@ example:
Action: func(ctx context.Context, c *cli.Command) error { Action: func(ctx context.Context, c *cli.Command) error {
// try to connect to the relays here // try to connect to the relays here
var relays []*nostr.Relay var relays []*nostr.Relay
// these are defaults, they will be replaced if we use the magic dynamic thing
logthis := func(relayUrl string, s string, args ...any) { log(s, args...) }
colorizethis := func(relayUrl string, colorize func(string, ...any) string) {}
if relayUrls := c.Args().Slice(); len(relayUrls) > 0 { if relayUrls := c.Args().Slice(); len(relayUrls) > 0 {
relays = connectToAllRelays(ctx, relayUrls, false) relays = connectToAllRelays(ctx, c, relayUrls, nil,
nostr.WithAuthHandler(func(ctx context.Context, authEvent nostr.RelayEvent) error {
return authSigner(ctx, c, func(s string, args ...any) {}, authEvent)
}),
)
if len(relays) == 0 { if len(relays) == 0 {
log("failed to connect to any of the given relays.\n") log("failed to connect to any of the given relays.\n")
os.Exit(3) os.Exit(3)
@@ -209,13 +219,19 @@ example:
} }
for _, etag := range c.StringSlice("e") { for _, etag := range c.StringSlice("e") {
tags = tags.AppendUnique([]string{"e", etag}) if tags.FindWithValue("e", etag) == nil {
tags = append(tags, nostr.Tag{"e", etag})
}
} }
for _, ptag := range c.StringSlice("p") { for _, ptag := range c.StringSlice("p") {
tags = tags.AppendUnique([]string{"p", ptag}) if tags.FindWithValue("p", ptag) == nil {
tags = append(tags, nostr.Tag{"p", ptag})
}
} }
for _, dtag := range c.StringSlice("d") { for _, dtag := range c.StringSlice("d") {
tags = tags.AppendUnique([]string{"d", dtag}) if tags.FindWithValue("d", dtag) == nil {
tags = append(tags, nostr.Tag{"d", dtag})
}
} }
if len(tags) > 0 { if len(tags) > 0 {
for _, tag := range tags { for _, tag := range tags {
@@ -295,9 +311,73 @@ example:
successRelays := make([]string, 0, len(relays)) successRelays := make([]string, 0, len(relays))
if len(relays) > 0 { if len(relays) > 0 {
os.Stdout.Sync() os.Stdout.Sync()
if supportsDynamicMultilineMagic() {
// overcomplicated multiline rendering magic
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
urls := make([]string, len(relays))
lines := make([][][]byte, len(urls))
flush := func() {
for _, line := range lines {
for _, part := range line {
os.Stderr.Write(part)
}
os.Stderr.Write([]byte{'\n'})
}
}
render := func() {
clearLines(len(lines))
flush()
}
flush()
logthis = func(relayUrl, s string, args ...any) {
idx := slices.Index(urls, relayUrl)
lines[idx] = append(lines[idx], []byte(fmt.Sprintf(s, args...)))
render()
}
colorizethis = func(relayUrl string, colorize func(string, ...any) string) {
cleanUrl, _ := strings.CutPrefix(relayUrl, "wss://")
idx := slices.Index(urls, relayUrl)
lines[idx][0] = []byte(fmt.Sprintf("publishing to %s... ", colorize(cleanUrl)))
render()
}
for i, relay := range relays {
urls[i] = relay.URL
lines[i] = make([][]byte, 1, 3)
colorizethis(relay.URL, color.CyanString)
}
render()
for res := range sys.Pool.PublishMany(ctx, urls, evt) {
if res.Error == nil {
colorizethis(res.RelayURL, colors.successf)
logthis(res.RelayURL, "success.")
successRelays = append(successRelays, res.RelayURL)
} else {
colorizethis(res.RelayURL, colors.errorf)
// in this case it's likely that the lowest-level error is the one that will be more helpful
low := unwrapAll(res.Error)
// hack for some messages such as from relay.westernbtc.com
msg := strings.ReplaceAll(low.Error(), evt.PubKey, "author")
// do not allow the message to overflow the term window
msg = clampMessage(msg, 20+len(res.RelayURL))
logthis(res.RelayURL, msg)
}
}
} else {
// normal dumb flow
for _, relay := range relays { for _, relay := range relays {
publish: publish:
log("publishing to %s... ", relay.URL) cleanUrl, _ := strings.CutPrefix(relay.URL, "wss://")
log("publishing to %s... ", color.CyanString(cleanUrl))
ctx, cancel := context.WithTimeout(ctx, 10*time.Second) ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel() defer cancel()
@@ -313,7 +393,8 @@ example:
if strings.HasPrefix(err.Error(), "msg: auth-required:") && kr != nil && doAuth { if strings.HasPrefix(err.Error(), "msg: auth-required:") && kr != nil && doAuth {
// if the relay is requesting auth and we can auth, let's do it // if the relay is requesting auth and we can auth, let's do it
pk, _ := kr.GetPublicKey(ctx) pk, _ := kr.GetPublicKey(ctx)
log("performing auth as %s... ", pk) npub, _ := nip19.EncodePublicKey(pk)
log("authenticating as %s... ", color.YellowString("%s…%s", npub[0:7], npub[58:]))
if err := relay.Auth(ctx, func(authEvent *nostr.Event) error { if err := relay.Auth(ctx, func(authEvent *nostr.Event) error {
return kr.SignEvent(ctx, authEvent) return kr.SignEvent(ctx, authEvent)
}); err == nil { }); err == nil {
@@ -326,6 +407,8 @@ example:
} }
log("failed: %s\n", err) log("failed: %s\n", err)
} }
}
if len(successRelays) > 0 && c.Bool("nevent") { if len(successRelays) > 0 && c.Bool("nevent") {
nevent, _ := nip19.EncodeEvent(evt.ID, successRelays, evt.PubKey) nevent, _ := nip19.EncodeEvent(evt.ID, successRelays, evt.PubKey)
log(nevent + "\n") log(nevent + "\n")

8
go.mod
View File

@@ -17,9 +17,10 @@ require (
github.com/mailru/easyjson v0.9.0 github.com/mailru/easyjson v0.9.0
github.com/mark3labs/mcp-go v0.8.3 github.com/mark3labs/mcp-go v0.8.3
github.com/markusmobius/go-dateparser v1.2.3 github.com/markusmobius/go-dateparser v1.2.3
github.com/nbd-wtf/go-nostr v0.51.6 github.com/nbd-wtf/go-nostr v0.51.8
github.com/urfave/cli/v3 v3.0.0-beta1 github.com/urfave/cli/v3 v3.0.0-beta1
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 golang.org/x/exp v0.0.0-20250305212735-054e65f0b394
golang.org/x/term v0.30.0
) )
require ( require (
@@ -28,13 +29,13 @@ require (
github.com/btcsuite/btcd v0.24.2 // indirect github.com/btcsuite/btcd v0.24.2 // indirect
github.com/btcsuite/btcd/btcutil v1.1.5 // indirect github.com/btcsuite/btcd/btcutil v1.1.5 // indirect
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect
github.com/bytedance/sonic v1.13.1 // indirect github.com/bytedance/sonic v1.13.2 // indirect
github.com/bytedance/sonic/loader v0.2.4 // indirect github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/chzyer/logex v1.1.10 // indirect github.com/chzyer/logex v1.1.10 // indirect
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect github.com/cloudwego/base64x v0.1.5 // indirect
github.com/coder/websocket v1.8.12 // indirect github.com/coder/websocket v1.8.13 // indirect
github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect
github.com/dgraph-io/ristretto v1.0.0 // indirect github.com/dgraph-io/ristretto v1.0.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
@@ -43,7 +44,6 @@ require (
github.com/fasthttp/websocket v1.5.12 // indirect github.com/fasthttp/websocket v1.5.12 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/graph-gophers/dataloader/v7 v7.1.0 // indirect
github.com/hablullah/go-hijri v1.0.2 // indirect github.com/hablullah/go-hijri v1.0.2 // indirect
github.com/hablullah/go-juliandays v1.0.0 // indirect github.com/hablullah/go-juliandays v1.0.0 // indirect
github.com/jalaali/go-jalaali v0.0.0-20210801064154-80525e88d958 // indirect github.com/jalaali/go-jalaali v0.0.0-20210801064154-80525e88d958 // indirect

16
go.sum
View File

@@ -33,8 +33,8 @@ github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku
github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
github.com/bytedance/sonic v1.13.1 h1:Jyd5CIvdFnkOWuKXr+wm4Nyk2h0yAFsr8ucJgEasO3g= github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=
github.com/bytedance/sonic v1.13.1/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= 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 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
@@ -49,8 +49,8 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= 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/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.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE=
github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= github.com/coder/websocket v1.8.13/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 v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -102,8 +102,6 @@ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/graph-gophers/dataloader/v7 v7.1.0 h1:Wn8HGF/q7MNXcvfaBnLEPEFJttVHR8zuEqP1obys/oc=
github.com/graph-gophers/dataloader/v7 v7.1.0/go.mod h1:1bKE0Dm6OUcTB/OAuYVOZctgIz7Q3d0XrYtlIzTgg6Q=
github.com/hablullah/go-hijri v1.0.2 h1:drT/MZpSZJQXo7jftf5fthArShcaMtsal0Zf/dnmp6k= github.com/hablullah/go-hijri v1.0.2 h1:drT/MZpSZJQXo7jftf5fthArShcaMtsal0Zf/dnmp6k=
github.com/hablullah/go-hijri v1.0.2/go.mod h1:OS5qyYLDjORXzK4O1adFw9Q5WfhOcMdAKglDkcTxgWQ= github.com/hablullah/go-hijri v1.0.2/go.mod h1:OS5qyYLDjORXzK4O1adFw9Q5WfhOcMdAKglDkcTxgWQ=
github.com/hablullah/go-juliandays v1.0.0 h1:A8YM7wIj16SzlKT0SRJc9CD29iiaUzpBLzh5hr0/5p0= github.com/hablullah/go-juliandays v1.0.0 h1:A8YM7wIj16SzlKT0SRJc9CD29iiaUzpBLzh5hr0/5p0=
@@ -151,8 +149,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 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 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/nbd-wtf/go-nostr v0.51.6 h1:H51l39mp4dJztvtxjTNfNqNIxQyMoJMLSKt+1aGq3UU= github.com/nbd-wtf/go-nostr v0.51.8 h1:CIoS+YqChcm4e1L1rfMZ3/mIwTz4CwApM2qx7MHNzmE=
github.com/nbd-wtf/go-nostr v0.51.6/go.mod h1:so45r53GkZq+8vkdIW2jBlKEjdHyxEMrl/1g9tEoSFQ= github.com/nbd-wtf/go-nostr v0.51.8/go.mod h1:d6+DfvMWYG5pA3dmNMBJd6WCHVDDhkXbHqvfljf0Gzg=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
@@ -239,6 +237,8 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=

View File

@@ -3,6 +3,7 @@ package main
import ( import (
"bufio" "bufio"
"context" "context"
"errors"
"fmt" "fmt"
"iter" "iter"
"math/rand" "math/rand"
@@ -10,15 +11,19 @@ import (
"net/textproto" "net/textproto"
"net/url" "net/url"
"os" "os"
"runtime"
"slices" "slices"
"strings" "strings"
"sync"
"time" "time"
"github.com/fatih/color" "github.com/fatih/color"
jsoniter "github.com/json-iterator/go" jsoniter "github.com/json-iterator/go"
"github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nip19"
"github.com/nbd-wtf/go-nostr/sdk" "github.com/nbd-wtf/go-nostr/sdk"
"github.com/urfave/cli/v3" "github.com/urfave/cli/v3"
"golang.org/x/term"
) )
var sys *sdk.System var sys *sdk.System
@@ -148,8 +153,9 @@ func normalizeAndValidateRelayURLs(wsurls []string) error {
func connectToAllRelays( func connectToAllRelays(
ctx context.Context, ctx context.Context,
c *cli.Command,
relayUrls []string, relayUrls []string,
forcePreAuth bool, preAuthSigner func(ctx context.Context, c *cli.Command, log func(s string, args ...any), authEvent nostr.RelayEvent) (err error), // if this exists we will force preauth
opts ...nostr.PoolOption, opts ...nostr.PoolOption,
) []*nostr.Relay { ) []*nostr.Relay {
sys.Pool = nostr.NewSimplePool(context.Background(), sys.Pool = nostr.NewSimplePool(context.Background(),
@@ -163,50 +169,174 @@ func connectToAllRelays(
) )
relays := make([]*nostr.Relay, 0, len(relayUrls)) relays := make([]*nostr.Relay, 0, len(relayUrls))
relayLoop:
for _, url := range relayUrls { if supportsDynamicMultilineMagic() {
log("connecting to %s... ", url) // overcomplicated multiline rendering magic
if relay, err := sys.Pool.EnsureRelay(url); err == nil { lines := make([][][]byte, len(relayUrls))
if forcePreAuth { flush := func() {
log("waiting for auth challenge... ") for _, line := range lines {
signer := opts[0].(nostr.WithAuthHandler) for _, part := range line {
time.Sleep(time.Millisecond * 200) os.Stderr.Write(part)
challengeWaitLoop:
for {
// beginhack
// here starts the biggest and ugliest hack of this codebase
if err := relay.Auth(ctx, func(authEvent *nostr.Event) error {
challengeTag := authEvent.Tags.GetFirst([]string{"challenge", ""})
if (*challengeTag)[1] == "" {
return fmt.Errorf("auth not received yet *****")
} }
return signer(ctx, nostr.RelayEvent{Event: authEvent, Relay: relay}) os.Stderr.Write([]byte{'\n'})
}
}
render := func() {
clearLines(len(lines))
flush()
}
flush()
wg := sync.WaitGroup{}
wg.Add(len(relayUrls))
for i, url := range relayUrls {
lines[i] = make([][]byte, 1, 2)
logthis := func(s string, args ...any) {
lines[i] = append(lines[i], []byte(fmt.Sprintf(s, args...)))
render()
}
colorizepreamble := func(c func(string, ...any) string) {
lines[i][0] = []byte(fmt.Sprintf("%s... ", c(url)))
}
colorizepreamble(color.CyanString)
go func() {
relay := connectToSingleRelay(ctx, c, url, preAuthSigner, colorizepreamble, logthis)
if relay != nil {
relays = append(relays, relay)
}
wg.Done()
}()
}
wg.Wait()
} else {
// simple flow
for _, url := range relayUrls {
log("connecting to %s... ", color.CyanString(url))
relay := connectToSingleRelay(ctx, c, url, preAuthSigner, nil, log)
if relay != nil {
relays = append(relays, relay)
}
log("\n")
}
}
return relays
}
func connectToSingleRelay(
ctx context.Context,
c *cli.Command,
url string,
preAuthSigner func(ctx context.Context, c *cli.Command, log func(s string, args ...any), authEvent nostr.RelayEvent) (err error),
colorizepreamble func(c func(string, ...any) string),
logthis func(s string, args ...any),
) *nostr.Relay {
if relay, err := sys.Pool.EnsureRelay(url); err == nil {
if preAuthSigner != nil {
if colorizepreamble != nil {
colorizepreamble(color.YellowString)
}
logthis("waiting for auth challenge... ")
time.Sleep(time.Millisecond * 200)
for range 5 {
if err := relay.Auth(ctx, func(authEvent *nostr.Event) error {
challengeTag := authEvent.Tags.Find("challenge")
if challengeTag[1] == "" {
return fmt.Errorf("auth not received yet *****") // what a giant hack
}
return preAuthSigner(ctx, c, logthis, nostr.RelayEvent{Event: authEvent, Relay: relay})
}); err == nil { }); err == nil {
// auth succeeded // auth succeeded
break challengeWaitLoop goto preauthSuccess
} else { } else {
// auth failed // auth failed
if strings.HasSuffix(err.Error(), "auth not received yet *****") { if strings.HasSuffix(err.Error(), "auth not received yet *****") {
// it failed because we didn't receive the challenge yet, so keep waiting // it failed because we didn't receive the challenge yet, so keep waiting
time.Sleep(time.Second) time.Sleep(time.Second)
continue challengeWaitLoop continue
} else { } else {
// it failed for some other reason, so skip this relay // it failed for some other reason, so skip this relay
log(err.Error() + "\n") if colorizepreamble != nil {
continue relayLoop colorizepreamble(colors.errorf)
}
logthis(err.Error())
return nil
} }
} }
// endhack }
if colorizepreamble != nil {
colorizepreamble(colors.errorf)
}
logthis("failed to get an AUTH challenge in enough time.")
return nil
}
preauthSuccess:
if colorizepreamble != nil {
colorizepreamble(colors.successf)
}
logthis("ok.")
return relay
} else {
if colorizepreamble != nil {
colorizepreamble(colors.errorf)
}
// if we're here that means we've failed to connect, this may be a huge message
// but we're likely to only be interested in the lowest level error (although we can leave space)
logthis(clampError(err, len(url)+12))
return nil
} }
} }
relays = append(relays, relay) func clearLines(lineCount int) {
log("ok.\n") for i := 0; i < lineCount; i++ {
} else { os.Stderr.Write([]byte("\033[0A\033[2K\r"))
log(err.Error() + "\n")
} }
} }
return relays
func supportsDynamicMultilineMagic() bool {
if runtime.GOOS == "windows" {
return false
}
if !term.IsTerminal(0) {
return false
}
width, _, err := term.GetSize(0)
if err != nil {
return false
}
if width < 110 {
return false
}
return true
}
func authSigner(ctx context.Context, c *cli.Command, log func(s string, args ...any), authEvent nostr.RelayEvent) (err error) {
defer func() {
if err != nil {
cleanUrl, _ := strings.CutPrefix(authEvent.Relay.URL, "wss://")
log("%s auth failed: %s", colors.errorf(cleanUrl), err)
}
}()
if !c.Bool("auth") && !c.Bool("force-pre-auth") {
return fmt.Errorf("auth required, but --auth flag not given")
}
kr, _, err := gatherKeyerFromArguments(ctx, c)
if err != nil {
return err
}
pk, _ := kr.GetPublicKey(ctx)
npub, _ := nip19.EncodePublicKey(pk)
log("authenticating as %s... ", color.YellowString("%s…%s", npub[0:7], npub[58:]))
return kr.SignEvent(ctx, authEvent.Event)
} }
func lineProcessingError(ctx context.Context, msg string, args ...any) context.Context { func lineProcessingError(ctx context.Context, msg string, args ...any) context.Context {
@@ -234,16 +364,50 @@ func leftPadKey(k string) string {
return strings.Repeat("0", 64-len(k)) + k return strings.Repeat("0", 64-len(k)) + k
} }
func unwrapAll(err error) error {
low := err
for n := low; n != nil; n = errors.Unwrap(low) {
low = n
}
return low
}
func clampMessage(msg string, prefixAlreadyPrinted int) string {
termSize, _, _ := term.GetSize(0)
if len(msg) > termSize-prefixAlreadyPrinted {
msg = msg[0:termSize-prefixAlreadyPrinted-1] + "…"
}
return msg
}
func clampError(err error, prefixAlreadyPrinted int) string {
termSize, _, _ := term.GetSize(0)
msg := err.Error()
if len(msg) > termSize-prefixAlreadyPrinted {
err = unwrapAll(err)
msg = clampMessage(err.Error(), prefixAlreadyPrinted)
}
return msg
}
var colors = struct { var colors = struct {
reset func(...any) (int, error) reset func(...any) (int, error)
italic func(...any) string italic func(...any) string
italicf func(string, ...any) string italicf func(string, ...any) string
bold func(...any) string bold func(...any) string
boldf func(string, ...any) string boldf func(string, ...any) string
error func(...any) string
errorf func(string, ...any) string
success func(...any) string
successf func(string, ...any) string
}{ }{
color.New(color.Reset).Print, color.New(color.Reset).Print,
color.New(color.Italic).Sprint, color.New(color.Italic).Sprint,
color.New(color.Italic).Sprintf, color.New(color.Italic).Sprintf,
color.New(color.Bold).Sprint, color.New(color.Bold).Sprint,
color.New(color.Bold).Sprintf, color.New(color.Bold).Sprintf,
color.New(color.Bold, color.FgHiRed).Sprint,
color.New(color.Bold, color.FgHiRed).Sprintf,
color.New(color.Bold, color.FgHiGreen).Sprint,
color.New(color.Bold, color.FgHiGreen).Sprintf,
} }

View File

@@ -348,11 +348,7 @@ func (e *EntityDir) handleWrite() {
success := false success := false
first := true first := true
for res := range e.root.sys.Pool.PublishMany(e.root.ctx, relays, evt) { for res := range e.root.sys.Pool.PublishMany(e.root.ctx, relays, evt) {
cleanUrl, ok := strings.CutPrefix(res.RelayURL, "wss://") cleanUrl, _ := strings.CutPrefix(res.RelayURL, "wss://")
if !ok {
cleanUrl = res.RelayURL
}
if !first { if !first {
log(", ") log(", ")
} }

View File

@@ -18,6 +18,7 @@ import (
"github.com/nbd-wtf/go-nostr/nip19" "github.com/nbd-wtf/go-nostr/nip19"
"github.com/nbd-wtf/go-nostr/nip22" "github.com/nbd-wtf/go-nostr/nip22"
"github.com/nbd-wtf/go-nostr/nip27" "github.com/nbd-wtf/go-nostr/nip27"
"github.com/nbd-wtf/go-nostr/nip73"
"github.com/nbd-wtf/go-nostr/nip92" "github.com/nbd-wtf/go-nostr/nip92"
sdk "github.com/nbd-wtf/go-nostr/sdk" sdk "github.com/nbd-wtf/go-nostr/sdk"
) )
@@ -191,7 +192,7 @@ func (r *NostrRoot) CreateEventDir(
} }
} else if event.Kind == 1111 { } else if event.Kind == 1111 {
if pointer := nip22.GetThreadRoot(event.Tags); pointer != nil { if pointer := nip22.GetThreadRoot(event.Tags); pointer != nil {
if xp, ok := pointer.(nostr.ExternalPointer); ok { if xp, ok := pointer.(nip73.ExternalPointer); ok {
h.AddChild("@root", h.NewPersistentInode( h.AddChild("@root", h.NewPersistentInode(
r.ctx, r.ctx,
&fs.MemRegularFile{ &fs.MemRegularFile{
@@ -211,7 +212,7 @@ func (r *NostrRoot) CreateEventDir(
} }
} }
if pointer := nip22.GetImmediateParent(event.Tags); pointer != nil { if pointer := nip22.GetImmediateParent(event.Tags); pointer != nil {
if xp, ok := pointer.(nostr.ExternalPointer); ok { if xp, ok := pointer.(nip73.ExternalPointer); ok {
h.AddChild("@parent", h.NewPersistentInode( h.AddChild("@parent", h.NewPersistentInode(
r.ctx, r.ctx,
&fs.MemRegularFile{ &fs.MemRegularFile{

View File

@@ -179,11 +179,7 @@ func (n *ViewDir) publishNote() {
success := false success := false
first := true first := true
for res := range n.root.sys.Pool.PublishMany(n.root.ctx, relays, evt) { for res := range n.root.sys.Pool.PublishMany(n.root.ctx, relays, evt) {
cleanUrl, ok := strings.CutPrefix(res.RelayURL, "wss://") cleanUrl, _ := strings.CutPrefix(res.RelayURL, "wss://")
if !ok {
cleanUrl = res.RelayURL
}
if !first { if !first {
log(", ") log(", ")
} }

48
req.go
View File

@@ -6,6 +6,7 @@ import (
"os" "os"
"strings" "strings"
"github.com/fatih/color"
"github.com/mailru/easyjson" "github.com/mailru/easyjson"
"github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nip77" "github.com/nbd-wtf/go-nostr/nip77"
@@ -76,35 +77,29 @@ example:
Action: func(ctx context.Context, c *cli.Command) error { Action: func(ctx context.Context, c *cli.Command) error {
relayUrls := c.Args().Slice() relayUrls := c.Args().Slice()
if len(relayUrls) > 0 { if len(relayUrls) > 0 {
relays := connectToAllRelays(ctx, // this is used both for the normal AUTH (after "auth-required:" is received) or forced pre-auth
// connect to all relays we expect to use in this call in parallel
forcePreAuthSigner := authSigner
if !c.Bool("force-pre-auth") {
forcePreAuthSigner = nil
}
relays := connectToAllRelays(
ctx,
c,
relayUrls, relayUrls,
c.Bool("force-pre-auth"), forcePreAuthSigner,
nostr.WithAuthHandler( nostr.WithAuthHandler(func(ctx context.Context, authEvent nostr.RelayEvent) error {
func(ctx context.Context, authEvent nostr.RelayEvent) (err error) { return authSigner(ctx, c, func(s string, args ...any) {
defer func() { if strings.HasPrefix(s, "authenticating as") {
if err != nil { cleanUrl, _ := strings.CutPrefix(authEvent.Relay.URL, "wss://")
log("auth to %s failed: %s\n", s = "authenticating to " + color.CyanString(cleanUrl) + " as" + s[len("authenticating as"):]
(*authEvent.Tags.GetFirst([]string{"relay", ""}))[1], }
err, log(s+"\n", args...)
}, authEvent)
}),
) )
}
}()
if !c.Bool("auth") && !c.Bool("force-pre-auth") { // stop here already if all connections failed
return fmt.Errorf("auth not authorized")
}
kr, _, err := gatherKeyerFromArguments(ctx, c)
if err != nil {
return err
}
pk, _ := kr.GetPublicKey(ctx)
log("performing auth as %s... ", pk)
return kr.SignEvent(ctx, authEvent.Event)
},
),
)
if len(relays) == 0 { if len(relays) == 0 {
log("failed to connect to any of the given relays.\n") log("failed to connect to any of the given relays.\n")
os.Exit(3) os.Exit(3)
@@ -121,6 +116,7 @@ example:
}() }()
} }
// go line by line from stdin or run once with input from flags
for stdinFilter := range getJsonsOrBlank() { for stdinFilter := range getJsonsOrBlank() {
filter := nostr.Filter{} filter := nostr.Filter{}
if stdinFilter != "" { if stdinFilter != "" {

View File

@@ -6,7 +6,6 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/fatih/color"
"github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nip60" "github.com/nbd-wtf/go-nostr/nip60"
"github.com/nbd-wtf/go-nostr/nip61" "github.com/nbd-wtf/go-nostr/nip61"
@@ -60,20 +59,16 @@ func prepareWallet(ctx context.Context, c *cli.Command) (*nip60.Wallet, func(),
log("- saving kind:%d event (%s)... ", event.Kind, desc) log("- saving kind:%d event (%s)... ", event.Kind, desc)
first := true first := true
for res := range sys.Pool.PublishMany(ctx, relays, event) { for res := range sys.Pool.PublishMany(ctx, relays, event) {
cleanUrl, ok := strings.CutPrefix(res.RelayURL, "wss://") cleanUrl, _ := strings.CutPrefix(res.RelayURL, "wss://")
if !ok {
cleanUrl = res.RelayURL
}
if !first { if !first {
log(", ") log(", ")
} }
first = false first = false
if res.Error != nil { if res.Error != nil {
log("%s: %s", color.RedString(cleanUrl), res.Error) log("%s: %s", colors.errorf(cleanUrl), res.Error)
} else { } else {
log("%s: ok", color.GreenString(cleanUrl)) log("%s: ok", colors.successf(cleanUrl))
} }
} }
log("\n") log("\n")
@@ -376,19 +371,15 @@ var wallet = &cli.Command{
log("- publishing nutzap... ") log("- publishing nutzap... ")
first := true first := true
for res := range results { for res := range results {
cleanUrl, ok := strings.CutPrefix(res.RelayURL, "wss://") cleanUrl, _ := strings.CutPrefix(res.RelayURL, "wss://")
if !ok {
cleanUrl = res.RelayURL
}
if !first { if !first {
log(", ") log(", ")
} }
first = false first = false
if res.Error != nil { if res.Error != nil {
log("%s: %s", color.RedString(cleanUrl), res.Error) log("%s: %s", colors.errorf(cleanUrl), res.Error)
} else { } else {
log("%s: ok", color.GreenString(cleanUrl)) log("%s: ok", colors.successf(cleanUrl))
} }
} }
@@ -463,10 +454,7 @@ var wallet = &cli.Command{
log("- saving kind:10019 event... ") log("- saving kind:10019 event... ")
first := true first := true
for res := range sys.Pool.PublishMany(ctx, relays, evt) { for res := range sys.Pool.PublishMany(ctx, relays, evt) {
cleanUrl, ok := strings.CutPrefix(res.RelayURL, "wss://") cleanUrl, _ := strings.CutPrefix(res.RelayURL, "wss://")
if !ok {
cleanUrl = res.RelayURL
}
if !first { if !first {
log(", ") log(", ")
@@ -474,9 +462,9 @@ var wallet = &cli.Command{
first = false first = false
if res.Error != nil { if res.Error != nil {
log("%s: %s", color.RedString(cleanUrl), res.Error) log("%s: %s", colors.errorf(cleanUrl), res.Error)
} else { } else {
log("%s: ok", color.GreenString(cleanUrl)) log("%s: ok", colors.successf(cleanUrl))
} }
} }