convert to using nostrlib.

This commit is contained in:
fiatjaf 2025-04-20 18:11:21 -03:00
parent 1b43dbda02
commit d733a31898
32 changed files with 568 additions and 418 deletions

View File

@ -5,8 +5,9 @@ import (
"fmt" "fmt"
"os" "os"
"github.com/nbd-wtf/go-nostr/keyer" "fiatjaf.com/nostr"
"github.com/nbd-wtf/go-nostr/nipb0/blossom" "fiatjaf.com/nostr/keyer"
"fiatjaf.com/nostr/nipb0/blossom"
"github.com/urfave/cli/v3" "github.com/urfave/cli/v3"
) )
@ -35,7 +36,11 @@ var blossomCmd = &cli.Command{
var client *blossom.Client var client *blossom.Client
pubkey := c.Args().First() pubkey := c.Args().First()
if pubkey != "" { if pubkey != "" {
client = blossom.NewClient(client.GetMediaServer(), keyer.NewReadOnlySigner(pubkey)) if pk, err := nostr.PubKeyFromHex(pubkey); err != nil {
return fmt.Errorf("invalid public key '%s': %w", pubkey, err)
} else {
client = blossom.NewClient(client.GetMediaServer(), keyer.NewReadOnlySigner(pk))
}
} else { } else {
var err error var err error
client, err = getBlossomClient(ctx, c) client, err = getBlossomClient(ctx, c)

View File

@ -10,10 +10,10 @@ import (
"sync" "sync"
"time" "time"
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/nip19"
"fiatjaf.com/nostr/nip46"
"github.com/fatih/color" "github.com/fatih/color"
"github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nip19"
"github.com/nbd-wtf/go-nostr/nip46"
"github.com/urfave/cli/v3" "github.com/urfave/cli/v3"
) )
@ -38,7 +38,7 @@ var bunker = &cli.Command{
Aliases: []string{"s"}, Aliases: []string{"s"},
Usage: "secrets for which we will always respond", Usage: "secrets for which we will always respond",
}, },
&cli.StringSliceFlag{ &PubKeySliceFlag{
Name: "authorized-keys", Name: "authorized-keys",
Aliases: []string{"k"}, Aliases: []string{"k"},
Usage: "pubkeys for which we will always respond", Usage: "pubkeys for which we will always respond",
@ -49,7 +49,7 @@ var bunker = &cli.Command{
qs := url.Values{} qs := url.Values{}
relayURLs := make([]string, 0, c.Args().Len()) relayURLs := make([]string, 0, c.Args().Len())
if relayUrls := c.Args().Slice(); len(relayUrls) > 0 { if relayUrls := c.Args().Slice(); len(relayUrls) > 0 {
relays := connectToAllRelays(ctx, c, relayUrls, nil) relays := connectToAllRelays(ctx, c, relayUrls, nil, nostr.PoolOptions{})
if len(relays) == 0 { if len(relays) == 0 {
log("failed to connect to any of the given relays.\n") log("failed to connect to any of the given relays.\n")
os.Exit(3) os.Exit(3)
@ -70,7 +70,7 @@ var bunker = &cli.Command{
} }
// other arguments // other arguments
authorizedKeys := c.StringSlice("authorized-keys") authorizedKeys := getPubKeySlice(c, "authorized-keys")
authorizedSecrets := c.StringSlice("authorized-secrets") authorizedSecrets := c.StringSlice("authorized-secrets")
// this will be used to auto-authorize the next person who connects who isn't pre-authorized // this will be used to auto-authorize the next person who connects who isn't pre-authorized
@ -78,11 +78,8 @@ var bunker = &cli.Command{
newSecret := randString(12) newSecret := randString(12)
// static information // static information
pubkey, err := nostr.GetPublicKey(sec) pubkey := sec.Public()
if err != nil { npub := nip19.EncodeNpub(pubkey)
return err
}
npub, _ := nip19.EncodePublicKey(pubkey)
// this function will be called every now and then // this function will be called every now and then
printBunkerInfo := func() { printBunkerInfo := func() {
@ -91,7 +88,10 @@ var bunker = &cli.Command{
authorizedKeysStr := "" authorizedKeysStr := ""
if len(authorizedKeys) != 0 { if len(authorizedKeys) != 0 {
authorizedKeysStr = "\n authorized keys:\n - " + colors.italic(strings.Join(authorizedKeys, "\n - ")) authorizedKeysStr = "\n authorized keys:"
for _, pubkey := range authorizedKeys {
authorizedKeysStr += "\n - " + colors.italic(pubkey.Hex())
}
} }
authorizedSecretsStr := "" authorizedSecretsStr := ""
@ -101,7 +101,7 @@ var bunker = &cli.Command{
preauthorizedFlags := "" preauthorizedFlags := ""
for _, k := range authorizedKeys { for _, k := range authorizedKeys {
preauthorizedFlags += " -k " + k preauthorizedFlags += " -k " + k.Hex()
} }
for _, s := range authorizedSecrets { for _, s := range authorizedSecrets {
preauthorizedFlags += " -s " + s preauthorizedFlags += " -s " + s
@ -142,10 +142,12 @@ var bunker = &cli.Command{
// subscribe to relays // subscribe to relays
now := nostr.Now() now := nostr.Now()
events := sys.Pool.SubscribeMany(ctx, relayURLs, nostr.Filter{ events := sys.Pool.SubscribeMany(ctx, relayURLs, nostr.Filter{
Kinds: []int{nostr.KindNostrConnect}, Kinds: []nostr.Kind{nostr.KindNostrConnect},
Tags: nostr.TagMap{"p": []string{pubkey}}, Tags: nostr.TagMap{"p": []string{pubkey.Hex()}},
Since: &now, Since: &now,
LimitZero: true, LimitZero: true,
}, nostr.SubscriptionOptions{
Label: "nak-bunker",
}) })
signer := nip46.NewStaticKeySigner(sec) signer := nip46.NewStaticKeySigner(sec)
@ -158,7 +160,7 @@ var bunker = &cli.Command{
cancelPreviousBunkerInfoPrint = cancel cancelPreviousBunkerInfoPrint = cancel
// asking user for authorization // asking user for authorization
signer.AuthorizeRequest = func(harmless bool, from string, secret string) bool { signer.AuthorizeRequest = func(harmless bool, from nostr.PubKey, secret string) bool {
if secret == newSecret { if secret == newSecret {
// store this key // store this key
authorizedKeys = append(authorizedKeys, from) authorizedKeys = append(authorizedKeys, from)
@ -236,11 +238,13 @@ var bunker = &cli.Command{
} }
uri, err := url.Parse(c.Args().First()) uri, err := url.Parse(c.Args().First())
if err != nil || uri.Scheme != "nostrconnect" || !nostr.IsValidPublicKey(uri.Host) { if err != nil || uri.Scheme != "nostrconnect" {
return fmt.Errorf("invalid uri") return fmt.Errorf("invalid uri")
} }
return nil // TODO
return fmt.Errorf("this is not implemented yet")
}, },
}, },
}, },

View File

@ -6,9 +6,9 @@ import (
"os" "os"
"strings" "strings"
"github.com/nbd-wtf/go-nostr" "fiatjaf.com/nostr"
"github.com/nbd-wtf/go-nostr/nip45" "fiatjaf.com/nostr/nip45"
"github.com/nbd-wtf/go-nostr/nip45/hyperloglog" "fiatjaf.com/nostr/nip45/hyperloglog"
"github.com/urfave/cli/v3" "github.com/urfave/cli/v3"
) )
@ -18,7 +18,7 @@ var count = &cli.Command{
Description: `outputs a nip45 request (the flags are mostly the same as 'nak req').`, Description: `outputs a nip45 request (the flags are mostly the same as 'nak req').`,
DisableSliceFlagSeparator: true, DisableSliceFlagSeparator: true,
Flags: []cli.Flag{ Flags: []cli.Flag{
&cli.StringSliceFlag{ &PubKeySliceFlag{
Name: "author", Name: "author",
Aliases: []string{"a"}, Aliases: []string{"a"},
Usage: "only accept events from these authors (pubkey as hex)", Usage: "only accept events from these authors (pubkey as hex)",
@ -70,7 +70,7 @@ var count = &cli.Command{
biggerUrlSize := 0 biggerUrlSize := 0
relayUrls := c.Args().Slice() relayUrls := c.Args().Slice()
if len(relayUrls) > 0 { if len(relayUrls) > 0 {
relays := connectToAllRelays(ctx, c, relayUrls, nil) relays := connectToAllRelays(ctx, c, relayUrls, nil, nostr.PoolOptions{})
if len(relays) == 0 { if len(relays) == 0 {
log("failed to connect to any of the given relays.\n") log("failed to connect to any of the given relays.\n")
os.Exit(3) os.Exit(3)
@ -92,16 +92,13 @@ var count = &cli.Command{
filter := nostr.Filter{} filter := nostr.Filter{}
if authors := c.StringSlice("author"); len(authors) > 0 { if authors := getPubKeySlice(c, "author"); len(authors) > 0 {
filter.Authors = authors filter.Authors = authors
} }
if ids := c.StringSlice("id"); len(ids) > 0 {
filter.IDs = ids
}
if kinds64 := c.IntSlice("kind"); len(kinds64) > 0 { if kinds64 := c.IntSlice("kind"); len(kinds64) > 0 {
kinds := make([]int, len(kinds64)) kinds := make([]nostr.Kind, len(kinds64))
for i, v := range kinds64 { for i, v := range kinds64 {
kinds[i] = int(v) kinds[i] = nostr.Kind(v)
} }
filter.Kinds = kinds filter.Kinds = kinds
} }
@ -151,7 +148,7 @@ var count = &cli.Command{
} }
for _, relayUrl := range relayUrls { for _, relayUrl := range relayUrls {
relay, _ := sys.Pool.EnsureRelay(relayUrl) relay, _ := sys.Pool.EnsureRelay(relayUrl)
count, hllRegisters, err := relay.Count(ctx, nostr.Filters{filter}) count, hllRegisters, err := relay.Count(ctx, filter, nostr.SubscriptionOptions{})
fmt.Fprintf(os.Stderr, "%s%s: ", strings.Repeat(" ", biggerUrlSize-len(relayUrl)), relayUrl) fmt.Fprintf(os.Stderr, "%s%s: ", strings.Repeat(" ", biggerUrlSize-len(relayUrl)), relayUrl)
if err != nil { if err != nil {

View File

@ -8,7 +8,7 @@ import (
"os/exec" "os/exec"
"strings" "strings"
"github.com/nbd-wtf/go-nostr" "fiatjaf.com/nostr"
"github.com/urfave/cli/v3" "github.com/urfave/cli/v3"
"golang.org/x/exp/slices" "golang.org/x/exp/slices"
) )

View File

@ -5,10 +5,10 @@ import (
"encoding/hex" "encoding/hex"
"strings" "strings"
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/nip19"
"fiatjaf.com/nostr/sdk"
"github.com/urfave/cli/v3" "github.com/urfave/cli/v3"
"github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nip19"
"github.com/nbd-wtf/go-nostr/sdk"
) )
var decode = &cli.Command{ var decode = &cli.Command{
@ -73,7 +73,7 @@ var decode = &cli.Command{
} }
} else if prefix, value, err := nip19.Decode(input); err == nil && prefix == "nsec" { } else if prefix, value, err := nip19.Decode(input); err == nil && prefix == "nsec" {
decodeResult.PrivateKey.PrivateKey = value.(string) decodeResult.PrivateKey.PrivateKey = value.(string)
decodeResult.PrivateKey.PublicKey, _ = nostr.GetPublicKey(value.(string)) decodeResult.PrivateKey.PublicKey = nostr.GetPublicKey(value.(nostr.SecretKey))
} else { } else {
ctx = lineProcessingError(ctx, "couldn't decode input '%s': %s", input, err) ctx = lineProcessingError(ctx, "couldn't decode input '%s': %s", input, err)
continue continue

View File

@ -4,8 +4,8 @@ import (
"context" "context"
"fmt" "fmt"
"github.com/nbd-wtf/go-nostr" "fiatjaf.com/nostr"
"github.com/nbd-wtf/go-nostr/nip19" "fiatjaf.com/nostr/nip19"
"github.com/urfave/cli/v3" "github.com/urfave/cli/v3"
) )
@ -33,16 +33,13 @@ var encode = &cli.Command{
DisableSliceFlagSeparator: true, DisableSliceFlagSeparator: true,
Action: func(ctx context.Context, c *cli.Command) error { Action: func(ctx context.Context, c *cli.Command) error {
for target := range getStdinLinesOrArguments(c.Args()) { for target := range getStdinLinesOrArguments(c.Args()) {
if ok := nostr.IsValidPublicKey(target); !ok { pk, err := nostr.PubKeyFromHexCheap(target)
ctx = lineProcessingError(ctx, "invalid public key: %s", target) if err != nil {
ctx = lineProcessingError(ctx, "invalid public key '%s': %w", target, err)
continue continue
} }
if npub, err := nip19.EncodePublicKey(target); err == nil { stdout(nip19.EncodeNpub(pk))
stdout(npub)
} else {
return err
}
} }
exitIfLineProcessingError(ctx) exitIfLineProcessingError(ctx)
@ -55,16 +52,13 @@ var encode = &cli.Command{
DisableSliceFlagSeparator: true, DisableSliceFlagSeparator: true,
Action: func(ctx context.Context, c *cli.Command) error { Action: func(ctx context.Context, c *cli.Command) error {
for target := range getStdinLinesOrArguments(c.Args()) { for target := range getStdinLinesOrArguments(c.Args()) {
if ok := nostr.IsValid32ByteHex(target); !ok { sk, err := nostr.SecretKeyFromHex(target)
ctx = lineProcessingError(ctx, "invalid private key: %s", target) if err != nil {
ctx = lineProcessingError(ctx, "invalid private key '%s': %w", target, err)
continue continue
} }
if npub, err := nip19.EncodePrivateKey(target); err == nil { stdout(nip19.EncodeNsec(sk))
stdout(npub)
} else {
return err
}
} }
exitIfLineProcessingError(ctx) exitIfLineProcessingError(ctx)
@ -84,8 +78,9 @@ var encode = &cli.Command{
DisableSliceFlagSeparator: true, DisableSliceFlagSeparator: true,
Action: func(ctx context.Context, c *cli.Command) error { Action: func(ctx context.Context, c *cli.Command) error {
for target := range getStdinLinesOrArguments(c.Args()) { for target := range getStdinLinesOrArguments(c.Args()) {
if ok := nostr.IsValid32ByteHex(target); !ok { pk, err := nostr.PubKeyFromHexCheap(target)
ctx = lineProcessingError(ctx, "invalid public key: %s", target) if err != nil {
ctx = lineProcessingError(ctx, "invalid public key '%s': %w", target, err)
continue continue
} }
@ -94,11 +89,7 @@ var encode = &cli.Command{
return err return err
} }
if npub, err := nip19.EncodeProfile(target, relays); err == nil { stdout(nip19.EncodeNprofile(pk, relays))
stdout(npub)
} else {
return err
}
} }
exitIfLineProcessingError(ctx) exitIfLineProcessingError(ctx)
@ -114,7 +105,7 @@ var encode = &cli.Command{
Aliases: []string{"r"}, Aliases: []string{"r"},
Usage: "attach relay hints to nevent code", Usage: "attach relay hints to nevent code",
}, },
&cli.StringFlag{ &PubKeyFlag{
Name: "author", Name: "author",
Aliases: []string{"a"}, Aliases: []string{"a"},
Usage: "attach an author pubkey as a hint to the nevent code", Usage: "attach an author pubkey as a hint to the nevent code",
@ -123,28 +114,19 @@ var encode = &cli.Command{
DisableSliceFlagSeparator: true, DisableSliceFlagSeparator: true,
Action: func(ctx context.Context, c *cli.Command) error { Action: func(ctx context.Context, c *cli.Command) error {
for target := range getStdinLinesOrArguments(c.Args()) { for target := range getStdinLinesOrArguments(c.Args()) {
if ok := nostr.IsValid32ByteHex(target); !ok { id, err := nostr.IDFromHex(target)
if err != nil {
ctx = lineProcessingError(ctx, "invalid event id: %s", target) ctx = lineProcessingError(ctx, "invalid event id: %s", target)
continue continue
} }
author := c.String("author") author := getPubKey(c, "author")
if author != "" {
if ok := nostr.IsValidPublicKey(author); !ok {
return fmt.Errorf("invalid 'author' public key")
}
}
relays := c.StringSlice("relay") relays := c.StringSlice("relay")
if err := normalizeAndValidateRelayURLs(relays); err != nil { if err := normalizeAndValidateRelayURLs(relays); err != nil {
return err return err
} }
if npub, err := nip19.EncodeEvent(target, relays, author); err == nil { stdout(nip19.EncodeNevent(id, relays, author))
stdout(npub)
} else {
return err
}
} }
exitIfLineProcessingError(ctx) exitIfLineProcessingError(ctx)
@ -161,7 +143,7 @@ var encode = &cli.Command{
Usage: "the \"d\" tag identifier of this replaceable event -- can also be read from stdin", Usage: "the \"d\" tag identifier of this replaceable event -- can also be read from stdin",
Required: true, Required: true,
}, },
&cli.StringFlag{ &PubKeyFlag{
Name: "pubkey", Name: "pubkey",
Usage: "pubkey of the naddr author", Usage: "pubkey of the naddr author",
Aliases: []string{"author", "a", "p"}, Aliases: []string{"author", "a", "p"},
@ -182,10 +164,7 @@ var encode = &cli.Command{
DisableSliceFlagSeparator: true, DisableSliceFlagSeparator: true,
Action: func(ctx context.Context, c *cli.Command) error { Action: func(ctx context.Context, c *cli.Command) error {
for d := range getStdinLinesOrBlank() { for d := range getStdinLinesOrBlank() {
pubkey := c.String("pubkey") pubkey := getPubKey(c, "pubkey")
if ok := nostr.IsValidPublicKey(pubkey); !ok {
return fmt.Errorf("invalid 'pubkey'")
}
kind := c.Int("kind") kind := c.Int("kind")
if kind < 30000 || kind >= 40000 { if kind < 30000 || kind >= 40000 {
@ -205,11 +184,7 @@ var encode = &cli.Command{
return err return err
} }
if npub, err := nip19.EncodeEntity(pubkey, int(kind), d, relays); err == nil { stdout(nip19.EncodeNaddr(pubkey, nostr.Kind(kind), d, relays))
stdout(npub)
} else {
return err
}
} }
exitIfLineProcessingError(ctx) exitIfLineProcessingError(ctx)

View File

@ -4,9 +4,8 @@ import (
"context" "context"
"fmt" "fmt"
"fiatjaf.com/nostr/nip04"
"github.com/urfave/cli/v3" "github.com/urfave/cli/v3"
"github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nip04"
) )
var encrypt = &cli.Command{ var encrypt = &cli.Command{
@ -16,7 +15,7 @@ var encrypt = &cli.Command{
DisableSliceFlagSeparator: true, DisableSliceFlagSeparator: true,
Flags: append( Flags: append(
defaultKeyFlags, defaultKeyFlags,
&cli.StringFlag{ &PubKeyFlag{
Name: "recipient-pubkey", Name: "recipient-pubkey",
Aliases: []string{"p", "tgt", "target", "pubkey"}, Aliases: []string{"p", "tgt", "target", "pubkey"},
Required: true, Required: true,
@ -27,10 +26,7 @@ var encrypt = &cli.Command{
}, },
), ),
Action: func(ctx context.Context, c *cli.Command) error { Action: func(ctx context.Context, c *cli.Command) error {
target := c.String("recipient-pubkey") target := getPubKey(c, "recipient-pubkey")
if !nostr.IsValidPublicKey(target) {
return fmt.Errorf("target %s is not a valid public key", target)
}
plaintext := c.Args().First() plaintext := c.Args().First()
@ -81,7 +77,7 @@ var decrypt = &cli.Command{
DisableSliceFlagSeparator: true, DisableSliceFlagSeparator: true,
Flags: append( Flags: append(
defaultKeyFlags, defaultKeyFlags,
&cli.StringFlag{ &PubKeyFlag{
Name: "sender-pubkey", Name: "sender-pubkey",
Aliases: []string{"p", "src", "source", "pubkey"}, Aliases: []string{"p", "src", "source", "pubkey"},
Required: true, Required: true,
@ -92,10 +88,7 @@ var decrypt = &cli.Command{
}, },
), ),
Action: func(ctx context.Context, c *cli.Command) error { Action: func(ctx context.Context, c *cli.Command) error {
source := c.String("sender-pubkey") source := getPubKey(c, "sender-pubkey")
if !nostr.IsValidPublicKey(source) {
return fmt.Errorf("source %s is not a valid public key", source)
}
ciphertext := c.Args().First() ciphertext := c.Args().First()

View File

@ -8,11 +8,11 @@ import (
"strings" "strings"
"time" "time"
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/nip13"
"fiatjaf.com/nostr/nip19"
"github.com/fatih/color" "github.com/fatih/color"
"github.com/mailru/easyjson" "github.com/mailru/easyjson"
"github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nip13"
"github.com/nbd-wtf/go-nostr/nip19"
"github.com/urfave/cli/v3" "github.com/urfave/cli/v3"
) )
@ -141,9 +141,11 @@ example:
if relayUrls := c.Args().Slice(); len(relayUrls) > 0 { if relayUrls := c.Args().Slice(); len(relayUrls) > 0 {
relays = connectToAllRelays(ctx, c, relayUrls, nil, relays = connectToAllRelays(ctx, c, relayUrls, nil,
nostr.WithAuthHandler(func(ctx context.Context, authEvent nostr.RelayEvent) error { nostr.PoolOptions{
AuthHandler: func(ctx context.Context, authEvent *nostr.Event) error {
return authSigner(ctx, c, func(s string, args ...any) {}, authEvent) return authSigner(ctx, c, func(s string, args ...any) {}, authEvent)
}), },
},
) )
if len(relays) == 0 { if len(relays) == 0 {
log("failed to connect to any of the given relays.\n") log("failed to connect to any of the given relays.\n")
@ -180,7 +182,7 @@ example:
} }
if kind := c.Uint("kind"); slices.Contains(c.FlagNames(), "kind") { if kind := c.Uint("kind"); slices.Contains(c.FlagNames(), "kind") {
evt.Kind = int(kind) evt.Kind = nostr.Kind(kind)
mustRehashAndResign = true mustRehashAndResign = true
} else if !kindWasSupplied { } else if !kindWasSupplied {
evt.Kind = 1 evt.Kind = 1
@ -274,7 +276,7 @@ example:
mustRehashAndResign = true mustRehashAndResign = true
} }
if evt.Sig == "" || mustRehashAndResign { if evt.Sig == [64]byte{} || mustRehashAndResign {
if numSigners := c.Uint("musig"); numSigners > 1 { if numSigners := c.Uint("musig"); numSigners > 1 {
// must do musig // must do musig
pubkeys := c.StringSlice("musig-pubkey") pubkeys := c.StringSlice("musig-pubkey")
@ -364,7 +366,7 @@ example:
low := unwrapAll(res.Error) low := unwrapAll(res.Error)
// hack for some messages such as from relay.westernbtc.com // hack for some messages such as from relay.westernbtc.com
msg := strings.ReplaceAll(low.Error(), evt.PubKey, "author") msg := strings.ReplaceAll(low.Error(), evt.PubKey.Hex(), "author")
// do not allow the message to overflow the term window // do not allow the message to overflow the term window
msg = clampMessage(msg, 20+len(res.RelayURL)) msg = clampMessage(msg, 20+len(res.RelayURL))
@ -393,11 +395,9 @@ example:
if strings.HasPrefix(err.Error(), "msg: auth-required:") && kr != nil && doAuth { if strings.HasPrefix(err.Error(), "msg: auth-required:") && kr != nil && doAuth {
// if the relay is requesting auth and we can auth, let's do it // if the relay is requesting auth and we can auth, let's do it
pk, _ := kr.GetPublicKey(ctx) pk, _ := kr.GetPublicKey(ctx)
npub, _ := nip19.EncodePublicKey(pk) npub := nip19.EncodeNpub(pk)
log("authenticating as %s... ", color.YellowString("%s…%s", npub[0:7], npub[58:])) log("authenticating as %s... ", color.YellowString("%s…%s", npub[0:7], npub[58:]))
if err := relay.Auth(ctx, func(authEvent *nostr.Event) error { if err := relay.Auth(ctx, kr.SignEvent); err == nil {
return kr.SignEvent(ctx, authEvent)
}); err == nil {
// try to publish again, but this time don't try to auth again // try to publish again, but this time don't try to auth again
doAuth = false doAuth = false
goto publish goto publish
@ -410,8 +410,7 @@ example:
} }
if len(successRelays) > 0 && c.Bool("nevent") { if len(successRelays) > 0 && c.Bool("nevent") {
nevent, _ := nip19.EncodeEvent(evt.ID, successRelays, evt.PubKey) log(nip19.EncodeNevent(evt.ID, successRelays, evt.PubKey) + "\n")
log(nevent + "\n")
} }
} }

View File

@ -4,11 +4,11 @@ import (
"context" "context"
"fmt" "fmt"
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/nip05"
"fiatjaf.com/nostr/nip19"
"fiatjaf.com/nostr/sdk/hints"
"github.com/urfave/cli/v3" "github.com/urfave/cli/v3"
"github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nip05"
"github.com/nbd-wtf/go-nostr/nip19"
"github.com/nbd-wtf/go-nostr/sdk/hints"
) )
var fetch = &cli.Command{ var fetch = &cli.Command{
@ -36,7 +36,7 @@ var fetch = &cli.Command{
for code := range getStdinLinesOrArguments(c.Args()) { for code := range getStdinLinesOrArguments(c.Args()) {
filter := nostr.Filter{} filter := nostr.Filter{}
var authorHint string var authorHint nostr.PubKey
relays := c.StringSlice("relay") relays := c.StringSlice("relay")
if nip05.IsValidIdentifier(code) { if nip05.IsValidIdentifier(code) {
@ -63,15 +63,15 @@ var fetch = &cli.Command{
case "nevent": case "nevent":
v := value.(nostr.EventPointer) v := value.(nostr.EventPointer)
filter.IDs = append(filter.IDs, v.ID) filter.IDs = append(filter.IDs, v.ID)
if v.Author != "" { if v.Author != nostr.ZeroPK {
authorHint = v.Author authorHint = v.Author
} }
relays = append(relays, v.Relays...) relays = append(relays, v.Relays...)
case "note": case "note":
filter.IDs = append(filter.IDs, value.(string)) filter.IDs = append(filter.IDs, value.([32]byte))
case "naddr": case "naddr":
v := value.(nostr.EntityPointer) v := value.(nostr.EntityPointer)
filter.Kinds = []int{v.Kind} filter.Kinds = []nostr.Kind{v.Kind}
filter.Tags = nostr.TagMap{"d": []string{v.Identifier}} filter.Tags = nostr.TagMap{"d": []string{v.Identifier}}
filter.Authors = append(filter.Authors, v.PublicKey) filter.Authors = append(filter.Authors, v.PublicKey)
authorHint = v.PublicKey authorHint = v.PublicKey
@ -82,7 +82,7 @@ var fetch = &cli.Command{
authorHint = v.PublicKey authorHint = v.PublicKey
relays = append(relays, v.Relays...) relays = append(relays, v.Relays...)
case "npub": case "npub":
v := value.(string) v := value.(nostr.PubKey)
filter.Authors = append(filter.Authors, v) filter.Authors = append(filter.Authors, v)
authorHint = v authorHint = v
default: default:
@ -90,7 +90,7 @@ var fetch = &cli.Command{
} }
} }
if authorHint != "" { if authorHint != nostr.ZeroPK {
for _, url := range relays { for _, url := range relays {
sys.Hints.Save(authorHint, nostr.NormalizeURL(url), hints.LastInHint, nostr.Now()) sys.Hints.Save(authorHint, nostr.NormalizeURL(url), hints.LastInHint, nostr.Now())
} }
@ -113,7 +113,7 @@ var fetch = &cli.Command{
continue continue
} }
for ie := range sys.Pool.FetchMany(ctx, relays, filter) { for ie := range sys.Pool.FetchMany(ctx, relays, filter, nostr.SubscriptionOptions{}) {
stdout(ie.Event) stdout(ie.Event)
} }
} }

157
flags.go
View File

@ -6,14 +6,18 @@ import (
"strconv" "strconv"
"time" "time"
"github.com/urfave/cli/v3" "fiatjaf.com/nostr"
"github.com/markusmobius/go-dateparser" "github.com/markusmobius/go-dateparser"
"github.com/nbd-wtf/go-nostr" "github.com/urfave/cli/v3"
) )
//
//
//
type NaturalTimeFlag = cli.FlagBase[nostr.Timestamp, struct{}, naturalTimeValue] type NaturalTimeFlag = cli.FlagBase[nostr.Timestamp, struct{}, naturalTimeValue]
// wrap to satisfy golang's flag interface. // wrap to satisfy flag interface.
type naturalTimeValue struct { type naturalTimeValue struct {
timestamp *nostr.Timestamp timestamp *nostr.Timestamp
hasBeenSet bool hasBeenSet bool
@ -39,11 +43,6 @@ func (t naturalTimeValue) ToString(b nostr.Timestamp) string {
return fmt.Sprintf("%v", ts) return fmt.Sprintf("%v", ts)
} }
// Timestamp constructor(for internal testing only)
func newTimestamp(timestamp nostr.Timestamp) *naturalTimeValue {
return &naturalTimeValue{timestamp: &timestamp}
}
// Below functions are to satisfy the flag.Value interface // Below functions are to satisfy the flag.Value interface
// Parses the string value to timestamp // Parses the string value to timestamp
@ -93,3 +92,145 @@ func (t *naturalTimeValue) Get() any {
func getNaturalDate(cmd *cli.Command, name string) nostr.Timestamp { func getNaturalDate(cmd *cli.Command, name string) nostr.Timestamp {
return cmd.Value(name).(nostr.Timestamp) return cmd.Value(name).(nostr.Timestamp)
} }
//
//
//
type (
PubKeyFlag = cli.FlagBase[nostr.PubKey, struct{}, pubkeyValue]
)
// wrap to satisfy flag interface.
type pubkeyValue struct {
pubkey nostr.PubKey
hasBeenSet bool
}
var _ cli.ValueCreator[nostr.PubKey, struct{}] = pubkeyValue{}
// Below functions are to satisfy the ValueCreator interface
func (t pubkeyValue) Create(val nostr.PubKey, p *nostr.PubKey, c struct{}) cli.Value {
*p = val
return &pubkeyValue{
pubkey: val,
}
}
func (t pubkeyValue) ToString(b nostr.PubKey) string {
return t.pubkey.String()
}
// Below functions are to satisfy the flag.Value interface
// Parses the string value to timestamp
func (t *pubkeyValue) Set(value string) error {
pk, err := nostr.PubKeyFromHex(value)
t.pubkey = pk
t.hasBeenSet = true
return err
}
// String returns a readable representation of this value (for usage defaults)
func (t *pubkeyValue) String() string {
return fmt.Sprintf("%#v", t.pubkey)
}
// Value returns the pubkey value stored in the flag
func (t *pubkeyValue) Value() nostr.PubKey {
return t.pubkey
}
// Get returns the flag structure
func (t *pubkeyValue) Get() any {
return t.pubkey
}
func getPubKey(cmd *cli.Command, name string) nostr.PubKey {
return cmd.Value(name).(nostr.PubKey)
}
//
//
//
type (
pubkeySlice = cli.SliceBase[nostr.PubKey, struct{}, pubkeyValue]
PubKeySliceFlag = cli.FlagBase[[]nostr.PubKey, struct{}, pubkeySlice]
)
func getPubKeySlice(cmd *cli.Command, name string) []nostr.PubKey {
return cmd.Value(name).([]nostr.PubKey)
}
//
//
//
type (
IDFlag = cli.FlagBase[nostr.ID, struct{}, idValue]
)
// wrap to satisfy flag interface.
type idValue struct {
id nostr.ID
hasBeenSet bool
}
var _ cli.ValueCreator[nostr.ID, struct{}] = idValue{}
// Below functions are to satisfy the ValueCreator interface
func (t idValue) Create(val nostr.ID, p *nostr.ID, c struct{}) cli.Value {
*p = val
return &idValue{
id: val,
}
}
func (t idValue) ToString(b nostr.ID) string {
return t.id.String()
}
// Below functions are to satisfy the flag.Value interface
// Parses the string value to timestamp
func (t *idValue) Set(value string) error {
pk, err := nostr.IDFromHex(value)
t.id = pk
t.hasBeenSet = true
return err
}
// String returns a readable representation of this value (for usage defaults)
func (t *idValue) String() string {
return fmt.Sprintf("%#v", t.id)
}
// Value returns the id value stored in the flag
func (t *idValue) Value() nostr.ID {
return t.id
}
// Get returns the flag structure
func (t *idValue) Get() any {
return t.id
}
func getID(cmd *cli.Command, name string) nostr.ID {
return cmd.Value(name).(nostr.ID)
}
//
//
//
type (
idSlice = cli.SliceBase[nostr.ID, struct{}, idValue]
IDSliceFlag = cli.FlagBase[[]nostr.ID, struct{}, idSlice]
)
func getIDSlice(cmd *cli.Command, name string) []nostr.ID {
return cmd.Value(name).([]nostr.ID)
}

14
fs.go
View File

@ -10,12 +10,12 @@ import (
"syscall" "syscall"
"time" "time"
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/keyer"
"github.com/fatih/color" "github.com/fatih/color"
"github.com/fiatjaf/nak/nostrfs" "github.com/fiatjaf/nak/nostrfs"
"github.com/hanwen/go-fuse/v2/fs" "github.com/hanwen/go-fuse/v2/fs"
"github.com/hanwen/go-fuse/v2/fuse" "github.com/hanwen/go-fuse/v2/fuse"
"github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/keyer"
"github.com/urfave/cli/v3" "github.com/urfave/cli/v3"
) )
@ -25,15 +25,9 @@ var fsCmd = &cli.Command{
Description: `(experimental)`, Description: `(experimental)`,
ArgsUsage: "<mountpoint>", ArgsUsage: "<mountpoint>",
Flags: append(defaultKeyFlags, Flags: append(defaultKeyFlags,
&cli.StringFlag{ &PubKeyFlag{
Name: "pubkey", Name: "pubkey",
Usage: "public key from where to to prepopulate directories", Usage: "public key from where to to prepopulate directories",
Validator: func(pk string) error {
if nostr.IsValidPublicKey(pk) {
return nil
}
return fmt.Errorf("invalid public key '%s'", pk)
},
}, },
&cli.DurationFlag{ &cli.DurationFlag{
Name: "auto-publish-notes", Name: "auto-publish-notes",
@ -58,7 +52,7 @@ var fsCmd = &cli.Command{
if signer, _, err := gatherKeyerFromArguments(ctx, c); err == nil { if signer, _, err := gatherKeyerFromArguments(ctx, c); err == nil {
kr = signer kr = signer
} else { } else {
kr = keyer.NewReadOnlyUser(c.String("pubkey")) kr = keyer.NewReadOnlyUser(getPubKey(c, "pubkey"))
} }
apnt := c.Duration("auto-publish-notes") apnt := c.Duration("auto-publish-notes")

8
go.mod
View File

@ -4,26 +4,25 @@ go 1.24.1
require ( require (
fiatjaf.com/lib v0.3.1 fiatjaf.com/lib v0.3.1
fiatjaf.com/nostr v0.0.1
github.com/bep/debounce v1.2.1 github.com/bep/debounce v1.2.1
github.com/btcsuite/btcd/btcec/v2 v2.3.4 github.com/btcsuite/btcd/btcec/v2 v2.3.4
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0
github.com/fatih/color v1.16.0 github.com/fatih/color v1.16.0
github.com/fiatjaf/eventstore v0.16.2
github.com/fiatjaf/khatru v0.17.4
github.com/hanwen/go-fuse/v2 v2.7.2 github.com/hanwen/go-fuse/v2 v2.7.2
github.com/json-iterator/go v1.1.12 github.com/json-iterator/go v1.1.12
github.com/liamg/magic v0.0.1 github.com/liamg/magic v0.0.1
github.com/mailru/easyjson v0.9.0 github.com/mailru/easyjson v0.9.0
github.com/mark3labs/mcp-go v0.8.3 github.com/mark3labs/mcp-go v0.8.3
github.com/markusmobius/go-dateparser v1.2.3 github.com/markusmobius/go-dateparser v1.2.3
github.com/nbd-wtf/go-nostr v0.51.8
github.com/urfave/cli/v3 v3.0.0-beta1 github.com/urfave/cli/v3 v3.0.0-beta1
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 golang.org/x/exp v0.0.0-20250305212735-054e65f0b394
golang.org/x/term v0.30.0 golang.org/x/term v0.30.0
) )
require ( require (
github.com/FastFilter/xorfilter v0.2.1 // indirect
github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3 // indirect github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3 // indirect
github.com/andybalholm/brotli v1.1.1 // indirect github.com/andybalholm/brotli v1.1.1 // indirect
github.com/btcsuite/btcd v0.24.2 // indirect github.com/btcsuite/btcd v0.24.2 // indirect
@ -72,6 +71,9 @@ require (
golang.org/x/arch v0.15.0 // indirect golang.org/x/arch v0.15.0 // indirect
golang.org/x/crypto v0.36.0 // indirect golang.org/x/crypto v0.36.0 // indirect
golang.org/x/net v0.37.0 // indirect golang.org/x/net v0.37.0 // indirect
golang.org/x/sync v0.12.0 // indirect
golang.org/x/sys v0.31.0 // indirect golang.org/x/sys v0.31.0 // indirect
golang.org/x/text v0.23.0 // indirect golang.org/x/text v0.23.0 // indirect
) )
replace fiatjaf.com/nostr => ../nostrlib

14
go.sum
View File

@ -1,7 +1,11 @@
fiatjaf.com/lib v0.3.1 h1:/oFQwNtFRfV+ukmOCxfBEAuayoLwXp4wu2/fz5iHpwA= fiatjaf.com/lib v0.3.1 h1:/oFQwNtFRfV+ukmOCxfBEAuayoLwXp4wu2/fz5iHpwA=
fiatjaf.com/lib v0.3.1/go.mod h1:Ycqq3+mJ9jAWu7XjbQI1cVr+OFgnHn79dQR5oTII47g= fiatjaf.com/lib v0.3.1/go.mod h1:Ycqq3+mJ9jAWu7XjbQI1cVr+OFgnHn79dQR5oTII47g=
github.com/FastFilter/xorfilter v0.2.1 h1:lbdeLG9BdpquK64ZsleBS8B4xO/QW1IM0gMzF7KaBKc=
github.com/FastFilter/xorfilter v0.2.1/go.mod h1:aumvdkhscz6YBZF9ZA/6O4fIoNod4YR50kIVGGZ7l9I=
github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3 h1:ClzzXMDDuUbWfNNZqGeYq4PnYOlwlOVIvSyNaIy0ykg= github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3 h1:ClzzXMDDuUbWfNNZqGeYq4PnYOlwlOVIvSyNaIy0ykg=
github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3/go.mod h1:we0YA5CsBbH5+/NUzC/AlMmxaDtWlXeNsqrwXjTzmzA= github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3/go.mod h1:we0YA5CsBbH5+/NUzC/AlMmxaDtWlXeNsqrwXjTzmzA=
github.com/PowerDNS/lmdb-go v1.9.3 h1:AUMY2pZT8WRpkEv39I9Id3MuoHd+NZbTVpNhruVkPTg=
github.com/PowerDNS/lmdb-go v1.9.3/go.mod h1:TE0l+EZK8Z1B4dx070ZxkWTlp8RG1mjN0/+FkFRQMtU=
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
@ -38,6 +42,8 @@ github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY= github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
@ -77,10 +83,6 @@ github.com/fasthttp/websocket v1.5.12 h1:e4RGPpWW2HTbL3zV0Y/t7g0ub294LkiuXXUuTOU
github.com/fasthttp/websocket v1.5.12/go.mod h1:I+liyL7/4moHojiOgUOIKEWm9EIxHqxZChS+aMFltyg= github.com/fasthttp/websocket v1.5.12/go.mod h1:I+liyL7/4moHojiOgUOIKEWm9EIxHqxZChS+aMFltyg=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/fiatjaf/eventstore v0.16.2 h1:h4rHwSwPcqAKqWUsAbYWUhDeSgm2Kp+PBkJc3FgBYu4=
github.com/fiatjaf/eventstore v0.16.2/go.mod h1:0gU8fzYO/bG+NQAVlHtJWOlt3JKKFefh5Xjj2d1dLIs=
github.com/fiatjaf/khatru v0.17.4 h1:VzcLUyBKMlP/CAG4iHJbDJmnZgzhbGLKLxJAUuLRogg=
github.com/fiatjaf/khatru v0.17.4/go.mod h1:VYQ7ZNhs3C1+E4gBnx+DtEgU0BrPdrl3XYF3H+mq6fg=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
@ -149,8 +151,6 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/nbd-wtf/go-nostr v0.51.8 h1:CIoS+YqChcm4e1L1rfMZ3/mIwTz4CwApM2qx7MHNzmE=
github.com/nbd-wtf/go-nostr v0.51.8/go.mod h1:d6+DfvMWYG5pA3dmNMBJd6WCHVDDhkXbHqvfljf0Gzg=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
@ -224,6 +224,8 @@ golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81R
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

View File

@ -17,11 +17,12 @@ import (
"sync" "sync"
"time" "time"
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/nip19"
"fiatjaf.com/nostr/nip42"
"fiatjaf.com/nostr/sdk"
"github.com/fatih/color" "github.com/fatih/color"
jsoniter "github.com/json-iterator/go" jsoniter "github.com/json-iterator/go"
"github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nip19"
"github.com/nbd-wtf/go-nostr/sdk"
"github.com/urfave/cli/v3" "github.com/urfave/cli/v3"
"golang.org/x/term" "golang.org/x/term"
) )
@ -155,8 +156,8 @@ func connectToAllRelays(
ctx context.Context, ctx context.Context,
c *cli.Command, c *cli.Command,
relayUrls []string, relayUrls []string,
preAuthSigner func(ctx context.Context, c *cli.Command, log func(s string, args ...any), authEvent nostr.RelayEvent) (err error), // if this exists we will force preauth preAuthSigner func(ctx context.Context, c *cli.Command, log func(s string, args ...any), authEvent *nostr.Event) (err error), // if this exists we will force preauth
opts ...nostr.PoolOption, opts nostr.PoolOptions,
) []*nostr.Relay { ) []*nostr.Relay {
// first pass to check if these are valid relay URLs // first pass to check if these are valid relay URLs
for _, url := range relayUrls { for _, url := range relayUrls {
@ -166,15 +167,12 @@ func connectToAllRelays(
} }
} }
sys.Pool = nostr.NewSimplePool(context.Background(), opts.EventMiddleware = sys.TrackEventHints
append(opts, opts.PenaltyBox = true
nostr.WithEventMiddleware(sys.TrackEventHints), opts.RelayOptions = nostr.RelayOptions{
nostr.WithPenaltyBox(), RequestHeader: http.Header{textproto.CanonicalMIMEHeaderKey("user-agent"): {"nak/s"}},
nostr.WithRelayOptions( }
nostr.WithRequestHeader(http.Header{textproto.CanonicalMIMEHeaderKey("user-agent"): {"nak/s"}}), sys.Pool = nostr.NewPool(opts)
),
)...,
)
relays := make([]*nostr.Relay, 0, len(relayUrls)) relays := make([]*nostr.Relay, 0, len(relayUrls))
@ -236,7 +234,7 @@ func connectToSingleRelay(
ctx context.Context, ctx context.Context,
c *cli.Command, c *cli.Command,
url string, url string,
preAuthSigner func(ctx context.Context, c *cli.Command, log func(s string, args ...any), authEvent nostr.RelayEvent) (err error), preAuthSigner func(ctx context.Context, c *cli.Command, log func(s string, args ...any), authEvent *nostr.Event) (err error),
colorizepreamble func(c func(string, ...any) string), colorizepreamble func(c func(string, ...any) string),
logthis func(s string, args ...any), logthis func(s string, args ...any),
) *nostr.Relay { ) *nostr.Relay {
@ -249,12 +247,12 @@ func connectToSingleRelay(
time.Sleep(time.Millisecond * 200) time.Sleep(time.Millisecond * 200)
for range 5 { for range 5 {
if err := relay.Auth(ctx, func(authEvent *nostr.Event) error { if err := relay.Auth(ctx, func(ctx context.Context, authEvent *nostr.Event) error {
challengeTag := authEvent.Tags.Find("challenge") challengeTag := authEvent.Tags.Find("challenge")
if challengeTag[1] == "" { if challengeTag[1] == "" {
return fmt.Errorf("auth not received yet *****") // what a giant hack return fmt.Errorf("auth not received yet *****") // what a giant hack
} }
return preAuthSigner(ctx, c, logthis, nostr.RelayEvent{Event: authEvent, Relay: relay}) return preAuthSigner(ctx, c, logthis, authEvent)
}); err == nil { }); err == nil {
// auth succeeded // auth succeeded
goto preauthSuccess goto preauthSuccess
@ -324,10 +322,10 @@ func supportsDynamicMultilineMagic() bool {
return true return true
} }
func authSigner(ctx context.Context, c *cli.Command, log func(s string, args ...any), authEvent nostr.RelayEvent) (err error) { func authSigner(ctx context.Context, c *cli.Command, log func(s string, args ...any), authEvent *nostr.Event) (err error) {
defer func() { defer func() {
if err != nil { if err != nil {
cleanUrl, _ := strings.CutPrefix(authEvent.Relay.URL, "wss://") cleanUrl, _ := strings.CutPrefix(nip42.GetRelayURLFromAuthEvent(*authEvent), "wss://")
log("%s auth failed: %s", colors.errorf(cleanUrl), err) log("%s auth failed: %s", colors.errorf(cleanUrl), err)
} }
}() }()
@ -341,10 +339,10 @@ func authSigner(ctx context.Context, c *cli.Command, log func(s string, args ...
} }
pk, _ := kr.GetPublicKey(ctx) pk, _ := kr.GetPublicKey(ctx)
npub, _ := nip19.EncodePublicKey(pk) npub := nip19.EncodeNpub(pk)
log("authenticating as %s... ", color.YellowString("%s…%s", npub[0:7], npub[58:])) log("authenticating as %s... ", color.YellowString("%s…%s", npub[0:7], npub[58:]))
return kr.SignEvent(ctx, authEvent.Event) return kr.SignEvent(ctx, authEvent)
} }
func lineProcessingError(ctx context.Context, msg string, args ...any) context.Context { func lineProcessingError(ctx context.Context, msg string, args ...any) context.Context {
@ -368,10 +366,6 @@ func randString(n int) string {
return string(b) return string(b)
} }
func leftPadKey(k string) string {
return strings.Repeat("0", 64-len(k)) + k
}
func unwrapAll(err error) error { func unwrapAll(err error) error {
low := err low := err
for n := low; n != nil; n = errors.Unwrap(low) { for n := low; n != nil; n = errors.Unwrap(low) {

View File

@ -2,19 +2,18 @@ package main
import ( import (
"context" "context"
"encoding/hex"
"fmt" "fmt"
"os" "os"
"strings" "strings"
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/keyer"
"fiatjaf.com/nostr/nip19"
"fiatjaf.com/nostr/nip46"
"fiatjaf.com/nostr/nip49"
"github.com/chzyer/readline" "github.com/chzyer/readline"
"github.com/fatih/color" "github.com/fatih/color"
"github.com/urfave/cli/v3" "github.com/urfave/cli/v3"
"github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/keyer"
"github.com/nbd-wtf/go-nostr/nip19"
"github.com/nbd-wtf/go-nostr/nip46"
"github.com/nbd-wtf/go-nostr/nip49"
) )
var defaultKeyFlags = []cli.Flag{ var defaultKeyFlags = []cli.Flag{
@ -39,10 +38,10 @@ var defaultKeyFlags = []cli.Flag{
}, },
} }
func gatherKeyerFromArguments(ctx context.Context, c *cli.Command) (nostr.Keyer, string, error) { func gatherKeyerFromArguments(ctx context.Context, c *cli.Command) (nostr.Keyer, nostr.SecretKey, error) {
key, bunker, err := gatherSecretKeyOrBunkerFromArguments(ctx, c) key, bunker, err := gatherSecretKeyOrBunkerFromArguments(ctx, c)
if err != nil { if err != nil {
return nil, "", err return nil, nostr.SecretKey{}, err
} }
var kr nostr.Keyer var kr nostr.Keyer
@ -55,23 +54,27 @@ func gatherKeyerFromArguments(ctx context.Context, c *cli.Command) (nostr.Keyer,
return kr, key, err return kr, key, err
} }
func gatherSecretKeyOrBunkerFromArguments(ctx context.Context, c *cli.Command) (string, *nip46.BunkerClient, error) { func gatherSecretKeyOrBunkerFromArguments(ctx context.Context, c *cli.Command) (nostr.SecretKey, *nip46.BunkerClient, error) {
var err error var err error
sec := c.String("sec") sec := c.String("sec")
if strings.HasPrefix(sec, "bunker://") { if strings.HasPrefix(sec, "bunker://") {
// it's a bunker // it's a bunker
bunkerURL := sec bunkerURL := sec
clientKey := c.String("connect-as") clientKeyHex := c.String("connect-as")
if clientKey != "" { var clientKey nostr.SecretKey
clientKey = strings.Repeat("0", 64-len(clientKey)) + clientKey
if clientKeyHex != "" {
clientKey, err = nostr.SecretKeyFromHex(sec)
} else { } else {
clientKey = nostr.GeneratePrivateKey() clientKey = nostr.Generate()
} }
bunker, err := nip46.ConnectBunker(ctx, clientKey, bunkerURL, nil, func(s string) { bunker, err := nip46.ConnectBunker(ctx, clientKey, bunkerURL, nil, func(s string) {
log(color.CyanString("[nip46]: open the following URL: %s"), s) log(color.CyanString("[nip46]: open the following URL: %s"), s)
}) })
return "", bunker, err
return nostr.SecretKey{}, bunker, err
} }
// take private from flags, environment variable or default to 1 // take private from flags, environment variable or default to 1
@ -85,35 +88,35 @@ func gatherSecretKeyOrBunkerFromArguments(ctx context.Context, c *cli.Command) (
if c.Bool("prompt-sec") { if c.Bool("prompt-sec") {
if isPiped() { if isPiped() {
return "", nil, fmt.Errorf("can't prompt for a secret key when processing data from a pipe, try again without --prompt-sec") return nostr.SecretKey{}, nil, fmt.Errorf("can't prompt for a secret key when processing data from a pipe, try again without --prompt-sec")
} }
sec, err = askPassword("type your secret key as ncryptsec, nsec or hex: ", nil) sec, err = askPassword("type your secret key as ncryptsec, nsec or hex: ", nil)
if err != nil { if err != nil {
return "", nil, fmt.Errorf("failed to get secret key: %w", err) return nostr.SecretKey{}, nil, fmt.Errorf("failed to get secret key: %w", err)
} }
} }
if strings.HasPrefix(sec, "ncryptsec1") { if strings.HasPrefix(sec, "ncryptsec1") {
sec, err = promptDecrypt(sec) sk, err := promptDecrypt(sec)
if err != nil { if err != nil {
return "", nil, fmt.Errorf("failed to decrypt: %w", err) return nostr.SecretKey{}, nil, fmt.Errorf("failed to decrypt: %w", err)
} }
} else if bsec, err := hex.DecodeString(leftPadKey(sec)); err == nil { return sk, nil, nil
sec = hex.EncodeToString(bsec)
} else if prefix, hexvalue, err := nip19.Decode(sec); err != nil {
return "", nil, fmt.Errorf("invalid nsec: %w", err)
} else if prefix == "nsec" {
sec = hexvalue.(string)
} }
if ok := nostr.IsValid32ByteHex(sec); !ok { if prefix, ski, err := nip19.Decode(sec); err == nil && prefix == "nsec" {
return "", nil, fmt.Errorf("invalid secret key") return ski.(nostr.SecretKey), nil, nil
} }
return sec, nil, nil sk, err := nostr.SecretKeyFromHex(sec)
if err != nil {
return nostr.SecretKey{}, nil, fmt.Errorf("invalid secret key")
}
return sk, nil, nil
} }
func promptDecrypt(ncryptsec string) (string, error) { func promptDecrypt(ncryptsec string) (nostr.SecretKey, error) {
for i := 1; i < 4; i++ { for i := 1; i < 4; i++ {
var attemptStr string var attemptStr string
if i > 1 { if i > 1 {
@ -121,7 +124,7 @@ func promptDecrypt(ncryptsec string) (string, error) {
} }
password, err := askPassword("type the password to decrypt your secret key"+attemptStr+": ", nil) password, err := askPassword("type the password to decrypt your secret key"+attemptStr+": ", nil)
if err != nil { if err != nil {
return "", err return nostr.SecretKey{}, err
} }
sec, err := nip49.Decrypt(ncryptsec, password) sec, err := nip49.Decrypt(ncryptsec, password)
if err != nil { if err != nil {
@ -129,7 +132,7 @@ func promptDecrypt(ncryptsec string) (string, error) {
} }
return sec, nil return sec, nil
} }
return "", fmt.Errorf("couldn't decrypt private key") return nostr.SecretKey{}, fmt.Errorf("couldn't decrypt private key")
} }
func askPassword(msg string, shouldAskAgain func(answer string) bool) (string, error) { func askPassword(msg string, shouldAskAgain func(answer string) bool) (string, error) {

33
key.go
View File

@ -6,13 +6,13 @@ import (
"fmt" "fmt"
"strings" "strings"
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/nip19"
"fiatjaf.com/nostr/nip49"
"github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
"github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/urfave/cli/v3" "github.com/urfave/cli/v3"
"github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nip19"
"github.com/nbd-wtf/go-nostr/nip49"
) )
var key = &cli.Command{ var key = &cli.Command{
@ -35,8 +35,8 @@ var generate = &cli.Command{
Description: ``, Description: ``,
DisableSliceFlagSeparator: true, DisableSliceFlagSeparator: true,
Action: func(ctx context.Context, c *cli.Command) error { Action: func(ctx context.Context, c *cli.Command) error {
sec := nostr.GeneratePrivateKey() sec := nostr.Generate()
stdout(sec) stdout(sec.Hex())
return nil return nil
}, },
} }
@ -54,9 +54,8 @@ var public = &cli.Command{
}, },
}, },
Action: func(ctx context.Context, c *cli.Command) error { Action: func(ctx context.Context, c *cli.Command) error {
for sec := range getSecretKeysFromStdinLinesOrSlice(ctx, c, c.Args().Slice()) { for sk := range getSecretKeysFromStdinLinesOrSlice(ctx, c, c.Args().Slice()) {
b, _ := hex.DecodeString(sec) _, pk := btcec.PrivKeyFromBytes(sk[:])
_, pk := btcec.PrivKeyFromBytes(b)
if c.Bool("with-parity") { if c.Bool("with-parity") {
stdout(hex.EncodeToString(pk.SerializeCompressed())) stdout(hex.EncodeToString(pk.SerializeCompressed()))
@ -264,27 +263,31 @@ However, if the intent is to check if two existing Nostr pubkeys match a given c
}, },
} }
func getSecretKeysFromStdinLinesOrSlice(ctx context.Context, _ *cli.Command, keys []string) chan string { func getSecretKeysFromStdinLinesOrSlice(ctx context.Context, _ *cli.Command, keys []string) chan nostr.SecretKey {
ch := make(chan string) ch := make(chan nostr.SecretKey)
go func() { go func() {
for sec := range getStdinLinesOrArgumentsFromSlice(keys) { for sec := range getStdinLinesOrArgumentsFromSlice(keys) {
if sec == "" { if sec == "" {
continue continue
} }
var sk nostr.SecretKey
if strings.HasPrefix(sec, "nsec1") { if strings.HasPrefix(sec, "nsec1") {
_, data, err := nip19.Decode(sec) _, data, err := nip19.Decode(sec)
if err != nil { if err != nil {
ctx = lineProcessingError(ctx, "invalid nsec code: %s", err) ctx = lineProcessingError(ctx, "invalid nsec code: %s", err)
continue continue
} }
sec = data.(string) sk = data.(nostr.SecretKey)
} }
sec = leftPadKey(sec)
if !nostr.IsValid32ByteHex(sec) { sk, err := nostr.SecretKeyFromHex(sec)
ctx = lineProcessingError(ctx, "invalid hex key") if err != nil {
ctx = lineProcessingError(ctx, "invalid hex key: %s", err)
continue continue
} }
ch <- sec
ch <- sk
} }
close(ch) close(ch)
}() }()

20
main.go
View File

@ -7,9 +7,9 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"github.com/nbd-wtf/go-nostr" "fiatjaf.com/nostr"
"github.com/nbd-wtf/go-nostr/sdk" "fiatjaf.com/nostr/sdk"
"github.com/nbd-wtf/go-nostr/sdk/hints/memoryh" "fiatjaf.com/nostr/sdk/hints/memoryh"
"github.com/urfave/cli/v3" "github.com/urfave/cli/v3"
) )
@ -111,13 +111,13 @@ var app = &cli.Command{
sys = sdk.NewSystem() sys = sdk.NewSystem()
systemOperational: systemOperational:
sys.Pool = nostr.NewSimplePool(context.Background(), sys.Pool = nostr.NewPool(nostr.PoolOptions{
nostr.WithAuthorKindQueryMiddleware(sys.TrackQueryAttempts), AuthorKindQueryMiddleware: sys.TrackQueryAttempts,
nostr.WithEventMiddleware(sys.TrackEventHints), EventMiddleware: sys.TrackEventHints,
nostr.WithRelayOptions( RelayOptions: nostr.RelayOptions{
nostr.WithRequestHeader(http.Header{textproto.CanonicalMIMEHeaderKey("user-agent"): {"nak/b"}}), RequestHeader: http.Header{textproto.CanonicalMIMEHeaderKey("user-agent"): {"nak/b"}},
), },
) })
return ctx, nil return ctx, nil
}, },

74
mcp.go
View File

@ -6,11 +6,11 @@ import (
"os" "os"
"strings" "strings"
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/nip19"
"fiatjaf.com/nostr/sdk"
"github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server" "github.com/mark3labs/mcp-go/server"
"github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nip19"
"github.com/nbd-wtf/go-nostr/sdk"
"github.com/urfave/cli/v3" "github.com/urfave/cli/v3"
) )
@ -19,13 +19,23 @@ var mcpServer = &cli.Command{
Usage: "pander to the AI gods", Usage: "pander to the AI gods",
Description: ``, Description: ``,
DisableSliceFlagSeparator: true, DisableSliceFlagSeparator: true,
Flags: []cli.Flag{}, Flags: append(
defaultKeyFlags,
),
Action: func(ctx context.Context, c *cli.Command) error { Action: func(ctx context.Context, c *cli.Command) error {
s := server.NewMCPServer( s := server.NewMCPServer(
"nak", "nak",
version, version,
) )
keyer, sk, err := gatherKeyerFromArguments(ctx, c)
if err != nil {
return err
}
if sk == nostr.KeyOne && !c.IsSet("sec") {
keyer = nil
}
s.AddTool(mcp.NewTool("publish_note", s.AddTool(mcp.NewTool("publish_note",
mcp.WithDescription("Publish a short note event to Nostr with the given text content"), mcp.WithDescription("Publish a short note event to Nostr with the given text content"),
mcp.WithString("content", mcp.Description("Arbitrary string to be published"), mcp.Required()), mcp.WithString("content", mcp.Description("Arbitrary string to be published"), mcp.Required()),
@ -36,10 +46,6 @@ var mcpServer = &cli.Command{
mention, _ := optional[string](r, "mention") mention, _ := optional[string](r, "mention")
relay, _ := optional[string](r, "relay") relay, _ := optional[string](r, "relay")
if mention != "" && !nostr.IsValidPublicKey(mention) {
return mcp.NewToolResultError("the given mention isn't a valid public key, it must be 32 bytes hex, like the ones returned by search_profile"), nil
}
sk := os.Getenv("NOSTR_SECRET_KEY") sk := os.Getenv("NOSTR_SECRET_KEY")
if sk == "" { if sk == "" {
sk = "0000000000000000000000000000000000000000000000000000000000000001" sk = "0000000000000000000000000000000000000000000000000000000000000001"
@ -54,12 +60,19 @@ var mcpServer = &cli.Command{
} }
if mention != "" { if mention != "" {
evt.Tags = append(evt.Tags, nostr.Tag{"p", mention}) pk, err := nostr.PubKeyFromHex(mention)
// their inbox relays if err != nil {
relays = sys.FetchInboxRelays(ctx, mention, 3) return mcp.NewToolResultError("the given mention isn't a valid public key, it must be 32 bytes hex, like the ones returned by search_profile. Got error: " + err.Error()), nil
} }
evt.Sign(sk) evt.Tags = append(evt.Tags, nostr.Tag{"p", pk.Hex()})
// their inbox relays
relays = sys.FetchInboxRelays(ctx, pk, 3)
}
if err := keyer.SignEvent(ctx, &evt); err != nil {
return mcp.NewToolResultError("it was impossible to sign the event, so we can't proceed to publishwith publishing it."), nil
}
// our write relays // our write relays
relays = append(relays, sys.FetchOutboxRelays(ctx, evt.PubKey, 3)...) relays = append(relays, sys.FetchOutboxRelays(ctx, evt.PubKey, 3)...)
@ -115,7 +128,7 @@ var mcpServer = &cli.Command{
switch prefix { switch prefix {
case "npub": case "npub":
pm := sys.FetchProfileMetadata(ctx, data.(string)) pm := sys.FetchProfileMetadata(ctx, data.(nostr.PubKey))
return mcp.NewToolResultText( return mcp.NewToolResultText(
fmt.Sprintf("this is a Nostr profile named '%s', their public key is '%s'", fmt.Sprintf("this is a Nostr profile named '%s', their public key is '%s'",
pm.ShortName(), pm.PubKey), pm.ShortName(), pm.PubKey),
@ -149,19 +162,23 @@ var mcpServer = &cli.Command{
mcp.WithString("name", mcp.Description("Name to be searched"), mcp.Required()), mcp.WithString("name", mcp.Description("Name to be searched"), mcp.Required()),
), func(ctx context.Context, r mcp.CallToolRequest) (*mcp.CallToolResult, error) { ), func(ctx context.Context, r mcp.CallToolRequest) (*mcp.CallToolResult, error) {
name := required[string](r, "name") name := required[string](r, "name")
re := sys.Pool.QuerySingle(ctx, []string{"relay.nostr.band", "nostr.wine"}, nostr.Filter{Search: name, Kinds: []int{0}}) re := sys.Pool.QuerySingle(ctx, []string{"relay.nostr.band", "nostr.wine"}, nostr.Filter{Search: name, Kinds: []nostr.Kind{0}}, nostr.SubscriptionOptions{})
if re == nil { if re == nil {
return mcp.NewToolResultError("couldn't find anyone with that name"), nil return mcp.NewToolResultError("couldn't find anyone with that name"), nil
} }
return mcp.NewToolResultText(re.PubKey), nil return mcp.NewToolResultText(re.PubKey.Hex()), nil
}) })
s.AddTool(mcp.NewTool("get_outbox_relay_for_pubkey", s.AddTool(mcp.NewTool("get_outbox_relay_for_pubkey",
mcp.WithDescription("Get the best relay from where to read notes from a specific Nostr user"), mcp.WithDescription("Get the best relay from where to read notes from a specific Nostr user"),
mcp.WithString("pubkey", mcp.Description("Public key of Nostr user we want to know the relay from where to read"), mcp.Required()), mcp.WithString("pubkey", mcp.Description("Public key of Nostr user we want to know the relay from where to read"), mcp.Required()),
), func(ctx context.Context, r mcp.CallToolRequest) (*mcp.CallToolResult, error) { ), func(ctx context.Context, r mcp.CallToolRequest) (*mcp.CallToolResult, error) {
pubkey := required[string](r, "pubkey") pubkey, err := nostr.PubKeyFromHex(required[string](r, "pubkey"))
if err != nil {
return mcp.NewToolResultError("the pubkey given isn't a valid public key, it must be 32 bytes hex, like the ones returned by search_profile. Got error: " + err.Error()), nil
}
res := sys.FetchOutboxRelays(ctx, pubkey, 1) res := sys.FetchOutboxRelays(ctx, pubkey, 1)
return mcp.NewToolResultText(res[0]), nil return mcp.NewToolResultText(res[0]), nil
}) })
@ -171,31 +188,32 @@ var mcpServer = &cli.Command{
mcp.WithString("relay", mcp.Description("relay URL to send the query to"), mcp.Required()), mcp.WithString("relay", mcp.Description("relay URL to send the query to"), mcp.Required()),
mcp.WithNumber("kind", mcp.Description("event kind number to include in the 'kinds' field"), mcp.Required()), mcp.WithNumber("kind", mcp.Description("event kind number to include in the 'kinds' field"), mcp.Required()),
mcp.WithNumber("limit", mcp.Description("maximum number of events to query"), mcp.Required()), mcp.WithNumber("limit", mcp.Description("maximum number of events to query"), mcp.Required()),
mcp.WithString("pubkey", mcp.Description("pubkey to include in the 'authors' field")), mcp.WithString("pubkey", mcp.Description("pubkey to include in the 'authors' field, if this is not given we will read any events from this relay")),
), func(ctx context.Context, r mcp.CallToolRequest) (*mcp.CallToolResult, error) { ), func(ctx context.Context, r mcp.CallToolRequest) (*mcp.CallToolResult, error) {
relay := required[string](r, "relay") relay := required[string](r, "relay")
kind := int(required[float64](r, "kind")) kind := int(required[float64](r, "kind"))
limit := int(required[float64](r, "limit")) limit := int(required[float64](r, "limit"))
pubkey, _ := optional[string](r, "pubkey") pubkey, hasPubKey := optional[string](r, "pubkey")
if pubkey != "" && !nostr.IsValidPublicKey(pubkey) {
return mcp.NewToolResultError("the given pubkey isn't a valid public key, it must be 32 bytes hex, like the ones returned by search_profile"), nil
}
filter := nostr.Filter{ filter := nostr.Filter{
Limit: limit, Limit: limit,
Kinds: []int{kind}, Kinds: []nostr.Kind{nostr.Kind(kind)},
}
if pubkey != "" {
filter.Authors = []string{pubkey}
} }
events := sys.Pool.FetchMany(ctx, []string{relay}, filter) if hasPubKey {
if pk, err := nostr.PubKeyFromHex(pubkey); err != nil {
return mcp.NewToolResultError("the pubkey given isn't a valid public key, it must be 32 bytes hex, like the ones returned by search_profile. Got error: " + err.Error()), nil
} else {
filter.Authors = append(filter.Authors, pk)
}
}
events := sys.Pool.FetchMany(ctx, []string{relay}, filter, nostr.SubscriptionOptions{})
result := strings.Builder{} result := strings.Builder{}
for ie := range events { for ie := range events {
result.WriteString("author public key: ") result.WriteString("author public key: ")
result.WriteString(ie.PubKey) result.WriteString(ie.PubKey.Hex())
result.WriteString("content: '") result.WriteString("content: '")
result.WriteString(ie.Content) result.WriteString(ie.Content)
result.WriteString("'") result.WriteString("'")

View File

@ -9,39 +9,39 @@ import (
"strconv" "strconv"
"strings" "strings"
"fiatjaf.com/nostr"
"github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
"github.com/nbd-wtf/go-nostr"
) )
func getMusigAggregatedKey(_ context.Context, keys []string) (string, error) { func getMusigAggregatedKey(_ context.Context, keys []string) (nostr.PubKey, error) {
knownSigners := make([]*btcec.PublicKey, len(keys)) knownSigners := make([]*btcec.PublicKey, len(keys))
for i, spk := range keys { for i, spk := range keys {
bpk, err := hex.DecodeString(spk) bpk, err := hex.DecodeString(spk)
if err != nil { if err != nil {
return "", fmt.Errorf("'%s' is invalid hex: %w", spk, err) return nostr.ZeroPK, fmt.Errorf("'%s' is invalid hex: %w", spk, err)
} }
if len(bpk) == 32 { if len(bpk) == 32 {
return "", fmt.Errorf("'%s' is missing the leading parity byte", spk) return nostr.ZeroPK, fmt.Errorf("'%s' is missing the leading parity byte", spk)
} }
pk, err := btcec.ParsePubKey(bpk) pk, err := btcec.ParsePubKey(bpk)
if err != nil { if err != nil {
return "", fmt.Errorf("'%s' is not a valid pubkey: %w", spk, err) return nostr.ZeroPK, fmt.Errorf("'%s' is not a valid pubkey: %w", spk, err)
} }
knownSigners[i] = pk knownSigners[i] = pk
} }
aggpk, _, _, err := musig2.AggregateKeys(knownSigners, true) aggpk, _, _, err := musig2.AggregateKeys(knownSigners, true)
if err != nil { if err != nil {
return "", fmt.Errorf("aggregation failed: %w", err) return nostr.ZeroPK, fmt.Errorf("aggregation failed: %w", err)
} }
return hex.EncodeToString(aggpk.FinalKey.SerializeCompressed()[1:]), nil return nostr.PubKey(aggpk.FinalKey.SerializeCompressed()[1:]), nil
} }
func performMusig( func performMusig(
_ context.Context, _ context.Context,
sec string, sec nostr.SecretKey,
evt *nostr.Event, evt *nostr.Event,
numSigners int, numSigners int,
keys []string, keys []string,
@ -50,11 +50,7 @@ func performMusig(
partialSigs []string, partialSigs []string,
) (signed bool, err error) { ) (signed bool, err error) {
// preprocess data received // preprocess data received
secb, err := hex.DecodeString(sec) seck, pubk := btcec.PrivKeyFromBytes(sec[:])
if err != nil {
return false, err
}
seck, pubk := btcec.PrivKeyFromBytes(secb)
knownSigners := make([]*btcec.PublicKey, 0, numSigners) knownSigners := make([]*btcec.PublicKey, 0, numSigners)
includesUs := false includesUs := false
@ -146,7 +142,7 @@ func performMusig(
if comb, err := mctx.CombinedKey(); err != nil { if comb, err := mctx.CombinedKey(); err != nil {
return false, fmt.Errorf("failed to combine keys (after %d signers): %w", len(knownSigners), err) return false, fmt.Errorf("failed to combine keys (after %d signers): %w", len(knownSigners), err)
} else { } else {
evt.PubKey = hex.EncodeToString(comb.SerializeCompressed()[1:]) evt.PubKey = nostr.PubKey(comb.SerializeCompressed()[1:])
evt.ID = evt.GetID() evt.ID = evt.GetID()
log("combined key: %x\n\n", comb.SerializeCompressed()) log("combined key: %x\n\n", comb.SerializeCompressed())
} }
@ -200,11 +196,7 @@ func performMusig(
// signing phase // signing phase
// we always have to sign, so let's do this // we always have to sign, so let's do this
id := evt.GetID() partialSig, err := session.Sign(evt.GetID()) // this will already include our sig in the bundle
hash, _ := hex.DecodeString(id)
var msg32 [32]byte
copy(msg32[:], hash)
partialSig, err := session.Sign(msg32) // this will already include our sig in the bundle
if err != nil { if err != nil {
return false, fmt.Errorf("failed to produce partial signature: %w", err) return false, fmt.Errorf("failed to produce partial signature: %w", err)
} }
@ -225,7 +217,7 @@ func performMusig(
} }
// we have the signature // we have the signature
evt.Sig = hex.EncodeToString(session.FinalSig().Serialize()) evt.Sig = [64]byte(session.FinalSig().Serialize())
return true, nil return true, nil
} }
@ -258,7 +250,7 @@ func eventToCliArgs(evt *nostr.Event) string {
b.Grow(100) b.Grow(100)
b.WriteString("-k ") b.WriteString("-k ")
b.WriteString(strconv.Itoa(evt.Kind)) b.WriteString(strconv.Itoa(int(evt.Kind)))
b.WriteString(" -ts ") b.WriteString(" -ts ")
b.WriteString(strconv.FormatInt(int64(evt.CreatedAt), 10)) b.WriteString(strconv.FormatInt(int64(evt.CreatedAt), 10))
@ -269,7 +261,7 @@ func eventToCliArgs(evt *nostr.Event) string {
for _, tag := range evt.Tags { for _, tag := range evt.Tags {
b.WriteString(" -t '") b.WriteString(" -t '")
b.WriteString(tag.Key()) b.WriteString(tag[0])
if len(tag) > 1 { if len(tag) > 1 {
b.WriteString("=") b.WriteString("=")
b.WriteString(tag[1]) b.WriteString(tag[1])

View File

@ -7,7 +7,7 @@ import (
"github.com/hanwen/go-fuse/v2/fs" "github.com/hanwen/go-fuse/v2/fs"
"github.com/hanwen/go-fuse/v2/fuse" "github.com/hanwen/go-fuse/v2/fuse"
"github.com/nbd-wtf/go-nostr" "fiatjaf.com/nostr"
) )
type AsyncFile struct { type AsyncFile struct {

View File

@ -15,14 +15,15 @@ import (
"unsafe" "unsafe"
"fiatjaf.com/lib/debouncer" "fiatjaf.com/lib/debouncer"
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/nip19"
"fiatjaf.com/nostr/nip27"
"fiatjaf.com/nostr/nip73"
"fiatjaf.com/nostr/nip92"
sdk "fiatjaf.com/nostr/sdk"
"github.com/fatih/color" "github.com/fatih/color"
"github.com/hanwen/go-fuse/v2/fs" "github.com/hanwen/go-fuse/v2/fs"
"github.com/hanwen/go-fuse/v2/fuse" "github.com/hanwen/go-fuse/v2/fuse"
"github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nip19"
"github.com/nbd-wtf/go-nostr/nip27"
"github.com/nbd-wtf/go-nostr/nip92"
sdk "github.com/nbd-wtf/go-nostr/sdk"
) )
type EntityDir struct { type EntityDir struct {
@ -95,11 +96,10 @@ func (e *EntityDir) Setattr(_ context.Context, _ fs.FileHandle, in *fuse.SetAttr
func (e *EntityDir) OnAdd(_ context.Context) { func (e *EntityDir) OnAdd(_ context.Context) {
log := e.root.ctx.Value("log").(func(msg string, args ...any)) log := e.root.ctx.Value("log").(func(msg string, args ...any))
npub, _ := nip19.EncodePublicKey(e.event.PubKey)
e.AddChild("@author", e.NewPersistentInode( e.AddChild("@author", e.NewPersistentInode(
e.root.ctx, e.root.ctx,
&fs.MemSymlink{ &fs.MemSymlink{
Data: []byte(e.root.wd + "/" + npub), Data: []byte(e.root.wd + "/" + nip19.EncodeNpub(e.event.PubKey)),
}, },
fs.StableAttr{Mode: syscall.S_IFLNK}, fs.StableAttr{Mode: syscall.S_IFLNK},
), true) ), true)
@ -180,8 +180,12 @@ func (e *EntityDir) OnAdd(_ context.Context) {
var refsdir *fs.Inode var refsdir *fs.Inode
i := 0 i := 0
for ref := range nip27.ParseReferences(*e.event) { for ref := range nip27.Parse(e.event.Content) {
if _, isExternal := ref.Pointer.(nip73.ExternalPointer); isExternal {
continue
}
i++ i++
if refsdir == nil { if refsdir == nil {
refsdir = e.NewPersistentInode(e.root.ctx, &fs.Inode{}, fs.StableAttr{Mode: syscall.S_IFDIR}) refsdir = e.NewPersistentInode(e.root.ctx, &fs.Inode{}, fs.StableAttr{Mode: syscall.S_IFDIR})
e.root.AddChild("references", refsdir, true) e.root.AddChild("references", refsdir, true)
@ -320,7 +324,11 @@ func (e *EntityDir) handleWrite() {
} }
// add "p" tags from people mentioned and "q" tags from events mentioned // add "p" tags from people mentioned and "q" tags from events mentioned
for ref := range nip27.ParseReferences(evt) { for ref := range nip27.Parse(evt.Content) {
if _, isExternal := ref.Pointer.(nip73.ExternalPointer); isExternal {
continue
}
tag := ref.Pointer.AsTag() tag := ref.Pointer.AsTag()
key := tag[0] key := tag[0]
val := tag[1] val := tag[1]
@ -339,7 +347,7 @@ func (e *EntityDir) handleWrite() {
} }
logverbose("%s\n", evt) logverbose("%s\n", evt)
relays := e.root.sys.FetchWriteRelays(e.root.ctx, e.root.rootPubKey, 8) relays := e.root.sys.FetchWriteRelays(e.root.ctx, e.root.rootPubKey)
if len(relays) == 0 { if len(relays) == 0 {
relays = e.root.sys.FetchOutboxRelays(e.root.ctx, e.root.rootPubKey, 6) relays = e.root.sys.FetchOutboxRelays(e.root.ctx, e.root.rootPubKey, 6)
} }

View File

@ -3,6 +3,7 @@ package nostrfs
import ( import (
"bytes" "bytes"
"context" "context"
"encoding/binary"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
@ -11,16 +12,16 @@ import (
"syscall" "syscall"
"time" "time"
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/nip10"
"fiatjaf.com/nostr/nip19"
"fiatjaf.com/nostr/nip22"
"fiatjaf.com/nostr/nip27"
"fiatjaf.com/nostr/nip73"
"fiatjaf.com/nostr/nip92"
sdk "fiatjaf.com/nostr/sdk"
"github.com/hanwen/go-fuse/v2/fs" "github.com/hanwen/go-fuse/v2/fs"
"github.com/hanwen/go-fuse/v2/fuse" "github.com/hanwen/go-fuse/v2/fuse"
"github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nip10"
"github.com/nbd-wtf/go-nostr/nip19"
"github.com/nbd-wtf/go-nostr/nip22"
"github.com/nbd-wtf/go-nostr/nip27"
"github.com/nbd-wtf/go-nostr/nip73"
"github.com/nbd-wtf/go-nostr/nip92"
sdk "github.com/nbd-wtf/go-nostr/sdk"
) )
type EventDir struct { type EventDir struct {
@ -58,14 +59,13 @@ func (r *NostrRoot) CreateEventDir(
h := parent.EmbeddedInode().NewPersistentInode( h := parent.EmbeddedInode().NewPersistentInode(
r.ctx, r.ctx,
&EventDir{ctx: r.ctx, wd: r.wd, evt: event}, &EventDir{ctx: r.ctx, wd: r.wd, evt: event},
fs.StableAttr{Mode: syscall.S_IFDIR, Ino: hexToUint64(event.ID)}, fs.StableAttr{Mode: syscall.S_IFDIR, Ino: binary.BigEndian.Uint64(event.ID[8:16])},
) )
npub, _ := nip19.EncodePublicKey(event.PubKey)
h.AddChild("@author", h.NewPersistentInode( h.AddChild("@author", h.NewPersistentInode(
r.ctx, r.ctx,
&fs.MemSymlink{ &fs.MemSymlink{
Data: []byte(r.wd + "/" + npub), Data: []byte(r.wd + "/" + nip19.EncodeNpub(event.PubKey)),
}, },
fs.StableAttr{Mode: syscall.S_IFLNK}, fs.StableAttr{Mode: syscall.S_IFLNK},
), true) ), true)
@ -88,7 +88,7 @@ func (r *NostrRoot) CreateEventDir(
h.AddChild("id", h.NewPersistentInode( h.AddChild("id", h.NewPersistentInode(
r.ctx, r.ctx,
&fs.MemRegularFile{ &fs.MemRegularFile{
Data: []byte(event.ID), Data: []byte(event.ID.Hex()),
Attr: fuse.Attr{ Attr: fuse.Attr{
Mode: 0444, Mode: 0444,
Ctime: uint64(event.CreatedAt), Ctime: uint64(event.CreatedAt),
@ -115,8 +115,12 @@ func (r *NostrRoot) CreateEventDir(
var refsdir *fs.Inode var refsdir *fs.Inode
i := 0 i := 0
for ref := range nip27.ParseReferences(*event) { for ref := range nip27.Parse(event.Content) {
if _, isExternal := ref.Pointer.(nip73.ExternalPointer); isExternal {
continue
}
i++ i++
if refsdir == nil { if refsdir == nil {
refsdir = h.NewPersistentInode(r.ctx, &fs.Inode{}, fs.StableAttr{Mode: syscall.S_IFDIR}) refsdir = h.NewPersistentInode(r.ctx, &fs.Inode{}, fs.StableAttr{Mode: syscall.S_IFDIR})
h.AddChild("references", refsdir, true) h.AddChild("references", refsdir, true)

View File

@ -1,8 +1,10 @@
package nostrfs package nostrfs
import "strconv" import (
"fiatjaf.com/nostr"
)
func kindToExtension(kind int) string { func kindToExtension(kind nostr.Kind) string {
switch kind { switch kind {
case 30023: case 30023:
return "md" return "md"
@ -12,8 +14,3 @@ func kindToExtension(kind int) string {
return "txt" return "txt"
} }
} }
func hexToUint64(hexStr string) uint64 {
v, _ := strconv.ParseUint(hexStr[16:32], 16, 64)
return v
}

View File

@ -3,6 +3,7 @@ package nostrfs
import ( import (
"bytes" "bytes"
"context" "context"
"encoding/binary"
"encoding/json" "encoding/json"
"io" "io"
"net/http" "net/http"
@ -10,12 +11,12 @@ import (
"syscall" "syscall"
"time" "time"
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/nip19"
"github.com/fatih/color" "github.com/fatih/color"
"github.com/hanwen/go-fuse/v2/fs" "github.com/hanwen/go-fuse/v2/fs"
"github.com/hanwen/go-fuse/v2/fuse" "github.com/hanwen/go-fuse/v2/fuse"
"github.com/liamg/magic" "github.com/liamg/magic"
"github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nip19"
) )
type NpubDir struct { type NpubDir struct {
@ -36,7 +37,7 @@ func (r *NostrRoot) CreateNpubDir(
return parent.EmbeddedInode().NewPersistentInode( return parent.EmbeddedInode().NewPersistentInode(
r.ctx, r.ctx,
npubdir, npubdir,
fs.StableAttr{Mode: syscall.S_IFDIR, Ino: hexToUint64(pointer.PublicKey)}, fs.StableAttr{Mode: syscall.S_IFDIR, Ino: binary.BigEndian.Uint64(pointer.PublicKey[8:16])},
) )
} }
@ -49,7 +50,7 @@ func (h *NpubDir) OnAdd(_ context.Context) {
h.AddChild("pubkey", h.NewPersistentInode( h.AddChild("pubkey", h.NewPersistentInode(
h.root.ctx, h.root.ctx,
&fs.MemRegularFile{Data: []byte(h.pointer.PublicKey + "\n"), Attr: fuse.Attr{Mode: 0444}}, &fs.MemRegularFile{Data: []byte(h.pointer.PublicKey.Hex() + "\n"), Attr: fuse.Attr{Mode: 0444}},
fs.StableAttr{}, fs.StableAttr{},
), true) ), true)
@ -116,8 +117,8 @@ func (h *NpubDir) OnAdd(_ context.Context) {
&ViewDir{ &ViewDir{
root: h.root, root: h.root,
filter: nostr.Filter{ filter: nostr.Filter{
Kinds: []int{1}, Kinds: []nostr.Kind{1},
Authors: []string{h.pointer.PublicKey}, Authors: []nostr.PubKey{h.pointer.PublicKey},
}, },
paginate: true, paginate: true,
relays: relays, relays: relays,
@ -138,8 +139,8 @@ func (h *NpubDir) OnAdd(_ context.Context) {
&ViewDir{ &ViewDir{
root: h.root, root: h.root,
filter: nostr.Filter{ filter: nostr.Filter{
Kinds: []int{1111}, Kinds: []nostr.Kind{1111},
Authors: []string{h.pointer.PublicKey}, Authors: []nostr.PubKey{h.pointer.PublicKey},
}, },
paginate: true, paginate: true,
relays: relays, relays: relays,
@ -159,8 +160,8 @@ func (h *NpubDir) OnAdd(_ context.Context) {
&ViewDir{ &ViewDir{
root: h.root, root: h.root,
filter: nostr.Filter{ filter: nostr.Filter{
Kinds: []int{20}, Kinds: []nostr.Kind{20},
Authors: []string{h.pointer.PublicKey}, Authors: []nostr.PubKey{h.pointer.PublicKey},
}, },
paginate: true, paginate: true,
relays: relays, relays: relays,
@ -180,8 +181,8 @@ func (h *NpubDir) OnAdd(_ context.Context) {
&ViewDir{ &ViewDir{
root: h.root, root: h.root,
filter: nostr.Filter{ filter: nostr.Filter{
Kinds: []int{21, 22}, Kinds: []nostr.Kind{21, 22},
Authors: []string{h.pointer.PublicKey}, Authors: []nostr.PubKey{h.pointer.PublicKey},
}, },
paginate: false, paginate: false,
relays: relays, relays: relays,
@ -201,8 +202,8 @@ func (h *NpubDir) OnAdd(_ context.Context) {
&ViewDir{ &ViewDir{
root: h.root, root: h.root,
filter: nostr.Filter{ filter: nostr.Filter{
Kinds: []int{9802}, Kinds: []nostr.Kind{9802},
Authors: []string{h.pointer.PublicKey}, Authors: []nostr.PubKey{h.pointer.PublicKey},
}, },
paginate: false, paginate: false,
relays: relays, relays: relays,
@ -222,8 +223,8 @@ func (h *NpubDir) OnAdd(_ context.Context) {
&ViewDir{ &ViewDir{
root: h.root, root: h.root,
filter: nostr.Filter{ filter: nostr.Filter{
Kinds: []int{30023}, Kinds: []nostr.Kind{30023},
Authors: []string{h.pointer.PublicKey}, Authors: []nostr.PubKey{h.pointer.PublicKey},
}, },
paginate: false, paginate: false,
relays: relays, relays: relays,
@ -244,8 +245,8 @@ func (h *NpubDir) OnAdd(_ context.Context) {
&ViewDir{ &ViewDir{
root: h.root, root: h.root,
filter: nostr.Filter{ filter: nostr.Filter{
Kinds: []int{30818}, Kinds: []nostr.Kind{30818},
Authors: []string{h.pointer.PublicKey}, Authors: []nostr.PubKey{h.pointer.PublicKey},
}, },
paginate: false, paginate: false,
relays: relays, relays: relays,

View File

@ -6,12 +6,12 @@ import (
"syscall" "syscall"
"time" "time"
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/nip05"
"fiatjaf.com/nostr/nip19"
"fiatjaf.com/nostr/sdk"
"github.com/hanwen/go-fuse/v2/fs" "github.com/hanwen/go-fuse/v2/fs"
"github.com/hanwen/go-fuse/v2/fuse" "github.com/hanwen/go-fuse/v2/fuse"
"github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nip05"
"github.com/nbd-wtf/go-nostr/nip19"
"github.com/nbd-wtf/go-nostr/sdk"
) )
type Options struct { type Options struct {
@ -25,7 +25,7 @@ type NostrRoot struct {
ctx context.Context ctx context.Context
wd string wd string
sys *sdk.System sys *sdk.System
rootPubKey string rootPubKey nostr.PubKey
signer nostr.Signer signer nostr.Signer
opts Options opts Options
@ -54,7 +54,7 @@ func NewNostrRoot(ctx context.Context, sys *sdk.System, user nostr.User, mountpo
} }
func (r *NostrRoot) OnAdd(_ context.Context) { func (r *NostrRoot) OnAdd(_ context.Context) {
if r.rootPubKey == "" { if r.rootPubKey == nostr.ZeroPK {
return return
} }
@ -65,16 +65,15 @@ func (r *NostrRoot) OnAdd(_ context.Context) {
fl := r.sys.FetchFollowList(r.ctx, r.rootPubKey) fl := r.sys.FetchFollowList(r.ctx, r.rootPubKey)
for _, f := range fl.Items { for _, f := range fl.Items {
pointer := nostr.ProfilePointer{PublicKey: f.Pubkey, Relays: []string{f.Relay}} pointer := nostr.ProfilePointer{PublicKey: f.Pubkey, Relays: []string{f.Relay}}
npub, _ := nip19.EncodePublicKey(f.Pubkey)
r.AddChild( r.AddChild(
npub, nip19.EncodeNpub(f.Pubkey),
r.CreateNpubDir(r, pointer, nil), r.CreateNpubDir(r, pointer, nil),
true, true,
) )
} }
// add ourselves // add ourselves
npub, _ := nip19.EncodePublicKey(r.rootPubKey) npub := nip19.EncodeNpub(r.rootPubKey)
if r.GetChild(npub) == nil { if r.GetChild(npub) == nil {
pointer := nostr.ProfilePointer{PublicKey: r.rootPubKey} pointer := nostr.ProfilePointer{PublicKey: r.rootPubKey}

View File

@ -8,11 +8,12 @@ import (
"syscall" "syscall"
"fiatjaf.com/lib/debouncer" "fiatjaf.com/lib/debouncer"
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/nip27"
"fiatjaf.com/nostr/nip73"
"github.com/fatih/color" "github.com/fatih/color"
"github.com/hanwen/go-fuse/v2/fs" "github.com/hanwen/go-fuse/v2/fs"
"github.com/hanwen/go-fuse/v2/fuse" "github.com/hanwen/go-fuse/v2/fuse"
"github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nip27"
) )
type ViewDir struct { type ViewDir struct {
@ -141,13 +142,17 @@ func (n *ViewDir) publishNote() {
} }
// our write relays // our write relays
relays := n.root.sys.FetchWriteRelays(n.root.ctx, n.root.rootPubKey, 8) relays := n.root.sys.FetchWriteRelays(n.root.ctx, n.root.rootPubKey)
if len(relays) == 0 { if len(relays) == 0 {
relays = n.root.sys.FetchOutboxRelays(n.root.ctx, n.root.rootPubKey, 6) relays = n.root.sys.FetchOutboxRelays(n.root.ctx, n.root.rootPubKey, 6)
} }
// add "p" tags from people mentioned and "q" tags from events mentioned // add "p" tags from people mentioned and "q" tags from events mentioned
for ref := range nip27.ParseReferences(evt) { for ref := range nip27.Parse(evt.Content) {
if _, isExternal := ref.Pointer.(nip73.ExternalPointer); isExternal {
continue
}
tag := ref.Pointer.AsTag() tag := ref.Pointer.AsTag()
key := tag[0] key := tag[0]
val := tag[1] val := tag[1]
@ -159,7 +164,12 @@ func (n *ViewDir) publishNote() {
// add their "read" relays // add their "read" relays
if key == "p" { if key == "p" {
for _, r := range n.root.sys.FetchInboxRelays(n.root.ctx, val, 4) { pk, err := nostr.PubKeyFromHex(val)
if err != nil {
continue
}
for _, r := range n.root.sys.FetchInboxRelays(n.root.ctx, pk, 4) {
if !slices.Contains(relays, r) { if !slices.Contains(relays, r) {
relays = append(relays, r) relays = append(relays, r)
} }
@ -196,8 +206,8 @@ func (n *ViewDir) publishNote() {
if success { if success {
n.RmChild("new") n.RmChild("new")
n.AddChild(evt.ID, n.root.CreateEventDir(n, &evt), true) n.AddChild(evt.ID.Hex(), n.root.CreateEventDir(n, &evt), true)
log("event published as %s and updated locally.\n", color.BlueString(evt.ID)) log("event published as %s and updated locally.\n", color.BlueString(evt.ID.Hex()))
} }
} }
@ -241,23 +251,24 @@ func (n *ViewDir) Opendir(ctx context.Context) syscall.Errno {
} }
if n.replaceable { if n.replaceable {
for rkey, evt := range n.root.sys.Pool.FetchManyReplaceable(n.root.ctx, n.relays, n.filter, for rkey, evt := range n.root.sys.Pool.FetchManyReplaceable(n.root.ctx, n.relays, n.filter, nostr.SubscriptionOptions{
nostr.WithLabel("nakfs"), Label: "nakfs",
).Range { }).Range {
name := rkey.D name := rkey.D
if name == "" { if name == "" {
name = "_" name = "_"
} }
if n.GetChild(name) == nil { if n.GetChild(name) == nil {
n.AddChild(name, n.root.CreateEntityDir(n, evt), true) n.AddChild(name, n.root.CreateEntityDir(n, &evt), true)
} }
} }
} else { } else {
for ie := range n.root.sys.Pool.FetchMany(n.root.ctx, n.relays, n.filter, for ie := range n.root.sys.Pool.FetchMany(n.root.ctx, n.relays, n.filter,
nostr.WithLabel("nakfs"), nostr.SubscriptionOptions{
) { Label: "nakfs",
if n.GetChild(ie.Event.ID) == nil { }) {
n.AddChild(ie.Event.ID, n.root.CreateEventDir(n, ie.Event), true) if n.GetChild(ie.Event.ID.Hex()) == nil {
n.AddChild(ie.Event.ID.Hex(), n.root.CreateEventDir(n, &ie.Event), true)
} }
} }
} }

View File

@ -6,8 +6,8 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"fiatjaf.com/nostr"
"github.com/urfave/cli/v3" "github.com/urfave/cli/v3"
"github.com/nbd-wtf/go-nostr"
) )
var outbox = &cli.Command{ var outbox = &cli.Command{
@ -52,12 +52,12 @@ var outbox = &cli.Command{
return fmt.Errorf("expected exactly one argument (pubkey)") return fmt.Errorf("expected exactly one argument (pubkey)")
} }
pubkey := c.Args().First() pk, err := nostr.PubKeyFromHex(c.Args().First())
if !nostr.IsValidPublicKey(pubkey) { if err != nil {
return fmt.Errorf("invalid public key: %s", pubkey) return fmt.Errorf("invalid public key '%s': %w", c.Args().First(), err)
} }
for _, relay := range sys.FetchOutboxRelays(ctx, pubkey, 6) { for _, relay := range sys.FetchOutboxRelays(ctx, pk, 6) {
stdout(relay) stdout(relay)
} }

View File

@ -10,9 +10,9 @@ import (
"io" "io"
"net/http" "net/http"
"github.com/nbd-wtf/go-nostr" "fiatjaf.com/nostr"
"github.com/nbd-wtf/go-nostr/nip11" "fiatjaf.com/nostr/nip11"
"github.com/nbd-wtf/go-nostr/nip86" "fiatjaf.com/nostr/nip86"
"github.com/urfave/cli/v3" "github.com/urfave/cli/v3"
) )

33
req.go
View File

@ -6,10 +6,11 @@ import (
"os" "os"
"strings" "strings"
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/nip42"
"fiatjaf.com/nostr/nip77"
"github.com/fatih/color" "github.com/fatih/color"
"github.com/mailru/easyjson" "github.com/mailru/easyjson"
"github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nip77"
"github.com/urfave/cli/v3" "github.com/urfave/cli/v3"
) )
@ -88,16 +89,20 @@ example:
c, c,
relayUrls, relayUrls,
forcePreAuthSigner, forcePreAuthSigner,
nostr.WithAuthHandler(func(ctx context.Context, authEvent nostr.RelayEvent) error { nostr.PoolOptions{
AuthHandler: func(ctx context.Context, authEvent *nostr.Event) error {
return authSigner(ctx, c, func(s string, args ...any) { return authSigner(ctx, c, func(s string, args ...any) {
if strings.HasPrefix(s, "authenticating as") { if strings.HasPrefix(s, "authenticating as") {
cleanUrl, _ := strings.CutPrefix(authEvent.Relay.URL, "wss://") cleanUrl, _ := strings.CutPrefix(
nip42.GetRelayURLFromAuthEvent(*authEvent),
"wss://",
)
s = "authenticating to " + color.CyanString(cleanUrl) + " as" + s[len("authenticating as"):] s = "authenticating to " + color.CyanString(cleanUrl) + " as" + s[len("authenticating as"):]
} }
log(s+"\n", args...) log(s+"\n", args...)
}, authEvent) }, authEvent)
}), },
) })
// stop here already if all connections failed // stop here already if all connections failed
if len(relays) == 0 { if len(relays) == 0 {
@ -132,7 +137,7 @@ example:
if len(relayUrls) > 0 { if len(relayUrls) > 0 {
if c.Bool("ids-only") { if c.Bool("ids-only") {
seen := make(map[string]struct{}, max(500, filter.Limit)) seen := make(map[nostr.ID]struct{}, max(500, filter.Limit))
for _, url := range relayUrls { for _, url := range relayUrls {
ch, err := nip77.FetchIDsOnly(ctx, url, filter) ch, err := nip77.FetchIDsOnly(ctx, url, filter)
if err != nil { if err != nil {
@ -155,7 +160,7 @@ example:
fn = sys.Pool.SubscribeMany fn = sys.Pool.SubscribeMany
} }
for ie := range fn(ctx, relayUrls, filter) { for ie := range fn(ctx, relayUrls, filter, nostr.SubscriptionOptions{}) {
stdout(ie.Event) stdout(ie.Event)
} }
} }
@ -165,7 +170,7 @@ example:
if c.Bool("bare") { if c.Bool("bare") {
result = filter.String() result = filter.String()
} else { } else {
j, _ := json.Marshal(nostr.ReqEnvelope{SubscriptionID: "nak", Filters: nostr.Filters{filter}}) j, _ := json.Marshal(nostr.ReqEnvelope{SubscriptionID: "nak", Filter: filter})
result = string(j) result = string(j)
} }
@ -179,13 +184,13 @@ example:
} }
var reqFilterFlags = []cli.Flag{ var reqFilterFlags = []cli.Flag{
&cli.StringSliceFlag{ &PubKeySliceFlag{
Name: "author", Name: "author",
Aliases: []string{"a"}, Aliases: []string{"a"},
Usage: "only accept events from these authors (pubkey as hex)", Usage: "only accept events from these authors (pubkey as hex)",
Category: CATEGORY_FILTER_ATTRIBUTES, Category: CATEGORY_FILTER_ATTRIBUTES,
}, },
&cli.StringSliceFlag{ &IDSliceFlag{
Name: "id", Name: "id",
Aliases: []string{"i"}, Aliases: []string{"i"},
Usage: "only accept events with these ids (hex)", Usage: "only accept events with these ids (hex)",
@ -244,14 +249,14 @@ var reqFilterFlags = []cli.Flag{
} }
func applyFlagsToFilter(c *cli.Command, filter *nostr.Filter) error { func applyFlagsToFilter(c *cli.Command, filter *nostr.Filter) error {
if authors := c.StringSlice("author"); len(authors) > 0 { if authors := getPubKeySlice(c, "author"); len(authors) > 0 {
filter.Authors = append(filter.Authors, authors...) filter.Authors = append(filter.Authors, authors...)
} }
if ids := c.StringSlice("id"); len(ids) > 0 { if ids := getIDSlice(c, "id"); len(ids) > 0 {
filter.IDs = append(filter.IDs, ids...) filter.IDs = append(filter.IDs, ids...)
} }
for _, kind64 := range c.IntSlice("kind") { for _, kind64 := range c.IntSlice("kind") {
filter.Kinds = append(filter.Kinds, int(kind64)) filter.Kinds = append(filter.Kinds, nostr.Kind(kind64))
} }
if search := c.String("search"); search != "" { if search := c.String("search"); search != "" {
filter.Search = search filter.Search = search

View File

@ -8,11 +8,11 @@ import (
"os" "os"
"time" "time"
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/eventstore/slicestore"
"fiatjaf.com/nostr/khatru"
"github.com/bep/debounce" "github.com/bep/debounce"
"github.com/fatih/color" "github.com/fatih/color"
"github.com/fiatjaf/eventstore/slicestore"
"github.com/fiatjaf/khatru"
"github.com/nbd-wtf/go-nostr"
"github.com/urfave/cli/v3" "github.com/urfave/cli/v3"
) )
@ -38,7 +38,7 @@ var serve = &cli.Command{
}, },
}, },
Action: func(ctx context.Context, c *cli.Command) error { Action: func(ctx context.Context, c *cli.Command) error {
db := slicestore.SliceStore{MaxLimit: math.MaxInt} db := &slicestore.SliceStore{MaxLimit: math.MaxInt}
var scanner *bufio.Scanner var scanner *bufio.Scanner
if path := c.String("events"); path != "" { if path := c.String("events"); path != "" {
@ -59,7 +59,7 @@ var serve = &cli.Command{
if err := json.Unmarshal(scanner.Bytes(), &evt); err != nil { if err := json.Unmarshal(scanner.Bytes(), &evt); err != nil {
return fmt.Errorf("invalid event received at line %d: %s (`%s`)", i, err, scanner.Text()) return fmt.Errorf("invalid event received at line %d: %s (`%s`)", i, err, scanner.Text())
} }
db.SaveEvent(ctx, &evt) db.SaveEvent(evt)
i++ i++
} }
} }
@ -71,10 +71,7 @@ var serve = &cli.Command{
rl.Info.Software = "https://github.com/fiatjaf/nak" rl.Info.Software = "https://github.com/fiatjaf/nak"
rl.Info.Version = version rl.Info.Version = version
rl.QueryEvents = append(rl.QueryEvents, db.QueryEvents) rl.UseEventstore(db)
rl.CountEvents = append(rl.CountEvents, db.CountEvents)
rl.DeleteEvent = append(rl.DeleteEvent, db.DeleteEvent)
rl.StoreEvent = append(rl.StoreEvent, db.SaveEvent)
started := make(chan bool) started := make(chan bool)
exited := make(chan error) exited := make(chan error)
@ -90,28 +87,29 @@ var serve = &cli.Command{
var printStatus func() var printStatus func()
// relay logging // relay logging
rl.RejectFilter = append(rl.RejectFilter, func(ctx context.Context, filter nostr.Filter) (reject bool, msg string) { rl.OnRequest = func(ctx context.Context, filter nostr.Filter) (reject bool, msg string) {
log(" got %s %v\n", color.HiYellowString("request"), colors.italic(filter)) log(" got %s %v\n", color.HiYellowString("request"), colors.italic(filter))
printStatus() printStatus()
return false, "" return false, ""
}) }
rl.RejectCountFilter = append(rl.RejectCountFilter, func(ctx context.Context, filter nostr.Filter) (reject bool, msg string) {
rl.OnCount = func(ctx context.Context, filter nostr.Filter) (reject bool, msg string) {
log(" got %s %v\n", color.HiCyanString("count request"), colors.italic(filter)) log(" got %s %v\n", color.HiCyanString("count request"), colors.italic(filter))
printStatus() printStatus()
return false, "" return false, ""
}) }
rl.RejectEvent = append(rl.RejectEvent, func(ctx context.Context, event *nostr.Event) (reject bool, msg string) {
rl.OnEvent = func(ctx context.Context, event nostr.Event) (reject bool, msg string) {
log(" got %s %v\n", color.BlueString("event"), colors.italic(event)) log(" got %s %v\n", color.BlueString("event"), colors.italic(event))
printStatus() printStatus()
return false, "" return false, ""
}) }
d := debounce.New(time.Second * 2) d := debounce.New(time.Second * 2)
printStatus = func() { printStatus = func() {
d(func() { d(func() {
totalEvents := 0 totalEvents := 0
ch, _ := db.QueryEvents(ctx, nostr.Filter{}) for range db.QueryEvents(nostr.Filter{}) {
for range ch {
totalEvents++ totalEvents++
} }
subs := rl.GetListeningFilters() subs := rl.GetListeningFilters()

View File

@ -3,8 +3,8 @@ package main
import ( import (
"context" "context"
"fiatjaf.com/nostr"
"github.com/urfave/cli/v3" "github.com/urfave/cli/v3"
"github.com/nbd-wtf/go-nostr"
) )
var verify = &cli.Command{ var verify = &cli.Command{
@ -30,8 +30,8 @@ it outputs nothing if the verification is successful.`,
continue continue
} }
if ok, err := evt.CheckSignature(); !ok { if !evt.VerifySignature() {
ctx = lineProcessingError(ctx, "invalid signature: %v", err) ctx = lineProcessingError(ctx, "invalid signature")
continue continue
} }
} }

View File

@ -6,10 +6,10 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/nbd-wtf/go-nostr" "fiatjaf.com/nostr"
"github.com/nbd-wtf/go-nostr/nip60" "fiatjaf.com/nostr/nip60"
"github.com/nbd-wtf/go-nostr/nip61" "fiatjaf.com/nostr/nip61"
"github.com/nbd-wtf/go-nostr/sdk" "fiatjaf.com/nostr/sdk"
"github.com/urfave/cli/v3" "github.com/urfave/cli/v3"
) )
@ -30,7 +30,7 @@ func prepareWallet(ctx context.Context, c *cli.Command) (*nip60.Wallet, func(),
return nil, nil, fmt.Errorf("error loading walle") return nil, nil, fmt.Errorf("error loading walle")
} }
w.Processed = func(evt *nostr.Event, err error) { w.Processed = func(evt nostr.Event, err error) {
if err == nil { if err == nil {
logverbose("processed event %s\n", evt) logverbose("processed event %s\n", evt)
} else { } else {
@ -321,11 +321,16 @@ var wallet = &cli.Command{
return err return err
} }
amount := c.Uint("amount") amount, err := strconv.ParseInt(c.Args().First(), 10, 64)
if err != nil {
return fmt.Errorf("invalid amount '%s': %w", c.Args().First(), err)
}
target := c.String("target") target := c.String("target")
var pm sdk.ProfileMetadata
var evt *nostr.Event var evt *nostr.Event
var eventId string var eventId nostr.ID
if strings.HasPrefix(target, "nevent1") { if strings.HasPrefix(target, "nevent1") {
evt, _, err = sys.FetchSpecificEventFromInput(ctx, target, sdk.FetchSpecificEventParameters{ evt, _, err = sys.FetchSpecificEventFromInput(ctx, target, sdk.FetchSpecificEventParameters{
@ -335,13 +340,13 @@ var wallet = &cli.Command{
return err return err
} }
eventId = evt.ID eventId = evt.ID
target = evt.PubKey pm = sys.FetchProfileMetadata(ctx, evt.PubKey)
} } else {
pm, err = sys.FetchProfileFromInput(ctx, target)
pm, err := sys.FetchProfileFromInput(ctx, target)
if err != nil { if err != nil {
return err return err
} }
}
log("sending %d sat to '%s' (%s)", amount, pm.ShortName(), pm.Npub()) log("sending %d sat to '%s' (%s)", amount, pm.ShortName(), pm.Npub())
@ -361,7 +366,7 @@ var wallet = &cli.Command{
sys.FetchInboxRelays, sys.FetchInboxRelays,
sys.FetchOutboxRelays(ctx, pm.PubKey, 3), sys.FetchOutboxRelays(ctx, pm.PubKey, 3),
eventId, eventId,
amount, uint64(amount),
c.String("message"), c.String("message"),
) )
if err != nil { if err != nil {
@ -426,14 +431,14 @@ var wallet = &cli.Command{
kr, _, _ := gatherKeyerFromArguments(ctx, c) kr, _, _ := gatherKeyerFromArguments(ctx, c)
pk, _ := kr.GetPublicKey(ctx) pk, _ := kr.GetPublicKey(ctx)
relays := sys.FetchWriteRelays(ctx, pk, 6) relays := sys.FetchWriteRelays(ctx, pk)
info := nip61.Info{} info := nip61.Info{}
ie := sys.Pool.QuerySingle(ctx, relays, nostr.Filter{ ie := sys.Pool.QuerySingle(ctx, relays, nostr.Filter{
Kinds: []int{10019}, Kinds: []nostr.Kind{10019},
Authors: []string{pk}, Authors: []nostr.PubKey{pk},
Limit: 1, Limit: 1,
}) }, nostr.SubscriptionOptions{})
if ie != nil { if ie != nil {
info.ParseEvent(ie.Event) info.ParseEvent(ie.Event)
} }