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
@@ -442,58 +469,149 @@ var bunker = &cli.Command{
442469 return false
443470 }
444471
445- for ie := range events {
446- cancelPreviousBunkerInfoPrint () // this prevents us from printing a million bunker info blocks
472+ // == SUBCOMMANDS ==
473+
474+ exitChan := make (chan bool , 1 )
475+
476+ // printHelp displays available commands for the bunker interface
477+ printHelp := func () {
478+ log ("%s\n " , color .CyanString ("Available Commands:" ))
479+ log (" %s - Show this help message\n " , color .GreenString ("help, h, ?" ))
480+ log (" %s - Display current bunker information\n " , color .GreenString ("info, i" ))
481+ log (" %s - Generate and display QR code for the bunker URI\n " , color .GreenString ("qr" ))
482+ log (" %s - Connect to a remote client using nostrconnect:// URI\n " , color .GreenString ("connect, c <nostrconnect://uri>" ))
483+ log (" %s - Shutdown the bunker\n " , color .GreenString ("exit, quit, q" ))
484+ log ("\n " )
485+ }
486+
487+ // handleConnectCommand processes nostrconnect:// URIs for interactive connection flow
488+ handleConnectCommand := func (connectURI string ) {
489+ if ! strings .HasPrefix (connectURI , "nostrconnect://" ) {
490+ log ("Error: URI must start with nostrconnect://\n " )
491+ return
492+ }
447493
448- // handle the NIP-46 request event
449- from := ie .Event .PubKey
450- req , resp , eventResponse , err := signer .HandleRequest (ctx , ie .Event )
494+ // Parse the nostrconnect URI
495+ u , err := url .Parse (connectURI )
451496 if err != nil {
452- if errors .Is (err , nip46 .AlreadyHandled ) {
453- continue
497+ log ("Error: Invalid nostrconnect URI: %v\n " , err )
498+ return
499+ }
500+
501+ handleNostrConnect (u )
502+ }
503+
504+ // handleBunkerCommand processes user commands in the bunker interface
505+ handleBunkerCommand := func (command string ) {
506+ parts := strings .Fields (command )
507+ if len (parts ) == 0 {
508+ return
509+ }
510+
511+ switch strings .ToLower (parts [0 ]) {
512+ case "help" , "h" , "?" :
513+ printHelp ()
514+ case "info" , "i" :
515+ printBunkerInfo ()
516+ case "qr" :
517+ printQR ()
518+ case "connect" , "c" :
519+ if len (parts ) < 2 {
520+ log ("Usage: connect <nostrconnect://uri>\n " )
521+ return
454522 }
523+ handleConnectCommand (parts [1 ])
524+ case "exit" , "quit" , "q" :
525+ log ("Exit command received.\n " )
526+ exitChan <- true
527+ case "" :
528+ // Ignore empty commands
529+ default :
530+ log ("Unknown command: %s. Type 'help' for available commands.\n " , command )
531+ }
532+ }
455533
456- log ("< failed to handle request from %s: %s\n " , from .Hex (), err .Error ())
457- continue
534+ // Start command input handler in a separate goroutine
535+ go func () {
536+ scanner := bufio .NewScanner (os .Stdin )
537+ for scanner .Scan () {
538+ command := strings .TrimSpace (scanner .Text ())
539+ handleBunkerCommand (command )
540+ }
541+ if err := scanner .Err (); err != nil {
542+ log ("error reading command: %v\n " , err )
458543 }
544+ }()
459545
460- jreq , _ := json . MarshalIndent ( req , "" , " " )
461- log ("- got request from '%s': %s\n " , color . New ( color . Bold , color . FgBlue ). Sprint ( from . Hex ()), string ( jreq ))
462- jresp , _ := json . MarshalIndent ( resp , "" , " " )
463- log ( "~ responding with %s \n " , string ( jresp ))
546+ // Print initial command help
547+ log ("%s \n Type 'help' for available commands or 'exit' to quit. \n %s\n " ,
548+ color . CyanString ( "--------------- Bunker Command Interface ---------------" ),
549+ color . CyanString ( "--------------------------------------------------------" ))
464550
465- // use custom relays if they are defined for this client
466- // (normally if the initial connection came from a nostrconnect:// URL)
467- relays := relayURLs
468- for _ , c := range config .Clients {
469- if c .PubKey == from && len (c .CustomRelays ) > 0 {
470- relays = c .CustomRelays
471- break
551+ // == END OF SUBCOMMANDS ==
552+
553+ for {
554+ // Check if exit was requested first
555+ select {
556+ case <- exitChan :
557+ log ("Shutting down bunker...\n " )
558+ return nil
559+ case ie := <- events :
560+ cancelPreviousBunkerInfoPrint () // this prevents us from printing a million bunker info blocks
561+
562+ // handle the NIP-46 request event
563+ from := ie .Event .PubKey
564+ req , resp , eventResponse , err := signer .HandleRequest (ctx , ie .Event )
565+ if err != nil {
566+ if errors .Is (err , nip46 .AlreadyHandled ) {
567+ continue
472568 }
473- }
474569
475- for res := range sys .Pool .PublishMany (ctx , relays , eventResponse ) {
476- if res .Error == nil {
477- log ("* sent response through %s\n " , res .Relay .URL )
478- } else {
479- log ("* failed to send response through %s: %s\n " , res .RelayURL , res .Error )
570+ log ("< failed to handle request from %s: %s\n " , from .Hex (), err .Error ())
571+ continue
480572 }
481- }
482573
483- // just after handling one request we trigger this
484- go func () {
485- ctx , cancel := context .WithCancel (ctx )
486- defer cancel ()
487- cancelPreviousBunkerInfoPrint = cancel
488- // the idea is that we will print the bunker URL again so it is easier to copy-paste by users
489- // but we will only do if the bunker is inactive for more than 5 minutes
490- select {
491- case <- ctx .Done ():
492- case <- time .After (time .Minute * 5 ):
493- log ("\n " )
494- printBunkerInfo ()
574+ jreq , _ := json .MarshalIndent (req , "" , " " )
575+ log ("- got request from '%s': %s\n " , color .New (color .Bold , color .FgBlue ).Sprint (from .Hex ()), string (jreq ))
576+ jresp , _ := json .MarshalIndent (resp , "" , " " )
577+ log ("~ responding with %s\n " , string (jresp ))
578+
579+ // use custom relays if they are defined for this client
580+ // (normally if the initial connection came from a nostrconnect:// URL)
581+ relays := relayURLs
582+ for _ , c := range config .Clients {
583+ if c .PubKey == from && len (c .CustomRelays ) > 0 {
584+ relays = c .CustomRelays
585+ break
586+ }
495587 }
496- }()
588+
589+ for res := range sys .Pool .PublishMany (ctx , relays , eventResponse ) {
590+ if res .Error == nil {
591+ log ("* sent response through %s\n " , res .Relay .URL )
592+ } else {
593+ log ("* failed to send response through %s: %s\n " , res .RelayURL , res .Error )
594+ }
595+ }
596+
597+ // just after handling one request we trigger this
598+ go func () {
599+ ctx , cancel := context .WithCancel (ctx )
600+ defer cancel ()
601+ cancelPreviousBunkerInfoPrint = cancel
602+ // the idea is that we will print the bunker URL again so it is easier to copy-paste by users
603+ // but we will only do if the bunker is inactive for more than 5 minutes
604+ select {
605+ case <- ctx .Done ():
606+ case <- time .After (time .Minute * 5 ):
607+ log ("\n " )
608+ printBunkerInfo ()
609+ }
610+ }()
611+ case <- time .After (100 * time .Millisecond ):
612+ // Continue to check for exit signal even when no events
613+ continue
614+ }
497615 }
498616
499617 return nil
0 commit comments