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"
"os"
"github.com/nbd-wtf/go-nostr/keyer"
"github.com/nbd-wtf/go-nostr/nipb0/blossom"
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/keyer"
"fiatjaf.com/nostr/nipb0/blossom"
"github.com/urfave/cli/v3"
)
@ -35,7 +36,11 @@ var blossomCmd = &cli.Command{
var client *blossom.Client
pubkey := c.Args().First()
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 {
var err error
client, err = getBlossomClient(ctx, c)

View File

@ -10,10 +10,10 @@ import (
"sync"
"time"
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/nip19"
"fiatjaf.com/nostr/nip46"
"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"
)
@ -38,7 +38,7 @@ var bunker = &cli.Command{
Aliases: []string{"s"},
Usage: "secrets for which we will always respond",
},
&cli.StringSliceFlag{
&PubKeySliceFlag{
Name: "authorized-keys",
Aliases: []string{"k"},
Usage: "pubkeys for which we will always respond",
@ -49,7 +49,7 @@ var bunker = &cli.Command{
qs := url.Values{}
relayURLs := make([]string, 0, c.Args().Len())
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 {
log("failed to connect to any of the given relays.\n")
os.Exit(3)
@ -70,7 +70,7 @@ var bunker = &cli.Command{
}
// other arguments
authorizedKeys := c.StringSlice("authorized-keys")
authorizedKeys := getPubKeySlice(c, "authorized-keys")
authorizedSecrets := c.StringSlice("authorized-secrets")
// 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)
// static information
pubkey, err := nostr.GetPublicKey(sec)
if err != nil {
return err
}
npub, _ := nip19.EncodePublicKey(pubkey)
pubkey := sec.Public()
npub := nip19.EncodeNpub(pubkey)
// this function will be called every now and then
printBunkerInfo := func() {
@ -91,7 +88,10 @@ var bunker = &cli.Command{
authorizedKeysStr := ""
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 := ""
@ -101,7 +101,7 @@ var bunker = &cli.Command{
preauthorizedFlags := ""
for _, k := range authorizedKeys {
preauthorizedFlags += " -k " + k
preauthorizedFlags += " -k " + k.Hex()
}
for _, s := range authorizedSecrets {
preauthorizedFlags += " -s " + s
@ -142,10 +142,12 @@ var bunker = &cli.Command{
// subscribe to relays
now := nostr.Now()
events := sys.Pool.SubscribeMany(ctx, relayURLs, nostr.Filter{
Kinds: []int{nostr.KindNostrConnect},
Tags: nostr.TagMap{"p": []string{pubkey}},
Kinds: []nostr.Kind{nostr.KindNostrConnect},
Tags: nostr.TagMap{"p": []string{pubkey.Hex()}},
Since: &now,
LimitZero: true,
}, nostr.SubscriptionOptions{
Label: "nak-bunker",
})
signer := nip46.NewStaticKeySigner(sec)
@ -158,7 +160,7 @@ var bunker = &cli.Command{
cancelPreviousBunkerInfoPrint = cancel
// 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 {
// store this key
authorizedKeys = append(authorizedKeys, from)
@ -236,11 +238,13 @@ var bunker = &cli.Command{
}
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 nil
// TODO
return fmt.Errorf("this is not implemented yet")
},
},
},

View File

@ -6,9 +6,9 @@ import (
"os"
"strings"
"github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nip45"
"github.com/nbd-wtf/go-nostr/nip45/hyperloglog"
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/nip45"
"fiatjaf.com/nostr/nip45/hyperloglog"
"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').`,
DisableSliceFlagSeparator: true,
Flags: []cli.Flag{
&cli.StringSliceFlag{
&PubKeySliceFlag{
Name: "author",
Aliases: []string{"a"},
Usage: "only accept events from these authors (pubkey as hex)",
@ -70,7 +70,7 @@ var count = &cli.Command{
biggerUrlSize := 0
relayUrls := c.Args().Slice()
if len(relayUrls) > 0 {
relays := connectToAllRelays(ctx, c, relayUrls, nil)
relays := connectToAllRelays(ctx, c, relayUrls, nil, nostr.PoolOptions{})
if len(relays) == 0 {
log("failed to connect to any of the given relays.\n")
os.Exit(3)
@ -92,16 +92,13 @@ var count = &cli.Command{
filter := nostr.Filter{}
if authors := c.StringSlice("author"); len(authors) > 0 {
if authors := getPubKeySlice(c, "author"); len(authors) > 0 {
filter.Authors = authors
}
if ids := c.StringSlice("id"); len(ids) > 0 {
filter.IDs = ids
}
if kinds64 := c.IntSlice("kind"); len(kinds64) > 0 {
kinds := make([]int, len(kinds64))
kinds := make([]nostr.Kind, len(kinds64))
for i, v := range kinds64 {
kinds[i] = int(v)
kinds[i] = nostr.Kind(v)
}
filter.Kinds = kinds
}
@ -151,7 +148,7 @@ var count = &cli.Command{
}
for _, relayUrl := range relayUrls {
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)
if err != nil {

View File

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

View File

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

View File

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

View File

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

View File

@ -8,11 +8,11 @@ import (
"strings"
"time"
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/nip13"
"fiatjaf.com/nostr/nip19"
"github.com/fatih/color"
"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"
)
@ -141,9 +141,11 @@ example:
if relayUrls := c.Args().Slice(); len(relayUrls) > 0 {
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)
}),
},
},
)
if len(relays) == 0 {
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") {
evt.Kind = int(kind)
evt.Kind = nostr.Kind(kind)
mustRehashAndResign = true
} else if !kindWasSupplied {
evt.Kind = 1
@ -274,7 +276,7 @@ example:
mustRehashAndResign = true
}
if evt.Sig == "" || mustRehashAndResign {
if evt.Sig == [64]byte{} || mustRehashAndResign {
if numSigners := c.Uint("musig"); numSigners > 1 {
// must do musig
pubkeys := c.StringSlice("musig-pubkey")
@ -364,7 +366,7 @@ example:
low := unwrapAll(res.Error)
// 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
msg = clampMessage(msg, 20+len(res.RelayURL))
@ -393,11 +395,9 @@ example:
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
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:]))
if err := relay.Auth(ctx, func(authEvent *nostr.Event) error {
return kr.SignEvent(ctx, authEvent)
}); err == nil {
if err := relay.Auth(ctx, kr.SignEvent); err == nil {
// try to publish again, but this time don't try to auth again
doAuth = false
goto publish
@ -410,8 +410,7 @@ example:
}
if len(successRelays) > 0 && c.Bool("nevent") {
nevent, _ := nip19.EncodeEvent(evt.ID, successRelays, evt.PubKey)
log(nevent + "\n")
log(nip19.EncodeNevent(evt.ID, successRelays, evt.PubKey) + "\n")
}
}

View File

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

157
flags.go
View File

@ -6,14 +6,18 @@ import (
"strconv"
"time"
"github.com/urfave/cli/v3"
"fiatjaf.com/nostr"
"github.com/markusmobius/go-dateparser"
"github.com/nbd-wtf/go-nostr"
"github.com/urfave/cli/v3"
)
//
//
//
type NaturalTimeFlag = cli.FlagBase[nostr.Timestamp, struct{}, naturalTimeValue]
// wrap to satisfy golang's flag interface.
// wrap to satisfy flag interface.
type naturalTimeValue struct {
timestamp *nostr.Timestamp
hasBeenSet bool
@ -39,11 +43,6 @@ func (t naturalTimeValue) ToString(b nostr.Timestamp) string {
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
// Parses the string value to timestamp
@ -93,3 +92,145 @@ func (t *naturalTimeValue) Get() any {
func getNaturalDate(cmd *cli.Command, name string) 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"
"time"
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/keyer"
"github.com/fatih/color"
"github.com/fiatjaf/nak/nostrfs"
"github.com/hanwen/go-fuse/v2/fs"
"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"
)
@ -25,15 +25,9 @@ var fsCmd = &cli.Command{
Description: `(experimental)`,
ArgsUsage: "<mountpoint>",
Flags: append(defaultKeyFlags,
&cli.StringFlag{
&PubKeyFlag{
Name: "pubkey",
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{
Name: "auto-publish-notes",
@ -58,7 +52,7 @@ var fsCmd = &cli.Command{
if signer, _, err := gatherKeyerFromArguments(ctx, c); err == nil {
kr = signer
} else {
kr = keyer.NewReadOnlyUser(c.String("pubkey"))
kr = keyer.NewReadOnlyUser(getPubKey(c, "pubkey"))
}
apnt := c.Duration("auto-publish-notes")

8
go.mod
View File

@ -4,26 +4,25 @@ go 1.24.1
require (
fiatjaf.com/lib v0.3.1
fiatjaf.com/nostr v0.0.1
github.com/bep/debounce v1.2.1
github.com/btcsuite/btcd/btcec/v2 v2.3.4
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.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/json-iterator/go v1.1.12
github.com/liamg/magic v0.0.1
github.com/mailru/easyjson v0.9.0
github.com/mark3labs/mcp-go v0.8.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
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394
golang.org/x/term v0.30.0
)
require (
github.com/FastFilter/xorfilter v0.2.1 // indirect
github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3 // indirect
github.com/andybalholm/brotli v1.1.1 // 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/crypto v0.36.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/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/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/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/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
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.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
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/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
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/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
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.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
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/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
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/onsi/ginkgo v1.6.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/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.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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

View File

@ -17,11 +17,12 @@ import (
"sync"
"time"
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/nip19"
"fiatjaf.com/nostr/nip42"
"fiatjaf.com/nostr/sdk"
"github.com/fatih/color"
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"
"golang.org/x/term"
)
@ -155,8 +156,8 @@ func connectToAllRelays(
ctx context.Context,
c *cli.Command,
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
opts ...nostr.PoolOption,
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.PoolOptions,
) []*nostr.Relay {
// first pass to check if these are valid relay URLs
for _, url := range relayUrls {
@ -166,15 +167,12 @@ func connectToAllRelays(
}
}
sys.Pool = nostr.NewSimplePool(context.Background(),
append(opts,
nostr.WithEventMiddleware(sys.TrackEventHints),
nostr.WithPenaltyBox(),
nostr.WithRelayOptions(
nostr.WithRequestHeader(http.Header{textproto.CanonicalMIMEHeaderKey("user-agent"): {"nak/s"}}),
),
)...,
)
opts.EventMiddleware = sys.TrackEventHints
opts.PenaltyBox = true
opts.RelayOptions = nostr.RelayOptions{
RequestHeader: http.Header{textproto.CanonicalMIMEHeaderKey("user-agent"): {"nak/s"}},
}
sys.Pool = nostr.NewPool(opts)
relays := make([]*nostr.Relay, 0, len(relayUrls))
@ -236,7 +234,7 @@ func connectToSingleRelay(
ctx context.Context,
c *cli.Command,
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),
logthis func(s string, args ...any),
) *nostr.Relay {
@ -249,12 +247,12 @@ func connectToSingleRelay(
time.Sleep(time.Millisecond * 200)
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")
if challengeTag[1] == "" {
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 {
// auth succeeded
goto preauthSuccess
@ -324,10 +322,10 @@ func supportsDynamicMultilineMagic() bool {
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() {
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)
}
}()
@ -341,10 +339,10 @@ func authSigner(ctx context.Context, c *cli.Command, log func(s string, args ...
}
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:]))
return kr.SignEvent(ctx, authEvent.Event)
return kr.SignEvent(ctx, authEvent)
}
func lineProcessingError(ctx context.Context, msg string, args ...any) context.Context {
@ -368,10 +366,6 @@ func randString(n int) string {
return string(b)
}
func leftPadKey(k string) string {
return strings.Repeat("0", 64-len(k)) + k
}
func unwrapAll(err error) error {
low := err
for n := low; n != nil; n = errors.Unwrap(low) {

View File

@ -2,19 +2,18 @@ package main
import (
"context"
"encoding/hex"
"fmt"
"os"
"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/fatih/color"
"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{
@ -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)
if err != nil {
return nil, "", err
return nil, nostr.SecretKey{}, err
}
var kr nostr.Keyer
@ -55,23 +54,27 @@ func gatherKeyerFromArguments(ctx context.Context, c *cli.Command) (nostr.Keyer,
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
sec := c.String("sec")
if strings.HasPrefix(sec, "bunker://") {
// it's a bunker
bunkerURL := sec
clientKey := c.String("connect-as")
if clientKey != "" {
clientKey = strings.Repeat("0", 64-len(clientKey)) + clientKey
clientKeyHex := c.String("connect-as")
var clientKey nostr.SecretKey
if clientKeyHex != "" {
clientKey, err = nostr.SecretKeyFromHex(sec)
} else {
clientKey = nostr.GeneratePrivateKey()
clientKey = nostr.Generate()
}
bunker, err := nip46.ConnectBunker(ctx, clientKey, bunkerURL, nil, func(s string) {
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
@ -85,35 +88,35 @@ func gatherSecretKeyOrBunkerFromArguments(ctx context.Context, c *cli.Command) (
if c.Bool("prompt-sec") {
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)
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") {
sec, err = promptDecrypt(sec)
sk, err := promptDecrypt(sec)
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 {
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)
return sk, nil, nil
}
if ok := nostr.IsValid32ByteHex(sec); !ok {
return "", nil, fmt.Errorf("invalid secret key")
if prefix, ski, err := nip19.Decode(sec); err == nil && prefix == "nsec" {
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++ {
var attemptStr string
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)
if err != nil {
return "", err
return nostr.SecretKey{}, err
}
sec, err := nip49.Decrypt(ncryptsec, password)
if err != nil {
@ -129,7 +132,7 @@ func promptDecrypt(ncryptsec string) (string, error) {
}
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) {

33
key.go
View File

@ -6,13 +6,13 @@ import (
"fmt"
"strings"
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/nip19"
"fiatjaf.com/nostr/nip49"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"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{
@ -35,8 +35,8 @@ var generate = &cli.Command{
Description: ``,
DisableSliceFlagSeparator: true,
Action: func(ctx context.Context, c *cli.Command) error {
sec := nostr.GeneratePrivateKey()
stdout(sec)
sec := nostr.Generate()
stdout(sec.Hex())
return nil
},
}
@ -54,9 +54,8 @@ var public = &cli.Command{
},
},
Action: func(ctx context.Context, c *cli.Command) error {
for sec := range getSecretKeysFromStdinLinesOrSlice(ctx, c, c.Args().Slice()) {
b, _ := hex.DecodeString(sec)
_, pk := btcec.PrivKeyFromBytes(b)
for sk := range getSecretKeysFromStdinLinesOrSlice(ctx, c, c.Args().Slice()) {
_, pk := btcec.PrivKeyFromBytes(sk[:])
if c.Bool("with-parity") {
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 {
ch := make(chan string)
func getSecretKeysFromStdinLinesOrSlice(ctx context.Context, _ *cli.Command, keys []string) chan nostr.SecretKey {
ch := make(chan nostr.SecretKey)
go func() {
for sec := range getStdinLinesOrArgumentsFromSlice(keys) {
if sec == "" {
continue
}
var sk nostr.SecretKey
if strings.HasPrefix(sec, "nsec1") {
_, data, err := nip19.Decode(sec)
if err != nil {
ctx = lineProcessingError(ctx, "invalid nsec code: %s", err)
continue
}
sec = data.(string)
sk = data.(nostr.SecretKey)
}
sec = leftPadKey(sec)
if !nostr.IsValid32ByteHex(sec) {
ctx = lineProcessingError(ctx, "invalid hex key")
sk, err := nostr.SecretKeyFromHex(sec)
if err != nil {
ctx = lineProcessingError(ctx, "invalid hex key: %s", err)
continue
}
ch <- sec
ch <- sk
}
close(ch)
}()

20
main.go
View File

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

74
mcp.go
View File

@ -6,11 +6,11 @@ import (
"os"
"strings"
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/nip19"
"fiatjaf.com/nostr/sdk"
"github.com/mark3labs/mcp-go/mcp"
"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"
)
@ -19,13 +19,23 @@ var mcpServer = &cli.Command{
Usage: "pander to the AI gods",
Description: ``,
DisableSliceFlagSeparator: true,
Flags: []cli.Flag{},
Flags: append(
defaultKeyFlags,
),
Action: func(ctx context.Context, c *cli.Command) error {
s := server.NewMCPServer(
"nak",
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",
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()),
@ -36,10 +46,6 @@ var mcpServer = &cli.Command{
mention, _ := optional[string](r, "mention")
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")
if sk == "" {
sk = "0000000000000000000000000000000000000000000000000000000000000001"
@ -54,12 +60,19 @@ var mcpServer = &cli.Command{
}
if mention != "" {
evt.Tags = append(evt.Tags, nostr.Tag{"p", mention})
// their inbox relays
relays = sys.FetchInboxRelays(ctx, mention, 3)
pk, err := nostr.PubKeyFromHex(mention)
if err != nil {
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
relays = append(relays, sys.FetchOutboxRelays(ctx, evt.PubKey, 3)...)
@ -115,7 +128,7 @@ var mcpServer = &cli.Command{
switch prefix {
case "npub":
pm := sys.FetchProfileMetadata(ctx, data.(string))
pm := sys.FetchProfileMetadata(ctx, data.(nostr.PubKey))
return mcp.NewToolResultText(
fmt.Sprintf("this is a Nostr profile named '%s', their public key is '%s'",
pm.ShortName(), pm.PubKey),
@ -149,19 +162,23 @@ var mcpServer = &cli.Command{
mcp.WithString("name", mcp.Description("Name to be searched"), mcp.Required()),
), func(ctx context.Context, r mcp.CallToolRequest) (*mcp.CallToolResult, error) {
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 {
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",
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()),
), 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)
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.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.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) {
relay := required[string](r, "relay")
kind := int(required[float64](r, "kind"))
limit := int(required[float64](r, "limit"))
pubkey, _ := 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
}
pubkey, hasPubKey := optional[string](r, "pubkey")
filter := nostr.Filter{
Limit: limit,
Kinds: []int{kind},
}
if pubkey != "" {
filter.Authors = []string{pubkey}
Kinds: []nostr.Kind{nostr.Kind(kind)},
}
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{}
for ie := range events {
result.WriteString("author public key: ")
result.WriteString(ie.PubKey)
result.WriteString(ie.PubKey.Hex())
result.WriteString("content: '")
result.WriteString(ie.Content)
result.WriteString("'")

View File

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

View File

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

View File

@ -15,14 +15,15 @@ import (
"unsafe"
"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/hanwen/go-fuse/v2/fs"
"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 {
@ -95,11 +96,10 @@ func (e *EntityDir) Setattr(_ context.Context, _ fs.FileHandle, in *fuse.SetAttr
func (e *EntityDir) OnAdd(_ context.Context) {
log := e.root.ctx.Value("log").(func(msg string, args ...any))
npub, _ := nip19.EncodePublicKey(e.event.PubKey)
e.AddChild("@author", e.NewPersistentInode(
e.root.ctx,
&fs.MemSymlink{
Data: []byte(e.root.wd + "/" + npub),
Data: []byte(e.root.wd + "/" + nip19.EncodeNpub(e.event.PubKey)),
},
fs.StableAttr{Mode: syscall.S_IFLNK},
), true)
@ -180,8 +180,12 @@ func (e *EntityDir) OnAdd(_ context.Context) {
var refsdir *fs.Inode
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++
if refsdir == nil {
refsdir = e.NewPersistentInode(e.root.ctx, &fs.Inode{}, fs.StableAttr{Mode: syscall.S_IFDIR})
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
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()
key := tag[0]
val := tag[1]
@ -339,7 +347,7 @@ func (e *EntityDir) handleWrite() {
}
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 {
relays = e.root.sys.FetchOutboxRelays(e.root.ctx, e.root.rootPubKey, 6)
}

View File

@ -3,6 +3,7 @@ package nostrfs
import (
"bytes"
"context"
"encoding/binary"
"encoding/json"
"fmt"
"io"
@ -11,16 +12,16 @@ import (
"syscall"
"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/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 {
@ -58,14 +59,13 @@ func (r *NostrRoot) CreateEventDir(
h := parent.EmbeddedInode().NewPersistentInode(
r.ctx,
&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(
r.ctx,
&fs.MemSymlink{
Data: []byte(r.wd + "/" + npub),
Data: []byte(r.wd + "/" + nip19.EncodeNpub(event.PubKey)),
},
fs.StableAttr{Mode: syscall.S_IFLNK},
), true)
@ -88,7 +88,7 @@ func (r *NostrRoot) CreateEventDir(
h.AddChild("id", h.NewPersistentInode(
r.ctx,
&fs.MemRegularFile{
Data: []byte(event.ID),
Data: []byte(event.ID.Hex()),
Attr: fuse.Attr{
Mode: 0444,
Ctime: uint64(event.CreatedAt),
@ -115,8 +115,12 @@ func (r *NostrRoot) CreateEventDir(
var refsdir *fs.Inode
i := 0
for ref := range nip27.ParseReferences(*event) {
for ref := range nip27.Parse(event.Content) {
if _, isExternal := ref.Pointer.(nip73.ExternalPointer); isExternal {
continue
}
i++
if refsdir == nil {
refsdir = h.NewPersistentInode(r.ctx, &fs.Inode{}, fs.StableAttr{Mode: syscall.S_IFDIR})
h.AddChild("references", refsdir, true)

View File

@ -1,8 +1,10 @@
package nostrfs
import "strconv"
import (
"fiatjaf.com/nostr"
)
func kindToExtension(kind int) string {
func kindToExtension(kind nostr.Kind) string {
switch kind {
case 30023:
return "md"
@ -12,8 +14,3 @@ func kindToExtension(kind int) string {
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 (
"bytes"
"context"
"encoding/binary"
"encoding/json"
"io"
"net/http"
@ -10,12 +11,12 @@ import (
"syscall"
"time"
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/nip19"
"github.com/fatih/color"
"github.com/hanwen/go-fuse/v2/fs"
"github.com/hanwen/go-fuse/v2/fuse"
"github.com/liamg/magic"
"github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nip19"
)
type NpubDir struct {
@ -36,7 +37,7 @@ func (r *NostrRoot) CreateNpubDir(
return parent.EmbeddedInode().NewPersistentInode(
r.ctx,
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.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{},
), true)
@ -116,8 +117,8 @@ func (h *NpubDir) OnAdd(_ context.Context) {
&ViewDir{
root: h.root,
filter: nostr.Filter{
Kinds: []int{1},
Authors: []string{h.pointer.PublicKey},
Kinds: []nostr.Kind{1},
Authors: []nostr.PubKey{h.pointer.PublicKey},
},
paginate: true,
relays: relays,
@ -138,8 +139,8 @@ func (h *NpubDir) OnAdd(_ context.Context) {
&ViewDir{
root: h.root,
filter: nostr.Filter{
Kinds: []int{1111},
Authors: []string{h.pointer.PublicKey},
Kinds: []nostr.Kind{1111},
Authors: []nostr.PubKey{h.pointer.PublicKey},
},
paginate: true,
relays: relays,
@ -159,8 +160,8 @@ func (h *NpubDir) OnAdd(_ context.Context) {
&ViewDir{
root: h.root,
filter: nostr.Filter{
Kinds: []int{20},
Authors: []string{h.pointer.PublicKey},
Kinds: []nostr.Kind{20},
Authors: []nostr.PubKey{h.pointer.PublicKey},
},
paginate: true,
relays: relays,
@ -180,8 +181,8 @@ func (h *NpubDir) OnAdd(_ context.Context) {
&ViewDir{
root: h.root,
filter: nostr.Filter{
Kinds: []int{21, 22},
Authors: []string{h.pointer.PublicKey},
Kinds: []nostr.Kind{21, 22},
Authors: []nostr.PubKey{h.pointer.PublicKey},
},
paginate: false,
relays: relays,
@ -201,8 +202,8 @@ func (h *NpubDir) OnAdd(_ context.Context) {
&ViewDir{
root: h.root,
filter: nostr.Filter{
Kinds: []int{9802},
Authors: []string{h.pointer.PublicKey},
Kinds: []nostr.Kind{9802},
Authors: []nostr.PubKey{h.pointer.PublicKey},
},
paginate: false,
relays: relays,
@ -222,8 +223,8 @@ func (h *NpubDir) OnAdd(_ context.Context) {
&ViewDir{
root: h.root,
filter: nostr.Filter{
Kinds: []int{30023},
Authors: []string{h.pointer.PublicKey},
Kinds: []nostr.Kind{30023},
Authors: []nostr.PubKey{h.pointer.PublicKey},
},
paginate: false,
relays: relays,
@ -244,8 +245,8 @@ func (h *NpubDir) OnAdd(_ context.Context) {
&ViewDir{
root: h.root,
filter: nostr.Filter{
Kinds: []int{30818},
Authors: []string{h.pointer.PublicKey},
Kinds: []nostr.Kind{30818},
Authors: []nostr.PubKey{h.pointer.PublicKey},
},
paginate: false,
relays: relays,

View File

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

View File

@ -8,11 +8,12 @@ import (
"syscall"
"fiatjaf.com/lib/debouncer"
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/nip27"
"fiatjaf.com/nostr/nip73"
"github.com/fatih/color"
"github.com/hanwen/go-fuse/v2/fs"
"github.com/hanwen/go-fuse/v2/fuse"
"github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nip27"
)
type ViewDir struct {
@ -141,13 +142,17 @@ func (n *ViewDir) publishNote() {
}
// 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 {
relays = n.root.sys.FetchOutboxRelays(n.root.ctx, n.root.rootPubKey, 6)
}
// 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()
key := tag[0]
val := tag[1]
@ -159,7 +164,12 @@ func (n *ViewDir) publishNote() {
// add their "read" relays
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) {
relays = append(relays, r)
}
@ -196,8 +206,8 @@ func (n *ViewDir) publishNote() {
if success {
n.RmChild("new")
n.AddChild(evt.ID, n.root.CreateEventDir(n, &evt), true)
log("event published as %s and updated locally.\n", color.BlueString(evt.ID))
n.AddChild(evt.ID.Hex(), n.root.CreateEventDir(n, &evt), true)
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 {
for rkey, evt := range n.root.sys.Pool.FetchManyReplaceable(n.root.ctx, n.relays, n.filter,
nostr.WithLabel("nakfs"),
).Range {
for rkey, evt := range n.root.sys.Pool.FetchManyReplaceable(n.root.ctx, n.relays, n.filter, nostr.SubscriptionOptions{
Label: "nakfs",
}).Range {
name := rkey.D
if name == "" {
name = "_"
}
if n.GetChild(name) == nil {
n.AddChild(name, n.root.CreateEntityDir(n, evt), true)
n.AddChild(name, n.root.CreateEntityDir(n, &evt), true)
}
}
} else {
for ie := range n.root.sys.Pool.FetchMany(n.root.ctx, n.relays, n.filter,
nostr.WithLabel("nakfs"),
) {
if n.GetChild(ie.Event.ID) == nil {
n.AddChild(ie.Event.ID, n.root.CreateEventDir(n, ie.Event), true)
nostr.SubscriptionOptions{
Label: "nakfs",
}) {
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"
"path/filepath"
"fiatjaf.com/nostr"
"github.com/urfave/cli/v3"
"github.com/nbd-wtf/go-nostr"
)
var outbox = &cli.Command{
@ -52,12 +52,12 @@ var outbox = &cli.Command{
return fmt.Errorf("expected exactly one argument (pubkey)")
}
pubkey := c.Args().First()
if !nostr.IsValidPublicKey(pubkey) {
return fmt.Errorf("invalid public key: %s", pubkey)
pk, err := nostr.PubKeyFromHex(c.Args().First())
if err != nil {
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)
}

View File

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

33
req.go
View File

@ -6,10 +6,11 @@ import (
"os"
"strings"
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/nip42"
"fiatjaf.com/nostr/nip77"
"github.com/fatih/color"
"github.com/mailru/easyjson"
"github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nip77"
"github.com/urfave/cli/v3"
)
@ -88,16 +89,20 @@ example:
c,
relayUrls,
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) {
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"):]
}
log(s+"\n", args...)
}, authEvent)
}),
)
},
})
// stop here already if all connections failed
if len(relays) == 0 {
@ -132,7 +137,7 @@ example:
if len(relayUrls) > 0 {
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 {
ch, err := nip77.FetchIDsOnly(ctx, url, filter)
if err != nil {
@ -155,7 +160,7 @@ example:
fn = sys.Pool.SubscribeMany
}
for ie := range fn(ctx, relayUrls, filter) {
for ie := range fn(ctx, relayUrls, filter, nostr.SubscriptionOptions{}) {
stdout(ie.Event)
}
}
@ -165,7 +170,7 @@ example:
if c.Bool("bare") {
result = filter.String()
} else {
j, _ := json.Marshal(nostr.ReqEnvelope{SubscriptionID: "nak", Filters: nostr.Filters{filter}})
j, _ := json.Marshal(nostr.ReqEnvelope{SubscriptionID: "nak", Filter: filter})
result = string(j)
}
@ -179,13 +184,13 @@ example:
}
var reqFilterFlags = []cli.Flag{
&cli.StringSliceFlag{
&PubKeySliceFlag{
Name: "author",
Aliases: []string{"a"},
Usage: "only accept events from these authors (pubkey as hex)",
Category: CATEGORY_FILTER_ATTRIBUTES,
},
&cli.StringSliceFlag{
&IDSliceFlag{
Name: "id",
Aliases: []string{"i"},
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 {
if authors := c.StringSlice("author"); len(authors) > 0 {
if authors := getPubKeySlice(c, "author"); len(authors) > 0 {
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...)
}
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 != "" {
filter.Search = search

View File

@ -8,11 +8,11 @@ import (
"os"
"time"
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/eventstore/slicestore"
"fiatjaf.com/nostr/khatru"
"github.com/bep/debounce"
"github.com/fatih/color"
"github.com/fiatjaf/eventstore/slicestore"
"github.com/fiatjaf/khatru"
"github.com/nbd-wtf/go-nostr"
"github.com/urfave/cli/v3"
)
@ -38,7 +38,7 @@ var serve = &cli.Command{
},
},
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
if path := c.String("events"); path != "" {
@ -59,7 +59,7 @@ var serve = &cli.Command{
if err := json.Unmarshal(scanner.Bytes(), &evt); err != nil {
return fmt.Errorf("invalid event received at line %d: %s (`%s`)", i, err, scanner.Text())
}
db.SaveEvent(ctx, &evt)
db.SaveEvent(evt)
i++
}
}
@ -71,10 +71,7 @@ var serve = &cli.Command{
rl.Info.Software = "https://github.com/fiatjaf/nak"
rl.Info.Version = version
rl.QueryEvents = append(rl.QueryEvents, db.QueryEvents)
rl.CountEvents = append(rl.CountEvents, db.CountEvents)
rl.DeleteEvent = append(rl.DeleteEvent, db.DeleteEvent)
rl.StoreEvent = append(rl.StoreEvent, db.SaveEvent)
rl.UseEventstore(db)
started := make(chan bool)
exited := make(chan error)
@ -90,28 +87,29 @@ var serve = &cli.Command{
var printStatus func()
// 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))
printStatus()
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))
printStatus()
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))
printStatus()
return false, ""
})
}
d := debounce.New(time.Second * 2)
printStatus = func() {
d(func() {
totalEvents := 0
ch, _ := db.QueryEvents(ctx, nostr.Filter{})
for range ch {
for range db.QueryEvents(nostr.Filter{}) {
totalEvents++
}
subs := rl.GetListeningFilters()

View File

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

View File

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