From cc4f3a0bf56df9813c79b775aded9c03bc72c3b9 Mon Sep 17 00:00:00 2001 From: Anthony Accioly <1591739+aaccioly@users.noreply.github.com> Date: Fri, 4 Jul 2025 12:40:31 +0100 Subject: [PATCH] feat(bunker): introduce command interface with QR code and help commands - Add interactive command interface for the bunker. - Implement `help`, `info`, `qr`, and `exit` commands for user convenience. - Display QR code on demand using `qr` command. - Ensure proper command handling with locking mechanism and safe shutdown using `exit`. --- bunker.go | 169 ++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 119 insertions(+), 50 deletions(-) diff --git a/bunker.go b/bunker.go index 850e2f9..3ae9908 100644 --- a/bunker.go +++ b/bunker.go @@ -1,6 +1,7 @@ package main import ( + "bufio" "bytes" "context" "encoding/hex" @@ -229,6 +230,31 @@ var bunker = &cli.Command{ // static information pubkey := sec.Public() npub := nip19.EncodeNpub(pubkey) + printLock := sync.Mutex{} + exitChan := make(chan bool, 1) + + // == SUB COMMANDS == + + // printHelp displays available commands for the bunker interface + printHelp := func() { + log("%s\n", color.CyanString("Available Commands:")) + log(" %s - Show this help message\n", color.GreenString("help, h, ?")) + log(" %s - Display current bunker information\n", color.GreenString("info, i")) + log(" %s - Generate and display QR code for the bunker URI\n", color.GreenString("qr")) + log(" %s - Shutdown the bunker\n", color.GreenString("exit, quit, q")) + log("\n") + } + + // Create a function to print QR code on demand + printQR := func() { + qs.Set("secret", newSecret) + bunkerURI := fmt.Sprintf("bunker://%s?%s", pubkey.Hex(), qs.Encode()) + printLock.Lock() + log("\nQR Code for bunker URI:\n") + qrterminal.Generate(bunkerURI, qrterminal.L, os.Stdout) + log("\n\n") + printLock.Unlock() + } // this function will be called every now and then printBunkerInfo := func() { @@ -301,11 +327,30 @@ var bunker = &cli.Command{ // print QR code if requested if c.Bool("qrcode") { - log("QR Code for bunker URI:\n") - qrterminal.Generate(bunkerURI, qrterminal.L, os.Stdout) - log("\n\n") + printQR() } } + + // handleBunkerCommand processes user commands in the bunker interface + handleBunkerCommand := func(command string) { + switch strings.ToLower(command) { + case "help", "h", "?": + printHelp() + case "info", "i": + printBunkerInfo() + case "qr": + printQR() + case "exit", "quit", "q": + log("Exit command received.\n") + exitChan <- true + case "": + // Ignore empty commands + default: + log("Unknown command: %s. Type 'help' for available commands.\n", command) + } + } + + // Print initial bunker information printBunkerInfo() // subscribe to relays @@ -320,7 +365,6 @@ var bunker = &cli.Command{ signer := nip46.NewStaticKeySigner(sec) handlerWg := sync.WaitGroup{} - printLock := sync.Mutex{} // just a gimmick var cancelPreviousBunkerInfoPrint context.CancelFunc @@ -348,56 +392,81 @@ var bunker = &cli.Command{ return slices.Contains(config.AuthorizedKeys, from) || slices.Contains(authorizedSecrets, secret) } - for ie := range events { - cancelPreviousBunkerInfoPrint() // this prevents us from printing a million bunker info blocks + // Start command input handler in a separate goroutine + go func() { + scanner := bufio.NewScanner(os.Stdin) + for scanner.Scan() { + command := strings.TrimSpace(scanner.Text()) + handleBunkerCommand(command) + } + if err := scanner.Err(); err != nil { + log("error reading command: %v\n", err) + } + }() - // handle the NIP-46 request event - req, resp, eventResponse, err := signer.HandleRequest(ctx, ie.Event) - if err != nil { - log("< failed to handle request from %s: %s\n", ie.Event.PubKey, err.Error()) + // Print initial command help + log("%s\nType 'help' for available commands or 'exit' to quit.\n%s\n", + color.CyanString("--------------- Bunker Command Interface ---------------"), + color.CyanString("--------------------------------------------------------")) + + for { + // Check if exit was requested first + select { + case <-exitChan: + log("Shutting down bunker...\n") + return nil + case ie := <-events: + cancelPreviousBunkerInfoPrint() // this prevents us from printing a million bunker info blocks + + // handle the NIP-46 request event + req, resp, eventResponse, err := signer.HandleRequest(ctx, ie.Event) + if err != nil { + log("< failed to handle request from %s: %s\n", ie.Event.PubKey, err.Error()) + continue + } + + jreq, _ := json.MarshalIndent(req, "", " ") + log("- got request from '%s': %s\n", color.New(color.Bold, color.FgBlue).Sprint(ie.Event.PubKey.Hex()), string(jreq)) + jresp, _ := json.MarshalIndent(resp, "", " ") + log("~ responding with %s\n", string(jresp)) + + handlerWg.Add(len(relayURLs)) + for _, relayURL := range relayURLs { + go func(relayURL string) { + if relay, _ := sys.Pool.EnsureRelay(relayURL); relay != nil { + err := relay.Publish(ctx, eventResponse) + printLock.Lock() + if err == nil { + log("* sent response through %s\n", relay.URL) + } else { + log("* failed to send response: %s\n", err) + } + printLock.Unlock() + handlerWg.Done() + } + }(relayURL) + } + handlerWg.Wait() + + // just after handling one request we trigger this + go func() { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + cancelPreviousBunkerInfoPrint = cancel + // the idea is that we will print the bunker URL again so it is easier to copy-paste by users + // but we will only do if the bunker is inactive for more than 5 minutes + select { + case <-ctx.Done(): + case <-time.After(time.Minute * 5): + log("\n") + printBunkerInfo() + } + }() + case <-time.After(100 * time.Millisecond): + // Continue to check for exit signal even when no events continue } - - jreq, _ := json.MarshalIndent(req, "", " ") - log("- got request from '%s': %s\n", color.New(color.Bold, color.FgBlue).Sprint(ie.Event.PubKey.Hex()), string(jreq)) - jresp, _ := json.MarshalIndent(resp, "", " ") - log("~ responding with %s\n", string(jresp)) - - handlerWg.Add(len(relayURLs)) - for _, relayURL := range relayURLs { - go func(relayURL string) { - if relay, _ := sys.Pool.EnsureRelay(relayURL); relay != nil { - err := relay.Publish(ctx, eventResponse) - printLock.Lock() - if err == nil { - log("* sent response through %s\n", relay.URL) - } else { - log("* failed to send response: %s\n", err) - } - printLock.Unlock() - handlerWg.Done() - } - }(relayURL) - } - handlerWg.Wait() - - // just after handling one request we trigger this - go func() { - ctx, cancel := context.WithCancel(ctx) - defer cancel() - cancelPreviousBunkerInfoPrint = cancel - // the idea is that we will print the bunker URL again so it is easier to copy-paste by users - // but we will only do if the bunker is inactive for more than 5 minutes - select { - case <-ctx.Done(): - case <-time.After(time.Minute * 5): - log("\n") - printBunkerInfo() - } - }() } - - return nil }, Commands: []*cli.Command{ {