mirror of
https://github.com/fiatjaf/nak.git
synced 2026-10-05 15:18:23 +00:00
feat(bunker): bunker info fixed at bottom
This commit is contained in:
committed by
fiatjaf
parent
160afb540c
commit
7228b83705
@@ -6,11 +6,13 @@ import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"maps"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -18,14 +20,95 @@ import (
|
||||
|
||||
"fiatjaf.com/nostr"
|
||||
"fiatjaf.com/nostr/nip19"
|
||||
"fiatjaf.com/nostr/nip42"
|
||||
"fiatjaf.com/nostr/nip46"
|
||||
"github.com/charmbracelet/x/ansi"
|
||||
"github.com/fatih/color"
|
||||
"github.com/mdp/qrterminal/v3"
|
||||
"github.com/urfave/cli/v3"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
const PERSISTENCE = "PERSISTENCE"
|
||||
|
||||
type bunkerTerminal struct {
|
||||
mu sync.Mutex
|
||||
out io.Writer
|
||||
log func(string, ...any)
|
||||
width int
|
||||
footer string
|
||||
footerRows int
|
||||
}
|
||||
|
||||
func newBunkerTerminal(out io.Writer, log func(string, ...any), width int) *bunkerTerminal {
|
||||
return &bunkerTerminal{out: out, log: log, width: width}
|
||||
}
|
||||
|
||||
func (t *bunkerTerminal) Log(msg string, args ...any) {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
|
||||
if t.width == 0 || t.footer == "" {
|
||||
t.log(msg, args...)
|
||||
return
|
||||
}
|
||||
|
||||
t.clearFooter()
|
||||
text := fmt.Sprintf(msg, args...)
|
||||
t.log("%s", text)
|
||||
if !strings.HasSuffix(text, "\n") {
|
||||
fmt.Fprintln(t.out)
|
||||
}
|
||||
t.drawFooter()
|
||||
}
|
||||
|
||||
func (t *bunkerTerminal) SetFooter(footer string) {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
|
||||
footer = strings.TrimRight(footer, "\n") + "\n"
|
||||
if t.width == 0 {
|
||||
t.footer = footer
|
||||
t.log("%s", footer)
|
||||
return
|
||||
}
|
||||
|
||||
t.clearFooter()
|
||||
t.footer = footer
|
||||
t.footerRows = bunkerFooterRows(footer, t.width)
|
||||
t.drawFooter()
|
||||
}
|
||||
|
||||
func (t *bunkerTerminal) clearFooter() {
|
||||
for range t.footerRows {
|
||||
fmt.Fprint(t.out, "\033[1A\033[2K\r")
|
||||
}
|
||||
}
|
||||
|
||||
func (t *bunkerTerminal) drawFooter() {
|
||||
fmt.Fprint(t.out, t.footer)
|
||||
}
|
||||
|
||||
func bunkerFooterRows(footer string, width int) int {
|
||||
rows := 0
|
||||
for line := range strings.SplitSeq(strings.TrimSuffix(footer, "\n"), "\n") {
|
||||
lineWidth := ansi.StringWidth(line)
|
||||
rows += max(1, (lineWidth+width-1)/width)
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
func bunkerTerminalWidth() int {
|
||||
if runtime.GOOS == "windows" || !term.IsTerminal(int(os.Stderr.Fd())) {
|
||||
return 0
|
||||
}
|
||||
width, _, err := term.GetSize(int(os.Stderr.Fd()))
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return width
|
||||
}
|
||||
|
||||
var bunker = &cli.Command{
|
||||
Name: "bunker",
|
||||
Usage: "starts a nip46 signer daemon with the given --sec key",
|
||||
@@ -206,6 +289,24 @@ var bunker = &cli.Command{
|
||||
persist()
|
||||
}
|
||||
|
||||
footerWidth := 0
|
||||
if c.Count("quiet") == 0 {
|
||||
footerWidth = bunkerTerminalWidth()
|
||||
}
|
||||
terminal := newBunkerTerminal(color.Error, log, footerWidth)
|
||||
sys.Pool.RelayOptions.NoticeHandler = func(relay *nostr.Relay, notice string) {
|
||||
terminal.Log("NOTICE from %s: '%s'\n", relay.URL, notice)
|
||||
}
|
||||
sys.Pool.AuthRequiredHandler = func(ctx context.Context, authEvent *nostr.Event) error {
|
||||
return authSigner(ctx, c, func(s string, args ...any) {
|
||||
if strings.HasPrefix(s, "authenticating as") {
|
||||
cleanURL, _ := strings.CutPrefix(nip42.GetRelayURLFromAuthEvent(*authEvent), "wss://")
|
||||
s = "authenticating to " + color.CyanString(cleanURL) + " as" + s[len("authenticating as"):]
|
||||
}
|
||||
terminal.Log(s+"\n", args...)
|
||||
}, authEvent)
|
||||
}
|
||||
|
||||
// try to connect to the relays here
|
||||
qs := url.Values{}
|
||||
allRelays := make([]string, len(config.Relays), len(config.Relays)+5)
|
||||
@@ -236,19 +337,15 @@ var bunker = &cli.Command{
|
||||
// it will be stored
|
||||
newSecret := randString(12)
|
||||
|
||||
// guards config.Clients, newSecret and cancelPreviousBunkerInfoPrint, which are
|
||||
// accessed from the socket goroutine, the per-request handler goroutines and here
|
||||
// guards config.Clients and newSecret, which are accessed from the socket
|
||||
// goroutine, the per-request handler goroutines and here
|
||||
var mu sync.Mutex
|
||||
|
||||
// static information
|
||||
pubkey := sec.Public()
|
||||
npub := nip19.EncodeNpub(pubkey)
|
||||
|
||||
// this function will be called every now and then
|
||||
printBunkerInfo := func() {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
bunkerInfo := func() (string, string) {
|
||||
iqs := make(url.Values)
|
||||
maps.Copy(iqs, qs)
|
||||
iqs.Set("secret", newSecret)
|
||||
@@ -309,7 +406,7 @@ var bunker = &cli.Command{
|
||||
strings.Join(relayURLsPossiblyWithoutSchema, " "),
|
||||
)
|
||||
|
||||
log("listening at %v:\n pubkey: %s \n npub: %s%s%s\n to restart: %s\n bunker: %s\n\n",
|
||||
return fmt.Sprintf("listening at %v:\n pubkey: %s \n npub: %s%s%s\n to restart: %s\n bunker: %s\n",
|
||||
colors.bold(config.Relays),
|
||||
colors.bold(pubkey.Hex()),
|
||||
colors.bold(npub),
|
||||
@@ -317,27 +414,32 @@ var bunker = &cli.Command{
|
||||
authorizedSecretsStr,
|
||||
color.CyanString(restartCommand),
|
||||
colors.bold(bunkerURI),
|
||||
)
|
||||
), bunkerURI
|
||||
} else {
|
||||
// otherwise just print the data
|
||||
log("listening at %v:\n pubkey: %s \n npub: %s%s%s\n bunker: %s\n\n",
|
||||
return fmt.Sprintf("listening at %v:\n pubkey: %s \n npub: %s%s%s\n bunker: %s\n",
|
||||
colors.bold(config.Relays),
|
||||
colors.bold(pubkey.Hex()),
|
||||
colors.bold(npub),
|
||||
authorizedKeysStr,
|
||||
authorizedSecretsStr,
|
||||
colors.bold(bunkerURI),
|
||||
)
|
||||
}
|
||||
|
||||
// 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")
|
||||
), bunkerURI
|
||||
}
|
||||
}
|
||||
printBunkerInfo()
|
||||
|
||||
setBunkerInfo := func() {
|
||||
info, _ := bunkerInfo()
|
||||
terminal.SetFooter(info)
|
||||
}
|
||||
|
||||
info, bunkerURI := bunkerInfo()
|
||||
if c.Bool("qrcode") {
|
||||
log("QR Code for bunker URI:\n")
|
||||
qrterminal.Generate(bunkerURI, qrterminal.L, os.Stdout)
|
||||
log("\n\n")
|
||||
}
|
||||
terminal.SetFooter(info)
|
||||
|
||||
// subscribe to relays
|
||||
events := sys.Pool.SubscribeMany(ctx, allRelays, nostr.Filter{
|
||||
@@ -350,26 +452,21 @@ var bunker = &cli.Command{
|
||||
signer := nip46.NewStaticKeySigner(sec)
|
||||
signer.DefaultRelays = config.Relays
|
||||
|
||||
// just a gimmick
|
||||
var cancelPreviousBunkerInfoPrint context.CancelFunc
|
||||
_, cancel := context.WithCancel(ctx)
|
||||
cancelPreviousBunkerInfoPrint = cancel
|
||||
|
||||
signer.AuthorizeRequest = func(harmless bool, from nostr.PubKey, secret string) bool {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
if slices.ContainsFunc(config.Clients, func(b BunkerConfigClient) bool { return b.PubKey == from }) {
|
||||
mu.Unlock()
|
||||
return true
|
||||
}
|
||||
if slices.Contains(authorizedSecrets, secret) {
|
||||
// add client to authorized list for subsequent requests
|
||||
if !slices.ContainsFunc(config.Clients, func(c BunkerConfigClient) bool { return c.PubKey == from }) {
|
||||
config.Clients = append(config.Clients, BunkerConfigClient{PubKey: from})
|
||||
if persist != nil {
|
||||
persist()
|
||||
}
|
||||
config.Clients = append(config.Clients, BunkerConfigClient{PubKey: from})
|
||||
if persist != nil {
|
||||
persist()
|
||||
}
|
||||
setBunkerInfo()
|
||||
mu.Unlock()
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -378,27 +475,21 @@ var bunker = &cli.Command{
|
||||
config.Clients = append(config.Clients, BunkerConfigClient{PubKey: from})
|
||||
// discard this and generate a new secret
|
||||
newSecret = randString(12)
|
||||
// print bunker info again after this
|
||||
go func() {
|
||||
time.Sleep(3 * time.Second)
|
||||
printBunkerInfo()
|
||||
}()
|
||||
|
||||
if persist != nil {
|
||||
persist()
|
||||
}
|
||||
|
||||
setBunkerInfo()
|
||||
mu.Unlock()
|
||||
return true
|
||||
}
|
||||
|
||||
mu.Unlock()
|
||||
return false
|
||||
}
|
||||
|
||||
handleBunkerRequest := func(ie nostr.RelayEvent) {
|
||||
mu.Lock()
|
||||
cancelPreviousBunkerInfoPrint() // this prevents us from printing a million bunker info blocks
|
||||
mu.Unlock()
|
||||
|
||||
// handle the NIP-46 request event
|
||||
from := ie.Event.PubKey
|
||||
req, resp, eventResponse, err := signer.HandleRequest(ctx, ie.Event)
|
||||
@@ -407,14 +498,14 @@ var bunker = &cli.Command{
|
||||
return
|
||||
}
|
||||
|
||||
log("< failed to handle request from %s: %s\n", from.Hex(), err.Error())
|
||||
terminal.Log("< failed to handle request from %s: %s\n", from.Hex(), err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
jreq, _ := json.MarshalIndent(req, "", " ")
|
||||
log("- got request from '%s': %s\n", color.New(color.Bold, color.FgBlue).Sprint(from.Hex()), string(jreq))
|
||||
terminal.Log("- got request from '%s': %s\n", color.New(color.Bold, color.FgBlue).Sprint(from.Hex()), string(jreq))
|
||||
jresp, _ := json.MarshalIndent(resp, "", " ")
|
||||
log("~ responding with %s\n", string(jresp))
|
||||
terminal.Log("~ responding with %s\n", string(jresp))
|
||||
|
||||
// use custom relays if they are defined for this client
|
||||
// (normally if the initial connection came from a nostrconnect:// URL)
|
||||
@@ -430,43 +521,27 @@ var bunker = &cli.Command{
|
||||
|
||||
for res := range sys.Pool.PublishMany(ctx, relays, eventResponse) {
|
||||
if res.Error == nil {
|
||||
log("* sent response through %s\n", res.Relay.URL)
|
||||
terminal.Log("* sent response through %s\n", res.Relay.URL)
|
||||
} else {
|
||||
log("* failed to send response through %s: %s\n", res.RelayURL, res.Error)
|
||||
terminal.Log("* failed to send response through %s: %s\n", res.RelayURL, res.Error)
|
||||
}
|
||||
}
|
||||
|
||||
// just after handling one request we trigger this
|
||||
go func() {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
mu.Lock()
|
||||
cancelPreviousBunkerInfoPrint = cancel
|
||||
mu.Unlock()
|
||||
// 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()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// unix socket nostrconnect:// handling
|
||||
go func() {
|
||||
for uri := range onSocketConnect(ctx, c) {
|
||||
for uri := range onSocketConnect(ctx, c, terminal.Log) {
|
||||
clientPublicKey, err := nostr.PubKeyFromHex(uri.Host)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
log("- got nostrconnect:// request from '%s': %s\n", color.New(color.Bold, color.FgBlue).Sprint(clientPublicKey.Hex()), uri.String())
|
||||
terminal.Log("- got nostrconnect:// request from '%s': %s\n", color.New(color.Bold, color.FgBlue).Sprint(clientPublicKey.Hex()), uri.String())
|
||||
|
||||
relays := uri.Query()["relay"]
|
||||
|
||||
// pre-authorize this client since the user has explicitly added it
|
||||
mu.Lock()
|
||||
clientAdded := false
|
||||
if !slices.ContainsFunc(config.Clients, func(c BunkerConfigClient) bool {
|
||||
return c.PubKey == clientPublicKey
|
||||
}) {
|
||||
@@ -477,16 +552,20 @@ var bunker = &cli.Command{
|
||||
Icon: uri.Query().Get("icon"),
|
||||
CustomRelays: relays,
|
||||
})
|
||||
clientAdded = true
|
||||
}
|
||||
|
||||
if persist != nil {
|
||||
persist()
|
||||
}
|
||||
if clientAdded {
|
||||
setBunkerInfo()
|
||||
}
|
||||
mu.Unlock()
|
||||
|
||||
resp, eventResponse, err := signer.HandleNostrConnectURI(ctx, uri)
|
||||
if err != nil {
|
||||
log("* failed to handle: %s\n", err)
|
||||
terminal.Log("* failed to handle: %s\n", err)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -505,12 +584,12 @@ var bunker = &cli.Command{
|
||||
|
||||
time.Sleep(time.Millisecond * 25)
|
||||
jresp, _ := json.MarshalIndent(resp, "", " ")
|
||||
log("~ responding with %s\n", string(jresp))
|
||||
terminal.Log("~ responding with %s\n", string(jresp))
|
||||
for res := range sys.Pool.PublishMany(ctx, relays, eventResponse) {
|
||||
if res.Error == nil {
|
||||
log("* sent through %s\n", res.Relay.URL)
|
||||
terminal.Log("* sent through %s\n", res.Relay.URL)
|
||||
} else {
|
||||
log("* failed to send through %s: %s\n", res.RelayURL, res.Error)
|
||||
terminal.Log("* failed to send through %s: %s\n", res.RelayURL, res.Error)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -641,7 +720,7 @@ func getSocketPath(c *cli.Command) string {
|
||||
return filepath.Join(c.String("config-path"), "bunkerconn", profile)
|
||||
}
|
||||
|
||||
func onSocketConnect(ctx context.Context, c *cli.Command) chan *url.URL {
|
||||
func onSocketConnect(ctx context.Context, c *cli.Command, log func(string, ...any)) chan *url.URL {
|
||||
res := make(chan *url.URL)
|
||||
socketPath := getSocketPath(c)
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ require (
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7
|
||||
github.com/bep/debounce v1.2.1
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.3.6
|
||||
github.com/charmbracelet/x/ansi v0.8.0
|
||||
github.com/charmbracelet/glamour v0.10.0
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0
|
||||
@@ -51,7 +52,6 @@ require (
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
||||
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.8.0 // indirect
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
|
||||
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect
|
||||
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||
|
||||
Reference in New Issue
Block a user