From 9547711e8df33d3f4a4c1998870cb9e2312c1a85 Mon Sep 17 00:00:00 2001 From: fiatjaf <fiatjaf@gmail.com> Date: Thu, 3 Apr 2025 11:42:33 -0300 Subject: [PATCH] nice dynamic UI when connecting to relays, and go much faster concurrently. --- .gitignore | 1 + bunker.go | 4 +- count.go | 7 +-- dvm.go | 2 +- event.go | 14 +++-- go.mod | 1 + go.sum | 2 + helpers.go | 182 +++++++++++++++++++++++++++++++++++++++++------------ req.go | 65 +++++++++++-------- 9 files changed, 199 insertions(+), 79 deletions(-) diff --git a/.gitignore b/.gitignore index 9f82d74..5f39b84 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ nak mnt +nak.exe diff --git a/bunker.go b/bunker.go index a1eedba..0787e18 100644 --- a/bunker.go +++ b/bunker.go @@ -11,10 +11,10 @@ import ( "time" "github.com/fatih/color" - "github.com/urfave/cli/v3" "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/nip19" "github.com/nbd-wtf/go-nostr/nip46" + "github.com/urfave/cli/v3" ) var bunker = &cli.Command{ @@ -49,7 +49,7 @@ var bunker = &cli.Command{ qs := url.Values{} relayURLs := make([]string, 0, c.Args().Len()) if relayUrls := c.Args().Slice(); len(relayUrls) > 0 { - relays := connectToAllRelays(ctx, relayUrls, false) + relays := connectToAllRelays(ctx, relayUrls, nil) if len(relays) == 0 { log("failed to connect to any of the given relays.\n") os.Exit(3) diff --git a/count.go b/count.go index 4599c0b..26157b0 100644 --- a/count.go +++ b/count.go @@ -6,10 +6,10 @@ import ( "os" "strings" - "github.com/urfave/cli/v3" "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/nip45" "github.com/nbd-wtf/go-nostr/nip45/hyperloglog" + "github.com/urfave/cli/v3" ) var count = &cli.Command{ @@ -70,10 +70,7 @@ var count = &cli.Command{ biggerUrlSize := 0 relayUrls := c.Args().Slice() if len(relayUrls) > 0 { - relays := connectToAllRelays(ctx, - relayUrls, - false, - ) + relays := connectToAllRelays(ctx, relayUrls, nil) if len(relays) == 0 { log("failed to connect to any of the given relays.\n") os.Exit(3) diff --git a/dvm.go b/dvm.go index f4142fc..88ed9cc 100644 --- a/dvm.go +++ b/dvm.go @@ -60,7 +60,7 @@ var dvm = &cli.Command{ Flags: flags, Action: func(ctx context.Context, c *cli.Command) error { relayUrls := c.StringSlice("relay") - relays := connectToAllRelays(ctx, relayUrls, false) + relays := connectToAllRelays(ctx, relayUrls, nil) if len(relays) == 0 { log("failed to connect to any of the given relays.\n") os.Exit(3) diff --git a/event.go b/event.go index dbc2a7c..5bae947 100644 --- a/event.go +++ b/event.go @@ -134,7 +134,7 @@ example: // try to connect to the relays here var relays []*nostr.Relay if relayUrls := c.Args().Slice(); len(relayUrls) > 0 { - relays = connectToAllRelays(ctx, relayUrls, false) + relays = connectToAllRelays(ctx, relayUrls, nil) if len(relays) == 0 { log("failed to connect to any of the given relays.\n") os.Exit(3) @@ -209,13 +209,19 @@ example: } 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") { - 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") { - tags = tags.AppendUnique([]string{"d", dtag}) + if tags.FindWithValue("d", dtag) == nil { + tags = append(tags, nostr.Tag{"d", dtag}) + } } if len(tags) > 0 { for _, tag := range tags { diff --git a/go.mod b/go.mod index 81cc8a0..4b7b3a6 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,7 @@ require ( github.com/nbd-wtf/go-nostr v0.51.8 github.com/urfave/cli/v3 v3.0.0-beta1 golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 + golang.org/x/term v0.30.0 ) require ( diff --git a/go.sum b/go.sum index 3c2ec46..9a3a22d 100644 --- a/go.sum +++ b/go.sum @@ -247,6 +247,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.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 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.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= diff --git a/helpers.go b/helpers.go index 70b8789..1e4a0db 100644 --- a/helpers.go +++ b/helpers.go @@ -10,8 +10,10 @@ import ( "net/textproto" "net/url" "os" + "runtime" "slices" "strings" + "sync" "time" "github.com/fatih/color" @@ -19,6 +21,7 @@ import ( "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/sdk" "github.com/urfave/cli/v3" + "golang.org/x/term" ) var sys *sdk.System @@ -149,7 +152,7 @@ func normalizeAndValidateRelayURLs(wsurls []string) error { func connectToAllRelays( ctx context.Context, relayUrls []string, - forcePreAuth bool, + preAuthSigner func(ctx context.Context, log func(s string, args ...any), authEvent nostr.RelayEvent) (err error), // if this exists we will force preauth opts ...nostr.PoolOption, ) []*nostr.Relay { sys.Pool = nostr.NewSimplePool(context.Background(), @@ -163,52 +166,149 @@ func connectToAllRelays( ) relays := make([]*nostr.Relay, 0, len(relayUrls)) -relayLoop: - for _, url := range relayUrls { - log("connecting to %s... ", url) - if relay, err := sys.Pool.EnsureRelay(url); err == nil { - if forcePreAuth { - log("waiting for auth challenge... ") - signer := opts[0].(nostr.WithAuthHandler) - time.Sleep(time.Millisecond * 200) - 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.Find("challenge") - if challengeTag[1] == "" { - return fmt.Errorf("auth not received yet *****") - } - return signer(ctx, nostr.RelayEvent{Event: authEvent, Relay: relay}) - }); err == nil { - // auth succeeded - break challengeWaitLoop - } else { - // auth failed - if strings.HasSuffix(err.Error(), "auth not received yet *****") { - // it failed because we didn't receive the challenge yet, so keep waiting - time.Sleep(time.Second) - continue challengeWaitLoop - } else { - // it failed for some other reason, so skip this relay - log(err.Error() + "\n") - continue relayLoop - } - } - // endhack - } - } - relays = append(relays, relay) - log("ok.\n") - } else { - log(err.Error() + "\n") + if supportsDynamicMultilineMagic() { + // overcomplicated multiline rendering magic + lines := make([][][]byte, len(relayUrls)) + 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() + + 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, 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... ", url) + relay := connectToSingleRelay(ctx, url, preAuthSigner, nil, log) + if relay != nil { + relays = append(relays, relay) + } + log("\n") } } + return relays } +func connectToSingleRelay( + ctx context.Context, + url string, + preAuthSigner func(ctx context.Context, 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, logthis, nostr.RelayEvent{Event: authEvent, Relay: relay}) + }); err == nil { + // auth succeeded + goto preauthSuccess + } else { + // auth failed + if strings.HasSuffix(err.Error(), "auth not received yet *****") { + // it failed because we didn't receive the challenge yet, so keep waiting + time.Sleep(time.Second) + continue + } else { + // it failed for some other reason, so skip this relay + if colorizepreamble != nil { + colorizepreamble(color.RedString) + } + logthis(err.Error()) + return nil + } + } + } + if colorizepreamble != nil { + colorizepreamble(color.RedString) + } + logthis("failed to get an AUTH challenge in enough time.") + return nil + } + + preauthSuccess: + if colorizepreamble != nil { + colorizepreamble(color.GreenString) + } + logthis("ok.") + return relay + } else { + if colorizepreamble != nil { + colorizepreamble(color.RedString) + } + logthis(err.Error()) + return nil + } +} + +func clearLines(lineCount int) { + for i := 0; i < lineCount; i++ { + os.Stderr.Write([]byte("\033[0A\033[2K\r")) + } +} + +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 lineProcessingError(ctx context.Context, msg string, args ...any) context.Context { log(msg+"\n", args...) return context.WithValue(ctx, LINE_PROCESSING_ERROR, true) diff --git a/req.go b/req.go index 854f1d7..559088e 100644 --- a/req.go +++ b/req.go @@ -8,6 +8,7 @@ import ( "github.com/mailru/easyjson" "github.com/nbd-wtf/go-nostr" + "github.com/nbd-wtf/go-nostr/nip19" "github.com/nbd-wtf/go-nostr/nip77" "github.com/urfave/cli/v3" ) @@ -76,35 +77,46 @@ example: Action: func(ctx context.Context, c *cli.Command) error { relayUrls := c.Args().Slice() if len(relayUrls) > 0 { + // this is used both for the normal AUTH (after "auth-required:" is received) or forced pre-auth + authSigner := func(ctx context.Context, log func(s string, args ...any), authEvent nostr.RelayEvent) (err error) { + defer func() { + if err != nil { + log("auth to %s failed: %s", + authEvent.Tags.Find("relay")[1], + err, + ) + } + }() + + if !c.Bool("auth") && !c.Bool("force-pre-auth") { + return fmt.Errorf("auth not authorized") + } + kr, _, err := gatherKeyerFromArguments(ctx, c) + if err != nil { + return err + } + + pk, _ := kr.GetPublicKey(ctx) + npub, _ := nip19.EncodePublicKey(pk) + log("performing auth as %s…%s... ", npub[0:7], npub[58:]) + + return kr.SignEvent(ctx, authEvent.Event) + } + + // 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, relayUrls, - c.Bool("force-pre-auth"), - nostr.WithAuthHandler( - func(ctx context.Context, authEvent nostr.RelayEvent) (err error) { - defer func() { - if err != nil { - log("auth to %s failed: %s\n", - authEvent.Tags.Find("relay")[1], - err, - ) - } - }() - - if !c.Bool("auth") && !c.Bool("force-pre-auth") { - 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) - }, - ), + forcePreAuthSigner, + nostr.WithAuthHandler(func(ctx context.Context, authEvent nostr.RelayEvent) error { + return authSigner(ctx, func(s string, args ...any) { log(s+"\n", args...) }, authEvent) + }), ) + + // stop here already if all connections failed if len(relays) == 0 { log("failed to connect to any of the given relays.\n") os.Exit(3) @@ -121,6 +133,7 @@ example: }() } + // go line by line from stdin or run once with input from flags for stdinFilter := range getJsonsOrBlank() { filter := nostr.Filter{} if stdinFilter != "" {