11package main
22
33import (
4+ "bufio"
45 "bytes"
56 "context"
67 "encoding/hex"
@@ -249,6 +250,15 @@ var bunker = &cli.Command{
249250 pubkey := sec .Public ()
250251 npub := nip19 .EncodeNpub (pubkey )
251252
253+ // printQR generates and prints the QR code for the bunker URI
254+ printQR := func () {
255+ qs .Set ("secret" , newSecret )
256+ bunkerURI := fmt .Sprintf ("bunker://%s?%s" , pubkey .Hex (), qs .Encode ())
257+ log ("\n QR Code for bunker URI:\n " )
258+ qrterminal .Generate (bunkerURI , qrterminal .L , os .Stdout )
259+ log ("\n \n " )
260+ }
261+
252262 // this function will be called every now and then
253263 printBunkerInfo := func () {
254264 qs .Set ("secret" , newSecret )
@@ -332,9 +342,7 @@ var bunker = &cli.Command{
332342
333343 // print QR code if requested
334344 if c .Bool ("qrcode" ) {
335- log ("QR Code for bunker URI:\n " )
336- qrterminal .Generate (bunkerURI , qrterminal .L , os .Stdout )
337- log ("\n \n " )
345+ printQR ()
338346 }
339347 }
340348 printBunkerInfo ()
@@ -350,40 +358,51 @@ var bunker = &cli.Command{
350358 signer := nip46 .NewStaticKeySigner (sec )
351359 signer .DefaultRelays = config .Relays
352360
353- // unix socket nostrconnect:// handling
354- go func () {
355- for uri := range onSocketConnect ( ctx , c ) {
356- clientPublicKey , err := nostr . PubKeyFromHex ( uri . Host )
357- if err != nil {
358- continue
359- }
360- log ("- got nostrconnect:// request from '%s': %s\n " , color .New (color .Bold , color .FgBlue ).Sprint (clientPublicKey .Hex ()), uri .String ())
361-
362- relays := uri .Query ()["relay" ]
363-
364- // pre-authorize this client since the user has explicitly added it
365- if ! slices .ContainsFunc (config .Clients , func (c BunkerConfigClient ) bool {
366- return c .PubKey == clientPublicKey
367- }) {
368- config .Clients = append (config .Clients , BunkerConfigClient {
369- PubKey : clientPublicKey ,
370- Name : uri .Query ().Get ("name" ),
371- URL : uri .Query ().Get ("url" ),
372- Icon : uri .Query ().Get ("icon" ),
373- CustomRelays : relays ,
374- })
375- }
361+ // common help to handle nostrconnect:// URIs
362+ handleNostrConnect := func (uri * url. URL ) {
363+ clientPublicKey , err := nostr . PubKeyFromHex ( uri . Host )
364+ if err != nil {
365+ log ( "* invalid nostrconnect:// URI: %s \n " , err )
366+ return
367+ }
368+ log ("- got nostrconnect:// request from '%s': %s\n " , color .New (color .Bold , color .FgBlue ).Sprint (clientPublicKey .Hex ()), uri .String ())
369+
370+ relays := uri .Query ()["relay" ]
371+
372+ // pre-authorize this client since the user has explicitly added it
373+ if ! slices .ContainsFunc (config .Clients , func (c BunkerConfigClient ) bool {
374+ return c .PubKey == clientPublicKey
375+ }) {
376+ config .Clients = append (config .Clients , BunkerConfigClient {
377+ PubKey : clientPublicKey ,
378+ Name : uri .Query ().Get ("name" ),
379+ URL : uri .Query ().Get ("url" ),
380+ Icon : uri .Query ().Get ("icon" ),
381+ CustomRelays : relays ,
382+ })
383+ }
376384
377- if persist != nil {
378- persist ()
379- }
385+ if persist != nil {
386+ persist ()
387+ }
380388
381- resp , eventResponse , err := signer .HandleNostrConnectURI (ctx , uri )
382- if err != nil {
383- log ("* failed to handle: %s\n " , err )
384- continue
389+ resp , eventResponse , err := signer .HandleNostrConnectURI (ctx , uri )
390+ if err != nil {
391+ log ("* failed to handle: %s\n " , err )
392+ return
393+ }
394+
395+ // compute new custom relays to avoid duplicate subscriptions
396+ newCustomRelays := make ([]string , 0 , len (relays ))
397+ for _ , r := range relays {
398+ if ! slices .Contains (allRelays , r ) {
399+ newCustomRelays = append (newCustomRelays , r )
400+ allRelays = append (allRelays , r )
385401 }
402+ }
386403
404+ if len (newCustomRelays ) > 0 {
405+ log ("subscribing to %d new relays: %s\n " , len (newCustomRelays ), strings .Join (newCustomRelays , "," ))
387406 go func () {
388407 for event := range sys .Pool .SubscribeMany (ctx , relays , nostr.Filter {
389408 Kinds : []nostr.Kind {nostr .KindNostrConnect },
@@ -396,16 +415,24 @@ var bunker = &cli.Command{
396415 }()
397416
398417 time .Sleep (time .Millisecond * 25 )
399- jresp , _ := json .MarshalIndent (resp , "" , " " )
400- log ("~ responding with %s\n " , string (jresp ))
401- for res := range sys .Pool .PublishMany (ctx , relays , eventResponse ) {
402- if res .Error == nil {
403- log ("* sent through %s\n " , res .Relay .URL )
404- } else {
405- log ("* failed to send through %s: %s\n " , res .RelayURL , res .Error )
406- }
418+ }
419+
420+ jresp , _ := json .MarshalIndent (resp , "" , " " )
421+ log ("~ responding with %s\n " , string (jresp ))
422+ for res := range sys .Pool .PublishMany (ctx , relays , eventResponse ) {
423+ if res .Error == nil {
424+ log ("* sent through %s\n " , res .Relay .URL )
425+ } else {
426+ log ("* failed to send through %s: %s\n " , res .RelayURL , res .Error )
407427 }
408428 }
429+ }
430+
431+ // unix socket nostrconnect:// handling
432+ go func () {
433+ for uri := range onSocketConnect (ctx , c ) {
434+ handleNostrConnect (uri )
435+ }
409436 }()
410437
411438 // just a gimmick
@@ -449,58 +476,149 @@ var bunker = &cli.Command{
449476 return false
450477 }
451478
452- for ie := range events {
453- cancelPreviousBunkerInfoPrint () // this prevents us from printing a million bunker info blocks
479+ // == SUBCOMMANDS ==
480+
481+ exitChan := make (chan bool , 1 )
482+
483+ // printHelp displays available commands for the bunker interface
484+ printHelp := func () {
485+ log ("%s\n " , color .CyanString ("Available Commands:" ))
486+ log (" %s - Show this help message\n " , color .GreenString ("help, h, ?" ))
487+ log (" %s - Display current bunker information\n " , color .GreenString ("info, i" ))
488+ log (" %s - Generate and display QR code for the bunker URI\n " , color .GreenString ("qr" ))
489+ log (" %s - Connect to a remote client using nostrconnect:// URI\n " , color .GreenString ("connect, c <nostrconnect://uri>" ))
490+ log (" %s - Shutdown the bunker\n " , color .GreenString ("exit, quit, q" ))
491+ log ("\n " )
492+ }
493+
494+ // handleConnectCommand processes nostrconnect:// URIs for interactive connection flow
495+ handleConnectCommand := func (connectURI string ) {
496+ if ! strings .HasPrefix (connectURI , "nostrconnect://" ) {
497+ log ("Error: URI must start with nostrconnect://\n " )
498+ return
499+ }
454500
455- // handle the NIP-46 request event
456- from := ie .Event .PubKey
457- req , resp , eventResponse , err := signer .HandleRequest (ctx , ie .Event )
501+ // Parse the nostrconnect URI
502+ u , err := url .Parse (connectURI )
458503 if err != nil {
459- if errors .Is (err , nip46 .AlreadyHandled ) {
460- continue
504+ log ("Error: Invalid nostrconnect URI: %v\n " , err )
505+ return
506+ }
507+
508+ handleNostrConnect (u )
509+ }
510+
511+ // handleBunkerCommand processes user commands in the bunker interface
512+ handleBunkerCommand := func (command string ) {
513+ parts := strings .Fields (command )
514+ if len (parts ) == 0 {
515+ return
516+ }
517+
518+ switch strings .ToLower (parts [0 ]) {
519+ case "help" , "h" , "?" :
520+ printHelp ()
521+ case "info" , "i" :
522+ printBunkerInfo ()
523+ case "qr" :
524+ printQR ()
525+ case "connect" , "c" :
526+ if len (parts ) < 2 {
527+ log ("Usage: connect <nostrconnect://uri>\n " )
528+ return
461529 }
530+ handleConnectCommand (parts [1 ])
531+ case "exit" , "quit" , "q" :
532+ log ("Exit command received.\n " )
533+ exitChan <- true
534+ case "" :
535+ // Ignore empty commands
536+ default :
537+ log ("Unknown command: %s. Type 'help' for available commands.\n " , command )
538+ }
539+ }
462540
463- log ("< failed to handle request from %s: %s\n " , from .Hex (), err .Error ())
464- continue
541+ // Start command input handler in a separate goroutine
542+ go func () {
543+ scanner := bufio .NewScanner (os .Stdin )
544+ for scanner .Scan () {
545+ command := strings .TrimSpace (scanner .Text ())
546+ handleBunkerCommand (command )
547+ }
548+ if err := scanner .Err (); err != nil {
549+ log ("error reading command: %v\n " , err )
465550 }
551+ }()
466552
467- jreq , _ := json . MarshalIndent ( req , "" , " " )
468- log ("- got request from '%s': %s\n " , color . New ( color . Bold , color . FgBlue ). Sprint ( from . Hex ()), string ( jreq ))
469- jresp , _ := json . MarshalIndent ( resp , "" , " " )
470- log ( "~ responding with %s \n " , string ( jresp ))
553+ // Print initial command help
554+ log ("%s \n Type 'help' for available commands or 'exit' to quit. \n %s\n " ,
555+ color . CyanString ( "--------------- Bunker Command Interface ---------------" ),
556+ color . CyanString ( "--------------------------------------------------------" ))
471557
472- // use custom relays if they are defined for this client
473- // (normally if the initial connection came from a nostrconnect:// URL)
474- relays := relayURLs
475- for _ , c := range config .Clients {
476- if c .PubKey == from && len (c .CustomRelays ) > 0 {
477- relays = c .CustomRelays
478- break
558+ // == END OF SUBCOMMANDS ==
559+
560+ for {
561+ // Check if exit was requested first
562+ select {
563+ case <- exitChan :
564+ log ("Shutting down bunker...\n " )
565+ return nil
566+ case ie := <- events :
567+ cancelPreviousBunkerInfoPrint () // this prevents us from printing a million bunker info blocks
568+
569+ // handle the NIP-46 request event
570+ from := ie .Event .PubKey
571+ req , resp , eventResponse , err := signer .HandleRequest (ctx , ie .Event )
572+ if err != nil {
573+ if errors .Is (err , nip46 .AlreadyHandled ) {
574+ continue
479575 }
480- }
481576
482- for res := range sys .Pool .PublishMany (ctx , relays , eventResponse ) {
483- if res .Error == nil {
484- log ("* sent response through %s\n " , res .Relay .URL )
485- } else {
486- log ("* failed to send response through %s: %s\n " , res .RelayURL , res .Error )
577+ log ("< failed to handle request from %s: %s\n " , from .Hex (), err .Error ())
578+ continue
487579 }
488- }
489580
490- // just after handling one request we trigger this
491- go func () {
492- ctx , cancel := context .WithCancel (ctx )
493- defer cancel ()
494- cancelPreviousBunkerInfoPrint = cancel
495- // the idea is that we will print the bunker URL again so it is easier to copy-paste by users
496- // but we will only do if the bunker is inactive for more than 5 minutes
497- select {
498- case <- ctx .Done ():
499- case <- time .After (time .Minute * 5 ):
500- log ("\n " )
501- printBunkerInfo ()
581+ jreq , _ := json .MarshalIndent (req , "" , " " )
582+ log ("- got request from '%s': %s\n " , color .New (color .Bold , color .FgBlue ).Sprint (from .Hex ()), string (jreq ))
583+ jresp , _ := json .MarshalIndent (resp , "" , " " )
584+ log ("~ responding with %s\n " , string (jresp ))
585+
586+ // use custom relays if they are defined for this client
587+ // (normally if the initial connection came from a nostrconnect:// URL)
588+ relays := relayURLs
589+ for _ , c := range config .Clients {
590+ if c .PubKey == from && len (c .CustomRelays ) > 0 {
591+ relays = c .CustomRelays
592+ break
593+ }
502594 }
503- }()
595+
596+ for res := range sys .Pool .PublishMany (ctx , relays , eventResponse ) {
597+ if res .Error == nil {
598+ log ("* sent response through %s\n " , res .Relay .URL )
599+ } else {
600+ log ("* failed to send response through %s: %s\n " , res .RelayURL , res .Error )
601+ }
602+ }
603+
604+ // just after handling one request we trigger this
605+ go func () {
606+ ctx , cancel := context .WithCancel (ctx )
607+ defer cancel ()
608+ cancelPreviousBunkerInfoPrint = cancel
609+ // the idea is that we will print the bunker URL again so it is easier to copy-paste by users
610+ // but we will only do if the bunker is inactive for more than 5 minutes
611+ select {
612+ case <- ctx .Done ():
613+ case <- time .After (time .Minute * 5 ):
614+ log ("\n " )
615+ printBunkerInfo ()
616+ }
617+ }()
618+ case <- time .After (100 * time .Millisecond ):
619+ // Continue to check for exit signal even when no events
620+ continue
621+ }
504622 }
505623
506624 return nil
0 commit comments