11package main
22
33import (
4+ "bufio"
45 "bytes"
56 "context"
67 "encoding/hex"
@@ -248,6 +249,15 @@ var bunker = &cli.Command{
248249 pubkey := sec .Public ()
249250 npub := nip19 .EncodeNpub (pubkey )
250251
252+ // printQR generates and prints the QR code for the bunker URI
253+ printQR := func () {
254+ qs .Set ("secret" , newSecret )
255+ bunkerURI := fmt .Sprintf ("bunker://%s?%s" , pubkey .Hex (), qs .Encode ())
256+ log ("\n QR Code for bunker URI:\n " )
257+ qrterminal .Generate (bunkerURI , qrterminal .L , os .Stdout )
258+ log ("\n \n " )
259+ }
260+
251261 // this function will be called every now and then
252262 printBunkerInfo := func () {
253263 qs .Set ("secret" , newSecret )
@@ -331,9 +341,7 @@ var bunker = &cli.Command{
331341
332342 // print QR code if requested
333343 if c .Bool ("qrcode" ) {
334- log ("QR Code for bunker URI:\n " )
335- qrterminal .Generate (bunkerURI , qrterminal .L , os .Stdout )
336- log ("\n \n " )
344+ printQR ()
337345 }
338346 }
339347 printBunkerInfo ()
@@ -348,40 +356,51 @@ var bunker = &cli.Command{
348356
349357 signer := nip46 .NewStaticKeySigner (sec )
350358
351- // unix socket nostrconnect:// handling
352- go func () {
353- for uri := range onSocketConnect ( ctx , c ) {
354- clientPublicKey , err := nostr . PubKeyFromHex ( uri . Host )
355- if err != nil {
356- continue
357- }
358- log ("- got nostrconnect:// request from '%s': %s\n " , color .New (color .Bold , color .FgBlue ).Sprint (clientPublicKey ), uri .String ())
359-
360- relays := uri .Query ()["relay" ]
361-
362- // pre-authorize this client since the user has explicitly added it
363- if ! slices .ContainsFunc (config .Clients , func (c BunkerConfigClient ) bool {
364- return c .PubKey == clientPublicKey
365- }) {
366- config .Clients = append (config .Clients , BunkerConfigClient {
367- PubKey : clientPublicKey ,
368- Name : uri .Query ().Get ("name" ),
369- URL : uri .Query ().Get ("url" ),
370- Icon : uri .Query ().Get ("icon" ),
371- CustomRelays : relays ,
372- })
373- }
359+ // common help to handle nostrconnect:// URIs
360+ handleNostrConnect := func (uri * url. URL ) {
361+ clientPublicKey , err := nostr . PubKeyFromHex ( uri . Host )
362+ if err != nil {
363+ log ( "* invalid nostrconnect:// URI: %s \n " , err )
364+ return
365+ }
366+ log ("- got nostrconnect:// request from '%s': %s\n " , color .New (color .Bold , color .FgBlue ).Sprint (clientPublicKey ), uri .String ())
367+
368+ relays := uri .Query ()["relay" ]
369+
370+ // pre-authorize this client since the user has explicitly added it
371+ if ! slices .ContainsFunc (config .Clients , func (c BunkerConfigClient ) bool {
372+ return c .PubKey == clientPublicKey
373+ }) {
374+ config .Clients = append (config .Clients , BunkerConfigClient {
375+ PubKey : clientPublicKey ,
376+ Name : uri .Query ().Get ("name" ),
377+ URL : uri .Query ().Get ("url" ),
378+ Icon : uri .Query ().Get ("icon" ),
379+ CustomRelays : relays ,
380+ })
381+ }
374382
375- if persist != nil {
376- persist ()
377- }
383+ if persist != nil {
384+ persist ()
385+ }
378386
379- resp , eventResponse , err := signer .HandleNostrConnectURI (ctx , uri )
380- if err != nil {
381- log ("* failed to handle: %s\n " , err )
382- continue
387+ resp , eventResponse , err := signer .HandleNostrConnectURI (ctx , uri )
388+ if err != nil {
389+ log ("* failed to handle: %s\n " , err )
390+ return
391+ }
392+
393+ // compute new custom relays to avoid duplicate subscriptions
394+ newCustomRelays := make ([]string , 0 , len (relays ))
395+ for _ , r := range relays {
396+ if ! slices .Contains (allRelays , r ) {
397+ newCustomRelays = append (newCustomRelays , r )
398+ allRelays = append (allRelays , r )
383399 }
400+ }
384401
402+ if len (newCustomRelays ) > 0 {
403+ log ("subscribing to %d new relays: %s\n " , len (newCustomRelays ), strings .Join (newCustomRelays , "," ))
385404 go func () {
386405 for event := range sys .Pool .SubscribeMany (ctx , relays , nostr.Filter {
387406 Kinds : []nostr.Kind {nostr .KindNostrConnect },
@@ -394,16 +413,24 @@ var bunker = &cli.Command{
394413 }()
395414
396415 time .Sleep (time .Millisecond * 25 )
397- jresp , _ := json .MarshalIndent (resp , "" , " " )
398- log ("~ responding with %s\n " , string (jresp ))
399- for res := range sys .Pool .PublishMany (ctx , relays , eventResponse ) {
400- if res .Error == nil {
401- log ("* sent through %s\n " , res .Relay .URL )
402- } else {
403- log ("* failed to send through %s: %s\n " , res .RelayURL , res .Error )
404- }
416+ }
417+
418+ jresp , _ := json .MarshalIndent (resp , "" , " " )
419+ log ("~ responding with %s\n " , string (jresp ))
420+ for res := range sys .Pool .PublishMany (ctx , relays , eventResponse ) {
421+ if res .Error == nil {
422+ log ("* sent through %s\n " , res .Relay .URL )
423+ } else {
424+ log ("* failed to send through %s: %s\n " , res .RelayURL , res .Error )
405425 }
406426 }
427+ }
428+
429+ // unix socket nostrconnect:// handling
430+ go func () {
431+ for uri := range onSocketConnect (ctx , c ) {
432+ handleNostrConnect (uri )
433+ }
407434 }()
408435
409436 // just a gimmick
@@ -440,54 +467,145 @@ var bunker = &cli.Command{
440467 return false
441468 }
442469
443- for ie := range events {
444- cancelPreviousBunkerInfoPrint () // this prevents us from printing a million bunker info blocks
470+ // == SUBCOMMANDS ==
471+
472+ exitChan := make (chan bool , 1 )
473+
474+ // printHelp displays available commands for the bunker interface
475+ printHelp := func () {
476+ log ("%s\n " , color .CyanString ("Available Commands:" ))
477+ log (" %s - Show this help message\n " , color .GreenString ("help, h, ?" ))
478+ log (" %s - Display current bunker information\n " , color .GreenString ("info, i" ))
479+ log (" %s - Generate and display QR code for the bunker URI\n " , color .GreenString ("qr" ))
480+ log (" %s - Connect to a remote client using nostrconnect:// URI\n " , color .GreenString ("connect, c <nostrconnect://uri>" ))
481+ log (" %s - Shutdown the bunker\n " , color .GreenString ("exit, quit, q" ))
482+ log ("\n " )
483+ }
484+
485+ // handleConnectCommand processes nostrconnect:// URIs for interactive connection flow
486+ handleConnectCommand := func (connectURI string ) {
487+ if ! strings .HasPrefix (connectURI , "nostrconnect://" ) {
488+ log ("Error: URI must start with nostrconnect://\n " )
489+ return
490+ }
445491
446- // handle the NIP-46 request event
447- from := ie .Event .PubKey
448- req , resp , eventResponse , err := signer .HandleRequest (ctx , ie .Event )
492+ // Parse the nostrconnect URI
493+ u , err := url .Parse (connectURI )
449494 if err != nil {
450- log ("< failed to handle request from %s : %s \n " , from , err . Error () )
451- continue
495+ log ("Error: Invalid nostrconnect URI : %v \n " , err )
496+ return
452497 }
453498
454- jreq , _ := json .MarshalIndent (req , "" , " " )
455- log ("- got request from '%s': %s\n " , color .New (color .Bold , color .FgBlue ).Sprint (from .Hex ()), string (jreq ))
456- jresp , _ := json .MarshalIndent (resp , "" , " " )
457- log ("~ responding with %s\n " , string (jresp ))
499+ handleNostrConnect (u )
500+ }
458501
459- // use custom relays if they are defined for this client
460- // (normally if the initial connection came from a nostrconnect:// URL)
461- relays := relayURLs
462- for _ , c := range config .Clients {
463- if c .PubKey == from && len (c .CustomRelays ) > 0 {
464- relays = c .CustomRelays
465- break
466- }
502+ // handleBunkerCommand processes user commands in the bunker interface
503+ handleBunkerCommand := func (command string ) {
504+ parts := strings .Fields (command )
505+ if len (parts ) == 0 {
506+ return
467507 }
468508
469- for res := range sys .Pool .PublishMany (ctx , relays , eventResponse ) {
470- if res .Error == nil {
471- log ("* sent response through %s\n " , res .Relay .URL )
472- } else {
473- log ("* failed to send response through %s: %s\n " , res .RelayURL , res .Error )
509+ switch strings .ToLower (parts [0 ]) {
510+ case "help" , "h" , "?" :
511+ printHelp ()
512+ case "info" , "i" :
513+ printBunkerInfo ()
514+ case "qr" :
515+ printQR ()
516+ case "connect" , "c" :
517+ if len (parts ) < 2 {
518+ log ("Usage: connect <nostrconnect://uri>\n " )
519+ return
474520 }
521+ handleConnectCommand (parts [1 ])
522+ case "exit" , "quit" , "q" :
523+ log ("Exit command received.\n " )
524+ exitChan <- true
525+ case "" :
526+ // Ignore empty commands
527+ default :
528+ log ("Unknown command: %s. Type 'help' for available commands.\n " , command )
529+ }
530+ }
531+
532+ // Start command input handler in a separate goroutine
533+ go func () {
534+ scanner := bufio .NewScanner (os .Stdin )
535+ for scanner .Scan () {
536+ command := strings .TrimSpace (scanner .Text ())
537+ handleBunkerCommand (command )
475538 }
539+ if err := scanner .Err (); err != nil {
540+ log ("error reading command: %v\n " , err )
541+ }
542+ }()
476543
477- // just after handling one request we trigger this
478- go func () {
479- ctx , cancel := context .WithCancel (ctx )
480- defer cancel ()
481- cancelPreviousBunkerInfoPrint = cancel
482- // the idea is that we will print the bunker URL again so it is easier to copy-paste by users
483- // but we will only do if the bunker is inactive for more than 5 minutes
484- select {
485- case <- ctx .Done ():
486- case <- time .After (time .Minute * 5 ):
487- log ("\n " )
488- printBunkerInfo ()
544+ // Print initial command help
545+ log ("%s\n Type 'help' for available commands or 'exit' to quit.\n %s\n " ,
546+ color .CyanString ("--------------- Bunker Command Interface ---------------" ),
547+ color .CyanString ("--------------------------------------------------------" ))
548+
549+ // == END OF SUBCOMMANDS ==
550+
551+ for {
552+ // Check if exit was requested first
553+ select {
554+ case <- exitChan :
555+ log ("Shutting down bunker...\n " )
556+ return nil
557+ case ie := <- events :
558+ cancelPreviousBunkerInfoPrint () // this prevents us from printing a million bunker info blocks
559+
560+ // handle the NIP-46 request event
561+ from := ie .Event .PubKey
562+ req , resp , eventResponse , err := signer .HandleRequest (ctx , ie .Event )
563+ if err != nil {
564+ log ("< failed to handle request from %s: %s\n " , from , err .Error ())
565+ continue
566+ }
567+
568+ jreq , _ := json .MarshalIndent (req , "" , " " )
569+ log ("- got request from '%s': %s\n " , color .New (color .Bold , color .FgBlue ).Sprint (from .Hex ()), string (jreq ))
570+ jresp , _ := json .MarshalIndent (resp , "" , " " )
571+ log ("~ responding with %s\n " , string (jresp ))
572+
573+ // use custom relays if they are defined for this client
574+ // (normally if the initial connection came from a nostrconnect:// URL)
575+ relays := relayURLs
576+ for _ , c := range config .Clients {
577+ if c .PubKey == from && len (c .CustomRelays ) > 0 {
578+ relays = c .CustomRelays
579+ break
580+ }
489581 }
490- }()
582+
583+ for res := range sys .Pool .PublishMany (ctx , relays , eventResponse ) {
584+ if res .Error == nil {
585+ log ("* sent response through %s\n " , res .Relay .URL )
586+ } else {
587+ log ("* failed to send response through %s: %s\n " , res .RelayURL , res .Error )
588+ }
589+ }
590+
591+ // just after handling one request we trigger this
592+ go func () {
593+ ctx , cancel := context .WithCancel (ctx )
594+ defer cancel ()
595+ cancelPreviousBunkerInfoPrint = cancel
596+ // the idea is that we will print the bunker URL again so it is easier to copy-paste by users
597+ // but we will only do if the bunker is inactive for more than 5 minutes
598+ select {
599+ case <- ctx .Done ():
600+ case <- time .After (time .Minute * 5 ):
601+ log ("\n " )
602+ printBunkerInfo ()
603+ }
604+ }()
605+ case <- time .After (100 * time .Millisecond ):
606+ // Continue to check for exit signal even when no events
607+ continue
608+ }
491609 }
492610
493611 return nil
0 commit comments