From 6ccca357e20d98a8d7f8ec61bd3e299df08a272b Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Wed, 23 Aug 2023 23:33:49 +0900 Subject: [PATCH 001/401] support NIP-45 COUNT --- README.md | 31 +++++++++++ count.go | 162 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ main.go | 1 + 3 files changed, 194 insertions(+) create mode 100644 count.go diff --git a/README.md b/README.md index e31da1f..97b98a4 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,7 @@ USAGE: COMMANDS: req generates encoded REQ messages and optionally use them to talk to relays + count generates encoded COUNT messages and optionally use them to talk to relays event generates an encoded event and either prints it or sends it to a set of relays decode decodes nip19, nip21, nip05 or hex entities encode encodes notes and other stuff to nip19 entities @@ -136,6 +137,36 @@ OPTIONS: -e value [ -e value ] shortcut for --tag e= -p value [ -p value ] shortcut for --tag p= +~> nak count --help +NAME: + nak count - generates encoded COUNT messages and optionally use them to talk to relays + +USAGE: + nak count [command options] [relay...] + +DESCRIPTION: + outputs a NIP-45 request. Mostly same as req. + + example usage (with 'nostcat'): + nak count -k 1 -a 3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d | nostcat wss://nos.lol + standalone: + nak count -k 1 wss://nos.lol + +OPTIONS: + --bare when printing the filter, print just the filter, not enveloped in a ["COUNT", ...] array (default: false) + --stream keep the subscription open, printing all events as they are returned (default: false, will close on EOSE) + + FILTER ATTRIBUTES + + --author value, -a value [ --author value, -a value ] only accept events from these authors (pubkey as hex) + --id value, -i value [ --id value, -i value ] only accept events with these ids (hex) + --kind value, -k value [ --kind value, -k value ] only accept events with these kind numbers + --limit value, -l value only accept up to this number of events (default: 0) + --since value, -s value only accept events newer than this (unix timestamp) (default: 0) + --tag value, -t value [ --tag value, -t value ] takes a tag like -t e=, only accept events with these tags + --until value, -u value only accept events older than this (unix timestamp) (default: 0) + -e value [ -e value ] shortcut for --tag e= + -p value [ -p value ] shortcut for --tag p= ~> nak decode --help NAME: diff --git a/count.go b/count.go new file mode 100644 index 0000000..7176fa5 --- /dev/null +++ b/count.go @@ -0,0 +1,162 @@ +package main + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/nbd-wtf/go-nostr" + "github.com/urfave/cli/v2" +) + +const CATEGORY_COUNT_ATTRIBUTES = "FILTER ATTRIBUTES" + +var count = &cli.Command{ + Name: "count", + Usage: "generates encoded COUNT messages and optionally use them to talk to relays", + Description: `outputs a NIP-45 request. Mostly same as req. + +example usage (with 'nostcat'): + nak count -k 1 -a 3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d | nostcat wss://nos.lol +standalone: + nak count -k 1 wss://nos.lol`, + Flags: []cli.Flag{ + &cli.StringSliceFlag{ + Name: "author", + Aliases: []string{"a"}, + Usage: "only accept events from these authors (pubkey as hex)", + Category: CATEGORY_FILTER_ATTRIBUTES, + }, + &cli.StringSliceFlag{ + Name: "id", + Aliases: []string{"i"}, + Usage: "only accept events with these ids (hex)", + Category: CATEGORY_FILTER_ATTRIBUTES, + }, + &cli.IntSliceFlag{ + Name: "kind", + Aliases: []string{"k"}, + Usage: "only accept events with these kind numbers", + Category: CATEGORY_FILTER_ATTRIBUTES, + }, + &cli.StringSliceFlag{ + Name: "tag", + Aliases: []string{"t"}, + Usage: "takes a tag like -t e=, only accept events with these tags", + Category: CATEGORY_FILTER_ATTRIBUTES, + }, + &cli.StringSliceFlag{ + Name: "e", + Usage: "shortcut for --tag e=", + Category: CATEGORY_FILTER_ATTRIBUTES, + }, + &cli.StringSliceFlag{ + Name: "p", + Usage: "shortcut for --tag p=", + Category: CATEGORY_FILTER_ATTRIBUTES, + }, + &cli.IntFlag{ + Name: "since", + Aliases: []string{"s"}, + Usage: "only accept events newer than this (unix timestamp)", + Category: CATEGORY_FILTER_ATTRIBUTES, + }, + &cli.IntFlag{ + Name: "until", + Aliases: []string{"u"}, + Usage: "only accept events older than this (unix timestamp)", + Category: CATEGORY_FILTER_ATTRIBUTES, + }, + &cli.IntFlag{ + Name: "limit", + Aliases: []string{"l"}, + Usage: "only accept up to this number of events", + Category: CATEGORY_FILTER_ATTRIBUTES, + }, + &cli.BoolFlag{ + Name: "bare", + Usage: "when printing the filter, print just the filter, not enveloped in a [\"COUNT\", ...] array", + }, + &cli.BoolFlag{ + Name: "stream", + Usage: "keep the subscription open, printing all events as they are returned", + DefaultText: "false, will close on EOSE", + }, + }, + ArgsUsage: "[relay...]", + Action: func(c *cli.Context) error { + filter := nostr.Filter{} + + if authors := c.StringSlice("author"); len(authors) > 0 { + filter.Authors = authors + } + if ids := c.StringSlice("id"); len(ids) > 0 { + filter.IDs = ids + } + if kinds := c.IntSlice("kind"); len(kinds) > 0 { + filter.Kinds = kinds + } + + tags := make([][]string, 0, 5) + for _, tagFlag := range c.StringSlice("tag") { + spl := strings.Split(tagFlag, "=") + if len(spl) == 2 && len(spl[0]) == 1 { + tags = append(tags, spl) + } else { + return fmt.Errorf("invalid --tag '%s'", tagFlag) + } + } + for _, etag := range c.StringSlice("e") { + tags = append(tags, []string{"e", etag}) + } + for _, ptag := range c.StringSlice("p") { + tags = append(tags, []string{"p", ptag}) + } + if len(tags) > 0 { + filter.Tags = make(nostr.TagMap) + for _, tag := range tags { + if _, ok := filter.Tags[tag[0]]; !ok { + filter.Tags[tag[0]] = make([]string, 0, 3) + } + filter.Tags[tag[0]] = append(filter.Tags[tag[0]], tag[1]) + } + } + + if since := c.Int("since"); since != 0 { + ts := nostr.Timestamp(since) + filter.Since = &ts + } + if until := c.Int("until"); until != 0 { + ts := nostr.Timestamp(until) + filter.Until = &ts + } + if limit := c.Int("limit"); limit != 0 { + filter.Limit = limit + } + + relays := c.Args().Slice() + if len(relays) > 0 { + pool := nostr.NewSimplePool(c.Context) + fn := pool.SubManyEose + if c.Bool("stream") { + fn = pool.SubMany + } + for evt := range fn(c.Context, relays, nostr.Filters{filter}) { + fmt.Println(evt) + } + } else { + // no relays given, will just print the filter + var result string + if c.Bool("bare") { + result = filter.String() + } else { + j, _ := json.Marshal([]any{"COUNT", "nak", filter}) + result = string(j) + } + + fmt.Println(result) + } + + return nil + }, +} diff --git a/main.go b/main.go index 00f461c..0c88b22 100644 --- a/main.go +++ b/main.go @@ -13,6 +13,7 @@ func main() { Usage: "the nostr army knife command-line tool", Commands: []*cli.Command{ req, + count, event, decode, encode, From c214513304fa20420c3fec1c788f967f20f1804b Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sun, 8 Oct 2023 14:40:46 -0300 Subject: [PATCH 002/401] improve `count`. --- count.go | 28 ++++------------------------ 1 file changed, 4 insertions(+), 24 deletions(-) diff --git a/count.go b/count.go index 7176fa5..cfcac73 100644 --- a/count.go +++ b/count.go @@ -12,14 +12,9 @@ import ( const CATEGORY_COUNT_ATTRIBUTES = "FILTER ATTRIBUTES" var count = &cli.Command{ - Name: "count", - Usage: "generates encoded COUNT messages and optionally use them to talk to relays", - Description: `outputs a NIP-45 request. Mostly same as req. - -example usage (with 'nostcat'): - nak count -k 1 -a 3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d | nostcat wss://nos.lol -standalone: - nak count -k 1 wss://nos.lol`, + Name: "count", + Usage: "generates encoded COUNT messages and optionally use them to talk to relays", + Description: `outputs a NIP-45 request -- (mostly the same as 'nak req').`, Flags: []cli.Flag{ &cli.StringSliceFlag{ Name: "author", @@ -27,12 +22,6 @@ standalone: Usage: "only accept events from these authors (pubkey as hex)", Category: CATEGORY_FILTER_ATTRIBUTES, }, - &cli.StringSliceFlag{ - Name: "id", - Aliases: []string{"i"}, - Usage: "only accept events with these ids (hex)", - Category: CATEGORY_FILTER_ATTRIBUTES, - }, &cli.IntSliceFlag{ Name: "kind", Aliases: []string{"k"}, @@ -77,11 +66,6 @@ standalone: Name: "bare", Usage: "when printing the filter, print just the filter, not enveloped in a [\"COUNT\", ...] array", }, - &cli.BoolFlag{ - Name: "stream", - Usage: "keep the subscription open, printing all events as they are returned", - DefaultText: "false, will close on EOSE", - }, }, ArgsUsage: "[relay...]", Action: func(c *cli.Context) error { @@ -137,11 +121,7 @@ standalone: relays := c.Args().Slice() if len(relays) > 0 { pool := nostr.NewSimplePool(c.Context) - fn := pool.SubManyEose - if c.Bool("stream") { - fn = pool.SubMany - } - for evt := range fn(c.Context, relays, nostr.Filters{filter}) { + for evt := range pool.SubManyEose(c.Context, relays, nostr.Filters{filter}) { fmt.Println(evt) } } else { From 3896ef323b9379a7de55f7a5a048f0d81e4db786 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sun, 8 Oct 2023 14:41:43 -0300 Subject: [PATCH 003/401] update go-nostr dependency. --- count.go | 4 ++-- go.mod | 9 +++++++-- go.sum | 26 ++++++++++++++++++++++---- req.go | 4 ++-- 4 files changed, 33 insertions(+), 10 deletions(-) diff --git a/count.go b/count.go index cfcac73..96e4574 100644 --- a/count.go +++ b/count.go @@ -121,8 +121,8 @@ var count = &cli.Command{ relays := c.Args().Slice() if len(relays) > 0 { pool := nostr.NewSimplePool(c.Context) - for evt := range pool.SubManyEose(c.Context, relays, nostr.Filters{filter}) { - fmt.Println(evt) + for ie := range pool.SubManyEose(c.Context, relays, nostr.Filters{filter}) { + fmt.Println(ie.Event) } } else { // no relays given, will just print the filter diff --git a/go.mod b/go.mod index f423dd8..1a60ce6 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.20 require ( github.com/mailru/easyjson v0.7.7 - github.com/nbd-wtf/go-nostr v0.19.2 + github.com/nbd-wtf/go-nostr v0.24.2 github.com/urfave/cli/v2 v2.25.3 ) @@ -12,14 +12,19 @@ require ( github.com/btcsuite/btcd/btcec/v2 v2.2.0 // indirect github.com/btcsuite/btcd/btcutil v1.1.3 // indirect github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.1.1 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/decred/dcrd/crypto/blake256 v1.0.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect + github.com/dgraph-io/ristretto v0.1.1 // indirect + github.com/dustin/go-humanize v1.0.0 // indirect github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/pool v0.2.1 // indirect github.com/gobwas/ws v1.2.0 // indirect + github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect github.com/josharian/intern v1.0.0 // indirect - github.com/puzpuzpuz/xsync v1.5.2 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/puzpuzpuz/xsync/v2 v2.5.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/tidwall/gjson v1.14.4 // indirect github.com/tidwall/match v1.1.1 // indirect diff --git a/go.sum b/go.sum index 6169c94..9204f7d 100644 --- a/go.sum +++ b/go.sum @@ -22,6 +22,8 @@ github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= +github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -33,6 +35,12 @@ github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= +github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8= +github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 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/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= @@ -41,6 +49,8 @@ github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.2.0 h1:u0p9s3xLYpZCA1z5JgCkMeB34CKCMMQbM+G8Ii7YD0I= github.com/gobwas/ws v1.2.0/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= @@ -61,8 +71,8 @@ github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlT github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/nbd-wtf/go-nostr v0.19.2 h1:Oofhe5+EKvf74fZQmYyX5G4RS74/na1aNabsB/cW9b4= -github.com/nbd-wtf/go-nostr v0.19.2/go.mod h1:F9y6+M8askJCjilLgMC3rD0moA6UtG1MCnyClNYXeys= +github.com/nbd-wtf/go-nostr v0.24.2 h1:1PdFED7uHh3BlXfDVD96npBc0YAgj9hPT+l6NWog4kc= +github.com/nbd-wtf/go-nostr v0.24.2/go.mod h1:eE8Qf8QszZbCd9arBQyotXqATNUElWsTEEx+LLORhyQ= 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= @@ -72,12 +82,17 @@ github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5 github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/puzpuzpuz/xsync v1.5.2 h1:yRAP4wqSOZG+/4pxJ08fPTwrfL0IzE/LKQ/cw509qGY= -github.com/puzpuzpuz/xsync v1.5.2/go.mod h1:K98BYhX3k1dQ2M63t1YNVDanbwUPmBCAhNmVrrxfiGg= +github.com/puzpuzpuz/xsync/v2 v2.5.0 h1:2k4qrO/orvmEXZ3hmtHqIy9XaQtPTwzMZk1+iErpE8c= +github.com/puzpuzpuz/xsync/v2 v2.5.0/go.mod h1:gD2H2krq/w52MfPLE+Uy64TzJDVY7lP2znR9qmR35kU= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= @@ -111,6 +126,7 @@ golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -129,6 +145,8 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/req.go b/req.go index a862698..dec8c8b 100644 --- a/req.go +++ b/req.go @@ -141,8 +141,8 @@ standalone: if c.Bool("stream") { fn = pool.SubMany } - for evt := range fn(c.Context, relays, nostr.Filters{filter}) { - fmt.Println(evt) + for ie := range fn(c.Context, relays, nostr.Filters{filter}) { + fmt.Println(ie.Event) } } else { // no relays given, will just print the filter From e4a9b3ccc7f06fb56d29932bfe9d88df40b0aff0 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sun, 8 Oct 2023 15:05:51 -0300 Subject: [PATCH 004/401] fix count again, it was sending REQs instead of COUNTs to relays. only use the first relay. --- count.go | 30 ++++++++++++------------------ 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/count.go b/count.go index 96e4574..1bff48d 100644 --- a/count.go +++ b/count.go @@ -9,8 +9,6 @@ import ( "github.com/urfave/cli/v2" ) -const CATEGORY_COUNT_ATTRIBUTES = "FILTER ATTRIBUTES" - var count = &cli.Command{ Name: "count", Usage: "generates encoded COUNT messages and optionally use them to talk to relays", @@ -62,10 +60,6 @@ var count = &cli.Command{ Usage: "only accept up to this number of events", Category: CATEGORY_FILTER_ATTRIBUTES, }, - &cli.BoolFlag{ - Name: "bare", - Usage: "when printing the filter, print just the filter, not enveloped in a [\"COUNT\", ...] array", - }, }, ArgsUsage: "[relay...]", Action: func(c *cli.Context) error { @@ -118,22 +112,22 @@ var count = &cli.Command{ filter.Limit = limit } - relays := c.Args().Slice() - if len(relays) > 0 { - pool := nostr.NewSimplePool(c.Context) - for ie := range pool.SubManyEose(c.Context, relays, nostr.Filters{filter}) { - fmt.Println(ie.Event) + relay := c.Args().First() + if relay != "" { + relay, err := nostr.RelayConnect(c.Context, relay) + if err != nil { + return err } + count, err := relay.Count(c.Context, nostr.Filters{filter}) + if err != nil { + return err + } + fmt.Println(count) } else { // no relays given, will just print the filter var result string - if c.Bool("bare") { - result = filter.String() - } else { - j, _ := json.Marshal([]any{"COUNT", "nak", filter}) - result = string(j) - } - + j, _ := json.Marshal([]any{"COUNT", "nak", filter}) + result = string(j) fmt.Println(result) } From 8d111e556e6812e31aa5650c74316841380f90db Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sun, 8 Oct 2023 15:31:20 -0300 Subject: [PATCH 005/401] count uses all relays again, now correctly. --- count.go | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/count.go b/count.go index 1bff48d..906cab1 100644 --- a/count.go +++ b/count.go @@ -2,6 +2,7 @@ package main import ( "encoding/json" + "errors" "fmt" "strings" @@ -12,7 +13,7 @@ import ( var count = &cli.Command{ Name: "count", Usage: "generates encoded COUNT messages and optionally use them to talk to relays", - Description: `outputs a NIP-45 request -- (mostly the same as 'nak req').`, + Description: `outputs a NIP-45 request (the flags are mostly the same as 'nak req').`, Flags: []cli.Flag{ &cli.StringSliceFlag{ Name: "author", @@ -112,17 +113,27 @@ var count = &cli.Command{ filter.Limit = limit } - relay := c.Args().First() - if relay != "" { - relay, err := nostr.RelayConnect(c.Context, relay) - if err != nil { - return err + relays := c.Args().Slice() + successes := 0 + failures := make([]error, 0, len(relays)) + if len(relays) > 0 { + for _, relayUrl := range relays { + relay, err := nostr.RelayConnect(c.Context, relayUrl) + if err != nil { + failures = append(failures, err) + continue + } + count, err := relay.Count(c.Context, nostr.Filters{filter}) + if err != nil { + failures = append(failures, err) + continue + } + fmt.Printf("%s: %d\n", relay.URL, count) + successes++ } - count, err := relay.Count(c.Context, nostr.Filters{filter}) - if err != nil { - return err + if successes == 0 { + return errors.Join(failures...) } - fmt.Println(count) } else { // no relays given, will just print the filter var result string From 455ec79e58219196a4b410272bc1a7273b8e99c5 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sun, 8 Oct 2023 15:49:11 -0300 Subject: [PATCH 006/401] NIP-50 search filter on req. --- req.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/req.go b/req.go index dec8c8b..674243b 100644 --- a/req.go +++ b/req.go @@ -73,6 +73,11 @@ standalone: Usage: "only accept up to this number of events", Category: CATEGORY_FILTER_ATTRIBUTES, }, + &cli.StringFlag{ + Name: "search", + Usage: "a NIP-50 search query, use it only with relays that explicitly support it", + Category: CATEGORY_FILTER_ATTRIBUTES, + }, &cli.BoolFlag{ Name: "bare", Usage: "when printing the filter, print just the filter, not enveloped in a [\"REQ\", ...] array", @@ -96,7 +101,9 @@ standalone: if kinds := c.IntSlice("kind"); len(kinds) > 0 { filter.Kinds = kinds } - + if search := c.String("search"); search != "" { + filter.Search = search + } tags := make([][]string, 0, 5) for _, tagFlag := range c.StringSlice("tag") { spl := strings.Split(tagFlag, "=") From ada76f281a1a12d5cfb8794573e86c3c537b404e Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 10 Oct 2023 11:28:17 -0300 Subject: [PATCH 007/401] add encode note. --- encode.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/encode.go b/encode.go index 41238b6..debec81 100644 --- a/encode.go +++ b/encode.go @@ -187,6 +187,23 @@ var encode = &cli.Command{ } }, }, + { + Name: "note", + Usage: "generate note1 event codes (not recommended)", + Action: func(c *cli.Context) error { + target := c.Args().First() + if err := validate32BytesHex(target); err != nil { + return err + } + + if npub, err := nip19.EncodeNote(target); err == nil { + fmt.Println(npub) + return nil + } else { + return err + } + }, + }, }, } From db157e6181b9ea8bdf5a37f8c7c8b5f195414491 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sun, 15 Oct 2023 09:18:19 -0300 Subject: [PATCH 008/401] fetch method to fetch events from nip19 codes and relay hints. --- fetch.go | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ main.go | 1 + 2 files changed, 61 insertions(+) create mode 100644 fetch.go diff --git a/fetch.go b/fetch.go new file mode 100644 index 0000000..36e866d --- /dev/null +++ b/fetch.go @@ -0,0 +1,60 @@ +package main + +import ( + "fmt" + + "github.com/nbd-wtf/go-nostr" + "github.com/nbd-wtf/go-nostr/nip19" + "github.com/urfave/cli/v2" +) + +var fetch = &cli.Command{ + Name: "fetch", + Usage: "fetches events related to the given nip19 code from the included relay hints", + Description: ``, + Flags: []cli.Flag{}, + ArgsUsage: "[nip19code]", + Action: func(c *cli.Context) error { + filter := nostr.Filter{} + code := c.Args().First() + + prefix, value, err := nip19.Decode(code) + if err != nil { + return err + } + + var relays []string + switch prefix { + case "nevent": + v := value.(nostr.EventPointer) + filter.IDs = append(filter.IDs, v.ID) + if v.Author != "" { + // TODO fetch relays from nip65 + } + relays = v.Relays + case "naddr": + v := value.(nostr.EntityPointer) + filter.Tags = nostr.TagMap{"d": []string{v.Identifier}} + filter.Kinds = append(filter.Kinds, v.Kind) + filter.Authors = append(filter.Authors, v.PublicKey) + // TODO fetch relays from nip65 + relays = v.Relays + case "nprofile": + v := value.(nostr.ProfilePointer) + filter.Authors = append(filter.Authors, v.PublicKey) + // TODO fetch relays from nip65 + relays = v.Relays + } + + if len(relays) == 0 { + return fmt.Errorf("no relay hints found") + } + + pool := nostr.NewSimplePool(c.Context) + for ie := range pool.SubManyEose(c.Context, relays, nostr.Filters{filter}) { + fmt.Println(ie.Event) + } + + return nil + }, +} diff --git a/main.go b/main.go index 0c88b22..15551ca 100644 --- a/main.go +++ b/main.go @@ -14,6 +14,7 @@ func main() { Commands: []*cli.Command{ req, count, + fetch, event, decode, encode, From 459b127988faa226b33b366c3ae92204fdcdbd0f Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sun, 15 Oct 2023 09:22:39 -0300 Subject: [PATCH 009/401] fetch: use relay hints from author pubkeys. --- fetch.go | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/fetch.go b/fetch.go index 36e866d..e6dc299 100644 --- a/fetch.go +++ b/fetch.go @@ -5,6 +5,7 @@ import ( "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/nip19" + "github.com/nbd-wtf/go-nostr/sdk" "github.com/urfave/cli/v2" ) @@ -24,12 +25,14 @@ var fetch = &cli.Command{ } var relays []string + var authorHint string + switch prefix { case "nevent": v := value.(nostr.EventPointer) filter.IDs = append(filter.IDs, v.ID) if v.Author != "" { - // TODO fetch relays from nip65 + authorHint = v.Author } relays = v.Relays case "naddr": @@ -37,20 +40,30 @@ var fetch = &cli.Command{ filter.Tags = nostr.TagMap{"d": []string{v.Identifier}} filter.Kinds = append(filter.Kinds, v.Kind) filter.Authors = append(filter.Authors, v.PublicKey) - // TODO fetch relays from nip65 + authorHint = v.PublicKey relays = v.Relays case "nprofile": v := value.(nostr.ProfilePointer) filter.Authors = append(filter.Authors, v.PublicKey) - // TODO fetch relays from nip65 + authorHint = v.PublicKey relays = v.Relays } + pool := nostr.NewSimplePool(c.Context) + if authorHint != "" { + relayList := sdk.FetchRelaysForPubkey(c.Context, pool, authorHint, + "wss://purplepag.es", "wss://offchain.pub", "wss://public.relaying.io") + for _, relayListItem := range relayList { + if relayListItem.Outbox { + relays = append(relays, relayListItem.URL) + } + } + } + if len(relays) == 0 { return fmt.Errorf("no relay hints found") } - pool := nostr.NewSimplePool(c.Context) for ie := range pool.SubManyEose(c.Context, relays, nostr.Filters{filter}) { fmt.Println(ie.Event) } From 208d9097279ae7fa4be309f2161f03b734888bfa Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Fri, 20 Oct 2023 20:57:29 -0300 Subject: [PATCH 010/401] support reading from stdin. --- encode.go | 10 +++++----- fetch.go | 2 +- helpers.go | 29 +++++++++++++++++++++++++++++ 3 files changed, 35 insertions(+), 6 deletions(-) create mode 100644 helpers.go diff --git a/encode.go b/encode.go index debec81..c8090b2 100644 --- a/encode.go +++ b/encode.go @@ -31,7 +31,7 @@ var encode = &cli.Command{ Name: "npub", Usage: "encode a hex private key into bech32 'npub' format", Action: func(c *cli.Context) error { - target := c.Args().First() + target := getStdinOrFirstArgument(c) if err := validate32BytesHex(target); err != nil { return err } @@ -48,7 +48,7 @@ var encode = &cli.Command{ Name: "nsec", Usage: "encode a hex private key into bech32 'nsec' format", Action: func(c *cli.Context) error { - target := c.Args().First() + target := getStdinOrFirstArgument(c) if err := validate32BytesHex(target); err != nil { return err } @@ -72,7 +72,7 @@ var encode = &cli.Command{ }, }, Action: func(c *cli.Context) error { - target := c.Args().First() + target := getStdinOrFirstArgument(c) if err := validate32BytesHex(target); err != nil { return err } @@ -105,7 +105,7 @@ var encode = &cli.Command{ }, }, Action: func(c *cli.Context) error { - target := c.Args().First() + target := getStdinOrFirstArgument(c) if err := validate32BytesHex(target); err != nil { return err } @@ -191,7 +191,7 @@ var encode = &cli.Command{ Name: "note", Usage: "generate note1 event codes (not recommended)", Action: func(c *cli.Context) error { - target := c.Args().First() + target := getStdinOrFirstArgument(c) if err := validate32BytesHex(target); err != nil { return err } diff --git a/fetch.go b/fetch.go index e6dc299..fa284be 100644 --- a/fetch.go +++ b/fetch.go @@ -17,7 +17,7 @@ var fetch = &cli.Command{ ArgsUsage: "[nip19code]", Action: func(c *cli.Context) error { filter := nostr.Filter{} - code := c.Args().First() + code := getStdinOrFirstArgument(c) prefix, value, err := nip19.Decode(code) if err != nil { diff --git a/helpers.go b/helpers.go new file mode 100644 index 0000000..d71c02a --- /dev/null +++ b/helpers.go @@ -0,0 +1,29 @@ +package main + +import ( + "bytes" + "io" + "os" + + "github.com/urfave/cli/v2" +) + +func getStdin() string { + stat, _ := os.Stdin.Stat() + if (stat.Mode() & os.ModeCharDevice) == 0 { + read := bytes.NewBuffer(make([]byte, 0, 1000)) + _, err := io.Copy(read, os.Stdin) + if err == nil { + return read.String() + } + } + return "" +} + +func getStdinOrFirstArgument(c *cli.Context) string { + target := c.Args().First() + if target != "" { + return target + } + return getStdin() +} From 757a6eb3132db11a1ff3f1c92edff7e722479344 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Fri, 20 Oct 2023 20:57:38 -0300 Subject: [PATCH 011/401] support fetch npub --- fetch.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/fetch.go b/fetch.go index fa284be..6b6fd2f 100644 --- a/fetch.go +++ b/fetch.go @@ -45,8 +45,14 @@ var fetch = &cli.Command{ case "nprofile": v := value.(nostr.ProfilePointer) filter.Authors = append(filter.Authors, v.PublicKey) + filter.Kinds = append(filter.Kinds, 0) authorHint = v.PublicKey relays = v.Relays + case "npub": + v := value.(string) + filter.Authors = append(filter.Authors, v) + filter.Kinds = append(filter.Kinds, 0) + authorHint = v } pool := nostr.NewSimplePool(c.Context) From ffa41046fdce17a3d5c32aabeb4311ce9792f3cf Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Fri, 20 Oct 2023 21:01:11 -0300 Subject: [PATCH 012/401] fetch with optional --relay flags. --- encode.go | 20 -------------------- fetch.go | 23 +++++++++++++++++------ helpers.go | 21 +++++++++++++++++++++ 3 files changed, 38 insertions(+), 26 deletions(-) diff --git a/encode.go b/encode.go index c8090b2..5fb2071 100644 --- a/encode.go +++ b/encode.go @@ -3,7 +3,6 @@ package main import ( "encoding/hex" "fmt" - "net/url" "strings" "github.com/nbd-wtf/go-nostr/nip19" @@ -220,22 +219,3 @@ func validate32BytesHex(target string) error { return nil } - -func validateRelayURLs(wsurls []string) error { - for _, wsurl := range wsurls { - u, err := url.Parse(wsurl) - if err != nil { - return fmt.Errorf("invalid relay url '%s': %s", wsurl, err) - } - - if u.Scheme != "ws" && u.Scheme != "wss" { - return fmt.Errorf("relay url must use wss:// or ws:// schemes, got '%s'", wsurl) - } - - if u.Host == "" { - return fmt.Errorf("relay url '%s' is missing the hostname", wsurl) - } - } - - return nil -} diff --git a/fetch.go b/fetch.go index 6b6fd2f..d626f32 100644 --- a/fetch.go +++ b/fetch.go @@ -10,11 +10,19 @@ import ( ) var fetch = &cli.Command{ - Name: "fetch", - Usage: "fetches events related to the given nip19 code from the included relay hints", - Description: ``, - Flags: []cli.Flag{}, - ArgsUsage: "[nip19code]", + Name: "fetch", + Usage: "fetches events related to the given nip19 code from the included relay hints", + Description: `example usage: + nak fetch nevent1qqsxrwm0hd3s3fddh4jc2574z3xzufq6qwuyz2rvv3n087zvym3dpaqprpmhxue69uhhqatzd35kxtnjv4kxz7tfdenju6t0xpnej4 + echo npub1h8spmtw9m2huyv6v2j2qd5zv956z2zdugl6mgx02f2upffwpm3nqv0j4ps | nak fetch --relay wss://relay.nostr.band`, + Flags: []cli.Flag{ + &cli.StringSliceFlag{ + Name: "relay", + Aliases: []string{"r"}, + Usage: "also use these relays to fetch from", + }, + }, + ArgsUsage: "[nip19code]", Action: func(c *cli.Context) error { filter := nostr.Filter{} code := getStdinOrFirstArgument(c) @@ -24,7 +32,10 @@ var fetch = &cli.Command{ return err } - var relays []string + relays := c.StringSlice("relay") + if err := validateRelayURLs(relays); err != nil { + return err + } var authorHint string switch prefix { diff --git a/helpers.go b/helpers.go index d71c02a..d319ede 100644 --- a/helpers.go +++ b/helpers.go @@ -2,7 +2,9 @@ package main import ( "bytes" + "fmt" "io" + "net/url" "os" "github.com/urfave/cli/v2" @@ -27,3 +29,22 @@ func getStdinOrFirstArgument(c *cli.Context) string { } return getStdin() } + +func validateRelayURLs(wsurls []string) error { + for _, wsurl := range wsurls { + u, err := url.Parse(wsurl) + if err != nil { + return fmt.Errorf("invalid relay url '%s': %s", wsurl, err) + } + + if u.Scheme != "ws" && u.Scheme != "wss" { + return fmt.Errorf("relay url must use wss:// or ws:// schemes, got '%s'", wsurl) + } + + if u.Host == "" { + return fmt.Errorf("relay url '%s' is missing the hostname", wsurl) + } + } + + return nil +} From 50dde2117c773ffb2edb88ba87860160d83783a6 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Mon, 23 Oct 2023 08:04:21 -0300 Subject: [PATCH 013/401] don't fail encode when reading from stdin because of the number of arguments. --- encode.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/encode.go b/encode.go index 5fb2071..3d6773a 100644 --- a/encode.go +++ b/encode.go @@ -20,8 +20,8 @@ var encode = &cli.Command{ nak encode nevent --author --relay --relay nak encode nsec `, Before: func(c *cli.Context) error { - if c.Args().Len() < 2 { - return fmt.Errorf("expected more than 2 arguments.") + if c.Args().Len() < 1 { + return fmt.Errorf("expected more than 1 argument.") } return nil }, From c6e9fdd053339beba10ff1b8615a9fd63c1ec943 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Mon, 23 Oct 2023 08:04:28 -0300 Subject: [PATCH 014/401] trim spaces from stdin. --- helpers.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/helpers.go b/helpers.go index d319ede..67805bb 100644 --- a/helpers.go +++ b/helpers.go @@ -6,6 +6,7 @@ import ( "io" "net/url" "os" + "strings" "github.com/urfave/cli/v2" ) @@ -16,7 +17,7 @@ func getStdin() string { read := bytes.NewBuffer(make([]byte, 0, 1000)) _, err := io.Copy(read, os.Stdin) if err == nil { - return read.String() + return strings.TrimSpace(read.String()) } } return "" From 0615a8b577487a3d69d1afd4d0b840529701571b Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sun, 29 Oct 2023 19:11:35 -0300 Subject: [PATCH 015/401] nak req can take (and optionally modify) filters from stdin. --- req.go | 37 ++++++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/req.go b/req.go index 674243b..a5d2170 100644 --- a/req.go +++ b/req.go @@ -16,10 +16,14 @@ var req = &cli.Command{ Usage: "generates encoded REQ messages and optionally use them to talk to relays", Description: `outputs a NIP-01 Nostr filter. when a relay is not given, will print the filter, otherwise will connect to the given relay and send the filter. -example usage (with 'nostcat'): - nak req -k 1 -a 3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d | nostcat wss://nos.lol -standalone: - nak req -k 1 wss://nos.lol`, +example: + nak req -k 1 -a 3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d wss://nos.lol wss://nostr.mom + +it can also take a filter from stdin, optionally modify it with flags and send it to specific relays (or just print it). + +example: + echo '{"kinds": [1], "#t": ["test"]}' | nak req -l 5 -k 4549 --tag t=spam wss://nostr-pub.wellorder.net +`, Flags: []cli.Flag{ &cli.StringSliceFlag{ Name: "author", @@ -91,15 +95,20 @@ standalone: ArgsUsage: "[relay...]", Action: func(c *cli.Context) error { filter := nostr.Filter{} + if stdinFilter := getStdin(); stdinFilter != "" { + if err := json.Unmarshal([]byte(stdinFilter), &filter); err != nil { + return fmt.Errorf("invalid filter received from stdin: %w", err) + } + } if authors := c.StringSlice("author"); len(authors) > 0 { - filter.Authors = authors + filter.Authors = append(filter.Authors, authors...) } if ids := c.StringSlice("id"); len(ids) > 0 { - filter.IDs = ids + filter.IDs = append(filter.IDs, ids...) } if kinds := c.IntSlice("kind"); len(kinds) > 0 { - filter.Kinds = kinds + filter.Kinds = append(filter.Kinds, kinds...) } if search := c.String("search"); search != "" { filter.Search = search @@ -119,14 +128,16 @@ standalone: for _, ptag := range c.StringSlice("p") { tags = append(tags, []string{"p", ptag}) } - if len(tags) > 0 { + + if len(tags) > 0 && filter.Tags == nil { filter.Tags = make(nostr.TagMap) - for _, tag := range tags { - if _, ok := filter.Tags[tag[0]]; !ok { - filter.Tags[tag[0]] = make([]string, 0, 3) - } - filter.Tags[tag[0]] = append(filter.Tags[tag[0]], tag[1]) + } + + for _, tag := range tags { + if _, ok := filter.Tags[tag[0]]; !ok { + filter.Tags[tag[0]] = make([]string, 0, 3) } + filter.Tags[tag[0]] = append(filter.Tags[tag[0]], tag[1]) } if since := c.Int("since"); since != 0 { From bf966b3e2c4d4d8241eb03e525f9358b858a2313 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sun, 29 Oct 2023 21:48:18 -0300 Subject: [PATCH 016/401] nak event can take (and optionally modify) events from stdin. --- event.go | 80 ++++++++++++++++++++++++++++++++++++++++++-------------- req.go | 3 ++- 2 files changed, 62 insertions(+), 21 deletions(-) diff --git a/event.go b/event.go index 6925f17..57a59fa 100644 --- a/event.go +++ b/event.go @@ -20,10 +20,18 @@ const CATEGORY_EVENT_FIELDS = "EVENT FIELDS" var event = &cli.Command{ Name: "event", Usage: "generates an encoded event and either prints it or sends it to a set of relays", - Description: `example usage (for sending directly to a relay with 'nostcat'): - nak event -k 1 -c hello --envelope | nostcat wss://nos.lol -standalone: - nak event -k 1 -c hello wss://nos.lol`, + Description: `outputs an event built with the flags. if one or more relays are given as arguments, an attempt is also made to publish the event to these relays. + +example: + nak event -c hello wss://nos.lol + nak event -k 3 -p 3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d + +if an event -- or a partial event -- is given on stdin, the flags can be used to optionally modify it. if it is modified it is rehashed and resigned, otherwise it is just returned as given, but that can be used to just publish to relays. + +example: + echo '{"id":"a889df6a387419ff204305f4c2d296ee328c3cd4f8b62f205648a541b4554dfb","pubkey":"c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5","created_at":1698623783,"kind":1,"tags":[],"content":"hello from the nostr army knife","sig":"84876e1ee3e726da84e5d195eb79358b2b3eaa4d9bd38456fde3e8a2af3f1cd4cda23f23fda454869975b3688797d4c66e12f4c51c1b43c6d2997c5e61865661"}' | nak event wss://offchain.pub + echo '{"tags": [["t", "spam"]]}' | nak event -c 'this is spam' +`, Flags: []cli.Flag{ &cli.StringFlag{ Name: "sec", @@ -44,7 +52,7 @@ standalone: Aliases: []string{"k"}, Usage: "event kind", DefaultText: "1", - Value: 1, + Value: 0, Category: CATEGORY_EVENT_FIELDS, }, &cli.StringFlag{ @@ -52,7 +60,7 @@ standalone: Aliases: []string{"c"}, Usage: "event content", DefaultText: "hello from the nostr army knife", - Value: "hello from the nostr army knife", + Value: "", Category: CATEGORY_EVENT_FIELDS, }, &cli.StringSliceFlag{ @@ -76,16 +84,38 @@ standalone: Aliases: []string{"time", "ts"}, Usage: "unix timestamp value for the created_at field", DefaultText: "now", - Value: "now", + Value: "", Category: CATEGORY_EVENT_FIELDS, }, }, ArgsUsage: "[relay...]", Action: func(c *cli.Context) error { evt := nostr.Event{ - Kind: c.Int("kind"), - Content: c.String("content"), - Tags: make(nostr.Tags, 0, 3), + Tags: make(nostr.Tags, 0, 3), + } + + mustRehashAndResign := false + + if stdinEvent := getStdin(); stdinEvent != "" { + if err := json.Unmarshal([]byte(stdinEvent), &evt); err != nil { + return fmt.Errorf("invalid event received from stdin: %w", err) + } + } + + if kind := c.Int("kind"); kind != 0 { + evt.Kind = kind + mustRehashAndResign = true + } else if evt.Kind == 0 { + evt.Kind = 1 + mustRehashAndResign = true + } + + if content := c.String("content"); content != "" { + evt.Content = content + mustRehashAndResign = true + } else if evt.Content == "" && evt.Kind == 1 { + evt.Content = "hello from the nostr army knife" + mustRehashAndResign = true } tags := make(nostr.Tags, 0, 5) @@ -103,29 +133,39 @@ standalone: } for _, etag := range c.StringSlice("e") { tags = append(tags, []string{"e", etag}) + mustRehashAndResign = true } for _, ptag := range c.StringSlice("p") { tags = append(tags, []string{"p", ptag}) + mustRehashAndResign = true } if len(tags) > 0 { for _, tag := range tags { evt.Tags = append(evt.Tags, tag) } + mustRehashAndResign = true } - createdAt := c.String("created-at") - ts := time.Now() - if createdAt != "now" { - if v, err := strconv.ParseInt(createdAt, 10, 64); err != nil { - return fmt.Errorf("failed to parse timestamp '%s': %w", createdAt, err) - } else { - ts = time.Unix(v, 0) + if createdAt := c.String("created-at"); createdAt != "" { + ts := time.Now() + if createdAt != "now" { + if v, err := strconv.ParseInt(createdAt, 10, 64); err != nil { + return fmt.Errorf("failed to parse timestamp '%s': %w", createdAt, err) + } else { + ts = time.Unix(v, 0) + } } + evt.CreatedAt = nostr.Timestamp(ts.Unix()) + mustRehashAndResign = true + } else if evt.CreatedAt == 0 { + evt.CreatedAt = nostr.Now() + mustRehashAndResign = true } - evt.CreatedAt = nostr.Timestamp(ts.Unix()) - if err := evt.Sign(c.String("sec")); err != nil { - return fmt.Errorf("error signing with provided key: %w", err) + if evt.Sig == "" || mustRehashAndResign { + if err := evt.Sign(c.String("sec")); err != nil { + return fmt.Errorf("error signing with provided key: %w", err) + } } relays := c.Args().Slice() diff --git a/req.go b/req.go index a5d2170..c8f3985 100644 --- a/req.go +++ b/req.go @@ -17,7 +17,8 @@ var req = &cli.Command{ Description: `outputs a NIP-01 Nostr filter. when a relay is not given, will print the filter, otherwise will connect to the given relay and send the filter. example: - nak req -k 1 -a 3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d wss://nos.lol wss://nostr.mom + nak req -k 1 -l 15 wss://nostr.wine wss://nostr-pub.wellorder.net + nak req -k 0 -a 3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d wss://nos.lol | jq '.content | fromjson | .name' it can also take a filter from stdin, optionally modify it with flags and send it to specific relays (or just print it). From 85d658bdd48cd27b73796f9030537b31a801ef0b Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sun, 29 Oct 2023 21:53:32 -0300 Subject: [PATCH 017/401] github action to publish the cli binaries. --- .../{publish.yml => publish-webapp.yml} | 0 .github/workflows/release-cli.yml | 39 +++++++++++++++++++ 2 files changed, 39 insertions(+) rename .github/workflows/{publish.yml => publish-webapp.yml} (100%) create mode 100644 .github/workflows/release-cli.yml diff --git a/.github/workflows/publish.yml b/.github/workflows/publish-webapp.yml similarity index 100% rename from .github/workflows/publish.yml rename to .github/workflows/publish-webapp.yml diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml new file mode 100644 index 0000000..005288f --- /dev/null +++ b/.github/workflows/release-cli.yml @@ -0,0 +1,39 @@ +name: build cli for all platforms + +on: + push: + tags: + - '*' + +permissions: + contents: write + +jobs: + make-release: + runs-on: ubuntu-latest + steps: + - uses: actions/create-release@latest + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.ref }} + release_name: ${{ github.ref }} + build-all-for-all: + runs-on: ubuntu-latest + needs: + - make-release + strategy: + matrix: + goos: [linux, freebsd, darwin, windows] + goarch: [amd64, arm64] + exclude: + - goarch: arm64 + goos: windows + steps: + - uses: actions/checkout@v3 + - uses: wangyoucao577/go-release-action@v1.40 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + goos: ${{ matrix.goos }} + goarch: ${{ matrix.goarch }} + overwrite: true From c5573410df6fb34b62b124cffc9f523ffda9e399 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sun, 29 Oct 2023 23:38:19 -0300 Subject: [PATCH 018/401] move nak-web into a separate repository. --- .github/workflows/publish-webapp.yml | 20 -- build.sbt | 14 - edit.svg | 7 - ext.svg | 1 - favicon.ico | Bin 37949 -> 0 bytes index.html | 7 - justfile | 13 - project/build.properties | 1 - project/plugins.sbt | 2 - src/main/scala/Components.scala | 476 --------------------------- src/main/scala/Main.scala | 147 --------- src/main/scala/Parser.scala | 61 ---- src/main/scala/Store.scala | 46 --- src/main/scala/Styles.scala | 9 - src/main/scala/Utils.scala | 22 -- 15 files changed, 826 deletions(-) delete mode 100644 .github/workflows/publish-webapp.yml delete mode 100644 build.sbt delete mode 100644 edit.svg delete mode 100644 ext.svg delete mode 100644 favicon.ico delete mode 100644 index.html delete mode 100644 justfile delete mode 100644 project/build.properties delete mode 100644 project/plugins.sbt delete mode 100644 src/main/scala/Components.scala delete mode 100644 src/main/scala/Main.scala delete mode 100644 src/main/scala/Parser.scala delete mode 100644 src/main/scala/Store.scala delete mode 100644 src/main/scala/Styles.scala delete mode 100644 src/main/scala/Utils.scala diff --git a/.github/workflows/publish-webapp.yml b/.github/workflows/publish-webapp.yml deleted file mode 100644 index eb2d043..0000000 --- a/.github/workflows/publish-webapp.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: build page and publish to cloudflare -on: - push: -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - with: - fetch-depth: 1 - - uses: olafurpg/setup-scala@v11 - - name: build page / compile scalajs - run: sbt fullLinkJS/esBuild - - name: publish to cloudflare - uses: cloudflare/pages-action@v1 - with: - apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} - accountId: 60325047cc7d0811c6b337717918cbc1 - projectName: nostr-army-knife - directory: . diff --git a/build.sbt b/build.sbt deleted file mode 100644 index 36ebb0b..0000000 --- a/build.sbt +++ /dev/null @@ -1,14 +0,0 @@ -enablePlugins(ScalaJSPlugin, EsbuildPlugin) - -name := "nostr-army-knife" -scalaVersion := "3.3.0-RC4" - -lazy val root = (project in file(".")) - .settings( - libraryDependencies ++= Seq( - "com.armanbilge" %%% "calico" % "0.2.0-RC2", - "com.fiatjaf" %%% "snow" % "0.0.1" - ), - scalaJSUseMainModuleInitializer := true, - scalaJSLinkerConfig ~= { _.withModuleKind(ModuleKind.CommonJSModule) } - ) diff --git a/edit.svg b/edit.svg deleted file mode 100644 index abfcd8b..0000000 --- a/edit.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/ext.svg b/ext.svg deleted file mode 100644 index 09048c0..0000000 --- a/ext.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/favicon.ico b/favicon.ico deleted file mode 100644 index 3736df307665dadda9627fd1c42ab4ecac2ce164..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 37949 zcmXtf1z42d6YlQP-6h>!(gK2XcXvs5Hwz*iBGL{1K)OYGX^{@;?rx;ve*EvfJ`V!> z@SSsZ&dfXSyfYiErXq`h`UVvQ0%6F@NojyU5a34$2pJLhcI7*L4}637kdW6x2L1&i zTSWoCqqxfHdw@Wgy{}*36jn@9;6)NoX+2L(7i&)+bN7!RA0HnMJ7)(EOLNzc94_uQ zStlZIKp-lRywnFR-|VAhKVPGd^S9@xM}xywri%>9E&mBO{P&;ZXPua%C{&goP^Q>l zg^;kqXOp1im04ZXKgD%A*w|Ld|BY){r9l2eTW*yF@6_2DN~xE#VhjMt1t4OL0-tn;(@zx>EU*oy&XKs(kRf`mBh_}je+ zkQO)v)&ZC34EMq&5hW=2s4R|b?#yq7Sc5G{n2w+Y`UvGq*!%(i6Ot907>#LsQz48HG)xg2nH<|_SS!q6%_*NV2@Y2RT@@EZJ=_=eZ~r;aOsEFA zhS5{1_E1(4q60Su&cYQYqYlUJHaddM!$id~u;nKPoY!8Un$f53pRpy7%~=M(tWba4 z5RZhLqI+20li;$LH1Gfo3O+=jAn1+CmGT^0;x(*MJR%{l{Ph8a5>R3xd6VK5#%x8R z%9@Iqcbu5z>lXMGwpn*8H0J`dRsn5(Yc`DG*b;OY!e<0#NYR~13=f;ebU3dIxa+K0 zG=vF8ceoWVNaBKlM~gifYLa?anKYly-?+PrBU6W?Xn_)wZ%H@x{KG>dp&ROtPr!@V z3Lin8NRM>S5rR6DIWP=5m;a=ISLJue8WMb?JHUS=0~|qwl*gP(z#oqnVf)7QuAXhV zZP7;Y({57yb5R9ej_T4EXOHfj7~I@TT!szyJd&eQBPKaRCCGA!j+3`%aYesO{Pvh* zR`ucL9+B2>Nj~RbY{nG!!xU~tq5!FsBP1#x4ou;pZeq!TY%$>NP~h!E&wlLRXwIJ& zm936-yr--THS2zMR7Zx0AqD%bb$-T{`~oi?wLXIIobYpM)svy5RWEz{qjN-7vx$I5 z+djS^d4x&*1f`jzEY>NXPhCM#R{VR)zaOSnELY**C-QsHh6~2gfh5q6->z{eH57~d z&FLrc(`OqTTnlI8{EWDIB)fqX{=?cpoV1>`rOilKj_DWb5g zX#pRKbcZy-&C>Z(@yC9q7a|EPql(=2(^pdPq4$@QJlIE5o1K>KP)VSD$oqU>VoI8FP>}O%ihD zaS(5VHy-B8F2&*-)N=z<%5s1IxwT7r*Kp`e{JBED*aRF-H#!{{TY?9}7c)IKD%NK4 zIIx&0(m^pMeOp_vG@S*oK347uA@T&t;%0mQ#TmLZ?m$za3#vak}oZ6%&f%BP(61 zFVG3~&)yXt4m2Db;ric4g%hxAo3Wkj1=(|nj8n4)Z~pYxvI+3yrp2>{P)T_OA+_Z@4C`H4+5zhY0LW%UxbRdh07R*~Xw|fDu#jH`zk`#A; z=OtYdrGom_yn~2Q>NI%7fTqs8#l?j=yw3l?Vg|Tpj%^%F;renAs{X=qXh2lYX-GoP z=ty$EhnGCH_t4z)?#>NTX~iLISIj>L%5|opisjSLG~iHg2D&9X^AzD=2-ly(6$Mdm z9KCCRh()Z#v%_>5@XvZ{r}VxiM|;JsK7t+jS%viXuWpdekRm#34WV`_j6}Ze4Us)Z zSCoE*;@9KQF%W6!v5ct_V0|BY?$kgWU*wuMeo6BC9L{;_xs#-iS5ZzBwA1fz@mv^h29 zS6`jjF;PV=W%GU<+~y$VN{uUz3rdRN06F(SMLFu*_8&dE(2E1C(mHV_jl3O;M98`F zVb+_;6j+qEfepmRviWrdg zNz5~25;>R^WFt%s5OF+zC`DmeeIrOiqYJ z7b-OZ34qdE`}db z+xYKnzB1{QBlu)~`lmjFOl;Pqr?=yM85|;}Y@Y^qWQ!1XBI{tpBsKZfhd&9`8R>n& z+ck96cP~aW2a@fc~|o9X z{O$H%uSEx6t)8}^IjCexg^IX%mFNklvs8tVe4WeiapIVsxJw=;3Z2ZC=>0?oqS0SQ zVbdaTlvhCv$rROp^FRl#UX%~e6x(8} z@j%?;y*ymqIL@^&$bZ+Lq_Xjetoz%nJX#hP$03)@jg(GHS!)(NJvVsL~~) zjsK}(aT|zp-#{G(U?&-6KCOhk_m;VEUd9hqBc5AR6Z1z^Moo$Gu+;cN*t!z51jG z3c^vgtcuq?`&a7M1B8UWmkU4paia?SKsBd(HPVJkzJzqXVTqDiv(|$m39xw9UkRJI zdJteCuCvh0nJ%3Mj?i)~c)V^o4J2{8PZ_WQrrV&LZzmg1csm<)f2xC7qNQk#Djd5g zduqi&rgYuAhX+5fWK7Lnk&VTf#|voEv1O9zK}qOU(&}~hFU8-0kVDXCBSdw#tlnO$ z)4TOgtZ17iRveBL;sFd-x)T_}1#mzSX6t44^Rk|C8`k=XB!V9J2|z@yWfDO}`9$^`5< zsTzH@x#R}Qu)eUW77!uTfVE{~CTb`Gj1?A40PHMrWN7m_DzUm0xsGb8N9b;zMIyrl zer{oTV3FL|@o?Wp*F{V?;C}w}l3U}2tQ>)%xHsIxrnt}S*VqX{fj!;+rv|2CGtL@_Zh{|L^1~M3L$@ zPrOsx%FJNrLW}JMtI%w)TESG;*KG9E9jlAdZZdcBou?zU<#G7{8`I(Hkmi!eyq^s} zh!r^1;62Nc8N%(qf{=M0n9q3HKm3_Aaul?4o>Xzd8X!ujX=(f5;`_BQ}zzWmHF&PjJU)P2Ee`<%WzwV z7#Br@czW3q%g+m+J~)?G>|Ttt`u+Hs;UpRqzTt6-)Ic8*6!bpV!G5Tn;aPhD{%kF(~}#Mx)s zw&j(PCqCuxixq6YuX>ltbP*zZWE5qOTJMYr(E#GudUbrzgV|64aD z@2HI7d&nD627QXOs@XA*W~N3~{ZTX5|KC$K{G{XZ@7x5owA zxCfp2m$%2(i!F`rIMe^#Qz`)J5X)2-OsF>fz}&&bK!x<%$bP+JUh`JV zTxF?her{6Pvb=--o8rO2TzLr3+WI@_h^;Y4Q+|%`9WwJ9x(%lDU9WJ#=~^bihY*tw z?kP_}->WhXf1c2$_i8@Vo`0q_`spXoq|CX=>b8)*@73zb2r4NqAx#2Vff*@%(RCtI z(a}}Ve?m$#2x7tkj-4KVQWOFjy8PDY&9~J4H$E^VfW9=~@Fh`=uDhwpE_zg({&R#W zr|3v1knAcp_qL+<_eJLBOI@{O`7i}fVV2^I3yxhf4V&Rvm?9heOi7tQ@nK!B=K^(X z&H0?6!0$qGa*9#;Xf`nS>kcalw7RM;pWEZ?8-f^%uw~j%1BYIIduO4AtumZ~aJ){)V@>1R8VLy>bfQ3^mO&O7mdtGJ@;F6`>M_5we6=T-TO2vGO~cNP5h>ZqjR*nXKRle@ zZG>>{VSc{7C|+*+Y0I#W$cLxkBh;|ULf$;X%@M?hpk%fbI&kq}?7D~^o)k%xA2i}d znInh%y2J1UDa@gsdmyXIf-R!iT$aGF#qv~_GJbei$$B`wD~^PRiIuhJ#}DMr)v@QN zhiEeI4cPAG#LSG0r6uj(zkiQbJM%i;Zw@p!x29!zYq>qdxl3*sRuURvlD-c)Qrw)% zR+(fcyw^DVCiBK%suCembhW>L2f+|}Gl7ekB=LHaZ#7+@n{P$ul-izOtRz{nX81|` zF(P}q3Z-KO_rE8n3wbc4_5@QnH<8BnNqqdcO+D6X#1Ru4%gn*ClWv(njT^0|t=%_M zX*l2LK-87*){cBqZfBwI%#g>yYprRPIaI(3t^xBHbG+A@qM18#7rcnF038c%tYw5L z+bW{b$iI4kl+Sgi<=Z8ee)FgVvnWz;EfT`jZTDKo`M0AYmr8YY2_3Z@tgNxoc#N1r zcw8`xS~VRV8GU^cQBl#r+x1AtrC&+Vd!BPBKTn;AKrA|(fk12a{pvySJ2Fdp;DX@< zcYq7_ZqN_wsz;!N$k&=%R3rnT5q&}eooC2>ehFzUx4RiM1CIY`8h+rIj$YS@U^hLhKTsW70>O9)OzF1@%mbgI|_iGUvvw!h+shsq^+!@)t zKdfeCS6+6ETs%%-&TthZ%MN+IQ_AF(pVtFD+vF|ekwAM2LOj-eGZ3QE&>LE)Oc9;o zOg@yWs-cM>J-+#co*v)On3w?&Ew-3zM5cJl>br@BrR5>Tk%`J`N@ZzjgjtgSc5gUm zsm=cQSmvq`>&+#Pv5$|p=YRSy#)`e)>lm?ina{p>GopxhV|)|qK=H+PQvHB*=CWq8 zmSv04_qCkdHtZFcByPZG=Ny;xpqszE%!a>*hlVx{SMLc`O1MK*%Ks{un>&)q>%5!luv41jmC8I-W) zZ9eO7?O}W=H!Dh00AP-h)F7p|&Y;rBV}#FFq~BWz*sQ3ul)1S6+0M=`JK&^QVGxUUv@FctIxVVDJ4bP(9qE<1GqlMyui1d9k+!iwH40$RQSOVvhEh(YIl4c9UkMrG;x< zrtm-#kL~LYLqOAO_isF@1vq;t6L_2)Qi6gDEtqrUXHZIqg`?nM`gN3)?5^d8g+3tw zTSvs1$JN6nV~LECUbU4DQ7XO$hYw_pYXTicii!)A&VxYW!H;9$cWyGAeHRSGV0Gtn z%L#w?i~ZroeVtp21vC?0ybm3!zKkEaaqgSW@APJ>SXubihzY;2sw&~&;6O4J=W28& ziW)=vS+KWYI}s@r=U;z-D^MBKk3XF+OWlO$9{j)}7FFayR%d5ry}Yiw=)?c~Q79`b+lngm z=g;4OZEk!R^C|FBCu@tVp-`*O@1?7RfMiJ$a@BAa*O5m zq;CKKx8Q=H+1X^vDB?M)-j$4>9v=fQE-$e%*v}rG0I#EXtNEJVT3TE21Sh|3fjLCa z^stuD+X5Si0F(^35T)kZeHj5VPmtgzU>U%`o_%|$e;9vTZF|0B=Hx7CYkT~%ySTWR z_`_XKS65c9tCO-TaE*QqXYCO7rPU7z8i`sJ1O%v-*Wan3;{1Q@G0Eu0?wWE!^Yimo zK-AleW`7#{EK%^O1RIeN@%Z?_z&?>XtYl;E`{d zZuOI`mnH-RD9{+Ieg^^7JS1~xNdn#&DVj)6buv8}TSrGS2U&dJZK6KD(&TV=5>%F% zjE_^vEAO76UpGxi0A0_sMvo0Ck!dtf>}Bn;%j`=;M1*82_P>+m@^W}@+*^b*-F)NM z0|L91RafhB;XY|8t(MDKYtd;-kc~PwVa-d|R#qNIu|zU^N^z6^!o2PNt3qZ2 zjE!@`lJ-hyz_w0Kf5|N^GKEy6qiU%c86$yP-WkmaIBRwlR5CVRrWVDqcW|(?w^vw` zv-Bo7a{x%Pnq#K0vQOOTsLH|NA)Wy?6B*{ANvDgQoqF9}nMBu^7P4rC&f%AOgyrRB zEp2Ubjn@3)VumWGJWDl9wp5_W(O3Lys>Xn-;*xF>ps9G7psVZa{r4)3fF~|*))<)8 z8CrwFh5Xu0tVc5YD)j3y-xFa*0=ZIXPG|nREWT>m>l%}lIts=dEDzV$>ZYB+qC=_d zn!IIurMFuh0XNRe+<*omr>T}G9_3qWP>GL^ucoHv+KbcYbvUE0uP^W6!80;4($LUA zcQAK)4!DRp6d{9R83i^#8KPupXxQBXIdEE=T3%N6@@hPBlOqyOVws$p%HVS#1RNKD zorwuIE^1q6=i<`R=!68!A{#UoAo{vNCEM0#i84W1t3d0>rTv<4H^*n?_iBZnAX#xq ziL7oMyWvkJKR98&0kR;Ra5eKEE5!Qx`c@x5a)12u?KKdn0j6o^=m>=S=hRffH*enT z9Up7+eG3H4&IPj*%23#-Fldb785N{GX$QX(7WN{LLxG1>n0A^B>ZP*W>!X?@62^Yi zTD7U4|NZ~+%xhrG*V|w@%v4MWf8*8H!|Mi3E!oBSFx!SrEsq>lcUGB6O-uxMhXABX z;UBh=t560DBS;E$!q9L0@S26W3c(Y|3gI6{nSWE6+{2JYkH#CvtFN?!K+!Q097acQMqV*adF5pQM8zD#ul*s zp8wP9=8n4cWqAcaa=V#&$Q2FnJRK7a!Y@*0dUIC|^;8g7~S0 z<;H2XBZpkrn~7v-%adrp#xJE8w;LT23O#`r|GV;LE$G!)<3#wuVL?DCQd(K5P3Xb| zTsQZuFyN%?e|1f@`BN49w|QEQH6-XFfBxh*X0!xc86a8eeAz^=2NpNYP!n${q~U*` zm$>?L;?q*H7m0@q4_tV{ek0)4BA3HFfBxjPR1k7d=2g?BVp{_xVDaJgY3EBvVGY1uoA`dGoi-ri!NU}EtUw7(DclIS~@xxdqs%{RV1#z00XHr9EV7w0{+$VoWsk@TjQ~-qTQnp zrxgm^PH$hOCc_uICgyk`AO=r^Z-jIG517Nc%#^qR*Voq8R(EQXKF(OD}^gOeNb?C0Em zJ(C{u2}q-|Ud6tv9YGaF&0m2CCYk)PH8Y6E9Ke1Jo1Fg! z1=`W6lz$esQR(LXBf~5~2PZ1)0A2|=6;1gFgM-C%sRH*dMK&_zdO2i=clZ2HL9$v? zS9bvLmpkM(eVMMYd;A&t0j%dV@f=5eSv6kyE&9N0NH6xG%yJw4ozx=*_b zisc#nam&d4Wvm+I3nZPmyQvVs7byeJLjSCbfXLo{W(3^S&0ATX8jyJ|9E)o6%8+>Z z_$H>Odr4i|aMBK?504%lt613;uDCQ*fJXyvPJm+I-x~K=*GJn*9aaFmwsv8vb!?UG>ju12&55?6AD!{)wfoOGshl zSGD9R<43!Hbj)|<;-;pQqyo-cJy>E^0}15Z%H+a1Du0HCGTsNhizQ?){4S+p)vKU7 zfAWe3EiLap7p=>$MR|k+*qsE@_a+@1%NxDG`l+#8UF=~}H|VqAXK#Wqe-W*jR%zID zaZp}eXE#-xp7;q9z^tRW+D`y}oB>sex`sw~G-K}X(vsYm0a``gqQ8KYqpPc{RK468 zB6c`asVS7rbv9JcLG^MGF8AdtFQY2CVHcy=oJ{Mez=>5d;L=xZ`%2o=>~0l6uc7VX zq_ypKBNlL3Qtup~n8@|rK*A(}`1XLC z{^QK2r>EByfE*cdIVzl3Kl)DbA1Zng03yYW53Yj4Xq;`>+Qtd$d+@=$OGMi)YEkgQ z+>1l81IoLAMEiZPx;QIWe_QbAbw`!o7YzPP@&mhaD!Qrx^BHA6=`f}ZB{=(6IPN@0Vg95!cwUSwIL?j=Y&08T+FNZZ%C6BuM zUm5O~i61_Gq!)XbAM-t|Y{K=vq^tmHg4M?zfybxQ5P?45Vc6-7rluy|l*4MkX?r@0 zUxJx{>%&CHE0a>P&}fO(9vIpBeiSE#CEeVoZ=%DQE&W+`=CP>JdgRUXL+6X3-Hj{( zfH<6OXAoAM3N=xu98cg^pt9k1>RtkZ&ZYC|C%9-M0H*$c^_s?_@mtB-ewn>%QhJlv zjoFQ23GOg4=#53Q=2EeL#A3#c#zuw*lJ0*^O=&<(NfclMa1S_&D1bd0Hz&cgPJp!a zdVV){vXA^;US8aM{R@E5R<2X|)=WwSA|gB~Fh~KMy&vdj3~iy1;Yuu z@UK>te<6Vdf{CWON_1n?F5Ho5@Q~@*S;`9|F5>US#Sx+b|7x5T`2#zOMwc=*jE%`V zpN?Icw^I$z{0I8qC+6nnrYD#?&)0p{HSz0|*M3cG@ox@Zd_O}nF0b|CJYw&W?4w3EEC;x_dHmn;Q`}@NUhS zoYVlMC?K^~RdKv~_fEr_XBiMyZ=Y{E6GyBSuhh`DFYo3W9dxHV=;|SPC&mh0x8|4#%+NL2q$p@USwcZDK{*Mornyyn3V`Xwj(*4O-bbBYL!HbnC`d z)Ym7cuj3WZ`iBg#!^AB83V;p1N9nJ4TKi2k zS2aWdjfvuu_uf}mwM9*N^!JGN_iy3$^h`>(pR^p%dqH?z6oKVv@f z&kz9-Dur67&o(^dO4+!sqADjGa#SlXT7y4})CL!btX0Stg#wX=X|3U#VImSl1Z*YA zrUQ!VfB*d_6E0xn;=&2U2B|i8F%Y^dzMB}df9^52mDk=j6oqgn0W)jd3eJzrE*P$y zXXsp$ZC=c-2xsI}s@qhL!D2qkUPMhR{;d1^`z(^ARqgEnd>`P%Uc-O-@cw>}7ARDR zs^4Iq!tEB&`*0`wfT+Xe*hxb*L#ILqi0^i=YAA1oxG-*G55RwCXFUMK?4M>3PzP$K z(z3F52P0zG$Th)F{uI7z;FZA35#QxU0XQ2lxj!7xtH+l3p78SF@P)$RX^(%+!B!FR z2K1-wF7WYu;1zg`g0BD09q+Wio&+es_f9A}W@LPP1hNENta~Cb&N*h=@C4y()p3T~ zLeM0Tsb$K^DF^)AgK*$P)C(*=f z#lX6x#H(ScW`PU9;80>@Un~(&fs+-hgL7`lr%uMCaSGX2hA2?>d>2t}I-C(w&& zGA#E>N=@~aL+V~xF#$3VkeaWquEwXPB!RmE%)=`nkaa8*@(cM)aZSfF`;s#H|614) z_GCMCjrGPe9xF5t-1(O2ISSEvFC2k4a$e&Lj{Uhr6<1G@ zu1ehye8GK8SM5dF-i2Qw=61~p9>asW3eF- zr@%WRaUgsx`JQ+8w=k#f_8oSbMi|_yZWJY0XW*3G<|y=ev!B!(rGXCiV;i~~N{SI9 znkqH!eMbo=k`o1ftx2%mtn23m?;2Ezh49R-a-lDhZlU?y){JK;4H<9fKL%O;Z(lm- zi1Y5&emoN=N63{89|INc1=0agf)7TXrt_34dadas(&e)G z5uXt(lcD@;4EbbEQ-;Z*vkKp?VHma{{Zmk+C=Y7N_SbiSoGhP22g)Zno?C=p3tV#% zukM=VRRB3v_o$9r3lf4}(2i8M@~^%CgSyD(5^(Y`LkHa{J(u5z4?uafAb~bpUrQXhpXV*08h$=uTta-4}Ne9NbQJ9-tWas zH2A=JVcyf*^|qbFEckGy@6V7yWBH9PMxS(!J^trx#TWQXp1>`R^B%bWlQZeV&A9>s^@Yzx) z#vBLiEZ6cTR>pdXzp$XdglrG55(`s|;!02(VpOw2W4VJX6_Ar4bST)-sYfW{)MtWc zstsRbqj@)BdL|N!zi7g-WeGfRYaIPV5|B+CrV+x0=sX~@)h>Vl)LXM$i5Do)YWP4MDkxY2RUkZf)$hyZ9uJ z1LVxI!GF4Rp?s}#{N56cx<5bngoC2x5HpjMROZnnnE?zp1UBV6fOPv>D}vZyr_j9Q z1$wV`7#6N5fN&e0K3*3vN@vA?8Bj559o|4L;Y0F%x8{=bk=Ig*bMP!IXvq&iNKI zWJ?sM>YcM0sWJeD+5{~A*fa!0M~2j?$!R&4#D!W)Gw>*!J@i_}NSwF_ed=fvN)#>0 zlLr)QPt!w%(O^NFa{#pMZ=zq!j&G)d|MSO}lm z?iR{<<2T?X;<{mfZMQ#3W|PR6d~ay@R$kqaITpCX^DQL>2leyb;UVd{ z7ho`;1pw%((m)3VP=J6G^@GfS;Vd8poSZ%kLYB<%LNtS}+B_d}%F ztst%h#vNgcNN?6Qr zfu=t3pCc5I94^%ct9lk_4EP8Kf{Zb2+x>jRB{*3%*)9xkzCYa2iywQ!uIZa^s1)bnSICg1KYyBrAAnp-k zeIRG#YhdyDf!&_%?QOTq5dj{*i;0`${pDq`;I=xf6R>vZ|9jx!#dq-s|M9*&1icE+ zmD4o`2-EvsRxmNsnMYRAEE_ZPx~$ke&SpRP6VW?j^aB+Tpya;Fl@?^p_Io*6#xR1Dq2IrKL9hQBDRN55#9-zVj=HmO+%i32;9W)Ag{?~W@UK|fPTVJ z0~3ANrR62%u}G&z=C-)lB{892;nSm%P{cV5SXb*Y+7J~}+15!{ZxK~?_Yy;{Zw`e~ z;s4-*N+C?)#02t?aC;eg&^Bxl7LC}4w3h8Ly3T5PhpOY?8G;TJN!WvOUqJ<50lSl} zDVw!`Yr)4N&mYr_`=yDD@vj|dlcG-GU#DW`=Sx}p!GNkkT}UGjRg?CD)ZicQxafI_ zwJwh~&YBEO{-THwQcbEkeVCMm=2G!hQtdLttprxJCJ0%Q<9WA9r4~Qhe0Uo@uAYdwW#aY8oA; z{2+#mpWf#K^2o^AdrV8W6sH+f`9maEBd7Fn|#`k@P|zLKWj% zpn_U#$4Bm=yU=9(#BJ=?%e)xkOuUbKnYg;S!Hbsi>;pz~#gHALQR$2qq>FLyqoBRTyxvF>*} zy)<=yJ&&|l*q7s%ol9C32h}$uU!i(@Ib<#7utU)J=4%hZ1NsdVPW2Xya4iz+F) zIM<)uByf?{-Wa*`2nL2V%MFEpc}{VR-kXtP1lt`Db7DM$Y=UdzIh_~!4rUT0ou5I% z0S|ZAD+W7ljnTWA3%I<0*8gi=HaZ_U{hDMq99mkH;C=GabBd9LQbXEFwg!4e%yt11 zIYXxK5!;&a8u6$8us(}*ptu$xGWJy0>ikmlu@Dbb_w3?+Gs}D4r`)=SoUl~3Uips$ z7!8uhQT+-k`g{M#X593-X|Yx2apj-cm)FC5YE*dR!f_#Ogcv&D`LmtRUawPBMI7eE@Zu_<^to2@6Iya(+ho_=oWkJZSOSP4;O=2i*tg^`m>NMjUr#6y|h+axPOj{?4 z<$dT^`Q+#wk6D;IG_(XR8-{0lLun43paT(s%AsUMi0kgnx1&BCMdh$DIjwIXOn8W= zr{@pigkey+7W}V|yTK6drQfTEdpk+@gx8;NK_Xg>gs;o?PO($2K-b{15KxZ zW?G}qKtfw(T@MrUv)ZDsPMgY0)^U zRyi`=wBg@Emk1k6UCzdi303vjiPH1C`HD-#XZx&sma$F4s zpgb<5eiJ~iz#YJXuSrSfTOTnCHjwiRNkAFT0<}5_pRF>VESreq)fXVNdHuxlin#bF ze>mATE?fc!YUuLo+j9Us=Psk8Vie2UK%rOfYg#-8YPRMic^G8P4*f`&CzUK0WQ1Vk zvW1}fZlA6L5cD?Po8#Jsae%iFV-2!`%a1OdTgTzw_O9P5-muAJ3t_cZfLx%){!Vl^ z2%$px&B+$y6-BF$SkVi6=CSNsqclTUnC>)B+;=JU_^YgF^o1t)tfs6XDvXSr zVl&hUTMxFCd(v@;xX_O|h&=?I0wGt!65Evk- zp7P@IcXVy~6C-khWGboI!7|f>lK48HT@nJ3zZi6&h>8bbc(UWV(#5SCg^?I1cv?$o znRNlgH1#48^@F&RY`h;Xq|Iq-8x;F_QeM+Fpzv_?z_Ufr>yB}7nE!ifHby1S@wp7V zs+UeaHrxTbjkUG=l(GP~&xz6Bf+B0+VugT=8f6fe3g=Ae#sIT#eCzf{E8A|usoSS{ zQt)w1&X^tvMH^Q#C?`FXLh1Sw;MV|p~?A)?n{EOh0 zBzkO@M-#>DHG6Z%&bq_o?UpLK!33HyL27-Y&h3D-%ls#Kg zV_Y@ff^TsZM?W^xzNPTEOl;^y`taJp#Kv2YW6bp?7N!m^0K#|%$i|9YuG6TIJ@62) zF*nd){z~_%5o04p>Ee!0cwZSx%imc|PA;ntR0k zjfp+)yG>M&KKhcK)S8x=^u$Di8dEPI(#k6<$H%xT+&`r?4cB|JkNqjh7nw; ztSk~ZOSyMQ@?1H*?y2$aqk@uRH=3$Q-;bRSrXLPHSurgUX39`!W@iVSnwDi`Wh2YJ zwCo*&knR5<7nx;91GTq>XAZRg-hiUz0rUPL!o_ampD&3i;nN@}YouSkB&hpG?P{na zsz?}t9>U%#ONcn2odIq6)~~9o{`>48c>Ax&4Q~J|n+p~l!cWg?UGABlP`iG+xjkA6 zyoiY@N;vjx7WA9n|7G+i?bsNGC!Tu%8yrUUgs7IGxA>fzoV4GVX#c&mDBid)P?PJD zdlgZ76DNjVsm0|jTnIzpx0`{DbG$QanUEkC83so7CwTs>i?HC2Fw;9#yf$u^HKg-S zThhPZ?#_C!`~b6SM<`iqfaH*IbO8&s{&ZU3@2n(y{*>A$A%BNYrlLDtdx?rFbP}HI zmEbC80}iEW>;T_*A|3vB6(+S!L-I=158TW(5yk8ElC-W?-?b_j&`Y zW6zdz)r&{`3@W*}Qvv6_^2r(R7r!{yiOD#G3G84z8{Sdmf8TpDW7Vy)nkw$ihVrOG z%zn@4P=^yoPsqcBtE<#ea)A zj~s~7rw9fnDq2rkVxJy1SCIzxJr5GpeQ@3W|17||yR=vkAN;FxqF8$3p8dHTt66^v z!FsqpS~7A+E6hV~)MFzH`D15g%`8UpCGjVvnUW8t$Rq8AA`DC0^m}J#9s#S)_{vJv zyDRn5J>@S+Nd;zfzd}n{ASl#QZ0O1mpd%l2gyZeB`1YtNj+MC&Kt1w>byrt_?YZ^W zq?y5&fW_3wc{9-YWS_-g?PZ+P3k!c@2w1gp$|(s-rb+`yV29Ub2YOyHF;o)BIIOhE zP*y*5_zEH0kbB*ni@6M=czDKxh4Vaarh_o?M5fld7<;o9!lBEpleVt8tcT{u500IH+ zdsLb3xA>kW_!7wN7qKySlYFaG?4rhgN2C9|Def+pZ-XQS)Gw_+CKFi>uEywlq;qlK zvwWf@#uE8WT!H1u!I*4c{|{n1n!?epjZ`=z9{!D8}O<&`Sg*@AKx-48drYO zNKxYZKmGCjKEHuE_!diiB3aW(cjCz}K={3(-%h^mPSG8h?sB0MZVDcbRTbslnq?zo zV$Q>z3I?_IbVZl2^H>ab!lN^bnCTQ{T=-Jn(jby!Rg{%!ko-gTZZ-D7RWmZGC}Z1} z7Ow(6aD`mZ{oQV=dGCU?wv4lV=)tA4E5N1W{njj*3KIRtWQzr?FBvH#Zj9OAeS9N5 z3dk|E#ON(Fii|IP5pP2eVjlwE>s07{Ff`0M7?1otIy!Uh4~&)Go^NWS(_{SlAWcOb zuJ-Z%s5$Te%`|4;TI6e5Uhvm;;U=s*J{KLr=DXVCS{C-a-UgETD|9C&j|po?f4D8b z=p)51&1(=a6RlTkf$;Ps=|ZedEi7z!quz0G;m-GIVkU#39F-v)n}y6{iDp#rRASq$ zXPrw`&;halY{4N}rwk^&NaVumPB04kJRCC`N^f*8}AQZXKa_tB&$Jb zU8b34YPR=q#ac|CmHE*_KNgk+b--b=BmS_-z7IzVWXs^jRI>&>389nTmxUeVvFYh$ zyXSyJQTPK25qah#ty#_|k3^7>idBEr_Ku%DmBUa=Jxs%~`N8V~QE)AfF& zFtlUwk+>hy;0+Xk&=v zU_yb=#V||q+tbxkNk104)_}<3nZ;~61?G2hU1-9RB>kIXBFjIEbqKqQ#-ao4L*7A` zD|*P^-QS!ScEG7OHh=)&PbnZ-i?)_i+NYr~33H!3gusY8n3 zcV>xe$?M>OM(Y^>1%pA8F&=P$3ecWerQV-0Bk-jv{TtTwR&XWI#mQ*Z3Dli!^KBDs zBkt5{XD73xHAXsO={AmIp^Q$Pi0h^?YUcT;ywMRM<#{s(sD`$DtMZ_}IySG+ys4bE zO(|c0HF*w29gPl%{duW)?X~QU4v_Xyt||(g*8Rgbb{!MGhs2bviFQljof44(|ObFP14J>TAdPw zunM^sZBCz+se~6wJUoLoVm{`<0o;UgS*@zzT@Glz5BA3~X@=&29^K&JjK$9dd){Y+ ztQta8V)M2q%yVo><4cR#39u%FyM!xOCLsdP=OyGHv8ZI31vWIkif+8xa5B2ztlV3w z-3;4YFl6&WsMv3HY0zgDlJ!Em82M%j?l!#b3fs(1#Zs}}pR0ai%gDXcWhMYyM2acrbh&z*QD%N22h_c$GV8 zZGN)e$3eDV3ASra-NX%F@xNxkmuR)Mv3hGx{Bs#|uRegb1oGUcBNIYvX}0Dh!iaQA zv{+m2iq_W)7WyZ~rn)DhM0`5^AVrh?SK%8RYzTv?t=|-?^N1n>A61Fjzeird*Ff!h z%v8)r-%$Tfe>kIiVhVFI0|;C;hPEpfK9Yp@>-Fg-6WIt>Ziad4xs?oJ8GIH);s%5O zDk{#jT#3E!X=vM3J9$HJ)FFi_fz08(ZOj@=;AZ(QEK1tA&0%W`W1sYcp|x27p=DUb%w{#a&w*Y-9d$SC{F^yzV9>|MV2dvGy272 zrN@$q+7Ioup4LjP`445x%^t7C_9Ac^yJsX{NuU1nJ>?4LI3KdX!Ob2c%8?T5j0gxr zMb^R+_l_}){}pRcbD3)B=PwkH5xd}ZkmzM`;Ztf;W)?roRa-7Nl6YG`!XFbrsDWe$ zn#q6boWNw|II$2sTWD~z@n|%zoE`kayZELleWKX47oikJ4t;>TJBDG2gG4)LXHLKt3fe&wCzE}J{Y~-N z*qA<#H(K)z)BabvcfMp4&6fZHN=r$SmP_*AJOsb3+gcsaSVFx**M09Y18VW**B|H_ zq}`UEXl8(&S%~q>DhBoebX7_{&Qk^4Oa~J<0)_DCgRwrDawgWTg<^h-%dU|bzEC8Y z{bjNbu5F~R5IfuIGElI+eWv+=>BV!qJV9Hdvmn(^=8{F}Ld60{peVcoBvXx0gsSa8 zz(M+1UXJxS$4=C-+53Dog6Yq6A1Ysc` z294-VLpl=b$SO`%&9(`Hj@wL60Ld7}Plj{w!9CF|CQX$42M2#Dr;Dur`&aY+)~fd# zrM}ywCR8s9OM*yI$q&lAGhu2lE8eiCgf5eRS zEtfrzU{IQQeyo9>fekYSC|hnX54DLeUa6|8f-8^bDN~@Lq7KgsSsg962OeM>6FDD< zBSYDbKAPyeZg0rFBNx`_*G~6$o|{&n#YO1ql1jX9^vTK1nULA94`E&=j)Ml#%%skU zAo=AA;@3Jnc82DQ^K8t4WscgSD`usJVhOTw=Fu@lQO>N=DmdUay zxFaQA+21mp03|sV78ZpB_P{Bwsu;B3&Q2)+JZvvC@`5ayqn@W^W?>N>8|&(kz$PupT+8_LL&t}7a!%1J~Hw5-~ z6KDB5ulbMY`fdOH{vMzIH|L9lO$oV{URI{=9-)<_)lC7~-1zwK_8=r;4pS+WrMRRa zi`BI?Sy@?1Vc}Hp`p(YI0M*X|$w8jA9*}WqzmoFfRBQgb1$##MTm6xekUqV; z_M(W5zsi50#inX#EnH3^tPBvMf&8oxqi8;a z(6EXjl==bdGcSO!($oK<-lq7al-a5$Vn)x^-LgnIa%b<%} z@PA(^M8aqUbl(o%%snSyN%$}~Jvc_-d~$b4ZK(4%@lGNKiI4)pJ0FdRK2r$-afhpg zD#eNWicpj_Up#O0IMgE6N?ctv^*c>oI?5Bh4+pD;q|mI~awP3;m9TXgka#odQtxX) z6nNf8U0f->Y*cp@-A;s+Wh?$0msAx0zYF zsuD9fT>qFo3jNx-_v|n8XF&QHXmZ$WX=cXmy#9u@=}2PGf(y_}>ss9AxQSL*SL@cJ zsidpeg-uk%ZbRjNBh+O*-932Nm4uM@(P`K75@ z!CVFGm@f9`iTRz^`%Z*j%E`)_EVl(lk_#vO{{8f1fQeeSAa_ffO3TCZBU3W;35Gmw zgJH7U&XgZ2NLJ}EQ`68?3!;RDhiAHM4)1~QJmV2PFB<-Zvn(7|3kXjO-sdI*pWN#3 znkmcQ>K%JGyY5bIu1}kqzH^$fjl#M!whyPkmjxiL2ahEX}i4>tl3txrX zoL3m{7>Ws(8lcJuJ6vdt2V&wcGUQ&JDwp#kz^H4q*1_oJ;y(AH8c>u0)WFEZ^d!Zp zu8srR&s#O0_#PjhS461_KJbu`kp0qgHXwZcL}G8;{t?S1j`;F9`U^67mh(n?L-JYQ5u-Ue_;I~xtSQGIpyV$THSiY3p_r&IH zz1OQhQ>(659!$ogA%srFdtrro@%3$waQ-UT3f$QTq1rHB=*`95@YAaRy^);T48}`d z-uU9;Vt4;KzPm#h0J`Z!Ohr{LbTcl1o}|{L5~9s;u^`mkr6uX>iac5nMk7cZD~DNs*Ng89zlYwL@TqzS{y8NqdleAi0MEH>#s*IM3cUF^-c z&rP50!6gh?AfnZa9sW`}hl%YMn;}G|K_E>qp&Wsy1^q-ItgnCTwuM2-$QS_}0MeXT z+v*b(XhHUsG|6lr$dm{>!Xd-$V7dHTcBa)vM={7>GRV#3YpMEDk&^{DjUV^v~XpWsG!@wG<{De z6cSk4`04E%624Ikgab`7PmtROQw1;YF!AJo&eo}i%;Vd)3|G=TEGmW0?k=Z^)Jyu$ zo5!A4W(O$~EkEM417=tKM;gTm(K?FlmD*k`vud`Vq2|FuG6+h9R1~qd830Zp=dh<9mknicHi?#}n-)1i`V8U%-Ji^6-t8)6x=wzPuNkM9GIQNqu5vR$2b zV~BntDc8|JAT(|J^sJchPf~)PLhSHB$%-ymEQ^foxzu=%)LF#TxK@cN%2y(2Ev>}L zQBmR7vny=;!bu<(-A|rv844Z=xd$F%%s*$8adOfII#A)+GEs$SOu<;Iq&P9r#J+1R z{i~Cj+Lacw@pPiRi3;oMgjZxErr}x!1Xq?-D;|{ir6>@q`?z!svOgA;945cvlakP8 z8{Q&QQjL#dXK($nRdUl__|1r|KjEKBdPKiM#)Q`2 z{7}dh$h3~KqPy#SLiLNBvmSIR3N5HJ&jaLSi1iY-hIAsT0v&7pbmfdMJ)r%pPh0b+6 z1D21KE~mYdPE0F$&Ve$T2&Xc2bPxz@)vpNQLYM=PGh8nDKZ2PCFkh$U^wJw=>!fHM zz|QZY7y=(3Kf(>`a>NMS9l6Rha%L(OB@~hmvxm3VKmx7>QEzK(6L-o1>v;GD2n8Iq zfHU88-{|3Fe0UI(PDf4qJ$C%T#DeaprG@Hfflk*rWE{|lQKt;QM?)ZcGB$YYOITYj z2|(imWZo)dTF8{0#AhTzvBL|Kilg^;9rbw&!$U0e5Hc6!8%**-=dbN$5c9le^Dm>p zrxe!~1u56vseCE(HUGbbE^_1O8OG z3b<)aQSqT%mX;|S5Yc@H9$|N_Y%6(h6d;fXXj8EO(9yE2^+ffos4NDnfVZgCcj*}F z7dYbP{!?kP!Mne>hJz!kbnH*~f`LDTb;n-)C-gC!QAh8!H5^3OtIKp!TwY#25y;&Q z%f=hgz<9RMP%?xMlrC~j%#l5gJ)QMfFf51n<*7I z0`&AmK%>5<09!MOcG!NNAJtvtI|!DsSzoYf(l(v;d0hS}08FP?pct7FKSWqnE-OsE;7TygKkjJK z;YdZmO_PhR&7`)%dz*LY-P$Vd8`*O(B0Q{L$(wHrb)Ji1vukOVpKc?ANkIgg!~OwS zo>D5~Mj$pdm+oW2!5)!htG-+ux?fCO+d^EI-oa@fTAJ#B_fQwz~*V*~A0ddeI( ztCe8U=DM?hQ(s1fJJZ2jst62;DSX3~Pv?B2pb>K1FboGu+tFri3VeCk7RuiO#+iE8 zpWp^>m+>dXcXxMlgxDn_`^Kk(d>ozOt}FT%QNq?x-@PMBh|OT?uHJr#_bK=@{EqyXUoynwgaS02Z=*aNEG=7=Pk{=@z+`}8J16{W&aQV$Ps!4H7 zGJ(#YvN0g&4I0Eo_lEtUZf$*R72IId|H3ypBCv6F-%OZ8%?X-VnQG=<#B7hZ>3fSc zG=Jn#m=gKx(TGLVWSR3i+LO$U((Z&rIKBr{Kl)#;t*&m*WyeS4K+z-w8wpg%IG~Q1 zc4hPq2d#kSzv#6|Hfi$uoA5#Hl*J4`sX{DacH|Q!glHw5ixBhxE z;KO+y$RU=v)ApH=&EnW}<33ah>&qL()h?0LaO)oYY(Cac#IO%lyC0-(6BKDJ(062= z1P?Z35{>l_S^a1*+Cj0bb2HmBn8bt7sOXicCMBn+y!rbxn<~zLNo}S3^*D$>Il#qp z(|@x)xKk>k(OtWo6Cw>~{UAA#l}!uDFt~F-(A7jr11)aA%~T`Pf*%V)m>ndYi8(eF&OMSObc*>Fr*R0;N-~V|^lYJfm&^Fuj%*+lH&zV>6GXH{IFNSYtnxaw zxVb!3_V#Y*FaM-rU{G?q*8!v$ZWLKQ`RvJeI&O5$A85LdRZMs)R8=+V_5U>drF&%N z>F?wwE)bfm#P#1*mC9=95M33*8M5$8Nbd?*hfn%T-cPC|l((00sT8@{8sY5eD|R`7 zfgxZ({ygbi@mU#g=$IaNen7fux2XQ*wV$y-+eDl`6PMoc|By1#s}ZC0{(Er|TA%-9x^RQ=hz0K~Nr$IPYFoilHyZ3WArPjqQdi%<` z>WZuBxhF%|7s^TJe`dv9J3p%w01?SPF9wN;(Y4F!vq?7VyFt699@FmzIh7S}OCtSu zM)>w_q6J0~Z|_dR>69OQr?oWIoc=sMuzoprW4_svT-X&jK&Y~bbtbzeXe)R?Z~7@< zB`FDek_pzV&C63Y<>(HcDMseG=ojnXp;PnpOKsNa+|YKXpRZEb^Azh2|M3@#VyJ`p6aLhd_?A-$^*6(-}n7z8N`fU zD}D|zTNc-BSE4~_;}rs`5KFk&xf(kXAoqRBjMytF7)TU@o-vq~Bi{}JEwR!QdI3_! z7_Mog*Kx4F)O2TQv1vT6Iv)Q$PO)bA*KhT^j}ZMb8YrIum!q(whTAe*>s|DS$gP5# z4y6=Q`TKqA+?FF|&7OYAX1oq!w+(Koo_zkhM$Y!fFotezk6iXBEPZDHh- zgnOn?LzXm^0{US~zLy3q4>vZGCLvg4Vv3qH6WyXC|J-aOn(-uM6_fzjW8J?ofJw1> z6nGE9T5sPZH#L1(`J3s;KnGPr9D0PDLD1n44)_m(?+q%E7;&w}OreOpnb>2@-&lyLW3U`MkImrdyvXMmo z8w{BH_LSt?2`aop&_i~#UkiB5$C#U$=>;9uO(Rx|o!0dIBfDTO7w2tZp3>lR96XrNPb#c8 zaH9m|%b{%+lYt(Gi$iDI6QFjMaAnuT-%udAY|@^GU(ELN3?ad|TX@obwh$|9;@ZH=|p zEwB=>rHn*s(~gb*EGu!ZV`M%kwtc=dhd-e7@R(1}^*-@N4(`Lnex8Rc9Ug2N7-gb} z&BtZZK>O(en*cZnUCvu!r|=p}7BXka<%kKT{|{P4!q`f;r3uF3DT=G-T3k=nHac)@go32;LRftWc}w#4 zGWG6}!>OXd=H|nNYRxmpm!q-{PuDnnSyc~}<4;ur4o6~uBC|6p5r$zs^3|^CY)^Ei z!DQi@f|L>zg&l0_T-5p3dVT7jhNIhlubSfE#JG0x4WkJa^8&fsgBMDPlu7|W8G#5l zbkbUrOc&XNw8YA;C+N3*C8x(fJ&IJ-oL?tJl=nT#3KEOkLP+nPF=8zPvxwc<>vZqNAU)9wz;-v{ZrfimQ{x#$L{ypdLLL&Y^SR)ReAj3rj5SJbVIfNavciO<7dA+1|5o9Xx zT(<%8pm&=wgjzdaJebU#nmCa=`bUMLI1fF1CEhTzkwc}{(6{BZx~iDj;1mkrDu%s3 zZ%4o$*T}o&Ee^z2H!wNyWg(2ss3qk;0Ffz}9fR=b!}5o=3S$wczTy3+!PB{&RP$NP z*q{)P`<_}-^xrk#C@Mb=PqX~6idLUEV^-X-3$PiZMYsW)uKTZM)^FQ7z$HA5tL7>%j>$vkRr4W&2j7s9_x7|B_}*CT|SQ8+^}l2elk`wvq- zueP1B1GsB@TFNH_?C z(3Ua*N1(=i;!9OrIVm%J13nf79|~81HH*@_?3U*n8y&YCulQB9(Xi`vib{oe2>muV2@dsJxX>&_;p_?S-am&K={}2OuaG@w0bLQNf~{GOA+7u z#qBpEe|Ty6xCE1{A|pW&a<@7EYAp-L0WZvp=ghZWQXBi zH8V42x|T5{9;5eMnOFx`xA}74Dik58u(Tm5u}s0@Vpsirei8b`1c|PiHRBk^Sc9qS z{+RrjwP$(rl5uhKdaqg_Pq_(AMw!)_9&qhb*3u%#mSzyPenF*DJ<+9oKIEgh)cM1L ziAGECi)H;x-dpA~g)nnRXR3Ihk@WILeamrzWAfrOK(lS2&92?=WMRHW?UxHuhqOWW zB=27Zh2j?VqwiES!ZCAlj}Fh=cE;mMNT!Rn9t7aFp6yxh^7cA-81G9B%!$HcQCF^a>!b{w2@))uBE_E_s^Ed|$p@rZo>*M5q({_WDA{;FS$gS5EX zxp|Hbezdh4KXLPNeG5kOQ1`hvl#FpQEcOgYyoAW7SdIFJOOosVbte-G*-EjeFMb>6 zs&u7+aBx(t6S}Q#Wvby1ctNft^(dHI#{z5duIhI(>}!)j{~1^=6$n%ztCx_otzl@6 z`fq5F&pHLL?auifi{eI&^bopz5o%h4z_jelf3KkG3V)yFW$9nC&l%2b%}WGNm*_NqNNgU z>v984Y-*1EZ@;Q+yD~%>tvDXH(#R0~@@C?S>g%-*%9ol)xq{BI^kfL?{E9DN6B=kr zoOtP1(^mh}d`GZ1KG{gj$LC{24jv?X{swSoKW;{~DNKqpF)=0bIb|Ji3Sp=17}y`o z>i#y+ljAe>K)k#D%CCRtx+#7zUlWCRoyZk&Id*W5*sKPr(u>yPDK=l_@_%>y`tr-v{P@kuf)Tv7xeIVHS+l=BSI+>MyM&&KK^r@JORf8g}?bm^lLKfpY^k* zl(Lo%{vowcd!*I*6EW}(17F>}FEe)9cBjiM7n{5aJ5YmkwU#=+7#*VYH$GJIn|ZKX ztg#YI?$|6-$KK!(g}IBdQzP-%mXt6X@})C8dJC-1p{C30TI7}c|XPCB;*lljQ;dkJAso4r>=CejtHX#q-8dj01Y# z+kdCmoDK4Y^VV<2 zpj+6>Xw#X?eO^T0nP@VsKac{-3k#K~{|5VT$$lPUnRxoI=#Qp$s53)<7aeWQ`@N%j zJP#B@Zcrg)Ucw)J{U4Z^!iOC%@2gmc+_EVjRZ%l`bx%IUTg~~P@PDx7kll_a&});*d$X7n2z+S7=qG6oGA^XLcbJg zvjfWQ3GjcA>MW;hu7!d=6+&u+CUY0uU#C)}H*m&i?#o&kQf|xVyVG1xoOzSiSoa0% z$Xc+)OHq>K>4BKrm#5?o!L#T-m~{wzSp6ZMf1fsI?VXkn-G37uRMqiYSccbdaosTG zc77i4v|Wyzhekt)WLR`@F*kU-o${DI@Z?5vzXRO;A=IJcRm6_;HJh{`7|7Ds$aTPoM_W2xo1;}NdY zxIWUkvOCuIVx2V9Sh&-<_DzG0p5zBuQ-^@8}&pz&Zl={LjmcMBGgzZP{owkk#Y{Y zF-oy@bWIOn5MIOQBTQ@}Jzs~6*0?>L6O(@A?%r}$x`a*hTU^!I1g>-417UmjIW^Sk zuPQJ*1$!(?&YViq9MVdzHR$f=s~gG zxBIoA8~zzU!gTnAL&3rDJwhF`x~BRBQe4%V%VqMXZx-**WN>n zPgqm@!?lMN>)YUJAR)I?*T9pNwcP8Nu&75skV+h4{NXak52C>Dm}hmkc1iJSz?04# z(7V7|r4+~|02iZkhcA?9OyMdjC^Q3=O4Cv8yd8TUeLj>&bQ?@M$yc!vRQcFcz6M)7)1v?PVvlx|a}LhLwvl-lM7Ya0Qj zBM`8dHjY+07{SZITdjrWWi33va;Q?>`lM;_*6DQ7Gu*^G^+5-{x2(R7$v^ zpeY$+B1htKtRTW~fL*~;X4G-U@VH{+{{VBbZ0TAgn?Fj)+2iJ5koDj3o+obKLX9g!>>qWP8ldRt<7TQ_og`v-9vE(ZNRo^fu!^QM6uM)(K9}*d=zmWe zERUr&%~F6G=CEg!v_%E9W++nLaeTuW$FB}taQEgMwQPHFgH9|5(2O?MPNZ z#q(>v*w(Q1)6niT78$OjQ|$q8PE%L;6V4}!WVi8;F#iqcXqrSrq;l1k35*#s{8Q!` zEJo54-Q9U4KcT4-^Ay+ni2T#|b5b_`j`NxoqAP4@q5X%mG3y8GamAf-o<2F)dMVk< zxV;*(ii1sz>drlURJWe{GQ8pX3jk>lo)WI_0M8Xyr91g7@O}I%>|^~kLg5eB>N~{O zrEYCHRg`4?DGB7BTceb5CLL4S=ZcX!jj8DeLiB`>FX*fJuND?N^s6icKUA9x#-CO}?VMKFy)cB_`){xY!tTq<>qy*hFWY#QN5y zuxo1IFtZgKZePyre=MJv8&~K%NcE|{;u7)kAYf|MA8c7@v=SHyo=Um%!B?JF#tG=X zWHXN5$v{U(f3i>nj^&e?=nnFgajLuS?0LV|XvL&Cz_`gz?VIU+KCwukYSMr`N)g@!kf%J5bvB}#>SI;;gS}Ux6UJ0lLFl$@(b4@ z=aV)c{q>6zP?N_qF%bN~)_g(l(ye#mPP%%@+Vbp^!64C0&{*QlhtkjG{}2c$9_b~L zQ*PKZ_PV{wXZLx&k~40A{jS2U!p|JbZ%JvHRIbr`5LYz&s*^T5&{UtV`NGzsB9b`s zjX;anasFSk^D%FRR#bdr?T;f>Malels>n5ZT@(|`<=eJRdJ$ZFTYwPXH0a7*#^)nj z#=&Q-SxF=#_$s(91H<5Qr|e8Y&z+jAmPDl5!yaO3bZ=(VLLbdeWP;G?9@zXv<*)YE zKo%;~`oMZC!Hk7Kx-rHafd*Y!h`^5yD_OCkoi^Z%Xbk*Q3p<|wh9(C!*8DJbkZg5$ z**7$~gh)k4C(|zXvFG62dHM$q#@tW0YQAi>O(+)>7i$pntdkm2h|bkS<+A_1IGk4Y=jsI>_KZ5Rj@%8xLgl{xQq}4N{a#~p z=#PXd$ai=;WaIbY^Th626pf5TGa7aaPga}?A(R*50Aryv_%2WeM-DE?gXxG_{@QK1BNVgJ ztPf|>B*?;7ok@P@_o0?_tKWSB$GcDC6SreyV}nWDOw1!NAH(_HX+^U0$as}bbf1q? zy26C}+%6&}Q+`)?=vIOxK<<__#t4gDVqXCMmLxp3Dr7wLhV+_-EW|2LH9T-g^|{`N z2d>f#QR%6vLtqE=MBn8InSIsbPk~zIFK=R{Wz6G9@-2ub)e&PwQ})Os0|Uq{ld`QQ zs$(4-N^~kdE;M=+VUVayDiNj!i%TZP#^Qli<;auj3{Y)7d1MBZ;M0Q)b-t0{OfJptzZ+lV7P;H+Vb(*3-M=)h@t%iU-oKDEozm zkn=846~IZ}o+=>$G#OEF%E`O2tk3|1woqX-SBW?|Ir|NPmhhFd$R2u%JC`xh;)$?*xITq-14!R8}gb!)k4| zOh%B`QBW5*ON^U5>6w1O)Y7sbn0?Dt=z*jSN%5cWFJ;h#g&k`(71t74AI{56T>iE9 z0-pQTfYC!1^^`~==8MQ2{Qbd6<+10O=ZNG8B2t3J`8|eJav_HW?P`Q-nx<<$mg*ZCl3y-(q<@@54ybt7!#ImDV+risiRKKe4vK5x65BS~ z)Z28G`0G3{c&gS{2r}4o%Rah9GeVqLH>`x=e!%(|VP(Jp9V`~_%YzcmV*5CZ_yZzf zf+K8EIW7@_F=Wf*0+`3AK)kRMp}yf7l+uc~(jIcr&`9bD_eRD>QHZF7%jEeoY-fy? z)E+jKK?Aj;MG4?bXR&fCeWr4EdIH=#V-gbHsH%SNFK?*wJX?Cy<_J+vQR-THUlcf! zhU`Rs-j=mS(cjAg86!xCdn^&i2S=u?{0@2biYQM?z6;t0ihl$q?7aTrDI+;M^wU{9 zaAF-wfK}1^wrBtCLjXJ+HIU=KYYNWBM0mx-&H@)mSU1^Z@vYtzl=KJe3MT;={;{a2L@8H@}ifJ$H)zr{i-@>u^H%S zbv+`LMXss$WGG3(!QKjV_{PZSm~52U_c6s|n+*y7usSpV0$* z&a|fq0R~R$hK8krN>Nuj2u0Xb&R`<%R0F59=lJq2@wQX*MFDLMxTP>#Lh0NnpJ54WkJr#BEv z;nxd@@NGHqhW!T%jd5KqB21cZnJKA4es)SA;IhN&cw;~7h%`}1#^Ad3@c(lhv)mvM zYH5p%`ttenOID@Fw?QEI0cLU?u$p*MgaRUcM5co|B`7jso()l=;K2UwqLWF!d z2;^Xx=X%#*c)*Awu&7gwi@}BZJFs{?op$BjpwoH6J^N!`01RqXd3z0u4e?5 zeVw8o+)>XtgV~wEvA-{vVSWTVueZ3V`2%GloI%EH6#}Oe^_vPm-ktWFB20`#I1|M( zS3gRmd;ur2su&`rp`25l-RNCX0cHjO?Dn;QT{oC!*PhvmcSuiKobs&YopihrqYroh zij*Kq0zdoq5UfQ`RD(09+QeNh7;eHRtMnR{wLf3W1nLr+q-;+cF%RNLrcSS3hcIMn ziX--n+*G|Vzy-0qX(N{ko%ISpz2C3~{-B_aIg(0Nc&Y>!P3ml3N|gh%NVuV|TEr?i zJ0V+61{RF!H*V-fl7Wo<=f4ZqM{GHdom)#Ihht0qC8|QeqYzn#2b{Duo^mGg=ZD&| zkf_+khVvr1Zk9F;s<6EdE2k_oqtbDQ)j|P!DiuXiIj+NA#fwnE0T>H>rSzv_?qOUvLal&=!5ycLrh z5D0FBtdzJ0HOLpnACNp5|9l>0;+p%N(k|@f;ixRo)n_kOzR@GiRc)lbNcJ}-(n>x* z{-u;o^b#pHsz0;=@=me|W5K>xZY|;r&ycOi;%?;~sY{(i^Up9XERqg3!Oi2fkG43! z%U`J@zXZm*!N0>#+NhQbvC3Z8O1a=Ymhr`d3BJm2NPCN4&0J{LNtWUCn@q8)uXAfq z<6vlDy?f8qH2ZfRjwJF+U=bG_fm?@LA|qA?ejh$VkGnK7w0v7n{Hjs7*vjf((T#Uq9n>@t#Dts4 z;}sA*3;f_;_J|}k7TI<5zVrpv)0+AtH`)&)gjIOM)knrcQvy-mRO-C3>2NTJpmAP2 z&ea)o{W5*7TofEZff=7PFEGUT5jitHB@0`+98RZzH*6wr>pn3|0kw2Ni5o@iGjKrI zq5%hHMj$prNfDQzSeYB@j>lS6u>YLmyum7q1V=E9?tCYMIJuJ}pkyh!L!{urFcXCw z@I$Lv`SoaqhqGUZ+a58>au#U<3om_-)w?!QL_C#}(c-9lHPwDVNXUL8%2Ad+5j-fi`q&Yy*X zAVmb=v)}X>yXcO+1XD4elyNC?9U&^sK-EjO27a!f}9*_hASXb>R@HvPDWY1J8hoJ__-P|Hd!0CsK`I;aR4r` zhpsv)(3=1w=IhBazb@d**9pW!al&Q&kBGvy0uE5m00Rsf^W5a8c|dqcy(y7m%{!{6 z91z!W%f-hpKISZu*R_^;dolzqumxCb2GD2*fcvRKbj3sYOa(E}kZJ_Y&3oFynEtQIZxjeX`G#lcQ$<4qROn z%NHq;-;C~{#qRfdm-By`ZkL`6`wD{}YSqIgwv`}Se|U>@LMer&_@naK|K@Q)Tj__N zopVCU8GuDgfh^=l5%BF}1W7FWZ4Pf+yqhAn3r2>6snE1#F31J@?k-9K_tC48&1ewa zk{bc3244}LEvtOjh@7XD|8$tMRZW(Qp?A>y&?o78blb?)?X=V|^lZC;1bSR$cQ%l< zAmH%QMX~@pB|4THK?qOQz3np-qNf`k!^xi^UN_aKwyQ;Eevn8pshOBxJs!U9e@?tT zY3-8b!DdyhDEod}+o}s*g6I>tR~W%AdU-kRuep-POdCqc-xhCd+;=m&N{sv944=XwrCg&lHAbS%W{5&D%?|aYHvpJT^4D zJxDSx-`6STztJUK>1ah^kEv2CAxx@NdjJ(X)s3Z&3Ju@}zBHL2F#qmdoFFl_*ChLL z&|BQn@JcvQv*Xs~wYS`8`t436o)-GyNY{H=asPuFE32vLy1dz4^Ma)P$69d@nk+0R zFsf$%#XEMr`u^P0i%G%*x_s4m^vwGyDgFv7C;3Khw^DD|1)fImXCvn3>glzW)wnj4 zvo!}HG|4`?5_msSVMV$`zX{{g!_Y6J6LZo>)008}ZJH6}5~y+{5_$_13k@~!y z?mW4n1zZA#YJSI5a~r`qvn(44X31#WdS;fyB2Kj}(~O3P8pnp_*x0G%Q&zJx0xgrdp7}=*sIfs7sj!!0tLc;xK z#S=qrm-&K6V9z0jw8%cUrv0(wgON_jT|usNmzp7>UoO=*tU#Q_*8QyPErdOZ&CE%t zEK2e7MG6?tI4-JZvxqxIe)sNXqd%MDW5qjCjftB}xGFAiP}GlOhbpPt)Ko{so~wHsb#aiwbo0BmofNVw{Q~f7yQ&yZ+hk!eG2! zpRTQchtJ29Mr9+UpHme`h(MAAK!l4$z&gD_N^K4%9pz2Z{u5zB z5&#iS7NVf8y4B<5_a%oFEJqkYO@kG;+y8g}FV@#%SJU-xJ1|E@AR&T55&#iy{-Gks z2bFbvRO5fQIAIKKkC&Qy|KI(8X|<~A`48}aXvG9Vgat_eM7Ws;tWXi?m%B?Nc2H19 zQCZWB*X!TA_ZF|07wYR(EoD72_E&@%NdQDR`W*`HvsO4<{84TNge|+nO+&N8@9+P2 zeLdCs`~9Er2jt$r2s4rZh;XzT1*3ui-YT!*{YvQ#5O#b}Ue)Ax%ikqdE1#(8_YW{1 zt@dAtup&LtM7ZjXcIYZjZ=m#O3u{L7r!Y1~|Efe#)HT{Duk!EYeudl3*oq38)%1g~ z1O0&mDuM|SOtJ$Y!qtA@pDF^?yWISE`+>6#$q~lla(k$#Y4-1XeLnuLwpP`}AK-c9 z;xEFKi~|sBPlPj1&sviv*gO5;BdOxf26M6?SAb(ACFa4vB}|or@%AV30wpy8y+A; zD9Njuh$MF4UZ6!)z_pDn+`F{|tBzUo!ap95mqX3TV1XC;%`Sg^)DBH5iukI z5Rqm#@UY5&OX`~Bc0iE%fWyZcak|>~cW(3f_)T>+>+N>cOZtDnHq}9e2qh^46p?5L zkOvG?74V4N$+G$;Zp%$4E=uy|Kr5?kqPeww@8BD~ULLKgVyoJB{s5DJr&I?OBGlvz zfQVGlz^A~~DuZ@Uh~tC7J*7l2fGVn+scGk-zjC*mUsqRC>Qdg6Z#8fw;8Y!0h)|OR zKt!@kw06PWRR+zCiRRtGJ-IL?QDs;GRM)psS<}8F9NQfZep^$cy3`Ng2rwM2?q7(A zBYT-5lGOsYp!Eq*rqb!+`mc90w@OOERjO^Uw)^70$ZF;1>g)VL`AEI9AiADX1I^xmwqpK2;O~Ka0wIh@5&+Rr ztwt*o{2l@7hq0~2%Ijq{#G9iSl9JfrD+R1}7o}wlIGpF6&gb^|c(kdAesO#$=`j@@WjKuEEN&nr6;-`+kMZ z#>9pO4h8Oz-wAHuCuslnLUc%y00;qBp_P551p0}FrzY{6F8SP$l@{oyG6b*JM|oA# zdGGgcaX6UL(4cyP-`TVP_W=un@nk|6l_UT{Xodpwflk4AvP;vGdAxHzqcc+@_LtCV zcX71h>=XTbz-pU~x0;&R9GnY3KpAiousK*yD1=c(>T8b>{>TG904@&J6ZT7r=NH9! z+?kt6Y=o2!`he=XR;ue-@ttAM-{tf1X=^L9nwtYz&wnPH(JuaF!F*C7j4Co-afDDO z7MOz84K8R>EoSb>&ETg+ISfcj2am@w9pXPQ~hT$ z6Zk368tf+)!l)zx5aP^!j&?dc=*9daQj@qVFOyrd)5(jC4))Xg#q0G^UDrxYgY~p` z{WTsBORQEFS*>gf-ZuZKoWSG2e?sx(LKvAO079JI1;A{eM<|{=$^@6BC38!5I@e{U z(%qnKh8vshRMfWMI)1PJu-naQn~kMbD_a~6g8p9r8;8+`K&=kl5`-`=NdSa6w-jJ1 zTDf3Ib&ZeZ+RRigPfuZFYEsB|2W$>E71hlgYO%4!;b4=)!D^e0GWWUb{DWjR@H3zx z)C&>9s3ZXpqHXR0UIH>gy^s^S#K$rsHHm>q2@FVzr+-oc`2kJ}${kL&G_fIjdT<((}N9|4y*&H0QJ1Dg|Xmp>sq|YEe zLwna>8sc>bA%uufV}S~^iZ}TP{YUlje-j~u5F+5xfJuOd&|d&M>W3G21IRk}Ka~(d z2%+9Zz$!w2G3;m`tAXKd`1cV)2q6N-1pELfA@moe}J5<&=JAkkV!xR!0c^g)`hgD$zRmg>QdkA%qYeP7Ls0z#c+>Aq}^W zJ;(pC#K%rTV5RE#9g zB!hhhCM1u0&b|J>TAk@`)oHEH*?aAXh%abi5j)Dx^}35|Q{xr8Sgmpv5fRZfrm!J% z1m4FL)~nn^L_~D89xP)MkLoYN?T!a*DW@lTf0)aNh=`6ffHkEVxKn>|{>!4jtxWCf z|CP)|L_~CnKCCDg4|T)l!3$+#;*#v-k7$$((}-xcF)UynbIO16CU1~C!2$NLkJEaW z6A>Rt0}# -nostr army knife - - -
- - diff --git a/justfile b/justfile deleted file mode 100644 index 50881fb..0000000 --- a/justfile +++ /dev/null @@ -1,13 +0,0 @@ -build-prod: - sbt fullLinkJS/esBuild - -cloudflare: - rm -fr cf - mkdir -p cf/target/esbuild - cp index.html cf/ - cp favicon.ico cf/ - cp target/esbuild/bundle.js cf/target/esbuild - wrangler pages publish cf --project-name nostr-army-knife --branch master - rm -fr cf - -build-and-deploy: build-prod cloudflare diff --git a/project/build.properties b/project/build.properties deleted file mode 100644 index 22af262..0000000 --- a/project/build.properties +++ /dev/null @@ -1 +0,0 @@ -sbt.version=1.7.1 diff --git a/project/plugins.sbt b/project/plugins.sbt deleted file mode 100644 index dabefb2..0000000 --- a/project/plugins.sbt +++ /dev/null @@ -1,2 +0,0 @@ -addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.13.0") -addSbtPlugin("com.fiatjaf" % "sbt-esbuild" % "0.1.1") diff --git a/src/main/scala/Components.scala b/src/main/scala/Components.scala deleted file mode 100644 index 6137b14..0000000 --- a/src/main/scala/Components.scala +++ /dev/null @@ -1,476 +0,0 @@ -import cats.data.{Store => *, *} -import cats.effect.* -import cats.effect.syntax.all.* -import cats.syntax.all.* -import fs2.concurrent.* -import fs2.dom.{Event => _, *} -import io.circe.parser.* -import io.circe.syntax.* -import calico.* -import calico.html.io.{*, given} -import calico.syntax.* -import scodec.bits.ByteVector -import scoin.* -import snow.* - -import Utils.* - -object Components { - def render32Bytes( - store: Store, - bytes32: ByteVector32 - ): Resource[IO, HtmlDivElement[IO]] = - div( - cls := "text-md", - entry("canonical hex", bytes32.toHex), - "if this is a public key:", - div( - cls := "mt-2 pl-2 mb-2", - entry( - "npub", - NIP19.encode(XOnlyPublicKey(bytes32)), - Some( - selectable( - store, - NIP19.encode(XOnlyPublicKey(bytes32)) - ) - ) - ), - nip19_21( - store, - "nprofile", - NIP19.encode(ProfilePointer(XOnlyPublicKey(bytes32))) - ) - ), - "if this is a private key:", - div( - cls := "pl-2 mb-2", - entry( - "nsec", - NIP19.encode(PrivateKey(bytes32)), - Some( - selectable( - store, - NIP19.encode(PrivateKey(bytes32)) - ) - ) - ), - entry( - "npub", - NIP19.encode(PrivateKey(bytes32).publicKey.xonly), - Some( - selectable( - store, - NIP19.encode(PrivateKey(bytes32).publicKey.xonly) - ) - ) - ), - nip19_21( - store, - "nprofile", - NIP19.encode(ProfilePointer(PrivateKey(bytes32).publicKey.xonly)) - ) - ), - "if this is an event id:", - div( - cls := "pl-2 mb-2", - nip19_21( - store, - "nevent", - NIP19.encode(EventPointer(bytes32.toHex)) - ) - ), - div( - cls := "pl-2 mb-2", - entry( - "note", - NIP19.encode(bytes32), - Some( - selectable( - store, - NIP19.encode(bytes32) - ) - ) - ) - ) - ) - - def renderEventPointer( - store: Store, - evp: snow.EventPointer - ): Resource[IO, HtmlDivElement[IO]] = - div( - cls := "text-md", - entry( - "event id (hex)", - evp.id, - Some(selectable(store, evp.id)) - ), - relayHints(store, evp.relays), - evp.author.map { pk => - entry("author hint (pubkey hex)", pk.value.toHex) - }, - nip19_21(store, "nevent", NIP19.encode(evp)), - entry( - "note", - NIP19.encode(ByteVector32.fromValidHex(evp.id)), - Some(selectable(store, NIP19.encode(ByteVector32.fromValidHex(evp.id)))) - ) - ) - - def renderProfilePointer( - store: Store, - pp: snow.ProfilePointer, - sk: Option[PrivateKey] = None - ): Resource[IO, HtmlDivElement[IO]] = - div( - cls := "text-md", - sk.map { k => - entry( - "private key (hex)", - k.value.toHex, - Some(selectable(store, k.value.toHex)) - ) - }, - sk.map { k => - entry( - "nsec", - NIP19.encode(k), - Some(selectable(store, NIP19.encode(k))) - ) - }, - entry( - "public key (hex)", - pp.pubkey.value.toHex, - Some(selectable(store, pp.pubkey.value.toHex)) - ), - relayHints( - store, - pp.relays, - dynamic = if sk.isDefined then false else true - ), - entry( - "npub", - NIP19.encode(pp.pubkey), - Some(selectable(store, NIP19.encode(pp.pubkey))) - ), - nip19_21(store, "nprofile", NIP19.encode(pp)) - ) - - def renderAddressPointer( - store: Store, - addr: snow.AddressPointer - ): Resource[IO, HtmlDivElement[IO]] = { - val nip33atag = - s"${addr.kind}:${addr.author.value.toHex}:${addr.d}" - - div( - cls := "text-md", - entry("author (pubkey hex)", addr.author.value.toHex), - entry("identifier (d tag)", addr.d), - entry("kind", addr.kind.toString), - relayHints(store, addr.relays), - nip19_21(store, "naddr", NIP19.encode(addr)), - entry("nip33 'a' tag", nip33atag, Some(selectable(store, nip33atag))) - ) - } - - def renderEvent( - store: Store, - event: Event - ): Resource[IO, HtmlDivElement[IO]] = - div( - cls := "text-md", - if event.pubkey.isEmpty then - Some( - div( - cls := "flex items-center", - entry("missing", "pubkey"), - button( - Styles.buttonSmall, - "fill with a debugging key", - onClick --> (_.foreach { _ => - store.input.set( - event - .copy(pubkey = Some(keyOne.publicKey.xonly)) - .asJson - .printWith(jsonPrinter) - ) - }) - ) - ) - ) - else None, - if event.id.isEmpty then - Some( - div( - cls := "flex items-center", - entry("missing", "id"), - if event.pubkey.isDefined then - Some( - button( - Styles.buttonSmall, - "fill id", - onClick --> (_.foreach(_ => - store.input.set( - event - .copy(id = Some(event.hash.toHex)) - .asJson - .printWith(jsonPrinter) - ) - )) - ) - ) - else None - ) - ) - else None, - if event.sig.isEmpty then - Some( - div( - cls := "flex items-center", - entry("missing", "sig"), - if event.id.isDefined && event.pubkey == Some( - keyOne.publicKey.xonly - ) - then - Some( - button( - Styles.buttonSmall, - "sign", - onClick --> (_.foreach(_ => - store.input.set( - event - .sign(keyOne) - .asJson - .printWith(jsonPrinter) - ) - )) - ) - ) - else None - ) - ) - else None, - entry("serialized event", event.serialized), - entry("implied event id", event.hash.toHex), - entry( - "does the implied event id match the given event id?", - event.id == Some(event.hash.toHex) match { - case true => "yes"; case false => "no" - } - ), - entry( - "is signature valid?", - event.isValid match { - case true => "yes"; case false => "no" - } - ), - event.id.map(id => - nip19_21( - store, - "nevent", - NIP19.encode(EventPointer(id, author = event.pubkey)) - ) - ), - if event.kind >= 30000 && event.kind < 40000 then - event.pubkey - .map(author => - nip19_21( - store, - "naddr", - NIP19.encode( - AddressPointer( - d = event.tags - .collectFirst { case "d" :: v :: _ => v } - .getOrElse(""), - kind = event.kind, - author = author, - relays = List.empty - ) - ) - ) - ) - else - event.id.map(id => - entry( - "note", - NIP19.encode(ByteVector32.fromValidHex(id)), - Some(selectable(store, NIP19.encode(ByteVector32.fromValidHex(id)))) - ) - ) - ) - - private def entry( - key: String, - value: String, - selectLink: Option[Resource[IO, HtmlSpanElement[IO]]] = None - ): Resource[IO, HtmlDivElement[IO]] = - div( - cls := "flex items-center space-x-3", - span(cls := "font-bold", key + " "), - span(Styles.mono, cls := "max-w-xl break-all", value), - selectLink - ) - - private def nip19_21( - store: Store, - key: String, - code: String - ): Resource[IO, HtmlDivElement[IO]] = - div( - span(cls := "font-bold", key + " "), - span(Styles.mono, cls := "break-all", code), - selectable(store, code), - a( - href := "nostr:" + code, - external - ) - ) - - private def relayHints( - store: Store, - relays: List[String], - dynamic: Boolean = true - ): Resource[IO, HtmlDivElement[IO]] = - if !dynamic && relays.isEmpty then div("") - else - SignallingRef[IO].of(false).toResource.flatMap { active => - val value = - if relays.size > 0 then relays.reduce((a, b) => s"$a, $b") else "" - - div( - cls := "flex items-center space-x-3", - span(cls := "font-bold", "relay hints "), - if relays.size == 0 then div("") - else - // displaying each relay hint - div( - cls := "flex flex-wrap max-w-xl", - relays - .map(url => - div( - Styles.mono, - cls := "flex items-center rounded py-0.5 px-1 mr-1 mb-1 bg-orange-100", - url, - // removing a relay hint by clicking on the x - div( - cls := "cursor-pointer ml-1 text-rose-600 hover:text-rose-300", - onClick --> (_.foreach(_ => { - store.result.get.flatMap(result => - store.input.set( - result - .map { - case a: AddressPointer => - NIP19 - .encode( - a.copy(relays = - relays.filterNot(_ == url) - ) - ) - case p: ProfilePointer => - NIP19 - .encode( - p.copy(relays = - relays.filterNot(_ == url) - ) - ) - case e: EventPointer => - NIP19 - .encode( - e.copy(relays = - relays.filterNot(_ == url) - ) - ) - case r => "" - } - .getOrElse("") - ) - ) - })), - "×" - ) - ) - ) - ) - , - active.map { - case true => - div( - input.withSelf { self => - ( - onKeyPress --> (_.foreach(evt => - // confirm adding a relay hint - evt.key match { - case "Enter" => - self.value.get.flatMap(url => - if url.startsWith("wss://") || url - .startsWith("ws://") - then { - store.result.get.flatMap(result => - store.input.set( - result - .map { - case a: AddressPointer => - NIP19 - .encode( - a.copy(relays = a.relays :+ url) - ) - case p: ProfilePointer => - NIP19 - .encode( - p.copy(relays = p.relays :+ url) - ) - case e: EventPointer => - NIP19 - .encode( - e.copy(relays = e.relays :+ url) - ) - case r => "" - } - .getOrElse("") - ) - ) - >> active.set(false) - } else IO.unit - ) - case _ => IO.unit - } - )) - ) - } - ) - case false if dynamic => - // button to add a new relay hint - button( - Styles.buttonSmall, - "add relay hint", - onClick --> (_.foreach(_ => active.set(true))) - ) - case false => div("") - } - ) - } - - private def selectable( - store: Store, - code: String - ): Resource[IO, HtmlSpanElement[IO]] = - span( - store.input.map(current => - if current == code then a("") - else - a( - href := "#/" + code, - onClick --> (_.foreach(evt => - evt.preventDefault >> - store.input.set(code) - )), - edit - ) - ) - ) - - private val edit = img(cls := "inline w-4 ml-2", src := "edit.svg") - private val external = img(cls := "inline w-4 ml-2", src := "ext.svg") -} diff --git a/src/main/scala/Main.scala b/src/main/scala/Main.scala deleted file mode 100644 index 7f3c914..0000000 --- a/src/main/scala/Main.scala +++ /dev/null @@ -1,147 +0,0 @@ -import cats.effect.* -import cats.effect.syntax.all.* -import cats.syntax.all.* -import fs2.concurrent.* -import fs2.dom.{Event => _, *} -import io.circe.parser.* -import io.circe.syntax.* -import calico.* -import calico.html.io.{*, given} -import calico.syntax.* -import scoin.* -import snow.* - -import Utils.* -import Components.* - -object Main extends IOWebApp { - def render: Resource[IO, HtmlDivElement[IO]] = Store(window).flatMap { - store => - div( - cls := "flex w-full flex-col items-center justify-center", - div( - cls := "w-4/5", - h1( - cls := "px-1 py-2 text-center text-xl", - img( - cls := "inline-block w-8 mr-2", - src := "/favicon.ico" - ), - a( - href := "/", - "nostr army knife" - ) - ), - div( - cls := "flex my-3", - input(store), - actions(store) - ), - result(store) - ), - div( - cls := "flex justify-end mr-5 mt-10 text-xs w-4/5", - a( - href := "https://github.com/fiatjaf/nak", - "source code" - ), - a( - cls := "ml-4", - href := "https://github.com/fiatjaf/nak", - "get the command-line tool" - ) - ) - ) - } - - def actions(store: Store): Resource[IO, HtmlDivElement[IO]] = - div( - cls := "flex flex-col space-y-1 my-3", - store.input.map { - case "" => div("") - case _ => - button( - Styles.button, - "clear", - onClick --> (_.foreach(_ => store.input.set(""))) - ) - }, - store.result.map { - case Right(_: Event) => - button( - Styles.button, - "format", - onClick --> (_.foreach(_ => - store.input.update(original => - parse(original).toOption - .map(_.printWith(jsonPrinter)) - .getOrElse(original) - ) - )) - ) - case _ => div("") - }, - button( - Styles.button, - "generate event", - onClick --> (_.foreach(_ => - store.input.set( - Event( - kind = 1, - content = "hello world" - ).sign(keyOne) - .asJson - .printWith(jsonPrinter) - ) - )) - ), - button( - Styles.button, - "generate keypair", - onClick --> (_.foreach(_ => - store.input.set( - NIP19.encode(PrivateKey(randomBytes32())) - ) - )) - ) - ) - - def input(store: Store): Resource[IO, HtmlDivElement[IO]] = - div( - cls := "w-full grow", - div( - cls := "w-full flex justify-center", - textArea.withSelf { self => - ( - cls := "w-full max-h-96 p-3 rounded", - styleAttr := "min-height: 280px; font-family: monospace", - spellCheck := false, - placeholder := "paste something nostric (event JSON, nprofile, npub, nevent etc or hex key or id)", - onInput --> (_.foreach(_ => - self.value.get.flatMap(store.input.set) - )), - value <-- store.input - ) - } - ) - ) - - def result(store: Store): Resource[IO, HtmlDivElement[IO]] = - div( - cls := "w-full flex my-5", - store.result.map { - case Left(msg) => div(msg) - case Right(bytes: ByteVector32) => render32Bytes(store, bytes) - case Right(event: Event) => renderEvent(store, event) - case Right(pp: ProfilePointer) => renderProfilePointer(store, pp) - case Right(evp: EventPointer) => renderEventPointer(store, evp) - case Right(sk: PrivateKey) => - renderProfilePointer( - store, - ProfilePointer(pubkey = sk.publicKey.xonly), - Some(sk) - ) - case Right(addr: AddressPointer) => renderAddressPointer(store, addr) - } - ) -} diff --git a/src/main/scala/Parser.scala b/src/main/scala/Parser.scala deleted file mode 100644 index 9033aaa..0000000 --- a/src/main/scala/Parser.scala +++ /dev/null @@ -1,61 +0,0 @@ -import scala.util.Try -import io.circe.parser.* -import cats.syntax.all.* -import scodec.bits.ByteVector -import scoin.* -import snow.* - -type Result = Either[ - String, - Event | PrivateKey | AddressPointer | EventPointer | ProfilePointer | - ByteVector32 -] - -object Parser { - val additions = raw" *\+ *".r - - def parseInput(input: String): Result = - if input == "" then Left("") - else - ByteVector - .fromHex(input) - .flatMap(b => Try(Right(ByteVector32(b))).toOption) - .getOrElse( - NIP19.decode(input) match { - case Right(pp: ProfilePointer) => Right(pp) - case Right(evp: EventPointer) => Right(evp) - case Right(sk: PrivateKey) => Right(sk) - case Right(addr: AddressPointer) => Right(addr) - case Left(_) if input.split(":").size == 3 => - // parse "a" tag format, nip 33 - val spl = input.split(":") - ( - spl(0).toIntOption, - ByteVector.fromHex(spl(1)), - Some(spl(2)) - ).mapN((kind, author, identifier) => - AddressPointer( - identifier, - kind, - scoin.XOnlyPublicKey(ByteVector32(author)), - relays = List.empty - ) - ).toRight("couldn't parse as a nip33 'a' tag") - case Left(_) => - // parse event json - parse(input) match { - case Left(err: io.circe.ParsingFailure) => - Left("not valid JSON or NIP-19 code") - case Right(json) => - json - .as[Event] - .leftMap { err => - err.pathToRootString match { - case None => s"decoding ${err.pathToRootString}" - case Some(path) => s"field $path is missing or wrong" - } - } - } - } - ) -} diff --git a/src/main/scala/Store.scala b/src/main/scala/Store.scala deleted file mode 100644 index 35dadd8..0000000 --- a/src/main/scala/Store.scala +++ /dev/null @@ -1,46 +0,0 @@ -import cats.data.* -import cats.effect.* -import cats.effect.syntax.all.* -import cats.syntax.all.* -import fs2.concurrent.* -import fs2.dom.{Event => _, *} -import scoin.PrivateKey - -case class Store( - input: SignallingRef[IO, String], - result: SignallingRef[IO, Result] -) - -object Store { - def apply(window: Window[IO]): Resource[IO, Store] = { - val key = "nak-input" - - for { - input <- SignallingRef[IO].of("").toResource - result <- SignallingRef[IO, Result](Left("")).toResource - - _ <- Resource.eval { - OptionT(window.localStorage.getItem(key)) - .foreachF(input.set(_)) - } - - _ <- window.localStorage - .events(window) - .foreach { - case Storage.Event.Updated(`key`, _, value, _) => - input.set(value) - case _ => IO.unit - } - .compile - .drain - .background - - _ <- input.discrete - .evalTap(input => IO.cede *> window.localStorage.setItem(key, input)) - .evalTap(input => result.set(Parser.parseInput(input.trim()))) - .compile - .drain - .background - } yield Store(input, result) - } -} diff --git a/src/main/scala/Styles.scala b/src/main/scala/Styles.scala deleted file mode 100644 index 4a72181..0000000 --- a/src/main/scala/Styles.scala +++ /dev/null @@ -1,9 +0,0 @@ -import calico.html.io.* - -object Styles { - val button = cls := - "shrink bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 mx-2 px-4 rounded " - val buttonSmall = cls := - "shrink text-sm bg-blue-500 hover:bg-blue-700 text-white font-bold mx-2 px-2 rounded " - val mono = styleAttr := "font-family: monospace" -} diff --git a/src/main/scala/Utils.scala b/src/main/scala/Utils.scala deleted file mode 100644 index 7a64810..0000000 --- a/src/main/scala/Utils.scala +++ /dev/null @@ -1,22 +0,0 @@ -import io.circe.Printer -import scodec.bits.ByteVector -import scoin.* - -object Utils { - val keyOne = PrivateKey(ByteVector32(ByteVector(0x01).padLeft(32))) - - val jsonPrinter = Printer( - dropNullValues = false, - indent = " ", - lbraceRight = "\n", - rbraceLeft = "\n", - lbracketRight = "\n", - rbracketLeft = "\n", - lrbracketsEmpty = "", - arrayCommaRight = "\n", - objectCommaRight = "\n", - colonLeft = "", - colonRight = " ", - sortKeys = true - ) -} From 9e5e736395c0c580d6fb2bf768b0897c04a1e445 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sun, 29 Oct 2023 23:38:39 -0300 Subject: [PATCH 019/401] update readme with some helpful examples instead of a giant wall of text. --- README.md | 263 ++++++++++++------------------------------------------ 1 file changed, 55 insertions(+), 208 deletions(-) diff --git a/README.md b/README.md index 97b98a4..408b2e8 100644 --- a/README.md +++ b/README.md @@ -1,225 +1,72 @@ -# nostr army knife +# nak, the nostr army knife -this repository contains two things: +install with `go install github.com/fiatjaf/nak` or +[download a binary](https://github.com/fiatjaf/nak/releases). -## a command-line tool for decoding and encoding nostr entities and talking to relays +## what can you do with it? -Install with `go install github.com/fiatjaf/nak`. +take a look at the help text that comes in it to learn all possibilities, but here are some: -It pairs nicely with https://github.com/blakejakopovic/nostcat using unix pipes. - -### examples +### make a nostr event signed with the default key (`01`) +```shell +~> nak event +{"id":"53443506e7d09e55b922a2369b80f926007a8a8a8ea5f09df1db59fe1993335e","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1698632644,"kind":1,"tags":[],"content":"hello from the nostr army knife","sig":"4bdb609c975b2b61338c2ff4c7ce91d4afe74bea4ed1601a62e1fd125bd4c0ae6e0166cca96e5cfb7e0f50583eb6a0dd0b66072566299b6007742db56278010c"} ``` -~> nak decode nsec1aqc5q5l8da0l7u6gra4p5xhleclngezlpsgd7z5dx07cpu8sxf2shqgn6y -{ - "pubkey": "5b36b874b2b983197ba4be80553b2e4b6db2895a04567cea0aa47585b2e0c620", - "private_key": "e8314053e76f5fff73481f6a1a1affce3f34645f0c10df0a8d33fd80f0f03255" -} -~> nak event -c hello --sec e8314053e76f5fff73481f6a1a1affce3f34645f0c10df0a8d33fd80f0f03255 -{"id":"ed840ef37a40cce4f4b8c361e5df13457ad664209cf4a297fd7df7e84fdd32e0","pubkey":"5b36b874b2b983197ba4be80553b2e4b6db2895a04567cea0aa47585b2e0c620","created_at":1683201092,"kind":1,"tags":[],"content":"hello","sig":"304a87dbbdf986a187eb9417316cfe3d6f8f31793ba20c9c6d7e4ebeeefe950d6ecba6098c201b7170c04e27c2f920d607a90f5c8763c35ac806dce37df1d05d"} -~> nak event -c hello --sec e8314053e76f5fff73481f6a1a1affce3f34645f0c10df0a8d33fd80f0f03255 wss://relay.stoner.com wss://nos.lol wss://nostr.wine wss://atlas.nostr.land wss://relay.damus.io -{"id":"54a534647bdcd2751d743fea4fc9eee5dba613887d69425f0891d9c2f82772a5","pubkey":"5b36b874b2b983197ba4be80553b2e4b6db2895a04567cea0aa47585b2e0c620","created_at":1684895417,"kind":1,"tags":[],"content":"hello","sig":"81a14cfe628fab6cd6135bb66f6e8b3bb4bfce4f666462a1303fdfbc9038fd141e73db3fe7e774a8f023fc70622c50a67d4fa41d3d09806c78f051985c11e0bd"} -publishing to wss://relay.stoner.com... failed: msg: blocked: pubkey is not allowed to publish to this relay -publishing to wss://nos.lol... success. -publishing to wss://nostr.wine... failed: msg: blocked: not an active paid member -publishing to wss://atlas.nostr.land... failed: msg: blocked: pubkey not admitted +### make a nostr event with custom content and tags, sign it with a different key and publish it to two relays +```shell +~> nak event --sec 02 -c 'good morning' --tag t=gm wss://nostr-pub.wellorder.net wss://relay.damus.io +{"id":"e20978737ab7cd36eca300a65f11738176123f2e0c23054544b18fe493e2aa1a","pubkey":"c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5","created_at":1698632753,"kind":1,"tags":[["t","gm"]],"content":"good morning","sig":"5687c1a97066c349cb3bde0c0719fd1652a13403ba6aca7557b646307ee6718528cd86989db08bf6a7fd04bea0b0b87c1dd1b78c2d21b80b80eebab7f40b8916"} +publishing to wss://nostr-pub.wellorder.net... success. publishing to wss://relay.damus.io... success. +``` -~> nak decode nevent1qqs29yet5tp0qq5xu5qgkeehkzqh5qu46739axzezcxpj4tjlkx9j7gpr4mhxue69uhkummnw3ez6ur4vgh8wetvd3hhyer9wghxuet5sh59ud +### query a bunch of relays for a tag with a limit of 2 for each, print their content +```shell +~> nak req -k 1 -t t=gm -l 2 wss://nostr.mom wss://nostr.wine wss://nostr-pub.wellorder.net | jq .content +"#GM, you sovereign savage #freeple of the #nostrverse. Let's cause some #nostroversy. " +"ITM slaves!\n#gm https://image.nostr.build/cbbcdf80bfc302a6678ecf9387c87d87deca3e0e288a12e262926c34feb3f6aa.jpg " +"good morning" +"The problem is to start, but along the way it's fixed #GM ☀️" +"Activando modo zen…\n\n#GM #Nostr #Hispano" +``` + +### decode a nip19 note1 code, add a relay hint, encode it back to nevent1 +```shell +~> nak decode note1ttnnrw78wy0hs5fa59yj03yvcu2r4y0xetg9vh7uf4em39n604vsyp37f2 | jq -r .id | nak encode nevent -r wss://nostr.zbd.gg +nevent1qqs94ee3h0rhz8mc2y76zjf8cjxvw9p6j8nv45zktlwy6uacjea86kgpzfmhxue69uhkummnw3ezu7nzvshxwec8zw8h7 +~> nak decode nevent1qqs94ee3h0rhz8mc2y76zjf8cjxvw9p6j8nv45zktlwy6uacjea86kgpzfmhxue69uhkummnw3ezu7nzvshxwec8zw8h7 { - "id": "a2932ba2c2f00286e5008b6737b0817a0395d7a25e9859160c195572fd8c5979", + "id": "5ae731bbc7711f78513da14927c48cc7143a91e6cad0565fdc4d73b8967a7d59", "relays": [ - "wss://nostr-pub.wellorder.net" + "wss://nostr.zbd.gg" ] } - -~> nak req -a a2932ba2c2f00286e5008b6737b0817a0395d7a25e9859160c195572fd8c5979 -k 1 -a e8b487c079b0f67c695ae6c4c2552a47f38adfa2533cc5926bd2c102942fdcb7 -["REQ","nak",{"kinds":[1],"authors":["a2932ba2c2f00286e5008b6737b0817a0395d7a25e9859160c195572fd8c5979","e8b487c079b0f67c695ae6c4c2552a47f38adfa2533cc5926bd2c102942fdcb7"]}] - -~> nak req -k 1 -l 1 --stream wss://relay.stoner.com -{"id":"1d73832917bf5a72276c53e9246c28b97225b51cd5735843434f7756fc0ddead","pubkey":"3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d","created_at":1684894689,"kind":1,"tags":[["p","bcbeb5a2e6b547f6d0c3d8c16145f7bb94f3639ec7ecbcfe50045dbb2eede70b","wss://nos.lol","artk42"],["e","b5af6815c8d89a7d5b6201b9e624fbd5389fca3337ba2dc05c6187234a7c1bd5","wss://nos.lol","root"],["e","5795e27aff0a459a30c64a61a32c43d968cd19c8f1926cf01fc02e9da7c56f2b","wss://nos.lol","reply"],["client","coracle"]],"content":"Because that makes no sense.","sig":"3ee5b2b26ec6b116ef1a6b1c10bc7e56674a3c36841814f68b57f63259f3d78e23629d4599afe67e72c220e27b4b0966cc51adc1da808c8c6111dedb531ac0c3"} ``` -### documentation - -``` -~> nak --help -NAME: - nak - the nostr army knife command-line tool - -USAGE: - nak [global options] command [command options] [arguments...] - -COMMANDS: - req generates encoded REQ messages and optionally use them to talk to relays - count generates encoded COUNT messages and optionally use them to talk to relays - event generates an encoded event and either prints it or sends it to a set of relays - decode decodes nip19, nip21, nip05 or hex entities - encode encodes notes and other stuff to nip19 entities - help, h Shows a list of commands or help for one command - -GLOBAL OPTIONS: - --help, -h show help - -~> nak event --help -NAME: - nak event - generates an encoded event and either prints it or sends it to a set of relays - -USAGE: - nak event [command options] [arguments...] - -DESCRIPTION: - example usage (for sending directly to a relay with 'nostcat'): - nak event -k 1 -c hello --envelope | nostcat wss://nos.lol - standalone: - nak event -k 1 -c hello wss://nos.lol`, - -OPTIONS: - --envelope print the event enveloped in a ["EVENT", ...] message ready to be sent to a relay (default: false) - --sec value secret key to sign the event (default: the key '1') - - EVENT FIELDS - - --content value, -c value event content (default: hello from the nostr army knife) - --created-at value, --time value, --ts value unix timestamp value for the created_at field (default: now) - --kind value, -k value event kind (default: 1) - --tag value, -t value [ --tag value, -t value ] sets a tag field on the event, takes a value like -t e= - -e value [ -e value ] shortcut for --tag e= - -p value [ -p value ] shortcut for --tag p= - -~> nak req --help -NAME: - nak req - generates encoded REQ messages and optionally use them to talk to relays - -USAGE: - nak req [command options] [relay...] - -DESCRIPTION: - outputs a NIP-01 Nostr filter. when a relay is not given, will print the filter, otherwise will connect to the given relay and send the filter. - - example usage (with 'nostcat'): - nak req -k 1 -a 3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d | nostcat wss://nos.lol - standalone: - nak req -k 1 wss://nos.lol - -OPTIONS: - --bare when printing the filter, print just the filter, not enveloped in a ["REQ", ...] array (default: false) - --stream keep the subscription open, printing all events as they are returned (default: false, will close on EOSE) - - FILTER ATTRIBUTES - - --author value, -a value [ --author value, -a value ] only accept events from these authors (pubkey as hex) - --id value, -i value [ --id value, -i value ] only accept events with these ids (hex) - --kind value, -k value [ --kind value, -k value ] only accept events with these kind numbers - --limit value, -l value only accept up to this number of events (default: 0) - --since value, -s value only accept events newer than this (unix timestamp) (default: 0) - --tag value, -t value [ --tag value, -t value ] takes a tag like -t e=, only accept events with these tags - --until value, -u value only accept events older than this (unix timestamp) (default: 0) - -e value [ -e value ] shortcut for --tag e= - -p value [ -p value ] shortcut for --tag p= - - -OPTIONS: - --bare when printing the filter, print just the filter, not enveloped in a ["REQ", ...] array (default: false) - --stream keep the subscription open, printing all events as they are returned (default: false, will close on EOSE) - - FILTER ATTRIBUTES - - --author value, -a value [ --author value, -a value ] only accept events from these authors (pubkey as hex) - --id value, -i value [ --id value, -i value ] only accept events with these ids (hex) - --kind value, -k value [ --kind value, -k value ] only accept events with these kind numbers - --limit value, -l value only accept up to this number of events (default: 0) - --since value, -s value only accept events newer than this (unix timestamp) (default: 0) - --tag value, -t value [ --tag value, -t value ] takes a tag like -t e=, only accept events with these tags - --until value, -u value only accept events older than this (unix timestamp) (default: 0) - -e value [ -e value ] shortcut for --tag e= - -p value [ -p value ] shortcut for --tag p= - -~> nak count --help -NAME: - nak count - generates encoded COUNT messages and optionally use them to talk to relays - -USAGE: - nak count [command options] [relay...] - -DESCRIPTION: - outputs a NIP-45 request. Mostly same as req. - - example usage (with 'nostcat'): - nak count -k 1 -a 3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d | nostcat wss://nos.lol - standalone: - nak count -k 1 wss://nos.lol - -OPTIONS: - --bare when printing the filter, print just the filter, not enveloped in a ["COUNT", ...] array (default: false) - --stream keep the subscription open, printing all events as they are returned (default: false, will close on EOSE) - - FILTER ATTRIBUTES - - --author value, -a value [ --author value, -a value ] only accept events from these authors (pubkey as hex) - --id value, -i value [ --id value, -i value ] only accept events with these ids (hex) - --kind value, -k value [ --kind value, -k value ] only accept events with these kind numbers - --limit value, -l value only accept up to this number of events (default: 0) - --since value, -s value only accept events newer than this (unix timestamp) (default: 0) - --tag value, -t value [ --tag value, -t value ] takes a tag like -t e=, only accept events with these tags - --until value, -u value only accept events older than this (unix timestamp) (default: 0) - -e value [ -e value ] shortcut for --tag e= - -p value [ -p value ] shortcut for --tag p= - -~> nak decode --help -NAME: - nak decode - decodes nip19, nip21, nip05 or hex entities - -USAGE: - nak decode [command options] - -DESCRIPTION: - example usage: - nak decode npub1uescmd5krhrmj9rcura833xpke5eqzvcz5nxjw74ufeewf2sscxq4g7chm - nak decode nevent1qqs29yet5tp0qq5xu5qgkeehkzqh5qu46739axzezcxpj4tjlkx9j7gpr4mhxue69uhkummnw3ez6ur4vgh8wetvd3hhyer9wghxuet5sh59ud - nak decode nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpz4mhxue69uhk2er9dchxummnw3ezumrpdejqz8thwden5te0dehhxarj94c82c3wwajkcmr0wfjx2u3wdejhgqgcwaehxw309aex2mrp0yhxummnw3exzarf9e3k7mgnp0sh5 - nak decode nsec1jrmyhtjhgd9yqalps8hf9mayvd58852gtz66m7tqpacjedkp6kxq4dyxsr - -OPTIONS: - --id, -e return just the event id, if applicable (default: false) - --pubkey, -p return just the pubkey, if applicable (default: false) - --help, -h show help - - -~> nak encode --help -NAME: - nak encode - encodes notes and other stuff to nip19 entities - -USAGE: - nak encode command [command options] [arguments...] - -DESCRIPTION: - example usage: - nak encode npub - nak encode nprofile - nak encode nprofile --relay - nak encode nevent - nak encode nevent --author --relay --relay - nak encode nsec - -COMMANDS: - npub encode a hex private key into bech32 'npub' format - nsec encode a hex private key into bech32 'nsec' format - nprofile generate profile codes with attached relay information - nevent generate event codes with optionally attached relay information - naddr generate codes for NIP-33 parameterized replaceable events - help, h Shows a list of commands or help for one command - -OPTIONS: - --help, -h show help +### fetch an event using relay and author hints automatically from a nevent1 code, pretty-print it +```shell +nak fetch nevent1qqs2e3k48vtrkzjm8vvyzcmsmkf58unrxtq2k4h5yspay6vhcqm4wqcpz9mhxue69uhkummnw3ezuamfdejj7q3ql2vyh47mk2p0qlsku7hg0vn29faehy9hy34ygaclpn66ukqp3afqxpqqqqqqz7ttjyq | jq +{ + "id": "acc6d53b163b0a5b3b18416370dd9343f26332c0ab56f42403d26997c0375703", + "pubkey": "fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52", + "created_at": 1697370933, + "kind": 1, + "tags": [], + "content": "`q` tags = a kind 1 that wanted to be a kind:6 but fell short\n\n🥁", + "sig": "b5b63d7c8491a4a0517df2c58151665c583abc6cd31fd50b957bf8fefc8e55c87c922cbdcb50888cb9f1c03c26ab5c02c1dccc14b46b78e1e16c60094f2358da" +} ``` -written in go using [go-nostr](https://github.com/nbd-wtf/go-nostr), heavily inspired by [nostril](http://git.jb55.com/nostril/). - -## a toolkit for debugging all things nostr as a webpage: - -![](https://user-images.githubusercontent.com/1653275/227681805-0cd20b39-de0d-4fcb-abb4-de3283404e8f.png) - -written in [scala](https://scala-lang.org/) with [calico](https://www.armanbilge.com/calico/) and [snow](https://github.com/fiatjaf/snow) +### republish an event from one relay to multiple others +```shell +~> nak req -i e20978737ab7cd36eca300a65f11738176123f2e0c23054544b18fe493e2aa1a wss://nostr.wine/ wss://nostr-pub.wellorder.net | nak event wss://nostr.wine wss://offchain.pub wss://public.relaying.io wss://eden.nostr.land wss://atlas.nostr.land wss://relayable.org +{"id":"e20978737ab7cd36eca300a65f11738176123f2e0c23054544b18fe493e2aa1a","pubkey":"c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5","created_at":1698632753,"kind":1,"tags":[["t","gm"]],"content":"good morning","sig":"5687c1a97066c349cb3bde0c0719fd1652a13403ba6aca7557b646307ee6718528cd86989db08bf6a7fd04bea0b0b87c1dd1b78c2d21b80b80eebab7f40b8916"} +publishing to wss://nostr.wine... failed: msg: blocked: not an active paid member +publishing to wss://offchain.pub... success. +publishing to wss://public.relaying.io... success. +publishing to wss://eden.nostr.land... failed: msg: blocked: not on white-list +publishing to wss://atlas.nostr.land... failed: msg: blocked: not on white-list +publishing to wss://relayable.org... success. +``` From e681ad87cd5bbacdc8dd9746f4cad1dc5a82c85f Mon Sep 17 00:00:00 2001 From: Bitkarrot <73979971+bitkarrot@users.noreply.github.com> Date: Mon, 30 Oct 2023 18:53:08 -0700 Subject: [PATCH 020/401] fix install instructions (#5) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 408b2e8..bb8e68f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # nak, the nostr army knife -install with `go install github.com/fiatjaf/nak` or +install with `go install github.com/fiatjaf/nak@latest` or [download a binary](https://github.com/fiatjaf/nak/releases). ## what can you do with it? From be8e3dfb396694e2b2c83985bedb5b17a6c0f102 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sun, 29 Oct 2023 23:55:40 -0300 Subject: [PATCH 021/401] tweak gh actions build settings. --- .github/workflows/release-cli.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml index 005288f..32dd6b7 100644 --- a/.github/workflows/release-cli.yml +++ b/.github/workflows/release-cli.yml @@ -37,3 +37,6 @@ jobs: goos: ${{ matrix.goos }} goarch: ${{ matrix.goarch }} overwrite: true + md5sum: false + sha256sum: false + compress_assets: false From 7bce92f56d764e75befd234c41f734537778dee6 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Thu, 2 Nov 2023 08:10:13 -0300 Subject: [PATCH 022/401] nak verify --- README.md | 6 ++++++ main.go | 1 + verify.go | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 44 insertions(+) create mode 100644 verify.go diff --git a/README.md b/README.md index bb8e68f..fa4ba53 100644 --- a/README.md +++ b/README.md @@ -70,3 +70,9 @@ publishing to wss://eden.nostr.land... failed: msg: blocked: not on white-list publishing to wss://atlas.nostr.land... failed: msg: blocked: not on white-list publishing to wss://relayable.org... success. ``` + +### verify if an event is good +```shell +~> echo '{"content":"hello world","created_at":1698923350,"id":"05bd99d54cb835f327e0092c4275ee44c7ff51219eff417c19f70c9e2c53ad5a","kind":1,"pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","sig":"0a04a296321ed933858577f36fb2fb9a0933e966f9ee32b539493f5a4d00120891b1ca9152ebfbc04fb403bdaa7c73f415e7c4954e55726b4b4fa8cebf008cd6","tags":[]}' | nak verify +invalid .id, expected 05bd99d54cb835f427e0092c4275ee44c7ff51219eff417c19f70c9e2c53ad5a, got 05bd99d54cb835f327e0092c4275ee44c7ff51219eff417c19f70c9e2c53ad5a +``` diff --git a/main.go b/main.go index 15551ca..d3c73c1 100644 --- a/main.go +++ b/main.go @@ -18,6 +18,7 @@ func main() { event, decode, encode, + verify, }, } diff --git a/verify.go b/verify.go new file mode 100644 index 0000000..b428b18 --- /dev/null +++ b/verify.go @@ -0,0 +1,37 @@ +package main + +import ( + "encoding/json" + "fmt" + + "github.com/nbd-wtf/go-nostr" + "github.com/urfave/cli/v2" +) + +var verify = &cli.Command{ + Name: "verify", + Usage: "checks the hash and signature of an event given through stdin", + Description: `example: + echo '{"id":"a889df6a387419ff204305f4c2d296ee328c3cd4f8b62f205648a541b4554dfb","pubkey":"c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5","created_at":1698623783,"kind":1,"tags":[],"content":"hello from the nostr army knife","sig":"84876e1ee3e726da84e5d195eb79358b2b3eaa4d9bd38456fde3e8a2af3f1cd4cda23f23fda454869975b3688797d4c66e12f4c51c1b43c6d2997c5e61865661"}' | nak verify + +it outputs nothing if the verification is successful. +`, + Action: func(c *cli.Context) error { + evt := nostr.Event{} + if stdinEvent := getStdin(); stdinEvent != "" { + if err := json.Unmarshal([]byte(stdinEvent), &evt); err != nil { + return fmt.Errorf("invalid JSON: %w", err) + } + } + + if evt.GetID() != evt.ID { + return fmt.Errorf("invalid .id, expected %s, got %s", evt.GetID(), evt.ID) + } + + if ok, err := evt.CheckSignature(); !ok { + return fmt.Errorf("invalid signature: %w", err) + } + + return nil + }, +} From 31b42c34993ab274378171be771b38437ad22770 Mon Sep 17 00:00:00 2001 From: fiatjaf_ Date: Sat, 4 Nov 2023 08:02:34 -0300 Subject: [PATCH 023/401] public domain license. --- LICENSE | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..fdddb29 --- /dev/null +++ b/LICENSE @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to From 78932833df423d9d14493a6398ed58c333e76da9 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 7 Nov 2023 17:57:43 -0300 Subject: [PATCH 024/401] support running nak with multiple lines of stdin sequentially. --- event.go | 191 +++++++++++++++++++++++++++-------------------------- helpers.go | 52 ++++++++++++--- req.go | 154 +++++++++++++++++++++--------------------- verify.go | 31 +++++---- 4 files changed, 237 insertions(+), 191 deletions(-) diff --git a/event.go b/event.go index 57a59fa..a060025 100644 --- a/event.go +++ b/event.go @@ -90,115 +90,118 @@ example: }, ArgsUsage: "[relay...]", Action: func(c *cli.Context) error { - evt := nostr.Event{ - Tags: make(nostr.Tags, 0, 3), - } - - mustRehashAndResign := false - - if stdinEvent := getStdin(); stdinEvent != "" { - if err := json.Unmarshal([]byte(stdinEvent), &evt); err != nil { - return fmt.Errorf("invalid event received from stdin: %w", err) + for stdinEvent := range getStdinLinesOrBlank() { + evt := nostr.Event{ + Tags: make(nostr.Tags, 0, 3), } - } - if kind := c.Int("kind"); kind != 0 { - evt.Kind = kind - mustRehashAndResign = true - } else if evt.Kind == 0 { - evt.Kind = 1 - mustRehashAndResign = true - } - - if content := c.String("content"); content != "" { - evt.Content = content - mustRehashAndResign = true - } else if evt.Content == "" && evt.Kind == 1 { - evt.Content = "hello from the nostr army knife" - mustRehashAndResign = true - } - - tags := make(nostr.Tags, 0, 5) - for _, tagFlag := range c.StringSlice("tag") { - // tags are in the format key=value - spl := strings.Split(tagFlag, "=") - if len(spl) == 2 && len(spl[0]) > 0 { - tag := nostr.Tag{spl[0]} - // tags may also contain extra elements separated with a ";" - spl2 := strings.Split(spl[1], ";") - tag = append(tag, spl2...) - // ~ - tags = append(tags, tag) - } - } - for _, etag := range c.StringSlice("e") { - tags = append(tags, []string{"e", etag}) - mustRehashAndResign = true - } - for _, ptag := range c.StringSlice("p") { - tags = append(tags, []string{"p", ptag}) - mustRehashAndResign = true - } - if len(tags) > 0 { - for _, tag := range tags { - evt.Tags = append(evt.Tags, tag) - } - mustRehashAndResign = true - } - - if createdAt := c.String("created-at"); createdAt != "" { - ts := time.Now() - if createdAt != "now" { - if v, err := strconv.ParseInt(createdAt, 10, 64); err != nil { - return fmt.Errorf("failed to parse timestamp '%s': %w", createdAt, err) - } else { - ts = time.Unix(v, 0) + mustRehashAndResign := false + if stdinEvent != "" { + if err := json.Unmarshal([]byte(stdinEvent), &evt); err != nil { + lineProcessingError(c, "invalid event received from stdin: %s", err) + continue } } - evt.CreatedAt = nostr.Timestamp(ts.Unix()) - mustRehashAndResign = true - } else if evt.CreatedAt == 0 { - evt.CreatedAt = nostr.Now() - mustRehashAndResign = true - } - if evt.Sig == "" || mustRehashAndResign { - if err := evt.Sign(c.String("sec")); err != nil { - return fmt.Errorf("error signing with provided key: %w", err) + if kind := c.Int("kind"); kind != 0 { + evt.Kind = kind + mustRehashAndResign = true + } else if evt.Kind == 0 { + evt.Kind = 1 + mustRehashAndResign = true } - } - relays := c.Args().Slice() - if len(relays) > 0 { - fmt.Println(evt.String()) - for _, url := range relays { - fmt.Fprintf(os.Stderr, "publishing to %s... ", url) - if relay, err := nostr.RelayConnect(c.Context, url); err != nil { - fmt.Fprintf(os.Stderr, "failed to connect: %s\n", err) - } else { - ctx, cancel := context.WithTimeout(c.Context, 10*time.Second) - defer cancel() - if status, err := relay.Publish(ctx, evt); err != nil { - fmt.Fprintf(os.Stderr, "failed: %s\n", err) + if content := c.String("content"); content != "" { + evt.Content = content + mustRehashAndResign = true + } else if evt.Content == "" && evt.Kind == 1 { + evt.Content = "hello from the nostr army knife" + mustRehashAndResign = true + } + + tags := make(nostr.Tags, 0, 5) + for _, tagFlag := range c.StringSlice("tag") { + // tags are in the format key=value + spl := strings.Split(tagFlag, "=") + if len(spl) == 2 && len(spl[0]) > 0 { + tag := nostr.Tag{spl[0]} + // tags may also contain extra elements separated with a ";" + spl2 := strings.Split(spl[1], ";") + tag = append(tag, spl2...) + // ~ + tags = append(tags, tag) + } + } + for _, etag := range c.StringSlice("e") { + tags = append(tags, []string{"e", etag}) + mustRehashAndResign = true + } + for _, ptag := range c.StringSlice("p") { + tags = append(tags, []string{"p", ptag}) + mustRehashAndResign = true + } + if len(tags) > 0 { + for _, tag := range tags { + evt.Tags = append(evt.Tags, tag) + } + mustRehashAndResign = true + } + + if createdAt := c.String("created-at"); createdAt != "" { + ts := time.Now() + if createdAt != "now" { + if v, err := strconv.ParseInt(createdAt, 10, 64); err != nil { + return fmt.Errorf("failed to parse timestamp '%s': %w", createdAt, err) } else { - fmt.Fprintf(os.Stderr, "%s.\n", status) + ts = time.Unix(v, 0) } } + evt.CreatedAt = nostr.Timestamp(ts.Unix()) + mustRehashAndResign = true + } else if evt.CreatedAt == 0 { + evt.CreatedAt = nostr.Now() + mustRehashAndResign = true } - } else { - var result string - if c.Bool("envelope") { - j, _ := json.Marshal([]any{"EVENT", evt}) - result = string(j) - } else if c.Bool("nson") { - result, _ = nson.Marshal(&evt) + + if evt.Sig == "" || mustRehashAndResign { + if err := evt.Sign(c.String("sec")); err != nil { + return fmt.Errorf("error signing with provided key: %w", err) + } + } + + relays := c.Args().Slice() + if len(relays) > 0 { + fmt.Println(evt.String()) + for _, url := range relays { + fmt.Fprintf(os.Stderr, "publishing to %s... ", url) + if relay, err := nostr.RelayConnect(c.Context, url); err != nil { + fmt.Fprintf(os.Stderr, "failed to connect: %s\n", err) + } else { + ctx, cancel := context.WithTimeout(c.Context, 10*time.Second) + defer cancel() + if status, err := relay.Publish(ctx, evt); err != nil { + fmt.Fprintf(os.Stderr, "failed: %s\n", err) + } else { + fmt.Fprintf(os.Stderr, "%s.\n", status) + } + } + } } else { - j, _ := easyjson.Marshal(&evt) - result = string(j) + var result string + if c.Bool("envelope") { + j, _ := json.Marshal([]any{"EVENT", evt}) + result = string(j) + } else if c.Bool("nson") { + result, _ = nson.Marshal(&evt) + } else { + j, _ := easyjson.Marshal(&evt) + result = string(j) + } + fmt.Println(result) } - fmt.Println(result) } + exitIfLineProcessingError(c) return nil }, } diff --git a/helpers.go b/helpers.go index 67805bb..80d9371 100644 --- a/helpers.go +++ b/helpers.go @@ -1,7 +1,9 @@ package main import ( + "bufio" "bytes" + "context" "fmt" "io" "net/url" @@ -11,7 +13,36 @@ import ( "github.com/urfave/cli/v2" ) -func getStdin() string { +const ( + LINE_PROCESSING_ERROR = iota +) + +func getStdinLinesOrBlank() chan string { + ch := make(chan string) + go func() { + r := bufio.NewReader(os.Stdin) + if _, err := r.Peek(1); err != nil { + ch <- "" + close(ch) + } else { + scanner := bufio.NewScanner(r) + for scanner.Scan() { + ch <- scanner.Text() + } + close(ch) + } + }() + return ch +} + +func getStdinOrFirstArgument(c *cli.Context) string { + // try the first argument + target := c.Args().First() + if target != "" { + return target + } + + // try the stdin stat, _ := os.Stdin.Stat() if (stat.Mode() & os.ModeCharDevice) == 0 { read := bytes.NewBuffer(make([]byte, 0, 1000)) @@ -23,14 +54,6 @@ func getStdin() string { return "" } -func getStdinOrFirstArgument(c *cli.Context) string { - target := c.Args().First() - if target != "" { - return target - } - return getStdin() -} - func validateRelayURLs(wsurls []string) error { for _, wsurl := range wsurls { u, err := url.Parse(wsurl) @@ -49,3 +72,14 @@ func validateRelayURLs(wsurls []string) error { return nil } + +func lineProcessingError(c *cli.Context, msg string, args ...any) { + c.Context = context.WithValue(c.Context, LINE_PROCESSING_ERROR, true) + fmt.Fprintf(os.Stderr, msg+"\n", args...) +} + +func exitIfLineProcessingError(c *cli.Context) { + if val := c.Context.Value(LINE_PROCESSING_ERROR); val != nil && val.(bool) { + os.Exit(123) + } +} diff --git a/req.go b/req.go index c8f3985..499b411 100644 --- a/req.go +++ b/req.go @@ -95,87 +95,91 @@ example: }, ArgsUsage: "[relay...]", Action: func(c *cli.Context) error { - filter := nostr.Filter{} - if stdinFilter := getStdin(); stdinFilter != "" { - if err := json.Unmarshal([]byte(stdinFilter), &filter); err != nil { - return fmt.Errorf("invalid filter received from stdin: %w", err) + for stdinFilter := range getStdinLinesOrBlank() { + filter := nostr.Filter{} + if stdinFilter != "" { + if err := json.Unmarshal([]byte(stdinFilter), &filter); err != nil { + lineProcessingError(c, "invalid filter received from stdin: %s", err) + continue + } } - } - if authors := c.StringSlice("author"); len(authors) > 0 { - filter.Authors = append(filter.Authors, authors...) - } - if ids := c.StringSlice("id"); len(ids) > 0 { - filter.IDs = append(filter.IDs, ids...) - } - if kinds := c.IntSlice("kind"); len(kinds) > 0 { - filter.Kinds = append(filter.Kinds, kinds...) - } - if search := c.String("search"); search != "" { - filter.Search = search - } - tags := make([][]string, 0, 5) - for _, tagFlag := range c.StringSlice("tag") { - spl := strings.Split(tagFlag, "=") - if len(spl) == 2 && len(spl[0]) == 1 { - tags = append(tags, spl) + if authors := c.StringSlice("author"); len(authors) > 0 { + filter.Authors = append(filter.Authors, authors...) + } + if ids := c.StringSlice("id"); len(ids) > 0 { + filter.IDs = append(filter.IDs, ids...) + } + if kinds := c.IntSlice("kind"); len(kinds) > 0 { + filter.Kinds = append(filter.Kinds, kinds...) + } + if search := c.String("search"); search != "" { + filter.Search = search + } + tags := make([][]string, 0, 5) + for _, tagFlag := range c.StringSlice("tag") { + spl := strings.Split(tagFlag, "=") + if len(spl) == 2 && len(spl[0]) == 1 { + tags = append(tags, spl) + } else { + return fmt.Errorf("invalid --tag '%s'", tagFlag) + } + } + for _, etag := range c.StringSlice("e") { + tags = append(tags, []string{"e", etag}) + } + for _, ptag := range c.StringSlice("p") { + tags = append(tags, []string{"p", ptag}) + } + + if len(tags) > 0 && filter.Tags == nil { + filter.Tags = make(nostr.TagMap) + } + + for _, tag := range tags { + if _, ok := filter.Tags[tag[0]]; !ok { + filter.Tags[tag[0]] = make([]string, 0, 3) + } + filter.Tags[tag[0]] = append(filter.Tags[tag[0]], tag[1]) + } + + if since := c.Int("since"); since != 0 { + ts := nostr.Timestamp(since) + filter.Since = &ts + } + if until := c.Int("until"); until != 0 { + ts := nostr.Timestamp(until) + filter.Until = &ts + } + if limit := c.Int("limit"); limit != 0 { + filter.Limit = limit + } + + relays := c.Args().Slice() + if len(relays) > 0 { + pool := nostr.NewSimplePool(c.Context) + fn := pool.SubManyEose + if c.Bool("stream") { + fn = pool.SubMany + } + for ie := range fn(c.Context, relays, nostr.Filters{filter}) { + fmt.Println(ie.Event) + } } else { - return fmt.Errorf("invalid --tag '%s'", tagFlag) + // no relays given, will just print the filter + var result string + if c.Bool("bare") { + result = filter.String() + } else { + j, _ := json.Marshal([]any{"REQ", "nak", filter}) + result = string(j) + } + + fmt.Println(result) } } - for _, etag := range c.StringSlice("e") { - tags = append(tags, []string{"e", etag}) - } - for _, ptag := range c.StringSlice("p") { - tags = append(tags, []string{"p", ptag}) - } - - if len(tags) > 0 && filter.Tags == nil { - filter.Tags = make(nostr.TagMap) - } - - for _, tag := range tags { - if _, ok := filter.Tags[tag[0]]; !ok { - filter.Tags[tag[0]] = make([]string, 0, 3) - } - filter.Tags[tag[0]] = append(filter.Tags[tag[0]], tag[1]) - } - - if since := c.Int("since"); since != 0 { - ts := nostr.Timestamp(since) - filter.Since = &ts - } - if until := c.Int("until"); until != 0 { - ts := nostr.Timestamp(until) - filter.Until = &ts - } - if limit := c.Int("limit"); limit != 0 { - filter.Limit = limit - } - - relays := c.Args().Slice() - if len(relays) > 0 { - pool := nostr.NewSimplePool(c.Context) - fn := pool.SubManyEose - if c.Bool("stream") { - fn = pool.SubMany - } - for ie := range fn(c.Context, relays, nostr.Filters{filter}) { - fmt.Println(ie.Event) - } - } else { - // no relays given, will just print the filter - var result string - if c.Bool("bare") { - result = filter.String() - } else { - j, _ := json.Marshal([]any{"REQ", "nak", filter}) - result = string(j) - } - - fmt.Println(result) - } + exitIfLineProcessingError(c) return nil }, } diff --git a/verify.go b/verify.go index b428b18..f31aa19 100644 --- a/verify.go +++ b/verify.go @@ -2,7 +2,6 @@ package main import ( "encoding/json" - "fmt" "github.com/nbd-wtf/go-nostr" "github.com/urfave/cli/v2" @@ -17,21 +16,27 @@ var verify = &cli.Command{ it outputs nothing if the verification is successful. `, Action: func(c *cli.Context) error { - evt := nostr.Event{} - if stdinEvent := getStdin(); stdinEvent != "" { - if err := json.Unmarshal([]byte(stdinEvent), &evt); err != nil { - return fmt.Errorf("invalid JSON: %w", err) + for stdinEvent := range getStdinLinesOrBlank() { + evt := nostr.Event{} + if stdinEvent != "" { + if err := json.Unmarshal([]byte(stdinEvent), &evt); err != nil { + lineProcessingError(c, "invalid event: %s", err) + continue + } + } + + if evt.GetID() != evt.ID { + lineProcessingError(c, "invalid .id, expected %s, got %s", evt.GetID(), evt.ID) + continue + } + + if ok, err := evt.CheckSignature(); !ok { + lineProcessingError(c, "invalid signature: %s", err) + continue } } - if evt.GetID() != evt.ID { - return fmt.Errorf("invalid .id, expected %s, got %s", evt.GetID(), evt.ID) - } - - if ok, err := evt.CheckSignature(); !ok { - return fmt.Errorf("invalid signature: %w", err) - } - + exitIfLineProcessingError(c) return nil }, } From 6f72d3c1335a8aa455354c679d382d83d311cbc9 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 7 Nov 2023 23:51:07 -0300 Subject: [PATCH 025/401] fix pipe check. --- helpers.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/helpers.go b/helpers.go index 80d9371..e172f26 100644 --- a/helpers.go +++ b/helpers.go @@ -20,17 +20,17 @@ const ( func getStdinLinesOrBlank() chan string { ch := make(chan string) go func() { - r := bufio.NewReader(os.Stdin) - if _, err := r.Peek(1); err != nil { - ch <- "" - close(ch) - } else { - scanner := bufio.NewScanner(r) + if stat, _ := os.Stdin.Stat(); stat.Mode()&os.ModeCharDevice == 0 { + // piped + scanner := bufio.NewScanner(os.Stdin) for scanner.Scan() { ch <- scanner.Text() } - close(ch) + } else { + // not piped + ch <- "" } + close(ch) }() return ch } From 5722061bf3064aa0215ee78768b02436156a06ce Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Wed, 8 Nov 2023 00:13:28 -0300 Subject: [PATCH 026/401] update go-nostr and sdk to standalone module. --- decode.go | 2 +- fetch.go | 2 +- go.mod | 26 +++++++++++++++----------- go.sum | 45 +++++++++++++++++++++++++++++---------------- 4 files changed, 46 insertions(+), 29 deletions(-) diff --git a/decode.go b/decode.go index 76991b1..c1929c9 100644 --- a/decode.go +++ b/decode.go @@ -8,7 +8,7 @@ import ( "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/nip19" - "github.com/nbd-wtf/go-nostr/sdk" + sdk "github.com/nbd-wtf/nostr-sdk" "github.com/urfave/cli/v2" ) diff --git a/fetch.go b/fetch.go index d626f32..d532d3b 100644 --- a/fetch.go +++ b/fetch.go @@ -5,7 +5,7 @@ import ( "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/nip19" - "github.com/nbd-wtf/go-nostr/sdk" + sdk "github.com/nbd-wtf/nostr-sdk" "github.com/urfave/cli/v2" ) diff --git a/go.mod b/go.mod index 1a60ce6..e39f434 100644 --- a/go.mod +++ b/go.mod @@ -1,35 +1,39 @@ module github.com/fiatjaf/nak -go 1.20 +go 1.21 + +toolchain go1.21.0 require ( github.com/mailru/easyjson v0.7.7 - github.com/nbd-wtf/go-nostr v0.24.2 + github.com/nbd-wtf/go-nostr v0.25.3 + github.com/nbd-wtf/nostr-sdk v0.0.2 github.com/urfave/cli/v2 v2.25.3 ) require ( - github.com/btcsuite/btcd/btcec/v2 v2.2.0 // indirect + github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect github.com/btcsuite/btcd/btcutil v1.1.3 // indirect - github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 // indirect - github.com/cespare/xxhash/v2 v2.1.1 // indirect + github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect - github.com/decred/dcrd/crypto/blake256 v1.0.0 // indirect - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect + github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect github.com/dgraph-io/ristretto v0.1.1 // indirect github.com/dustin/go-humanize v1.0.0 // indirect + github.com/fiatjaf/eventstore v0.1.0 // indirect github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/pool v0.2.1 // indirect github.com/gobwas/ws v1.2.0 // indirect - github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect + github.com/golang/glog v1.0.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/puzpuzpuz/xsync/v2 v2.5.0 // indirect + github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/tidwall/gjson v1.14.4 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.0 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect - golang.org/x/exp v0.0.0-20221106115401-f9659909a136 // indirect - golang.org/x/sys v0.6.0 // indirect + golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 // indirect + golang.org/x/sys v0.8.0 // indirect ) diff --git a/go.sum b/go.sum index 9204f7d..3237f09 100644 --- a/go.sum +++ b/go.sum @@ -4,15 +4,16 @@ github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tj github.com/btcsuite/btcd v0.23.0/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY= github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA= github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= -github.com/btcsuite/btcd/btcec/v2 v2.2.0 h1:fzn1qaOt32TuLjFlkzYSsBC35Q3KUjT1SwPxiMSCF5k= -github.com/btcsuite/btcd/btcec/v2 v2.2.0/go.mod h1:U7MHm051Al6XmscBQ0BoNydpOTsFAn707034b5nY8zU= +github.com/btcsuite/btcd/btcec/v2 v2.3.2 h1:5n0X6hX0Zk+6omWcihdYvdAlGf2DfasC0GMf7DClJ3U= +github.com/btcsuite/btcd/btcec/v2 v2.3.2/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A= github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE= github.com/btcsuite/btcd/btcutil v1.1.3 h1:xfbtw8lwpp0G6NwSHb+UE67ryTFHJAiNuipusjXSohQ= github.com/btcsuite/btcd/btcutil v1.1.3/go.mod h1:UR7dsSJzJUfMmFiiLlIrMq1lS9jh9EdCV7FStZSnpi0= github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= -github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U= github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2 h1:KdUfX2zKommPRa+PD0sWZUyXe9w277ABlgELO7H04IM= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= @@ -22,18 +23,21 @@ github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= -github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= +github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y= +github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8= github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= @@ -41,6 +45,8 @@ github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczC github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/fiatjaf/eventstore v0.1.0 h1:/g7VTw6dsXmjICD3rBuHNIvAammHJ5unrKJ71Dz+VTs= +github.com/fiatjaf/eventstore v0.1.0/go.mod h1:juMei5HL3HJi6t7vZjj7VdEItDPu31+GLROepdUK4tw= 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/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= @@ -49,8 +55,9 @@ github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.2.0 h1:u0p9s3xLYpZCA1z5JgCkMeB34CKCMMQbM+G8Ii7YD0I= github.com/gobwas/ws v1.2.0/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ= +github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= @@ -71,8 +78,10 @@ github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlT github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/nbd-wtf/go-nostr v0.24.2 h1:1PdFED7uHh3BlXfDVD96npBc0YAgj9hPT+l6NWog4kc= -github.com/nbd-wtf/go-nostr v0.24.2/go.mod h1:eE8Qf8QszZbCd9arBQyotXqATNUElWsTEEx+LLORhyQ= +github.com/nbd-wtf/go-nostr v0.25.3 h1:RPPh4cOosw0OZi5KG627pZ3GlKxiKsjARluzen/mB9g= +github.com/nbd-wtf/go-nostr v0.25.3/go.mod h1:bkffJI+x914sPQWum9ZRUn66D7NpDnAoWo1yICvj3/0= +github.com/nbd-wtf/nostr-sdk v0.0.2 h1:mZIeti+DOF0D1179q+NLL/h0LVMMOPRQAYpOuUrn5Zk= +github.com/nbd-wtf/nostr-sdk v0.0.2/go.mod h1:KQZOtzcrXBlVhpZYG1tw83ADIONNMMPjUU3ZAH5U2RY= 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= @@ -86,14 +95,15 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/puzpuzpuz/xsync/v2 v2.5.0 h1:2k4qrO/orvmEXZ3hmtHqIy9XaQtPTwzMZk1+iErpE8c= -github.com/puzpuzpuz/xsync/v2 v2.5.0/go.mod h1:gD2H2krq/w52MfPLE+Uy64TzJDVY7lP2znR9qmR35kU= +github.com/puzpuzpuz/xsync/v2 v2.5.1 h1:mVGYAvzDSu52+zaGyNjC+24Xw2bQi3kTr4QJ6N9pIIU= +github.com/puzpuzpuz/xsync/v2 v2.5.1/go.mod h1:gD2H2krq/w52MfPLE+Uy64TzJDVY7lP2znR9qmR35kU= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= @@ -108,14 +118,15 @@ github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsr golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/exp v0.0.0-20221106115401-f9659909a136 h1:Fq7F/w7MAa1KJ5bt2aJ62ihqp9HDcRuyILskkpIAurw= -golang.org/x/exp v0.0.0-20221106115401-f9659909a136/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= +golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 h1:5llv2sWeaMSnA3w2kS57ouQQ4pudlXrR0dCgw51QK9o= +golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc h1:zK/HqS5bZxDptfPJNq8v7vJfXtkU7r9TLIoSr1bXaP4= golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 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= @@ -127,8 +138,9 @@ golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -150,3 +162,4 @@ gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From 714d65312c196626636b6c0633eb9d34b0c22c05 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Wed, 8 Nov 2023 12:50:36 -0300 Subject: [PATCH 027/401] support multiline stdin on decode, encode and fetch, and improve the helpers. --- decode.go | 70 ++++++++++--------- encode.go | 197 ++++++++++++++++++++++++++++++----------------------- fetch.go | 112 +++++++++++++++--------------- helpers.go | 59 ++++++++-------- 4 files changed, 238 insertions(+), 200 deletions(-) diff --git a/decode.go b/decode.go index c1929c9..d8b0fd9 100644 --- a/decode.go +++ b/decode.go @@ -34,43 +34,45 @@ var decode = &cli.Command{ }, ArgsUsage: "", Action: func(c *cli.Context) error { - args := c.Args() - if args.Len() != 1 { - return fmt.Errorf("invalid number of arguments, need just one") - } - input := args.First() - if strings.HasPrefix(input, "nostr:") { - input = input[6:] - } - - var decodeResult DecodeResult - if b, err := hex.DecodeString(input); err == nil { - if len(b) == 64 { - decodeResult.HexResult.PossibleTypes = []string{"sig"} - decodeResult.HexResult.Signature = hex.EncodeToString(b) - } else if len(b) == 32 { - decodeResult.HexResult.PossibleTypes = []string{"pubkey", "private_key", "event_id"} - decodeResult.HexResult.ID = hex.EncodeToString(b) - decodeResult.HexResult.PrivateKey = hex.EncodeToString(b) - decodeResult.HexResult.PublicKey = hex.EncodeToString(b) - } else { - return fmt.Errorf("hex string with invalid number of bytes: %d", len(b)) + for input := range getStdinLinesOrFirstArgument(c) { + if strings.HasPrefix(input, "nostr:") { + input = input[6:] } - } else if evp := sdk.InputToEventPointer(input); evp != nil { - decodeResult = DecodeResult{EventPointer: evp} - } else if pp := sdk.InputToProfile(c.Context, input); pp != nil { - decodeResult = DecodeResult{ProfilePointer: pp} - } else if prefix, value, err := nip19.Decode(input); err == nil && prefix == "naddr" { - ep := value.(nostr.EntityPointer) - decodeResult = DecodeResult{EntityPointer: &ep} - } else if prefix, value, err := nip19.Decode(input); err == nil && prefix == "nsec" { - decodeResult.PrivateKey.PrivateKey = value.(string) - decodeResult.PrivateKey.PublicKey, _ = nostr.GetPublicKey(value.(string)) - } else { - return fmt.Errorf("couldn't decode input") + + var decodeResult DecodeResult + if b, err := hex.DecodeString(input); err == nil { + if len(b) == 64 { + decodeResult.HexResult.PossibleTypes = []string{"sig"} + decodeResult.HexResult.Signature = hex.EncodeToString(b) + } else if len(b) == 32 { + decodeResult.HexResult.PossibleTypes = []string{"pubkey", "private_key", "event_id"} + decodeResult.HexResult.ID = hex.EncodeToString(b) + decodeResult.HexResult.PrivateKey = hex.EncodeToString(b) + decodeResult.HexResult.PublicKey = hex.EncodeToString(b) + } else { + lineProcessingError(c, "hex string with invalid number of bytes: %d", len(b)) + continue + } + } else if evp := sdk.InputToEventPointer(input); evp != nil { + decodeResult = DecodeResult{EventPointer: evp} + } else if pp := sdk.InputToProfile(c.Context, input); pp != nil { + decodeResult = DecodeResult{ProfilePointer: pp} + } else if prefix, value, err := nip19.Decode(input); err == nil && prefix == "naddr" { + ep := value.(nostr.EntityPointer) + decodeResult = DecodeResult{EntityPointer: &ep} + } else if prefix, value, err := nip19.Decode(input); err == nil && prefix == "nsec" { + decodeResult.PrivateKey.PrivateKey = value.(string) + decodeResult.PrivateKey.PublicKey, _ = nostr.GetPublicKey(value.(string)) + } else { + lineProcessingError(c, "couldn't decode input '%s': %s", input, err) + continue + } + + fmt.Println(decodeResult.JSON()) + } - fmt.Println(decodeResult.JSON()) + exitIfLineProcessingError(c) return nil }, } diff --git a/encode.go b/encode.go index 3d6773a..2c337d0 100644 --- a/encode.go +++ b/encode.go @@ -28,36 +28,44 @@ var encode = &cli.Command{ Subcommands: []*cli.Command{ { Name: "npub", - Usage: "encode a hex private key into bech32 'npub' format", + Usage: "encode a hex public key into bech32 'npub' format", Action: func(c *cli.Context) error { - target := getStdinOrFirstArgument(c) - if err := validate32BytesHex(target); err != nil { - return err + for target := range getStdinLinesOrFirstArgument(c) { + if err := validate32BytesHex(target); err != nil { + lineProcessingError(c, "invalid public key: %s", target, err) + continue + } + + if npub, err := nip19.EncodePublicKey(target); err == nil { + fmt.Println(npub) + } else { + return err + } } - if npub, err := nip19.EncodePublicKey(target); err == nil { - fmt.Println(npub) - return nil - } else { - return err - } + exitIfLineProcessingError(c) + return nil }, }, { Name: "nsec", Usage: "encode a hex private key into bech32 'nsec' format", Action: func(c *cli.Context) error { - target := getStdinOrFirstArgument(c) - if err := validate32BytesHex(target); err != nil { - return err + for target := range getStdinLinesOrFirstArgument(c) { + if err := validate32BytesHex(target); err != nil { + lineProcessingError(c, "invalid private key: %s", target, err) + continue + } + + if npub, err := nip19.EncodePrivateKey(target); err == nil { + fmt.Println(npub) + } else { + return err + } } - if npub, err := nip19.EncodePrivateKey(target); err == nil { - fmt.Println(npub) - return nil - } else { - return err - } + exitIfLineProcessingError(c) + return nil }, }, { @@ -71,22 +79,26 @@ var encode = &cli.Command{ }, }, Action: func(c *cli.Context) error { - target := getStdinOrFirstArgument(c) - if err := validate32BytesHex(target); err != nil { - return err + for target := range getStdinLinesOrFirstArgument(c) { + if err := validate32BytesHex(target); err != nil { + lineProcessingError(c, "invalid public key: %s", target, err) + continue + } + + relays := c.StringSlice("relay") + if err := validateRelayURLs(relays); err != nil { + return err + } + + if npub, err := nip19.EncodeProfile(target, relays); err == nil { + fmt.Println(npub) + } else { + return err + } } - relays := c.StringSlice("relay") - if err := validateRelayURLs(relays); err != nil { - return err - } - - if npub, err := nip19.EncodeProfile(target, relays); err == nil { - fmt.Println(npub) - return nil - } else { - return err - } + exitIfLineProcessingError(c) + return nil }, }, { @@ -104,29 +116,33 @@ var encode = &cli.Command{ }, }, Action: func(c *cli.Context) error { - target := getStdinOrFirstArgument(c) - if err := validate32BytesHex(target); err != nil { - return err - } + for target := range getStdinLinesOrFirstArgument(c) { + if err := validate32BytesHex(target); err != nil { + lineProcessingError(c, "invalid event id: %s", target, err) + continue + } - author := c.String("author") - if author != "" { - if err := validate32BytesHex(author); err != nil { + author := c.String("author") + if author != "" { + if err := validate32BytesHex(author); err != nil { + return err + } + } + + relays := c.StringSlice("relay") + if err := validateRelayURLs(relays); err != nil { + return err + } + + if npub, err := nip19.EncodeEvent(target, relays, author); err == nil { + fmt.Println(npub) + } else { return err } } - relays := c.StringSlice("relay") - if err := validateRelayURLs(relays); err != nil { - return err - } - - if npub, err := nip19.EncodeEvent(target, relays, author); err == nil { - fmt.Println(npub) - return nil - } else { - return err - } + exitIfLineProcessingError(c) + return nil }, }, { @@ -136,7 +152,7 @@ var encode = &cli.Command{ &cli.StringFlag{ Name: "identifier", Aliases: []string{"d"}, - Usage: "the \"d\" tag identifier of this replaceable event", + Usage: "the \"d\" tag identifier of this replaceable event -- can also be read from stdin", Required: true, }, &cli.StringFlag{ @@ -158,49 +174,60 @@ var encode = &cli.Command{ }, }, Action: func(c *cli.Context) error { - pubkey := c.String("pubkey") - if err := validate32BytesHex(pubkey); err != nil { - return err + for d := range getStdinLinesOrBlank() { + pubkey := c.String("pubkey") + if err := validate32BytesHex(pubkey); err != nil { + return err + } + + kind := c.Int("kind") + if kind < 30000 || kind >= 40000 { + return fmt.Errorf("kind must be between 30000 and 39999, as per NIP-16, got %d", kind) + } + + if d == "" { + d = c.String("identifier") + if d == "" { + lineProcessingError(c, "\"d\" tag identifier can't be empty") + continue + } + } + + relays := c.StringSlice("relay") + if err := validateRelayURLs(relays); err != nil { + return err + } + + if npub, err := nip19.EncodeEntity(pubkey, kind, d, relays); err == nil { + fmt.Println(npub) + } else { + return err + } } - kind := c.Int("kind") - if kind < 30000 || kind >= 40000 { - return fmt.Errorf("kind must be between 30000 and 39999, as per NIP-16, got %d", kind) - } - - d := c.String("identifier") - if d == "" { - return fmt.Errorf("\"d\" tag identifier can't be empty") - } - - relays := c.StringSlice("relay") - if err := validateRelayURLs(relays); err != nil { - return err - } - - if npub, err := nip19.EncodeEntity(pubkey, kind, d, relays); err == nil { - fmt.Println(npub) - return nil - } else { - return err - } + exitIfLineProcessingError(c) + return nil }, }, { Name: "note", Usage: "generate note1 event codes (not recommended)", Action: func(c *cli.Context) error { - target := getStdinOrFirstArgument(c) - if err := validate32BytesHex(target); err != nil { - return err + for target := range getStdinLinesOrFirstArgument(c) { + if err := validate32BytesHex(target); err != nil { + lineProcessingError(c, "invalid event id: %s", target, err) + continue + } + + if note, err := nip19.EncodeNote(target); err == nil { + fmt.Println(note) + } else { + return err + } } - if npub, err := nip19.EncodeNote(target); err == nil { - fmt.Println(npub) - return nil - } else { - return err - } + exitIfLineProcessingError(c) + return nil }, }, }, diff --git a/fetch.go b/fetch.go index d532d3b..43c24ed 100644 --- a/fetch.go +++ b/fetch.go @@ -24,67 +24,71 @@ var fetch = &cli.Command{ }, ArgsUsage: "[nip19code]", Action: func(c *cli.Context) error { - filter := nostr.Filter{} - code := getStdinOrFirstArgument(c) + for code := range getStdinLinesOrFirstArgument(c) { + filter := nostr.Filter{} - prefix, value, err := nip19.Decode(code) - if err != nil { - return err - } - - relays := c.StringSlice("relay") - if err := validateRelayURLs(relays); err != nil { - return err - } - var authorHint string - - switch prefix { - case "nevent": - v := value.(nostr.EventPointer) - filter.IDs = append(filter.IDs, v.ID) - if v.Author != "" { - authorHint = v.Author + prefix, value, err := nip19.Decode(code) + if err != nil { + lineProcessingError(c, "failed to decode: %s", err) + continue } - relays = v.Relays - case "naddr": - v := value.(nostr.EntityPointer) - filter.Tags = nostr.TagMap{"d": []string{v.Identifier}} - filter.Kinds = append(filter.Kinds, v.Kind) - filter.Authors = append(filter.Authors, v.PublicKey) - authorHint = v.PublicKey - relays = v.Relays - case "nprofile": - v := value.(nostr.ProfilePointer) - filter.Authors = append(filter.Authors, v.PublicKey) - filter.Kinds = append(filter.Kinds, 0) - authorHint = v.PublicKey - relays = v.Relays - case "npub": - v := value.(string) - filter.Authors = append(filter.Authors, v) - filter.Kinds = append(filter.Kinds, 0) - authorHint = v - } - pool := nostr.NewSimplePool(c.Context) - if authorHint != "" { - relayList := sdk.FetchRelaysForPubkey(c.Context, pool, authorHint, - "wss://purplepag.es", "wss://offchain.pub", "wss://public.relaying.io") - for _, relayListItem := range relayList { - if relayListItem.Outbox { - relays = append(relays, relayListItem.URL) + relays := c.StringSlice("relay") + if err := validateRelayURLs(relays); err != nil { + return err + } + var authorHint string + + switch prefix { + case "nevent": + v := value.(nostr.EventPointer) + filter.IDs = append(filter.IDs, v.ID) + if v.Author != "" { + authorHint = v.Author + } + relays = v.Relays + case "naddr": + v := value.(nostr.EntityPointer) + filter.Tags = nostr.TagMap{"d": []string{v.Identifier}} + filter.Kinds = append(filter.Kinds, v.Kind) + filter.Authors = append(filter.Authors, v.PublicKey) + authorHint = v.PublicKey + relays = v.Relays + case "nprofile": + v := value.(nostr.ProfilePointer) + filter.Authors = append(filter.Authors, v.PublicKey) + filter.Kinds = append(filter.Kinds, 0) + authorHint = v.PublicKey + relays = v.Relays + case "npub": + v := value.(string) + filter.Authors = append(filter.Authors, v) + filter.Kinds = append(filter.Kinds, 0) + authorHint = v + } + + pool := nostr.NewSimplePool(c.Context) + if authorHint != "" { + relayList := sdk.FetchRelaysForPubkey(c.Context, pool, authorHint, + "wss://purplepag.es", "wss://offchain.pub", "wss://public.relaying.io") + for _, relayListItem := range relayList { + if relayListItem.Outbox { + relays = append(relays, relayListItem.URL) + } } } + + if len(relays) == 0 { + lineProcessingError(c, "no relay hints found") + continue + } + + for ie := range pool.SubManyEose(c.Context, relays, nostr.Filters{filter}) { + fmt.Println(ie.Event) + } } - if len(relays) == 0 { - return fmt.Errorf("no relay hints found") - } - - for ie := range pool.SubManyEose(c.Context, relays, nostr.Filters{filter}) { - fmt.Println(ie.Event) - } - + exitIfLineProcessingError(c) return nil }, } diff --git a/helpers.go b/helpers.go index e172f26..6f05666 100644 --- a/helpers.go +++ b/helpers.go @@ -2,10 +2,8 @@ package main import ( "bufio" - "bytes" "context" "fmt" - "io" "net/url" "os" "strings" @@ -18,40 +16,47 @@ const ( ) func getStdinLinesOrBlank() chan string { - ch := make(chan string) - go func() { - if stat, _ := os.Stdin.Stat(); stat.Mode()&os.ModeCharDevice == 0 { - // piped - scanner := bufio.NewScanner(os.Stdin) - for scanner.Scan() { - ch <- scanner.Text() - } - } else { - // not piped - ch <- "" - } - close(ch) - }() - return ch + multi := make(chan string) + if hasStdinLines := writeStdinLinesOrNothing(multi); !hasStdinLines { + single := make(chan string, 1) + single <- "" + close(single) + return single + } else { + return multi + } } -func getStdinOrFirstArgument(c *cli.Context) string { +func getStdinLinesOrFirstArgument(c *cli.Context) chan string { // try the first argument target := c.Args().First() if target != "" { - return target + single := make(chan string, 1) + single <- target + return single } // try the stdin - stat, _ := os.Stdin.Stat() - if (stat.Mode() & os.ModeCharDevice) == 0 { - read := bytes.NewBuffer(make([]byte, 0, 1000)) - _, err := io.Copy(read, os.Stdin) - if err == nil { - return strings.TrimSpace(read.String()) - } + multi := make(chan string) + writeStdinLinesOrNothing(multi) + return multi +} + +func writeStdinLinesOrNothing(ch chan string) (hasStdinLines bool) { + if stat, _ := os.Stdin.Stat(); stat.Mode()&os.ModeCharDevice == 0 { + // piped + go func() { + scanner := bufio.NewScanner(os.Stdin) + for scanner.Scan() { + ch <- strings.TrimSpace(scanner.Text()) + } + close(ch) + }() + return true + } else { + // not piped + return false } - return "" } func validateRelayURLs(wsurls []string) error { From 200e4e61f73fc1502126cf7b42eb9b2176812084 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Wed, 8 Nov 2023 12:56:38 -0300 Subject: [PATCH 028/401] add a more complex example of fetching subnotes to readme. --- README.md | 5 +++++ req.go | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index fa4ba53..fe93d49 100644 --- a/README.md +++ b/README.md @@ -76,3 +76,8 @@ publishing to wss://relayable.org... success. ~> echo '{"content":"hello world","created_at":1698923350,"id":"05bd99d54cb835f327e0092c4275ee44c7ff51219eff417c19f70c9e2c53ad5a","kind":1,"pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","sig":"0a04a296321ed933858577f36fb2fb9a0933e966f9ee32b539493f5a4d00120891b1ca9152ebfbc04fb403bdaa7c73f415e7c4954e55726b4b4fa8cebf008cd6","tags":[]}' | nak verify invalid .id, expected 05bd99d54cb835f427e0092c4275ee44c7ff51219eff417c19f70c9e2c53ad5a, got 05bd99d54cb835f327e0092c4275ee44c7ff51219eff417c19f70c9e2c53ad5a ``` + +### fetch all quoted events by a given pubkey in their last 100 notes +```shell +nak req -l 100 -k 1 -a 2edbcea694d164629854a52583458fd6d965b161e3c48b57d3aff01940558884 wss://relay.damus.io | jq -r '.content | match("nostr:((note1|nevent1)[a-z0-9]+)";"g") | .captures[0].string' | nak decode | jq -cr '{ids: [.id]}' | nak req wss://relay.damus.io +``` diff --git a/req.go b/req.go index 499b411..06c0955 100644 --- a/req.go +++ b/req.go @@ -99,7 +99,7 @@ example: filter := nostr.Filter{} if stdinFilter != "" { if err := json.Unmarshal([]byte(stdinFilter), &filter); err != nil { - lineProcessingError(c, "invalid filter received from stdin: %s", err) + lineProcessingError(c, "invalid filter '%s' received from stdin: %s", stdinFilter, err) continue } } From d95b6f50ff25f1170e8259a1de49d278e252221a Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Wed, 8 Nov 2023 14:26:25 -0300 Subject: [PATCH 029/401] --prompt-sec for getting a secret key from a prompt. --- encode.go | 16 ---------------- event.go | 38 ++++++++++++++++++++++++++++++++++++-- go.mod | 1 + go.sum | 2 ++ helpers.go | 22 +++++++++++++++++++++- 5 files changed, 60 insertions(+), 19 deletions(-) diff --git a/encode.go b/encode.go index 2c337d0..1fc3b08 100644 --- a/encode.go +++ b/encode.go @@ -1,9 +1,7 @@ package main import ( - "encoding/hex" "fmt" - "strings" "github.com/nbd-wtf/go-nostr/nip19" "github.com/urfave/cli/v2" @@ -232,17 +230,3 @@ var encode = &cli.Command{ }, }, } - -func validate32BytesHex(target string) error { - if _, err := hex.DecodeString(target); err != nil { - return fmt.Errorf("target '%s' is not valid hex: %s", target, err) - } - if len(target) != 64 { - return fmt.Errorf("expected '%s' to be 64 characters (32 bytes), got %d", target, len(target)) - } - if strings.ToLower(target) != target { - return fmt.Errorf("expected target to be all lowercase hex. try again with '%s'", strings.ToLower(target)) - } - - return nil -} diff --git a/event.go b/event.go index a060025..bbefc94 100644 --- a/event.go +++ b/event.go @@ -9,8 +9,10 @@ import ( "strings" "time" + "github.com/bgentry/speakeasy" "github.com/mailru/easyjson" "github.com/nbd-wtf/go-nostr" + "github.com/nbd-wtf/go-nostr/nip19" "github.com/nbd-wtf/go-nostr/nson" "github.com/urfave/cli/v2" ) @@ -35,10 +37,14 @@ example: Flags: []cli.Flag{ &cli.StringFlag{ Name: "sec", - Usage: "secret key to sign the event", + Usage: "secret key to sign the event, as hex or nsec", DefaultText: "the key '1'", Value: "0000000000000000000000000000000000000000000000000000000000000001", }, + &cli.BoolFlag{ + Name: "prompt-sec", + Usage: "prompt the user to paste a hex or nsec with which to sign the event", + }, &cli.BoolFlag{ Name: "envelope", Usage: "print the event enveloped in a [\"EVENT\", ...] message ready to be sent to a relay", @@ -90,6 +96,34 @@ example: }, ArgsUsage: "[relay...]", Action: func(c *cli.Context) error { + // gather the secret key first + sec := c.String("sec") + if c.Bool("prompt-sec") { + if isPiped() { + return fmt.Errorf("can't prompt for a secret key when processing data from a pipe, try again without --prompt-sec") + } + var err error + sec, err = speakeasy.FAsk(os.Stderr, "type your secret key as nsec or hex: ") + if err != nil { + return fmt.Errorf("failed to get secret key: %w", err) + } + } + if strings.HasPrefix(sec, "nsec1") { + _, hex, err := nip19.Decode(sec) + if err != nil { + return fmt.Errorf("invalid nsec: %w", err) + } + sec = hex.(string) + } + if len(sec) > 64 { + return fmt.Errorf("invalid secret key: too large") + } + sec = strings.Repeat("0", 64-len(sec)) + sec // left-pad + if err := validate32BytesHex(sec); err != nil { + return fmt.Errorf("invalid secret key") + } + + // then process input and generate events for stdinEvent := range getStdinLinesOrBlank() { evt := nostr.Event{ Tags: make(nostr.Tags, 0, 3), @@ -164,7 +198,7 @@ example: } if evt.Sig == "" || mustRehashAndResign { - if err := evt.Sign(c.String("sec")); err != nil { + if err := evt.Sign(sec); err != nil { return fmt.Errorf("error signing with provided key: %w", err) } } diff --git a/go.mod b/go.mod index e39f434..40ffeb4 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.21 toolchain go1.21.0 require ( + github.com/bgentry/speakeasy v0.1.0 github.com/mailru/easyjson v0.7.7 github.com/nbd-wtf/go-nostr v0.25.3 github.com/nbd-wtf/nostr-sdk v0.0.2 diff --git a/go.sum b/go.sum index 3237f09..8856e51 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,6 @@ github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= +github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M= github.com/btcsuite/btcd v0.23.0/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY= diff --git a/helpers.go b/helpers.go index 6f05666..2497352 100644 --- a/helpers.go +++ b/helpers.go @@ -3,6 +3,7 @@ package main import ( "bufio" "context" + "encoding/hex" "fmt" "net/url" "os" @@ -15,6 +16,11 @@ const ( LINE_PROCESSING_ERROR = iota ) +func isPiped() bool { + stat, _ := os.Stdin.Stat() + return stat.Mode()&os.ModeCharDevice == 0 +} + func getStdinLinesOrBlank() chan string { multi := make(chan string) if hasStdinLines := writeStdinLinesOrNothing(multi); !hasStdinLines { @@ -43,7 +49,7 @@ func getStdinLinesOrFirstArgument(c *cli.Context) chan string { } func writeStdinLinesOrNothing(ch chan string) (hasStdinLines bool) { - if stat, _ := os.Stdin.Stat(); stat.Mode()&os.ModeCharDevice == 0 { + if isPiped() { // piped go func() { scanner := bufio.NewScanner(os.Stdin) @@ -78,6 +84,20 @@ func validateRelayURLs(wsurls []string) error { return nil } +func validate32BytesHex(target string) error { + if _, err := hex.DecodeString(target); err != nil { + return fmt.Errorf("target '%s' is not valid hex: %s", target, err) + } + if len(target) != 64 { + return fmt.Errorf("expected '%s' to be 64 characters (32 bytes), got %d", target, len(target)) + } + if strings.ToLower(target) != target { + return fmt.Errorf("expected target to be all lowercase hex. try again with '%s'", strings.ToLower(target)) + } + + return nil +} + func lineProcessingError(c *cli.Context, msg string, args ...any) { c.Context = context.WithValue(c.Context, LINE_PROCESSING_ERROR, true) fmt.Fprintf(os.Stderr, msg+"\n", args...) From e507d907662acdf41fa19fdaeefb166337f8e2d7 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Wed, 8 Nov 2023 22:26:41 -0300 Subject: [PATCH 030/401] beginnings of some humble tests. --- example_test.go | 19 +++++++++++++++++++ main.go | 28 ++++++++++++++-------------- 2 files changed, 33 insertions(+), 14 deletions(-) create mode 100644 example_test.go diff --git a/example_test.go b/example_test.go new file mode 100644 index 0000000..2bcca5b --- /dev/null +++ b/example_test.go @@ -0,0 +1,19 @@ +package main + +func ExampleEventBasic() { + app.Run([]string{"nak", "event", "--ts", "1699485669"}) + // Output: + // {"id":"36d88cf5fcc449f2390a424907023eda7a74278120eebab8d02797cd92e7e29c","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1699485669,"kind":1,"tags":[],"content":"hello from the nostr army knife","sig":"68e71a192e8abcf8582a222434ac823ecc50607450ebe8cc4c145eb047794cc382dc3f888ce879d2f404f5ba6085a47601360a0fa2dd4b50d317bd0c6197c2c2"} +} + +func ExampleEventComplex() { + app.Run([]string{"nak", "event", "--ts", "1699485669", "-k", "11", "-c", "skjdbaskd", "--sec", "17", "-t", "t=spam", "-e", "36d88cf5fcc449f2390a424907023eda7a74278120eebab8d02797cd92e7e29c", "-t", "r=https://abc.def;nothing"}) + // Output: + // {"id":"aec4de6d051a7c2b6ca2d087903d42051a31e07fb742f1240970084822de10a6","pubkey":"2fa2104d6b38d11b0230010559879124e42ab8dfeff5ff29dc9cdadd4ecacc3f","created_at":1699485669,"kind":11,"tags":[["t","spam"],["r","https://abc.def","nothing"],["e","36d88cf5fcc449f2390a424907023eda7a74278120eebab8d02797cd92e7e29c"]],"content":"skjdbaskd","sig":"1165ac7a27d774d351ef19c8e918fb22f4005fcba193976c3d7edba6ef87ead7f14467f376a9e199f8371835368d86a8506f591e382528d00287fb168a7b8f38"} +} + +func ExampleReq() { + app.Run([]string{"nak", "req", "-k", "1", "-l", "18", "-a", "2fa2104d6b38d11b0230010559879124e42ab8dfeff5ff29dc9cdadd4ecacc3f", "-e", "aec4de6d051a7c2b6ca2d087903d42051a31e07fb742f1240970084822de10a6"}) + // Output + // ["REQ","nak",{"kinds":[1],"authors":["2fa2104d6b38d11b0230010559879124e42ab8dfeff5ff29dc9cdadd4ecacc3f"],"limit":18,"#e":["aec4de6d051a7c2b6ca2d087903d42051a31e07fb742f1240970084822de10a6"]}] +} diff --git a/main.go b/main.go index d3c73c1..b07350b 100644 --- a/main.go +++ b/main.go @@ -7,21 +7,21 @@ import ( "github.com/urfave/cli/v2" ) -func main() { - app := &cli.App{ - Name: "nak", - Usage: "the nostr army knife command-line tool", - Commands: []*cli.Command{ - req, - count, - fetch, - event, - decode, - encode, - verify, - }, - } +var app = &cli.App{ + Name: "nak", + Usage: "the nostr army knife command-line tool", + Commands: []*cli.Command{ + req, + count, + fetch, + event, + decode, + encode, + verify, + }, +} +func main() { if err := app.Run(os.Args); err != nil { fmt.Println(err) os.Exit(1) From 4fdd80670ad04bb73b732545ab7b646b31bd039a Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Wed, 8 Nov 2023 22:54:34 -0300 Subject: [PATCH 031/401] encode npub and nprofile tests. --- example_test.go | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/example_test.go b/example_test.go index 2bcca5b..9005b8d 100644 --- a/example_test.go +++ b/example_test.go @@ -14,6 +14,18 @@ func ExampleEventComplex() { func ExampleReq() { app.Run([]string{"nak", "req", "-k", "1", "-l", "18", "-a", "2fa2104d6b38d11b0230010559879124e42ab8dfeff5ff29dc9cdadd4ecacc3f", "-e", "aec4de6d051a7c2b6ca2d087903d42051a31e07fb742f1240970084822de10a6"}) - // Output + // Output: // ["REQ","nak",{"kinds":[1],"authors":["2fa2104d6b38d11b0230010559879124e42ab8dfeff5ff29dc9cdadd4ecacc3f"],"limit":18,"#e":["aec4de6d051a7c2b6ca2d087903d42051a31e07fb742f1240970084822de10a6"]}] } + +func ExampleEncodeNpub() { + app.Run([]string{"nak", "encode", "npub", "a6a67ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179822"}) + // Output: + // npub156n8a7wuhwk9tgrzjh8gwzc8q2dlekedec5djk0js9d3d7qhnq3qjpdq28 +} + +func ExampleEncodeNprofile() { + app.Run([]string{"nak", "encode", "nprofile", "-r", "wss://example.com", "a6a67ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179822"}) + // Output: + // nprofile1qqs2dfn7l8wthtz45p3ftn58pvrs9xlumvkuu2xet8egzkcklqtesgspz9mhxue69uhk27rpd4cxcefwvdhk6fl5jug +} From 795e98bc2e7a1ddd26b04d7cc6f0c10bf472b122 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Wed, 8 Nov 2023 22:54:52 -0300 Subject: [PATCH 032/401] close channel in getStdinLinesOrFirstArgument() --- helpers.go | 1 + 1 file changed, 1 insertion(+) diff --git a/helpers.go b/helpers.go index 2497352..8960ae1 100644 --- a/helpers.go +++ b/helpers.go @@ -39,6 +39,7 @@ func getStdinLinesOrFirstArgument(c *cli.Context) chan string { if target != "" { single := make(chan string, 1) single <- target + close(single) return single } From 6a7a5eb26ea344ec2a6582b124465b8fad500f5f Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Mon, 13 Nov 2023 10:34:09 -0300 Subject: [PATCH 033/401] fix bug with kind being set to zero and replaced silently. --- event.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/event.go b/event.go index bbefc94..30959fb 100644 --- a/event.go +++ b/event.go @@ -15,6 +15,7 @@ import ( "github.com/nbd-wtf/go-nostr/nip19" "github.com/nbd-wtf/go-nostr/nson" "github.com/urfave/cli/v2" + "golang.org/x/exp/slices" ) const CATEGORY_EVENT_FIELDS = "EVENT FIELDS" @@ -129,20 +130,20 @@ example: Tags: make(nostr.Tags, 0, 3), } + kindWasSupplied := true mustRehashAndResign := false if stdinEvent != "" { if err := json.Unmarshal([]byte(stdinEvent), &evt); err != nil { lineProcessingError(c, "invalid event received from stdin: %s", err) continue } + kindWasSupplied = strings.Contains(stdinEvent, `"kind"`) } + kindWasSupplied = slices.Contains(c.FlagNames(), "kind") - if kind := c.Int("kind"); kind != 0 { + if kind := c.Int("kind"); kindWasSupplied { evt.Kind = kind mustRehashAndResign = true - } else if evt.Kind == 0 { - evt.Kind = 1 - mustRehashAndResign = true } if content := c.String("content"); content != "" { From 11fe6b5809ded7a588ffa1cb81bfe8fdec89f8f8 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Mon, 13 Nov 2023 14:57:35 -0300 Subject: [PATCH 034/401] connect to relays once per call instead of in each iteration and fail early if no connection works. --- event.go | 18 ++++++++++++++---- helpers.go | 16 ++++++++++++++++ req.go | 22 ++++++++++++++++++---- 3 files changed, 48 insertions(+), 8 deletions(-) diff --git a/event.go b/event.go index 30959fb..c538ecb 100644 --- a/event.go +++ b/event.go @@ -97,6 +97,16 @@ example: }, ArgsUsage: "[relay...]", Action: func(c *cli.Context) error { + // try to connect to the relays here + var relays []*nostr.Relay + if relayUrls := c.Args().Slice(); len(relayUrls) > 0 { + _, relays = connectToAllRelays(c.Context, relayUrls) + if len(relays) == 0 { + fmt.Fprintf(os.Stderr, "failed to connect to any of the given relays.\n") + os.Exit(3) + } + } + // gather the secret key first sec := c.String("sec") if c.Bool("prompt-sec") { @@ -204,12 +214,12 @@ example: } } - relays := c.Args().Slice() if len(relays) > 0 { fmt.Println(evt.String()) - for _, url := range relays { - fmt.Fprintf(os.Stderr, "publishing to %s... ", url) - if relay, err := nostr.RelayConnect(c.Context, url); err != nil { + os.Stdout.Sync() + for _, relay := range relays { + fmt.Fprintf(os.Stderr, "publishing to %s... ", relay.URL) + if relay, err := nostr.RelayConnect(c.Context, relay.URL); err != nil { fmt.Fprintf(os.Stderr, "failed to connect: %s\n", err) } else { ctx, cancel := context.WithTimeout(c.Context, 10*time.Second) diff --git a/helpers.go b/helpers.go index 8960ae1..0be6c38 100644 --- a/helpers.go +++ b/helpers.go @@ -9,6 +9,7 @@ import ( "os" "strings" + "github.com/nbd-wtf/go-nostr" "github.com/urfave/cli/v2" ) @@ -99,6 +100,21 @@ func validate32BytesHex(target string) error { return nil } +func connectToAllRelays(ctx context.Context, relayUrls []string) (*nostr.SimplePool, []*nostr.Relay) { + relays := make([]*nostr.Relay, 0, len(relayUrls)) + pool := nostr.NewSimplePool(ctx) + for _, url := range relayUrls { + fmt.Fprintf(os.Stderr, "connecting to %s... ", url) + if relay, err := pool.EnsureRelay(url); err == nil { + relays = append(relays, relay) + fmt.Fprintf(os.Stderr, "ok.\n") + } else { + fmt.Fprintf(os.Stderr, err.Error()+"\n") + } + } + return pool, relays +} + func lineProcessingError(c *cli.Context, msg string, args ...any) { c.Context = context.WithValue(c.Context, LINE_PROCESSING_ERROR, true) fmt.Fprintf(os.Stderr, msg+"\n", args...) diff --git a/req.go b/req.go index 06c0955..cbc11e9 100644 --- a/req.go +++ b/req.go @@ -3,6 +3,7 @@ package main import ( "encoding/json" "fmt" + "os" "strings" "github.com/nbd-wtf/go-nostr" @@ -95,6 +96,21 @@ example: }, ArgsUsage: "[relay...]", Action: func(c *cli.Context) error { + var pool *nostr.SimplePool + relayUrls := c.Args().Slice() + if len(relayUrls) > 0 { + var relays []*nostr.Relay + pool, relays = connectToAllRelays(c.Context, relayUrls) + if len(relays) == 0 { + fmt.Fprintf(os.Stderr, "failed to connect to any of the given relays.\n") + os.Exit(3) + } + relayUrls = make([]string, len(relays)) + for i, relay := range relays { + relayUrls[i] = relay.URL + } + } + for stdinFilter := range getStdinLinesOrBlank() { filter := nostr.Filter{} if stdinFilter != "" { @@ -155,14 +171,12 @@ example: filter.Limit = limit } - relays := c.Args().Slice() - if len(relays) > 0 { - pool := nostr.NewSimplePool(c.Context) + if len(relayUrls) > 0 { fn := pool.SubManyEose if c.Bool("stream") { fn = pool.SubMany } - for ie := range fn(c.Context, relays, nostr.Filters{filter}) { + for ie := range fn(c.Context, relayUrls, nostr.Filters{filter}) { fmt.Println(ie.Event) } } else { From 8fbfdc65c8a4fcacee23701bbb0821794e1519c8 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Mon, 13 Nov 2023 15:03:27 -0300 Subject: [PATCH 035/401] add --silent global option to remove the stderr logs. --- event.go | 10 +++++----- helpers.go | 12 ++++++++---- main.go | 13 +++++++++++++ req.go | 2 +- 4 files changed, 27 insertions(+), 10 deletions(-) diff --git a/event.go b/event.go index c538ecb..5101479 100644 --- a/event.go +++ b/event.go @@ -102,7 +102,7 @@ example: if relayUrls := c.Args().Slice(); len(relayUrls) > 0 { _, relays = connectToAllRelays(c.Context, relayUrls) if len(relays) == 0 { - fmt.Fprintf(os.Stderr, "failed to connect to any of the given relays.\n") + log("failed to connect to any of the given relays.\n") os.Exit(3) } } @@ -218,16 +218,16 @@ example: fmt.Println(evt.String()) os.Stdout.Sync() for _, relay := range relays { - fmt.Fprintf(os.Stderr, "publishing to %s... ", relay.URL) + log("publishing to %s... ", relay.URL) if relay, err := nostr.RelayConnect(c.Context, relay.URL); err != nil { - fmt.Fprintf(os.Stderr, "failed to connect: %s\n", err) + log("failed to connect: %s\n", err) } else { ctx, cancel := context.WithTimeout(c.Context, 10*time.Second) defer cancel() if status, err := relay.Publish(ctx, evt); err != nil { - fmt.Fprintf(os.Stderr, "failed: %s\n", err) + log("failed: %s\n", err) } else { - fmt.Fprintf(os.Stderr, "%s.\n", status) + log("%s.\n", status) } } } diff --git a/helpers.go b/helpers.go index 0be6c38..94fce32 100644 --- a/helpers.go +++ b/helpers.go @@ -17,6 +17,10 @@ const ( LINE_PROCESSING_ERROR = iota ) +var log = func(msg string, args ...any) { + fmt.Fprintf(os.Stderr, msg, args...) +} + func isPiped() bool { stat, _ := os.Stdin.Stat() return stat.Mode()&os.ModeCharDevice == 0 @@ -104,12 +108,12 @@ func connectToAllRelays(ctx context.Context, relayUrls []string) (*nostr.SimpleP relays := make([]*nostr.Relay, 0, len(relayUrls)) pool := nostr.NewSimplePool(ctx) for _, url := range relayUrls { - fmt.Fprintf(os.Stderr, "connecting to %s... ", url) + log("connecting to %s... ", url) if relay, err := pool.EnsureRelay(url); err == nil { relays = append(relays, relay) - fmt.Fprintf(os.Stderr, "ok.\n") + log("ok.\n") } else { - fmt.Fprintf(os.Stderr, err.Error()+"\n") + log(err.Error() + "\n") } } return pool, relays @@ -117,7 +121,7 @@ func connectToAllRelays(ctx context.Context, relayUrls []string) (*nostr.SimpleP func lineProcessingError(c *cli.Context, msg string, args ...any) { c.Context = context.WithValue(c.Context, LINE_PROCESSING_ERROR, true) - fmt.Fprintf(os.Stderr, msg+"\n", args...) + log(msg+"\n", args...) } func exitIfLineProcessingError(c *cli.Context) { diff --git a/main.go b/main.go index b07350b..56c8192 100644 --- a/main.go +++ b/main.go @@ -19,6 +19,19 @@ var app = &cli.App{ encode, verify, }, + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "silent", + Usage: "do not print logs and info messages to stderr", + Aliases: []string{"s"}, + Action: func(ctx *cli.Context, b bool) error { + if b { + log = func(msg string, args ...any) {} + } + return nil + }, + }, + }, } func main() { diff --git a/req.go b/req.go index cbc11e9..b75cfb9 100644 --- a/req.go +++ b/req.go @@ -102,7 +102,7 @@ example: var relays []*nostr.Relay pool, relays = connectToAllRelays(c.Context, relayUrls) if len(relays) == 0 { - fmt.Fprintf(os.Stderr, "failed to connect to any of the given relays.\n") + log("failed to connect to any of the given relays.\n") os.Exit(3) } relayUrls = make([]string, len(relays)) From 15217f2466670d3e26a8a8a01a2c621483a00103 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Wed, 15 Nov 2023 09:48:57 -0300 Subject: [PATCH 036/401] fetch: fix handling of --relay tags. --- fetch.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/fetch.go b/fetch.go index 43c24ed..96e267e 100644 --- a/fetch.go +++ b/fetch.go @@ -46,20 +46,20 @@ var fetch = &cli.Command{ if v.Author != "" { authorHint = v.Author } - relays = v.Relays + relays = append(relays, v.Relays...) case "naddr": v := value.(nostr.EntityPointer) filter.Tags = nostr.TagMap{"d": []string{v.Identifier}} filter.Kinds = append(filter.Kinds, v.Kind) filter.Authors = append(filter.Authors, v.PublicKey) authorHint = v.PublicKey - relays = v.Relays + relays = append(relays, v.Relays...) case "nprofile": v := value.(nostr.ProfilePointer) filter.Authors = append(filter.Authors, v.PublicKey) filter.Kinds = append(filter.Kinds, 0) authorHint = v.PublicKey - relays = v.Relays + relays = append(relays, v.Relays...) case "npub": v := value.(string) filter.Authors = append(filter.Authors, v) From 082be94614e8d4e5d0f932c57e8b7103d5f477e8 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sun, 19 Nov 2023 07:20:52 -0300 Subject: [PATCH 037/401] update go-nostr. --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 40ffeb4..fb6b65b 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ toolchain go1.21.0 require ( github.com/bgentry/speakeasy v0.1.0 github.com/mailru/easyjson v0.7.7 - github.com/nbd-wtf/go-nostr v0.25.3 + github.com/nbd-wtf/go-nostr v0.25.7 github.com/nbd-wtf/nostr-sdk v0.0.2 github.com/urfave/cli/v2 v2.25.3 ) diff --git a/go.sum b/go.sum index 8856e51..29fd1ba 100644 --- a/go.sum +++ b/go.sum @@ -82,6 +82,8 @@ github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0 github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/nbd-wtf/go-nostr v0.25.3 h1:RPPh4cOosw0OZi5KG627pZ3GlKxiKsjARluzen/mB9g= github.com/nbd-wtf/go-nostr v0.25.3/go.mod h1:bkffJI+x914sPQWum9ZRUn66D7NpDnAoWo1yICvj3/0= +github.com/nbd-wtf/go-nostr v0.25.7 h1:DcGOSgKVr/L6w62tRtKeV2t46sRyFcq9pWcyIFkh0eM= +github.com/nbd-wtf/go-nostr v0.25.7/go.mod h1:bkffJI+x914sPQWum9ZRUn66D7NpDnAoWo1yICvj3/0= github.com/nbd-wtf/nostr-sdk v0.0.2 h1:mZIeti+DOF0D1179q+NLL/h0LVMMOPRQAYpOuUrn5Zk= github.com/nbd-wtf/nostr-sdk v0.0.2/go.mod h1:KQZOtzcrXBlVhpZYG1tw83ADIONNMMPjUU3ZAH5U2RY= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= From 05f2275c9eb7de258a36c9caaf884bafd0f3b9b7 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Mon, 20 Nov 2023 15:00:50 -0300 Subject: [PATCH 038/401] nak relay --- main.go | 1 + relay.go | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 relay.go diff --git a/main.go b/main.go index 56c8192..e701dea 100644 --- a/main.go +++ b/main.go @@ -18,6 +18,7 @@ var app = &cli.App{ decode, encode, verify, + relay, }, Flags: []cli.Flag{ &cli.BoolFlag{ diff --git a/relay.go b/relay.go new file mode 100644 index 0000000..11d3a65 --- /dev/null +++ b/relay.go @@ -0,0 +1,38 @@ +package main + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/nbd-wtf/go-nostr/nip11" + "github.com/urfave/cli/v2" +) + +var relay = &cli.Command{ + Name: "relay", + Usage: "gets the relay information document for the given relay, as JSON", + Description: `example: + nak relay nostr.wine +`, + ArgsUsage: "", + Action: func(c *cli.Context) error { + url := c.Args().First() + if url == "" { + return fmt.Errorf("specify the ") + } + + if !strings.HasPrefix(url, "wss://") && !strings.HasPrefix(url, "ws://") { + url = "wss://" + url + } + + info, err := nip11.Fetch(c.Context, url) + if err != nil { + return fmt.Errorf("failed to fetch '%s' information document: %w", url, err) + } + + pretty, _ := json.MarshalIndent(info, "", " ") + fmt.Println(string(pretty)) + return nil + }, +} From 4a3c7dc825314fe48413775ebaff63a1fc0c9bd7 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Mon, 20 Nov 2023 15:01:51 -0300 Subject: [PATCH 039/401] remove extra whitespace on usage strings. --- event.go | 3 +-- relay.go | 3 +-- req.go | 3 +-- verify.go | 3 +-- 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/event.go b/event.go index 5101479..2f8ecd2 100644 --- a/event.go +++ b/event.go @@ -33,8 +33,7 @@ if an event -- or a partial event -- is given on stdin, the flags can be used to example: echo '{"id":"a889df6a387419ff204305f4c2d296ee328c3cd4f8b62f205648a541b4554dfb","pubkey":"c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5","created_at":1698623783,"kind":1,"tags":[],"content":"hello from the nostr army knife","sig":"84876e1ee3e726da84e5d195eb79358b2b3eaa4d9bd38456fde3e8a2af3f1cd4cda23f23fda454869975b3688797d4c66e12f4c51c1b43c6d2997c5e61865661"}' | nak event wss://offchain.pub - echo '{"tags": [["t", "spam"]]}' | nak event -c 'this is spam' -`, + echo '{"tags": [["t", "spam"]]}' | nak event -c 'this is spam'`, Flags: []cli.Flag{ &cli.StringFlag{ Name: "sec", diff --git a/relay.go b/relay.go index 11d3a65..a0faee4 100644 --- a/relay.go +++ b/relay.go @@ -13,8 +13,7 @@ var relay = &cli.Command{ Name: "relay", Usage: "gets the relay information document for the given relay, as JSON", Description: `example: - nak relay nostr.wine -`, + nak relay nostr.wine`, ArgsUsage: "", Action: func(c *cli.Context) error { url := c.Args().First() diff --git a/req.go b/req.go index b75cfb9..8d8d3a3 100644 --- a/req.go +++ b/req.go @@ -24,8 +24,7 @@ example: it can also take a filter from stdin, optionally modify it with flags and send it to specific relays (or just print it). example: - echo '{"kinds": [1], "#t": ["test"]}' | nak req -l 5 -k 4549 --tag t=spam wss://nostr-pub.wellorder.net -`, + echo '{"kinds": [1], "#t": ["test"]}' | nak req -l 5 -k 4549 --tag t=spam wss://nostr-pub.wellorder.net`, Flags: []cli.Flag{ &cli.StringSliceFlag{ Name: "author", diff --git a/verify.go b/verify.go index f31aa19..f5f174b 100644 --- a/verify.go +++ b/verify.go @@ -13,8 +13,7 @@ var verify = &cli.Command{ Description: `example: echo '{"id":"a889df6a387419ff204305f4c2d296ee328c3cd4f8b62f205648a541b4554dfb","pubkey":"c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5","created_at":1698623783,"kind":1,"tags":[],"content":"hello from the nostr army knife","sig":"84876e1ee3e726da84e5d195eb79358b2b3eaa4d9bd38456fde3e8a2af3f1cd4cda23f23fda454869975b3688797d4c66e12f4c51c1b43c6d2997c5e61865661"}' | nak verify -it outputs nothing if the verification is successful. -`, +it outputs nothing if the verification is successful.`, Action: func(c *cli.Context) error { for stdinEvent := range getStdinLinesOrBlank() { evt := nostr.Event{} From 53cb2c0490560e8b14631d2086b773461321d8ec Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Fri, 24 Nov 2023 21:08:13 -0300 Subject: [PATCH 040/401] event: fix handling of -k and kind in stdin event, and default to 1. --- event.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/event.go b/event.go index 2f8ecd2..bbb4171 100644 --- a/event.go +++ b/event.go @@ -148,11 +148,13 @@ example: } kindWasSupplied = strings.Contains(stdinEvent, `"kind"`) } - kindWasSupplied = slices.Contains(c.FlagNames(), "kind") - if kind := c.Int("kind"); kindWasSupplied { + if kind := c.Int("kind"); slices.Contains(c.FlagNames(), "kind") { evt.Kind = kind mustRehashAndResign = true + } else if !kindWasSupplied { + evt.Kind = 1 + mustRehashAndResign = true } if content := c.String("content"); content != "" { From f2f9dda33aeb40e205a0960996c945d006eef165 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Fri, 24 Nov 2023 21:19:10 -0300 Subject: [PATCH 041/401] actually this is the fix. --- event.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/event.go b/event.go index bbb4171..37c9c4e 100644 --- a/event.go +++ b/event.go @@ -139,7 +139,7 @@ example: Tags: make(nostr.Tags, 0, 3), } - kindWasSupplied := true + kindWasSupplied := false mustRehashAndResign := false if stdinEvent != "" { if err := json.Unmarshal([]byte(stdinEvent), &evt); err != nil { From d9d36e761928f893dcf34c370a03c8b7d419f2a1 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 28 Nov 2023 15:18:43 -0300 Subject: [PATCH 042/401] use easyjson and envelopes. --- event.go | 4 ++-- req.go | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/event.go b/event.go index 37c9c4e..8aa0255 100644 --- a/event.go +++ b/event.go @@ -142,7 +142,7 @@ example: kindWasSupplied := false mustRehashAndResign := false if stdinEvent != "" { - if err := json.Unmarshal([]byte(stdinEvent), &evt); err != nil { + if err := easyjson.Unmarshal([]byte(stdinEvent), &evt); err != nil { lineProcessingError(c, "invalid event received from stdin: %s", err) continue } @@ -235,7 +235,7 @@ example: } else { var result string if c.Bool("envelope") { - j, _ := json.Marshal([]any{"EVENT", evt}) + j, _ := json.Marshal(nostr.EventEnvelope{Event: evt}) result = string(j) } else if c.Bool("nson") { result, _ = nson.Marshal(&evt) diff --git a/req.go b/req.go index 8d8d3a3..d4ac174 100644 --- a/req.go +++ b/req.go @@ -6,6 +6,7 @@ import ( "os" "strings" + "github.com/mailru/easyjson" "github.com/nbd-wtf/go-nostr" "github.com/urfave/cli/v2" ) @@ -113,7 +114,7 @@ example: for stdinFilter := range getStdinLinesOrBlank() { filter := nostr.Filter{} if stdinFilter != "" { - if err := json.Unmarshal([]byte(stdinFilter), &filter); err != nil { + if err := easyjson.Unmarshal([]byte(stdinFilter), &filter); err != nil { lineProcessingError(c, "invalid filter '%s' received from stdin: %s", stdinFilter, err) continue } @@ -184,7 +185,7 @@ example: if c.Bool("bare") { result = filter.String() } else { - j, _ := json.Marshal([]any{"REQ", "nak", filter}) + j, _ := json.Marshal(nostr.ReqEnvelope{SubscriptionID: "nak", Filters: nostr.Filters{filter}}) result = string(j) } From 5657fdc6a7a340dce1743541df9101673026f83a Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Fri, 1 Dec 2023 13:22:04 -0300 Subject: [PATCH 043/401] update go-nostr. --- go.mod | 4 ++-- go.sum | 6 ++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index fb6b65b..663aab5 100644 --- a/go.mod +++ b/go.mod @@ -7,9 +7,10 @@ toolchain go1.21.0 require ( github.com/bgentry/speakeasy v0.1.0 github.com/mailru/easyjson v0.7.7 - github.com/nbd-wtf/go-nostr v0.25.7 + github.com/nbd-wtf/go-nostr v0.26.1 github.com/nbd-wtf/nostr-sdk v0.0.2 github.com/urfave/cli/v2 v2.25.3 + golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 ) require ( @@ -35,6 +36,5 @@ require ( github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.0 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect - golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 // indirect golang.org/x/sys v0.8.0 // indirect ) diff --git a/go.sum b/go.sum index 29fd1ba..5e936c1 100644 --- a/go.sum +++ b/go.sum @@ -80,10 +80,8 @@ github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlT github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/nbd-wtf/go-nostr v0.25.3 h1:RPPh4cOosw0OZi5KG627pZ3GlKxiKsjARluzen/mB9g= -github.com/nbd-wtf/go-nostr v0.25.3/go.mod h1:bkffJI+x914sPQWum9ZRUn66D7NpDnAoWo1yICvj3/0= -github.com/nbd-wtf/go-nostr v0.25.7 h1:DcGOSgKVr/L6w62tRtKeV2t46sRyFcq9pWcyIFkh0eM= -github.com/nbd-wtf/go-nostr v0.25.7/go.mod h1:bkffJI+x914sPQWum9ZRUn66D7NpDnAoWo1yICvj3/0= +github.com/nbd-wtf/go-nostr v0.26.1 h1:aqvXYdPycETozrRPp2roSNALNk2XMxRkBrHu3jXmhxA= +github.com/nbd-wtf/go-nostr v0.26.1/go.mod h1:bkffJI+x914sPQWum9ZRUn66D7NpDnAoWo1yICvj3/0= github.com/nbd-wtf/nostr-sdk v0.0.2 h1:mZIeti+DOF0D1179q+NLL/h0LVMMOPRQAYpOuUrn5Zk= github.com/nbd-wtf/nostr-sdk v0.0.2/go.mod h1:KQZOtzcrXBlVhpZYG1tw83ADIONNMMPjUU3ZAH5U2RY= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= From bc7cd0939c9d3edaf696d31b045cdaa671c2cd9d Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sat, 2 Dec 2023 12:18:55 -0300 Subject: [PATCH 044/401] nsecbunker work-in-progress. --- event.go | 31 ++----------- go.mod | 2 +- go.sum | 4 +- helpers.go | 35 ++++++++++++++ main.go | 1 + nsecbunker.go | 124 ++++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 167 insertions(+), 30 deletions(-) create mode 100644 nsecbunker.go diff --git a/event.go b/event.go index 8aa0255..8daaaa3 100644 --- a/event.go +++ b/event.go @@ -9,10 +9,8 @@ import ( "strings" "time" - "github.com/bgentry/speakeasy" "github.com/mailru/easyjson" "github.com/nbd-wtf/go-nostr" - "github.com/nbd-wtf/go-nostr/nip19" "github.com/nbd-wtf/go-nostr/nson" "github.com/urfave/cli/v2" "golang.org/x/exp/slices" @@ -106,31 +104,10 @@ example: } } - // gather the secret key first - sec := c.String("sec") - if c.Bool("prompt-sec") { - if isPiped() { - return fmt.Errorf("can't prompt for a secret key when processing data from a pipe, try again without --prompt-sec") - } - var err error - sec, err = speakeasy.FAsk(os.Stderr, "type your secret key as nsec or hex: ") - if err != nil { - return fmt.Errorf("failed to get secret key: %w", err) - } - } - if strings.HasPrefix(sec, "nsec1") { - _, hex, err := nip19.Decode(sec) - if err != nil { - return fmt.Errorf("invalid nsec: %w", err) - } - sec = hex.(string) - } - if len(sec) > 64 { - return fmt.Errorf("invalid secret key: too large") - } - sec = strings.Repeat("0", 64-len(sec)) + sec // left-pad - if err := validate32BytesHex(sec); err != nil { - return fmt.Errorf("invalid secret key") + // gather the secret key + sec, err := gatherSecretKeyFromArguments(c) + if err != nil { + return err } // then process input and generate events diff --git a/go.mod b/go.mod index 663aab5..8fe7917 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ toolchain go1.21.0 require ( github.com/bgentry/speakeasy v0.1.0 github.com/mailru/easyjson v0.7.7 - github.com/nbd-wtf/go-nostr v0.26.1 + github.com/nbd-wtf/go-nostr v0.26.2 github.com/nbd-wtf/nostr-sdk v0.0.2 github.com/urfave/cli/v2 v2.25.3 golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 diff --git a/go.sum b/go.sum index 5e936c1..7caf913 100644 --- a/go.sum +++ b/go.sum @@ -80,8 +80,8 @@ github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlT github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/nbd-wtf/go-nostr v0.26.1 h1:aqvXYdPycETozrRPp2roSNALNk2XMxRkBrHu3jXmhxA= -github.com/nbd-wtf/go-nostr v0.26.1/go.mod h1:bkffJI+x914sPQWum9ZRUn66D7NpDnAoWo1yICvj3/0= +github.com/nbd-wtf/go-nostr v0.26.2 h1:VI47qW7jqLrofB9DbDqH9lQH38eYgLYI7lBPYywIS78= +github.com/nbd-wtf/go-nostr v0.26.2/go.mod h1:bkffJI+x914sPQWum9ZRUn66D7NpDnAoWo1yICvj3/0= github.com/nbd-wtf/nostr-sdk v0.0.2 h1:mZIeti+DOF0D1179q+NLL/h0LVMMOPRQAYpOuUrn5Zk= github.com/nbd-wtf/nostr-sdk v0.0.2/go.mod h1:KQZOtzcrXBlVhpZYG1tw83ADIONNMMPjUU3ZAH5U2RY= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= diff --git a/helpers.go b/helpers.go index 94fce32..47fda35 100644 --- a/helpers.go +++ b/helpers.go @@ -9,12 +9,17 @@ import ( "os" "strings" + "github.com/bgentry/speakeasy" "github.com/nbd-wtf/go-nostr" + "github.com/nbd-wtf/go-nostr/nip19" "github.com/urfave/cli/v2" ) const ( LINE_PROCESSING_ERROR = iota + + BOLD_ON = "\033[1m" + BOLD_OFF = "\033[21m" ) var log = func(msg string, args ...any) { @@ -129,3 +134,33 @@ func exitIfLineProcessingError(c *cli.Context) { os.Exit(123) } } + +func gatherSecretKeyFromArguments(c *cli.Context) (string, error) { + sec := c.String("sec") + if c.Bool("prompt-sec") { + if isPiped() { + return "", fmt.Errorf("can't prompt for a secret key when processing data from a pipe, try again without --prompt-sec") + } + var err error + sec, err = speakeasy.FAsk(os.Stderr, "type your secret key as nsec or hex: ") + if err != nil { + return "", fmt.Errorf("failed to get secret key: %w", err) + } + } + if strings.HasPrefix(sec, "nsec1") { + _, hex, err := nip19.Decode(sec) + if err != nil { + return "", fmt.Errorf("invalid nsec: %w", err) + } + sec = hex.(string) + } + if len(sec) > 64 { + return "", fmt.Errorf("invalid secret key: too large") + } + sec = strings.Repeat("0", 64-len(sec)) + sec // left-pad + if err := validate32BytesHex(sec); err != nil { + return "", fmt.Errorf("invalid secret key") + } + + return sec, nil +} diff --git a/main.go b/main.go index e701dea..556b957 100644 --- a/main.go +++ b/main.go @@ -19,6 +19,7 @@ var app = &cli.App{ encode, verify, relay, + nsecbunker, }, Flags: []cli.Flag{ &cli.BoolFlag{ diff --git a/nsecbunker.go b/nsecbunker.go new file mode 100644 index 0000000..393f565 --- /dev/null +++ b/nsecbunker.go @@ -0,0 +1,124 @@ +package main + +import ( + "encoding/json" + "fmt" + "net/url" + "os" + + "github.com/bgentry/speakeasy" + "github.com/nbd-wtf/go-nostr" + "github.com/nbd-wtf/go-nostr/nip19" + "github.com/nbd-wtf/go-nostr/nip46" + "github.com/urfave/cli/v2" +) + +var nsecbunker = &cli.Command{ + Name: "nsecbunker", + Usage: "starts a NIP-46 signer daemon with the given --sec key", + ArgsUsage: "[relay...]", + Description: ``, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "sec", + Usage: "secret key to sign the event, as hex or nsec", + DefaultText: "the key '1'", + Value: "0000000000000000000000000000000000000000000000000000000000000001", + }, + &cli.BoolFlag{ + Name: "prompt-sec", + Usage: "prompt the user to paste a hex or nsec with which to sign the event", + }, + &cli.BoolFlag{ + Name: "yes", + Aliases: []string{"y"}, + Usage: "always respond to any NIP-46 requests from anyone", + }, + }, + Action: func(c *cli.Context) error { + // try to connect to the relays here + qs := url.Values{} + relayURLs := make([]string, 0, c.Args().Len()) + if relayUrls := c.Args().Slice(); len(relayUrls) > 0 { + _, relays := connectToAllRelays(c.Context, relayUrls) + if len(relays) == 0 { + log("failed to connect to any of the given relays.\n") + os.Exit(3) + } + for _, relay := range relays { + relayURLs = append(relayURLs, relay.URL) + qs.Add("relay", relay.URL) + } + } + if len(relayURLs) == 0 { + return fmt.Errorf("not connected to any relays: please specify at least one") + } + + // gather the secret key + sec, err := gatherSecretKeyFromArguments(c) + if err != nil { + return err + } + pubkey, err := nostr.GetPublicKey(sec) + if err != nil { + return err + } + npub, _ := nip19.EncodePublicKey(pubkey) + code := fmt.Sprintf("%s#secret?%s", npub, qs.Encode()) + log("listening at %s%v%s:\n %spubkey:%s %s\n %snpub:%s %s\n %sconnection code:%s %s\n\n", + BOLD_ON, relayURLs, BOLD_OFF, + BOLD_ON, BOLD_OFF, pubkey, + BOLD_ON, BOLD_OFF, npub, + BOLD_ON, BOLD_OFF, code, + ) + + alwaysYes := c.Bool("yes") + + // subscribe to relays + pool := nostr.NewSimplePool(c.Context) + events := pool.SubMany(c.Context, relayURLs, nostr.Filters{ + { + Kinds: []int{24133}, + Tags: nostr.TagMap{"p": []string{pubkey}}, + }, + }) + + signer := nip46.NewSigner(sec) + for ie := range events { + req, resp, eventResponse, harmless, err := signer.HandleRequest(ie.Event) + if err != nil { + log("< failed to handle request from %s: %w", ie.Event.PubKey, err) + continue + } + + jreq, _ := json.MarshalIndent(req, " ", " ") + log("- got request from '%s': %s\n", ie.Event.PubKey, string(jreq)) + jresp, _ := json.MarshalIndent(resp, " ", " ") + log("~ responding with %s\n", string(jresp)) + + if alwaysYes || harmless || askUserIfWeCanRespond() { + _, err := ie.Relay.Publish(c.Context, eventResponse) + if err == nil { + log("* sent response!\n") + } else { + log("* failed to send response: %s\n", err) + } + } + } + + return nil + }, +} + +func askUserIfWeCanRespond() bool { + answer, err := speakeasy.FAsk(os.Stderr, + fmt.Sprintf("proceed? y/n")) + if err != nil { + return false + } + if answer == "y" || answer == "yes" { + return true + } + + return askUserIfWeCanRespond() +} From 26b1aa359adcad15742261a8fcf5b35a33d2bc48 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sat, 2 Dec 2023 15:32:40 -0300 Subject: [PATCH 045/401] nsecbunker/nip46 is working now. --- go.mod | 2 +- go.sum | 4 ++-- nsecbunker.go | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 8fe7917..4f8b930 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ toolchain go1.21.0 require ( github.com/bgentry/speakeasy v0.1.0 github.com/mailru/easyjson v0.7.7 - github.com/nbd-wtf/go-nostr v0.26.2 + github.com/nbd-wtf/go-nostr v0.26.4 github.com/nbd-wtf/nostr-sdk v0.0.2 github.com/urfave/cli/v2 v2.25.3 golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 diff --git a/go.sum b/go.sum index 7caf913..1007eca 100644 --- a/go.sum +++ b/go.sum @@ -80,8 +80,8 @@ github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlT github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/nbd-wtf/go-nostr v0.26.2 h1:VI47qW7jqLrofB9DbDqH9lQH38eYgLYI7lBPYywIS78= -github.com/nbd-wtf/go-nostr v0.26.2/go.mod h1:bkffJI+x914sPQWum9ZRUn66D7NpDnAoWo1yICvj3/0= +github.com/nbd-wtf/go-nostr v0.26.4 h1:C5AXvMaRZactznfjfvscciUFypCJT6AYYGpyQqRAQxQ= +github.com/nbd-wtf/go-nostr v0.26.4/go.mod h1:bkffJI+x914sPQWum9ZRUn66D7NpDnAoWo1yICvj3/0= github.com/nbd-wtf/nostr-sdk v0.0.2 h1:mZIeti+DOF0D1179q+NLL/h0LVMMOPRQAYpOuUrn5Zk= github.com/nbd-wtf/nostr-sdk v0.0.2/go.mod h1:KQZOtzcrXBlVhpZYG1tw83ADIONNMMPjUU3ZAH5U2RY= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= diff --git a/nsecbunker.go b/nsecbunker.go index 393f565..8ebaad8 100644 --- a/nsecbunker.go +++ b/nsecbunker.go @@ -64,12 +64,12 @@ var nsecbunker = &cli.Command{ return err } npub, _ := nip19.EncodePublicKey(pubkey) - code := fmt.Sprintf("%s#secret?%s", npub, qs.Encode()) - log("listening at %s%v%s:\n %spubkey:%s %s\n %snpub:%s %s\n %sconnection code:%s %s\n\n", + log("listening at %s%v%s:\n %spubkey:%s %s\n %snpub:%s %s\n %sconnection code:%s %s\n %sbunker:%s %s\n\n", BOLD_ON, relayURLs, BOLD_OFF, BOLD_ON, BOLD_OFF, pubkey, BOLD_ON, BOLD_OFF, npub, - BOLD_ON, BOLD_OFF, code, + BOLD_ON, BOLD_OFF, fmt.Sprintf("%s#secret?%s", npub, qs.Encode()), + BOLD_ON, BOLD_OFF, fmt.Sprintf("bunker://%s?%s", pubkey, qs.Encode()), ) alwaysYes := c.Bool("yes") From 4d75605c2050472536c29b21515d08c2fe20e5a2 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Thu, 7 Dec 2023 18:12:12 -0300 Subject: [PATCH 046/401] print event more consistently and auth when required and allowed. --- event.go | 63 ++++++++++++++++++++++++++++++++++++++++-------------- helpers.go | 4 ++++ 2 files changed, 51 insertions(+), 16 deletions(-) diff --git a/event.go b/event.go index 8daaaa3..0cb8e70 100644 --- a/event.go +++ b/event.go @@ -47,6 +47,10 @@ example: Name: "envelope", Usage: "print the event enveloped in a [\"EVENT\", ...] message ready to be sent to a relay", }, + &cli.BoolFlag{ + Name: "auth", + Usage: "always perform NIP-42 \"AUTH\" when facing an \"auth-required: \" rejection and try again", + }, &cli.BoolFlag{ Name: "nson", Usage: "encode the event using NSON", @@ -110,6 +114,8 @@ example: return err } + doAuth := c.Bool("auth") + // then process input and generate events for stdinEvent := range getStdinLinesOrBlank() { evt := nostr.Event{ @@ -118,6 +124,7 @@ example: kindWasSupplied := false mustRehashAndResign := false + if stdinEvent != "" { if err := easyjson.Unmarshal([]byte(stdinEvent), &evt); err != nil { lineProcessingError(c, "invalid event received from stdin: %s", err) @@ -192,38 +199,62 @@ example: } } + // print event as json + var result string + if c.Bool("envelope") { + j, _ := json.Marshal(nostr.EventEnvelope{Event: evt}) + result = string(j) + } else if c.Bool("nson") { + result, _ = nson.Marshal(&evt) + } else { + j, _ := easyjson.Marshal(&evt) + result = string(j) + } + fmt.Println(result) + + // publish to relays if len(relays) > 0 { - fmt.Println(evt.String()) os.Stdout.Sync() for _, relay := range relays { log("publishing to %s... ", relay.URL) if relay, err := nostr.RelayConnect(c.Context, relay.URL); err != nil { log("failed to connect: %s\n", err) } else { + publish: ctx, cancel := context.WithTimeout(c.Context, 10*time.Second) defer cancel() - if status, err := relay.Publish(ctx, evt); err != nil { - log("failed: %s\n", err) - } else { + + status, err := relay.Publish(ctx, evt) + if err == nil { + // published fine probably log("%s.\n", status) + goto end } + + // error publishing + if isAuthRequired(err.Error()) && sec != "" && doAuth { + // if the relay is requesting auth and we can auth, let's do it + log("performing auth... ") + st, err := relay.Auth(c.Context, func(evt *nostr.Event) error { return evt.Sign(sec) }) + if st == nostr.PublishStatusSucceeded { + // try to publish again, but this time don't try to auth again + doAuth = false + goto publish + } else { + // auth error + if err == nil { + err = fmt.Errorf("no response from relay") + } + log("auth error: %s. ", err) + } + } + log("failed: %s\n", err) } } - } else { - var result string - if c.Bool("envelope") { - j, _ := json.Marshal(nostr.EventEnvelope{Event: evt}) - result = string(j) - } else if c.Bool("nson") { - result, _ = nson.Marshal(&evt) - } else { - j, _ := easyjson.Marshal(&evt) - result = string(j) - } - fmt.Println(result) } } + end: exitIfLineProcessingError(c) return nil }, diff --git a/helpers.go b/helpers.go index 47fda35..1efbd8c 100644 --- a/helpers.go +++ b/helpers.go @@ -164,3 +164,7 @@ func gatherSecretKeyFromArguments(c *cli.Context) (string, error) { return sec, nil } + +func isAuthRequired(msg string) bool { + return strings.HasPrefix(msg, "msg: auth-required:") +} From 30dbe2c1c03f3046c7214b1f484a7efd4e4d0c47 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Thu, 7 Dec 2023 18:14:26 -0300 Subject: [PATCH 047/401] fix handling multiple lines in event (broken in previous commit). --- event.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/event.go b/event.go index 0cb8e70..19ece30 100644 --- a/event.go +++ b/event.go @@ -117,6 +117,7 @@ example: doAuth := c.Bool("auth") // then process input and generate events + nextline: for stdinEvent := range getStdinLinesOrBlank() { evt := nostr.Event{ Tags: make(nostr.Tags, 0, 3), @@ -228,7 +229,7 @@ example: if err == nil { // published fine probably log("%s.\n", status) - goto end + continue nextline } // error publishing @@ -254,7 +255,6 @@ example: } } - end: exitIfLineProcessingError(c) return nil }, From ed3156ae107ab0b39614aa12d838cac2b820c6c2 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sat, 9 Dec 2023 09:14:45 -0300 Subject: [PATCH 048/401] fix event publishing flow: no need to reconnect and AUTH messages make sense. --- event.go | 58 ++++++++++++++++++++++++++------------------------------ 1 file changed, 27 insertions(+), 31 deletions(-) diff --git a/event.go b/event.go index 19ece30..345dcf0 100644 --- a/event.go +++ b/event.go @@ -217,40 +217,36 @@ example: if len(relays) > 0 { os.Stdout.Sync() for _, relay := range relays { + publish: log("publishing to %s... ", relay.URL) - if relay, err := nostr.RelayConnect(c.Context, relay.URL); err != nil { - log("failed to connect: %s\n", err) - } else { - publish: - ctx, cancel := context.WithTimeout(c.Context, 10*time.Second) - defer cancel() + ctx, cancel := context.WithTimeout(c.Context, 10*time.Second) + defer cancel() - status, err := relay.Publish(ctx, evt) - if err == nil { - // published fine probably - log("%s.\n", status) - continue nextline - } - - // error publishing - if isAuthRequired(err.Error()) && sec != "" && doAuth { - // if the relay is requesting auth and we can auth, let's do it - log("performing auth... ") - st, err := relay.Auth(c.Context, func(evt *nostr.Event) error { return evt.Sign(sec) }) - if st == nostr.PublishStatusSucceeded { - // try to publish again, but this time don't try to auth again - doAuth = false - goto publish - } else { - // auth error - if err == nil { - err = fmt.Errorf("no response from relay") - } - log("auth error: %s. ", err) - } - } - log("failed: %s\n", err) + status, err := relay.Publish(ctx, evt) + if err == nil { + // published fine probably + log("%s.\n", status) + continue nextline } + + // error publishing + if strings.HasPrefix(err.Error(), "msg: auth-required:") && sec != "" && doAuth { + // if the relay is requesting auth and we can auth, let's do it + log("performing auth... ") + st, err := relay.Auth(c.Context, func(evt *nostr.Event) error { return evt.Sign(sec) }) + if st == nostr.PublishStatusSucceeded { + // try to publish again, but this time don't try to auth again + doAuth = false + goto publish + } else { + // auth error + if err == nil { + err = fmt.Errorf("no response from relay") + } + log("auth error: %s. ", err) + } + } + log("failed: %s\n", err) } } } From b7b61c07231a72271b840b6d3dd26d5aa605fcd8 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sat, 9 Dec 2023 16:32:04 -0300 Subject: [PATCH 049/401] support --auth/--sec/--prompt-sec on `req`. --- .gitignore | 10 +--------- event.go | 17 ++++++----------- go.mod | 22 +++++++++++----------- go.sum | 46 ++++++++++++++++++++++++---------------------- helpers.go | 12 ++++++------ nsecbunker.go | 3 +-- req.go | 33 +++++++++++++++++++++++++++++---- 7 files changed, 78 insertions(+), 65 deletions(-) diff --git a/.gitignore b/.gitignore index 536fd06..1e09c80 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1 @@ -target -.bsp -globals.bundle.js -yarn.lock -node_modules -project/project -.metals -.bloop -project/metals.sbt +nak diff --git a/event.go b/event.go index 345dcf0..6fa3e99 100644 --- a/event.go +++ b/event.go @@ -222,27 +222,22 @@ example: ctx, cancel := context.WithTimeout(c.Context, 10*time.Second) defer cancel() - status, err := relay.Publish(ctx, evt) - if err == nil { - // published fine probably - log("%s.\n", status) + if err := relay.Publish(ctx, evt); err == nil { + // published fine + log("success.\n") continue nextline } // error publishing if strings.HasPrefix(err.Error(), "msg: auth-required:") && sec != "" && doAuth { // if the relay is requesting auth and we can auth, let's do it - log("performing auth... ") - st, err := relay.Auth(c.Context, func(evt *nostr.Event) error { return evt.Sign(sec) }) - if st == nostr.PublishStatusSucceeded { + pk, _ := nostr.GetPublicKey(sec) + log("performing auth as %s... ", pk) + if err := relay.Auth(c.Context, func(evt *nostr.Event) error { return evt.Sign(sec) }); err == nil { // try to publish again, but this time don't try to auth again doAuth = false goto publish } else { - // auth error - if err == nil { - err = fmt.Errorf("no response from relay") - } log("auth error: %s. ", err) } } diff --git a/go.mod b/go.mod index 4f8b930..8c42cf8 100644 --- a/go.mod +++ b/go.mod @@ -7,10 +7,10 @@ toolchain go1.21.0 require ( github.com/bgentry/speakeasy v0.1.0 github.com/mailru/easyjson v0.7.7 - github.com/nbd-wtf/go-nostr v0.26.4 - github.com/nbd-wtf/nostr-sdk v0.0.2 - github.com/urfave/cli/v2 v2.25.3 - golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 + github.com/nbd-wtf/go-nostr v0.27.0 + github.com/nbd-wtf/nostr-sdk v0.0.5 + github.com/urfave/cli/v2 v2.25.7 + golang.org/x/exp v0.0.0-20231006140011-7918f672742d ) require ( @@ -22,19 +22,19 @@ require ( github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect github.com/dgraph-io/ristretto v0.1.1 // indirect - github.com/dustin/go-humanize v1.0.0 // indirect - github.com/fiatjaf/eventstore v0.1.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/fiatjaf/eventstore v0.2.16 // indirect github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/pool v0.2.1 // indirect - github.com/gobwas/ws v1.2.0 // indirect - github.com/golang/glog v1.0.0 // indirect + github.com/gobwas/ws v1.3.1 // indirect + github.com/golang/glog v1.1.2 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/tidwall/gjson v1.14.4 // indirect + github.com/tidwall/gjson v1.17.0 // indirect github.com/tidwall/match v1.1.1 // indirect - github.com/tidwall/pretty v1.2.0 // indirect + github.com/tidwall/pretty v1.2.1 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect - golang.org/x/sys v0.8.0 // indirect + golang.org/x/sys v0.14.0 // indirect ) diff --git a/go.sum b/go.sum index 1007eca..6c8567a 100644 --- a/go.sum +++ b/go.sum @@ -45,21 +45,22 @@ github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWa github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= -github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/fiatjaf/eventstore v0.1.0 h1:/g7VTw6dsXmjICD3rBuHNIvAammHJ5unrKJ71Dz+VTs= -github.com/fiatjaf/eventstore v0.1.0/go.mod h1:juMei5HL3HJi6t7vZjj7VdEItDPu31+GLROepdUK4tw= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/fiatjaf/eventstore v0.2.16 h1:NR64mnyUT5nJR8Sj2AwJTd1Hqs5kKJcCFO21ggUkvWg= +github.com/fiatjaf/eventstore v0.2.16/go.mod h1:rUc1KhVufVmC+HUOiuPweGAcvG6lEOQCkRCn2Xn5VRA= 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/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= -github.com/gobwas/ws v1.2.0 h1:u0p9s3xLYpZCA1z5JgCkMeB34CKCMMQbM+G8Ii7YD0I= -github.com/gobwas/ws v1.2.0/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= +github.com/gobwas/ws v1.3.1 h1:Qi34dfLMWJbiKaNbDVzM9x27nZBjmkaW6i4+Ku+pGVU= +github.com/gobwas/ws v1.3.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ= -github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= +github.com/golang/glog v1.1.2 h1:DVjP2PbBOzHyzA+dn3WhHIq4NdVu3Q+pvivFICf/7fo= +github.com/golang/glog v1.1.2/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= @@ -80,10 +81,10 @@ github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlT github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/nbd-wtf/go-nostr v0.26.4 h1:C5AXvMaRZactznfjfvscciUFypCJT6AYYGpyQqRAQxQ= -github.com/nbd-wtf/go-nostr v0.26.4/go.mod h1:bkffJI+x914sPQWum9ZRUn66D7NpDnAoWo1yICvj3/0= -github.com/nbd-wtf/nostr-sdk v0.0.2 h1:mZIeti+DOF0D1179q+NLL/h0LVMMOPRQAYpOuUrn5Zk= -github.com/nbd-wtf/nostr-sdk v0.0.2/go.mod h1:KQZOtzcrXBlVhpZYG1tw83ADIONNMMPjUU3ZAH5U2RY= +github.com/nbd-wtf/go-nostr v0.27.0 h1:h6JmMMmfNcAORTL2kk/K3+U6Mju6rk/IjcHA/PMeOc8= +github.com/nbd-wtf/go-nostr v0.27.0/go.mod h1:bkffJI+x914sPQWum9ZRUn66D7NpDnAoWo1yICvj3/0= +github.com/nbd-wtf/nostr-sdk v0.0.5 h1:rec+FcDizDVO0W25PX0lgYMXvP7zNNOgI3Fu9UCm4BY= +github.com/nbd-wtf/nostr-sdk v0.0.5/go.mod h1:iJJsikesCGLNFZ9dLqhLPDzdt924EagUmdQxT3w2Lmk= 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= @@ -107,28 +108,29 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= -github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= -github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM= +github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= -github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= -github.com/urfave/cli/v2 v2.25.3 h1:VJkt6wvEBOoSjPFQvOkv6iWIrsJyCrKGtCtxXWwmGeY= -github.com/urfave/cli/v2 v2.25.3/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs= +github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 h1:5llv2sWeaMSnA3w2kS57ouQQ4pudlXrR0dCgw51QK9o= -golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= -golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 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= @@ -141,8 +143,8 @@ golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= +golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= diff --git a/helpers.go b/helpers.go index 1efbd8c..971fafd 100644 --- a/helpers.go +++ b/helpers.go @@ -109,9 +109,13 @@ func validate32BytesHex(target string) error { return nil } -func connectToAllRelays(ctx context.Context, relayUrls []string) (*nostr.SimplePool, []*nostr.Relay) { +func connectToAllRelays( + ctx context.Context, + relayUrls []string, + opts ...nostr.PoolOption, +) (*nostr.SimplePool, []*nostr.Relay) { relays := make([]*nostr.Relay, 0, len(relayUrls)) - pool := nostr.NewSimplePool(ctx) + pool := nostr.NewSimplePool(ctx, opts...) for _, url := range relayUrls { log("connecting to %s... ", url) if relay, err := pool.EnsureRelay(url); err == nil { @@ -164,7 +168,3 @@ func gatherSecretKeyFromArguments(c *cli.Context) (string, error) { return sec, nil } - -func isAuthRequired(msg string) bool { - return strings.HasPrefix(msg, "msg: auth-required:") -} diff --git a/nsecbunker.go b/nsecbunker.go index 8ebaad8..b960d7f 100644 --- a/nsecbunker.go +++ b/nsecbunker.go @@ -97,8 +97,7 @@ var nsecbunker = &cli.Command{ log("~ responding with %s\n", string(jresp)) if alwaysYes || harmless || askUserIfWeCanRespond() { - _, err := ie.Relay.Publish(c.Context, eventResponse) - if err == nil { + if err := ie.Relay.Publish(c.Context, eventResponse); err == nil { log("* sent response!\n") } else { log("* failed to send response: %s\n", err) diff --git a/req.go b/req.go index d4ac174..2374b04 100644 --- a/req.go +++ b/req.go @@ -84,14 +84,28 @@ example: Usage: "a NIP-50 search query, use it only with relays that explicitly support it", Category: CATEGORY_FILTER_ATTRIBUTES, }, + &cli.BoolFlag{ + Name: "stream", + Usage: "keep the subscription open, printing all events as they are returned", + DefaultText: "false, will close on EOSE", + }, &cli.BoolFlag{ Name: "bare", Usage: "when printing the filter, print just the filter, not enveloped in a [\"REQ\", ...] array", }, &cli.BoolFlag{ - Name: "stream", - Usage: "keep the subscription open, printing all events as they are returned", - DefaultText: "false, will close on EOSE", + Name: "auth", + Usage: "always perform NIP-42 \"AUTH\" when facing an \"auth-required: \" rejection and try again", + }, + &cli.StringFlag{ + Name: "sec", + Usage: "secret key to sign the AUTH challenge, as hex or nsec", + DefaultText: "the key '1'", + Value: "0000000000000000000000000000000000000000000000000000000000000001", + }, + &cli.BoolFlag{ + Name: "prompt-sec", + Usage: "prompt the user to paste a hex or nsec with which to sign the AUTH challenge", }, }, ArgsUsage: "[relay...]", @@ -100,7 +114,18 @@ example: relayUrls := c.Args().Slice() if len(relayUrls) > 0 { var relays []*nostr.Relay - pool, relays = connectToAllRelays(c.Context, relayUrls) + pool, relays = connectToAllRelays(c.Context, relayUrls, nostr.WithAuthHandler(func(evt *nostr.Event) error { + if !c.Bool("auth") { + return fmt.Errorf("auth not authorized") + } + sec, err := gatherSecretKeyFromArguments(c) + if err != nil { + return err + } + pk, _ := nostr.GetPublicKey(sec) + log("performing auth as %s...\n", pk) + return evt.Sign(sec) + })) if len(relays) == 0 { log("failed to connect to any of the given relays.\n") os.Exit(3) From 0860cfcf6d1c53dc9bb383ab55c611bbc86b2718 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sat, 9 Dec 2023 16:37:47 -0300 Subject: [PATCH 050/401] rename nsecbunker->bunker. --- nsecbunker.go => bunker.go | 4 ++-- main.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename nsecbunker.go => bunker.go (98%) diff --git a/nsecbunker.go b/bunker.go similarity index 98% rename from nsecbunker.go rename to bunker.go index b960d7f..36418a6 100644 --- a/nsecbunker.go +++ b/bunker.go @@ -13,8 +13,8 @@ import ( "github.com/urfave/cli/v2" ) -var nsecbunker = &cli.Command{ - Name: "nsecbunker", +var bunker = &cli.Command{ + Name: "bunker", Usage: "starts a NIP-46 signer daemon with the given --sec key", ArgsUsage: "[relay...]", Description: ``, diff --git a/main.go b/main.go index 556b957..e70a64c 100644 --- a/main.go +++ b/main.go @@ -19,7 +19,7 @@ var app = &cli.App{ encode, verify, relay, - nsecbunker, + bunker, }, Flags: []cli.Flag{ &cli.BoolFlag{ From e5b0b159084c4b3f6648fedd3f28aeb3c1229a52 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sat, 9 Dec 2023 17:11:55 -0300 Subject: [PATCH 051/401] bunker: improve error message. --- bunker.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bunker.go b/bunker.go index 36418a6..cc96738 100644 --- a/bunker.go +++ b/bunker.go @@ -87,7 +87,7 @@ var bunker = &cli.Command{ for ie := range events { req, resp, eventResponse, harmless, err := signer.HandleRequest(ie.Event) if err != nil { - log("< failed to handle request from %s: %w", ie.Event.PubKey, err) + log("< failed to handle request from %s: %s", ie.Event.PubKey, err.Error()) continue } From bfa72640cd6bae6d61943a928b3f1dd248a3bf2d Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sat, 9 Dec 2023 17:42:01 -0300 Subject: [PATCH 052/401] bunker: a better prompt. --- bunker.go | 37 +++++++++++++++++++++++++++---------- go.mod | 2 ++ go.sum | 7 +++++++ 3 files changed, 36 insertions(+), 10 deletions(-) diff --git a/bunker.go b/bunker.go index cc96738..162aad7 100644 --- a/bunker.go +++ b/bunker.go @@ -6,11 +6,12 @@ import ( "net/url" "os" - "github.com/bgentry/speakeasy" + "github.com/manifoldco/promptui" "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/nip19" "github.com/nbd-wtf/go-nostr/nip46" "github.com/urfave/cli/v2" + "golang.org/x/exp/slices" ) var bunker = &cli.Command{ @@ -96,7 +97,7 @@ var bunker = &cli.Command{ jresp, _ := json.MarshalIndent(resp, " ", " ") log("~ responding with %s\n", string(jresp)) - if alwaysYes || harmless || askUserIfWeCanRespond() { + if alwaysYes || harmless || askProceed(ie.Event.PubKey) { if err := ie.Relay.Publish(c.Context, eventResponse); err == nil { log("* sent response!\n") } else { @@ -109,15 +110,31 @@ var bunker = &cli.Command{ }, } -func askUserIfWeCanRespond() bool { - answer, err := speakeasy.FAsk(os.Stderr, - fmt.Sprintf("proceed? y/n")) - if err != nil { - return false - } - if answer == "y" || answer == "yes" { +var allowedSources = make([]string, 0, 2) + +func askProceed(source string) bool { + if slices.Contains(allowedSources, source) { return true } - return askUserIfWeCanRespond() + prompt := promptui.Select{ + Label: "proceed?", + Items: []string{ + "no", + "yes", + "always from this source", + }, + } + n, _, _ := prompt.Run() + switch n { + case 0: + return false + case 1: + return true + case 2: + allowedSources = append(allowedSources, source) + return true + } + + return false } diff --git a/go.mod b/go.mod index 8c42cf8..5d589d5 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/btcsuite/btcd/btcutil v1.1.3 // indirect github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect @@ -29,6 +30,7 @@ require ( github.com/gobwas/ws v1.3.1 // indirect github.com/golang/glog v1.1.2 // indirect github.com/josharian/intern v1.0.0 // indirect + github.com/manifoldco/promptui v0.9.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect diff --git a/go.sum b/go.sum index 6c8567a..5f846fa 100644 --- a/go.sum +++ b/go.sum @@ -28,6 +28,10 @@ github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46f github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -81,6 +85,8 @@ github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlT github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= +github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= github.com/nbd-wtf/go-nostr v0.27.0 h1:h6JmMMmfNcAORTL2kk/K3+U6Mju6rk/IjcHA/PMeOc8= github.com/nbd-wtf/go-nostr v0.27.0/go.mod h1:bkffJI+x914sPQWum9ZRUn66D7NpDnAoWo1yICvj3/0= github.com/nbd-wtf/nostr-sdk v0.0.5 h1:rec+FcDizDVO0W25PX0lgYMXvP7zNNOgI3Fu9UCm4BY= @@ -133,6 +139,7 @@ golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/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-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= From 2d1e27f766be997feea7f1d09b71459704e40d5e Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sun, 10 Dec 2023 20:49:08 -0300 Subject: [PATCH 053/401] fix for nil error case on publish. --- event.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/event.go b/event.go index 6fa3e99..c143ac6 100644 --- a/event.go +++ b/event.go @@ -222,7 +222,8 @@ example: ctx, cancel := context.WithTimeout(c.Context, 10*time.Second) defer cancel() - if err := relay.Publish(ctx, evt); err == nil { + err := relay.Publish(ctx, evt) + if err == nil { // published fine log("success.\n") continue nextline From f0d90b567ce3eb0d2eaa4ed32b308540f26b888a Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Tue, 12 Dec 2023 21:30:04 +0900 Subject: [PATCH 054/401] -since now --- req.go | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/req.go b/req.go index 2374b04..12c7e0e 100644 --- a/req.go +++ b/req.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "os" + "strconv" "strings" "github.com/mailru/easyjson" @@ -61,7 +62,7 @@ example: Usage: "shortcut for --tag p=", Category: CATEGORY_FILTER_ATTRIBUTES, }, - &cli.IntFlag{ + &cli.StringFlag{ Name: "since", Aliases: []string{"s"}, Usage: "only accept events newer than this (unix timestamp)", @@ -184,9 +185,16 @@ example: filter.Tags[tag[0]] = append(filter.Tags[tag[0]], tag[1]) } - if since := c.Int("since"); since != 0 { - ts := nostr.Timestamp(since) - filter.Since = &ts + if since := c.String("since"); since != "" { + if since == "now" { + ts := nostr.Now() + filter.Since = &ts + } else if i, err := strconv.Atoi(since); err == nil { + ts := nostr.Timestamp(i) + filter.Since = &ts + } else { + return fmt.Errorf("parse error: Invalid numeric literal %q", since) + } } if until := c.Int("until"); until != 0 { ts := nostr.Timestamp(until) From 242b028656868d11841de72c6ec06991100bd0a5 Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Tue, 12 Dec 2023 21:34:50 +0900 Subject: [PATCH 055/401] -until now --- req.go | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/req.go b/req.go index 12c7e0e..8819f84 100644 --- a/req.go +++ b/req.go @@ -68,7 +68,7 @@ example: Usage: "only accept events newer than this (unix timestamp)", Category: CATEGORY_FILTER_ATTRIBUTES, }, - &cli.IntFlag{ + &cli.StringFlag{ Name: "until", Aliases: []string{"u"}, Usage: "only accept events older than this (unix timestamp)", @@ -196,9 +196,16 @@ example: return fmt.Errorf("parse error: Invalid numeric literal %q", since) } } - if until := c.Int("until"); until != 0 { - ts := nostr.Timestamp(until) - filter.Until = &ts + if until := c.String("until"); until != "" { + if until == "now" { + ts := nostr.Now() + filter.Until = &ts + } else if i, err := strconv.Atoi(until); err == nil { + ts := nostr.Timestamp(i) + filter.Until = &ts + } else { + return fmt.Errorf("parse error: Invalid numeric literal %q", until) + } } if limit := c.Int("limit"); limit != 0 { filter.Limit = limit From f35cb4bd1dd4628b86e73e4ba865abefbefb36a0 Mon Sep 17 00:00:00 2001 From: Daniel Cadenas Date: Wed, 13 Dec 2023 19:31:52 -0300 Subject: [PATCH 056/401] Fix tags with values containing = --- event.go | 10 +++++----- example_test.go | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/event.go b/event.go index c143ac6..cf1c5d1 100644 --- a/event.go +++ b/event.go @@ -153,12 +153,12 @@ example: tags := make(nostr.Tags, 0, 5) for _, tagFlag := range c.StringSlice("tag") { // tags are in the format key=value - spl := strings.Split(tagFlag, "=") - if len(spl) == 2 && len(spl[0]) > 0 { - tag := nostr.Tag{spl[0]} + tagName, tagValue, found := strings.Cut(tagFlag, "=") + tag := []string{tagName} + if found { // tags may also contain extra elements separated with a ";" - spl2 := strings.Split(spl[1], ";") - tag = append(tag, spl2...) + tagValues := strings.Split(tagValue, ";") + tag = append(tag, tagValues...) // ~ tags = append(tags, tag) } diff --git a/example_test.go b/example_test.go index 9005b8d..f1c4cd8 100644 --- a/example_test.go +++ b/example_test.go @@ -7,9 +7,9 @@ func ExampleEventBasic() { } func ExampleEventComplex() { - app.Run([]string{"nak", "event", "--ts", "1699485669", "-k", "11", "-c", "skjdbaskd", "--sec", "17", "-t", "t=spam", "-e", "36d88cf5fcc449f2390a424907023eda7a74278120eebab8d02797cd92e7e29c", "-t", "r=https://abc.def;nothing"}) + app.Run([]string{"nak", "event", "--ts", "1699485669", "-k", "11", "-c", "skjdbaskd", "--sec", "17", "-t", "t=spam", "-e", "36d88cf5fcc449f2390a424907023eda7a74278120eebab8d02797cd92e7e29c", "-t", "r=https://abc.def?name=foobar;nothing"}) // Output: - // {"id":"aec4de6d051a7c2b6ca2d087903d42051a31e07fb742f1240970084822de10a6","pubkey":"2fa2104d6b38d11b0230010559879124e42ab8dfeff5ff29dc9cdadd4ecacc3f","created_at":1699485669,"kind":11,"tags":[["t","spam"],["r","https://abc.def","nothing"],["e","36d88cf5fcc449f2390a424907023eda7a74278120eebab8d02797cd92e7e29c"]],"content":"skjdbaskd","sig":"1165ac7a27d774d351ef19c8e918fb22f4005fcba193976c3d7edba6ef87ead7f14467f376a9e199f8371835368d86a8506f591e382528d00287fb168a7b8f38"} + // {"id":"19aba166dcf354bf5ef64f4afe69ada1eb851495001ee05e07d393ee8c8ea179","pubkey":"2fa2104d6b38d11b0230010559879124e42ab8dfeff5ff29dc9cdadd4ecacc3f","created_at":1699485669,"kind":11,"tags":[["t","spam"],["r","https://abc.def?name=foobar","nothing"],["e","36d88cf5fcc449f2390a424907023eda7a74278120eebab8d02797cd92e7e29c"]],"content":"skjdbaskd","sig":"cf452def4a68341c897c3fc96fa34dc6895a5b8cc266d4c041bcdf758ec992ec5adb8b0179e98552aaaf9450526a26d7e62e413b15b1c57e0cfc8db6b29215d7"} } func ExampleReq() { From 5415fd369cd49bb32e7c0850dd22f6fab962c7e3 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Fri, 15 Dec 2023 11:15:12 -0300 Subject: [PATCH 057/401] update go-nostr to fix pool infinite loop. --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 5d589d5..9d35890 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ toolchain go1.21.0 require ( github.com/bgentry/speakeasy v0.1.0 github.com/mailru/easyjson v0.7.7 - github.com/nbd-wtf/go-nostr v0.27.0 + github.com/nbd-wtf/go-nostr v0.27.2 github.com/nbd-wtf/nostr-sdk v0.0.5 github.com/urfave/cli/v2 v2.25.7 golang.org/x/exp v0.0.0-20231006140011-7918f672742d diff --git a/go.sum b/go.sum index 5f846fa..c9719b4 100644 --- a/go.sum +++ b/go.sum @@ -89,6 +89,8 @@ github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYt github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= github.com/nbd-wtf/go-nostr v0.27.0 h1:h6JmMMmfNcAORTL2kk/K3+U6Mju6rk/IjcHA/PMeOc8= github.com/nbd-wtf/go-nostr v0.27.0/go.mod h1:bkffJI+x914sPQWum9ZRUn66D7NpDnAoWo1yICvj3/0= +github.com/nbd-wtf/go-nostr v0.27.2 h1:RImJfo8IgOK2rgGNif2FHFXq4sjNwzeZILUp9JGtQDg= +github.com/nbd-wtf/go-nostr v0.27.2/go.mod h1:e5WOFsKEpslDOxIgK00NqemH7KuAvKIW6sBXeJYCfUs= github.com/nbd-wtf/nostr-sdk v0.0.5 h1:rec+FcDizDVO0W25PX0lgYMXvP7zNNOgI3Fu9UCm4BY= github.com/nbd-wtf/nostr-sdk v0.0.5/go.mod h1:iJJsikesCGLNFZ9dLqhLPDzdt924EagUmdQxT3w2Lmk= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= From f295f130f26045c391e03d830820da3f45cbea51 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sat, 23 Dec 2023 21:30:49 -0300 Subject: [PATCH 058/401] remove .scalafmt.conf --- .scalafmt.conf | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 .scalafmt.conf diff --git a/.scalafmt.conf b/.scalafmt.conf deleted file mode 100644 index 7976ad1..0000000 --- a/.scalafmt.conf +++ /dev/null @@ -1,2 +0,0 @@ -version = 3.5.8 -runner.dialect = scala3 From 8373da647edafa7e5343276d17445e247da3b312 Mon Sep 17 00:00:00 2001 From: OHASHI Hideya Date: Sun, 24 Dec 2023 14:46:35 +0900 Subject: [PATCH 059/401] Fix tags with values containing = --- count.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/count.go b/count.go index 906cab1..339c04b 100644 --- a/count.go +++ b/count.go @@ -78,7 +78,7 @@ var count = &cli.Command{ tags := make([][]string, 0, 5) for _, tagFlag := range c.StringSlice("tag") { - spl := strings.Split(tagFlag, "=") + spl := strings.SplitN(tagFlag, "=", 2) if len(spl) == 2 && len(spl[0]) == 1 { tags = append(tags, spl) } else { From 16c1e795bdbfda06b539c2932ca09b752e761822 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 2 Jan 2024 11:05:43 -0300 Subject: [PATCH 060/401] fetch: more places to fetch relay lists from. --- fetch.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fetch.go b/fetch.go index 96e267e..86936a7 100644 --- a/fetch.go +++ b/fetch.go @@ -70,7 +70,8 @@ var fetch = &cli.Command{ pool := nostr.NewSimplePool(c.Context) if authorHint != "" { relayList := sdk.FetchRelaysForPubkey(c.Context, pool, authorHint, - "wss://purplepag.es", "wss://offchain.pub", "wss://public.relaying.io") + "wss://purplepag.es", "wss://relay.damus.io", "wss://relay.noswhere.com", + "wss://nos.lol", "wss://public.relaying.io", "wss://relay.nostr.band") for _, relayListItem := range relayList { if relayListItem.Outbox { relays = append(relays, relayListItem.URL) From 637b9440efd47b64594ba8d33a9ddfb5afed7cb4 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Wed, 10 Jan 2024 21:19:19 -0300 Subject: [PATCH 061/401] upgrade go-nostr and xsync. --- go.mod | 11 +++-------- go.sum | 35 ++++++----------------------------- 2 files changed, 9 insertions(+), 37 deletions(-) diff --git a/go.mod b/go.mod index 9d35890..116ccf6 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,8 @@ toolchain go1.21.0 require ( github.com/bgentry/speakeasy v0.1.0 github.com/mailru/easyjson v0.7.7 - github.com/nbd-wtf/go-nostr v0.27.2 + github.com/manifoldco/promptui v0.9.0 + github.com/nbd-wtf/go-nostr v0.28.0 github.com/nbd-wtf/nostr-sdk v0.0.5 github.com/urfave/cli/v2 v2.25.7 golang.org/x/exp v0.0.0-20231006140011-7918f672742d @@ -17,22 +18,16 @@ require ( github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect github.com/btcsuite/btcd/btcutil v1.1.3 // indirect github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2 // indirect - github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect - github.com/dgraph-io/ristretto v0.1.1 // indirect - github.com/dustin/go-humanize v1.0.1 // indirect github.com/fiatjaf/eventstore v0.2.16 // indirect github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/pool v0.2.1 // indirect github.com/gobwas/ws v1.3.1 // indirect - github.com/golang/glog v1.1.2 // indirect github.com/josharian/intern v1.0.0 // indirect - github.com/manifoldco/promptui v0.9.0 // indirect - github.com/pkg/errors v0.9.1 // indirect - github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect + github.com/puzpuzpuz/xsync/v3 v3.0.2 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/tidwall/gjson v1.17.0 // indirect github.com/tidwall/match v1.1.1 // indirect diff --git a/go.sum b/go.sum index c9719b4..8d6139d 100644 --- a/go.sum +++ b/go.sum @@ -25,12 +25,11 @@ github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= @@ -45,13 +44,6 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeC github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= -github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8= -github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= -github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= -github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= -github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= -github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/fiatjaf/eventstore v0.2.16 h1:NR64mnyUT5nJR8Sj2AwJTd1Hqs5kKJcCFO21ggUkvWg= github.com/fiatjaf/eventstore v0.2.16/go.mod h1:rUc1KhVufVmC+HUOiuPweGAcvG6lEOQCkRCn2Xn5VRA= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= @@ -62,9 +54,6 @@ github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.3.1 h1:Qi34dfLMWJbiKaNbDVzM9x27nZBjmkaW6i4+Ku+pGVU= github.com/gobwas/ws v1.3.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/glog v1.1.2 h1:DVjP2PbBOzHyzA+dn3WhHIq4NdVu3Q+pvivFICf/7fo= -github.com/golang/glog v1.1.2/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= @@ -87,10 +76,8 @@ github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0 github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= -github.com/nbd-wtf/go-nostr v0.27.0 h1:h6JmMMmfNcAORTL2kk/K3+U6Mju6rk/IjcHA/PMeOc8= -github.com/nbd-wtf/go-nostr v0.27.0/go.mod h1:bkffJI+x914sPQWum9ZRUn66D7NpDnAoWo1yICvj3/0= -github.com/nbd-wtf/go-nostr v0.27.2 h1:RImJfo8IgOK2rgGNif2FHFXq4sjNwzeZILUp9JGtQDg= -github.com/nbd-wtf/go-nostr v0.27.2/go.mod h1:e5WOFsKEpslDOxIgK00NqemH7KuAvKIW6sBXeJYCfUs= +github.com/nbd-wtf/go-nostr v0.28.0 h1:SLYyoFeCNYb7HyWtmPUzD6rifBOMR66Spj5fzCk+5GE= +github.com/nbd-wtf/go-nostr v0.28.0/go.mod h1:OQ8sNLFJnsj17BdqZiLSmjJBIFTfDqckEYC3utS4qoY= github.com/nbd-wtf/nostr-sdk v0.0.5 h1:rec+FcDizDVO0W25PX0lgYMXvP7zNNOgI3Fu9UCm4BY= github.com/nbd-wtf/nostr-sdk v0.0.5/go.mod h1:iJJsikesCGLNFZ9dLqhLPDzdt924EagUmdQxT3w2Lmk= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= @@ -102,19 +89,13 @@ github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5 github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/puzpuzpuz/xsync/v2 v2.5.1 h1:mVGYAvzDSu52+zaGyNjC+24Xw2bQi3kTr4QJ6N9pIIU= -github.com/puzpuzpuz/xsync/v2 v2.5.1/go.mod h1:gD2H2krq/w52MfPLE+Uy64TzJDVY7lP2znR9qmR35kU= +github.com/puzpuzpuz/xsync/v3 v3.0.2 h1:3yESHrRFYr6xzkz61LLkvNiPFXxJEAABanTQpKbAaew= +github.com/puzpuzpuz/xsync/v3 v3.0.2/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= github.com/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM= github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= @@ -150,7 +131,6 @@ golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= @@ -170,9 +150,6 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From a30f422d7dc8f371aebdcb5312aec6ecad48d91d Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Thu, 11 Jan 2024 21:29:39 -0300 Subject: [PATCH 062/401] close relay websockets cleanly. --- event.go | 6 ++++++ fetch.go | 10 +++++++++- req.go | 6 ++++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/event.go b/event.go index cf1c5d1..59e6ec3 100644 --- a/event.go +++ b/event.go @@ -108,6 +108,12 @@ example: } } + defer func() { + for _, relay := range relays { + relay.Close() + } + }() + // gather the secret key sec, err := gatherSecretKeyFromArguments(c) if err != nil { diff --git a/fetch.go b/fetch.go index 86936a7..33cf46c 100644 --- a/fetch.go +++ b/fetch.go @@ -24,6 +24,15 @@ var fetch = &cli.Command{ }, ArgsUsage: "[nip19code]", Action: func(c *cli.Context) error { + pool := nostr.NewSimplePool(c.Context) + + defer func() { + pool.Relays.Range(func(_ string, relay *nostr.Relay) bool { + relay.Close() + return true + }) + }() + for code := range getStdinLinesOrFirstArgument(c) { filter := nostr.Filter{} @@ -67,7 +76,6 @@ var fetch = &cli.Command{ authorHint = v } - pool := nostr.NewSimplePool(c.Context) if authorHint != "" { relayList := sdk.FetchRelaysForPubkey(c.Context, pool, authorHint, "wss://purplepag.es", "wss://relay.damus.io", "wss://relay.noswhere.com", diff --git a/req.go b/req.go index 8819f84..eda6c08 100644 --- a/req.go +++ b/req.go @@ -135,6 +135,12 @@ example: for i, relay := range relays { relayUrls[i] = relay.URL } + + defer func() { + for _, relay := range relays { + relay.Close() + } + }() } for stdinFilter := range getStdinLinesOrBlank() { From 584881266ed5fc98302b7412741412789f6c8eee Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Thu, 11 Jan 2024 21:41:50 -0300 Subject: [PATCH 063/401] bunker: update to go-nostr nip46 api breaking change. --- bunker.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bunker.go b/bunker.go index 162aad7..26dd92e 100644 --- a/bunker.go +++ b/bunker.go @@ -84,7 +84,7 @@ var bunker = &cli.Command{ }, }) - signer := nip46.NewSigner(sec) + signer := nip46.NewStaticKeySigner(sec) for ie := range events { req, resp, eventResponse, harmless, err := signer.HandleRequest(ie.Event) if err != nil { From ad7010e506bf1988ee430824a91d10a67331d31b Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Thu, 11 Jan 2024 21:48:54 -0300 Subject: [PATCH 064/401] use scanner.Buffer() to make stdin able to parse hellish giant events up to 256kb (the default was 64kb). --- event.go | 1 + helpers.go | 1 + 2 files changed, 2 insertions(+) diff --git a/event.go b/event.go index 59e6ec3..6f456fa 100644 --- a/event.go +++ b/event.go @@ -169,6 +169,7 @@ example: tags = append(tags, tag) } } + for _, etag := range c.StringSlice("e") { tags = append(tags, []string{"e", etag}) mustRehashAndResign = true diff --git a/helpers.go b/helpers.go index 971fafd..f8a2661 100644 --- a/helpers.go +++ b/helpers.go @@ -64,6 +64,7 @@ func writeStdinLinesOrNothing(ch chan string) (hasStdinLines bool) { // piped go func() { scanner := bufio.NewScanner(os.Stdin) + scanner.Buffer(make([]byte, 16*1024), 256*1024) for scanner.Scan() { ch <- strings.TrimSpace(scanner.Text()) } From 48d19196bbe073150e9e38b26705083e9e52ac62 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 16 Jan 2024 09:16:56 -0300 Subject: [PATCH 065/401] fix publishing to multiple relays. --- event.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/event.go b/event.go index 6f456fa..cd06629 100644 --- a/event.go +++ b/event.go @@ -123,7 +123,6 @@ example: doAuth := c.Bool("auth") // then process input and generate events - nextline: for stdinEvent := range getStdinLinesOrBlank() { evt := nostr.Event{ Tags: make(nostr.Tags, 0, 3), @@ -233,7 +232,7 @@ example: if err == nil { // published fine log("success.\n") - continue nextline + continue // continue to next relay } // error publishing From 59a2c16b42c680cc949b335514061c75fb0649ac Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Wed, 17 Jan 2024 08:47:51 -0300 Subject: [PATCH 066/401] event: -d shortcut flag and use .AppendUnique() --- event.go | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/event.go b/event.go index cd06629..cdaca12 100644 --- a/event.go +++ b/event.go @@ -87,6 +87,11 @@ example: Usage: "shortcut for --tag p=", Category: CATEGORY_EVENT_FIELDS, }, + &cli.StringSliceFlag{ + Name: "d", + Usage: "shortcut for --tag d=", + Category: CATEGORY_EVENT_FIELDS, + }, &cli.StringFlag{ Name: "created-at", Aliases: []string{"time", "ts"}, @@ -165,16 +170,20 @@ example: tagValues := strings.Split(tagValue, ";") tag = append(tag, tagValues...) // ~ - tags = append(tags, tag) + tags = tags.AppendUnique(tag) } } for _, etag := range c.StringSlice("e") { - tags = append(tags, []string{"e", etag}) + tags = tags.AppendUnique([]string{"e", etag}) mustRehashAndResign = true } for _, ptag := range c.StringSlice("p") { - tags = append(tags, []string{"p", ptag}) + tags = tags.AppendUnique([]string{"p", ptag}) + mustRehashAndResign = true + } + for _, dtag := range c.StringSlice("d") { + tags = tags.AppendUnique([]string{"d", dtag}) mustRehashAndResign = true } if len(tags) > 0 { From 77103cae0cb26b699a427ded66dc3615c56e811d Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Wed, 17 Jan 2024 08:50:59 -0300 Subject: [PATCH 067/401] req: -d flag too. --- req.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/req.go b/req.go index eda6c08..878fa7d 100644 --- a/req.go +++ b/req.go @@ -62,6 +62,11 @@ example: Usage: "shortcut for --tag p=", Category: CATEGORY_FILTER_ATTRIBUTES, }, + &cli.StringSliceFlag{ + Name: "d", + Usage: "shortcut for --tag d=", + Category: CATEGORY_FILTER_ATTRIBUTES, + }, &cli.StringFlag{ Name: "since", Aliases: []string{"s"}, @@ -179,6 +184,9 @@ example: for _, ptag := range c.StringSlice("p") { tags = append(tags, []string{"p", ptag}) } + for _, dtag := range c.StringSlice("d") { + tags = append(tags, []string{"d", dtag}) + } if len(tags) > 0 && filter.Tags == nil { filter.Tags = make(nostr.TagMap) From b17887fe218294a2433542e6b5b659036e1dd7a9 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sun, 21 Jan 2024 07:45:22 -0300 Subject: [PATCH 068/401] replace validate32BytesHex() with native calls from go-nostr. --- encode.go | 29 +++++++++++++++-------------- go.mod | 2 +- go.sum | 4 ++-- helpers.go | 17 +---------------- 4 files changed, 19 insertions(+), 33 deletions(-) diff --git a/encode.go b/encode.go index 1fc3b08..320bef2 100644 --- a/encode.go +++ b/encode.go @@ -3,6 +3,7 @@ package main import ( "fmt" + "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/nip19" "github.com/urfave/cli/v2" ) @@ -29,8 +30,8 @@ var encode = &cli.Command{ Usage: "encode a hex public key into bech32 'npub' format", Action: func(c *cli.Context) error { for target := range getStdinLinesOrFirstArgument(c) { - if err := validate32BytesHex(target); err != nil { - lineProcessingError(c, "invalid public key: %s", target, err) + if ok := nostr.IsValidPublicKey(target); !ok { + lineProcessingError(c, "invalid public key: %s", target) continue } @@ -50,8 +51,8 @@ var encode = &cli.Command{ Usage: "encode a hex private key into bech32 'nsec' format", Action: func(c *cli.Context) error { for target := range getStdinLinesOrFirstArgument(c) { - if err := validate32BytesHex(target); err != nil { - lineProcessingError(c, "invalid private key: %s", target, err) + if ok := nostr.IsValid32ByteHex(target); !ok { + lineProcessingError(c, "invalid private key: %s", target) continue } @@ -78,8 +79,8 @@ var encode = &cli.Command{ }, Action: func(c *cli.Context) error { for target := range getStdinLinesOrFirstArgument(c) { - if err := validate32BytesHex(target); err != nil { - lineProcessingError(c, "invalid public key: %s", target, err) + if ok := nostr.IsValid32ByteHex(target); !ok { + lineProcessingError(c, "invalid public key: %s", target) continue } @@ -115,15 +116,15 @@ var encode = &cli.Command{ }, Action: func(c *cli.Context) error { for target := range getStdinLinesOrFirstArgument(c) { - if err := validate32BytesHex(target); err != nil { - lineProcessingError(c, "invalid event id: %s", target, err) + if ok := nostr.IsValid32ByteHex(target); !ok { + lineProcessingError(c, "invalid event id: %s", target) continue } author := c.String("author") if author != "" { - if err := validate32BytesHex(author); err != nil { - return err + if ok := nostr.IsValidPublicKey(author); !ok { + return fmt.Errorf("invalid 'author' public key") } } @@ -174,8 +175,8 @@ var encode = &cli.Command{ Action: func(c *cli.Context) error { for d := range getStdinLinesOrBlank() { pubkey := c.String("pubkey") - if err := validate32BytesHex(pubkey); err != nil { - return err + if ok := nostr.IsValidPublicKey(pubkey); !ok { + return fmt.Errorf("invalid 'pubkey'") } kind := c.Int("kind") @@ -212,8 +213,8 @@ var encode = &cli.Command{ Usage: "generate note1 event codes (not recommended)", Action: func(c *cli.Context) error { for target := range getStdinLinesOrFirstArgument(c) { - if err := validate32BytesHex(target); err != nil { - lineProcessingError(c, "invalid event id: %s", target, err) + if ok := nostr.IsValid32ByteHex(target); !ok { + lineProcessingError(c, "invalid event id: %s", target) continue } diff --git a/go.mod b/go.mod index 116ccf6..95baf94 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/bgentry/speakeasy v0.1.0 github.com/mailru/easyjson v0.7.7 github.com/manifoldco/promptui v0.9.0 - github.com/nbd-wtf/go-nostr v0.28.0 + github.com/nbd-wtf/go-nostr v0.28.1 github.com/nbd-wtf/nostr-sdk v0.0.5 github.com/urfave/cli/v2 v2.25.7 golang.org/x/exp v0.0.0-20231006140011-7918f672742d diff --git a/go.sum b/go.sum index 8d6139d..328fc7a 100644 --- a/go.sum +++ b/go.sum @@ -76,8 +76,8 @@ github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0 github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= -github.com/nbd-wtf/go-nostr v0.28.0 h1:SLYyoFeCNYb7HyWtmPUzD6rifBOMR66Spj5fzCk+5GE= -github.com/nbd-wtf/go-nostr v0.28.0/go.mod h1:OQ8sNLFJnsj17BdqZiLSmjJBIFTfDqckEYC3utS4qoY= +github.com/nbd-wtf/go-nostr v0.28.1 h1:XQi/lBsigBXHRm7IDBJE7SR9citCh9srgf8sA5iVW3A= +github.com/nbd-wtf/go-nostr v0.28.1/go.mod h1:OQ8sNLFJnsj17BdqZiLSmjJBIFTfDqckEYC3utS4qoY= github.com/nbd-wtf/nostr-sdk v0.0.5 h1:rec+FcDizDVO0W25PX0lgYMXvP7zNNOgI3Fu9UCm4BY= github.com/nbd-wtf/nostr-sdk v0.0.5/go.mod h1:iJJsikesCGLNFZ9dLqhLPDzdt924EagUmdQxT3w2Lmk= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= diff --git a/helpers.go b/helpers.go index f8a2661..204df0f 100644 --- a/helpers.go +++ b/helpers.go @@ -3,7 +3,6 @@ package main import ( "bufio" "context" - "encoding/hex" "fmt" "net/url" "os" @@ -96,20 +95,6 @@ func validateRelayURLs(wsurls []string) error { return nil } -func validate32BytesHex(target string) error { - if _, err := hex.DecodeString(target); err != nil { - return fmt.Errorf("target '%s' is not valid hex: %s", target, err) - } - if len(target) != 64 { - return fmt.Errorf("expected '%s' to be 64 characters (32 bytes), got %d", target, len(target)) - } - if strings.ToLower(target) != target { - return fmt.Errorf("expected target to be all lowercase hex. try again with '%s'", strings.ToLower(target)) - } - - return nil -} - func connectToAllRelays( ctx context.Context, relayUrls []string, @@ -163,7 +148,7 @@ func gatherSecretKeyFromArguments(c *cli.Context) (string, error) { return "", fmt.Errorf("invalid secret key: too large") } sec = strings.Repeat("0", 64-len(sec)) + sec // left-pad - if err := validate32BytesHex(sec); err != nil { + if ok := nostr.IsValid32ByteHex(sec); !ok { return "", fmt.Errorf("invalid secret key") } From 6a75c8aec316560aaa135c5601be1da9550f8037 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sun, 21 Jan 2024 18:11:52 -0300 Subject: [PATCH 069/401] UseShortOptionHandling and Suggest --- main.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/main.go b/main.go index e70a64c..c18b50e 100644 --- a/main.go +++ b/main.go @@ -8,8 +8,10 @@ import ( ) var app = &cli.App{ - Name: "nak", - Usage: "the nostr army knife command-line tool", + Name: "nak", + Suggest: true, + UseShortOptionHandling: true, + Usage: "the nostr army knife command-line tool", Commands: []*cli.Command{ req, count, From 3f7089e27ebad5be6cd9d3407a8864fcfa98f673 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 23 Jan 2024 10:20:44 -0300 Subject: [PATCH 070/401] signal that we accept patches over NIP-34. --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index fe93d49..4e771cf 100644 --- a/README.md +++ b/README.md @@ -81,3 +81,8 @@ invalid .id, expected 05bd99d54cb835f427e0092c4275ee44c7ff51219eff417c19f70c9e2c ```shell nak req -l 100 -k 1 -a 2edbcea694d164629854a52583458fd6d965b161e3c48b57d3aff01940558884 wss://relay.damus.io | jq -r '.content | match("nostr:((note1|nevent1)[a-z0-9]+)";"g") | .captures[0].string' | nak decode | jq -cr '{ids: [.id]}' | nak req wss://relay.damus.io ``` + +## Contributing to this repository + +Use NIP-34 to send your patches to `naddr1qqpkucttqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9nhwden5te0wfjkccte9ehx7um5wghxyctwvsq3vamnwvaz7tmjv4kxz7fwwpexjmtpdshxuet5qgsg04q5ypr6f4n65mv7e5hs05z50hy7vvgua8uc8szwtp262cfwn6srqsqqqauedy5x7y`. + From 14b69f36cf6a6842ee43bf74dbf255f4425466da Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Wed, 24 Jan 2024 22:38:51 -0300 Subject: [PATCH 071/401] -q to silence stderr, -qq to silence everything. --- count.go | 2 +- decode.go | 3 +-- encode.go | 12 ++++++------ event.go | 2 +- fetch.go | 4 +--- helpers.go | 2 ++ main.go | 17 +++++++++++------ relay.go | 2 +- req.go | 4 ++-- 9 files changed, 26 insertions(+), 22 deletions(-) diff --git a/count.go b/count.go index 339c04b..ba3f1ed 100644 --- a/count.go +++ b/count.go @@ -139,7 +139,7 @@ var count = &cli.Command{ var result string j, _ := json.Marshal([]any{"COUNT", "nak", filter}) result = string(j) - fmt.Println(result) + stdout(result) } return nil diff --git a/decode.go b/decode.go index d8b0fd9..371ddc2 100644 --- a/decode.go +++ b/decode.go @@ -3,7 +3,6 @@ package main import ( "encoding/hex" "encoding/json" - "fmt" "strings" "github.com/nbd-wtf/go-nostr" @@ -68,7 +67,7 @@ var decode = &cli.Command{ continue } - fmt.Println(decodeResult.JSON()) + stdout(decodeResult.JSON()) } diff --git a/encode.go b/encode.go index 320bef2..107c47d 100644 --- a/encode.go +++ b/encode.go @@ -36,7 +36,7 @@ var encode = &cli.Command{ } if npub, err := nip19.EncodePublicKey(target); err == nil { - fmt.Println(npub) + stdout(npub) } else { return err } @@ -57,7 +57,7 @@ var encode = &cli.Command{ } if npub, err := nip19.EncodePrivateKey(target); err == nil { - fmt.Println(npub) + stdout(npub) } else { return err } @@ -90,7 +90,7 @@ var encode = &cli.Command{ } if npub, err := nip19.EncodeProfile(target, relays); err == nil { - fmt.Println(npub) + stdout(npub) } else { return err } @@ -134,7 +134,7 @@ var encode = &cli.Command{ } if npub, err := nip19.EncodeEvent(target, relays, author); err == nil { - fmt.Println(npub) + stdout(npub) } else { return err } @@ -198,7 +198,7 @@ var encode = &cli.Command{ } if npub, err := nip19.EncodeEntity(pubkey, kind, d, relays); err == nil { - fmt.Println(npub) + stdout(npub) } else { return err } @@ -219,7 +219,7 @@ var encode = &cli.Command{ } if note, err := nip19.EncodeNote(target); err == nil { - fmt.Println(note) + stdout(note) } else { return err } diff --git a/event.go b/event.go index cdaca12..e75d837 100644 --- a/event.go +++ b/event.go @@ -226,7 +226,7 @@ example: j, _ := easyjson.Marshal(&evt) result = string(j) } - fmt.Println(result) + stdout(result) // publish to relays if len(relays) > 0 { diff --git a/fetch.go b/fetch.go index 33cf46c..7f3d966 100644 --- a/fetch.go +++ b/fetch.go @@ -1,8 +1,6 @@ package main import ( - "fmt" - "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/nip19" sdk "github.com/nbd-wtf/nostr-sdk" @@ -93,7 +91,7 @@ var fetch = &cli.Command{ } for ie := range pool.SubManyEose(c.Context, relays, nostr.Filters{filter}) { - fmt.Println(ie.Event) + stdout(ie.Event) } } diff --git a/helpers.go b/helpers.go index 204df0f..5cf95f7 100644 --- a/helpers.go +++ b/helpers.go @@ -25,6 +25,8 @@ var log = func(msg string, args ...any) { fmt.Fprintf(os.Stderr, msg, args...) } +var stdout = fmt.Println + func isPiped() bool { stat, _ := os.Stdin.Stat() return stat.Mode()&os.ModeCharDevice == 0 diff --git a/main.go b/main.go index c18b50e..c0175fa 100644 --- a/main.go +++ b/main.go @@ -1,12 +1,13 @@ package main import ( - "fmt" "os" "github.com/urfave/cli/v2" ) +var q int + var app = &cli.App{ Name: "nak", Suggest: true, @@ -25,12 +26,16 @@ var app = &cli.App{ }, Flags: []cli.Flag{ &cli.BoolFlag{ - Name: "silent", - Usage: "do not print logs and info messages to stderr", - Aliases: []string{"s"}, + Name: "quiet", + Usage: "do not print logs and info messages to stderr, use -qq to also not print anything to stdout", + Count: &q, + Aliases: []string{"q"}, Action: func(ctx *cli.Context, b bool) error { - if b { + if q >= 1 { log = func(msg string, args ...any) {} + if q >= 2 { + stdout = func(a ...any) (int, error) { return 0, nil } + } } return nil }, @@ -40,7 +45,7 @@ var app = &cli.App{ func main() { if err := app.Run(os.Args); err != nil { - fmt.Println(err) + stdout(err) os.Exit(1) } } diff --git a/relay.go b/relay.go index a0faee4..a3f7551 100644 --- a/relay.go +++ b/relay.go @@ -31,7 +31,7 @@ var relay = &cli.Command{ } pretty, _ := json.MarshalIndent(info, "", " ") - fmt.Println(string(pretty)) + stdout(string(pretty)) return nil }, } diff --git a/req.go b/req.go index 878fa7d..d164d6a 100644 --- a/req.go +++ b/req.go @@ -231,7 +231,7 @@ example: fn = pool.SubMany } for ie := range fn(c.Context, relayUrls, nostr.Filters{filter}) { - fmt.Println(ie.Event) + stdout(ie.Event) } } else { // no relays given, will just print the filter @@ -243,7 +243,7 @@ example: result = string(j) } - fmt.Println(result) + stdout(result) } } From 3dfcec69b71975533f61cd36a636fb099aa91c33 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Wed, 24 Jan 2024 22:43:23 -0300 Subject: [PATCH 072/401] --nevent flag on nak event to print an nevent at the end. --- event.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/event.go b/event.go index e75d837..a752ee8 100644 --- a/event.go +++ b/event.go @@ -11,6 +11,7 @@ import ( "github.com/mailru/easyjson" "github.com/nbd-wtf/go-nostr" + "github.com/nbd-wtf/go-nostr/nip19" "github.com/nbd-wtf/go-nostr/nson" "github.com/urfave/cli/v2" "golang.org/x/exp/slices" @@ -51,6 +52,10 @@ example: Name: "auth", Usage: "always perform NIP-42 \"AUTH\" when facing an \"auth-required: \" rejection and try again", }, + &cli.BoolFlag{ + Name: "nevent", + Usage: "print the nevent code (to stderr) after the event is published", + }, &cli.BoolFlag{ Name: "nson", Usage: "encode the event using NSON", @@ -229,6 +234,7 @@ example: stdout(result) // publish to relays + successRelays := make([]string, 0, len(relays)) if len(relays) > 0 { os.Stdout.Sync() for _, relay := range relays { @@ -241,6 +247,7 @@ example: if err == nil { // published fine log("success.\n") + successRelays = append(successRelays, relay.URL) continue // continue to next relay } @@ -259,6 +266,10 @@ example: } log("failed: %s\n", err) } + if len(successRelays) > 0 && c.Bool("nevent") { + nevent, _ := nip19.EncodeEvent(evt.ID, successRelays, evt.PubKey) + log(nevent + "\n") + } } } From f4921f1fe9e56921daa95dbf6cc86a8a0b826a81 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Thu, 25 Jan 2024 08:19:55 -0300 Subject: [PATCH 073/401] nak key: generate, public, encrypt, decrypt. --- go.mod | 3 +- go.sum | 6 ++- key.go | 131 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ main.go | 1 + 4 files changed, 138 insertions(+), 3 deletions(-) create mode 100644 key.go diff --git a/go.mod b/go.mod index 95baf94..4949687 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/bgentry/speakeasy v0.1.0 github.com/mailru/easyjson v0.7.7 github.com/manifoldco/promptui v0.9.0 - github.com/nbd-wtf/go-nostr v0.28.1 + github.com/nbd-wtf/go-nostr v0.28.2 github.com/nbd-wtf/nostr-sdk v0.0.5 github.com/urfave/cli/v2 v2.25.7 golang.org/x/exp v0.0.0-20231006140011-7918f672742d @@ -33,5 +33,6 @@ require ( github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect + golang.org/x/crypto v0.7.0 // indirect golang.org/x/sys v0.14.0 // indirect ) diff --git a/go.sum b/go.sum index 328fc7a..3b65c90 100644 --- a/go.sum +++ b/go.sum @@ -76,8 +76,8 @@ github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0 github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= -github.com/nbd-wtf/go-nostr v0.28.1 h1:XQi/lBsigBXHRm7IDBJE7SR9citCh9srgf8sA5iVW3A= -github.com/nbd-wtf/go-nostr v0.28.1/go.mod h1:OQ8sNLFJnsj17BdqZiLSmjJBIFTfDqckEYC3utS4qoY= +github.com/nbd-wtf/go-nostr v0.28.2 h1:KhpGcs6KMLBqYExzKoqt7vP5Re2f8Kpy9SavYZa2PTI= +github.com/nbd-wtf/go-nostr v0.28.2/go.mod h1:l9NRRaHPN+QwkqrjNKhnfYjQ0+nKP1xZrVxePPGUs+A= github.com/nbd-wtf/nostr-sdk v0.0.5 h1:rec+FcDizDVO0W25PX0lgYMXvP7zNNOgI3Fu9UCm4BY= github.com/nbd-wtf/nostr-sdk v0.0.5/go.mod h1:iJJsikesCGLNFZ9dLqhLPDzdt924EagUmdQxT3w2Lmk= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= @@ -111,6 +111,8 @@ github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsr golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= diff --git a/key.go b/key.go new file mode 100644 index 0000000..d51ee0f --- /dev/null +++ b/key.go @@ -0,0 +1,131 @@ +package main + +import ( + "fmt" + "strings" + + "github.com/nbd-wtf/go-nostr" + "github.com/nbd-wtf/go-nostr/nip19" + "github.com/nbd-wtf/go-nostr/nip49" + "github.com/urfave/cli/v2" +) + +var key = &cli.Command{ + Name: "key", + Usage: "operations on secret keys: generate, derive, encrypt, decrypt.", + Description: ``, + Subcommands: []*cli.Command{ + generate, + public, + encrypt, + decrypt, + }, +} + +var generate = &cli.Command{ + Name: "generate", + Usage: "generates a secret key", + Description: ``, + Action: func(c *cli.Context) error { + sec := nostr.GeneratePrivateKey() + stdout(sec) + return nil + }, +} + +var public = &cli.Command{ + Name: "public", + Usage: "computes a public key from a secret key", + Description: ``, + ArgsUsage: "[secret]", + Action: func(c *cli.Context) error { + for sec := range getSecretKeyFromStdinLinesOrFirstArgument(c) { + pubkey, err := nostr.GetPublicKey(sec) + if err != nil { + lineProcessingError(c, "failed to derive public key: %s", err) + continue + } + stdout(pubkey) + } + return nil + }, +} + +var encrypt = &cli.Command{ + Name: "encrypt", + Usage: "encrypts a secret key and prints an ncryptsec code", + Description: `uses the NIP-49 standard.`, + ArgsUsage: " ", + Flags: []cli.Flag{ + &cli.IntFlag{ + Name: "logn", + Usage: "the bigger the number the harder it will be to bruteforce the password", + Value: 16, + DefaultText: "16", + }, + }, + Action: func(c *cli.Context) error { + password := c.Args().Get(c.Args().Len() - 1) + if password == "" { + return fmt.Errorf("no password given") + } + for sec := range getSecretKeyFromStdinLinesOrFirstArgument(c) { + ncryptsec, err := nip49.Encrypt(sec, password, uint8(c.Int("logn")), 0x02) + if err != nil { + lineProcessingError(c, "failed to encrypt: %s", err) + continue + } + stdout(ncryptsec) + } + return nil + }, +} + +var decrypt = &cli.Command{ + Name: "decrypt", + Usage: "takes an ncrypsec and a password and decrypts it into an nsec", + Description: `uses the NIP-49 standard.`, + ArgsUsage: " ", + Action: func(c *cli.Context) error { + password := c.Args().Get(c.Args().Len() - 1) + if password == "" { + return fmt.Errorf("no password given") + } + for ncryptsec := range getStdinLinesOrFirstArgument(c) { + sec, err := nip49.Decrypt(ncryptsec, password) + if err != nil { + lineProcessingError(c, "failed to decrypt: %s", err) + continue + } + nsec, _ := nip19.EncodePrivateKey(sec) + stdout(nsec) + } + return nil + }, +} + +func getSecretKeyFromStdinLinesOrFirstArgument(c *cli.Context) chan string { + ch := make(chan string) + go func() { + for sec := range getStdinLinesOrFirstArgument(c) { + if sec == "" { + continue + } + if strings.HasPrefix(sec, "nsec1") { + _, data, err := nip19.Decode(sec) + if err != nil { + lineProcessingError(c, "invalid nsec code: %s", err) + continue + } + sec = data.(string) + } + if !nostr.IsValid32ByteHex(sec) { + lineProcessingError(c, "invalid hex secret key") + continue + } + ch <- sec + } + close(ch) + }() + return ch +} diff --git a/main.go b/main.go index c0175fa..1d0c194 100644 --- a/main.go +++ b/main.go @@ -20,6 +20,7 @@ var app = &cli.App{ event, decode, encode, + key, verify, relay, bunker, From 6f24112b5e0833c96d0b5fd34d1049a20f9422e1 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Mon, 29 Jan 2024 09:47:26 -0300 Subject: [PATCH 074/401] support ncryptsec in all operations that require a private key and have a nice password prompt. --- go.mod | 6 ++-- go.sum | 10 +++++-- helpers.go | 86 ++++++++++++++++++++++++++++++++++++++++++++++-------- 3 files changed, 86 insertions(+), 16 deletions(-) diff --git a/go.mod b/go.mod index 4949687..4b72b35 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,8 @@ go 1.21 toolchain go1.21.0 require ( - github.com/bgentry/speakeasy v0.1.0 + github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e + github.com/fatih/color v1.16.0 github.com/mailru/easyjson v0.7.7 github.com/manifoldco/promptui v0.9.0 github.com/nbd-wtf/go-nostr v0.28.2 @@ -18,7 +19,6 @@ require ( github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect github.com/btcsuite/btcd/btcutil v1.1.3 // indirect github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2 // indirect - github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect @@ -27,6 +27,8 @@ require ( github.com/gobwas/pool v0.2.1 // indirect github.com/gobwas/ws v1.3.1 // indirect github.com/josharian/intern v1.0.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/puzpuzpuz/xsync/v3 v3.0.2 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/tidwall/gjson v1.17.0 // indirect diff --git a/go.sum b/go.sum index 3b65c90..29a9176 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,4 @@ github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= -github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= -github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M= github.com/btcsuite/btcd v0.23.0/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY= @@ -44,6 +42,8 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeC github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= +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.2.16 h1:NR64mnyUT5nJR8Sj2AwJTd1Hqs5kKJcCFO21ggUkvWg= github.com/fiatjaf/eventstore v0.2.16/go.mod h1:rUc1KhVufVmC+HUOiuPweGAcvG6lEOQCkRCn2Xn5VRA= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= @@ -76,6 +76,11 @@ github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0 github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/nbd-wtf/go-nostr v0.28.2 h1:KhpGcs6KMLBqYExzKoqt7vP5Re2f8Kpy9SavYZa2PTI= github.com/nbd-wtf/go-nostr v0.28.2/go.mod h1:l9NRRaHPN+QwkqrjNKhnfYjQ0+nKP1xZrVxePPGUs+A= github.com/nbd-wtf/nostr-sdk v0.0.5 h1:rec+FcDizDVO0W25PX0lgYMXvP7zNNOgI3Fu9UCm4BY= @@ -133,6 +138,7 @@ golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/helpers.go b/helpers.go index 5cf95f7..ddd0bd1 100644 --- a/helpers.go +++ b/helpers.go @@ -3,14 +3,17 @@ package main import ( "bufio" "context" + "encoding/hex" "fmt" "net/url" "os" "strings" - "github.com/bgentry/speakeasy" + "github.com/chzyer/readline" + "github.com/fatih/color" "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/nip19" + "github.com/nbd-wtf/go-nostr/nip49" "github.com/urfave/cli/v2" ) @@ -128,31 +131,90 @@ func exitIfLineProcessingError(c *cli.Context) { } func gatherSecretKeyFromArguments(c *cli.Context) (string, error) { + var err error sec := c.String("sec") if c.Bool("prompt-sec") { if isPiped() { return "", fmt.Errorf("can't prompt for a secret key when processing data from a pipe, try again without --prompt-sec") } - var err error - sec, err = speakeasy.FAsk(os.Stderr, "type your secret key as nsec or hex: ") + sec, err = askPassword("type your secret key as ncryptsec, nsec or hex: ", nil) if err != nil { return "", fmt.Errorf("failed to get secret key: %w", err) } } - if strings.HasPrefix(sec, "nsec1") { - _, hex, err := nip19.Decode(sec) + if strings.HasPrefix(sec, "ncryptsec1") { + sec, err = promptDecrypt(sec) if err != nil { - return "", fmt.Errorf("invalid nsec: %w", err) + return "", fmt.Errorf("failed to decrypt: %w", err) } - sec = hex.(string) + } else if prefix, hexvalue, err := nip19.Decode(sec); err != nil { + return "", fmt.Errorf("invalid nsec: %w", err) + } else if prefix == "nsec" { + sec = hexvalue.(string) + } else if bsec, err := hex.DecodeString(strings.Repeat("0", 64-len(sec)) + sec); err == nil { + sec = hex.EncodeToString(bsec) } - if len(sec) > 64 { - return "", fmt.Errorf("invalid secret key: too large") - } - sec = strings.Repeat("0", 64-len(sec)) + sec // left-pad + if ok := nostr.IsValid32ByteHex(sec); !ok { return "", fmt.Errorf("invalid secret key") } - return sec, nil } + +func promptDecrypt(ncryptsec1 string) (string, error) { + for i := 1; i < 4; i++ { + var attemptStr string + if i > 1 { + attemptStr = fmt.Sprintf(" [%d/3]", i) + } + password, err := askPassword("type the password to decrypt your secret key"+attemptStr+": ", nil) + if err != nil { + return "", err + } + sec, err := nip49.Decrypt(ncryptsec1, password) + if err != nil { + continue + } + return sec, nil + } + return "", fmt.Errorf("couldn't decrypt private key") +} + +func ask(msg string, defaultValue string, shouldAskAgain func(answer string) bool) (string, error) { + return _ask(&readline.Config{ + Prompt: color.YellowString(msg), + InterruptPrompt: "^C", + DisableAutoSaveHistory: true, + }, msg, defaultValue, shouldAskAgain) +} + +func askPassword(msg string, shouldAskAgain func(answer string) bool) (string, error) { + config := &readline.Config{ + Prompt: color.YellowString(msg), + InterruptPrompt: "^C", + DisableAutoSaveHistory: true, + EnableMask: true, + MaskRune: '*', + } + return _ask(config, msg, "", shouldAskAgain) +} + +func _ask(config *readline.Config, msg string, defaultValue string, shouldAskAgain func(answer string) bool) (string, error) { + rl, err := readline.NewEx(config) + if err != nil { + return "", err + } + + rl.WriteStdin([]byte(defaultValue)) + for { + answer, err := rl.Readline() + if err != nil { + return "", err + } + answer = strings.TrimSpace(strings.ToLower(answer)) + if shouldAskAgain != nil && shouldAskAgain(answer) { + continue + } + return answer, err + } +} From 0d46d488814daa5b82850f5bd448240f59caea73 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Mon, 29 Jan 2024 15:37:21 -0300 Subject: [PATCH 075/401] fix naddr. --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 4e771cf..aff1f72 100644 --- a/README.md +++ b/README.md @@ -84,5 +84,4 @@ nak req -l 100 -k 1 -a 2edbcea694d164629854a52583458fd6d965b161e3c48b57d3aff0194 ## Contributing to this repository -Use NIP-34 to send your patches to `naddr1qqpkucttqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9nhwden5te0wfjkccte9ehx7um5wghxyctwvsq3vamnwvaz7tmjv4kxz7fwwpexjmtpdshxuet5qgsg04q5ypr6f4n65mv7e5hs05z50hy7vvgua8uc8szwtp262cfwn6srqsqqqauedy5x7y`. - +Use NIP-34 to send your patches to `naddr1qqpkucttqy28wumn8ghj7un9d3shjtnwdaehgu3wvfnsz9nhwden5te0wfjkccte9ehx7um5wghxyctwvsq3gamnwvaz7tmjv4kxz7fwv3sk6atn9e5k7q3q80cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsxpqqqpmej2wctpn`. From bda18e035af1c97ed0186b005b8c8f62fa31e798 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Fri, 2 Feb 2024 14:11:58 -0300 Subject: [PATCH 076/401] fix reading hex secret key from input. --- helpers.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/helpers.go b/helpers.go index ddd0bd1..0534f02 100644 --- a/helpers.go +++ b/helpers.go @@ -147,12 +147,12 @@ func gatherSecretKeyFromArguments(c *cli.Context) (string, error) { if err != nil { return "", fmt.Errorf("failed to decrypt: %w", err) } + } else if bsec, err := hex.DecodeString(strings.Repeat("0", 64-len(sec)) + sec); err == nil { + sec = hex.EncodeToString(bsec) } else if prefix, hexvalue, err := nip19.Decode(sec); err != nil { return "", fmt.Errorf("invalid nsec: %w", err) } else if prefix == "nsec" { sec = hexvalue.(string) - } else if bsec, err := hex.DecodeString(strings.Repeat("0", 64-len(sec)) + sec); err == nil { - sec = hex.EncodeToString(bsec) } if ok := nostr.IsValid32ByteHex(sec); !ok { From 0b9e861f9003d041a8c9d7e43394686fc79f42d3 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Fri, 2 Feb 2024 14:13:11 -0300 Subject: [PATCH 077/401] fix: print prompt to stderr. --- helpers.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/helpers.go b/helpers.go index 0534f02..9353fa7 100644 --- a/helpers.go +++ b/helpers.go @@ -182,6 +182,7 @@ func promptDecrypt(ncryptsec1 string) (string, error) { func ask(msg string, defaultValue string, shouldAskAgain func(answer string) bool) (string, error) { return _ask(&readline.Config{ + Stdout: os.Stderr, Prompt: color.YellowString(msg), InterruptPrompt: "^C", DisableAutoSaveHistory: true, @@ -190,6 +191,7 @@ func ask(msg string, defaultValue string, shouldAskAgain func(answer string) boo func askPassword(msg string, shouldAskAgain func(answer string) bool) (string, error) { config := &readline.Config{ + Stdout: os.Stderr, Prompt: color.YellowString(msg), InterruptPrompt: "^C", DisableAutoSaveHistory: true, From 01e1f52a70be321365e6ef3d6e177516130c1f5a Mon Sep 17 00:00:00 2001 From: mattn Date: Sat, 3 Feb 2024 22:03:32 +0900 Subject: [PATCH 078/401] fix panic (#12) * c.Args().Len() - 1 might be negative value * fix getStdinLinesOrFirstArgument * fix getStdinLinesOrFirstArgument --- decode.go | 2 +- encode.go | 10 +++++----- fetch.go | 2 +- helpers.go | 7 +++---- key.go | 32 +++++++++++++++++++++++++------- 5 files changed, 35 insertions(+), 18 deletions(-) diff --git a/decode.go b/decode.go index 371ddc2..4a9dced 100644 --- a/decode.go +++ b/decode.go @@ -33,7 +33,7 @@ var decode = &cli.Command{ }, ArgsUsage: "", Action: func(c *cli.Context) error { - for input := range getStdinLinesOrFirstArgument(c) { + for input := range getStdinLinesOrFirstArgument(c.Args().First()) { if strings.HasPrefix(input, "nostr:") { input = input[6:] } diff --git a/encode.go b/encode.go index 107c47d..3362595 100644 --- a/encode.go +++ b/encode.go @@ -29,7 +29,7 @@ var encode = &cli.Command{ Name: "npub", Usage: "encode a hex public key into bech32 'npub' format", Action: func(c *cli.Context) error { - for target := range getStdinLinesOrFirstArgument(c) { + for target := range getStdinLinesOrFirstArgument(c.Args().First()) { if ok := nostr.IsValidPublicKey(target); !ok { lineProcessingError(c, "invalid public key: %s", target) continue @@ -50,7 +50,7 @@ var encode = &cli.Command{ Name: "nsec", Usage: "encode a hex private key into bech32 'nsec' format", Action: func(c *cli.Context) error { - for target := range getStdinLinesOrFirstArgument(c) { + for target := range getStdinLinesOrFirstArgument(c.Args().First()) { if ok := nostr.IsValid32ByteHex(target); !ok { lineProcessingError(c, "invalid private key: %s", target) continue @@ -78,7 +78,7 @@ var encode = &cli.Command{ }, }, Action: func(c *cli.Context) error { - for target := range getStdinLinesOrFirstArgument(c) { + for target := range getStdinLinesOrFirstArgument(c.Args().First()) { if ok := nostr.IsValid32ByteHex(target); !ok { lineProcessingError(c, "invalid public key: %s", target) continue @@ -115,7 +115,7 @@ var encode = &cli.Command{ }, }, Action: func(c *cli.Context) error { - for target := range getStdinLinesOrFirstArgument(c) { + for target := range getStdinLinesOrFirstArgument(c.Args().First()) { if ok := nostr.IsValid32ByteHex(target); !ok { lineProcessingError(c, "invalid event id: %s", target) continue @@ -212,7 +212,7 @@ var encode = &cli.Command{ Name: "note", Usage: "generate note1 event codes (not recommended)", Action: func(c *cli.Context) error { - for target := range getStdinLinesOrFirstArgument(c) { + for target := range getStdinLinesOrFirstArgument(c.Args().First()) { if ok := nostr.IsValid32ByteHex(target); !ok { lineProcessingError(c, "invalid event id: %s", target) continue diff --git a/fetch.go b/fetch.go index 7f3d966..724d8ff 100644 --- a/fetch.go +++ b/fetch.go @@ -31,7 +31,7 @@ var fetch = &cli.Command{ }) }() - for code := range getStdinLinesOrFirstArgument(c) { + for code := range getStdinLinesOrFirstArgument(c.Args().First()) { filter := nostr.Filter{} prefix, value, err := nip19.Decode(code) diff --git a/helpers.go b/helpers.go index 9353fa7..b3a6921 100644 --- a/helpers.go +++ b/helpers.go @@ -47,12 +47,11 @@ func getStdinLinesOrBlank() chan string { } } -func getStdinLinesOrFirstArgument(c *cli.Context) chan string { +func getStdinLinesOrFirstArgument(arg string) chan string { // try the first argument - target := c.Args().First() - if target != "" { + if arg != "" { single := make(chan string, 1) - single <- target + single <- arg close(single) return single } diff --git a/key.go b/key.go index d51ee0f..645fdbf 100644 --- a/key.go +++ b/key.go @@ -39,7 +39,7 @@ var public = &cli.Command{ Description: ``, ArgsUsage: "[secret]", Action: func(c *cli.Context) error { - for sec := range getSecretKeyFromStdinLinesOrFirstArgument(c) { + for sec := range getSecretKeyFromStdinLinesOrFirstArgument(c, c.Args().First()) { pubkey, err := nostr.GetPublicKey(sec) if err != nil { lineProcessingError(c, "failed to derive public key: %s", err) @@ -65,11 +65,20 @@ var encrypt = &cli.Command{ }, }, Action: func(c *cli.Context) error { - password := c.Args().Get(c.Args().Len() - 1) + var content string + var password string + switch c.Args().Len() { + case 1: + content = "" + password = c.Args().Get(0) + case 2: + content = c.Args().Get(0) + password = c.Args().Get(1) + } if password == "" { return fmt.Errorf("no password given") } - for sec := range getSecretKeyFromStdinLinesOrFirstArgument(c) { + for sec := range getSecretKeyFromStdinLinesOrFirstArgument(c, content) { ncryptsec, err := nip49.Encrypt(sec, password, uint8(c.Int("logn")), 0x02) if err != nil { lineProcessingError(c, "failed to encrypt: %s", err) @@ -87,11 +96,20 @@ var decrypt = &cli.Command{ Description: `uses the NIP-49 standard.`, ArgsUsage: " ", Action: func(c *cli.Context) error { - password := c.Args().Get(c.Args().Len() - 1) + var content string + var password string + switch c.Args().Len() { + case 1: + content = "" + password = c.Args().Get(0) + case 2: + content = c.Args().Get(0) + password = c.Args().Get(1) + } if password == "" { return fmt.Errorf("no password given") } - for ncryptsec := range getStdinLinesOrFirstArgument(c) { + for ncryptsec := range getStdinLinesOrFirstArgument(content) { sec, err := nip49.Decrypt(ncryptsec, password) if err != nil { lineProcessingError(c, "failed to decrypt: %s", err) @@ -104,10 +122,10 @@ var decrypt = &cli.Command{ }, } -func getSecretKeyFromStdinLinesOrFirstArgument(c *cli.Context) chan string { +func getSecretKeyFromStdinLinesOrFirstArgument(c *cli.Context, content string) chan string { ch := make(chan string) go func() { - for sec := range getStdinLinesOrFirstArgument(c) { + for sec := range getStdinLinesOrFirstArgument(content) { if sec == "" { continue } From b7a7e0504fa073a6ae4bd7b4ebb63132679e3c53 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 6 Feb 2024 00:58:26 -0300 Subject: [PATCH 079/401] --connect to use nip46 as a client to sign event and auth messages. --- bunker.go | 2 +- event.go | 32 ++++++++++++++++++++++++++------ go.mod | 2 +- go.sum | 4 ++-- helpers.go | 21 ++++++++++++++------- req.go | 24 +++++++++++++++++++++--- 6 files changed, 65 insertions(+), 20 deletions(-) diff --git a/bunker.go b/bunker.go index 26dd92e..b8e25bd 100644 --- a/bunker.go +++ b/bunker.go @@ -56,7 +56,7 @@ var bunker = &cli.Command{ } // gather the secret key - sec, err := gatherSecretKeyFromArguments(c) + sec, _, err := gatherSecretKeyOrBunkerFromArguments(c) if err != nil { return err } diff --git a/event.go b/event.go index a752ee8..eb8cd05 100644 --- a/event.go +++ b/event.go @@ -44,6 +44,10 @@ example: Name: "prompt-sec", Usage: "prompt the user to paste a hex or nsec with which to sign the event", }, + &cli.StringFlag{ + Name: "connect", + Usage: "sign event using NIP-46, expects a bunker://... URL", + }, &cli.BoolFlag{ Name: "envelope", Usage: "print the event enveloped in a [\"EVENT\", ...] message ready to be sent to a relay", @@ -124,8 +128,7 @@ example: } }() - // gather the secret key - sec, err := gatherSecretKeyFromArguments(c) + sec, bunker, err := gatherSecretKeyOrBunkerFromArguments(c) if err != nil { return err } @@ -215,7 +218,11 @@ example: } if evt.Sig == "" || mustRehashAndResign { - if err := evt.Sign(sec); err != nil { + if bunker != nil { + if err := bunker.SignEvent(c.Context, &evt); err != nil { + return fmt.Errorf("failed to sign with bunker: %w", err) + } + } else if err := evt.Sign(sec); err != nil { return fmt.Errorf("error signing with provided key: %w", err) } } @@ -252,11 +259,24 @@ example: } // error publishing - if strings.HasPrefix(err.Error(), "msg: auth-required:") && sec != "" && doAuth { + if strings.HasPrefix(err.Error(), "msg: auth-required:") && (sec != "" || bunker != nil) && doAuth { // if the relay is requesting auth and we can auth, let's do it - pk, _ := nostr.GetPublicKey(sec) + var pk string + if bunker != nil { + pk, err = bunker.GetPublicKey(c.Context) + if err != nil { + return fmt.Errorf("failed to get public key from bunker: %w", err) + } + } else { + pk, _ = nostr.GetPublicKey(sec) + } log("performing auth as %s... ", pk) - if err := relay.Auth(c.Context, func(evt *nostr.Event) error { return evt.Sign(sec) }); err == nil { + if err := relay.Auth(c.Context, func(evt *nostr.Event) error { + if bunker != nil { + return bunker.SignEvent(c.Context, evt) + } + return evt.Sign(sec) + }); err == nil { // try to publish again, but this time don't try to auth again doAuth = false goto publish diff --git a/go.mod b/go.mod index 4b72b35..cd4fe2d 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/fatih/color v1.16.0 github.com/mailru/easyjson v0.7.7 github.com/manifoldco/promptui v0.9.0 - github.com/nbd-wtf/go-nostr v0.28.2 + github.com/nbd-wtf/go-nostr v0.28.4 github.com/nbd-wtf/nostr-sdk v0.0.5 github.com/urfave/cli/v2 v2.25.7 golang.org/x/exp v0.0.0-20231006140011-7918f672742d diff --git a/go.sum b/go.sum index 29a9176..659e784 100644 --- a/go.sum +++ b/go.sum @@ -81,8 +81,8 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/nbd-wtf/go-nostr v0.28.2 h1:KhpGcs6KMLBqYExzKoqt7vP5Re2f8Kpy9SavYZa2PTI= -github.com/nbd-wtf/go-nostr v0.28.2/go.mod h1:l9NRRaHPN+QwkqrjNKhnfYjQ0+nKP1xZrVxePPGUs+A= +github.com/nbd-wtf/go-nostr v0.28.4 h1:chGBpdCQvM9aInf/vVDishY8GHapgqFc/RLl2WDnHQM= +github.com/nbd-wtf/go-nostr v0.28.4/go.mod h1:l9NRRaHPN+QwkqrjNKhnfYjQ0+nKP1xZrVxePPGUs+A= github.com/nbd-wtf/nostr-sdk v0.0.5 h1:rec+FcDizDVO0W25PX0lgYMXvP7zNNOgI3Fu9UCm4BY= github.com/nbd-wtf/nostr-sdk v0.0.5/go.mod h1:iJJsikesCGLNFZ9dLqhLPDzdt924EagUmdQxT3w2Lmk= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= diff --git a/helpers.go b/helpers.go index b3a6921..6a2ce76 100644 --- a/helpers.go +++ b/helpers.go @@ -13,6 +13,7 @@ import ( "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/nbd-wtf/go-nostr/nip49" "github.com/urfave/cli/v2" ) @@ -129,35 +130,41 @@ func exitIfLineProcessingError(c *cli.Context) { } } -func gatherSecretKeyFromArguments(c *cli.Context) (string, error) { +func gatherSecretKeyOrBunkerFromArguments(c *cli.Context) (string, *nip46.BunkerClient, error) { var err error + + if bunkerURL := c.String("connect"); bunkerURL != "" { + clientKey := nostr.GeneratePrivateKey() + bunker, err := nip46.ConnectBunker(c.Context, clientKey, bunkerURL, nil) + return "", bunker, err + } sec := c.String("sec") if c.Bool("prompt-sec") { if isPiped() { - return "", fmt.Errorf("can't prompt for a secret key when processing data from a pipe, try again without --prompt-sec") + return "", 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 "", fmt.Errorf("failed to get secret key: %w", err) + return "", nil, fmt.Errorf("failed to get secret key: %w", err) } } if strings.HasPrefix(sec, "ncryptsec1") { sec, err = promptDecrypt(sec) if err != nil { - return "", fmt.Errorf("failed to decrypt: %w", err) + return "", nil, fmt.Errorf("failed to decrypt: %w", err) } } else if bsec, err := hex.DecodeString(strings.Repeat("0", 64-len(sec)) + sec); err == nil { sec = hex.EncodeToString(bsec) } else if prefix, hexvalue, err := nip19.Decode(sec); err != nil { - return "", fmt.Errorf("invalid nsec: %w", err) + return "", nil, fmt.Errorf("invalid nsec: %w", err) } else if prefix == "nsec" { sec = hexvalue.(string) } if ok := nostr.IsValid32ByteHex(sec); !ok { - return "", fmt.Errorf("invalid secret key") + return "", nil, fmt.Errorf("invalid secret key") } - return sec, nil + return sec, nil, nil } func promptDecrypt(ncryptsec1 string) (string, error) { diff --git a/req.go b/req.go index d164d6a..5801b39 100644 --- a/req.go +++ b/req.go @@ -113,6 +113,10 @@ example: Name: "prompt-sec", Usage: "prompt the user to paste a hex or nsec with which to sign the AUTH challenge", }, + &cli.StringFlag{ + Name: "connect", + Usage: "sign AUTH using NIP-46, expects a bunker://... URL", + }, }, ArgsUsage: "[relay...]", Action: func(c *cli.Context) error { @@ -124,13 +128,27 @@ example: if !c.Bool("auth") { return fmt.Errorf("auth not authorized") } - sec, err := gatherSecretKeyFromArguments(c) + sec, bunker, err := gatherSecretKeyOrBunkerFromArguments(c) if err != nil { return err } - pk, _ := nostr.GetPublicKey(sec) + + var pk string + if bunker != nil { + pk, err = bunker.GetPublicKey(c.Context) + if err != nil { + return fmt.Errorf("failed to get public key from bunker: %w", err) + } + } else { + pk, _ = nostr.GetPublicKey(sec) + } log("performing auth as %s...\n", pk) - return evt.Sign(sec) + + if bunker != nil { + return bunker.SignEvent(c.Context, evt) + } else { + return evt.Sign(sec) + } })) if len(relays) == 0 { log("failed to connect to any of the given relays.\n") From 6626001dd2d691193c6055ceb8bbb4424adb2b99 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 6 Feb 2024 12:47:46 -0300 Subject: [PATCH 080/401] --connect-as to specify client pubkey when using --connect to bunker --- event.go | 5 +++++ helpers.go | 7 ++++++- req.go | 5 +++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/event.go b/event.go index eb8cd05..319ed38 100644 --- a/event.go +++ b/event.go @@ -48,6 +48,11 @@ example: Name: "connect", Usage: "sign event using NIP-46, expects a bunker://... URL", }, + &cli.StringFlag{ + Name: "connect-as", + Usage: "private key to when communicating with the bunker given on --connect", + DefaultText: "a random key", + }, &cli.BoolFlag{ Name: "envelope", Usage: "print the event enveloped in a [\"EVENT\", ...] message ready to be sent to a relay", diff --git a/helpers.go b/helpers.go index 6a2ce76..3b8f015 100644 --- a/helpers.go +++ b/helpers.go @@ -134,7 +134,12 @@ func gatherSecretKeyOrBunkerFromArguments(c *cli.Context) (string, *nip46.Bunker var err error if bunkerURL := c.String("connect"); bunkerURL != "" { - clientKey := nostr.GeneratePrivateKey() + clientKey := c.String("connect-as") + if clientKey != "" { + clientKey = strings.Repeat("0", 64-len(clientKey)) + clientKey + } else { + clientKey = nostr.GeneratePrivateKey() + } bunker, err := nip46.ConnectBunker(c.Context, clientKey, bunkerURL, nil) return "", bunker, err } diff --git a/req.go b/req.go index 5801b39..aceb9ff 100644 --- a/req.go +++ b/req.go @@ -117,6 +117,11 @@ example: Name: "connect", Usage: "sign AUTH using NIP-46, expects a bunker://... URL", }, + &cli.StringFlag{ + Name: "connect-as", + Usage: "private key to when communicating with the bunker given on --connect", + DefaultText: "a random key", + }, }, ArgsUsage: "[relay...]", Action: func(c *cli.Context) error { From e89823b10ea23de493a5c6893120e410279945c5 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 6 Feb 2024 12:47:58 -0300 Subject: [PATCH 081/401] ensure at least one blank line will be emitted when piped. --- helpers.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/helpers.go b/helpers.go index 3b8f015..9a7e27e 100644 --- a/helpers.go +++ b/helpers.go @@ -69,8 +69,13 @@ func writeStdinLinesOrNothing(ch chan string) (hasStdinLines bool) { go func() { scanner := bufio.NewScanner(os.Stdin) scanner.Buffer(make([]byte, 16*1024), 256*1024) + hasEmittedAtLeastOne := false for scanner.Scan() { ch <- strings.TrimSpace(scanner.Text()) + hasEmittedAtLeastOne = true + } + if !hasEmittedAtLeastOne { + ch <- "" } close(ch) }() From 347a82eaa924d191c7448e9e204a11eea5bcee80 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Mon, 12 Feb 2024 15:38:43 -0300 Subject: [PATCH 082/401] verify: accept event to be verified as json argument. --- verify.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/verify.go b/verify.go index f5f174b..6b820d3 100644 --- a/verify.go +++ b/verify.go @@ -15,7 +15,7 @@ var verify = &cli.Command{ it outputs nothing if the verification is successful.`, Action: func(c *cli.Context) error { - for stdinEvent := range getStdinLinesOrBlank() { + for stdinEvent := range getStdinLinesOrFirstArgument(c.Args().First()) { evt := nostr.Event{} if stdinEvent != "" { if err := json.Unmarshal([]byte(stdinEvent), &evt); err != nil { From 5dd5a7c6992a461937aae4e00d136297ea04dd17 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Mon, 12 Feb 2024 15:39:13 -0300 Subject: [PATCH 083/401] bunker: better colors and prompts. --- bunker.go | 56 ++++++++++++++++++++++++++++++------------------------ go.mod | 5 +++-- go.sum | 7 ++----- helpers.go | 3 --- 4 files changed, 36 insertions(+), 35 deletions(-) diff --git a/bunker.go b/bunker.go index b8e25bd..1a0d24a 100644 --- a/bunker.go +++ b/bunker.go @@ -6,7 +6,7 @@ import ( "net/url" "os" - "github.com/manifoldco/promptui" + "github.com/fatih/color" "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/nip19" "github.com/nbd-wtf/go-nostr/nip46" @@ -65,12 +65,12 @@ var bunker = &cli.Command{ return err } npub, _ := nip19.EncodePublicKey(pubkey) - log("listening at %s%v%s:\n %spubkey:%s %s\n %snpub:%s %s\n %sconnection code:%s %s\n %sbunker:%s %s\n\n", - BOLD_ON, relayURLs, BOLD_OFF, - BOLD_ON, BOLD_OFF, pubkey, - BOLD_ON, BOLD_OFF, npub, - BOLD_ON, BOLD_OFF, fmt.Sprintf("%s#secret?%s", npub, qs.Encode()), - BOLD_ON, BOLD_OFF, fmt.Sprintf("bunker://%s?%s", pubkey, qs.Encode()), + bold := color.New(color.Bold).Sprint + log("listening at %v:\n pubkey: %s \n npub: %s\n bunker: %s\n\n", + bold(relayURLs), + bold(pubkey), + bold(npub), + bold(fmt.Sprintf("bunker://%s?%s", pubkey, qs.Encode())), ) alwaysYes := c.Bool("yes") @@ -93,15 +93,19 @@ var bunker = &cli.Command{ } jreq, _ := json.MarshalIndent(req, " ", " ") - log("- got request from '%s': %s\n", ie.Event.PubKey, string(jreq)) + log("- got request from '%s': %s\n", color.New(color.Bold, color.FgBlue).Sprint(ie.Event.PubKey), string(jreq)) jresp, _ := json.MarshalIndent(resp, " ", " ") log("~ responding with %s\n", string(jresp)) if alwaysYes || harmless || askProceed(ie.Event.PubKey) { - if err := ie.Relay.Publish(c.Context, eventResponse); err == nil { - log("* sent response!\n") - } else { - log("* failed to send response: %s\n", err) + for _, relayURL := range relayURLs { + if relay, _ := pool.EnsureRelay(relayURL); relay != nil { + if err := relay.Publish(c.Context, eventResponse); err == nil { + log("* sent response through %s\n", relay.URL) + } else { + log("* failed to send response: %s\n", err) + } + } } } } @@ -117,21 +121,23 @@ func askProceed(source string) bool { return true } - prompt := promptui.Select{ - Label: "proceed?", - Items: []string{ - "no", - "yes", - "always from this source", - }, - } - n, _, _ := prompt.Run() - switch n { - case 0: + fmt.Fprintf(os.Stderr, "request from %s:\n", color.New(color.Bold, color.FgBlue).Sprint(source)) + res, err := ask(" proceed to fulfill this request? (yes/no/always from this) (y/n/a): ", "", + func(answer string) bool { + if answer != "y" && answer != "n" && answer != "a" { + return true + } + return false + }) + if err != nil { return false - case 1: + } + switch res { + case "n": + return false + case "y": return true - case 2: + case "a": allowedSources = append(allowedSources, source) return true } diff --git a/go.mod b/go.mod index cd4fe2d..f1b2328 100644 --- a/go.mod +++ b/go.mod @@ -8,8 +8,7 @@ require ( github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e github.com/fatih/color v1.16.0 github.com/mailru/easyjson v0.7.7 - github.com/manifoldco/promptui v0.9.0 - github.com/nbd-wtf/go-nostr v0.28.4 + github.com/nbd-wtf/go-nostr v0.28.6 github.com/nbd-wtf/nostr-sdk v0.0.5 github.com/urfave/cli/v2 v2.25.7 golang.org/x/exp v0.0.0-20231006140011-7918f672742d @@ -19,6 +18,8 @@ require ( github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect github.com/btcsuite/btcd/btcutil v1.1.3 // indirect github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2 // indirect + github.com/chzyer/logex v1.1.10 // indirect + github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect diff --git a/go.sum b/go.sum index 659e784..a23cf72 100644 --- a/go.sum +++ b/go.sum @@ -74,15 +74,13 @@ github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlT github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= -github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/nbd-wtf/go-nostr v0.28.4 h1:chGBpdCQvM9aInf/vVDishY8GHapgqFc/RLl2WDnHQM= -github.com/nbd-wtf/go-nostr v0.28.4/go.mod h1:l9NRRaHPN+QwkqrjNKhnfYjQ0+nKP1xZrVxePPGUs+A= +github.com/nbd-wtf/go-nostr v0.28.6 h1:iOyzk+6ReG0lvyRAar7w7omFmUk5mnXDyFYkJ+zEjiw= +github.com/nbd-wtf/go-nostr v0.28.6/go.mod h1:aFcp8NO3erHg+glzBfh4wpaMrV1/ahcUPAgITdptxwA= github.com/nbd-wtf/nostr-sdk v0.0.5 h1:rec+FcDizDVO0W25PX0lgYMXvP7zNNOgI3Fu9UCm4BY= github.com/nbd-wtf/nostr-sdk v0.0.5/go.mod h1:iJJsikesCGLNFZ9dLqhLPDzdt924EagUmdQxT3w2Lmk= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= @@ -129,7 +127,6 @@ golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/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-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/helpers.go b/helpers.go index 9a7e27e..858f6a4 100644 --- a/helpers.go +++ b/helpers.go @@ -20,9 +20,6 @@ import ( const ( LINE_PROCESSING_ERROR = iota - - BOLD_ON = "\033[1m" - BOLD_OFF = "\033[21m" ) var log = func(msg string, args ...any) { From e008e08105cd5e9ee791c8c5c24f03af9f2d484b Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Fri, 16 Feb 2024 11:08:48 -0300 Subject: [PATCH 084/401] bunker: send responses to relays concurrently. --- bunker.go | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/bunker.go b/bunker.go index 1a0d24a..2c53b20 100644 --- a/bunker.go +++ b/bunker.go @@ -5,6 +5,7 @@ import ( "fmt" "net/url" "os" + "sync" "github.com/fatih/color" "github.com/nbd-wtf/go-nostr" @@ -85,10 +86,12 @@ var bunker = &cli.Command{ }) signer := nip46.NewStaticKeySigner(sec) + handlerWg := sync.WaitGroup{} + printLock := sync.Mutex{} for ie := range events { req, resp, eventResponse, harmless, err := signer.HandleRequest(ie.Event) if err != nil { - log("< failed to handle request from %s: %s", ie.Event.PubKey, err.Error()) + log("< failed to handle request from %s: %s\n", ie.Event.PubKey, err.Error()) continue } @@ -98,15 +101,23 @@ var bunker = &cli.Command{ log("~ responding with %s\n", string(jresp)) if alwaysYes || harmless || askProceed(ie.Event.PubKey) { + handlerWg.Add(len(relayURLs)) for _, relayURL := range relayURLs { - if relay, _ := pool.EnsureRelay(relayURL); relay != nil { - if err := relay.Publish(c.Context, eventResponse); err == nil { - log("* sent response through %s\n", relay.URL) - } else { - log("* failed to send response: %s\n", err) + go func(relayURL string) { + if relay, _ := pool.EnsureRelay(relayURL); relay != nil { + err := relay.Publish(c.Context, eventResponse) + printLock.Lock() + if err == nil { + log("* sent response through %s\n", relay.URL) + } else { + log("* failed to send response: %s\n", err) + } + printLock.Unlock() + handlerWg.Done() } - } + }(relayURL) } + handlerWg.Wait() } } From c5f7926471226b9854a1292f89ca0265a651887e Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sat, 17 Feb 2024 17:56:57 -0300 Subject: [PATCH 085/401] bunker: repeat connection info every now and then. --- bunker.go | 43 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/bunker.go b/bunker.go index 2c53b20..2991f25 100644 --- a/bunker.go +++ b/bunker.go @@ -1,11 +1,13 @@ package main import ( + "context" "encoding/json" "fmt" "net/url" "os" "sync" + "time" "github.com/fatih/color" "github.com/nbd-wtf/go-nostr" @@ -66,13 +68,18 @@ var bunker = &cli.Command{ return err } npub, _ := nip19.EncodePublicKey(pubkey) + bunkerURI := fmt.Sprintf("bunker://%s?%s", pubkey, qs.Encode()) bold := color.New(color.Bold).Sprint - log("listening at %v:\n pubkey: %s \n npub: %s\n bunker: %s\n\n", - bold(relayURLs), - bold(pubkey), - bold(npub), - bold(fmt.Sprintf("bunker://%s?%s", pubkey, qs.Encode())), - ) + + printBunkerInfo := func() { + log("listening at %v:\n pubkey: %s \n npub: %s\n bunker: %s\n\n", + bold(relayURLs), + bold(pubkey), + bold(npub), + bold(bunkerURI), + ) + } + printBunkerInfo() alwaysYes := c.Bool("yes") @@ -88,7 +95,16 @@ var bunker = &cli.Command{ signer := nip46.NewStaticKeySigner(sec) handlerWg := sync.WaitGroup{} printLock := sync.Mutex{} + + // just a gimmick + var cancelPreviousBunkerInfoPrint context.CancelFunc + _, cancel := context.WithCancel(c.Context) + cancelPreviousBunkerInfoPrint = cancel + for ie := range events { + cancelPreviousBunkerInfoPrint() // this prevents us from printing a million bunker info blocks + + // handle the NIP-46 request event req, resp, eventResponse, harmless, err := signer.HandleRequest(ie.Event) if err != nil { log("< failed to handle request from %s: %s\n", ie.Event.PubKey, err.Error()) @@ -119,6 +135,21 @@ var bunker = &cli.Command{ } handlerWg.Wait() } + + // just after handling one request we trigger this + go func() { + ctx, cancel := context.WithCancel(c.Context) + defer cancel() + cancelPreviousBunkerInfoPrint = cancel + // the idea is that we will print the bunker URL again so it is easier to copy-paste by users + // but we will only do if the bunker is inactive for more than 5 minutes + select { + case <-ctx.Done(): + case <-time.After(time.Minute * 5): + fmt.Fprintf(os.Stderr, "\n") + printBunkerInfo() + } + }() } return nil From ffe2db7f964532965985ff2787dbbae83d42f957 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Wed, 21 Feb 2024 09:47:34 -0300 Subject: [PATCH 086/401] event: accept tags with a single item. --- event.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/event.go b/event.go index 319ed38..7e8b288 100644 --- a/event.go +++ b/event.go @@ -182,9 +182,8 @@ example: // tags may also contain extra elements separated with a ";" tagValues := strings.Split(tagValue, ";") tag = append(tag, tagValues...) - // ~ - tags = tags.AppendUnique(tag) } + tags = tags.AppendUnique(tag) } for _, etag := range c.StringSlice("e") { From 34c189af2868c9a5d7f95a26a4dd88c9660f112f Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sat, 2 Mar 2024 08:18:40 -0300 Subject: [PATCH 087/401] bunker improvements. --- bunker.go | 39 +++++++++++++++++++++------------------ go.mod | 3 ++- go.sum | 6 ++++-- helpers.go | 4 +++- 4 files changed, 30 insertions(+), 22 deletions(-) diff --git a/bunker.go b/bunker.go index 2991f25..2191701 100644 --- a/bunker.go +++ b/bunker.go @@ -101,11 +101,16 @@ var bunker = &cli.Command{ _, cancel := context.WithCancel(c.Context) cancelPreviousBunkerInfoPrint = cancel + // asking user for authorization + signer.AuthorizeRequest = func(harmless bool, from string) bool { + return alwaysYes || harmless || askProceed(from) + } + for ie := range events { cancelPreviousBunkerInfoPrint() // this prevents us from printing a million bunker info blocks // handle the NIP-46 request event - req, resp, eventResponse, harmless, err := signer.HandleRequest(ie.Event) + req, resp, eventResponse, err := signer.HandleRequest(ie.Event) if err != nil { log("< failed to handle request from %s: %s\n", ie.Event.PubKey, err.Error()) continue @@ -116,25 +121,23 @@ var bunker = &cli.Command{ jresp, _ := json.MarshalIndent(resp, " ", " ") log("~ responding with %s\n", string(jresp)) - if alwaysYes || harmless || askProceed(ie.Event.PubKey) { - handlerWg.Add(len(relayURLs)) - for _, relayURL := range relayURLs { - go func(relayURL string) { - if relay, _ := pool.EnsureRelay(relayURL); relay != nil { - err := relay.Publish(c.Context, eventResponse) - printLock.Lock() - if err == nil { - log("* sent response through %s\n", relay.URL) - } else { - log("* failed to send response: %s\n", err) - } - printLock.Unlock() - handlerWg.Done() + handlerWg.Add(len(relayURLs)) + for _, relayURL := range relayURLs { + go func(relayURL string) { + if relay, _ := pool.EnsureRelay(relayURL); relay != nil { + err := relay.Publish(c.Context, eventResponse) + printLock.Lock() + if err == nil { + log("* sent response through %s\n", relay.URL) + } else { + log("* failed to send response: %s\n", err) } - }(relayURL) - } - handlerWg.Wait() + printLock.Unlock() + handlerWg.Done() + } + }(relayURL) } + handlerWg.Wait() // just after handling one request we trigger this go func() { diff --git a/go.mod b/go.mod index f1b2328..286d2d0 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e github.com/fatih/color v1.16.0 github.com/mailru/easyjson v0.7.7 - github.com/nbd-wtf/go-nostr v0.28.6 + github.com/nbd-wtf/go-nostr v0.29.0 github.com/nbd-wtf/nostr-sdk v0.0.5 github.com/urfave/cli/v2 v2.25.7 golang.org/x/exp v0.0.0-20231006140011-7918f672742d @@ -38,4 +38,5 @@ require ( github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect golang.org/x/crypto v0.7.0 // indirect golang.org/x/sys v0.14.0 // indirect + golang.org/x/text v0.8.0 // indirect ) diff --git a/go.sum b/go.sum index a23cf72..ac963f2 100644 --- a/go.sum +++ b/go.sum @@ -79,8 +79,8 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/nbd-wtf/go-nostr v0.28.6 h1:iOyzk+6ReG0lvyRAar7w7omFmUk5mnXDyFYkJ+zEjiw= -github.com/nbd-wtf/go-nostr v0.28.6/go.mod h1:aFcp8NO3erHg+glzBfh4wpaMrV1/ahcUPAgITdptxwA= +github.com/nbd-wtf/go-nostr v0.29.0 h1:kpAZ9oQPFeB9aJPloCsGS+UCNDPyN0jkt7sxlxxZock= +github.com/nbd-wtf/go-nostr v0.29.0/go.mod h1:tiKJY6fWYSujbTQb201Y+IQ3l4szqYVt+fsTnsm7FCk= github.com/nbd-wtf/nostr-sdk v0.0.5 h1:rec+FcDizDVO0W25PX0lgYMXvP7zNNOgI3Fu9UCm4BY= github.com/nbd-wtf/nostr-sdk v0.0.5/go.mod h1:iJJsikesCGLNFZ9dLqhLPDzdt924EagUmdQxT3w2Lmk= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= @@ -142,6 +142,8 @@ golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/helpers.go b/helpers.go index 858f6a4..30b2bfd 100644 --- a/helpers.go +++ b/helpers.go @@ -142,7 +142,9 @@ func gatherSecretKeyOrBunkerFromArguments(c *cli.Context) (string, *nip46.Bunker } else { clientKey = nostr.GeneratePrivateKey() } - bunker, err := nip46.ConnectBunker(c.Context, clientKey, bunkerURL, nil) + bunker, err := nip46.ConnectBunker(c.Context, clientKey, bunkerURL, nil, func(s string) { + fmt.Fprintf(os.Stderr, color.CyanString("[nip46]: open the following URL: %s"), s) + }) return "", bunker, err } sec := c.String("sec") From 569d38a13736885c75fb74bfdf81dea3f6196ef3 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 19 Mar 2024 11:34:59 -0300 Subject: [PATCH 088/401] accept multiple arguments in many commands, add a lot of more tests. --- decode.go | 2 +- encode.go | 10 ++--- example_test.go | 116 +++++++++++++++++++++++++++++++++++++++++++++--- fetch.go | 2 +- helpers.go | 20 ++++++--- key.go | 10 ++--- relay.go | 28 ++++++------ verify.go | 2 +- 8 files changed, 152 insertions(+), 38 deletions(-) diff --git a/decode.go b/decode.go index 4a9dced..8dd752d 100644 --- a/decode.go +++ b/decode.go @@ -33,7 +33,7 @@ var decode = &cli.Command{ }, ArgsUsage: "", Action: func(c *cli.Context) error { - for input := range getStdinLinesOrFirstArgument(c.Args().First()) { + for input := range getStdinLinesOrArguments(c.Args()) { if strings.HasPrefix(input, "nostr:") { input = input[6:] } diff --git a/encode.go b/encode.go index 3362595..a4efbb8 100644 --- a/encode.go +++ b/encode.go @@ -29,7 +29,7 @@ var encode = &cli.Command{ Name: "npub", Usage: "encode a hex public key into bech32 'npub' format", Action: func(c *cli.Context) error { - for target := range getStdinLinesOrFirstArgument(c.Args().First()) { + for target := range getStdinLinesOrArguments(c.Args()) { if ok := nostr.IsValidPublicKey(target); !ok { lineProcessingError(c, "invalid public key: %s", target) continue @@ -50,7 +50,7 @@ var encode = &cli.Command{ Name: "nsec", Usage: "encode a hex private key into bech32 'nsec' format", Action: func(c *cli.Context) error { - for target := range getStdinLinesOrFirstArgument(c.Args().First()) { + for target := range getStdinLinesOrArguments(c.Args()) { if ok := nostr.IsValid32ByteHex(target); !ok { lineProcessingError(c, "invalid private key: %s", target) continue @@ -78,7 +78,7 @@ var encode = &cli.Command{ }, }, Action: func(c *cli.Context) error { - for target := range getStdinLinesOrFirstArgument(c.Args().First()) { + for target := range getStdinLinesOrArguments(c.Args()) { if ok := nostr.IsValid32ByteHex(target); !ok { lineProcessingError(c, "invalid public key: %s", target) continue @@ -115,7 +115,7 @@ var encode = &cli.Command{ }, }, Action: func(c *cli.Context) error { - for target := range getStdinLinesOrFirstArgument(c.Args().First()) { + for target := range getStdinLinesOrArguments(c.Args()) { if ok := nostr.IsValid32ByteHex(target); !ok { lineProcessingError(c, "invalid event id: %s", target) continue @@ -212,7 +212,7 @@ var encode = &cli.Command{ Name: "note", Usage: "generate note1 event codes (not recommended)", Action: func(c *cli.Context) error { - for target := range getStdinLinesOrFirstArgument(c.Args().First()) { + for target := range getStdinLinesOrArguments(c.Args()) { if ok := nostr.IsValid32ByteHex(target); !ok { lineProcessingError(c, "invalid event id: %s", target) continue diff --git a/example_test.go b/example_test.go index f1c4cd8..8e5dd77 100644 --- a/example_test.go +++ b/example_test.go @@ -1,31 +1,135 @@ package main +import "os" + func ExampleEventBasic() { app.Run([]string{"nak", "event", "--ts", "1699485669"}) // Output: // {"id":"36d88cf5fcc449f2390a424907023eda7a74278120eebab8d02797cd92e7e29c","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1699485669,"kind":1,"tags":[],"content":"hello from the nostr army knife","sig":"68e71a192e8abcf8582a222434ac823ecc50607450ebe8cc4c145eb047794cc382dc3f888ce879d2f404f5ba6085a47601360a0fa2dd4b50d317bd0c6197c2c2"} } +// (for some reason there can only be one test dealing with stdin in the suite otherwise it halts) +func ExampleEventParsingFromStdin() { + prevStdin := os.Stdin + defer func() { os.Stdin = prevStdin }() + r, w, _ := os.Pipe() + os.Stdin = r + w.WriteString("{\"content\":\"hello world\"}\n{\"content\":\"hello sun\"}\n") + app.Run([]string{"nak", "event", "-t", "t=spam", "--ts", "1699485669"}) + // Output: + // {"id":"bda134f9077c11973afe6aa5a1cc6f5bcea01c40d318b8f91dcb8e50507cfa52","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1699485669,"kind":1,"tags":[["t","spam"]],"content":"hello world","sig":"7552454bb8e7944230142634e3e34ac7468bad9b21ed6909da572c611018dff1d14d0792e98b5806f6330edc51e09efa6d0b66a9694dc34606c70f4e580e7493"} + // {"id":"879c36ec73acca288825b53585389581d3836e7f0fe4d46e5eba237ca56d6af5","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1699485669,"kind":1,"tags":[["t","spam"]],"content":"hello sun","sig":"6c7e6b13ebdf931d26acfdd00bec2ec1140ddaf8d1ed61453543a14e729a460fe36c40c488ccb194a0e1ab9511cb6c36741485f501bdb93c39ca4c51bc59cbd4"} +} + func ExampleEventComplex() { app.Run([]string{"nak", "event", "--ts", "1699485669", "-k", "11", "-c", "skjdbaskd", "--sec", "17", "-t", "t=spam", "-e", "36d88cf5fcc449f2390a424907023eda7a74278120eebab8d02797cd92e7e29c", "-t", "r=https://abc.def?name=foobar;nothing"}) // Output: // {"id":"19aba166dcf354bf5ef64f4afe69ada1eb851495001ee05e07d393ee8c8ea179","pubkey":"2fa2104d6b38d11b0230010559879124e42ab8dfeff5ff29dc9cdadd4ecacc3f","created_at":1699485669,"kind":11,"tags":[["t","spam"],["r","https://abc.def?name=foobar","nothing"],["e","36d88cf5fcc449f2390a424907023eda7a74278120eebab8d02797cd92e7e29c"]],"content":"skjdbaskd","sig":"cf452def4a68341c897c3fc96fa34dc6895a5b8cc266d4c041bcdf758ec992ec5adb8b0179e98552aaaf9450526a26d7e62e413b15b1c57e0cfc8db6b29215d7"} } +func ExampleEncode() { + app.Run([]string{"nak", "encode", "npub", "a6a67ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179822"}) + app.Run([]string{"nak", "encode", "nprofile", "-r", "wss://example.com", "a6a67ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179822"}) + app.Run([]string{"nak", "encode", "nprofile", "-r", "wss://example.com", "a6a67ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179822", "a5592173975ded9f836a9572ea8b11a7e16ceb66464d66d50b27163f7f039d2c"}) + // npub156n8a7wuhwk9tgrzjh8gwzc8q2dlekedec5djk0js9d3d7qhnq3qjpdq28 + // nprofile1qqs2dfn7l8wthtz45p3ftn58pvrs9xlumvkuu2xet8egzkcklqtesgspz9mhxue69uhk27rpd4cxcefwvdhk6fl5jug + // nprofile1qqs2dfn7l8wthtz45p3ftn58pvrs9xlumvkuu2xet8egzkcklqtesgspz9mhxue69uhk27rpd4cxcefwvdhk6fl5jug + // nprofile1qqs22kfpwwt4mmvlsd4f2uh23vg60ctvadnyvntx659jw93l0upe6tqpz9mhxue69uhk27rpd4cxcefwvdhk64h265a +} + +func ExampleDecode() { + app.Run([]string{"nak", "decode", "naddr1qqyrgcmyxe3kvefhqyxhwumn8ghj7mn0wvhxcmmvqgs9kqvr4dkruv3t7n2pc6e6a7v9v2s5fprmwjv4gde8c4fe5y29v0srqsqqql9ngrt6tu", "nevent1qyd8wumn8ghj7urewfsk66ty9enxjct5dfskvtnrdakj7qgmwaehxw309aex2mrp0yh8wetnw3jhymnzw33jucm0d5hszxthwden5te0wfjkccte9eekummjwsh8xmmrd9skctcpzamhxue69uhkzarvv9ejumn0wd68ytnvv9hxgtcqyqllp5v5j0nxr74fptqxkhvfv0h3uj870qpk3ln8a58agyxl3fka296ewr8"}) + // Output: + // { + // "pubkey": "5b0183ab6c3e322bf4d41c6b3aef98562a144847b7499543727c5539a114563e", + // "kind": 31923, + // "identifier": "4cd6cfe7", + // "relays": [ + // "wss://nos.lol" + // ] + // } + // { + // "id": "3ff0d19493e661faa90ac06b5d8963ef1e48fe780368fe67ed0fd410df8a6dd5", + // "relays": [ + // "wss://pyramid.fiatjaf.com/", + // "wss://relay.westernbtc.com/", + // "wss://relay.snort.social/", + // "wss://atlas.nostr.land/" + // ] + // } +} + func ExampleReq() { app.Run([]string{"nak", "req", "-k", "1", "-l", "18", "-a", "2fa2104d6b38d11b0230010559879124e42ab8dfeff5ff29dc9cdadd4ecacc3f", "-e", "aec4de6d051a7c2b6ca2d087903d42051a31e07fb742f1240970084822de10a6"}) // Output: // ["REQ","nak",{"kinds":[1],"authors":["2fa2104d6b38d11b0230010559879124e42ab8dfeff5ff29dc9cdadd4ecacc3f"],"limit":18,"#e":["aec4de6d051a7c2b6ca2d087903d42051a31e07fb742f1240970084822de10a6"]}] } -func ExampleEncodeNpub() { - app.Run([]string{"nak", "encode", "npub", "a6a67ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179822"}) +func ExampleReqIdFromRelay() { + app.Run([]string{"nak", "req", "-i", "3ff0d19493e661faa90ac06b5d8963ef1e48fe780368fe67ed0fd410df8a6dd5", "wss://nostr.wine"}) // Output: - // npub156n8a7wuhwk9tgrzjh8gwzc8q2dlekedec5djk0js9d3d7qhnq3qjpdq28 + // {"id":"3ff0d19493e661faa90ac06b5d8963ef1e48fe780368fe67ed0fd410df8a6dd5","pubkey":"3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d","created_at":1710759386,"kind":1,"tags":[],"content":"Nostr was coopted by our the corporate overlords. It is now featured in https://www.iana.org/assignments/well-known-uris/well-known-uris.xhtml.","sig":"faaec167cca4de50b562b7702e8854e2023f0ccd5f36d1b95b6eac20d352206342d6987e9516d283068c768e94dbe8858e2990c3e05405e707fb6fb771ef92f9"} } -func ExampleEncodeNprofile() { - app.Run([]string{"nak", "encode", "nprofile", "-r", "wss://example.com", "a6a67ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179822"}) +func ExampleMultipleFetch() { + app.Run([]string{"nak", "fetch", "naddr1qqyrgcmyxe3kvefhqyxhwumn8ghj7mn0wvhxcmmvqgs9kqvr4dkruv3t7n2pc6e6a7v9v2s5fprmwjv4gde8c4fe5y29v0srqsqqql9ngrt6tu", "nevent1qyd8wumn8ghj7urewfsk66ty9enxjct5dfskvtnrdakj7qgmwaehxw309aex2mrp0yh8wetnw3jhymnzw33jucm0d5hszxthwden5te0wfjkccte9eekummjwsh8xmmrd9skctcpzamhxue69uhkzarvv9ejumn0wd68ytnvv9hxgtcqyqllp5v5j0nxr74fptqxkhvfv0h3uj870qpk3ln8a58agyxl3fka296ewr8"}) // Output: - // nprofile1qqs2dfn7l8wthtz45p3ftn58pvrs9xlumvkuu2xet8egzkcklqtesgspz9mhxue69uhk27rpd4cxcefwvdhk6fl5jug + // {"id":"9ae5014573fc75ced00b343868d2cd9343ebcbbae50591c6fa8ae1cd99568f05","pubkey":"5b0183ab6c3e322bf4d41c6b3aef98562a144847b7499543727c5539a114563e","created_at":1707764605,"kind":31923,"tags":[["d","4cd6cfe7"],["name","Nostr PHX Presents Culture Shock"],["description","Nostr PHX presents Culture Shock the first Value 4 Value Cultural Event in Downtown Phoenix. We will showcase the power of Nostr + Bitcoin / Lightning with a full day of education, food, drinks, conversation, vendors and best of all, a live convert which will stream globally for the world to zap. "],["start","1708185600"],["end","1708228800"],["start_tzid","America/Phoenix"],["p","5b0183ab6c3e322bf4d41c6b3aef98562a144847b7499543727c5539a114563e","","host"],["location","Hello Merch, 850 W Lincoln St, Phoenix, AZ 85007, USA","Hello Merch","850 W Lincoln St, Phoenix, AZ 85007, USA"],["address","Hello Merch, 850 W Lincoln St, Phoenix, AZ 85007, USA","Hello Merch","850 W Lincoln St, Phoenix, AZ 85007, USA"],["g","9tbq1rzn"],["image","https://flockstr.s3.amazonaws.com/event/15vSaiscDhVH1KBXhA0i8"],["about","Nostr PHX presents Culture Shock : the first Value 4 Value Cultural Event in Downtown Phoenix. We will showcase the power of Nostr + Bitcoin / Lightning with a full day of education, conversation, food and goods which will be capped off with a live concert streamed globally for the world to boost \u0026 zap. \n\nWe strive to source local vendors, local artists, local partnerships. Please reach out to us if you are interested in participating in this historic event. "],["calendar","31924:5b0183ab6c3e322bf4d41c6b3aef98562a144847b7499543727c5539a114563e:1f238c94"]],"content":"Nostr PHX presents Culture Shock : the first Value 4 Value Cultural Event in Downtown Phoenix. We will showcase the power of Nostr + Bitcoin / Lightning with a full day of education, conversation, food and goods which will be capped off with a live concert streamed globally for the world to boost \u0026 zap. \n\nWe strive to source local vendors, local artists, local partnerships. Please reach out to us if you are interested in participating in this historic event. ","sig":"f676629d1414d96b464644de6babde0c96bd21ef9b41ba69ad886a1d13a942b855b715b22ccf38bc07fead18d3bdeee82d9e3825cf6f003fb5ff1766d95c70a0"} + // {"id":"3ff0d19493e661faa90ac06b5d8963ef1e48fe780368fe67ed0fd410df8a6dd5","pubkey":"3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d","created_at":1710759386,"kind":1,"tags":[],"content":"Nostr was coopted by our the corporate overlords. It is now featured in https://www.iana.org/assignments/well-known-uris/well-known-uris.xhtml.","sig":"faaec167cca4de50b562b7702e8854e2023f0ccd5f36d1b95b6eac20d352206342d6987e9516d283068c768e94dbe8858e2990c3e05405e707fb6fb771ef92f9"} +} + +func ExampleKeyPublic() { + app.Run([]string{"nak", "key", "public", "3ff0d19493e661faa90ac06b5d8963ef1e48fe780368fe67ed0fd410df8a6dd5", "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"}) + // Output: + // 70f7120d065870513a6bddb61c8d400ad1e43449b1900ffdb5551e4c421375c8 + // 718d756f60cf5179ef35b39dc6db3ff58f04c0734f81f6d4410f0b047ddf9029 +} + +func ExampleKeyDecrypt() { + app.Run([]string{"nak", "key", "decrypt", "ncryptsec1qggfep0m5ythsegkmwfrhhx2zx5gazyhdygvlngcds4wsgdpzfy6nr0exy0pdk0ydwrqyhndt2trtwcgwwag0ja3aqclzptfxxqvprdyaz3qfrmazpecx2ff6dph5mfdjnh5sw8sgecul32eru6xet34", "banana"}) + // Output: + // nsec180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsgyumg0 +} + +func ExampleRelay() { + app.Run([]string{"nak", "relay", "relay.nos.social", "pyramid.fiatjaf.com"}) + // Output: + // { + // "name": "nos.social strfry relay", + // "description": "This is a strfry instance handled by nos.social", + // "pubkey": "89ef92b9ebe6dc1e4ea398f6477f227e95429627b0a33dc89b640e137b256be5", + // "contact": "https://nos.social", + // "supported_nips": [ + // 1, + // 2, + // 4, + // 9, + // 11, + // 12, + // 16, + // 20, + // 22, + // 28, + // 33, + // 40 + // ], + // "software": "git+https://github.com/hoytech/strfry.git", + // "version": "0.9.4", + // "icon": "" + // } + // { + // "name": "the fiatjaf pyramid", + // "description": "a relay just for the coolest of the coolest", + // "pubkey": "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d", + // "contact": "", + // "supported_nips": [], + // "software": "https://github.com/fiatjaf/khatru", + // "version": "n/a", + // "limitation": { + // "auth_required": false, + // "payment_required": false, + // "restricted_writes": true + // }, + // "icon": "https://clipart-library.com/images_k/pyramid-transparent/pyramid-transparent-19.png" + // } } diff --git a/fetch.go b/fetch.go index 724d8ff..4c279fc 100644 --- a/fetch.go +++ b/fetch.go @@ -31,7 +31,7 @@ var fetch = &cli.Command{ }) }() - for code := range getStdinLinesOrFirstArgument(c.Args().First()) { + for code := range getStdinLinesOrArguments(c.Args()) { filter := nostr.Filter{} prefix, value, err := nip19.Decode(code) diff --git a/helpers.go b/helpers.go index 30b2bfd..93f3858 100644 --- a/helpers.go +++ b/helpers.go @@ -45,13 +45,21 @@ func getStdinLinesOrBlank() chan string { } } -func getStdinLinesOrFirstArgument(arg string) chan string { +func getStdinLinesOrArguments(args cli.Args) chan string { + return getStdinLinesOrArgumentsFromSlice(args.Slice()) +} + +func getStdinLinesOrArgumentsFromSlice(args []string) chan string { // try the first argument - if arg != "" { - single := make(chan string, 1) - single <- arg - close(single) - return single + if len(args) > 0 { + argsCh := make(chan string, 1) + go func() { + for _, arg := range args { + argsCh <- arg + } + close(argsCh) + }() + return argsCh } // try the stdin diff --git a/key.go b/key.go index 645fdbf..ba2575a 100644 --- a/key.go +++ b/key.go @@ -39,7 +39,7 @@ var public = &cli.Command{ Description: ``, ArgsUsage: "[secret]", Action: func(c *cli.Context) error { - for sec := range getSecretKeyFromStdinLinesOrFirstArgument(c, c.Args().First()) { + for sec := range getSecretKeyFromStdinLinesOrSlice(c, c.Args().Slice()) { pubkey, err := nostr.GetPublicKey(sec) if err != nil { lineProcessingError(c, "failed to derive public key: %s", err) @@ -78,7 +78,7 @@ var encrypt = &cli.Command{ if password == "" { return fmt.Errorf("no password given") } - for sec := range getSecretKeyFromStdinLinesOrFirstArgument(c, content) { + for sec := range getSecretKeyFromStdinLinesOrSlice(c, []string{content}) { ncryptsec, err := nip49.Encrypt(sec, password, uint8(c.Int("logn")), 0x02) if err != nil { lineProcessingError(c, "failed to encrypt: %s", err) @@ -109,7 +109,7 @@ var decrypt = &cli.Command{ if password == "" { return fmt.Errorf("no password given") } - for ncryptsec := range getStdinLinesOrFirstArgument(content) { + for ncryptsec := range getStdinLinesOrArgumentsFromSlice([]string{content}) { sec, err := nip49.Decrypt(ncryptsec, password) if err != nil { lineProcessingError(c, "failed to decrypt: %s", err) @@ -122,10 +122,10 @@ var decrypt = &cli.Command{ }, } -func getSecretKeyFromStdinLinesOrFirstArgument(c *cli.Context, content string) chan string { +func getSecretKeyFromStdinLinesOrSlice(c *cli.Context, keys []string) chan string { ch := make(chan string) go func() { - for sec := range getStdinLinesOrFirstArgument(content) { + for sec := range getStdinLinesOrArgumentsFromSlice(keys) { if sec == "" { continue } diff --git a/relay.go b/relay.go index a3f7551..8174873 100644 --- a/relay.go +++ b/relay.go @@ -16,22 +16,24 @@ var relay = &cli.Command{ nak relay nostr.wine`, ArgsUsage: "", Action: func(c *cli.Context) error { - url := c.Args().First() - if url == "" { - return fmt.Errorf("specify the ") - } + for url := range getStdinLinesOrArguments(c.Args()) { + if url == "" { + return fmt.Errorf("specify the ") + } - if !strings.HasPrefix(url, "wss://") && !strings.HasPrefix(url, "ws://") { - url = "wss://" + url - } + if !strings.HasPrefix(url, "wss://") && !strings.HasPrefix(url, "ws://") { + url = "wss://" + url + } - info, err := nip11.Fetch(c.Context, url) - if err != nil { - return fmt.Errorf("failed to fetch '%s' information document: %w", url, err) - } + info, err := nip11.Fetch(c.Context, url) + if err != nil { + lineProcessingError(c, "failed to fetch '%s' information document: %w", url, err) + continue + } - pretty, _ := json.MarshalIndent(info, "", " ") - stdout(string(pretty)) + pretty, _ := json.MarshalIndent(info, "", " ") + stdout(string(pretty)) + } return nil }, } diff --git a/verify.go b/verify.go index 6b820d3..39da863 100644 --- a/verify.go +++ b/verify.go @@ -15,7 +15,7 @@ var verify = &cli.Command{ it outputs nothing if the verification is successful.`, Action: func(c *cli.Context) error { - for stdinEvent := range getStdinLinesOrFirstArgument(c.Args().First()) { + for stdinEvent := range getStdinLinesOrArguments(c.Args()) { evt := nostr.Event{} if stdinEvent != "" { if err := json.Unmarshal([]byte(stdinEvent), &evt); err != nil { From 8ddb9ce021452cef24a9712567f3cbf2a759f23f Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Fri, 29 Mar 2024 00:19:20 +0900 Subject: [PATCH 089/401] fix color output on Windows --- helpers.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/helpers.go b/helpers.go index 93f3858..d90056b 100644 --- a/helpers.go +++ b/helpers.go @@ -23,7 +23,7 @@ const ( ) var log = func(msg string, args ...any) { - fmt.Fprintf(os.Stderr, msg, args...) + fmt.Fprintf(color.Error, msg, args...) } var stdout = fmt.Println @@ -151,7 +151,7 @@ func gatherSecretKeyOrBunkerFromArguments(c *cli.Context) (string, *nip46.Bunker clientKey = nostr.GeneratePrivateKey() } bunker, err := nip46.ConnectBunker(c.Context, clientKey, bunkerURL, nil, func(s string) { - fmt.Fprintf(os.Stderr, color.CyanString("[nip46]: open the following URL: %s"), s) + fmt.Fprintf(color.Error, color.CyanString("[nip46]: open the following URL: %s"), s) }) return "", bunker, err } @@ -205,7 +205,7 @@ func promptDecrypt(ncryptsec1 string) (string, error) { func ask(msg string, defaultValue string, shouldAskAgain func(answer string) bool) (string, error) { return _ask(&readline.Config{ - Stdout: os.Stderr, + Stdout: color.Error, Prompt: color.YellowString(msg), InterruptPrompt: "^C", DisableAutoSaveHistory: true, @@ -214,7 +214,7 @@ func ask(msg string, defaultValue string, shouldAskAgain func(answer string) boo func askPassword(msg string, shouldAskAgain func(answer string) bool) (string, error) { config := &readline.Config{ - Stdout: os.Stderr, + Stdout: color.Error, Prompt: color.YellowString(msg), InterruptPrompt: "^C", DisableAutoSaveHistory: true, From c3ea9c15f6bca3b5c728078c6462767ea02abe5e Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Fri, 29 Mar 2024 08:16:15 -0300 Subject: [PATCH 090/401] LimitZero when -l 0 and when --stream --- go.mod | 2 +- req.go | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 286d2d0..2be400d 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e github.com/fatih/color v1.16.0 github.com/mailru/easyjson v0.7.7 - github.com/nbd-wtf/go-nostr v0.29.0 + github.com/nbd-wtf/go-nostr v0.30.0 github.com/nbd-wtf/nostr-sdk v0.0.5 github.com/urfave/cli/v2 v2.25.7 golang.org/x/exp v0.0.0-20231006140011-7918f672742d diff --git a/req.go b/req.go index aceb9ff..39d0def 100644 --- a/req.go +++ b/req.go @@ -246,6 +246,8 @@ example: } if limit := c.Int("limit"); limit != 0 { filter.Limit = limit + } else if c.IsSet("limit") || c.Bool("stream") { + filter.LimitZero = true } if len(relayUrls) > 0 { From f198a46c197f570d2543efcbb358e523a54e6ea1 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Mon, 29 Apr 2024 08:36:59 -0300 Subject: [PATCH 091/401] remove wss:// from relay urls in readme. --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index aff1f72..55c0d93 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ take a look at the help text that comes in it to learn all possibilities, but he ### make a nostr event with custom content and tags, sign it with a different key and publish it to two relays ```shell -~> nak event --sec 02 -c 'good morning' --tag t=gm wss://nostr-pub.wellorder.net wss://relay.damus.io +~> nak event --sec 02 -c 'good morning' --tag t=gm nostr-pub.wellorder.net relay.damus.io {"id":"e20978737ab7cd36eca300a65f11738176123f2e0c23054544b18fe493e2aa1a","pubkey":"c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5","created_at":1698632753,"kind":1,"tags":[["t","gm"]],"content":"good morning","sig":"5687c1a97066c349cb3bde0c0719fd1652a13403ba6aca7557b646307ee6718528cd86989db08bf6a7fd04bea0b0b87c1dd1b78c2d21b80b80eebab7f40b8916"} publishing to wss://nostr-pub.wellorder.net... success. publishing to wss://relay.damus.io... success. @@ -24,7 +24,7 @@ publishing to wss://relay.damus.io... success. ### query a bunch of relays for a tag with a limit of 2 for each, print their content ```shell -~> nak req -k 1 -t t=gm -l 2 wss://nostr.mom wss://nostr.wine wss://nostr-pub.wellorder.net | jq .content +~> nak req -k 1 -t t=gm -l 2 nostr.mom nostr.wine nostr-pub.wellorder.net | jq .content "#GM, you sovereign savage #freeple of the #nostrverse. Let's cause some #nostroversy. " "ITM slaves!\n#gm https://image.nostr.build/cbbcdf80bfc302a6678ecf9387c87d87deca3e0e288a12e262926c34feb3f6aa.jpg " "good morning" @@ -34,13 +34,13 @@ publishing to wss://relay.damus.io... success. ### decode a nip19 note1 code, add a relay hint, encode it back to nevent1 ```shell -~> nak decode note1ttnnrw78wy0hs5fa59yj03yvcu2r4y0xetg9vh7uf4em39n604vsyp37f2 | jq -r .id | nak encode nevent -r wss://nostr.zbd.gg +~> nak decode note1ttnnrw78wy0hs5fa59yj03yvcu2r4y0xetg9vh7uf4em39n604vsyp37f2 | jq -r .id | nak encode nevent -r nostr.zbd.gg nevent1qqs94ee3h0rhz8mc2y76zjf8cjxvw9p6j8nv45zktlwy6uacjea86kgpzfmhxue69uhkummnw3ezu7nzvshxwec8zw8h7 ~> nak decode nevent1qqs94ee3h0rhz8mc2y76zjf8cjxvw9p6j8nv45zktlwy6uacjea86kgpzfmhxue69uhkummnw3ezu7nzvshxwec8zw8h7 { "id": "5ae731bbc7711f78513da14927c48cc7143a91e6cad0565fdc4d73b8967a7d59", "relays": [ - "wss://nostr.zbd.gg" + "nostr.zbd.gg" ] } ``` @@ -61,7 +61,7 @@ nak fetch nevent1qqs2e3k48vtrkzjm8vvyzcmsmkf58unrxtq2k4h5yspay6vhcqm4wqcpz9mhxue ### republish an event from one relay to multiple others ```shell -~> nak req -i e20978737ab7cd36eca300a65f11738176123f2e0c23054544b18fe493e2aa1a wss://nostr.wine/ wss://nostr-pub.wellorder.net | nak event wss://nostr.wine wss://offchain.pub wss://public.relaying.io wss://eden.nostr.land wss://atlas.nostr.land wss://relayable.org +~> nak req -i e20978737ab7cd36eca300a65f11738176123f2e0c23054544b18fe493e2aa1a nostr.wine/ nostr-pub.wellorder.net | nak event nostr.wine offchain.pub public.relaying.io eden.nostr.land atlas.nostr.land relayable.org {"id":"e20978737ab7cd36eca300a65f11738176123f2e0c23054544b18fe493e2aa1a","pubkey":"c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5","created_at":1698632753,"kind":1,"tags":[["t","gm"]],"content":"good morning","sig":"5687c1a97066c349cb3bde0c0719fd1652a13403ba6aca7557b646307ee6718528cd86989db08bf6a7fd04bea0b0b87c1dd1b78c2d21b80b80eebab7f40b8916"} publishing to wss://nostr.wine... failed: msg: blocked: not an active paid member publishing to wss://offchain.pub... success. @@ -79,7 +79,7 @@ invalid .id, expected 05bd99d54cb835f427e0092c4275ee44c7ff51219eff417c19f70c9e2c ### fetch all quoted events by a given pubkey in their last 100 notes ```shell -nak req -l 100 -k 1 -a 2edbcea694d164629854a52583458fd6d965b161e3c48b57d3aff01940558884 wss://relay.damus.io | jq -r '.content | match("nostr:((note1|nevent1)[a-z0-9]+)";"g") | .captures[0].string' | nak decode | jq -cr '{ids: [.id]}' | nak req wss://relay.damus.io +nak req -l 100 -k 1 -a 2edbcea694d164629854a52583458fd6d965b161e3c48b57d3aff01940558884 relay.damus.io | jq -r '.content | match("nostr:((note1|nevent1)[a-z0-9]+)";"g") | .captures[0].string' | nak decode | jq -cr '{ids: [.id]}' | nak req relay.damus.io ``` ## Contributing to this repository From 81968f6c0cbffa5c64c160dc744bfcc19a561ffe Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 14 May 2024 15:23:08 -0300 Subject: [PATCH 092/401] `nak key combine` and `nak event --musig2` --- event.go | 40 +++++++ go.mod | 4 +- go.sum | 9 +- key.go | 43 +++++++- musig2.go | 324 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 412 insertions(+), 8 deletions(-) create mode 100644 musig2.go diff --git a/event.go b/event.go index 7e8b288..48c41d3 100644 --- a/event.go +++ b/event.go @@ -53,6 +53,31 @@ example: Usage: "private key to when communicating with the bunker given on --connect", DefaultText: "a random key", }, + // ~ these args are only for the convoluted musig2 signing process + // they will be generally copy-shared-pasted across some manual coordination method between participants + &cli.UintFlag{ + Name: "musig2", + Usage: "number of signers to use for musig2", + Value: 1, + DefaultText: "1 -- i.e. do not use musig2 at all", + }, + &cli.StringSliceFlag{ + Name: "musig2-pubkey", + Hidden: true, + }, + &cli.StringFlag{ + Name: "musig2-nonce-secret", + Hidden: true, + }, + &cli.StringSliceFlag{ + Name: "musig2-nonce", + Hidden: true, + }, + &cli.StringSliceFlag{ + Name: "musig2-partial", + Hidden: true, + }, + // ~~~ &cli.BoolFlag{ Name: "envelope", Usage: "print the event enveloped in a [\"EVENT\", ...] message ready to be sent to a relay", @@ -226,6 +251,21 @@ example: if err := bunker.SignEvent(c.Context, &evt); err != nil { return fmt.Errorf("failed to sign with bunker: %w", err) } + } else if numSigners := c.Uint("musig2"); numSigners > 1 && sec != "" { + pubkeys := c.StringSlice("musig2-pubkey") + secNonce := c.String("musig2-nonce-secret") + pubNonces := c.StringSlice("musig2-nonce") + partialSigs := c.StringSlice("musig2-partial") + signed, err := performMusig(c.Context, + sec, &evt, int(numSigners), pubkeys, pubNonces, secNonce, partialSigs) + if err != nil { + return fmt.Errorf("musig2 error: %w", err) + } + if !signed { + // we haven't finished signing the event, so the users still have to do more steps + // instructions for what to do should have been printed by the performMusig() function + return nil + } } else if err := evt.Sign(sec); err != nil { return fmt.Errorf("error signing with provided key: %w", err) } diff --git a/go.mod b/go.mod index 2be400d..6ec6656 100644 --- a/go.mod +++ b/go.mod @@ -5,17 +5,17 @@ go 1.21 toolchain go1.21.0 require ( + github.com/btcsuite/btcd/btcec/v2 v2.3.2 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e github.com/fatih/color v1.16.0 github.com/mailru/easyjson v0.7.7 - github.com/nbd-wtf/go-nostr v0.30.0 + github.com/nbd-wtf/go-nostr v0.30.2 github.com/nbd-wtf/nostr-sdk v0.0.5 github.com/urfave/cli/v2 v2.25.7 golang.org/x/exp v0.0.0-20231006140011-7918f672742d ) require ( - github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect github.com/btcsuite/btcd/btcutil v1.1.3 // indirect github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2 // indirect github.com/chzyer/logex v1.1.10 // indirect diff --git a/go.sum b/go.sum index ac963f2..be183eb 100644 --- a/go.sum +++ b/go.sum @@ -79,8 +79,8 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/nbd-wtf/go-nostr v0.29.0 h1:kpAZ9oQPFeB9aJPloCsGS+UCNDPyN0jkt7sxlxxZock= -github.com/nbd-wtf/go-nostr v0.29.0/go.mod h1:tiKJY6fWYSujbTQb201Y+IQ3l4szqYVt+fsTnsm7FCk= +github.com/nbd-wtf/go-nostr v0.30.2 h1:dG/2X52/XDg+7phZH+BClcvA5D+S6dXvxJKkBaySEzI= +github.com/nbd-wtf/go-nostr v0.30.2/go.mod h1:tiKJY6fWYSujbTQb201Y+IQ3l4szqYVt+fsTnsm7FCk= github.com/nbd-wtf/nostr-sdk v0.0.5 h1:rec+FcDizDVO0W25PX0lgYMXvP7zNNOgI3Fu9UCm4BY= github.com/nbd-wtf/nostr-sdk v0.0.5/go.mod h1:iJJsikesCGLNFZ9dLqhLPDzdt924EagUmdQxT3w2Lmk= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= @@ -92,6 +92,7 @@ github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5 github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/puzpuzpuz/xsync/v3 v3.0.2 h1:3yESHrRFYr6xzkz61LLkvNiPFXxJEAABanTQpKbAaew= github.com/puzpuzpuz/xsync/v3 v3.0.2/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= @@ -99,6 +100,8 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= github.com/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM= github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= @@ -160,3 +163,5 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/key.go b/key.go index ba2575a..2ede0c9 100644 --- a/key.go +++ b/key.go @@ -1,9 +1,12 @@ package main import ( + "encoding/hex" "fmt" "strings" + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/nip19" "github.com/nbd-wtf/go-nostr/nip49" @@ -19,6 +22,7 @@ var key = &cli.Command{ public, encrypt, decrypt, + combine, }, } @@ -39,7 +43,7 @@ var public = &cli.Command{ Description: ``, ArgsUsage: "[secret]", Action: func(c *cli.Context) error { - for sec := range getSecretKeyFromStdinLinesOrSlice(c, c.Args().Slice()) { + for sec := range getSecretKeysFromStdinLinesOrSlice(c, c.Args().Slice()) { pubkey, err := nostr.GetPublicKey(sec) if err != nil { lineProcessingError(c, "failed to derive public key: %s", err) @@ -78,7 +82,7 @@ var encrypt = &cli.Command{ if password == "" { return fmt.Errorf("no password given") } - for sec := range getSecretKeyFromStdinLinesOrSlice(c, []string{content}) { + for sec := range getSecretKeysFromStdinLinesOrSlice(c, []string{content}) { ncryptsec, err := nip49.Encrypt(sec, password, uint8(c.Int("logn")), 0x02) if err != nil { lineProcessingError(c, "failed to encrypt: %s", err) @@ -122,7 +126,38 @@ var decrypt = &cli.Command{ }, } -func getSecretKeyFromStdinLinesOrSlice(c *cli.Context, keys []string) chan string { +var combine = &cli.Command{ + Name: "combine", + Usage: "combines two or more pubkeys using musig2", + Description: `The public keys must have 33 bytes (66 characters hex), with the 02 or 03 prefix. It is common in Nostr to drop that first byte, so you'll have to derive the public keys again from the private keys in order to get it back.`, + ArgsUsage: "[pubkey...]", + Action: func(c *cli.Context) error { + keys := make([]*btcec.PublicKey, 0, 5) + for _, pub := range c.Args().Slice() { + keyb, err := hex.DecodeString(pub) + if err != nil { + return fmt.Errorf("error parsing key %s: %w", pub, err) + } + + pubk, err := btcec.ParsePubKey(keyb) + if err != nil { + return fmt.Errorf("error parsing key %s: %w", pub, err) + } + + keys = append(keys, pubk) + } + + agg, _, _, err := musig2.AggregateKeys(keys, true) + if err != nil { + return err + } + + fmt.Println(hex.EncodeToString(agg.FinalKey.X().Bytes())) + return nil + }, +} + +func getSecretKeysFromStdinLinesOrSlice(c *cli.Context, keys []string) chan string { ch := make(chan string) go func() { for sec := range getStdinLinesOrArgumentsFromSlice(keys) { @@ -138,7 +173,7 @@ func getSecretKeyFromStdinLinesOrSlice(c *cli.Context, keys []string) chan strin sec = data.(string) } if !nostr.IsValid32ByteHex(sec) { - lineProcessingError(c, "invalid hex secret key") + lineProcessingError(c, "invalid hex key") continue } ch <- sec diff --git a/musig2.go b/musig2.go new file mode 100644 index 0000000..13fc56c --- /dev/null +++ b/musig2.go @@ -0,0 +1,324 @@ +package main + +import ( + "bytes" + "context" + "encoding/base64" + "encoding/hex" + "fmt" + "os" + "strconv" + "strings" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" + "github.com/nbd-wtf/go-nostr" +) + +func performMusig( + ctx context.Context, + sec string, + evt *nostr.Event, + numSigners int, + keys []string, + nonces []string, + secNonce string, + 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) + + knownSigners := make([]*btcec.PublicKey, 0, numSigners) + includesUs := false + for _, hexpub := range keys { + bpub, err := hex.DecodeString(hexpub) + if err != nil { + return false, err + } + spub, err := btcec.ParsePubKey(bpub) + if err != nil { + return false, err + } + knownSigners = append(knownSigners, spub) + + if spub.IsEqual(pubk) { + includesUs = true + } + } + if !includesUs { + knownSigners = append(knownSigners, pubk) + } + + knownNonces := make([][66]byte, 0, numSigners) + for _, hexnonce := range nonces { + bnonce, err := hex.DecodeString(hexnonce) + if err != nil { + return false, err + } + if len(bnonce) != 66 { + return false, fmt.Errorf("nonce is not 66 bytes: %s", hexnonce) + } + var b66nonce [66]byte + copy(b66nonce[:], bnonce) + knownNonces = append(knownNonces, b66nonce) + } + + knownPartialSigs := make([]*musig2.PartialSignature, 0, numSigners) + for _, hexps := range partialSigs { + bps, err := hex.DecodeString(hexps) + if err != nil { + return false, err + } + var ps musig2.PartialSignature + if err := ps.Decode(bytes.NewBuffer(bps)); err != nil { + return false, fmt.Errorf("invalid partial signature %s: %w", hexps, err) + } + knownPartialSigs = append(knownPartialSigs, &ps) + } + + // create the context + var mctx *musig2.Context + if len(knownSigners) < numSigners { + // we don't know all the signers yet + mctx, err = musig2.NewContext(seck, true, + musig2.WithNumSigners(numSigners), + musig2.WithEarlyNonceGen(), + ) + if err != nil { + return false, fmt.Errorf("failed to create signing context with %d unknown signers: %w", + numSigners, err) + } + } else { + // we know all the signers + mctx, err = musig2.NewContext(seck, true, + musig2.WithKnownSigners(knownSigners), + ) + if err != nil { + return false, fmt.Errorf("failed to create signing context with %d known signers: %w", + len(knownSigners), err) + } + } + + // nonce generation phase -- for sharing + if len(knownSigners) < numSigners { + // if we don't have all the signers we just generate a nonce and yield it to the next people + nonce, err := mctx.EarlySessionNonce() + if err != nil { + return false, err + } + fmt.Fprintf(os.Stderr, "the following code should be saved secretly until the next step an included with --musig2-nonce-secret:\n") + fmt.Fprintf(os.Stderr, "%s\n\n", base64.StdEncoding.EncodeToString(nonce.SecNonce[:])) + + knownNonces = append(knownNonces, nonce.PubNonce) + printPublicCommandForNextPeer(evt, numSigners, knownSigners, knownNonces, nil, false) + return false, nil + } + + // if we got here we have all the pubkeys, so we can print the combined key + if comb, err := mctx.CombinedKey(); err != nil { + return false, fmt.Errorf("failed to combine keys (after %d signers): %w", len(knownSigners), err) + } else { + fmt.Fprintf(os.Stderr, "combined key: %x\n\n", comb.SerializeCompressed()) + } + + // we have all the signers, which means we must also have all the nonces + var session *musig2.Session + if len(keys) == numSigners-1 { + // if we were the last to include our key, that means we have to include our nonce here to + // i.e. we didn't input our own pub nonce in the parameters + session, err = mctx.NewSession() + if err != nil { + return false, fmt.Errorf("failed to create session as the last peer to include our key: %w", err) + } + } else { + // otherwise we have included our own nonce in the parameters (from copypasting) but must + // also include the secret nonce that wasn't shared with peers + if secNonce == "" { + return false, fmt.Errorf("missing --musig2-nonce-secret value") + } + secNonceB, err := base64.StdEncoding.DecodeString(secNonce) + if err != nil { + return false, fmt.Errorf("invalid --musig2-nonce-secret: %w", err) + } + var secNonce97 [97]byte + copy(secNonce97[:], secNonceB) + session, err = mctx.NewSession(musig2.WithPreGeneratedNonce(&musig2.Nonces{ + SecNonce: secNonce97, + PubNonce: secNonceToPubNonce(secNonce97), + })) + if err != nil { + return false, fmt.Errorf("failed to create signing session with secret nonce: %w", err) + } + } + + var noncesOk bool + for _, b66nonce := range knownNonces { + noncesOk, err = session.RegisterPubNonce(b66nonce) + if err != nil { + return false, fmt.Errorf("failed to register nonce: %w", err) + } + } + if !noncesOk { + return false, fmt.Errorf("we've registered all the nonces we had but at least one is missing") + } + + // 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) + fmt.Println("signing over", hex.EncodeToString(msg32[:])) + partialSig, err := session.Sign(msg32) // this will already include our sig in the bundle + if err != nil { + return false, fmt.Errorf("failed to produce partial signature: %w", err) + } + + if len(knownPartialSigs)+1 < len(knownSigners) { + // still missing some signatures + knownPartialSigs = append(knownPartialSigs, partialSig) // we include ours here just so it's printed + printPublicCommandForNextPeer(evt, numSigners, knownSigners, knownNonces, knownPartialSigs, true) + return false, nil + } else { + // we have all signatures + for _, ps := range knownPartialSigs { + _, err = session.CombineSig(ps) + if err != nil { + return false, fmt.Errorf("failed to combine partial signature: %w", err) + } + } + } + + // we have the signature + evt.Sig = hex.EncodeToString(session.FinalSig().Serialize()) + + return true, nil +} + +func printPublicCommandForNextPeer( + evt *nostr.Event, + numSigners int, + knownSigners []*btcec.PublicKey, + knownNonces [][66]byte, + knownPartialSigs []*musig2.PartialSignature, + includeNonceSecret bool, +) { + maybeNonceSecret := "" + if includeNonceSecret { + maybeNonceSecret = " --musig2-nonce-secret ''" + } + + fmt.Fprintf(os.Stderr, "the next signer and they should call this on their side:\nnak event --sec --musig2 %d %s%s%s%s%s", + numSigners, + eventToCliArgs(evt), + signersToCliArgs(knownSigners), + noncesToCliArgs(knownNonces), + partialSigsToCliArgs(knownPartialSigs), + maybeNonceSecret, + ) +} + +func eventToCliArgs(evt *nostr.Event) string { + b := strings.Builder{} + b.Grow(100) + + b.WriteString("-k ") + b.WriteString(strconv.Itoa(evt.Kind)) + + b.WriteString(" -ts ") + b.WriteString(strconv.FormatInt(int64(evt.CreatedAt), 10)) + + b.WriteString(" -c '") + b.WriteString(evt.Content) + b.WriteString("'") + + for _, tag := range evt.Tags { + b.WriteString(" -t '") + b.WriteString(tag.Key()) + if len(tag) > 1 { + b.WriteString("=") + b.WriteString(tag[1]) + if len(tag) > 2 { + for _, item := range tag[2:] { + b.WriteString(",") + b.WriteString(item) + } + } + } + b.WriteString("'") + } + + return b.String() +} + +func signersToCliArgs(knownSigners []*btcec.PublicKey) string { + b := strings.Builder{} + b.Grow(len(knownSigners) * (17 + 66)) + + for _, signerPub := range knownSigners { + b.WriteString(" --musig2-pubkey ") + b.WriteString(hex.EncodeToString(signerPub.SerializeCompressed())) + } + + return b.String() +} + +func noncesToCliArgs(knownNonces [][66]byte) string { + b := strings.Builder{} + b.Grow(len(knownNonces) * (16 + 132)) + + for _, nonce := range knownNonces { + b.WriteString(" --musig2-nonce ") + b.WriteString(hex.EncodeToString(nonce[:])) + } + + return b.String() +} + +func partialSigsToCliArgs(knownPartialSigs []*musig2.PartialSignature) string { + b := strings.Builder{} + b.Grow(len(knownPartialSigs) * (18 + 64)) + + for _, partialSig := range knownPartialSigs { + b.WriteString(" --musig2-partial ") + w := &bytes.Buffer{} + partialSig.Encode(w) + b.Write([]byte(hex.EncodeToString(w.Bytes()))) + } + + return b.String() +} + +// this function is copied from btcec because it's not exported for some reason +func secNonceToPubNonce(secNonce [musig2.SecNonceSize]byte) [musig2.PubNonceSize]byte { + var k1Mod, k2Mod btcec.ModNScalar + k1Mod.SetByteSlice(secNonce[:btcec.PrivKeyBytesLen]) + k2Mod.SetByteSlice(secNonce[btcec.PrivKeyBytesLen:]) + + var r1, r2 btcec.JacobianPoint + btcec.ScalarBaseMultNonConst(&k1Mod, &r1) + btcec.ScalarBaseMultNonConst(&k2Mod, &r2) + + // Next, we'll convert the key in jacobian format to a normal public + // key expressed in affine coordinates. + r1.ToAffine() + r2.ToAffine() + r1Pub := btcec.NewPublicKey(&r1.X, &r1.Y) + r2Pub := btcec.NewPublicKey(&r2.X, &r2.Y) + + var pubNonce [musig2.PubNonceSize]byte + + // The public nonces are serialized as: R1 || R2, where both keys are + // serialized in compressed format. + copy(pubNonce[:], r1Pub.SerializeCompressed()) + copy( + pubNonce[btcec.PubKeyBytesLenCompressed:], + r2Pub.SerializeCompressed(), + ) + + return pubNonce +} From 84bde7dacd4bcd5e57fc506e34db00fd12faa298 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 14 May 2024 23:41:12 -0300 Subject: [PATCH 093/401] musig2 works now. --- go.mod | 4 ++-- go.sum | 4 ++++ key.go | 2 +- musig2.go | 11 ++++++++--- 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 6ec6656..66c89dc 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.21 toolchain go1.21.0 require ( - github.com/btcsuite/btcd/btcec/v2 v2.3.2 + github.com/btcsuite/btcd/btcec/v2 v2.3.3 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e github.com/fatih/color v1.16.0 github.com/mailru/easyjson v0.7.7 @@ -22,7 +22,7 @@ require ( github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect github.com/fiatjaf/eventstore v0.2.16 // indirect github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/pool v0.2.1 // indirect diff --git a/go.sum b/go.sum index be183eb..c2d4069 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,8 @@ github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v5 github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= github.com/btcsuite/btcd/btcec/v2 v2.3.2 h1:5n0X6hX0Zk+6omWcihdYvdAlGf2DfasC0GMf7DClJ3U= github.com/btcsuite/btcd/btcec/v2 v2.3.2/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= +github.com/btcsuite/btcd/btcec/v2 v2.3.3 h1:6+iXlDKE8RMtKsvK0gshlXIuPbyWM/h84Ensb7o3sC0= +github.com/btcsuite/btcd/btcec/v2 v2.3.3/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A= github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE= github.com/btcsuite/btcd/btcutil v1.1.3 h1:xfbtw8lwpp0G6NwSHb+UE67ryTFHJAiNuipusjXSohQ= @@ -41,6 +43,8 @@ github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPc github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= diff --git a/key.go b/key.go index 2ede0c9..7e0cb4b 100644 --- a/key.go +++ b/key.go @@ -152,7 +152,7 @@ var combine = &cli.Command{ return err } - fmt.Println(hex.EncodeToString(agg.FinalKey.X().Bytes())) + fmt.Println(hex.EncodeToString(agg.FinalKey.SerializeCompressed())) return nil }, } diff --git a/musig2.go b/musig2.go index 13fc56c..3ad902a 100644 --- a/musig2.go +++ b/musig2.go @@ -134,6 +134,7 @@ func performMusig( if err != nil { return false, fmt.Errorf("failed to create session as the last peer to include our key: %w", err) } + knownNonces = append(knownNonces, session.PublicNonce()) } else { // otherwise we have included our own nonce in the parameters (from copypasting) but must // also include the secret nonce that wasn't shared with peers @@ -157,13 +158,18 @@ func performMusig( var noncesOk bool for _, b66nonce := range knownNonces { + if b66nonce == session.PublicNonce() { + // don't add our own nonce + continue + } + noncesOk, err = session.RegisterPubNonce(b66nonce) if err != nil { return false, fmt.Errorf("failed to register nonce: %w", err) } } if !noncesOk { - return false, fmt.Errorf("we've registered all the nonces we had but at least one is missing") + return false, fmt.Errorf("we've registered all the nonces we had but at least one is missing, this shouldn't happen") } // signing phase @@ -172,7 +178,6 @@ func performMusig( hash, _ := hex.DecodeString(id) var msg32 [32]byte copy(msg32[:], hash) - fmt.Println("signing over", hex.EncodeToString(msg32[:])) partialSig, err := session.Sign(msg32) // this will already include our sig in the bundle if err != nil { return false, fmt.Errorf("failed to produce partial signature: %w", err) @@ -212,7 +217,7 @@ func printPublicCommandForNextPeer( maybeNonceSecret = " --musig2-nonce-secret ''" } - fmt.Fprintf(os.Stderr, "the next signer and they should call this on their side:\nnak event --sec --musig2 %d %s%s%s%s%s", + fmt.Fprintf(os.Stderr, "the next signer and they should call this on their side:\nnak event --sec --musig2 %d %s%s%s%s%s\n", numSigners, eventToCliArgs(evt), signersToCliArgs(knownSigners), From 71dfe583ede4f05098fecc73ea87bae329a210fa Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 14 May 2024 23:52:56 -0300 Subject: [PATCH 094/401] rename flags from --musig2-... to --musig-..., add id to event and other small tweaks. --- event.go | 22 +++++++++++----------- musig2.go | 24 +++++++++++++----------- 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/event.go b/event.go index 48c41d3..fa7e639 100644 --- a/event.go +++ b/event.go @@ -56,25 +56,25 @@ example: // ~ these args are only for the convoluted musig2 signing process // they will be generally copy-shared-pasted across some manual coordination method between participants &cli.UintFlag{ - Name: "musig2", + Name: "musig", Usage: "number of signers to use for musig2", Value: 1, DefaultText: "1 -- i.e. do not use musig2 at all", }, &cli.StringSliceFlag{ - Name: "musig2-pubkey", + Name: "musig-pubkey", Hidden: true, }, &cli.StringFlag{ - Name: "musig2-nonce-secret", + Name: "musig-nonce-secret", Hidden: true, }, &cli.StringSliceFlag{ - Name: "musig2-nonce", + Name: "musig-nonce", Hidden: true, }, &cli.StringSliceFlag{ - Name: "musig2-partial", + Name: "musig-partial", Hidden: true, }, // ~~~ @@ -251,15 +251,15 @@ example: if err := bunker.SignEvent(c.Context, &evt); err != nil { return fmt.Errorf("failed to sign with bunker: %w", err) } - } else if numSigners := c.Uint("musig2"); numSigners > 1 && sec != "" { - pubkeys := c.StringSlice("musig2-pubkey") - secNonce := c.String("musig2-nonce-secret") - pubNonces := c.StringSlice("musig2-nonce") - partialSigs := c.StringSlice("musig2-partial") + } else if numSigners := c.Uint("musig"); numSigners > 1 && sec != "" { + pubkeys := c.StringSlice("musig-pubkey") + secNonce := c.String("musig-nonce-secret") + pubNonces := c.StringSlice("musig-nonce") + partialSigs := c.StringSlice("musig-partial") signed, err := performMusig(c.Context, sec, &evt, int(numSigners), pubkeys, pubNonces, secNonce, partialSigs) if err != nil { - return fmt.Errorf("musig2 error: %w", err) + return fmt.Errorf("musig error: %w", err) } if !signed { // we haven't finished signing the event, so the users still have to do more steps diff --git a/musig2.go b/musig2.go index 3ad902a..68b96b8 100644 --- a/musig2.go +++ b/musig2.go @@ -110,7 +110,7 @@ func performMusig( if err != nil { return false, err } - fmt.Fprintf(os.Stderr, "the following code should be saved secretly until the next step an included with --musig2-nonce-secret:\n") + fmt.Fprintf(os.Stderr, "the following code should be saved secretly until the next step an included with --musig-nonce-secret:\n") fmt.Fprintf(os.Stderr, "%s\n\n", base64.StdEncoding.EncodeToString(nonce.SecNonce[:])) knownNonces = append(knownNonces, nonce.PubNonce) @@ -122,6 +122,8 @@ 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.ID = evt.GetID() fmt.Fprintf(os.Stderr, "combined key: %x\n\n", comb.SerializeCompressed()) } @@ -139,11 +141,11 @@ func performMusig( // otherwise we have included our own nonce in the parameters (from copypasting) but must // also include the secret nonce that wasn't shared with peers if secNonce == "" { - return false, fmt.Errorf("missing --musig2-nonce-secret value") + return false, fmt.Errorf("missing --musig-nonce-secret value") } secNonceB, err := base64.StdEncoding.DecodeString(secNonce) if err != nil { - return false, fmt.Errorf("invalid --musig2-nonce-secret: %w", err) + return false, fmt.Errorf("invalid --musig-nonce-secret: %w", err) } var secNonce97 [97]byte copy(secNonce97[:], secNonceB) @@ -214,10 +216,10 @@ func printPublicCommandForNextPeer( ) { maybeNonceSecret := "" if includeNonceSecret { - maybeNonceSecret = " --musig2-nonce-secret ''" + maybeNonceSecret = " --musig-nonce-secret ''" } - fmt.Fprintf(os.Stderr, "the next signer and they should call this on their side:\nnak event --sec --musig2 %d %s%s%s%s%s\n", + fmt.Fprintf(os.Stderr, "the next signer and they should call this on their side:\nnak event --sec --musig %d %s%s%s%s%s\n", numSigners, eventToCliArgs(evt), signersToCliArgs(knownSigners), @@ -262,10 +264,10 @@ func eventToCliArgs(evt *nostr.Event) string { func signersToCliArgs(knownSigners []*btcec.PublicKey) string { b := strings.Builder{} - b.Grow(len(knownSigners) * (17 + 66)) + b.Grow(len(knownSigners) * (16 + 66)) for _, signerPub := range knownSigners { - b.WriteString(" --musig2-pubkey ") + b.WriteString(" --musig-pubkey ") b.WriteString(hex.EncodeToString(signerPub.SerializeCompressed())) } @@ -274,10 +276,10 @@ func signersToCliArgs(knownSigners []*btcec.PublicKey) string { func noncesToCliArgs(knownNonces [][66]byte) string { b := strings.Builder{} - b.Grow(len(knownNonces) * (16 + 132)) + b.Grow(len(knownNonces) * (15 + 132)) for _, nonce := range knownNonces { - b.WriteString(" --musig2-nonce ") + b.WriteString(" --musig-nonce ") b.WriteString(hex.EncodeToString(nonce[:])) } @@ -286,10 +288,10 @@ func noncesToCliArgs(knownNonces [][66]byte) string { func partialSigsToCliArgs(knownPartialSigs []*musig2.PartialSignature) string { b := strings.Builder{} - b.Grow(len(knownPartialSigs) * (18 + 64)) + b.Grow(len(knownPartialSigs) * (17 + 64)) for _, partialSig := range knownPartialSigs { - b.WriteString(" --musig2-partial ") + b.WriteString(" --musig-partial ") w := &bytes.Buffer{} partialSig.Encode(w) b.Write([]byte(hex.EncodeToString(w.Bytes()))) From bb450592182a27d98d481f851d147eee3b43758c Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Wed, 15 May 2024 17:31:01 -0300 Subject: [PATCH 095/401] refactor bunker to work better. remove prompts, use lists of keys and secrets and a new random key. --- bunker.go | 120 ++++++++++++++++++++++++++++++++++------------------- go.mod | 14 +++---- go.sum | 32 +++++++------- helpers.go | 20 +++++---- 4 files changed, 109 insertions(+), 77 deletions(-) diff --git a/bunker.go b/bunker.go index 2191701..f2f150e 100644 --- a/bunker.go +++ b/bunker.go @@ -6,6 +6,7 @@ import ( "fmt" "net/url" "os" + "strings" "sync" "time" @@ -33,10 +34,15 @@ var bunker = &cli.Command{ Name: "prompt-sec", Usage: "prompt the user to paste a hex or nsec with which to sign the event", }, - &cli.BoolFlag{ - Name: "yes", - Aliases: []string{"y"}, - Usage: "always respond to any NIP-46 requests from anyone", + &cli.StringSliceFlag{ + Name: "authorized-secrets", + Aliases: []string{"s"}, + Usage: "secrets for which we will always respond", + }, + &cli.StringSliceFlag{ + Name: "authorized-keys", + Aliases: []string{"k"}, + Usage: "pubkeys for which we will always respond", }, }, Action: func(c *cli.Context) error { @@ -63,32 +69,79 @@ var bunker = &cli.Command{ if err != nil { return err } + + // other arguments + authorizedKeys := c.StringSlice("authorized-keys") + authorizedSecrets := c.StringSlice("authorized-secrets") + + // this will be used to auto-authorize the next person who connects who isn't pre-authorized + // it will be stored + newSecret := randString(12) + + // static information pubkey, err := nostr.GetPublicKey(sec) if err != nil { return err } npub, _ := nip19.EncodePublicKey(pubkey) - bunkerURI := fmt.Sprintf("bunker://%s?%s", pubkey, qs.Encode()) bold := color.New(color.Bold).Sprint + italic := color.New(color.Italic).Sprint + // this function will be called every now and then printBunkerInfo := func() { - log("listening at %v:\n pubkey: %s \n npub: %s\n bunker: %s\n\n", + qs.Set("secret", newSecret) + bunkerURI := fmt.Sprintf("bunker://%s?%s", pubkey, qs.Encode()) + + authorizedKeysStr := "" + if len(authorizedKeys) != 0 { + authorizedKeysStr = "\n authorized keys:\n - " + italic(strings.Join(authorizedKeys, "\n - ")) + } + + authorizedSecretsStr := "" + if len(authorizedSecrets) != 0 { + authorizedSecretsStr = "\n authorized secrets:\n - " + italic(strings.Join(authorizedSecrets, "\n - ")) + } + + preauthorizedFlags := "" + for _, k := range authorizedKeys { + preauthorizedFlags += " -k " + k + } + for _, s := range authorizedSecrets { + preauthorizedFlags += " -s " + s + } + + secretKeyFlag := "" + if sec := c.String("sec"); sec != "" { + secretKeyFlag = "--sec " + sec + } + + restartCommand := fmt.Sprintf("nak bunker %s%s %s", + secretKeyFlag, + preauthorizedFlags, + strings.Join(relayURLs, " "), + ) + + log("listening at %v:\n pubkey: %s \n npub: %s%s%s\n to restart: %s\n bunker: %s\n\n", bold(relayURLs), bold(pubkey), bold(npub), + authorizedKeysStr, + authorizedSecretsStr, + color.CyanString(restartCommand), bold(bunkerURI), ) } printBunkerInfo() - alwaysYes := c.Bool("yes") - // subscribe to relays pool := nostr.NewSimplePool(c.Context) + now := nostr.Now() events := pool.SubMany(c.Context, relayURLs, nostr.Filters{ { - Kinds: []int{24133}, - Tags: nostr.TagMap{"p": []string{pubkey}}, + Kinds: []int{nostr.KindNostrConnect}, + Tags: nostr.TagMap{"p": []string{pubkey}}, + Since: &now, + LimitZero: true, }, }) @@ -102,8 +155,20 @@ var bunker = &cli.Command{ cancelPreviousBunkerInfoPrint = cancel // asking user for authorization - signer.AuthorizeRequest = func(harmless bool, from string) bool { - return alwaysYes || harmless || askProceed(from) + signer.AuthorizeRequest = func(harmless bool, from string, secret string) bool { + if secret == newSecret { + // store this key + authorizedKeys = append(authorizedKeys, from) + // discard this and generate a new secret + newSecret = randString(12) + // print bunker info again after this + go func() { + time.Sleep(3 * time.Second) + printBunkerInfo() + }() + } + + return harmless || slices.Contains(authorizedKeys, from) || slices.Contains(authorizedSecrets, secret) } for ie := range events { @@ -158,34 +223,3 @@ var bunker = &cli.Command{ return nil }, } - -var allowedSources = make([]string, 0, 2) - -func askProceed(source string) bool { - if slices.Contains(allowedSources, source) { - return true - } - - fmt.Fprintf(os.Stderr, "request from %s:\n", color.New(color.Bold, color.FgBlue).Sprint(source)) - res, err := ask(" proceed to fulfill this request? (yes/no/always from this) (y/n/a): ", "", - func(answer string) bool { - if answer != "y" && answer != "n" && answer != "a" { - return true - } - return false - }) - if err != nil { - return false - } - switch res { - case "n": - return false - case "y": - return true - case "a": - allowedSources = append(allowedSources, source) - return true - } - - return false -} diff --git a/go.mod b/go.mod index 66c89dc..7537692 100644 --- a/go.mod +++ b/go.mod @@ -9,15 +9,15 @@ require ( github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e github.com/fatih/color v1.16.0 github.com/mailru/easyjson v0.7.7 - github.com/nbd-wtf/go-nostr v0.30.2 + github.com/nbd-wtf/go-nostr v0.31.2 github.com/nbd-wtf/nostr-sdk v0.0.5 github.com/urfave/cli/v2 v2.25.7 - golang.org/x/exp v0.0.0-20231006140011-7918f672742d + golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 ) require ( github.com/btcsuite/btcd/btcutil v1.1.3 // indirect - github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2 // indirect + github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect github.com/chzyer/logex v1.1.10 // indirect github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect @@ -26,17 +26,17 @@ require ( github.com/fiatjaf/eventstore v0.2.16 // indirect github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/pool v0.2.1 // indirect - github.com/gobwas/ws v1.3.1 // indirect + github.com/gobwas/ws v1.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/puzpuzpuz/xsync/v3 v3.0.2 // indirect + github.com/puzpuzpuz/xsync/v3 v3.1.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/tidwall/gjson v1.17.0 // indirect + github.com/tidwall/gjson v1.17.1 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect golang.org/x/crypto v0.7.0 // indirect - golang.org/x/sys v0.14.0 // indirect + golang.org/x/sys v0.20.0 // indirect golang.org/x/text v0.8.0 // indirect ) diff --git a/go.sum b/go.sum index c2d4069..17ac72c 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,6 @@ github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tj github.com/btcsuite/btcd v0.23.0/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY= github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA= github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= -github.com/btcsuite/btcd/btcec/v2 v2.3.2 h1:5n0X6hX0Zk+6omWcihdYvdAlGf2DfasC0GMf7DClJ3U= -github.com/btcsuite/btcd/btcec/v2 v2.3.2/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= github.com/btcsuite/btcd/btcec/v2 v2.3.3 h1:6+iXlDKE8RMtKsvK0gshlXIuPbyWM/h84Ensb7o3sC0= github.com/btcsuite/btcd/btcec/v2 v2.3.3/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A= @@ -14,8 +12,8 @@ github.com/btcsuite/btcd/btcutil v1.1.3 h1:xfbtw8lwpp0G6NwSHb+UE67ryTFHJAiNuipus github.com/btcsuite/btcd/btcutil v1.1.3/go.mod h1:UR7dsSJzJUfMmFiiLlIrMq1lS9jh9EdCV7FStZSnpi0= github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= -github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2 h1:KdUfX2zKommPRa+PD0sWZUyXe9w277ABlgELO7H04IM= -github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 h1:59Kx4K6lzOW5w6nFlA0v5+lk/6sjybR934QNHSJZPTQ= +github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= @@ -41,8 +39,6 @@ github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y= github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= @@ -56,8 +52,8 @@ github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= -github.com/gobwas/ws v1.3.1 h1:Qi34dfLMWJbiKaNbDVzM9x27nZBjmkaW6i4+Ku+pGVU= -github.com/gobwas/ws v1.3.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= +github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs= +github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= @@ -83,8 +79,8 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/nbd-wtf/go-nostr v0.30.2 h1:dG/2X52/XDg+7phZH+BClcvA5D+S6dXvxJKkBaySEzI= -github.com/nbd-wtf/go-nostr v0.30.2/go.mod h1:tiKJY6fWYSujbTQb201Y+IQ3l4szqYVt+fsTnsm7FCk= +github.com/nbd-wtf/go-nostr v0.31.2 h1:PkHCAsSzG0Ce8tfF7LKyvZOjYtCdC+hPh5KfO/Rl1b4= +github.com/nbd-wtf/go-nostr v0.31.2/go.mod h1:vHKtHyLXDXzYBN0fi/9Y/Q5AD0p+hk8TQVKlldAi0gI= github.com/nbd-wtf/nostr-sdk v0.0.5 h1:rec+FcDizDVO0W25PX0lgYMXvP7zNNOgI3Fu9UCm4BY= github.com/nbd-wtf/nostr-sdk v0.0.5/go.mod h1:iJJsikesCGLNFZ9dLqhLPDzdt924EagUmdQxT3w2Lmk= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= @@ -98,8 +94,8 @@ github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7J github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/puzpuzpuz/xsync/v3 v3.0.2 h1:3yESHrRFYr6xzkz61LLkvNiPFXxJEAABanTQpKbAaew= -github.com/puzpuzpuz/xsync/v3 v3.0.2/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= +github.com/puzpuzpuz/xsync/v3 v3.1.0 h1:EewKT7/LNac5SLiEblJeUu8z5eERHrmRLnMQL2d7qX4= +github.com/puzpuzpuz/xsync/v3 v3.1.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -107,8 +103,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= -github.com/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM= -github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U= +github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= @@ -123,8 +119,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= -golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= +golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= +golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -144,8 +140,8 @@ golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= -golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= diff --git a/helpers.go b/helpers.go index d90056b..60a57ae 100644 --- a/helpers.go +++ b/helpers.go @@ -5,6 +5,7 @@ import ( "context" "encoding/hex" "fmt" + "math/rand" "net/url" "os" "strings" @@ -203,15 +204,6 @@ func promptDecrypt(ncryptsec1 string) (string, error) { return "", fmt.Errorf("couldn't decrypt private key") } -func ask(msg string, defaultValue string, shouldAskAgain func(answer string) bool) (string, error) { - return _ask(&readline.Config{ - Stdout: color.Error, - Prompt: color.YellowString(msg), - InterruptPrompt: "^C", - DisableAutoSaveHistory: true, - }, msg, defaultValue, shouldAskAgain) -} - func askPassword(msg string, shouldAskAgain func(answer string) bool) (string, error) { config := &readline.Config{ Stdout: color.Error, @@ -243,3 +235,13 @@ func _ask(config *readline.Config, msg string, defaultValue string, shouldAskAga return answer, err } } + +const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + +func randString(n int) string { + b := make([]byte, n) + for i := range b { + b[i] = letterBytes[rand.Intn(len(letterBytes))] + } + return string(b) +} From 31f007ffc20524d1b991f056a225482710145989 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Wed, 15 May 2024 18:49:03 -0300 Subject: [PATCH 096/401] fix: musig2 event to cli args was generating multivalue tags wrongly. --- musig2.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/musig2.go b/musig2.go index 68b96b8..201c057 100644 --- a/musig2.go +++ b/musig2.go @@ -251,7 +251,7 @@ func eventToCliArgs(evt *nostr.Event) string { b.WriteString(tag[1]) if len(tag) > 2 { for _, item := range tag[2:] { - b.WriteString(",") + b.WriteString(";") b.WriteString(item) } } From eccce6dc4a43029b4cbb99ebcd582384b2f5dfdd Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Thu, 16 May 2024 19:45:30 -0300 Subject: [PATCH 097/401] `nak key combine` now returns all possible combinations. --- key.go | 121 +++++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 100 insertions(+), 21 deletions(-) diff --git a/key.go b/key.go index 7e0cb4b..a7814d9 100644 --- a/key.go +++ b/key.go @@ -2,11 +2,14 @@ package main import ( "encoding/hex" + "encoding/json" "fmt" + "os" "strings" "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" + "github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/nip19" "github.com/nbd-wtf/go-nostr/nip49" @@ -127,32 +130,108 @@ var decrypt = &cli.Command{ } var combine = &cli.Command{ - Name: "combine", - Usage: "combines two or more pubkeys using musig2", - Description: `The public keys must have 33 bytes (66 characters hex), with the 02 or 03 prefix. It is common in Nostr to drop that first byte, so you'll have to derive the public keys again from the private keys in order to get it back.`, - ArgsUsage: "[pubkey...]", + Name: "combine", + Usage: "combines two or more pubkeys using musig2", + Description: `The public keys must have 33 bytes (66 characters hex), with the 02 or 03 prefix. It is common in Nostr to drop that first byte, so you'll have to derive the public keys again from the private keys in order to get it back. + +However, if the intent is to check if two existing Nostr pubkeys match a given combined pubkey, then it might be sufficient to calculate the combined key for all the possible combinations of pubkeys in the input.`, + ArgsUsage: "[pubkey...]", Action: func(c *cli.Context) error { - keys := make([]*btcec.PublicKey, 0, 5) - for _, pub := range c.Args().Slice() { - keyb, err := hex.DecodeString(pub) - if err != nil { - return fmt.Errorf("error parsing key %s: %w", pub, err) - } - - pubk, err := btcec.ParsePubKey(keyb) - if err != nil { - return fmt.Errorf("error parsing key %s: %w", pub, err) - } - - keys = append(keys, pubk) + type Combination struct { + Variants []string `json:"input_variants"` + Output struct { + XOnly string `json:"x_only"` + Variant string `json:"variant"` + } `json:"combined_key"` } - agg, _, _, err := musig2.AggregateKeys(keys, true) - if err != nil { - return err + type Result struct { + Keys []string `json:"keys"` + Combinations []Combination `json:"combinations"` } - fmt.Println(hex.EncodeToString(agg.FinalKey.SerializeCompressed())) + result := Result{} + + result.Keys = c.Args().Slice() + keyGroups := make([][]*btcec.PublicKey, 0, len(result.Keys)) + + for i, keyhex := range result.Keys { + keyb, err := hex.DecodeString(keyhex) + if err != nil { + return fmt.Errorf("error parsing key %s: %w", keyhex, err) + } + + if len(keyb) == 32 /* we'll use both the 02 and the 03 prefix versions */ { + group := make([]*btcec.PublicKey, 2) + for i, prefix := range []byte{0x02, 0x03} { + pubk, err := btcec.ParsePubKey(append([]byte{prefix}, keyb...)) + if err != nil { + fmt.Fprintf(os.Stderr, "error parsing key %s: %s", keyhex, err) + continue + } + group[i] = pubk + } + keyGroups = append(keyGroups, group) + } else /* assume it's 33 */ { + pubk, err := btcec.ParsePubKey(keyb) + if err != nil { + return fmt.Errorf("error parsing key %s: %w", keyhex, err) + } + keyGroups = append(keyGroups, []*btcec.PublicKey{pubk}) + + // remove the leading byte from the output just so it is all uniform + result.Keys[i] = result.Keys[i][2:] + } + } + + result.Combinations = make([]Combination, 0, 16) + + var fn func(prepend int, curr []int) + fn = func(prepend int, curr []int) { + curr = append([]int{prepend}, curr...) + if len(curr) == len(keyGroups) { + combi := Combination{ + Variants: make([]string, len(keyGroups)), + } + + combining := make([]*btcec.PublicKey, len(keyGroups)) + for g, altKeys := range keyGroups { + altKey := altKeys[curr[g]] + variant := secp256k1.PubKeyFormatCompressedEven + if altKey.Y().Bit(0) == 1 { + variant = secp256k1.PubKeyFormatCompressedOdd + } + combi.Variants[g] = hex.EncodeToString([]byte{variant}) + combining[g] = altKey + } + + agg, _, _, err := musig2.AggregateKeys(combining, true) + if err != nil { + fmt.Fprintf(os.Stderr, "error aggregating: %s", err) + return + } + + serialized := agg.FinalKey.SerializeCompressed() + combi.Output.XOnly = hex.EncodeToString(serialized[1:]) + combi.Output.Variant = hex.EncodeToString(serialized[0:1]) + result.Combinations = append(result.Combinations, combi) + return + } + + fn(0, curr) + if len(keyGroups[len(keyGroups)-len(curr)-1]) > 1 { + fn(1, curr) + } + } + + fn(0, nil) + if len(keyGroups[len(keyGroups)-1]) > 1 { + fn(1, nil) + } + + res, _ := json.MarshalIndent(result, "", " ") + fmt.Println(string(res)) + return nil }, } From 363bd66a8a4edf219ec6e3df839c4bb6aced97a4 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Thu, 6 Jun 2024 15:38:40 -0300 Subject: [PATCH 098/401] accept relay URLs without scheme everywhere. --- encode.go | 6 +++--- fetch.go | 4 ++-- helpers.go | 7 +++++-- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/encode.go b/encode.go index a4efbb8..4bee620 100644 --- a/encode.go +++ b/encode.go @@ -85,7 +85,7 @@ var encode = &cli.Command{ } relays := c.StringSlice("relay") - if err := validateRelayURLs(relays); err != nil { + if err := normalizeAndValidateRelayURLs(relays); err != nil { return err } @@ -129,7 +129,7 @@ var encode = &cli.Command{ } relays := c.StringSlice("relay") - if err := validateRelayURLs(relays); err != nil { + if err := normalizeAndValidateRelayURLs(relays); err != nil { return err } @@ -193,7 +193,7 @@ var encode = &cli.Command{ } relays := c.StringSlice("relay") - if err := validateRelayURLs(relays); err != nil { + if err := normalizeAndValidateRelayURLs(relays); err != nil { return err } diff --git a/fetch.go b/fetch.go index 4c279fc..64a8056 100644 --- a/fetch.go +++ b/fetch.go @@ -9,7 +9,7 @@ import ( var fetch = &cli.Command{ Name: "fetch", - Usage: "fetches events related to the given nip19 code from the included relay hints", + Usage: "fetches events related to the given nip19 code from the included relay hints or the author's NIP-65 relays.", Description: `example usage: nak fetch nevent1qqsxrwm0hd3s3fddh4jc2574z3xzufq6qwuyz2rvv3n087zvym3dpaqprpmhxue69uhhqatzd35kxtnjv4kxz7tfdenju6t0xpnej4 echo npub1h8spmtw9m2huyv6v2j2qd5zv956z2zdugl6mgx02f2upffwpm3nqv0j4ps | nak fetch --relay wss://relay.nostr.band`, @@ -41,7 +41,7 @@ var fetch = &cli.Command{ } relays := c.StringSlice("relay") - if err := validateRelayURLs(relays); err != nil { + if err := normalizeAndValidateRelayURLs(relays); err != nil { return err } var authorHint string diff --git a/helpers.go b/helpers.go index 60a57ae..77abe36 100644 --- a/helpers.go +++ b/helpers.go @@ -92,8 +92,11 @@ func writeStdinLinesOrNothing(ch chan string) (hasStdinLines bool) { } } -func validateRelayURLs(wsurls []string) error { - for _, wsurl := range wsurls { +func normalizeAndValidateRelayURLs(wsurls []string) error { + for i, wsurl := range wsurls { + wsurl = nostr.NormalizeURL(wsurl) + wsurls[i] = wsurl + u, err := url.Parse(wsurl) if err != nil { return fmt.Errorf("invalid relay url '%s': %s", wsurl, err) From 262c0c892aa39657e90a8746b715a9b90d60e0ce Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Thu, 6 Jun 2024 15:43:42 -0300 Subject: [PATCH 099/401] fix simple test event ordering. --- example_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example_test.go b/example_test.go index 8e5dd77..89fd4d0 100644 --- a/example_test.go +++ b/example_test.go @@ -5,7 +5,7 @@ import "os" func ExampleEventBasic() { app.Run([]string{"nak", "event", "--ts", "1699485669"}) // Output: - // {"id":"36d88cf5fcc449f2390a424907023eda7a74278120eebab8d02797cd92e7e29c","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1699485669,"kind":1,"tags":[],"content":"hello from the nostr army knife","sig":"68e71a192e8abcf8582a222434ac823ecc50607450ebe8cc4c145eb047794cc382dc3f888ce879d2f404f5ba6085a47601360a0fa2dd4b50d317bd0c6197c2c2"} + // {"kind":1,"id":"36d88cf5fcc449f2390a424907023eda7a74278120eebab8d02797cd92e7e29c","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1699485669,"tags":[],"content":"hello from the nostr army knife","sig":"68e71a192e8abcf8582a222434ac823ecc50607450ebe8cc4c145eb047794cc382dc3f888ce879d2f404f5ba6085a47601360a0fa2dd4b50d317bd0c6197c2c2"} } // (for some reason there can only be one test dealing with stdin in the suite otherwise it halts) From 1ba39ca7d73ebcacba97b8db6a2ae33c01954666 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Fri, 7 Jun 2024 06:30:55 -0300 Subject: [PATCH 100/401] print bunker restart command without schemes in relay urls when possible. --- bunker.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/bunker.go b/bunker.go index f2f150e..daf1c5e 100644 --- a/bunker.go +++ b/bunker.go @@ -115,10 +115,19 @@ var bunker = &cli.Command{ secretKeyFlag = "--sec " + sec } + relayURLsPossiblyWithoutSchema := make([]string, len(relayURLs)) + for i, url := range relayURLs { + if strings.HasPrefix(url, "wss://") { + relayURLsPossiblyWithoutSchema[i] = url[6:] + } else { + relayURLsPossiblyWithoutSchema[i] = url + } + } + restartCommand := fmt.Sprintf("nak bunker %s%s %s", secretKeyFlag, preauthorizedFlags, - strings.Join(relayURLs, " "), + strings.Join(relayURLsPossiblyWithoutSchema, " "), ) log("listening at %v:\n pubkey: %s \n npub: %s%s%s\n to restart: %s\n bunker: %s\n\n", From 9f98a0aea36e6899bc0266cf24be6a56b94f78cb Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Wed, 12 Jun 2024 08:54:00 -0300 Subject: [PATCH 101/401] fix nak key encrypt reading from stdin. --- key.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/key.go b/key.go index a7814d9..fe2fbbd 100644 --- a/key.go +++ b/key.go @@ -72,20 +72,19 @@ var encrypt = &cli.Command{ }, }, Action: func(c *cli.Context) error { - var content string + keys := make([]string, 0, 1) var password string switch c.Args().Len() { case 1: - content = "" password = c.Args().Get(0) case 2: - content = c.Args().Get(0) + keys = append(keys, c.Args().Get(0)) password = c.Args().Get(1) } if password == "" { return fmt.Errorf("no password given") } - for sec := range getSecretKeysFromStdinLinesOrSlice(c, []string{content}) { + for sec := range getSecretKeysFromStdinLinesOrSlice(c, keys) { ncryptsec, err := nip49.Encrypt(sec, password, uint8(c.Int("logn")), 0x02) if err != nil { lineProcessingError(c, "failed to encrypt: %s", err) From 2135b681060ed5ebcf22ccb40a7158e6ba298a9c Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Thu, 13 Jun 2024 12:05:27 -0300 Subject: [PATCH 102/401] more aliases in nak encode flags. --- encode.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/encode.go b/encode.go index 4bee620..24689e3 100644 --- a/encode.go +++ b/encode.go @@ -110,8 +110,9 @@ var encode = &cli.Command{ Usage: "attach relay hints to nevent code", }, &cli.StringFlag{ - Name: "author", - Usage: "attach an author pubkey as a hint to the nevent code", + Name: "author", + Aliases: []string{"a"}, + Usage: "attach an author pubkey as a hint to the nevent code", }, }, Action: func(c *cli.Context) error { @@ -157,7 +158,7 @@ var encode = &cli.Command{ &cli.StringFlag{ Name: "pubkey", Usage: "pubkey of the naddr author", - Aliases: []string{"p"}, + Aliases: []string{"author", "a", "p"}, Required: true, }, &cli.Int64Flag{ From 2079ddf818c2ae5c95925a660cf878f3de58bd49 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 25 Jun 2024 13:46:15 -0300 Subject: [PATCH 103/401] support prompting for a password on nak decrypt. --- helpers.go | 4 ++-- key.go | 44 +++++++++++++++++++++++++++++++------------- 2 files changed, 33 insertions(+), 15 deletions(-) diff --git a/helpers.go b/helpers.go index 77abe36..5e0cb9a 100644 --- a/helpers.go +++ b/helpers.go @@ -188,7 +188,7 @@ func gatherSecretKeyOrBunkerFromArguments(c *cli.Context) (string, *nip46.Bunker return sec, nil, nil } -func promptDecrypt(ncryptsec1 string) (string, error) { +func promptDecrypt(ncryptsec string) (string, error) { for i := 1; i < 4; i++ { var attemptStr string if i > 1 { @@ -198,7 +198,7 @@ func promptDecrypt(ncryptsec1 string) (string, error) { if err != nil { return "", err } - sec, err := nip49.Decrypt(ncryptsec1, password) + sec, err := nip49.Decrypt(ncryptsec, password) if err != nil { continue } diff --git a/key.go b/key.go index fe2fbbd..e14600c 100644 --- a/key.go +++ b/key.go @@ -102,29 +102,47 @@ var decrypt = &cli.Command{ Description: `uses the NIP-49 standard.`, ArgsUsage: " ", Action: func(c *cli.Context) error { - var content string + var ncryptsec string var password string switch c.Args().Len() { - case 1: - content = "" - password = c.Args().Get(0) case 2: - content = c.Args().Get(0) + ncryptsec = c.Args().Get(0) password = c.Args().Get(1) - } - if password == "" { - return fmt.Errorf("no password given") - } - for ncryptsec := range getStdinLinesOrArgumentsFromSlice([]string{content}) { + if password == "" { + return fmt.Errorf("no password given") + } sec, err := nip49.Decrypt(ncryptsec, password) if err != nil { - lineProcessingError(c, "failed to decrypt: %s", err) - continue + return fmt.Errorf("failed to decrypt: %s", err) } nsec, _ := nip19.EncodePrivateKey(sec) stdout(nsec) + return nil + case 1: + if arg := c.Args().Get(0); strings.HasPrefix(arg, "ncryptsec1") { + ncryptsec = arg + if res, err := promptDecrypt(ncryptsec); err != nil { + return err + } else { + stdout(res) + return nil + } + } else { + password = c.Args().Get(0) + for ncryptsec := range getStdinLinesOrArgumentsFromSlice([]string{ncryptsec}) { + sec, err := nip49.Decrypt(ncryptsec, password) + if err != nil { + lineProcessingError(c, "failed to decrypt: %s", err) + continue + } + nsec, _ := nip19.EncodePrivateKey(sec) + stdout(nsec) + } + return nil + } + default: + return fmt.Errorf("invalid number of arguments") } - return nil }, } From dba2ed0b5fecd355e2452482b98a8ae013c38c9a Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 25 Jun 2024 22:18:26 -0300 Subject: [PATCH 104/401] update to cli v3. --- bunker.go | 18 +++++++++--------- count.go | 17 +++++++++++------ decode.go | 13 +++++++------ encode.go | 47 ++++++++++++++++++++++++----------------------- event.go | 28 ++++++++++++++-------------- fetch.go | 18 ++++++++++-------- go.mod | 6 ++---- go.sum | 8 ++------ helpers.go | 14 +++++++------- key.go | 31 ++++++++++++++++--------------- main.go | 13 ++++++------- relay.go | 9 +++++---- req.go | 25 +++++++++++++------------ verify.go | 13 +++++++------ 14 files changed, 133 insertions(+), 127 deletions(-) diff --git a/bunker.go b/bunker.go index daf1c5e..64d2d4e 100644 --- a/bunker.go +++ b/bunker.go @@ -14,7 +14,7 @@ import ( "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/nip19" "github.com/nbd-wtf/go-nostr/nip46" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" "golang.org/x/exp/slices" ) @@ -45,12 +45,12 @@ var bunker = &cli.Command{ Usage: "pubkeys for which we will always respond", }, }, - Action: func(c *cli.Context) error { + Action: func(ctx context.Context, c *cli.Command) error { // try to connect to the relays here qs := url.Values{} relayURLs := make([]string, 0, c.Args().Len()) if relayUrls := c.Args().Slice(); len(relayUrls) > 0 { - _, relays := connectToAllRelays(c.Context, relayUrls) + _, relays := connectToAllRelays(ctx, relayUrls) if len(relays) == 0 { log("failed to connect to any of the given relays.\n") os.Exit(3) @@ -65,7 +65,7 @@ var bunker = &cli.Command{ } // gather the secret key - sec, _, err := gatherSecretKeyOrBunkerFromArguments(c) + sec, _, err := gatherSecretKeyOrBunkerFromArguments(ctx, c) if err != nil { return err } @@ -143,9 +143,9 @@ var bunker = &cli.Command{ printBunkerInfo() // subscribe to relays - pool := nostr.NewSimplePool(c.Context) + pool := nostr.NewSimplePool(ctx) now := nostr.Now() - events := pool.SubMany(c.Context, relayURLs, nostr.Filters{ + events := pool.SubMany(ctx, relayURLs, nostr.Filters{ { Kinds: []int{nostr.KindNostrConnect}, Tags: nostr.TagMap{"p": []string{pubkey}}, @@ -160,7 +160,7 @@ var bunker = &cli.Command{ // just a gimmick var cancelPreviousBunkerInfoPrint context.CancelFunc - _, cancel := context.WithCancel(c.Context) + _, cancel := context.WithCancel(ctx) cancelPreviousBunkerInfoPrint = cancel // asking user for authorization @@ -199,7 +199,7 @@ var bunker = &cli.Command{ for _, relayURL := range relayURLs { go func(relayURL string) { if relay, _ := pool.EnsureRelay(relayURL); relay != nil { - err := relay.Publish(c.Context, eventResponse) + err := relay.Publish(ctx, eventResponse) printLock.Lock() if err == nil { log("* sent response through %s\n", relay.URL) @@ -215,7 +215,7 @@ var bunker = &cli.Command{ // just after handling one request we trigger this go func() { - ctx, cancel := context.WithCancel(c.Context) + ctx, cancel := context.WithCancel(ctx) defer cancel() cancelPreviousBunkerInfoPrint = cancel // the idea is that we will print the bunker URL again so it is easier to copy-paste by users diff --git a/count.go b/count.go index ba3f1ed..cbb3540 100644 --- a/count.go +++ b/count.go @@ -1,13 +1,14 @@ package main import ( + "context" "encoding/json" "errors" "fmt" "strings" "github.com/nbd-wtf/go-nostr" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) var count = &cli.Command{ @@ -63,7 +64,7 @@ var count = &cli.Command{ }, }, ArgsUsage: "[relay...]", - Action: func(c *cli.Context) error { + Action: func(ctx context.Context, c *cli.Command) error { filter := nostr.Filter{} if authors := c.StringSlice("author"); len(authors) > 0 { @@ -72,7 +73,11 @@ var count = &cli.Command{ if ids := c.StringSlice("id"); len(ids) > 0 { filter.IDs = ids } - if kinds := c.IntSlice("kind"); len(kinds) > 0 { + if kinds64 := c.IntSlice("kind"); len(kinds64) > 0 { + kinds := make([]int, len(kinds64)) + for i, v := range kinds64 { + kinds[i] = int(v) + } filter.Kinds = kinds } @@ -110,7 +115,7 @@ var count = &cli.Command{ filter.Until = &ts } if limit := c.Int("limit"); limit != 0 { - filter.Limit = limit + filter.Limit = int(limit) } relays := c.Args().Slice() @@ -118,12 +123,12 @@ var count = &cli.Command{ failures := make([]error, 0, len(relays)) if len(relays) > 0 { for _, relayUrl := range relays { - relay, err := nostr.RelayConnect(c.Context, relayUrl) + relay, err := nostr.RelayConnect(ctx, relayUrl) if err != nil { failures = append(failures, err) continue } - count, err := relay.Count(c.Context, nostr.Filters{filter}) + count, err := relay.Count(ctx, nostr.Filters{filter}) if err != nil { failures = append(failures, err) continue diff --git a/decode.go b/decode.go index 8dd752d..9c9d8be 100644 --- a/decode.go +++ b/decode.go @@ -1,6 +1,7 @@ package main import ( + "context" "encoding/hex" "encoding/json" "strings" @@ -8,7 +9,7 @@ import ( "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/nip19" sdk "github.com/nbd-wtf/nostr-sdk" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) var decode = &cli.Command{ @@ -32,7 +33,7 @@ var decode = &cli.Command{ }, }, ArgsUsage: "", - Action: func(c *cli.Context) error { + Action: func(ctx context.Context, c *cli.Command) error { for input := range getStdinLinesOrArguments(c.Args()) { if strings.HasPrefix(input, "nostr:") { input = input[6:] @@ -49,12 +50,12 @@ var decode = &cli.Command{ decodeResult.HexResult.PrivateKey = hex.EncodeToString(b) decodeResult.HexResult.PublicKey = hex.EncodeToString(b) } else { - lineProcessingError(c, "hex string with invalid number of bytes: %d", len(b)) + lineProcessingError(ctx, "hex string with invalid number of bytes: %d", len(b)) continue } } else if evp := sdk.InputToEventPointer(input); evp != nil { decodeResult = DecodeResult{EventPointer: evp} - } else if pp := sdk.InputToProfile(c.Context, input); pp != nil { + } else if pp := sdk.InputToProfile(ctx, input); pp != nil { decodeResult = DecodeResult{ProfilePointer: pp} } else if prefix, value, err := nip19.Decode(input); err == nil && prefix == "naddr" { ep := value.(nostr.EntityPointer) @@ -63,7 +64,7 @@ var decode = &cli.Command{ decodeResult.PrivateKey.PrivateKey = value.(string) decodeResult.PrivateKey.PublicKey, _ = nostr.GetPublicKey(value.(string)) } else { - lineProcessingError(c, "couldn't decode input '%s': %s", input, err) + lineProcessingError(ctx, "couldn't decode input '%s': %s", input, err) continue } @@ -71,7 +72,7 @@ var decode = &cli.Command{ } - exitIfLineProcessingError(c) + exitIfLineProcessingError(ctx) return nil }, } diff --git a/encode.go b/encode.go index 24689e3..4eaf3ac 100644 --- a/encode.go +++ b/encode.go @@ -1,11 +1,12 @@ package main import ( + "context" "fmt" "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/nip19" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) var encode = &cli.Command{ @@ -18,20 +19,20 @@ var encode = &cli.Command{ nak encode nevent nak encode nevent --author --relay --relay nak encode nsec `, - Before: func(c *cli.Context) error { + Before: func(ctx context.Context, c *cli.Command) error { if c.Args().Len() < 1 { return fmt.Errorf("expected more than 1 argument.") } return nil }, - Subcommands: []*cli.Command{ + Commands: []*cli.Command{ { Name: "npub", Usage: "encode a hex public key into bech32 'npub' format", - Action: func(c *cli.Context) error { + Action: func(ctx context.Context, c *cli.Command) error { for target := range getStdinLinesOrArguments(c.Args()) { if ok := nostr.IsValidPublicKey(target); !ok { - lineProcessingError(c, "invalid public key: %s", target) + lineProcessingError(ctx, "invalid public key: %s", target) continue } @@ -42,17 +43,17 @@ var encode = &cli.Command{ } } - exitIfLineProcessingError(c) + exitIfLineProcessingError(ctx) return nil }, }, { Name: "nsec", Usage: "encode a hex private key into bech32 'nsec' format", - Action: func(c *cli.Context) error { + Action: func(ctx context.Context, c *cli.Command) error { for target := range getStdinLinesOrArguments(c.Args()) { if ok := nostr.IsValid32ByteHex(target); !ok { - lineProcessingError(c, "invalid private key: %s", target) + lineProcessingError(ctx, "invalid private key: %s", target) continue } @@ -63,7 +64,7 @@ var encode = &cli.Command{ } } - exitIfLineProcessingError(c) + exitIfLineProcessingError(ctx) return nil }, }, @@ -77,10 +78,10 @@ var encode = &cli.Command{ Usage: "attach relay hints to nprofile code", }, }, - Action: func(c *cli.Context) error { + Action: func(ctx context.Context, c *cli.Command) error { for target := range getStdinLinesOrArguments(c.Args()) { if ok := nostr.IsValid32ByteHex(target); !ok { - lineProcessingError(c, "invalid public key: %s", target) + lineProcessingError(ctx, "invalid public key: %s", target) continue } @@ -96,7 +97,7 @@ var encode = &cli.Command{ } } - exitIfLineProcessingError(c) + exitIfLineProcessingError(ctx) return nil }, }, @@ -115,10 +116,10 @@ var encode = &cli.Command{ Usage: "attach an author pubkey as a hint to the nevent code", }, }, - Action: func(c *cli.Context) error { + Action: func(ctx context.Context, c *cli.Command) error { for target := range getStdinLinesOrArguments(c.Args()) { if ok := nostr.IsValid32ByteHex(target); !ok { - lineProcessingError(c, "invalid event id: %s", target) + lineProcessingError(ctx, "invalid event id: %s", target) continue } @@ -141,7 +142,7 @@ var encode = &cli.Command{ } } - exitIfLineProcessingError(c) + exitIfLineProcessingError(ctx) return nil }, }, @@ -161,7 +162,7 @@ var encode = &cli.Command{ Aliases: []string{"author", "a", "p"}, Required: true, }, - &cli.Int64Flag{ + &cli.IntFlag{ Name: "kind", Aliases: []string{"k"}, Usage: "kind of referred replaceable event", @@ -173,7 +174,7 @@ var encode = &cli.Command{ Usage: "attach relay hints to naddr code", }, }, - Action: func(c *cli.Context) error { + Action: func(ctx context.Context, c *cli.Command) error { for d := range getStdinLinesOrBlank() { pubkey := c.String("pubkey") if ok := nostr.IsValidPublicKey(pubkey); !ok { @@ -188,7 +189,7 @@ var encode = &cli.Command{ if d == "" { d = c.String("identifier") if d == "" { - lineProcessingError(c, "\"d\" tag identifier can't be empty") + lineProcessingError(ctx, "\"d\" tag identifier can't be empty") continue } } @@ -198,24 +199,24 @@ var encode = &cli.Command{ return err } - if npub, err := nip19.EncodeEntity(pubkey, kind, d, relays); err == nil { + if npub, err := nip19.EncodeEntity(pubkey, int(kind), d, relays); err == nil { stdout(npub) } else { return err } } - exitIfLineProcessingError(c) + exitIfLineProcessingError(ctx) return nil }, }, { Name: "note", Usage: "generate note1 event codes (not recommended)", - Action: func(c *cli.Context) error { + Action: func(ctx context.Context, c *cli.Command) error { for target := range getStdinLinesOrArguments(c.Args()) { if ok := nostr.IsValid32ByteHex(target); !ok { - lineProcessingError(c, "invalid event id: %s", target) + lineProcessingError(ctx, "invalid event id: %s", target) continue } @@ -226,7 +227,7 @@ var encode = &cli.Command{ } } - exitIfLineProcessingError(c) + exitIfLineProcessingError(ctx) return nil }, }, diff --git a/event.go b/event.go index fa7e639..6e6b225 100644 --- a/event.go +++ b/event.go @@ -13,7 +13,7 @@ import ( "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/nip19" "github.com/nbd-wtf/go-nostr/nson" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" "golang.org/x/exp/slices" ) @@ -36,7 +36,7 @@ example: Flags: []cli.Flag{ &cli.StringFlag{ Name: "sec", - Usage: "secret key to sign the event, as hex or nsec", + Usage: "secret key to sign the event, as nsec, ncryptsec or hex", DefaultText: "the key '1'", Value: "0000000000000000000000000000000000000000000000000000000000000001", }, @@ -141,11 +141,11 @@ example: }, }, ArgsUsage: "[relay...]", - Action: func(c *cli.Context) error { + Action: func(ctx context.Context, c *cli.Command) error { // try to connect to the relays here var relays []*nostr.Relay if relayUrls := c.Args().Slice(); len(relayUrls) > 0 { - _, relays = connectToAllRelays(c.Context, relayUrls) + _, relays = connectToAllRelays(ctx, relayUrls) if len(relays) == 0 { log("failed to connect to any of the given relays.\n") os.Exit(3) @@ -158,7 +158,7 @@ example: } }() - sec, bunker, err := gatherSecretKeyOrBunkerFromArguments(c) + sec, bunker, err := gatherSecretKeyOrBunkerFromArguments(ctx, c) if err != nil { return err } @@ -176,14 +176,14 @@ example: if stdinEvent != "" { if err := easyjson.Unmarshal([]byte(stdinEvent), &evt); err != nil { - lineProcessingError(c, "invalid event received from stdin: %s", err) + lineProcessingError(ctx, "invalid event received from stdin: %s", err) continue } kindWasSupplied = strings.Contains(stdinEvent, `"kind"`) } if kind := c.Int("kind"); slices.Contains(c.FlagNames(), "kind") { - evt.Kind = kind + evt.Kind = int(kind) mustRehashAndResign = true } else if !kindWasSupplied { evt.Kind = 1 @@ -248,7 +248,7 @@ example: if evt.Sig == "" || mustRehashAndResign { if bunker != nil { - if err := bunker.SignEvent(c.Context, &evt); err != nil { + if err := bunker.SignEvent(ctx, &evt); err != nil { return fmt.Errorf("failed to sign with bunker: %w", err) } } else if numSigners := c.Uint("musig"); numSigners > 1 && sec != "" { @@ -256,7 +256,7 @@ example: secNonce := c.String("musig-nonce-secret") pubNonces := c.StringSlice("musig-nonce") partialSigs := c.StringSlice("musig-partial") - signed, err := performMusig(c.Context, + signed, err := performMusig(ctx, sec, &evt, int(numSigners), pubkeys, pubNonces, secNonce, partialSigs) if err != nil { return fmt.Errorf("musig error: %w", err) @@ -291,7 +291,7 @@ example: for _, relay := range relays { publish: log("publishing to %s... ", relay.URL) - ctx, cancel := context.WithTimeout(c.Context, 10*time.Second) + ctx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() err := relay.Publish(ctx, evt) @@ -307,7 +307,7 @@ example: // if the relay is requesting auth and we can auth, let's do it var pk string if bunker != nil { - pk, err = bunker.GetPublicKey(c.Context) + pk, err = bunker.GetPublicKey(ctx) if err != nil { return fmt.Errorf("failed to get public key from bunker: %w", err) } @@ -315,9 +315,9 @@ example: pk, _ = nostr.GetPublicKey(sec) } log("performing auth as %s... ", pk) - if err := relay.Auth(c.Context, func(evt *nostr.Event) error { + if err := relay.Auth(ctx, func(evt *nostr.Event) error { if bunker != nil { - return bunker.SignEvent(c.Context, evt) + return bunker.SignEvent(ctx, evt) } return evt.Sign(sec) }); err == nil { @@ -337,7 +337,7 @@ example: } } - exitIfLineProcessingError(c) + exitIfLineProcessingError(ctx) return nil }, } diff --git a/fetch.go b/fetch.go index 64a8056..0a59108 100644 --- a/fetch.go +++ b/fetch.go @@ -1,10 +1,12 @@ package main import ( + "context" + "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/nip19" sdk "github.com/nbd-wtf/nostr-sdk" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) var fetch = &cli.Command{ @@ -21,8 +23,8 @@ var fetch = &cli.Command{ }, }, ArgsUsage: "[nip19code]", - Action: func(c *cli.Context) error { - pool := nostr.NewSimplePool(c.Context) + Action: func(ctx context.Context, c *cli.Command) error { + pool := nostr.NewSimplePool(ctx) defer func() { pool.Relays.Range(func(_ string, relay *nostr.Relay) bool { @@ -36,7 +38,7 @@ var fetch = &cli.Command{ prefix, value, err := nip19.Decode(code) if err != nil { - lineProcessingError(c, "failed to decode: %s", err) + lineProcessingError(ctx, "failed to decode: %s", err) continue } @@ -75,7 +77,7 @@ var fetch = &cli.Command{ } if authorHint != "" { - relayList := sdk.FetchRelaysForPubkey(c.Context, pool, authorHint, + relayList := sdk.FetchRelaysForPubkey(ctx, pool, authorHint, "wss://purplepag.es", "wss://relay.damus.io", "wss://relay.noswhere.com", "wss://nos.lol", "wss://public.relaying.io", "wss://relay.nostr.band") for _, relayListItem := range relayList { @@ -86,16 +88,16 @@ var fetch = &cli.Command{ } if len(relays) == 0 { - lineProcessingError(c, "no relay hints found") + lineProcessingError(ctx, "no relay hints found") continue } - for ie := range pool.SubManyEose(c.Context, relays, nostr.Filters{filter}) { + for ie := range pool.SubManyEose(ctx, relays, nostr.Filters{filter}) { stdout(ie.Event) } } - exitIfLineProcessingError(c) + exitIfLineProcessingError(ctx) return nil }, } diff --git a/go.mod b/go.mod index 7537692..d1c7446 100644 --- a/go.mod +++ b/go.mod @@ -7,11 +7,12 @@ toolchain go1.21.0 require ( github.com/btcsuite/btcd/btcec/v2 v2.3.3 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 github.com/fatih/color v1.16.0 github.com/mailru/easyjson v0.7.7 github.com/nbd-wtf/go-nostr v0.31.2 github.com/nbd-wtf/nostr-sdk v0.0.5 - github.com/urfave/cli/v2 v2.25.7 + github.com/urfave/cli/v3 v3.0.0-alpha9 golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 ) @@ -20,9 +21,7 @@ require ( github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect github.com/chzyer/logex v1.1.10 // indirect github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect github.com/fiatjaf/eventstore v0.2.16 // indirect github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/pool v0.2.1 // indirect @@ -31,7 +30,6 @@ require ( github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/puzpuzpuz/xsync/v3 v3.1.0 // indirect - github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/tidwall/gjson v1.17.1 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect diff --git a/go.sum b/go.sum index 17ac72c..5b5786c 100644 --- a/go.sum +++ b/go.sum @@ -29,8 +29,6 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5O github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -96,8 +94,6 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/puzpuzpuz/xsync/v3 v3.1.0 h1:EewKT7/LNac5SLiEblJeUu8z5eERHrmRLnMQL2d7qX4= github.com/puzpuzpuz/xsync/v3 v3.1.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= -github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= @@ -110,8 +106,8 @@ github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JT github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= -github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs= -github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= +github.com/urfave/cli/v3 v3.0.0-alpha9 h1:P0RMy5fQm1AslQS+XCmy9UknDXctOmG/q/FZkUFnJSo= +github.com/urfave/cli/v3 v3.0.0-alpha9/go.mod h1:0kK/RUFHyh+yIKSfWxwheGndfnrvYSmYFVeKCh03ZUc= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= diff --git a/helpers.go b/helpers.go index 5e0cb9a..d6778db 100644 --- a/helpers.go +++ b/helpers.go @@ -16,7 +16,7 @@ import ( "github.com/nbd-wtf/go-nostr/nip19" "github.com/nbd-wtf/go-nostr/nip46" "github.com/nbd-wtf/go-nostr/nip49" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) const ( @@ -133,18 +133,18 @@ func connectToAllRelays( return pool, relays } -func lineProcessingError(c *cli.Context, msg string, args ...any) { - c.Context = context.WithValue(c.Context, LINE_PROCESSING_ERROR, true) +func lineProcessingError(ctx context.Context, msg string, args ...any) { + ctx = context.WithValue(ctx, LINE_PROCESSING_ERROR, true) log(msg+"\n", args...) } -func exitIfLineProcessingError(c *cli.Context) { - if val := c.Context.Value(LINE_PROCESSING_ERROR); val != nil && val.(bool) { +func exitIfLineProcessingError(ctx context.Context) { + if val := ctx.Value(LINE_PROCESSING_ERROR); val != nil && val.(bool) { os.Exit(123) } } -func gatherSecretKeyOrBunkerFromArguments(c *cli.Context) (string, *nip46.BunkerClient, error) { +func gatherSecretKeyOrBunkerFromArguments(ctx context.Context, c *cli.Command) (string, *nip46.BunkerClient, error) { var err error if bunkerURL := c.String("connect"); bunkerURL != "" { @@ -154,7 +154,7 @@ func gatherSecretKeyOrBunkerFromArguments(c *cli.Context) (string, *nip46.Bunker } else { clientKey = nostr.GeneratePrivateKey() } - bunker, err := nip46.ConnectBunker(c.Context, clientKey, bunkerURL, nil, func(s string) { + bunker, err := nip46.ConnectBunker(ctx, clientKey, bunkerURL, nil, func(s string) { fmt.Fprintf(color.Error, color.CyanString("[nip46]: open the following URL: %s"), s) }) return "", bunker, err diff --git a/key.go b/key.go index e14600c..0bb12cf 100644 --- a/key.go +++ b/key.go @@ -1,6 +1,7 @@ package main import ( + "context" "encoding/hex" "encoding/json" "fmt" @@ -13,14 +14,14 @@ import ( "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/nip19" "github.com/nbd-wtf/go-nostr/nip49" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) var key = &cli.Command{ Name: "key", Usage: "operations on secret keys: generate, derive, encrypt, decrypt.", Description: ``, - Subcommands: []*cli.Command{ + Commands: []*cli.Command{ generate, public, encrypt, @@ -33,7 +34,7 @@ var generate = &cli.Command{ Name: "generate", Usage: "generates a secret key", Description: ``, - Action: func(c *cli.Context) error { + Action: func(ctx context.Context, c *cli.Command) error { sec := nostr.GeneratePrivateKey() stdout(sec) return nil @@ -45,11 +46,11 @@ var public = &cli.Command{ Usage: "computes a public key from a secret key", Description: ``, ArgsUsage: "[secret]", - Action: func(c *cli.Context) error { - for sec := range getSecretKeysFromStdinLinesOrSlice(c, c.Args().Slice()) { + Action: func(ctx context.Context, c *cli.Command) error { + for sec := range getSecretKeysFromStdinLinesOrSlice(ctx, c, c.Args().Slice()) { pubkey, err := nostr.GetPublicKey(sec) if err != nil { - lineProcessingError(c, "failed to derive public key: %s", err) + lineProcessingError(ctx, "failed to derive public key: %s", err) continue } stdout(pubkey) @@ -71,7 +72,7 @@ var encrypt = &cli.Command{ DefaultText: "16", }, }, - Action: func(c *cli.Context) error { + Action: func(ctx context.Context, c *cli.Command) error { keys := make([]string, 0, 1) var password string switch c.Args().Len() { @@ -84,10 +85,10 @@ var encrypt = &cli.Command{ if password == "" { return fmt.Errorf("no password given") } - for sec := range getSecretKeysFromStdinLinesOrSlice(c, keys) { + for sec := range getSecretKeysFromStdinLinesOrSlice(ctx, c, keys) { ncryptsec, err := nip49.Encrypt(sec, password, uint8(c.Int("logn")), 0x02) if err != nil { - lineProcessingError(c, "failed to encrypt: %s", err) + lineProcessingError(ctx, "failed to encrypt: %s", err) continue } stdout(ncryptsec) @@ -101,7 +102,7 @@ var decrypt = &cli.Command{ Usage: "takes an ncrypsec and a password and decrypts it into an nsec", Description: `uses the NIP-49 standard.`, ArgsUsage: " ", - Action: func(c *cli.Context) error { + Action: func(ctx context.Context, c *cli.Command) error { var ncryptsec string var password string switch c.Args().Len() { @@ -132,7 +133,7 @@ var decrypt = &cli.Command{ for ncryptsec := range getStdinLinesOrArgumentsFromSlice([]string{ncryptsec}) { sec, err := nip49.Decrypt(ncryptsec, password) if err != nil { - lineProcessingError(c, "failed to decrypt: %s", err) + lineProcessingError(ctx, "failed to decrypt: %s", err) continue } nsec, _ := nip19.EncodePrivateKey(sec) @@ -153,7 +154,7 @@ var combine = &cli.Command{ However, if the intent is to check if two existing Nostr pubkeys match a given combined pubkey, then it might be sufficient to calculate the combined key for all the possible combinations of pubkeys in the input.`, ArgsUsage: "[pubkey...]", - Action: func(c *cli.Context) error { + Action: func(ctx context.Context, c *cli.Command) error { type Combination struct { Variants []string `json:"input_variants"` Output struct { @@ -253,7 +254,7 @@ However, if the intent is to check if two existing Nostr pubkeys match a given c }, } -func getSecretKeysFromStdinLinesOrSlice(c *cli.Context, keys []string) chan string { +func getSecretKeysFromStdinLinesOrSlice(ctx context.Context, c *cli.Command, keys []string) chan string { ch := make(chan string) go func() { for sec := range getStdinLinesOrArgumentsFromSlice(keys) { @@ -263,13 +264,13 @@ func getSecretKeysFromStdinLinesOrSlice(c *cli.Context, keys []string) chan stri if strings.HasPrefix(sec, "nsec1") { _, data, err := nip19.Decode(sec) if err != nil { - lineProcessingError(c, "invalid nsec code: %s", err) + lineProcessingError(ctx, "invalid nsec code: %s", err) continue } sec = data.(string) } if !nostr.IsValid32ByteHex(sec) { - lineProcessingError(c, "invalid hex key") + lineProcessingError(ctx, "invalid hex key") continue } ch <- sec diff --git a/main.go b/main.go index 1d0c194..ac761bc 100644 --- a/main.go +++ b/main.go @@ -1,14 +1,13 @@ package main import ( + "context" "os" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) -var q int - -var app = &cli.App{ +var app = &cli.Command{ Name: "nak", Suggest: true, UseShortOptionHandling: true, @@ -29,9 +28,9 @@ var app = &cli.App{ &cli.BoolFlag{ Name: "quiet", Usage: "do not print logs and info messages to stderr, use -qq to also not print anything to stdout", - Count: &q, Aliases: []string{"q"}, - Action: func(ctx *cli.Context, b bool) error { + Action: func(ctx context.Context, c *cli.Command, b bool) error { + q := c.Count("quiet") if q >= 1 { log = func(msg string, args ...any) {} if q >= 2 { @@ -45,7 +44,7 @@ var app = &cli.App{ } func main() { - if err := app.Run(os.Args); err != nil { + if err := app.Run(context.Background(), os.Args); err != nil { stdout(err) os.Exit(1) } diff --git a/relay.go b/relay.go index 8174873..0ff9f3d 100644 --- a/relay.go +++ b/relay.go @@ -1,12 +1,13 @@ package main import ( + "context" "encoding/json" "fmt" "strings" "github.com/nbd-wtf/go-nostr/nip11" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) var relay = &cli.Command{ @@ -15,7 +16,7 @@ var relay = &cli.Command{ Description: `example: nak relay nostr.wine`, ArgsUsage: "", - Action: func(c *cli.Context) error { + Action: func(ctx context.Context, c *cli.Command) error { for url := range getStdinLinesOrArguments(c.Args()) { if url == "" { return fmt.Errorf("specify the ") @@ -25,9 +26,9 @@ var relay = &cli.Command{ url = "wss://" + url } - info, err := nip11.Fetch(c.Context, url) + info, err := nip11.Fetch(ctx, url) if err != nil { - lineProcessingError(c, "failed to fetch '%s' information document: %w", url, err) + lineProcessingError(ctx, "failed to fetch '%s' information document: %w", url, err) continue } diff --git a/req.go b/req.go index 39d0def..3ab5295 100644 --- a/req.go +++ b/req.go @@ -1,6 +1,7 @@ package main import ( + "context" "encoding/json" "fmt" "os" @@ -9,7 +10,7 @@ import ( "github.com/mailru/easyjson" "github.com/nbd-wtf/go-nostr" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) const CATEGORY_FILTER_ATTRIBUTES = "FILTER ATTRIBUTES" @@ -124,23 +125,23 @@ example: }, }, ArgsUsage: "[relay...]", - Action: func(c *cli.Context) error { + Action: func(ctx context.Context, c *cli.Command) error { var pool *nostr.SimplePool relayUrls := c.Args().Slice() if len(relayUrls) > 0 { var relays []*nostr.Relay - pool, relays = connectToAllRelays(c.Context, relayUrls, nostr.WithAuthHandler(func(evt *nostr.Event) error { + pool, relays = connectToAllRelays(ctx, relayUrls, nostr.WithAuthHandler(func(evt *nostr.Event) error { if !c.Bool("auth") { return fmt.Errorf("auth not authorized") } - sec, bunker, err := gatherSecretKeyOrBunkerFromArguments(c) + sec, bunker, err := gatherSecretKeyOrBunkerFromArguments(ctx, c) if err != nil { return err } var pk string if bunker != nil { - pk, err = bunker.GetPublicKey(c.Context) + pk, err = bunker.GetPublicKey(ctx) if err != nil { return fmt.Errorf("failed to get public key from bunker: %w", err) } @@ -150,7 +151,7 @@ example: log("performing auth as %s...\n", pk) if bunker != nil { - return bunker.SignEvent(c.Context, evt) + return bunker.SignEvent(ctx, evt) } else { return evt.Sign(sec) } @@ -175,7 +176,7 @@ example: filter := nostr.Filter{} if stdinFilter != "" { if err := easyjson.Unmarshal([]byte(stdinFilter), &filter); err != nil { - lineProcessingError(c, "invalid filter '%s' received from stdin: %s", stdinFilter, err) + lineProcessingError(ctx, "invalid filter '%s' received from stdin: %s", stdinFilter, err) continue } } @@ -186,8 +187,8 @@ example: if ids := c.StringSlice("id"); len(ids) > 0 { filter.IDs = append(filter.IDs, ids...) } - if kinds := c.IntSlice("kind"); len(kinds) > 0 { - filter.Kinds = append(filter.Kinds, kinds...) + for _, kind64 := range c.IntSlice("kind") { + filter.Kinds = append(filter.Kinds, int(kind64)) } if search := c.String("search"); search != "" { filter.Search = search @@ -245,7 +246,7 @@ example: } } if limit := c.Int("limit"); limit != 0 { - filter.Limit = limit + filter.Limit = int(limit) } else if c.IsSet("limit") || c.Bool("stream") { filter.LimitZero = true } @@ -255,7 +256,7 @@ example: if c.Bool("stream") { fn = pool.SubMany } - for ie := range fn(c.Context, relayUrls, nostr.Filters{filter}) { + for ie := range fn(ctx, relayUrls, nostr.Filters{filter}) { stdout(ie.Event) } } else { @@ -272,7 +273,7 @@ example: } } - exitIfLineProcessingError(c) + exitIfLineProcessingError(ctx) return nil }, } diff --git a/verify.go b/verify.go index 39da863..1857fd0 100644 --- a/verify.go +++ b/verify.go @@ -1,10 +1,11 @@ package main import ( + "context" "encoding/json" "github.com/nbd-wtf/go-nostr" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) var verify = &cli.Command{ @@ -14,28 +15,28 @@ var verify = &cli.Command{ echo '{"id":"a889df6a387419ff204305f4c2d296ee328c3cd4f8b62f205648a541b4554dfb","pubkey":"c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5","created_at":1698623783,"kind":1,"tags":[],"content":"hello from the nostr army knife","sig":"84876e1ee3e726da84e5d195eb79358b2b3eaa4d9bd38456fde3e8a2af3f1cd4cda23f23fda454869975b3688797d4c66e12f4c51c1b43c6d2997c5e61865661"}' | nak verify it outputs nothing if the verification is successful.`, - Action: func(c *cli.Context) error { + Action: func(ctx context.Context, c *cli.Command) error { for stdinEvent := range getStdinLinesOrArguments(c.Args()) { evt := nostr.Event{} if stdinEvent != "" { if err := json.Unmarshal([]byte(stdinEvent), &evt); err != nil { - lineProcessingError(c, "invalid event: %s", err) + lineProcessingError(ctx, "invalid event: %s", err) continue } } if evt.GetID() != evt.ID { - lineProcessingError(c, "invalid .id, expected %s, got %s", evt.GetID(), evt.ID) + lineProcessingError(ctx, "invalid .id, expected %s, got %s", evt.GetID(), evt.ID) continue } if ok, err := evt.CheckSignature(); !ok { - lineProcessingError(c, "invalid signature: %s", err) + lineProcessingError(ctx, "invalid signature: %s", err) continue } } - exitIfLineProcessingError(c) + exitIfLineProcessingError(ctx) return nil }, } From 9a4145020913d4c6c91147873b97086ed8789f27 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 25 Jun 2024 23:23:51 -0300 Subject: [PATCH 105/401] use modified cli library that accepts flags after arguments. --- go.mod | 3 ++- go.sum | 6 ++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index d1c7446..34161ea 100644 --- a/go.mod +++ b/go.mod @@ -33,8 +33,9 @@ require ( github.com/tidwall/gjson v1.17.1 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect - github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect golang.org/x/crypto v0.7.0 // indirect golang.org/x/sys v0.20.0 // indirect golang.org/x/text v0.8.0 // indirect ) + +replace github.com/urfave/cli/v3 => github.com/fiatjaf/cli/v3 v3.0.0-20240626022047-0fc2565ea728 diff --git a/go.sum b/go.sum index 5b5786c..d6d58fa 100644 --- a/go.sum +++ b/go.sum @@ -42,6 +42,8 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3 github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= 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/cli/v3 v3.0.0-20240626022047-0fc2565ea728 h1:MgEQQSCPDkpILGlJKCj/Gj0l8XwYFpBKYATPJC14ros= +github.com/fiatjaf/cli/v3 v3.0.0-20240626022047-0fc2565ea728/go.mod h1:Z1ItyMma7t6I7zHG9OpbExhHQOSkFf/96n+mAZ9MtVI= github.com/fiatjaf/eventstore v0.2.16 h1:NR64mnyUT5nJR8Sj2AwJTd1Hqs5kKJcCFO21ggUkvWg= github.com/fiatjaf/eventstore v0.2.16/go.mod h1:rUc1KhVufVmC+HUOiuPweGAcvG6lEOQCkRCn2Xn5VRA= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= @@ -106,10 +108,6 @@ github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JT github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= -github.com/urfave/cli/v3 v3.0.0-alpha9 h1:P0RMy5fQm1AslQS+XCmy9UknDXctOmG/q/FZkUFnJSo= -github.com/urfave/cli/v3 v3.0.0-alpha9/go.mod h1:0kK/RUFHyh+yIKSfWxwheGndfnrvYSmYFVeKCh03ZUc= -github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= -github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= From 441ee9a5ededabd2b83653f2e8ebcd0a32395864 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Fri, 5 Jul 2024 00:11:59 -0300 Subject: [PATCH 106/401] update go-nostr so just "localhost[:port]" works as a relay url. --- go.mod | 2 +- go.sum | 4 ++-- relay.go | 5 ----- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/go.mod b/go.mod index 34161ea..b81a30c 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 github.com/fatih/color v1.16.0 github.com/mailru/easyjson v0.7.7 - github.com/nbd-wtf/go-nostr v0.31.2 + github.com/nbd-wtf/go-nostr v0.34.0 github.com/nbd-wtf/nostr-sdk v0.0.5 github.com/urfave/cli/v3 v3.0.0-alpha9 golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 diff --git a/go.sum b/go.sum index d6d58fa..767e47c 100644 --- a/go.sum +++ b/go.sum @@ -79,8 +79,8 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/nbd-wtf/go-nostr v0.31.2 h1:PkHCAsSzG0Ce8tfF7LKyvZOjYtCdC+hPh5KfO/Rl1b4= -github.com/nbd-wtf/go-nostr v0.31.2/go.mod h1:vHKtHyLXDXzYBN0fi/9Y/Q5AD0p+hk8TQVKlldAi0gI= +github.com/nbd-wtf/go-nostr v0.34.0 h1:E7tDHFx42gvWwFv1Eysn+NxJqGLmo21x/VEwj2+F21E= +github.com/nbd-wtf/go-nostr v0.34.0/go.mod h1:NZQkxl96ggbO8rvDpVjcsojJqKTPwqhP4i82O7K5DJs= github.com/nbd-wtf/nostr-sdk v0.0.5 h1:rec+FcDizDVO0W25PX0lgYMXvP7zNNOgI3Fu9UCm4BY= github.com/nbd-wtf/nostr-sdk v0.0.5/go.mod h1:iJJsikesCGLNFZ9dLqhLPDzdt924EagUmdQxT3w2Lmk= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= diff --git a/relay.go b/relay.go index 0ff9f3d..d0e9e3a 100644 --- a/relay.go +++ b/relay.go @@ -4,7 +4,6 @@ import ( "context" "encoding/json" "fmt" - "strings" "github.com/nbd-wtf/go-nostr/nip11" "github.com/urfave/cli/v3" @@ -22,10 +21,6 @@ var relay = &cli.Command{ return fmt.Errorf("specify the ") } - if !strings.HasPrefix(url, "wss://") && !strings.HasPrefix(url, "ws://") { - url = "wss://" + url - } - info, err := nip11.Fetch(ctx, url) if err != nil { lineProcessingError(ctx, "failed to fetch '%s' information document: %w", url, err) From ac00c5065ff79f5cc1cba61a7531cba7e0a99c11 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Wed, 10 Jul 2024 14:48:02 -0300 Subject: [PATCH 107/401] nak req: --force-pre-auth flag. --- bunker.go | 2 +- event.go | 2 +- helpers.go | 36 ++++++++++++++++++++++++++++++++++++ req.go | 12 +++++++++--- 4 files changed, 47 insertions(+), 5 deletions(-) diff --git a/bunker.go b/bunker.go index 64d2d4e..4732af5 100644 --- a/bunker.go +++ b/bunker.go @@ -50,7 +50,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, relayUrls) + _, relays := connectToAllRelays(ctx, relayUrls, false) if len(relays) == 0 { log("failed to connect to any of the given relays.\n") os.Exit(3) diff --git a/event.go b/event.go index 6e6b225..9fc3f4d 100644 --- a/event.go +++ b/event.go @@ -145,7 +145,7 @@ example: // try to connect to the relays here var relays []*nostr.Relay if relayUrls := c.Args().Slice(); len(relayUrls) > 0 { - _, relays = connectToAllRelays(ctx, relayUrls) + _, relays = connectToAllRelays(ctx, relayUrls, false) if len(relays) == 0 { log("failed to connect to any of the given relays.\n") os.Exit(3) diff --git a/helpers.go b/helpers.go index d6778db..673165a 100644 --- a/helpers.go +++ b/helpers.go @@ -9,6 +9,7 @@ import ( "net/url" "os" "strings" + "time" "github.com/chzyer/readline" "github.com/fatih/color" @@ -117,13 +118,48 @@ func normalizeAndValidateRelayURLs(wsurls []string) error { func connectToAllRelays( ctx context.Context, relayUrls []string, + forcePreAuth bool, opts ...nostr.PoolOption, ) (*nostr.SimplePool, []*nostr.Relay) { relays := make([]*nostr.Relay, 0, len(relayUrls)) pool := nostr.NewSimplePool(ctx, opts...) +relayLoop: for _, url := range relayUrls { log("connecting to %s... ", url) if relay, err := pool.EnsureRelay(url); err == nil { + if forcePreAuth { + log("waiting for auth challenge... ") + signer := opts[0].(nostr.WithAuthHandler) + time.Sleep(time.Millisecond * 200) + challengeWaitLoop: + for { + // beginhack + // here starts the biggest and ugliest hack of this codebase + if err := relay.Auth(ctx, func(authEvent *nostr.Event) error { + challengeTag := authEvent.Tags.GetFirst([]string{"challenge", ""}) + if (*challengeTag)[1] == "" { + return fmt.Errorf("auth not received yet *****") + } + return signer(authEvent) + }); err == nil { + // auth succeeded + break challengeWaitLoop + } else { + // auth failed + if strings.HasSuffix(err.Error(), "auth not received yet *****") { + // it failed because we didn't receive the challenge yet, so keep waiting + time.Sleep(time.Second) + continue challengeWaitLoop + } else { + // it failed for some other reason, so skip this relay + log(err.Error() + "\n") + continue relayLoop + } + } + // endhack + } + } + relays = append(relays, relay) log("ok.\n") } else { diff --git a/req.go b/req.go index 3ab5295..b987866 100644 --- a/req.go +++ b/req.go @@ -104,6 +104,11 @@ example: Name: "auth", Usage: "always perform NIP-42 \"AUTH\" when facing an \"auth-required: \" rejection and try again", }, + &cli.BoolFlag{ + Name: "force-pre-auth", + Aliases: []string{"fpa"}, + Usage: "after connecting, for a NIP-42 \"AUTH\" message to be received, act on it and only then send the \"REQ\"", + }, &cli.StringFlag{ Name: "sec", Usage: "secret key to sign the AUTH challenge, as hex or nsec", @@ -127,11 +132,12 @@ example: ArgsUsage: "[relay...]", Action: func(ctx context.Context, c *cli.Command) error { var pool *nostr.SimplePool + relayUrls := c.Args().Slice() if len(relayUrls) > 0 { var relays []*nostr.Relay - pool, relays = connectToAllRelays(ctx, relayUrls, nostr.WithAuthHandler(func(evt *nostr.Event) error { - if !c.Bool("auth") { + pool, relays = connectToAllRelays(ctx, relayUrls, c.Bool("force-pre-auth"), nostr.WithAuthHandler(func(evt *nostr.Event) error { + if !c.Bool("auth") && !c.Bool("force-pre-auth") { return fmt.Errorf("auth not authorized") } sec, bunker, err := gatherSecretKeyOrBunkerFromArguments(ctx, c) @@ -148,7 +154,7 @@ example: } else { pk, _ = nostr.GetPublicKey(sec) } - log("performing auth as %s...\n", pk) + log("performing auth as %s... ", pk) if bunker != nil { return bunker.SignEvent(ctx, evt) From 2ca6bb094050ad317ce5ae1499ce3c8ed2c1838d Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Wed, 10 Jul 2024 14:48:18 -0300 Subject: [PATCH 108/401] update tests so they can run again (but they're not working). --- example_test.go | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/example_test.go b/example_test.go index 89fd4d0..9b6cd28 100644 --- a/example_test.go +++ b/example_test.go @@ -1,9 +1,14 @@ package main -import "os" +import ( + "context" + "os" +) + +var ctx = context.Background() func ExampleEventBasic() { - app.Run([]string{"nak", "event", "--ts", "1699485669"}) + app.Run(ctx, []string{"nak", "event", "--ts", "1699485669"}) // Output: // {"kind":1,"id":"36d88cf5fcc449f2390a424907023eda7a74278120eebab8d02797cd92e7e29c","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1699485669,"tags":[],"content":"hello from the nostr army knife","sig":"68e71a192e8abcf8582a222434ac823ecc50607450ebe8cc4c145eb047794cc382dc3f888ce879d2f404f5ba6085a47601360a0fa2dd4b50d317bd0c6197c2c2"} } @@ -15,22 +20,22 @@ func ExampleEventParsingFromStdin() { r, w, _ := os.Pipe() os.Stdin = r w.WriteString("{\"content\":\"hello world\"}\n{\"content\":\"hello sun\"}\n") - app.Run([]string{"nak", "event", "-t", "t=spam", "--ts", "1699485669"}) + app.Run(ctx, []string{"nak", "event", "-t", "t=spam", "--ts", "1699485669"}) // Output: // {"id":"bda134f9077c11973afe6aa5a1cc6f5bcea01c40d318b8f91dcb8e50507cfa52","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1699485669,"kind":1,"tags":[["t","spam"]],"content":"hello world","sig":"7552454bb8e7944230142634e3e34ac7468bad9b21ed6909da572c611018dff1d14d0792e98b5806f6330edc51e09efa6d0b66a9694dc34606c70f4e580e7493"} // {"id":"879c36ec73acca288825b53585389581d3836e7f0fe4d46e5eba237ca56d6af5","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1699485669,"kind":1,"tags":[["t","spam"]],"content":"hello sun","sig":"6c7e6b13ebdf931d26acfdd00bec2ec1140ddaf8d1ed61453543a14e729a460fe36c40c488ccb194a0e1ab9511cb6c36741485f501bdb93c39ca4c51bc59cbd4"} } func ExampleEventComplex() { - app.Run([]string{"nak", "event", "--ts", "1699485669", "-k", "11", "-c", "skjdbaskd", "--sec", "17", "-t", "t=spam", "-e", "36d88cf5fcc449f2390a424907023eda7a74278120eebab8d02797cd92e7e29c", "-t", "r=https://abc.def?name=foobar;nothing"}) + app.Run(ctx, []string{"nak", "event", "--ts", "1699485669", "-k", "11", "-c", "skjdbaskd", "--sec", "17", "-t", "t=spam", "-e", "36d88cf5fcc449f2390a424907023eda7a74278120eebab8d02797cd92e7e29c", "-t", "r=https://abc.def?name=foobar;nothing"}) // Output: // {"id":"19aba166dcf354bf5ef64f4afe69ada1eb851495001ee05e07d393ee8c8ea179","pubkey":"2fa2104d6b38d11b0230010559879124e42ab8dfeff5ff29dc9cdadd4ecacc3f","created_at":1699485669,"kind":11,"tags":[["t","spam"],["r","https://abc.def?name=foobar","nothing"],["e","36d88cf5fcc449f2390a424907023eda7a74278120eebab8d02797cd92e7e29c"]],"content":"skjdbaskd","sig":"cf452def4a68341c897c3fc96fa34dc6895a5b8cc266d4c041bcdf758ec992ec5adb8b0179e98552aaaf9450526a26d7e62e413b15b1c57e0cfc8db6b29215d7"} } func ExampleEncode() { - app.Run([]string{"nak", "encode", "npub", "a6a67ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179822"}) - app.Run([]string{"nak", "encode", "nprofile", "-r", "wss://example.com", "a6a67ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179822"}) - app.Run([]string{"nak", "encode", "nprofile", "-r", "wss://example.com", "a6a67ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179822", "a5592173975ded9f836a9572ea8b11a7e16ceb66464d66d50b27163f7f039d2c"}) + app.Run(ctx, []string{"nak", "encode", "npub", "a6a67ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179822"}) + app.Run(ctx, []string{"nak", "encode", "nprofile", "-r", "wss://example.com", "a6a67ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179822"}) + app.Run(ctx, []string{"nak", "encode", "nprofile", "-r", "wss://example.com", "a6a67ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179822", "a5592173975ded9f836a9572ea8b11a7e16ceb66464d66d50b27163f7f039d2c"}) // npub156n8a7wuhwk9tgrzjh8gwzc8q2dlekedec5djk0js9d3d7qhnq3qjpdq28 // nprofile1qqs2dfn7l8wthtz45p3ftn58pvrs9xlumvkuu2xet8egzkcklqtesgspz9mhxue69uhk27rpd4cxcefwvdhk6fl5jug // nprofile1qqs2dfn7l8wthtz45p3ftn58pvrs9xlumvkuu2xet8egzkcklqtesgspz9mhxue69uhk27rpd4cxcefwvdhk6fl5jug @@ -38,7 +43,7 @@ func ExampleEncode() { } func ExampleDecode() { - app.Run([]string{"nak", "decode", "naddr1qqyrgcmyxe3kvefhqyxhwumn8ghj7mn0wvhxcmmvqgs9kqvr4dkruv3t7n2pc6e6a7v9v2s5fprmwjv4gde8c4fe5y29v0srqsqqql9ngrt6tu", "nevent1qyd8wumn8ghj7urewfsk66ty9enxjct5dfskvtnrdakj7qgmwaehxw309aex2mrp0yh8wetnw3jhymnzw33jucm0d5hszxthwden5te0wfjkccte9eekummjwsh8xmmrd9skctcpzamhxue69uhkzarvv9ejumn0wd68ytnvv9hxgtcqyqllp5v5j0nxr74fptqxkhvfv0h3uj870qpk3ln8a58agyxl3fka296ewr8"}) + app.Run(ctx, []string{"nak", "decode", "naddr1qqyrgcmyxe3kvefhqyxhwumn8ghj7mn0wvhxcmmvqgs9kqvr4dkruv3t7n2pc6e6a7v9v2s5fprmwjv4gde8c4fe5y29v0srqsqqql9ngrt6tu", "nevent1qyd8wumn8ghj7urewfsk66ty9enxjct5dfskvtnrdakj7qgmwaehxw309aex2mrp0yh8wetnw3jhymnzw33jucm0d5hszxthwden5te0wfjkccte9eekummjwsh8xmmrd9skctcpzamhxue69uhkzarvv9ejumn0wd68ytnvv9hxgtcqyqllp5v5j0nxr74fptqxkhvfv0h3uj870qpk3ln8a58agyxl3fka296ewr8"}) // Output: // { // "pubkey": "5b0183ab6c3e322bf4d41c6b3aef98562a144847b7499543727c5539a114563e", @@ -60,39 +65,39 @@ func ExampleDecode() { } func ExampleReq() { - app.Run([]string{"nak", "req", "-k", "1", "-l", "18", "-a", "2fa2104d6b38d11b0230010559879124e42ab8dfeff5ff29dc9cdadd4ecacc3f", "-e", "aec4de6d051a7c2b6ca2d087903d42051a31e07fb742f1240970084822de10a6"}) + app.Run(ctx, []string{"nak", "req", "-k", "1", "-l", "18", "-a", "2fa2104d6b38d11b0230010559879124e42ab8dfeff5ff29dc9cdadd4ecacc3f", "-e", "aec4de6d051a7c2b6ca2d087903d42051a31e07fb742f1240970084822de10a6"}) // Output: // ["REQ","nak",{"kinds":[1],"authors":["2fa2104d6b38d11b0230010559879124e42ab8dfeff5ff29dc9cdadd4ecacc3f"],"limit":18,"#e":["aec4de6d051a7c2b6ca2d087903d42051a31e07fb742f1240970084822de10a6"]}] } func ExampleReqIdFromRelay() { - app.Run([]string{"nak", "req", "-i", "3ff0d19493e661faa90ac06b5d8963ef1e48fe780368fe67ed0fd410df8a6dd5", "wss://nostr.wine"}) + app.Run(ctx, []string{"nak", "req", "-i", "3ff0d19493e661faa90ac06b5d8963ef1e48fe780368fe67ed0fd410df8a6dd5", "wss://nostr.wine"}) // Output: // {"id":"3ff0d19493e661faa90ac06b5d8963ef1e48fe780368fe67ed0fd410df8a6dd5","pubkey":"3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d","created_at":1710759386,"kind":1,"tags":[],"content":"Nostr was coopted by our the corporate overlords. It is now featured in https://www.iana.org/assignments/well-known-uris/well-known-uris.xhtml.","sig":"faaec167cca4de50b562b7702e8854e2023f0ccd5f36d1b95b6eac20d352206342d6987e9516d283068c768e94dbe8858e2990c3e05405e707fb6fb771ef92f9"} } func ExampleMultipleFetch() { - app.Run([]string{"nak", "fetch", "naddr1qqyrgcmyxe3kvefhqyxhwumn8ghj7mn0wvhxcmmvqgs9kqvr4dkruv3t7n2pc6e6a7v9v2s5fprmwjv4gde8c4fe5y29v0srqsqqql9ngrt6tu", "nevent1qyd8wumn8ghj7urewfsk66ty9enxjct5dfskvtnrdakj7qgmwaehxw309aex2mrp0yh8wetnw3jhymnzw33jucm0d5hszxthwden5te0wfjkccte9eekummjwsh8xmmrd9skctcpzamhxue69uhkzarvv9ejumn0wd68ytnvv9hxgtcqyqllp5v5j0nxr74fptqxkhvfv0h3uj870qpk3ln8a58agyxl3fka296ewr8"}) + app.Run(ctx, []string{"nak", "fetch", "naddr1qqyrgcmyxe3kvefhqyxhwumn8ghj7mn0wvhxcmmvqgs9kqvr4dkruv3t7n2pc6e6a7v9v2s5fprmwjv4gde8c4fe5y29v0srqsqqql9ngrt6tu", "nevent1qyd8wumn8ghj7urewfsk66ty9enxjct5dfskvtnrdakj7qgmwaehxw309aex2mrp0yh8wetnw3jhymnzw33jucm0d5hszxthwden5te0wfjkccte9eekummjwsh8xmmrd9skctcpzamhxue69uhkzarvv9ejumn0wd68ytnvv9hxgtcqyqllp5v5j0nxr74fptqxkhvfv0h3uj870qpk3ln8a58agyxl3fka296ewr8"}) // Output: // {"id":"9ae5014573fc75ced00b343868d2cd9343ebcbbae50591c6fa8ae1cd99568f05","pubkey":"5b0183ab6c3e322bf4d41c6b3aef98562a144847b7499543727c5539a114563e","created_at":1707764605,"kind":31923,"tags":[["d","4cd6cfe7"],["name","Nostr PHX Presents Culture Shock"],["description","Nostr PHX presents Culture Shock the first Value 4 Value Cultural Event in Downtown Phoenix. We will showcase the power of Nostr + Bitcoin / Lightning with a full day of education, food, drinks, conversation, vendors and best of all, a live convert which will stream globally for the world to zap. "],["start","1708185600"],["end","1708228800"],["start_tzid","America/Phoenix"],["p","5b0183ab6c3e322bf4d41c6b3aef98562a144847b7499543727c5539a114563e","","host"],["location","Hello Merch, 850 W Lincoln St, Phoenix, AZ 85007, USA","Hello Merch","850 W Lincoln St, Phoenix, AZ 85007, USA"],["address","Hello Merch, 850 W Lincoln St, Phoenix, AZ 85007, USA","Hello Merch","850 W Lincoln St, Phoenix, AZ 85007, USA"],["g","9tbq1rzn"],["image","https://flockstr.s3.amazonaws.com/event/15vSaiscDhVH1KBXhA0i8"],["about","Nostr PHX presents Culture Shock : the first Value 4 Value Cultural Event in Downtown Phoenix. We will showcase the power of Nostr + Bitcoin / Lightning with a full day of education, conversation, food and goods which will be capped off with a live concert streamed globally for the world to boost \u0026 zap. \n\nWe strive to source local vendors, local artists, local partnerships. Please reach out to us if you are interested in participating in this historic event. "],["calendar","31924:5b0183ab6c3e322bf4d41c6b3aef98562a144847b7499543727c5539a114563e:1f238c94"]],"content":"Nostr PHX presents Culture Shock : the first Value 4 Value Cultural Event in Downtown Phoenix. We will showcase the power of Nostr + Bitcoin / Lightning with a full day of education, conversation, food and goods which will be capped off with a live concert streamed globally for the world to boost \u0026 zap. \n\nWe strive to source local vendors, local artists, local partnerships. Please reach out to us if you are interested in participating in this historic event. ","sig":"f676629d1414d96b464644de6babde0c96bd21ef9b41ba69ad886a1d13a942b855b715b22ccf38bc07fead18d3bdeee82d9e3825cf6f003fb5ff1766d95c70a0"} // {"id":"3ff0d19493e661faa90ac06b5d8963ef1e48fe780368fe67ed0fd410df8a6dd5","pubkey":"3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d","created_at":1710759386,"kind":1,"tags":[],"content":"Nostr was coopted by our the corporate overlords. It is now featured in https://www.iana.org/assignments/well-known-uris/well-known-uris.xhtml.","sig":"faaec167cca4de50b562b7702e8854e2023f0ccd5f36d1b95b6eac20d352206342d6987e9516d283068c768e94dbe8858e2990c3e05405e707fb6fb771ef92f9"} } func ExampleKeyPublic() { - app.Run([]string{"nak", "key", "public", "3ff0d19493e661faa90ac06b5d8963ef1e48fe780368fe67ed0fd410df8a6dd5", "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"}) + app.Run(ctx, []string{"nak", "key", "public", "3ff0d19493e661faa90ac06b5d8963ef1e48fe780368fe67ed0fd410df8a6dd5", "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"}) // Output: // 70f7120d065870513a6bddb61c8d400ad1e43449b1900ffdb5551e4c421375c8 // 718d756f60cf5179ef35b39dc6db3ff58f04c0734f81f6d4410f0b047ddf9029 } func ExampleKeyDecrypt() { - app.Run([]string{"nak", "key", "decrypt", "ncryptsec1qggfep0m5ythsegkmwfrhhx2zx5gazyhdygvlngcds4wsgdpzfy6nr0exy0pdk0ydwrqyhndt2trtwcgwwag0ja3aqclzptfxxqvprdyaz3qfrmazpecx2ff6dph5mfdjnh5sw8sgecul32eru6xet34", "banana"}) + app.Run(ctx, []string{"nak", "key", "decrypt", "ncryptsec1qggfep0m5ythsegkmwfrhhx2zx5gazyhdygvlngcds4wsgdpzfy6nr0exy0pdk0ydwrqyhndt2trtwcgwwag0ja3aqclzptfxxqvprdyaz3qfrmazpecx2ff6dph5mfdjnh5sw8sgecul32eru6xet34", "banana"}) // Output: // nsec180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsgyumg0 } func ExampleRelay() { - app.Run([]string{"nak", "relay", "relay.nos.social", "pyramid.fiatjaf.com"}) + app.Run(ctx, []string{"nak", "relay", "relay.nos.social", "pyramid.fiatjaf.com"}) // Output: // { // "name": "nos.social strfry relay", From 316d94166eeff1f8923411cd564a93ad5a688457 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Thu, 11 Jul 2024 15:33:19 -0300 Subject: [PATCH 109/401] fix lineProcessingError() -- it wasn't returning a ctx so it was a noop. --- decode.go | 4 ++-- encode.go | 12 ++++++------ event.go | 2 +- fetch.go | 4 ++-- helpers.go | 4 ++-- key.go | 10 +++++----- relay.go | 2 +- req.go | 2 +- verify.go | 6 +++--- 9 files changed, 23 insertions(+), 23 deletions(-) diff --git a/decode.go b/decode.go index 9c9d8be..734041a 100644 --- a/decode.go +++ b/decode.go @@ -50,7 +50,7 @@ var decode = &cli.Command{ decodeResult.HexResult.PrivateKey = hex.EncodeToString(b) decodeResult.HexResult.PublicKey = hex.EncodeToString(b) } else { - lineProcessingError(ctx, "hex string with invalid number of bytes: %d", len(b)) + ctx = lineProcessingError(ctx, "hex string with invalid number of bytes: %d", len(b)) continue } } else if evp := sdk.InputToEventPointer(input); evp != nil { @@ -64,7 +64,7 @@ var decode = &cli.Command{ decodeResult.PrivateKey.PrivateKey = value.(string) decodeResult.PrivateKey.PublicKey, _ = nostr.GetPublicKey(value.(string)) } else { - lineProcessingError(ctx, "couldn't decode input '%s': %s", input, err) + ctx = lineProcessingError(ctx, "couldn't decode input '%s': %s", input, err) continue } diff --git a/encode.go b/encode.go index 4eaf3ac..8e86e90 100644 --- a/encode.go +++ b/encode.go @@ -32,7 +32,7 @@ var encode = &cli.Command{ Action: func(ctx context.Context, c *cli.Command) error { for target := range getStdinLinesOrArguments(c.Args()) { if ok := nostr.IsValidPublicKey(target); !ok { - lineProcessingError(ctx, "invalid public key: %s", target) + ctx = lineProcessingError(ctx, "invalid public key: %s", target) continue } @@ -53,7 +53,7 @@ var encode = &cli.Command{ Action: func(ctx context.Context, c *cli.Command) error { for target := range getStdinLinesOrArguments(c.Args()) { if ok := nostr.IsValid32ByteHex(target); !ok { - lineProcessingError(ctx, "invalid private key: %s", target) + ctx = lineProcessingError(ctx, "invalid private key: %s", target) continue } @@ -81,7 +81,7 @@ var encode = &cli.Command{ Action: func(ctx context.Context, c *cli.Command) error { for target := range getStdinLinesOrArguments(c.Args()) { if ok := nostr.IsValid32ByteHex(target); !ok { - lineProcessingError(ctx, "invalid public key: %s", target) + ctx = lineProcessingError(ctx, "invalid public key: %s", target) continue } @@ -119,7 +119,7 @@ var encode = &cli.Command{ Action: func(ctx context.Context, c *cli.Command) error { for target := range getStdinLinesOrArguments(c.Args()) { if ok := nostr.IsValid32ByteHex(target); !ok { - lineProcessingError(ctx, "invalid event id: %s", target) + ctx = lineProcessingError(ctx, "invalid event id: %s", target) continue } @@ -189,7 +189,7 @@ var encode = &cli.Command{ if d == "" { d = c.String("identifier") if d == "" { - lineProcessingError(ctx, "\"d\" tag identifier can't be empty") + ctx = lineProcessingError(ctx, "\"d\" tag identifier can't be empty") continue } } @@ -216,7 +216,7 @@ var encode = &cli.Command{ Action: func(ctx context.Context, c *cli.Command) error { for target := range getStdinLinesOrArguments(c.Args()) { if ok := nostr.IsValid32ByteHex(target); !ok { - lineProcessingError(ctx, "invalid event id: %s", target) + ctx = lineProcessingError(ctx, "invalid event id: %s", target) continue } diff --git a/event.go b/event.go index 9fc3f4d..c1b3bfa 100644 --- a/event.go +++ b/event.go @@ -176,7 +176,7 @@ example: if stdinEvent != "" { if err := easyjson.Unmarshal([]byte(stdinEvent), &evt); err != nil { - lineProcessingError(ctx, "invalid event received from stdin: %s", err) + ctx = lineProcessingError(ctx, "invalid event received from stdin: %s", err) continue } kindWasSupplied = strings.Contains(stdinEvent, `"kind"`) diff --git a/fetch.go b/fetch.go index 0a59108..b71fd9a 100644 --- a/fetch.go +++ b/fetch.go @@ -38,7 +38,7 @@ var fetch = &cli.Command{ prefix, value, err := nip19.Decode(code) if err != nil { - lineProcessingError(ctx, "failed to decode: %s", err) + ctx = lineProcessingError(ctx, "failed to decode: %s", err) continue } @@ -88,7 +88,7 @@ var fetch = &cli.Command{ } if len(relays) == 0 { - lineProcessingError(ctx, "no relay hints found") + ctx = lineProcessingError(ctx, "no relay hints found") continue } diff --git a/helpers.go b/helpers.go index 673165a..52da7d7 100644 --- a/helpers.go +++ b/helpers.go @@ -169,9 +169,9 @@ relayLoop: return pool, relays } -func lineProcessingError(ctx context.Context, msg string, args ...any) { - ctx = context.WithValue(ctx, LINE_PROCESSING_ERROR, true) +func lineProcessingError(ctx context.Context, msg string, args ...any) context.Context { log(msg+"\n", args...) + return context.WithValue(ctx, LINE_PROCESSING_ERROR, true) } func exitIfLineProcessingError(ctx context.Context) { diff --git a/key.go b/key.go index 0bb12cf..f885b14 100644 --- a/key.go +++ b/key.go @@ -50,7 +50,7 @@ var public = &cli.Command{ for sec := range getSecretKeysFromStdinLinesOrSlice(ctx, c, c.Args().Slice()) { pubkey, err := nostr.GetPublicKey(sec) if err != nil { - lineProcessingError(ctx, "failed to derive public key: %s", err) + ctx = lineProcessingError(ctx, "failed to derive public key: %s", err) continue } stdout(pubkey) @@ -88,7 +88,7 @@ var encrypt = &cli.Command{ for sec := range getSecretKeysFromStdinLinesOrSlice(ctx, c, keys) { ncryptsec, err := nip49.Encrypt(sec, password, uint8(c.Int("logn")), 0x02) if err != nil { - lineProcessingError(ctx, "failed to encrypt: %s", err) + ctx = lineProcessingError(ctx, "failed to encrypt: %s", err) continue } stdout(ncryptsec) @@ -133,7 +133,7 @@ var decrypt = &cli.Command{ for ncryptsec := range getStdinLinesOrArgumentsFromSlice([]string{ncryptsec}) { sec, err := nip49.Decrypt(ncryptsec, password) if err != nil { - lineProcessingError(ctx, "failed to decrypt: %s", err) + ctx = lineProcessingError(ctx, "failed to decrypt: %s", err) continue } nsec, _ := nip19.EncodePrivateKey(sec) @@ -264,13 +264,13 @@ func getSecretKeysFromStdinLinesOrSlice(ctx context.Context, c *cli.Command, key if strings.HasPrefix(sec, "nsec1") { _, data, err := nip19.Decode(sec) if err != nil { - lineProcessingError(ctx, "invalid nsec code: %s", err) + ctx = lineProcessingError(ctx, "invalid nsec code: %s", err) continue } sec = data.(string) } if !nostr.IsValid32ByteHex(sec) { - lineProcessingError(ctx, "invalid hex key") + ctx = lineProcessingError(ctx, "invalid hex key") continue } ch <- sec diff --git a/relay.go b/relay.go index d0e9e3a..5e54145 100644 --- a/relay.go +++ b/relay.go @@ -23,7 +23,7 @@ var relay = &cli.Command{ info, err := nip11.Fetch(ctx, url) if err != nil { - lineProcessingError(ctx, "failed to fetch '%s' information document: %w", url, err) + ctx = lineProcessingError(ctx, "failed to fetch '%s' information document: %w", url, err) continue } diff --git a/req.go b/req.go index b987866..1f542ad 100644 --- a/req.go +++ b/req.go @@ -182,7 +182,7 @@ example: filter := nostr.Filter{} if stdinFilter != "" { if err := easyjson.Unmarshal([]byte(stdinFilter), &filter); err != nil { - lineProcessingError(ctx, "invalid filter '%s' received from stdin: %s", stdinFilter, err) + ctx = lineProcessingError(ctx, "invalid filter '%s' received from stdin: %s", stdinFilter, err) continue } } diff --git a/verify.go b/verify.go index 1857fd0..7860b8d 100644 --- a/verify.go +++ b/verify.go @@ -20,18 +20,18 @@ it outputs nothing if the verification is successful.`, evt := nostr.Event{} if stdinEvent != "" { if err := json.Unmarshal([]byte(stdinEvent), &evt); err != nil { - lineProcessingError(ctx, "invalid event: %s", err) + ctx = lineProcessingError(ctx, "invalid event: %s", err) continue } } if evt.GetID() != evt.ID { - lineProcessingError(ctx, "invalid .id, expected %s, got %s", evt.GetID(), evt.ID) + ctx = lineProcessingError(ctx, "invalid .id, expected %s, got %s", evt.GetID(), evt.ID) continue } if ok, err := evt.CheckSignature(); !ok { - lineProcessingError(ctx, "invalid signature: %s", err) + ctx = lineProcessingError(ctx, "invalid signature: %s", err) continue } } From 5ee0036128ef01e380529a5af46f1aa227d0fc9b Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Thu, 11 Jul 2024 15:34:15 -0300 Subject: [PATCH 110/401] implement nip86 client: making management RPC calls to relays. --- go.mod | 2 +- go.sum | 4 +- relay.go | 170 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 173 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index b81a30c..1e41794 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 github.com/fatih/color v1.16.0 github.com/mailru/easyjson v0.7.7 - github.com/nbd-wtf/go-nostr v0.34.0 + github.com/nbd-wtf/go-nostr v0.34.2 github.com/nbd-wtf/nostr-sdk v0.0.5 github.com/urfave/cli/v3 v3.0.0-alpha9 golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 diff --git a/go.sum b/go.sum index 767e47c..934577a 100644 --- a/go.sum +++ b/go.sum @@ -79,8 +79,8 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/nbd-wtf/go-nostr v0.34.0 h1:E7tDHFx42gvWwFv1Eysn+NxJqGLmo21x/VEwj2+F21E= -github.com/nbd-wtf/go-nostr v0.34.0/go.mod h1:NZQkxl96ggbO8rvDpVjcsojJqKTPwqhP4i82O7K5DJs= +github.com/nbd-wtf/go-nostr v0.34.2 h1:9b4qZ29DhQf9xEWN8/7zfDD868r1jFbpjrR3c+BHc+E= +github.com/nbd-wtf/go-nostr v0.34.2/go.mod h1:NZQkxl96ggbO8rvDpVjcsojJqKTPwqhP4i82O7K5DJs= github.com/nbd-wtf/nostr-sdk v0.0.5 h1:rec+FcDizDVO0W25PX0lgYMXvP7zNNOgI3Fu9UCm4BY= github.com/nbd-wtf/nostr-sdk v0.0.5/go.mod h1:iJJsikesCGLNFZ9dLqhLPDzdt924EagUmdQxT3w2Lmk= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= diff --git a/relay.go b/relay.go index 5e54145..ce62cfd 100644 --- a/relay.go +++ b/relay.go @@ -1,11 +1,20 @@ package main import ( + "bytes" "context" + "crypto/sha256" + "encoding/base64" + "encoding/hex" "encoding/json" "fmt" + "io" + "net/http" + "strings" + "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/nip11" + "github.com/nbd-wtf/go-nostr/nip86" "github.com/urfave/cli/v3" ) @@ -32,4 +41,165 @@ var relay = &cli.Command{ } return nil }, + Commands: (func() []*cli.Command { + commands := make([]*cli.Command, 0, 12) + + for _, def := range []struct { + method string + args []string + }{{"allowpubkey", []string{"pubkey", "reason"}}} { + flags := make([]cli.Flag, len(def.args), len(def.args)+4) + for i, argName := range def.args { + flags[i] = declareFlag(argName) + } + + flags = append(flags, + &cli.StringFlag{ + Name: "sec", + Usage: "secret key to sign the event, as nsec, ncryptsec or hex", + DefaultText: "the key '1'", + Value: "0000000000000000000000000000000000000000000000000000000000000001", + }, + &cli.BoolFlag{ + Name: "prompt-sec", + Usage: "prompt the user to paste a hex or nsec with which to sign the event", + }, + &cli.StringFlag{ + Name: "connect", + Usage: "sign event using NIP-46, expects a bunker://... URL", + }, + &cli.StringFlag{ + Name: "connect-as", + Usage: "private key to when communicating with the bunker given on --connect", + DefaultText: "a random key", + }, + ) + + cmd := &cli.Command{ + Name: def.method, + Usage: fmt.Sprintf(`the "%s" relay management RPC call`, def.method), + Description: fmt.Sprintf( + `the "%s" management RPC call, see https://nips.nostr.com/86 for more information`, def.method), + Action: func(ctx context.Context, c *cli.Command) error { + params := make([]any, len(def.args)) + for i, argName := range def.args { + params[i] = getArgument(c, argName) + } + req := nip86.Request{Method: def.method, Params: params} + reqj, _ := json.Marshal(req) + + relayUrls := c.Args().Slice() + if len(relayUrls) == 0 { + stdout(string(reqj)) + return nil + } + + sec, bunker, err := gatherSecretKeyOrBunkerFromArguments(ctx, c) + if err != nil { + return err + } + + for _, relayUrl := range relayUrls { + httpUrl := "http" + nostr.NormalizeURL(relayUrl)[2:] + log("calling %s... ", httpUrl) + body := bytes.NewBuffer(nil) + body.Write(reqj) + req, err := http.NewRequestWithContext(ctx, "POST", httpUrl, body) + if err != nil { + return fmt.Errorf("failed to create request: %w", err) + } + + // Authorization + hostname := strings.Split(strings.Split(httpUrl, "://")[1], "/")[0] + payloadHash := sha256.Sum256(reqj) + authEvent := nostr.Event{ + Kind: 27235, + CreatedAt: nostr.Now(), + Tags: nostr.Tags{ + {"host", hostname}, + {"payload", hex.EncodeToString(payloadHash[:])}, + }, + } + if bunker != nil { + if err := bunker.SignEvent(ctx, &authEvent); err != nil { + return fmt.Errorf("failed to sign with bunker: %w", err) + } + } else if err := authEvent.Sign(sec); err != nil { + return fmt.Errorf("error signing with provided key: %w", err) + } + evtj, _ := json.Marshal(authEvent) + req.Header.Set("Authorization", "Nostr "+base64.StdEncoding.EncodeToString(evtj)) + + // Content-Type + req.Header.Set("Content-Type", "application/nostr+json+rpc") + + // make request to relay + resp, err := http.DefaultClient.Do(req) + if err != nil { + log("failed: %s\n", err) + continue + } + b, err := io.ReadAll(resp.Body) + if err != nil { + log("failed to read response: %s\n", err) + continue + } + if resp.StatusCode >= 300 { + log("failed with status %d\n", resp.StatusCode) + bodyPrintable := string(b) + if len(bodyPrintable) > 300 { + bodyPrintable = bodyPrintable[0:297] + "..." + } + log(bodyPrintable) + continue + } + var response nip86.Response + if err := json.Unmarshal(b, &response); err != nil { + log("bad json response: %s\n", err) + bodyPrintable := string(b) + if len(bodyPrintable) > 300 { + bodyPrintable = bodyPrintable[0:297] + "..." + } + log(bodyPrintable) + continue + } + resp.Body.Close() + + // print the result + log("\n") + pretty, _ := json.MarshalIndent(response, "", " ") + stdout(string(pretty)) + } + + return nil + }, + Flags: flags, + } + + commands = append(commands, cmd) + } + + return commands + })(), +} + +func declareFlag(argName string) cli.Flag { + usage := "parameter for this management RPC call, see https://nips.nostr.com/86 for more information." + switch argName { + case "kind": + return &cli.IntFlag{Name: argName, Required: true, Usage: usage} + case "reason": + return &cli.StringFlag{Name: argName, Usage: usage} + default: + return &cli.StringFlag{Name: argName, Required: true, Usage: usage} + } +} + +func getArgument(c *cli.Command, argName string) any { + switch argName { + case "kind": + return c.Int(argName) + default: + return c.String(argName) + } } From 79cb63a1b4b499baee8022f8be5b157af3496e55 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Fri, 12 Jul 2024 13:57:58 -0300 Subject: [PATCH 111/401] add all other relay management rpc methods. --- relay.go | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/relay.go b/relay.go index ce62cfd..555990b 100644 --- a/relay.go +++ b/relay.go @@ -47,7 +47,28 @@ var relay = &cli.Command{ for _, def := range []struct { method string args []string - }{{"allowpubkey", []string{"pubkey", "reason"}}} { + }{ + {"allowpubkey", []string{"pubkey", "reason"}}, + {"banpubkey", []string{"pubkey", "reason"}}, + {"listallowedpubkeys", nil}, + {"allowpubkey", []string{"pubkey", "reason"}}, + {"listallowedpubkeys", nil}, + {"listeventsneedingmoderation", nil}, + {"allowevent", []string{"id", "reason"}}, + {"banevent", []string{"id", "reason"}}, + {"listbannedevents", nil}, + {"changerelayname", []string{"name"}}, + {"changerelaydescription", []string{"description"}}, + {"changerelayicon", []string{"icon"}}, + {"allowkind", []string{"kind"}}, + {"disallowkind", []string{"kind"}}, + {"listallowedkinds", nil}, + {"blockip", []string{"ip", "reason"}}, + {"unblockip", []string{"ip", "reason"}}, + {"listblockedips", nil}, + } { + def := def + flags := make([]cli.Flag, len(def.args), len(def.args)+4) for i, argName := range def.args { flags[i] = declareFlag(argName) @@ -101,7 +122,7 @@ var relay = &cli.Command{ for _, relayUrl := range relayUrls { httpUrl := "http" + nostr.NormalizeURL(relayUrl)[2:] - log("calling %s... ", httpUrl) + log("calling '%s' on %s... ", def.method, httpUrl) body := bytes.NewBuffer(nil) body.Write(reqj) req, err := http.NewRequestWithContext(ctx, "POST", httpUrl, body) From 27f925c05ea9d88012680ce8c707c8091ec41db4 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Fri, 12 Jul 2024 14:04:14 -0300 Subject: [PATCH 112/401] left pad keys on `nak key` too so `nak key public 02` works, for example. --- helpers.go | 6 +++++- key.go | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/helpers.go b/helpers.go index 52da7d7..e3b1348 100644 --- a/helpers.go +++ b/helpers.go @@ -210,7 +210,7 @@ func gatherSecretKeyOrBunkerFromArguments(ctx context.Context, c *cli.Command) ( if err != nil { return "", nil, fmt.Errorf("failed to decrypt: %w", err) } - } else if bsec, err := hex.DecodeString(strings.Repeat("0", 64-len(sec)) + sec); err == nil { + } 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) @@ -284,3 +284,7 @@ func randString(n int) string { } return string(b) } + +func leftPadKey(k string) string { + return strings.Repeat("0", 64-len(k)) + k +} diff --git a/key.go b/key.go index f885b14..7c9d28a 100644 --- a/key.go +++ b/key.go @@ -269,6 +269,7 @@ func getSecretKeysFromStdinLinesOrSlice(ctx context.Context, c *cli.Command, key } sec = data.(string) } + sec = leftPadKey(sec) if !nostr.IsValid32ByteHex(sec) { ctx = lineProcessingError(ctx, "invalid hex key") continue From 54c4be10bd25c08b10e5d515f0067624fd50ce29 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Fri, 12 Jul 2024 18:51:07 -0300 Subject: [PATCH 113/401] fix and improve flag reordering for subcommands. --- go.mod | 2 +- go.sum | 4 ++-- main.go | 9 +++++---- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index 1e41794..3bbe16b 100644 --- a/go.mod +++ b/go.mod @@ -38,4 +38,4 @@ require ( golang.org/x/text v0.8.0 // indirect ) -replace github.com/urfave/cli/v3 => github.com/fiatjaf/cli/v3 v3.0.0-20240626022047-0fc2565ea728 +replace github.com/urfave/cli/v3 => github.com/fiatjaf/cli/v3 v3.0.0-20240712212113-3a8b0280e2c5 diff --git a/go.sum b/go.sum index 934577a..113aca7 100644 --- a/go.sum +++ b/go.sum @@ -42,8 +42,8 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3 github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= 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/cli/v3 v3.0.0-20240626022047-0fc2565ea728 h1:MgEQQSCPDkpILGlJKCj/Gj0l8XwYFpBKYATPJC14ros= -github.com/fiatjaf/cli/v3 v3.0.0-20240626022047-0fc2565ea728/go.mod h1:Z1ItyMma7t6I7zHG9OpbExhHQOSkFf/96n+mAZ9MtVI= +github.com/fiatjaf/cli/v3 v3.0.0-20240712212113-3a8b0280e2c5 h1:yhTRU02Hn1jwq50uUKRxbPZQg0PODe37s73IJNsCJb0= +github.com/fiatjaf/cli/v3 v3.0.0-20240712212113-3a8b0280e2c5/go.mod h1:Z1ItyMma7t6I7zHG9OpbExhHQOSkFf/96n+mAZ9MtVI= github.com/fiatjaf/eventstore v0.2.16 h1:NR64mnyUT5nJR8Sj2AwJTd1Hqs5kKJcCFO21ggUkvWg= github.com/fiatjaf/eventstore v0.2.16/go.mod h1:rUc1KhVufVmC+HUOiuPweGAcvG6lEOQCkRCn2Xn5VRA= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= diff --git a/main.go b/main.go index ac761bc..4a08a39 100644 --- a/main.go +++ b/main.go @@ -8,10 +8,11 @@ import ( ) var app = &cli.Command{ - Name: "nak", - Suggest: true, - UseShortOptionHandling: true, - Usage: "the nostr army knife command-line tool", + Name: "nak", + Suggest: true, + UseShortOptionHandling: true, + AllowFlagsAfterArguments: true, + Usage: "the nostr army knife command-line tool", Commands: []*cli.Command{ req, count, From bca4362ca59f418f452f162be8b97ca838e989fe Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Fri, 12 Jul 2024 19:15:13 -0300 Subject: [PATCH 114/401] fix inconsistency in nak key decrypt output: print hex always. --- key.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/key.go b/key.go index 7c9d28a..ad66e63 100644 --- a/key.go +++ b/key.go @@ -116,8 +116,7 @@ var decrypt = &cli.Command{ if err != nil { return fmt.Errorf("failed to decrypt: %s", err) } - nsec, _ := nip19.EncodePrivateKey(sec) - stdout(nsec) + stdout(sec) return nil case 1: if arg := c.Args().Get(0); strings.HasPrefix(arg, "ncryptsec1") { @@ -136,8 +135,7 @@ var decrypt = &cli.Command{ ctx = lineProcessingError(ctx, "failed to decrypt: %s", err) continue } - nsec, _ := nip19.EncodePrivateKey(sec) - stdout(nsec) + stdout(sec) } return nil } From e18e8c00e75cd70b017f368892a579e5057a89e3 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Fri, 12 Jul 2024 19:15:33 -0300 Subject: [PATCH 115/401] add 9 new examples to readme. --- README.md | 90 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 88 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 55c0d93..f4ffbc9 100644 --- a/README.md +++ b/README.md @@ -79,9 +79,95 @@ invalid .id, expected 05bd99d54cb835f427e0092c4275ee44c7ff51219eff417c19f70c9e2c ### fetch all quoted events by a given pubkey in their last 100 notes ```shell -nak req -l 100 -k 1 -a 2edbcea694d164629854a52583458fd6d965b161e3c48b57d3aff01940558884 relay.damus.io | jq -r '.content | match("nostr:((note1|nevent1)[a-z0-9]+)";"g") | .captures[0].string' | nak decode | jq -cr '{ids: [.id]}' | nak req relay.damus.io +~> nak req -l 100 -k 1 -a 2edbcea694d164629854a52583458fd6d965b161e3c48b57d3aff01940558884 relay.damus.io | jq -r '.content | match("nostr:((note1|nevent1)[a-z0-9]+)";"g") | .captures[0].string' | nak decode | jq -cr '{ids: [.id]}' | nak req relay.damus.io +connecting to relay.damus.io... +ok. +{"kind":1,"id":"dad32411fea62fda6ae057e97c73402f2031913388a721e059728a0efee5f0dd","pubkey":"5b0183ab6c3e322bf4d41c6b3aef98562a144847b7499543727c5539a114563e","created_at":1709057416,"tags":[["p","ad9d42203fd2480ea2e5c4c64593a027708aebe2b02aa60bd7b1d666daa5b08d"],["p","5ea721dd7828229a39a372477090208db30a6c2d357951b8ae504d2ecf86c06c"]],"content":"Fridays Edition of nostr:npub14kw5ygpl6fyqagh9cnrytyaqyacg46lzkq42vz7hk8txdk49kzxs04j7y0 will feature nostr:npub1t6njrhtc9q3f5wdrwfrhpypq3kes5mpdx4u4rw9w2pxjanuxcpkqveagv3 \n\nWe will be diving into Bitcoin, Content Creating, Music and the future of V4V. \n\nNostr Nest 2.0 🤘🏻\nSet your BlockClocks for 4:30EST","sig":"69cc403982e6c5fe996d545d6057c581a46be97ab79d818c1bc01e84e9f11a64a275d8834a4063a59fa135f7f116e38c51125173d5ce88671a4ddc2f656e01e4"} +{"kind":1,"id":"a681e9ca594dc455018be0a1c895576a8264956aee3e4fc01b872aa6df580632","pubkey":"5b0183ab6c3e322bf4d41c6b3aef98562a144847b7499543727c5539a114563e","created_at":1708456729,"tags":[["p","ad9d42203fd2480ea2e5c4c64593a027708aebe2b02aa60bd7b1d666daa5b08d"],["t","plebchain"],["p","4ce6abbd68dab6e9fdf6e8e9912a8e12f9b539e078c634c55a9bff2994a514dd"],["p","b83a28b7e4e5d20bd960c5faeb6625f95529166b8bdb045d42634a2f35919450"],["p","5a9c48c8f4782351135dd89c5d8930feb59cb70652ffd37d9167bf922f2d1069"],["p","f8e6c64342f1e052480630e27e1016dce35fc3a614e60434fef4aa2503328ca9"],["p","2edbcea694d164629854a52583458fd6d965b161e3c48b57d3aff01940558884"],["r","https://nostrnests.com/plebchainradio"]],"content":"This weeks edition of nostr:npub14kw5ygpl6fyqagh9cnrytyaqyacg46lzkq42vz7hk8txdk49kzxs04j7y0 will feature the wildly talented #Plebchain Legend nostr:npub1fnn2h0tgm2mwnl0kar5ez25wztum2w0q0rrrf326n0ljn999znwsqf4xnx \n\nWe will be discussing building his local community in Tanzania, his worldly success in V4V music and all things Nostr and Bitcoin. \n\nFilling in for nostr:npub1hqaz3dlyuhfqhktqchawke39l92jj9nt30dsgh2zvd9z7dv3j3gqpkt56s this week will be nostr:npub1t2wy3j850q34zy6amzw9mzfsl66eedcx2tlaxlv3v7leytedzp5szs8c2u \u0026 nostr:npub1lrnvvs6z78s9yjqxxr38uyqkmn34lsaxznnqgd877j4z2qej3j5s09qnw5 from nostr:npub19mduaf5569jx9xz555jcx3v06mvktvtpu0zgk47n4lcpjsz43zzqhj6vzk \n\nSet your Blockclocks for this Friday 4PM EST! \nhttps://nostrnests.com/plebchainradio","sig":"b4528c7a248bf04ab9fcd0ce8033fdc9656b0e92dccf5f3a6b8cd7ad66cf074619100c7d192ae9a87745bc5445f6fe36221c1fd5820d5038bbcae2aedb5090d8"} +{"kind":1,"id":"cc396bbc9e01910e56ef169916c39197d468b65e80c42aaa0a874a32500039c4","pubkey":"b9d02cb8fddeb191701ec0648e37ed1f6afba263e0060fc06099a62851d25e04","created_at":1708210291,"tags":[["imeta","url https://image.nostr.build/185e4c10dc2e46dacf6ad38fbc4319f52860fc3f96efb433be55bdb91fc225b8.jpg","blurhash eUGR9vR*4pt6OqK0WVspWWoL0fso%eWBwM+~aySxs:S2tQW;VtWVW;","dim 4032x3024"],["p","f133b246f07633fde1a894133ac270ab8750502b64a9779c0bac3c9228198dda"],["p","5ea721dd7828229a39a372477090208db30a6c2d357951b8ae504d2ecf86c06c"],["t","cultureshock"],["r","https://image.nostr.build/185e4c10dc2e46dacf6ad38fbc4319f52860fc3f96efb433be55bdb91fc225b8.jpg"]],"content":"10 minutes.\n\nFireside chat between nostr:npub17yemy3hswcelmcdgjsfn4sns4wr4q5ptvj5h08qt4s7fy2qe3hdqsczs99 and nostr:npub1t6njrhtc9q3f5wdrwfrhpypq3kes5mpdx4u4rw9w2pxjanuxcpkqveagv3 followed by live set to close down #CultureShock.\n\nStreaming V4V at tunestr.io https://image.nostr.build/185e4c10dc2e46dacf6ad38fbc4319f52860fc3f96efb433be55bdb91fc225b8.jpg ","sig":"b0464529b2d2de2b4df911d47dbfe4aa31ac8f2db285b1a5930941cf1877425c14d6901d1be1bfaa13cd3d981d5e9a722debfc47c4f087d95da628a7035437ec"} +{"kind":1,"id":"f24aed86b493f266952ed35d4724582946cdf73985581f9986641f81fad6b73d","pubkey":"fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52","created_at":1707912005,"tags":[["p","fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52","wss://140.f7z.io/","mention"]],"content":"Releasing: Whynotstr 0.0.0 😂\n\nCollaborative editing, the left-side-of-the-curve approach\n\nTry it out: https://collab-lemon.vercel.app/\n\nObviously open source: https://github.com/pablof7z/collab\n\nnostr:nevent1qvzqqqqqqypzp75cf0tahv5z7plpdeaws7ex52nmnwgtwfr2g3m37r844evqrr6jqy88wumn8ghj7mn0wvhxcmmv9uq35amnwvaz7tms09exzmtfvshxv6tpw34xze3wvdhk6tcqyznqm6guz9k38dpqtc84s7jflec7m7wpzvtx2xjkjkm2xm89ucy2jhdcnur","sig":"6f562d733e50f5934dcf359a4f16dece1734302c0cc3a793ee2f08007ccb4ade3591373a633538617611f327feb7534ad4d11a8475163c7f734a01c63e52b79f"} +... ``` -## Contributing to this repository +### sign an event collaboratively with multiple parties using musig2 +```shell +~> nak event --sec 1234 -k 1 -c 'hello from a combined key' --musig 2 +the following code should be saved secretly until the next step an included with --musig-nonce-secret: +QebOT03ERmV7km22CqEqBPFmzAkgxQzGGbR7Si8yIZCBrd1N9A3LKwGLO71kbgXZ9EYFKpjiwun4u0mj5Tq6vwM3pK7x+EI8oHbkt9majKv/QN24Ix8qnwEIHxXX+mXBug== + +the next signer and they should call this on their side: +nak event --sec --musig 2 -k 1 -ts 1720821287 -c 'hello from a combined key' --musig-pubkey 0337a4aef1f8423ca076e4b7d99a8cabff40ddb8231f2a9f01081f15d7fa65c1ba --musig-nonce 0285af37c6c43638cda2c773098e867c749ddf1e9d096b78686c5d000603935ad3025c4a1e042eb6b0dcfd864d1e072d2ce8da06f2c0dcf13fd7d1fcef0dd26dbc92 +``` + +demo videos with [2](https://njump.me/nevent1qqs8pmmae89agph80928l6gjm0wymechqazv80jwqrqy4cgk08epjaczyqalp33lewf5vdq847t6te0wvnags0gs0mu72kz8938tn24wlfze674zkzz), [3](https://njump.me/nevent1qqsrp320drqcnmnam6jvmdd4lgdvh2ay0xrdesrvy6q9qqdfsk7r55qzyqalp33lewf5vdq847t6te0wvnags0gs0mu72kz8938tn24wlfze6c32d4m) and [4](https://njump.me/nevent1qqsre84xe6qpagf2w2xjtjwc95j4dd5ccue68gxl8grkd6t6hjhaj5qzyqalp33lewf5vdq847t6te0wvnags0gs0mu72kz8938tn24wlfze6t8t7ak) parties. + +### generate a private key +```shell +~> nak key generate 18:59 +7b94e287b1fafa694ded1619b27de7effd3646104a158e187ff4edc56bc6148d +``` + +### encrypt key with NIP-49 +```shell +~> nak key encrypt 7b94e287b1fafa694ded1619b27de7effd3646104a158e187ff4edc56bc6148d mypassword +ncryptsec1qggx54cg270zy9y8krwmfz29jyypsuxken2fkk99gr52qhje968n6mwkrfstqaqhq9eq94pnzl4nff437l4lp4ur2cs4f9um8738s35l2esx2tas48thtfhrk5kq94pf9j2tpk54yuermra0xu6hl5ls +``` + +### decrypt key with NIP-49 +```shell +~> nak key decrypt ncryptsec1qggx54cg270zy9y8krwmfz29jyypsuxken2fkk99gr52qhje968n6mwkrfstqaqhq9eq94pnzl4nff437l4lp4ur2cs4f9um8738s35l2esx2tas48thtfhrk5kq94pf9j2tpk54yuermra0xu6hl5ls mypassword +7b94e287b1fafa694ded1619b27de7effd3646104a158e187ff4edc56bc6148d +~> +~> nak key decrypt ncryptsec1qggx54cg270zy9y8krwmfz29jyypsuxken2fkk99gr52qhje968n6mwkrfstqaqhq9eq94pnzl4nff437l4lp4ur2cs4f9um8738s35l2esx2tas48thtfhrk5kq94pf9j2tpk54yuermra0xu6hl5ls +type the password to decrypt your secret key: ********** +7b94e287b1fafa694ded1619b27de7effd3646104a158e187ff4edc56bc6148d +``` + +### get a public key from a private key +```shell +~> nak key public 7b94e287b1fafa694ded1619b27de7effd3646104a158e187ff4edc56bc6148d +985d66d2644dfa7676e26046914470d66ebc7fa783a3f57f139fde32d0d631d7 +``` + +### sign an event using a remote NIP-46 bunker +```shell +~> nak event --connect 'bunker://a9e0f110f636f3191644110c19a33448daf09d7cda9708a769e91b7e91340208?relay=wss%3A%2F%2Frelay.damus.io&relay=wss%3A%2F%2Frelay.nsecbunker.com&relay=wss%3A%2F%2Fnos.lol&secret=TWfGbjQCLxUf' -c 'hello from bunker' +``` + +### sign an event using a NIP-49 encrypted key +```shell +~> nak event --sec ncryptsec1qggx54cg270zy9y8krwmfz29jyypsuxken2fkk99gr52qhje968n6mwkrfstqaqhq9eq94pnzl4nff437l4lp4ur2cs4f9um8738s35l2esx2tas48thtfhrk5kq94pf9j2tpk54yuermra0xu6hl5ls -c 'hello from encrypted key' +type the password to decrypt your secret key: ********** +{"kind":1,"id":"8aa5c931fb1da507f14801de6a1814b7f0baae984dc502b9889f347f5aa3cc4e","pubkey":"985d66d2644dfa7676e26046914470d66ebc7fa783a3f57f139fde32d0d631d7","created_at":1720822280,"tags":[],"content":"hello from encrypted key","sig":"9d1c9e56e87f787cc5b6191ec47690ce59fa4bef105b56297484253953e18fb930f6683f007e84a9ce9dc9a25b20c191c510629156dcd24bd16e15d302d20944"} +``` + +### talk to a relay's NIP-86 management API +```shell +nak relay allowpubkey --sec ncryptsec1qggx54cg270zy9y8krwmfz29jyypsuxken2fkk99gr52qhje968n6mwkrfstqaqhq9eq94pnzl4nff437l4lp4ur2cs4f9um8738s35l2esx2tas48thtfhrk5kq94pf9j2tpk54yuermra0xu6hl5ls --pubkey a9e0f110f636f3191644110c19a33448daf09d7cda9708a769e91b7e91340208 pyramid.fiatjaf.com +type the password to decrypt your secret key: ********** +calling 'allowpubkey' on https://pyramid.fiatjaf.com... +{ + "result": null, + "error": "failed to add to whitelist: pubkey 985d66d2644dfa7676e26046914470d66ebc7fa783a3f57f139fde32d0d631d7 doesn't have permission to invite" +} +``` + +### start a bunker locally +```shell +~> nak bunker --sec ncryptsec1qggrp80ptf0s7kyl0r38ktzg60fem85m89uz7um6rjn4pnep2nnvcgqm8h7q36c76z9sypatdh4fmw6etfxu99mv5cxkw4ymcsryw0zz7evyuplsgvnj5yysf449lq94klzvnahsw2lzxflvcq4qpf5q -k 3fbf7fbb2a2111e205f74aca0166e29e421729c9a07bc45aa85d39535b47c9ed relay.damus.io nos.lol relay.nsecbunker.com +connecting to relay.damus.io... ok. +connecting to nos.lol... ok. +connecting to relay.nsecbunker.com... ok. +type the password to decrypt your secret key: *** +listening at [wss://relay.damus.io wss://nos.lol wss://relay.nsecbunker.com]: + pubkey: f59911b561c37c90b01e9e5c2557307380835c83399756f4d62d8167227e420a + npub: npub17kv3rdtpcd7fpvq7newz24eswwqgxhyr8xt4daxk9kqkwgn7gg9q4gy8vf + authorized keys: + - 3fbf7fbb2a2111e205f74aca0166e29e421729c9a07bc45aa85d39535b47c9ed + to restart: nak bunker --sec ncryptsec1qggrp80ptf0s7kyl0r38ktzg60fem85m89uz7um6rjn4pnep2nnvcgqm8h7q36c76z9sypatdh4fmw6etfxu99mv5cxkw4ymcsryw0zz7evyuplsgvnj5yysf449lq94klzvnahsw2lzxflvcq4qpf5q -k 3fbf7fbb2a2111e205f74aca0166e29e421729c9a07bc45aa85d39535b47c9ed relay.damus.io nos.lol relay.nsecbunker.com + bunker: bunker://f59911b561c37c90b01e9e5c2557307380835c83399756f4d62d8167227e420a?relay=wss%3A%2F%2Frelay.damus.io&relay=wss%3A%2F%2Fnos.lol&relay=wss%3A%2F%2Frelay.nsecbunker.com&secret=XuuiMbcLwuwL +``` + +## contributing to this repository Use NIP-34 to send your patches to `naddr1qqpkucttqy28wumn8ghj7un9d3shjtnwdaehgu3wvfnsz9nhwden5te0wfjkccte9ehx7um5wghxyctwvsq3gamnwvaz7tmjv4kxz7fwv3sk6atn9e5k7q3q80cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsxpqqqpmej2wctpn`. From 30ca5776c5dbdd8265ecb79c378c5007029729af Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sat, 13 Jul 2024 09:42:43 -0300 Subject: [PATCH 116/401] global -q is persistent. --- main.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/main.go b/main.go index 4a08a39..9226f3f 100644 --- a/main.go +++ b/main.go @@ -27,9 +27,10 @@ var app = &cli.Command{ }, Flags: []cli.Flag{ &cli.BoolFlag{ - Name: "quiet", - Usage: "do not print logs and info messages to stderr, use -qq to also not print anything to stdout", - Aliases: []string{"q"}, + Name: "quiet", + Usage: "do not print logs and info messages to stderr, use -qq to also not print anything to stdout", + Aliases: []string{"q"}, + Persistent: true, Action: func(ctx context.Context, c *cli.Command, b bool) error { q := c.Count("quiet") if q >= 1 { From 8f51fe757b76c27c7879072ee81d0a2cebbcb684 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sat, 13 Jul 2024 10:48:50 -0300 Subject: [PATCH 117/401] remove nson. it's not being used by anyone and didn't gain enough traction, and also now I think I have a more efficient way of encoding this, so using that new scheme in the future will be better than this. --- event.go | 7 ------- 1 file changed, 7 deletions(-) diff --git a/event.go b/event.go index c1b3bfa..6b10896 100644 --- a/event.go +++ b/event.go @@ -12,7 +12,6 @@ import ( "github.com/mailru/easyjson" "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/nip19" - "github.com/nbd-wtf/go-nostr/nson" "github.com/urfave/cli/v3" "golang.org/x/exp/slices" ) @@ -90,10 +89,6 @@ example: Name: "nevent", Usage: "print the nevent code (to stderr) after the event is published", }, - &cli.BoolFlag{ - Name: "nson", - Usage: "encode the event using NSON", - }, &cli.IntFlag{ Name: "kind", Aliases: []string{"k"}, @@ -276,8 +271,6 @@ example: if c.Bool("envelope") { j, _ := json.Marshal(nostr.EventEnvelope{Event: evt}) result = string(j) - } else if c.Bool("nson") { - result, _ = nson.Marshal(&evt) } else { j, _ := easyjson.Marshal(&evt) result = string(j) From a5013c513d6ab6e4129db89adad7a988b434c671 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sat, 13 Jul 2024 11:16:28 -0300 Subject: [PATCH 118/401] disallow negative kinds and limits. --- event.go | 4 ++-- req.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/event.go b/event.go index 6b10896..b824205 100644 --- a/event.go +++ b/event.go @@ -89,7 +89,7 @@ example: Name: "nevent", Usage: "print the nevent code (to stderr) after the event is published", }, - &cli.IntFlag{ + &cli.UintFlag{ Name: "kind", Aliases: []string{"k"}, Usage: "event kind", @@ -177,7 +177,7 @@ example: kindWasSupplied = strings.Contains(stdinEvent, `"kind"`) } - if kind := c.Int("kind"); slices.Contains(c.FlagNames(), "kind") { + if kind := c.Uint("kind"); slices.Contains(c.FlagNames(), "kind") { evt.Kind = int(kind) mustRehashAndResign = true } else if !kindWasSupplied { diff --git a/req.go b/req.go index 1f542ad..2e8e7cf 100644 --- a/req.go +++ b/req.go @@ -80,7 +80,7 @@ example: Usage: "only accept events older than this (unix timestamp)", Category: CATEGORY_FILTER_ATTRIBUTES, }, - &cli.IntFlag{ + &cli.UintFlag{ Name: "limit", Aliases: []string{"l"}, Usage: "only accept up to this number of events", @@ -251,7 +251,7 @@ example: return fmt.Errorf("parse error: Invalid numeric literal %q", until) } } - if limit := c.Int("limit"); limit != 0 { + if limit := c.Uint("limit"); limit != 0 { filter.Limit = int(limit) } else if c.IsSet("limit") || c.Bool("stream") { filter.LimitZero = true From 49ce12ffc2e46660bad8a0515e2fcf5edfced536 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sat, 13 Jul 2024 13:06:24 -0300 Subject: [PATCH 119/401] use natural date parser thing for req "since", "until" and event "ts". --- event.go | 17 +++------- flags.go | 95 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 16 +++++++--- go.sum | 42 ++++++++++++++++++------- req.go | 33 ++++++-------------- 5 files changed, 151 insertions(+), 52 deletions(-) create mode 100644 flags.go diff --git a/event.go b/event.go index b824205..becd869 100644 --- a/event.go +++ b/event.go @@ -5,7 +5,6 @@ import ( "encoding/json" "fmt" "os" - "strconv" "strings" "time" @@ -126,12 +125,12 @@ example: Usage: "shortcut for --tag d=", Category: CATEGORY_EVENT_FIELDS, }, - &cli.StringFlag{ + &NaturalTimeFlag{ Name: "created-at", Aliases: []string{"time", "ts"}, Usage: "unix timestamp value for the created_at field", DefaultText: "now", - Value: "", + Value: nostr.Now(), Category: CATEGORY_EVENT_FIELDS, }, }, @@ -225,16 +224,8 @@ example: mustRehashAndResign = true } - if createdAt := c.String("created-at"); createdAt != "" { - ts := time.Now() - if createdAt != "now" { - if v, err := strconv.ParseInt(createdAt, 10, 64); err != nil { - return fmt.Errorf("failed to parse timestamp '%s': %w", createdAt, err) - } else { - ts = time.Unix(v, 0) - } - } - evt.CreatedAt = nostr.Timestamp(ts.Unix()) + if c.IsSet("created-at") { + evt.CreatedAt = getNaturalDate(c, "created-at") mustRehashAndResign = true } else if evt.CreatedAt == 0 { evt.CreatedAt = nostr.Now() diff --git a/flags.go b/flags.go new file mode 100644 index 0000000..5e80b2d --- /dev/null +++ b/flags.go @@ -0,0 +1,95 @@ +package main + +import ( + "errors" + "fmt" + "strconv" + "time" + + "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. +type naturalTimeValue struct { + timestamp *nostr.Timestamp + hasBeenSet bool +} + +var _ cli.ValueCreator[nostr.Timestamp, struct{}] = naturalTimeValue{} + +// Below functions are to satisfy the ValueCreator interface + +func (t naturalTimeValue) Create(val nostr.Timestamp, p *nostr.Timestamp, c struct{}) cli.Value { + *p = val + return &naturalTimeValue{ + timestamp: p, + } +} + +func (t naturalTimeValue) ToString(b nostr.Timestamp) string { + ts := b.Time() + + if ts.IsZero() { + return "" + } + return fmt.Sprintf("%v", ts) +} + +// Timestamp constructor(for internal testing only) +func newTimestamp(timestamp nostr.Timestamp) *naturalTimeValue { + return &naturalTimeValue{timestamp: ×tamp} +} + +// Below functions are to satisfy the flag.Value interface + +// Parses the string value to timestamp +func (t *naturalTimeValue) Set(value string) error { + var ts time.Time + if n, err := strconv.ParseInt(value, 10, 64); err == nil { + // when the input is a raw number, treat it as an exact timestamp + ts = time.Unix(n, 0) + } else if errors.Is(err, strconv.ErrRange) { + // this means a huge number, so we should fail + return err + } else { + // otherwise try to parse it as a human date string in natural language + date, err := dateparser.Parse(&dateparser.Configuration{ + DefaultTimezone: time.Local, + CurrentTime: time.Now(), + }, value) + ts = date.Time + if err != nil { + return err + } + } + + if t.timestamp != nil { + *t.timestamp = nostr.Timestamp(ts.Unix()) + } + + t.hasBeenSet = true + return nil +} + +// String returns a readable representation of this value (for usage defaults) +func (t *naturalTimeValue) String() string { + return fmt.Sprintf("%#v", t.timestamp) +} + +// Value returns the timestamp value stored in the flag +func (t *naturalTimeValue) Value() *nostr.Timestamp { + return t.timestamp +} + +// Get returns the flag structure +func (t *naturalTimeValue) Get() any { + return *t.timestamp +} + +func getNaturalDate(cmd *cli.Command, name string) nostr.Timestamp { + return cmd.Value(name).(nostr.Timestamp) +} diff --git a/go.mod b/go.mod index 3bbe16b..7d75e66 100644 --- a/go.mod +++ b/go.mod @@ -10,10 +10,11 @@ require ( github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 github.com/fatih/color v1.16.0 github.com/mailru/easyjson v0.7.7 + github.com/markusmobius/go-dateparser v1.2.3 github.com/nbd-wtf/go-nostr v0.34.2 github.com/nbd-wtf/nostr-sdk v0.0.5 github.com/urfave/cli/v3 v3.0.0-alpha9 - golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 + golang.org/x/exp v0.0.0-20240707233637-46b078467d37 ) require ( @@ -22,20 +23,27 @@ require ( github.com/chzyer/logex v1.1.10 // indirect github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect + github.com/elliotchance/pie/v2 v2.7.0 // indirect github.com/fiatjaf/eventstore v0.2.16 // indirect github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/pool v0.2.1 // indirect github.com/gobwas/ws v1.4.0 // indirect + github.com/hablullah/go-hijri v1.0.2 // indirect + github.com/hablullah/go-juliandays v1.0.0 // indirect + github.com/jalaali/go-jalaali v0.0.0-20210801064154-80525e88d958 // indirect github.com/josharian/intern v1.0.0 // indirect + github.com/magefile/mage v1.14.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/puzpuzpuz/xsync/v3 v3.1.0 // indirect + github.com/tetratelabs/wazero v1.2.1 // indirect github.com/tidwall/gjson v1.17.1 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect - golang.org/x/crypto v0.7.0 // indirect - golang.org/x/sys v0.20.0 // indirect - golang.org/x/text v0.8.0 // indirect + github.com/wasilibs/go-re2 v1.3.0 // indirect + golang.org/x/crypto v0.21.0 // indirect + golang.org/x/sys v0.22.0 // indirect + golang.org/x/text v0.16.0 // indirect ) replace github.com/urfave/cli/v3 => github.com/fiatjaf/cli/v3 v3.0.0-20240712212113-3a8b0280e2c5 diff --git a/go.sum b/go.sum index 113aca7..68d559c 100644 --- a/go.sum +++ b/go.sum @@ -40,6 +40,8 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeC github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= +github.com/elliotchance/pie/v2 v2.7.0 h1:FqoIKg4uj0G/CrLGuMS9ejnFKa92lxE1dEgBD3pShXg= +github.com/elliotchance/pie/v2 v2.7.0/go.mod h1:18t0dgGFH006g4eVdDtWfgFZPQEgl10IoEO8YWEq3Og= 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/cli/v3 v3.0.0-20240712212113-3a8b0280e2c5 h1:yhTRU02Hn1jwq50uUKRxbPZQg0PODe37s73IJNsCJb0= @@ -65,15 +67,25 @@ github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/hablullah/go-hijri v1.0.2 h1:drT/MZpSZJQXo7jftf5fthArShcaMtsal0Zf/dnmp6k= +github.com/hablullah/go-hijri v1.0.2/go.mod h1:OS5qyYLDjORXzK4O1adFw9Q5WfhOcMdAKglDkcTxgWQ= +github.com/hablullah/go-juliandays v1.0.0 h1:A8YM7wIj16SzlKT0SRJc9CD29iiaUzpBLzh5hr0/5p0= +github.com/hablullah/go-juliandays v1.0.0/go.mod h1:0JOYq4oFOuDja+oospuc61YoX+uNEn7Z6uHYTbBzdGc= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/jalaali/go-jalaali v0.0.0-20210801064154-80525e88d958 h1:qxLoi6CAcXVzjfvu+KXIXJOAsQB62LXjsfbOaErsVzE= +github.com/jalaali/go-jalaali v0.0.0-20210801064154-80525e88d958/go.mod h1:Wqfu7mjUHj9WDzSSPI5KfBclTTEnLveRUFr/ujWnTgE= github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= +github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo= +github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/markusmobius/go-dateparser v1.2.3 h1:TvrsIvr5uk+3v6poDjaicnAFJ5IgtFHgLiuMY2Eb7Nw= +github.com/markusmobius/go-dateparser v1.2.3/go.mod h1:cMwQRrBUQlK1UI5TIFHEcvpsMbkWrQLXuaPNMFzuYLk= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= @@ -98,9 +110,11 @@ github.com/puzpuzpuz/xsync/v3 v3.1.0 h1:EewKT7/LNac5SLiEblJeUu8z5eERHrmRLnMQL2d7 github.com/puzpuzpuz/xsync/v3 v3.1.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +github.com/tetratelabs/wazero v1.2.1 h1:J4X2hrGzJvt+wqltuvcSjHQ7ujQxA9gb6PeMs4qlUWs= +github.com/tetratelabs/wazero v1.2.1/go.mod h1:wYx2gNRg8/WihJfSDxA1TIL8H+GkfLYm+bIfbblu9VQ= github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U= github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= @@ -108,20 +122,24 @@ github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JT github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/wasilibs/go-re2 v1.3.0 h1:LFhBNzoStM3wMie6rN2slD1cuYH2CGiHpvNL3UtcsMw= +github.com/wasilibs/go-re2 v1.3.0/go.mod h1:AafrCXVvGRJJOImMajgJ2M7rVmWyisVK7sFshbxnVrg= +github.com/wasilibs/nottinygc v0.4.0 h1:h1TJMihMC4neN6Zq+WKpLxgd9xCFMw7O9ETLwY2exJQ= +github.com/wasilibs/nottinygc v0.4.0/go.mod h1:oDcIotskuYNMpqMF23l7Z8uzD4TC0WXHK8jetlB3HIo= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= -golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= -golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/exp v0.0.0-20240707233637-46b078467d37 h1:uLDX+AfeFCct3a2C7uIWBKMJIR3CJMhcgfrUAqjRK6w= +golang.org/x/exp v0.0.0-20240707233637-46b078467d37/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= +golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 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= @@ -134,13 +152,13 @@ golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= -golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/req.go b/req.go index 2e8e7cf..e8aed05 100644 --- a/req.go +++ b/req.go @@ -5,7 +5,6 @@ import ( "encoding/json" "fmt" "os" - "strconv" "strings" "github.com/mailru/easyjson" @@ -68,13 +67,13 @@ example: Usage: "shortcut for --tag d=", Category: CATEGORY_FILTER_ATTRIBUTES, }, - &cli.StringFlag{ + &NaturalTimeFlag{ Name: "since", Aliases: []string{"s"}, Usage: "only accept events newer than this (unix timestamp)", Category: CATEGORY_FILTER_ATTRIBUTES, }, - &cli.StringFlag{ + &NaturalTimeFlag{ Name: "until", Aliases: []string{"u"}, Usage: "only accept events older than this (unix timestamp)", @@ -229,28 +228,16 @@ example: filter.Tags[tag[0]] = append(filter.Tags[tag[0]], tag[1]) } - if since := c.String("since"); since != "" { - if since == "now" { - ts := nostr.Now() - filter.Since = &ts - } else if i, err := strconv.Atoi(since); err == nil { - ts := nostr.Timestamp(i) - filter.Since = &ts - } else { - return fmt.Errorf("parse error: Invalid numeric literal %q", since) - } + if c.IsSet("since") { + nts := getNaturalDate(c, "since") + filter.Since = &nts } - if until := c.String("until"); until != "" { - if until == "now" { - ts := nostr.Now() - filter.Until = &ts - } else if i, err := strconv.Atoi(until); err == nil { - ts := nostr.Timestamp(i) - filter.Until = &ts - } else { - return fmt.Errorf("parse error: Invalid numeric literal %q", until) - } + + if c.IsSet("until") { + nts := getNaturalDate(c, "until") + filter.Until = &nts } + if limit := c.Uint("limit"); limit != 0 { filter.Limit = int(limit) } else if c.IsSet("limit") || c.Bool("stream") { From ce6bb0aa2201894ba345369b379e178100c20b54 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sat, 13 Jul 2024 13:17:20 -0300 Subject: [PATCH 120/401] natural time examples. --- README.md | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index f4ffbc9..245531b 100644 --- a/README.md +++ b/README.md @@ -77,15 +77,14 @@ publishing to wss://relayable.org... success. invalid .id, expected 05bd99d54cb835f427e0092c4275ee44c7ff51219eff417c19f70c9e2c53ad5a, got 05bd99d54cb835f327e0092c4275ee44c7ff51219eff417c19f70c9e2c53ad5a ``` -### fetch all quoted events by a given pubkey in their last 100 notes +### fetch all quoted events by a given pubkey in their last 10 notes of 2023 ```shell -~> nak req -l 100 -k 1 -a 2edbcea694d164629854a52583458fd6d965b161e3c48b57d3aff01940558884 relay.damus.io | jq -r '.content | match("nostr:((note1|nevent1)[a-z0-9]+)";"g") | .captures[0].string' | nak decode | jq -cr '{ids: [.id]}' | nak req relay.damus.io +~> nak req -l 10 -k 1 --until 'December 31 2023' -a 2edbcea694d164629854a52583458fd6d965b161e3c48b57d3aff01940558884 relay.damus.io | jq -r '.content | match("nostr:((note1|nevent1)[a-z0-9]+)";"g") | .captures[0].string' | nak decode | jq -cr '{ids: [.id]}' | nak req relay.damus.io connecting to relay.damus.io... ok. -{"kind":1,"id":"dad32411fea62fda6ae057e97c73402f2031913388a721e059728a0efee5f0dd","pubkey":"5b0183ab6c3e322bf4d41c6b3aef98562a144847b7499543727c5539a114563e","created_at":1709057416,"tags":[["p","ad9d42203fd2480ea2e5c4c64593a027708aebe2b02aa60bd7b1d666daa5b08d"],["p","5ea721dd7828229a39a372477090208db30a6c2d357951b8ae504d2ecf86c06c"]],"content":"Fridays Edition of nostr:npub14kw5ygpl6fyqagh9cnrytyaqyacg46lzkq42vz7hk8txdk49kzxs04j7y0 will feature nostr:npub1t6njrhtc9q3f5wdrwfrhpypq3kes5mpdx4u4rw9w2pxjanuxcpkqveagv3 \n\nWe will be diving into Bitcoin, Content Creating, Music and the future of V4V. \n\nNostr Nest 2.0 🤘🏻\nSet your BlockClocks for 4:30EST","sig":"69cc403982e6c5fe996d545d6057c581a46be97ab79d818c1bc01e84e9f11a64a275d8834a4063a59fa135f7f116e38c51125173d5ce88671a4ddc2f656e01e4"} -{"kind":1,"id":"a681e9ca594dc455018be0a1c895576a8264956aee3e4fc01b872aa6df580632","pubkey":"5b0183ab6c3e322bf4d41c6b3aef98562a144847b7499543727c5539a114563e","created_at":1708456729,"tags":[["p","ad9d42203fd2480ea2e5c4c64593a027708aebe2b02aa60bd7b1d666daa5b08d"],["t","plebchain"],["p","4ce6abbd68dab6e9fdf6e8e9912a8e12f9b539e078c634c55a9bff2994a514dd"],["p","b83a28b7e4e5d20bd960c5faeb6625f95529166b8bdb045d42634a2f35919450"],["p","5a9c48c8f4782351135dd89c5d8930feb59cb70652ffd37d9167bf922f2d1069"],["p","f8e6c64342f1e052480630e27e1016dce35fc3a614e60434fef4aa2503328ca9"],["p","2edbcea694d164629854a52583458fd6d965b161e3c48b57d3aff01940558884"],["r","https://nostrnests.com/plebchainradio"]],"content":"This weeks edition of nostr:npub14kw5ygpl6fyqagh9cnrytyaqyacg46lzkq42vz7hk8txdk49kzxs04j7y0 will feature the wildly talented #Plebchain Legend nostr:npub1fnn2h0tgm2mwnl0kar5ez25wztum2w0q0rrrf326n0ljn999znwsqf4xnx \n\nWe will be discussing building his local community in Tanzania, his worldly success in V4V music and all things Nostr and Bitcoin. \n\nFilling in for nostr:npub1hqaz3dlyuhfqhktqchawke39l92jj9nt30dsgh2zvd9z7dv3j3gqpkt56s this week will be nostr:npub1t2wy3j850q34zy6amzw9mzfsl66eedcx2tlaxlv3v7leytedzp5szs8c2u \u0026 nostr:npub1lrnvvs6z78s9yjqxxr38uyqkmn34lsaxznnqgd877j4z2qej3j5s09qnw5 from nostr:npub19mduaf5569jx9xz555jcx3v06mvktvtpu0zgk47n4lcpjsz43zzqhj6vzk \n\nSet your Blockclocks for this Friday 4PM EST! \nhttps://nostrnests.com/plebchainradio","sig":"b4528c7a248bf04ab9fcd0ce8033fdc9656b0e92dccf5f3a6b8cd7ad66cf074619100c7d192ae9a87745bc5445f6fe36221c1fd5820d5038bbcae2aedb5090d8"} -{"kind":1,"id":"cc396bbc9e01910e56ef169916c39197d468b65e80c42aaa0a874a32500039c4","pubkey":"b9d02cb8fddeb191701ec0648e37ed1f6afba263e0060fc06099a62851d25e04","created_at":1708210291,"tags":[["imeta","url https://image.nostr.build/185e4c10dc2e46dacf6ad38fbc4319f52860fc3f96efb433be55bdb91fc225b8.jpg","blurhash eUGR9vR*4pt6OqK0WVspWWoL0fso%eWBwM+~aySxs:S2tQW;VtWVW;","dim 4032x3024"],["p","f133b246f07633fde1a894133ac270ab8750502b64a9779c0bac3c9228198dda"],["p","5ea721dd7828229a39a372477090208db30a6c2d357951b8ae504d2ecf86c06c"],["t","cultureshock"],["r","https://image.nostr.build/185e4c10dc2e46dacf6ad38fbc4319f52860fc3f96efb433be55bdb91fc225b8.jpg"]],"content":"10 minutes.\n\nFireside chat between nostr:npub17yemy3hswcelmcdgjsfn4sns4wr4q5ptvj5h08qt4s7fy2qe3hdqsczs99 and nostr:npub1t6njrhtc9q3f5wdrwfrhpypq3kes5mpdx4u4rw9w2pxjanuxcpkqveagv3 followed by live set to close down #CultureShock.\n\nStreaming V4V at tunestr.io https://image.nostr.build/185e4c10dc2e46dacf6ad38fbc4319f52860fc3f96efb433be55bdb91fc225b8.jpg ","sig":"b0464529b2d2de2b4df911d47dbfe4aa31ac8f2db285b1a5930941cf1877425c14d6901d1be1bfaa13cd3d981d5e9a722debfc47c4f087d95da628a7035437ec"} -{"kind":1,"id":"f24aed86b493f266952ed35d4724582946cdf73985581f9986641f81fad6b73d","pubkey":"fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52","created_at":1707912005,"tags":[["p","fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52","wss://140.f7z.io/","mention"]],"content":"Releasing: Whynotstr 0.0.0 😂\n\nCollaborative editing, the left-side-of-the-curve approach\n\nTry it out: https://collab-lemon.vercel.app/\n\nObviously open source: https://github.com/pablof7z/collab\n\nnostr:nevent1qvzqqqqqqypzp75cf0tahv5z7plpdeaws7ex52nmnwgtwfr2g3m37r844evqrr6jqy88wumn8ghj7mn0wvhxcmmv9uq35amnwvaz7tms09exzmtfvshxv6tpw34xze3wvdhk6tcqyznqm6guz9k38dpqtc84s7jflec7m7wpzvtx2xjkjkm2xm89ucy2jhdcnur","sig":"6f562d733e50f5934dcf359a4f16dece1734302c0cc3a793ee2f08007ccb4ade3591373a633538617611f327feb7534ad4d11a8475163c7f734a01c63e52b79f"} +{"kind":1,"id":"0000000a5109c9747e3847282fcaef3d221d1be5e864ced7b2099d416a18d15a","pubkey":"7bdef7be22dd8e59f4600e044aa53a1cf975a9dc7d27df5833bc77db784a5805","created_at":1703869609,"tags":[["nonce","12912720851599460299","25"]],"content":"https://image.nostr.build/5eb40d3cae799bc572763b8f8bee95643344fa392d280efcb0fd28a935879e2a.png\n\nNostr is not dying.\nIt is just a physiological and healthy slowdown on the part of all those who have made this possible in such a short time, sharing extraordinary enthusiasm. This is necessary to regain a little energy, it will allow some things to be cleaned up and more focused goals to be set.\n\nIt is like the caterpillar that is about to become a butterfly, it has to stop moving, acting, doing all the time; it has to do one last silent work and rest, letting time go by. And then a new phase of life can begin.\n\nWe have an amazing 2024 ahead.\nThank you all, who have given so much and believe in Nostr.\n\nPS: an interesting cue suggested by this image, you cannot have both silk and butterfly, you have to choose: a precious and sophisticated ornament, or the living, colorful beauty of freedom.","sig":"16fe157fb13dba2474d510db5253edc409b465515371015a91b26b8f39e5aa873453bc366947c37463c49466f5fceb7dea0485432f979a03471c8f76b73e553c"} +{"kind":1,"id":"ac0cc72dfee39f41d94568f574e7b613d3979facbd7b477a16b52eb763db4b6e","pubkey":"2250f69694c2a43929e77e5de0f6a61ae5e37a1ee6d6a3baef1706ed9901248b","created_at":1703873865,"tags":[["r","https://zine.wavlake.com/2023-year-in-review/"]],"content":"It's been an incredible year for us here at Wavlake and we wanted to take a moment to look back and see how far we've come since launch. Read more.. https://zine.wavlake.com/2023-year-in-review/","sig":"189e354f67f48f3046fd762c83f9bf3a776d502d514e2839a1b459c30107a02453304ef695cdc7d254724041feec3800806b21eb76259df87144aaef821ace5b"} +{"kind":1,"id":"6215766c5aadfaf51488134682f7d28f237218b5405da2fc11d1fefe1ebf8154","pubkey":"4ce6abbd68dab6e9fdf6e8e9912a8e12f9b539e078c634c55a9bff2994a514dd","created_at":1703879775,"tags":[["imeta","url https://video.nostr.build/7b4e7c326fa4fcba58a40914ce9db4f060bd917878f2194f6d139948b085ebb9.mp4","blurhash eHD,QG_4ogMu_3to%O-:MwM_IWRjx^-pIUoe-;t7%Nt7%gV?M{WBxu","dim 480x268"],["t","zaps"],["t","powakili23"],["p","4f82bced42584a6acfced2a657b5acabc4f90d75a95ed3ff888f3b04b8928630"],["p","ce75bae2349804caa5f4de8ae8f775bb558135f412441d9e32f88e4226c5d165"],["p","94bd495b78f8f6e5aff8ebc90e052d3a409d1f9d82e43ab56ca2cafb81b18ddf"],["p","50ff5b7ebeac1cc0d03dc878be8a59f1b63d45a7d5e60ade4b6f6f31eca25954"],["p","f300cf2bdf9808ed229dfa468260753a0b179935bdb87612b6d4f5b9fe3fc7cf"],["r","https://geyser.fund/entry/2636"],["r","https://video.nostr.build/7b4e7c326fa4fcba58a40914ce9db4f060bd917878f2194f6d139948b085ebb9.mp4"]],"content":"POWA - HQ UPDATE - DEC 2023\nTLDR: plan to open January 2024, 1 million Sats to go to reach milestone. #zaps go to fund this project. ⚡️powa@geyser.fund\n\nHello,\n\nFirst and foremost, I’d like to thank you for the incredible support shown for this project. It’s been an absolute honor to oversee this Proof of Work initiative.\n\nI am thrilled to announce that we are right on track for the grand opening in January 2024.\n\nCurrently, we're just over 1 million Sats away from reaching our target for this phase.\n\nPlease take a moment to enjoy the video and stay tuned for further updates about POWA. \n\nMan Like Who?\nMan Like Kweks!\n🇹🇿⚡️💜🏔️\n#powakili23\nnostr:npub1f7ptem2ztp9x4n7w62n90ddv40z0jrt4490d8lug3uasfwyjsccqkknerm nostr:npub1ee6m4c35nqzv4f05m69w3am4hd2czd05zfzpm83jlz8yyfk969js78tfcv nostr:npub1jj75jkmclrmwttlca0ysupfd8fqf68uastjr4dtv5t90hqd33h0s4gcksp nostr:npub12rl4kl474swvp5paeputazje7xmr63d86hnq4hjtdahnrm9zt92qgq500s nostr:npub17vqv727lnqyw6g5alfrgycr48g930xf4hku8vy4k6n6mnl3lcl8sglecc5 \n\nhttps://geyser.fund/entry/2636 https://video.nostr.build/7b4e7c326fa4fcba58a40914ce9db4f060bd917878f2194f6d139948b085ebb9.mp4 ","sig":"97d13c17d91c319f343cc770222d6d4a0a714d0e7e4ef43373adaf215a4c077f0bdf12bac488c74dbd4d55718d46c17a617b93c8660736b70bcd61a8820ece67"} ... ``` @@ -168,6 +167,12 @@ listening at [wss://relay.damus.io wss://nos.lol wss://relay.nsecbunker.com]: bunker: bunker://f59911b561c37c90b01e9e5c2557307380835c83399756f4d62d8167227e420a?relay=wss%3A%2F%2Frelay.damus.io&relay=wss%3A%2F%2Fnos.lol&relay=wss%3A%2F%2Frelay.nsecbunker.com&secret=XuuiMbcLwuwL ``` +### generate a NIP-70 protected event with a date set to two weeks ago and some multi-value tags +```shell +~> nak event --ts 'two weeks ago' -t '-' -t 'e=f59911b561c37c90b01e9e5c2557307380835c83399756f4d62d8167227e420a;wss://relay.whatever.com;root;a9e0f110f636f3191644110c19a33448daf09d7cda9708a769e91b7e91340208' -t 'p=a9e0f110f636f3191644110c19a33448daf09d7cda9708a769e91b7e91340208;wss://p-relay.com' -c 'I know the future' +{"kind":1,"id":"f030fccd90c783858dfcee204af94826cf0f1c85d6fc85a0087e9e5172419393","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1719677535,"tags":[["-"],["e","f59911b561c37c90b01e9e5c2557307380835c83399756f4d62d8167227e420a","wss://relay.whatever.com","root","a9e0f110f636f3191644110c19a33448daf09d7cda9708a769e91b7e91340208"],["p","a9e0f110f636f3191644110c19a33448daf09d7cda9708a769e91b7e91340208","wss://p-relay.com"]],"content":"I know the future","sig":"8b36a74e29df8bc12bed66896820da6940d4d9409721b3ed2e910c838833a178cb45fd5bb1c6eb6adc66ab2808bfac9f6644a2c55a6570bb2ad90f221c9c7551"} +``` + ## contributing to this repository Use NIP-34 to send your patches to `naddr1qqpkucttqy28wumn8ghj7un9d3shjtnwdaehgu3wvfnsz9nhwden5te0wfjkccte9ehx7um5wghxyctwvsq3gamnwvaz7tmjv4kxz7fwv3sk6atn9e5k7q3q80cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsxpqqqpmej2wctpn`. From 7846960c4eb0e0dec9f3cff88bf923ee92a5e1e5 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sun, 14 Jul 2024 12:46:11 -0300 Subject: [PATCH 121/401] use latest fixed version of urfave/cli fork with reorder flags fixed. --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 7d75e66..e963847 100644 --- a/go.mod +++ b/go.mod @@ -46,4 +46,4 @@ require ( golang.org/x/text v0.16.0 // indirect ) -replace github.com/urfave/cli/v3 => github.com/fiatjaf/cli/v3 v3.0.0-20240712212113-3a8b0280e2c5 +replace github.com/urfave/cli/v3 => github.com/fiatjaf/cli/v3 v3.0.0-20240713223955-ebd1e3ed26b9 diff --git a/go.sum b/go.sum index 68d559c..4a0f631 100644 --- a/go.sum +++ b/go.sum @@ -44,8 +44,8 @@ github.com/elliotchance/pie/v2 v2.7.0 h1:FqoIKg4uj0G/CrLGuMS9ejnFKa92lxE1dEgBD3p github.com/elliotchance/pie/v2 v2.7.0/go.mod h1:18t0dgGFH006g4eVdDtWfgFZPQEgl10IoEO8YWEq3Og= 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/cli/v3 v3.0.0-20240712212113-3a8b0280e2c5 h1:yhTRU02Hn1jwq50uUKRxbPZQg0PODe37s73IJNsCJb0= -github.com/fiatjaf/cli/v3 v3.0.0-20240712212113-3a8b0280e2c5/go.mod h1:Z1ItyMma7t6I7zHG9OpbExhHQOSkFf/96n+mAZ9MtVI= +github.com/fiatjaf/cli/v3 v3.0.0-20240713223955-ebd1e3ed26b9 h1:jsirhYQc6AVJ01BgHSLSYOtz5cA2IhrsVOhC2rJK3PA= +github.com/fiatjaf/cli/v3 v3.0.0-20240713223955-ebd1e3ed26b9/go.mod h1:Z1ItyMma7t6I7zHG9OpbExhHQOSkFf/96n+mAZ9MtVI= github.com/fiatjaf/eventstore v0.2.16 h1:NR64mnyUT5nJR8Sj2AwJTd1Hqs5kKJcCFO21ggUkvWg= github.com/fiatjaf/eventstore v0.2.16/go.mod h1:rUc1KhVufVmC+HUOiuPweGAcvG6lEOQCkRCn2Xn5VRA= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= From 09ed2a040a03a69ff4a7f5fe3cc2c62b97f87151 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sun, 14 Jul 2024 16:18:34 -0300 Subject: [PATCH 122/401] make tests work again. --- example_test.go | 86 +++++++++++++------------------------------------ 1 file changed, 23 insertions(+), 63 deletions(-) diff --git a/example_test.go b/example_test.go index 9b6cd28..fe085f4 100644 --- a/example_test.go +++ b/example_test.go @@ -2,9 +2,12 @@ package main import ( "context" - "os" ) +// these tests are tricky because commands and flags are declared as globals and values set in one call may persist +// to the next. for example, if in the first test we set --limit 2 then doesn't specify --limit in the second then +// it will still return true for cmd.IsSet("limit") and then we will set .LimitZero = true + var ctx = context.Background() func ExampleEventBasic() { @@ -14,22 +17,22 @@ func ExampleEventBasic() { } // (for some reason there can only be one test dealing with stdin in the suite otherwise it halts) -func ExampleEventParsingFromStdin() { - prevStdin := os.Stdin - defer func() { os.Stdin = prevStdin }() - r, w, _ := os.Pipe() - os.Stdin = r - w.WriteString("{\"content\":\"hello world\"}\n{\"content\":\"hello sun\"}\n") - app.Run(ctx, []string{"nak", "event", "-t", "t=spam", "--ts", "1699485669"}) - // Output: - // {"id":"bda134f9077c11973afe6aa5a1cc6f5bcea01c40d318b8f91dcb8e50507cfa52","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1699485669,"kind":1,"tags":[["t","spam"]],"content":"hello world","sig":"7552454bb8e7944230142634e3e34ac7468bad9b21ed6909da572c611018dff1d14d0792e98b5806f6330edc51e09efa6d0b66a9694dc34606c70f4e580e7493"} - // {"id":"879c36ec73acca288825b53585389581d3836e7f0fe4d46e5eba237ca56d6af5","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1699485669,"kind":1,"tags":[["t","spam"]],"content":"hello sun","sig":"6c7e6b13ebdf931d26acfdd00bec2ec1140ddaf8d1ed61453543a14e729a460fe36c40c488ccb194a0e1ab9511cb6c36741485f501bdb93c39ca4c51bc59cbd4"} -} +// func ExampleEventParsingFromStdin() { +// prevStdin := os.Stdin +// defer func() { os.Stdin = prevStdin }() +// r, w, _ := os.Pipe() +// os.Stdin = r +// w.WriteString("{\"content\":\"hello world\"}\n{\"content\":\"hello sun\"}\n") +// app.Run(ctx, []string{"nak", "event", "-t", "t=spam", "--ts", "1699485669"}) +// // Output: +// // {"id":"bda134f9077c11973afe6aa5a1cc6f5bcea01c40d318b8f91dcb8e50507cfa52","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1699485669,"kind":1,"tags":[["t","spam"]],"content":"hello world","sig":"7552454bb8e7944230142634e3e34ac7468bad9b21ed6909da572c611018dff1d14d0792e98b5806f6330edc51e09efa6d0b66a9694dc34606c70f4e580e7493"} +// // {"id":"879c36ec73acca288825b53585389581d3836e7f0fe4d46e5eba237ca56d6af5","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1699485669,"kind":1,"tags":[["t","spam"]],"content":"hello sun","sig":"6c7e6b13ebdf931d26acfdd00bec2ec1140ddaf8d1ed61453543a14e729a460fe36c40c488ccb194a0e1ab9511cb6c36741485f501bdb93c39ca4c51bc59cbd4"} +// } func ExampleEventComplex() { app.Run(ctx, []string{"nak", "event", "--ts", "1699485669", "-k", "11", "-c", "skjdbaskd", "--sec", "17", "-t", "t=spam", "-e", "36d88cf5fcc449f2390a424907023eda7a74278120eebab8d02797cd92e7e29c", "-t", "r=https://abc.def?name=foobar;nothing"}) // Output: - // {"id":"19aba166dcf354bf5ef64f4afe69ada1eb851495001ee05e07d393ee8c8ea179","pubkey":"2fa2104d6b38d11b0230010559879124e42ab8dfeff5ff29dc9cdadd4ecacc3f","created_at":1699485669,"kind":11,"tags":[["t","spam"],["r","https://abc.def?name=foobar","nothing"],["e","36d88cf5fcc449f2390a424907023eda7a74278120eebab8d02797cd92e7e29c"]],"content":"skjdbaskd","sig":"cf452def4a68341c897c3fc96fa34dc6895a5b8cc266d4c041bcdf758ec992ec5adb8b0179e98552aaaf9450526a26d7e62e413b15b1c57e0cfc8db6b29215d7"} + // {"kind":11,"id":"19aba166dcf354bf5ef64f4afe69ada1eb851495001ee05e07d393ee8c8ea179","pubkey":"2fa2104d6b38d11b0230010559879124e42ab8dfeff5ff29dc9cdadd4ecacc3f","created_at":1699485669,"tags":[["t","spam"],["r","https://abc.def?name=foobar","nothing"],["e","36d88cf5fcc449f2390a424907023eda7a74278120eebab8d02797cd92e7e29c"]],"content":"skjdbaskd","sig":"cf452def4a68341c897c3fc96fa34dc6895a5b8cc266d4c041bcdf758ec992ec5adb8b0179e98552aaaf9450526a26d7e62e413b15b1c57e0cfc8db6b29215d7"} } func ExampleEncode() { @@ -70,17 +73,11 @@ func ExampleReq() { // ["REQ","nak",{"kinds":[1],"authors":["2fa2104d6b38d11b0230010559879124e42ab8dfeff5ff29dc9cdadd4ecacc3f"],"limit":18,"#e":["aec4de6d051a7c2b6ca2d087903d42051a31e07fb742f1240970084822de10a6"]}] } -func ExampleReqIdFromRelay() { - app.Run(ctx, []string{"nak", "req", "-i", "3ff0d19493e661faa90ac06b5d8963ef1e48fe780368fe67ed0fd410df8a6dd5", "wss://nostr.wine"}) - // Output: - // {"id":"3ff0d19493e661faa90ac06b5d8963ef1e48fe780368fe67ed0fd410df8a6dd5","pubkey":"3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d","created_at":1710759386,"kind":1,"tags":[],"content":"Nostr was coopted by our the corporate overlords. It is now featured in https://www.iana.org/assignments/well-known-uris/well-known-uris.xhtml.","sig":"faaec167cca4de50b562b7702e8854e2023f0ccd5f36d1b95b6eac20d352206342d6987e9516d283068c768e94dbe8858e2990c3e05405e707fb6fb771ef92f9"} -} - func ExampleMultipleFetch() { app.Run(ctx, []string{"nak", "fetch", "naddr1qqyrgcmyxe3kvefhqyxhwumn8ghj7mn0wvhxcmmvqgs9kqvr4dkruv3t7n2pc6e6a7v9v2s5fprmwjv4gde8c4fe5y29v0srqsqqql9ngrt6tu", "nevent1qyd8wumn8ghj7urewfsk66ty9enxjct5dfskvtnrdakj7qgmwaehxw309aex2mrp0yh8wetnw3jhymnzw33jucm0d5hszxthwden5te0wfjkccte9eekummjwsh8xmmrd9skctcpzamhxue69uhkzarvv9ejumn0wd68ytnvv9hxgtcqyqllp5v5j0nxr74fptqxkhvfv0h3uj870qpk3ln8a58agyxl3fka296ewr8"}) // Output: - // {"id":"9ae5014573fc75ced00b343868d2cd9343ebcbbae50591c6fa8ae1cd99568f05","pubkey":"5b0183ab6c3e322bf4d41c6b3aef98562a144847b7499543727c5539a114563e","created_at":1707764605,"kind":31923,"tags":[["d","4cd6cfe7"],["name","Nostr PHX Presents Culture Shock"],["description","Nostr PHX presents Culture Shock the first Value 4 Value Cultural Event in Downtown Phoenix. We will showcase the power of Nostr + Bitcoin / Lightning with a full day of education, food, drinks, conversation, vendors and best of all, a live convert which will stream globally for the world to zap. "],["start","1708185600"],["end","1708228800"],["start_tzid","America/Phoenix"],["p","5b0183ab6c3e322bf4d41c6b3aef98562a144847b7499543727c5539a114563e","","host"],["location","Hello Merch, 850 W Lincoln St, Phoenix, AZ 85007, USA","Hello Merch","850 W Lincoln St, Phoenix, AZ 85007, USA"],["address","Hello Merch, 850 W Lincoln St, Phoenix, AZ 85007, USA","Hello Merch","850 W Lincoln St, Phoenix, AZ 85007, USA"],["g","9tbq1rzn"],["image","https://flockstr.s3.amazonaws.com/event/15vSaiscDhVH1KBXhA0i8"],["about","Nostr PHX presents Culture Shock : the first Value 4 Value Cultural Event in Downtown Phoenix. We will showcase the power of Nostr + Bitcoin / Lightning with a full day of education, conversation, food and goods which will be capped off with a live concert streamed globally for the world to boost \u0026 zap. \n\nWe strive to source local vendors, local artists, local partnerships. Please reach out to us if you are interested in participating in this historic event. "],["calendar","31924:5b0183ab6c3e322bf4d41c6b3aef98562a144847b7499543727c5539a114563e:1f238c94"]],"content":"Nostr PHX presents Culture Shock : the first Value 4 Value Cultural Event in Downtown Phoenix. We will showcase the power of Nostr + Bitcoin / Lightning with a full day of education, conversation, food and goods which will be capped off with a live concert streamed globally for the world to boost \u0026 zap. \n\nWe strive to source local vendors, local artists, local partnerships. Please reach out to us if you are interested in participating in this historic event. ","sig":"f676629d1414d96b464644de6babde0c96bd21ef9b41ba69ad886a1d13a942b855b715b22ccf38bc07fead18d3bdeee82d9e3825cf6f003fb5ff1766d95c70a0"} - // {"id":"3ff0d19493e661faa90ac06b5d8963ef1e48fe780368fe67ed0fd410df8a6dd5","pubkey":"3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d","created_at":1710759386,"kind":1,"tags":[],"content":"Nostr was coopted by our the corporate overlords. It is now featured in https://www.iana.org/assignments/well-known-uris/well-known-uris.xhtml.","sig":"faaec167cca4de50b562b7702e8854e2023f0ccd5f36d1b95b6eac20d352206342d6987e9516d283068c768e94dbe8858e2990c3e05405e707fb6fb771ef92f9"} + // {"kind":31923,"id":"9ae5014573fc75ced00b343868d2cd9343ebcbbae50591c6fa8ae1cd99568f05","pubkey":"5b0183ab6c3e322bf4d41c6b3aef98562a144847b7499543727c5539a114563e","created_at":1707764605,"tags":[["d","4cd6cfe7"],["name","Nostr PHX Presents Culture Shock"],["description","Nostr PHX presents Culture Shock the first Value 4 Value Cultural Event in Downtown Phoenix. We will showcase the power of Nostr + Bitcoin / Lightning with a full day of education, food, drinks, conversation, vendors and best of all, a live convert which will stream globally for the world to zap. "],["start","1708185600"],["end","1708228800"],["start_tzid","America/Phoenix"],["p","5b0183ab6c3e322bf4d41c6b3aef98562a144847b7499543727c5539a114563e","","host"],["location","Hello Merch, 850 W Lincoln St, Phoenix, AZ 85007, USA","Hello Merch","850 W Lincoln St, Phoenix, AZ 85007, USA"],["address","Hello Merch, 850 W Lincoln St, Phoenix, AZ 85007, USA","Hello Merch","850 W Lincoln St, Phoenix, AZ 85007, USA"],["g","9tbq1rzn"],["image","https://flockstr.s3.amazonaws.com/event/15vSaiscDhVH1KBXhA0i8"],["about","Nostr PHX presents Culture Shock : the first Value 4 Value Cultural Event in Downtown Phoenix. We will showcase the power of Nostr + Bitcoin / Lightning with a full day of education, conversation, food and goods which will be capped off with a live concert streamed globally for the world to boost \u0026 zap. \n\nWe strive to source local vendors, local artists, local partnerships. Please reach out to us if you are interested in participating in this historic event. "],["calendar","31924:5b0183ab6c3e322bf4d41c6b3aef98562a144847b7499543727c5539a114563e:1f238c94"]],"content":"Nostr PHX presents Culture Shock : the first Value 4 Value Cultural Event in Downtown Phoenix. We will showcase the power of Nostr + Bitcoin / Lightning with a full day of education, conversation, food and goods which will be capped off with a live concert streamed globally for the world to boost \u0026 zap. \n\nWe strive to source local vendors, local artists, local partnerships. Please reach out to us if you are interested in participating in this historic event. ","sig":"f676629d1414d96b464644de6babde0c96bd21ef9b41ba69ad886a1d13a942b855b715b22ccf38bc07fead18d3bdeee82d9e3825cf6f003fb5ff1766d95c70a0"} + // {"kind":1,"id":"3ff0d19493e661faa90ac06b5d8963ef1e48fe780368fe67ed0fd410df8a6dd5","pubkey":"3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d","created_at":1710759386,"tags":[],"content":"Nostr was coopted by our the corporate overlords. It is now featured in https://www.iana.org/assignments/well-known-uris/well-known-uris.xhtml.","sig":"faaec167cca4de50b562b7702e8854e2023f0ccd5f36d1b95b6eac20d352206342d6987e9516d283068c768e94dbe8858e2990c3e05405e707fb6fb771ef92f9"} } func ExampleKeyPublic() { @@ -91,50 +88,13 @@ func ExampleKeyPublic() { } func ExampleKeyDecrypt() { - app.Run(ctx, []string{"nak", "key", "decrypt", "ncryptsec1qggfep0m5ythsegkmwfrhhx2zx5gazyhdygvlngcds4wsgdpzfy6nr0exy0pdk0ydwrqyhndt2trtwcgwwag0ja3aqclzptfxxqvprdyaz3qfrmazpecx2ff6dph5mfdjnh5sw8sgecul32eru6xet34", "banana"}) + app.Run(ctx, []string{"nak", "key", "decrypt", "ncryptsec1qgg2gx2a7hxpsse2zulrv7m8qwccvl3mh8e9k8vtz3wpyrwuuclaq73gz7ddt5kpa93qyfhfjakguuf8uhw90jn6mszh7kqeh9mxzlyw8hy75fluzx4h75frwmu2yngsq7hx7w32d0vdyxyns5g6rqft", "banana"}) // Output: - // nsec180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsgyumg0 + // 718d756f60cf5179ef35b39dc6db3ff58f04c0734f81f6d4410f0b047ddf9029 } -func ExampleRelay() { - app.Run(ctx, []string{"nak", "relay", "relay.nos.social", "pyramid.fiatjaf.com"}) +func ExampleReqIdFromRelay() { + app.Run(ctx, []string{"nak", "req", "-i", "20a6606ed548fe7107533cf3416ce1aa5e957c315c2a40249e12bd9873dca7da", "--limit", "1", "nos.lol"}) // Output: - // { - // "name": "nos.social strfry relay", - // "description": "This is a strfry instance handled by nos.social", - // "pubkey": "89ef92b9ebe6dc1e4ea398f6477f227e95429627b0a33dc89b640e137b256be5", - // "contact": "https://nos.social", - // "supported_nips": [ - // 1, - // 2, - // 4, - // 9, - // 11, - // 12, - // 16, - // 20, - // 22, - // 28, - // 33, - // 40 - // ], - // "software": "git+https://github.com/hoytech/strfry.git", - // "version": "0.9.4", - // "icon": "" - // } - // { - // "name": "the fiatjaf pyramid", - // "description": "a relay just for the coolest of the coolest", - // "pubkey": "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d", - // "contact": "", - // "supported_nips": [], - // "software": "https://github.com/fiatjaf/khatru", - // "version": "n/a", - // "limitation": { - // "auth_required": false, - // "payment_required": false, - // "restricted_writes": true - // }, - // "icon": "https://clipart-library.com/images_k/pyramid-transparent/pyramid-transparent-19.png" - // } + // {"kind":1,"id":"20a6606ed548fe7107533cf3416ce1aa5e957c315c2a40249e12bd9873dca7da","pubkey":"dd664d5e4016433a8cd69f005ae1480804351789b59de5af06276de65633d319","created_at":1720972243,"tags":[["e","bdb2210fe6d9c4b141f08b5d9d1147cd8e1dc1d82f552a889ab171894249d21d","","root"],["e","c2e45f09e7d62ed12afe2b8b1bcf6be823b560a53ef06905365a78979a1b9ee3","","reply"],["p","036533caa872376946d4e4fdea4c1a0441eda38ca2d9d9417bb36006cbaabf58","","mention"]],"content":"Yeah, so bizarre, but I guess most people are meant to be serfs.","sig":"9ea7488415c250d0ac8fcb2219f211cb369dddf2a75c0f63d2db773c6dc1ef9dd9679b8941c0e7551744ea386afebad2024be8ce3ac418d4f47c95e7491af38e"} } From 813ab3b6ac238604e000e390997c7ae399986442 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sun, 14 Jul 2024 20:34:44 -0300 Subject: [PATCH 123/401] test flags after args. --- example_test.go | 18 ++++++++++++++++++ go.mod | 2 +- go.sum | 4 ++-- req.go | 1 + 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/example_test.go b/example_test.go index fe085f4..f7b2b34 100644 --- a/example_test.go +++ b/example_test.go @@ -98,3 +98,21 @@ func ExampleReqIdFromRelay() { // Output: // {"kind":1,"id":"20a6606ed548fe7107533cf3416ce1aa5e957c315c2a40249e12bd9873dca7da","pubkey":"dd664d5e4016433a8cd69f005ae1480804351789b59de5af06276de65633d319","created_at":1720972243,"tags":[["e","bdb2210fe6d9c4b141f08b5d9d1147cd8e1dc1d82f552a889ab171894249d21d","","root"],["e","c2e45f09e7d62ed12afe2b8b1bcf6be823b560a53ef06905365a78979a1b9ee3","","reply"],["p","036533caa872376946d4e4fdea4c1a0441eda38ca2d9d9417bb36006cbaabf58","","mention"]],"content":"Yeah, so bizarre, but I guess most people are meant to be serfs.","sig":"9ea7488415c250d0ac8fcb2219f211cb369dddf2a75c0f63d2db773c6dc1ef9dd9679b8941c0e7551744ea386afebad2024be8ce3ac418d4f47c95e7491af38e"} } + +func ExampleReqWithFlagsAfter1() { + app.Run(ctx, []string{"nak", "req", "nos.lol", "-i", "20a6606ed548fe7107533cf3416ce1aa5e957c315c2a40249e12bd9873dca7da", "--limit", "1"}) + // Output: + // {"kind":1,"id":"20a6606ed548fe7107533cf3416ce1aa5e957c315c2a40249e12bd9873dca7da","pubkey":"dd664d5e4016433a8cd69f005ae1480804351789b59de5af06276de65633d319","created_at":1720972243,"tags":[["e","bdb2210fe6d9c4b141f08b5d9d1147cd8e1dc1d82f552a889ab171894249d21d","","root"],["e","c2e45f09e7d62ed12afe2b8b1bcf6be823b560a53ef06905365a78979a1b9ee3","","reply"],["p","036533caa872376946d4e4fdea4c1a0441eda38ca2d9d9417bb36006cbaabf58","","mention"]],"content":"Yeah, so bizarre, but I guess most people are meant to be serfs.","sig":"9ea7488415c250d0ac8fcb2219f211cb369dddf2a75c0f63d2db773c6dc1ef9dd9679b8941c0e7551744ea386afebad2024be8ce3ac418d4f47c95e7491af38e"} +} + +func ExampleReqWithFlagsAfter2() { + app.Run(ctx, []string{"nak", "req", "-e", "893d4c10f1c230240812c6bdf9ad877eed1e29e87029d153820c24680bb183b1", "nostr.mom", "--author", "2a7dcf382bcc96a393ada5c975f500393b3f7be6e466bff220aa161ad6b15eb6", "--limit", "1", "-k", "7"}) + // Output: + // {"kind":7,"id":"9b4868b068ea34ae51092807586c4541b3569d9efc23862aea48ef13de275857","pubkey":"2a7dcf382bcc96a393ada5c975f500393b3f7be6e466bff220aa161ad6b15eb6","created_at":1720987327,"tags":[["e","893d4c10f1c230240812c6bdf9ad877eed1e29e87029d153820c24680bb183b1"],["p","1e978baae414eee990dba992871549ad4a099b9d6f7e71c8059b254ea024dddc"],["k","1"]],"content":"❤️","sig":"7eddd112c642ecdb031330dadc021790642b3c10ecc64158ba3ae63edd798b26afb9b5a3bba72835ce171719a724de1472f65c9b3339b6bead0ce2846f93dfc9"} +} + +func ExampleReqWithFlagsAfter3() { + app.Run(ctx, []string{"nak", "req", "--limit", "1", "pyramid.fiatjaf.com", "-a", "3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24", "-qp", "3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24", "-e", "9f3c1121c96edf17d84b9194f74d66d012b28c4e25b3ef190582c76b8546a188"}) + // Output: + // {"kind":1,"id":"101572c80ebdc963dab8440f6307387a3023b6d90f7e495d6c5ee1ef77045a67","pubkey":"3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24","created_at":1720987305,"tags":[["e","ceacdc29fa7a0b51640b30d2424e188215460617db5ba5bb52d3fbf0094eebb3","","root"],["e","9f3c1121c96edf17d84b9194f74d66d012b28c4e25b3ef190582c76b8546a188","","reply"],["p","3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24"],["p","6b96c3eb36c6cd457d906bbaafe7b36cacfb8bcc4ab235be6eab3b71c6669251"]],"content":"Nope. I grew up playing in the woods. Never once saw a bear in the woods. If I did, I'd probably shiy my pants, then scream at it like I was a crazy person with my arms above my head to make me seem huge.","sig":"b098820b4a5635865cada9f9a5813be2bc6dd7180e16e590cf30e07916d8ed6ed98ab38b64f3bfba12d88d37335f229f7ef8c084bc48132e936c664a54d3e650"} +} diff --git a/go.mod b/go.mod index e963847..a2c63bf 100644 --- a/go.mod +++ b/go.mod @@ -46,4 +46,4 @@ require ( golang.org/x/text v0.16.0 // indirect ) -replace github.com/urfave/cli/v3 => github.com/fiatjaf/cli/v3 v3.0.0-20240713223955-ebd1e3ed26b9 +replace github.com/urfave/cli/v3 => github.com/fiatjaf/cli/v3 v3.0.0-20240714232133-bb036558919f diff --git a/go.sum b/go.sum index 4a0f631..d5b18e0 100644 --- a/go.sum +++ b/go.sum @@ -44,8 +44,8 @@ github.com/elliotchance/pie/v2 v2.7.0 h1:FqoIKg4uj0G/CrLGuMS9ejnFKa92lxE1dEgBD3p github.com/elliotchance/pie/v2 v2.7.0/go.mod h1:18t0dgGFH006g4eVdDtWfgFZPQEgl10IoEO8YWEq3Og= 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/cli/v3 v3.0.0-20240713223955-ebd1e3ed26b9 h1:jsirhYQc6AVJ01BgHSLSYOtz5cA2IhrsVOhC2rJK3PA= -github.com/fiatjaf/cli/v3 v3.0.0-20240713223955-ebd1e3ed26b9/go.mod h1:Z1ItyMma7t6I7zHG9OpbExhHQOSkFf/96n+mAZ9MtVI= +github.com/fiatjaf/cli/v3 v3.0.0-20240714232133-bb036558919f h1:SQ5W4q4HfpAPA8e8uPADqs4dhI6VvVwUq00DtlpHIt0= +github.com/fiatjaf/cli/v3 v3.0.0-20240714232133-bb036558919f/go.mod h1:Z1ItyMma7t6I7zHG9OpbExhHQOSkFf/96n+mAZ9MtVI= github.com/fiatjaf/eventstore v0.2.16 h1:NR64mnyUT5nJR8Sj2AwJTd1Hqs5kKJcCFO21ggUkvWg= github.com/fiatjaf/eventstore v0.2.16/go.mod h1:rUc1KhVufVmC+HUOiuPweGAcvG6lEOQCkRCn2Xn5VRA= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= diff --git a/req.go b/req.go index e8aed05..d00ce37 100644 --- a/req.go +++ b/req.go @@ -249,6 +249,7 @@ example: if c.Bool("stream") { fn = pool.SubMany } + for ie := range fn(ctx, relayUrls, nostr.Filters{filter}) { stdout(ie.Event) } From 809865ca0ccfd28b82b8c3f038b5891d4c58e81f Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 16 Jul 2024 13:26:54 -0300 Subject: [PATCH 124/401] relay management: adhere to NIP-98 stupidity. --- relay.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/relay.go b/relay.go index 555990b..703898b 100644 --- a/relay.go +++ b/relay.go @@ -10,7 +10,6 @@ import ( "fmt" "io" "net/http" - "strings" "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/nip11" @@ -131,13 +130,13 @@ var relay = &cli.Command{ } // Authorization - hostname := strings.Split(strings.Split(httpUrl, "://")[1], "/")[0] payloadHash := sha256.Sum256(reqj) authEvent := nostr.Event{ Kind: 27235, CreatedAt: nostr.Now(), Tags: nostr.Tags{ - {"host", hostname}, + {"u", httpUrl}, + {"method", "POST"}, {"payload", hex.EncodeToString(payloadHash[:])}, }, } From 9f62d4679f330d762f017148551cd26e33aa659f Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Wed, 17 Jul 2024 13:16:40 -0300 Subject: [PATCH 125/401] increase stdin line limit. --- helpers.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helpers.go b/helpers.go index e3b1348..a761503 100644 --- a/helpers.go +++ b/helpers.go @@ -75,7 +75,7 @@ func writeStdinLinesOrNothing(ch chan string) (hasStdinLines bool) { // piped go func() { scanner := bufio.NewScanner(os.Stdin) - scanner.Buffer(make([]byte, 16*1024), 256*1024) + scanner.Buffer(make([]byte, 16*1024*1024), 256*1024*1024) hasEmittedAtLeastOne := false for scanner.Scan() { ch <- strings.TrimSpace(scanner.Text()) From ec2e214c02343806ef0cda7d835fe3f1f07f4693 Mon Sep 17 00:00:00 2001 From: jeremyd Date: Thu, 27 Jun 2024 00:22:01 -0700 Subject: [PATCH 126/401] Allow setting private key via ENV variable --- helpers.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/helpers.go b/helpers.go index a761503..d7832e7 100644 --- a/helpers.go +++ b/helpers.go @@ -195,7 +195,13 @@ func gatherSecretKeyOrBunkerFromArguments(ctx context.Context, c *cli.Command) ( }) return "", bunker, err } + + // Check in the Env for the secret key first sec := c.String("sec") + if env, ok := os.LookupEnv("NOSTR_PRIVATE_KEY"); ok { + sec = env + } + 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") From 48c0e342e3e5ffc8f75c686407a71a7b6980d83d Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 23 Jul 2024 15:21:14 -0300 Subject: [PATCH 127/401] only look for private key in environment variable if --sec is not given. --- helpers.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/helpers.go b/helpers.go index d7832e7..830fe7f 100644 --- a/helpers.go +++ b/helpers.go @@ -196,10 +196,13 @@ func gatherSecretKeyOrBunkerFromArguments(ctx context.Context, c *cli.Command) ( return "", bunker, err } - // Check in the Env for the secret key first sec := c.String("sec") - if env, ok := os.LookupEnv("NOSTR_PRIVATE_KEY"); ok { - sec = env + + // check in the environment for the secret key + if sec == "" { + if key, ok := os.LookupEnv("NOSTR_PRIVATE_KEY"); ok { + sec = key + } } if c.Bool("prompt-sec") { From 220fe84f1b5c5808fa48151d40317009a5240bf1 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 23 Jul 2024 15:23:07 -0300 Subject: [PATCH 128/401] hardcode our fork of urfave/cli because go is stupid. fixes https://github.com/fiatjaf/nak/issues/26 --- bunker.go | 2 +- count.go | 2 +- decode.go | 2 +- encode.go | 2 +- event.go | 2 +- fetch.go | 2 +- flags.go | 2 +- go.mod | 8 +++----- go.sum | 2 ++ helpers.go | 2 +- key.go | 2 +- main.go | 2 +- relay.go | 2 +- req.go | 2 +- verify.go | 2 +- 15 files changed, 18 insertions(+), 18 deletions(-) diff --git a/bunker.go b/bunker.go index 4732af5..b5e38de 100644 --- a/bunker.go +++ b/bunker.go @@ -14,7 +14,7 @@ import ( "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/fiatjaf/cli/v3" "golang.org/x/exp/slices" ) diff --git a/count.go b/count.go index cbb3540..5b5ebe3 100644 --- a/count.go +++ b/count.go @@ -8,7 +8,7 @@ import ( "strings" "github.com/nbd-wtf/go-nostr" - "github.com/urfave/cli/v3" + "github.com/fiatjaf/cli/v3" ) var count = &cli.Command{ diff --git a/decode.go b/decode.go index 734041a..d57c80e 100644 --- a/decode.go +++ b/decode.go @@ -9,7 +9,7 @@ import ( "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/nip19" sdk "github.com/nbd-wtf/nostr-sdk" - "github.com/urfave/cli/v3" + "github.com/fiatjaf/cli/v3" ) var decode = &cli.Command{ diff --git a/encode.go b/encode.go index 8e86e90..027193a 100644 --- a/encode.go +++ b/encode.go @@ -6,7 +6,7 @@ import ( "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/nip19" - "github.com/urfave/cli/v3" + "github.com/fiatjaf/cli/v3" ) var encode = &cli.Command{ diff --git a/event.go b/event.go index becd869..443e6ea 100644 --- a/event.go +++ b/event.go @@ -11,7 +11,7 @@ import ( "github.com/mailru/easyjson" "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/nip19" - "github.com/urfave/cli/v3" + "github.com/fiatjaf/cli/v3" "golang.org/x/exp/slices" ) diff --git a/fetch.go b/fetch.go index b71fd9a..2ddc4b9 100644 --- a/fetch.go +++ b/fetch.go @@ -6,7 +6,7 @@ import ( "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/nip19" sdk "github.com/nbd-wtf/nostr-sdk" - "github.com/urfave/cli/v3" + "github.com/fiatjaf/cli/v3" ) var fetch = &cli.Command{ diff --git a/flags.go b/flags.go index 5e80b2d..1f332af 100644 --- a/flags.go +++ b/flags.go @@ -8,7 +8,7 @@ import ( "github.com/markusmobius/go-dateparser" "github.com/nbd-wtf/go-nostr" - "github.com/urfave/cli/v3" + "github.com/fiatjaf/cli/v3" ) type NaturalTimeFlag = cli.FlagBase[nostr.Timestamp, struct{}, naturalTimeValue] diff --git a/go.mod b/go.mod index a2c63bf..636588e 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,8 @@ module github.com/fiatjaf/nak -go 1.21 +go 1.22 -toolchain go1.21.0 +toolchain go1.22.4 require ( github.com/btcsuite/btcd/btcec/v2 v2.3.3 @@ -13,7 +13,6 @@ require ( github.com/markusmobius/go-dateparser v1.2.3 github.com/nbd-wtf/go-nostr v0.34.2 github.com/nbd-wtf/nostr-sdk v0.0.5 - github.com/urfave/cli/v3 v3.0.0-alpha9 golang.org/x/exp v0.0.0-20240707233637-46b078467d37 ) @@ -24,6 +23,7 @@ require ( github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect github.com/elliotchance/pie/v2 v2.7.0 // indirect + github.com/fiatjaf/cli/v3 v3.0.0-20240723181502-e7dd498b16ae // indirect github.com/fiatjaf/eventstore v0.2.16 // indirect github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/pool v0.2.1 // indirect @@ -45,5 +45,3 @@ require ( golang.org/x/sys v0.22.0 // indirect golang.org/x/text v0.16.0 // indirect ) - -replace github.com/urfave/cli/v3 => github.com/fiatjaf/cli/v3 v3.0.0-20240714232133-bb036558919f diff --git a/go.sum b/go.sum index d5b18e0..2e3f780 100644 --- a/go.sum +++ b/go.sum @@ -46,6 +46,8 @@ 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/cli/v3 v3.0.0-20240714232133-bb036558919f h1:SQ5W4q4HfpAPA8e8uPADqs4dhI6VvVwUq00DtlpHIt0= github.com/fiatjaf/cli/v3 v3.0.0-20240714232133-bb036558919f/go.mod h1:Z1ItyMma7t6I7zHG9OpbExhHQOSkFf/96n+mAZ9MtVI= +github.com/fiatjaf/cli/v3 v3.0.0-20240723181502-e7dd498b16ae h1:0B/1dU3YECIbPoBIRTQ4c0scZCNz9TVHtQpiODGrTTo= +github.com/fiatjaf/cli/v3 v3.0.0-20240723181502-e7dd498b16ae/go.mod h1:aAWPO4bixZZxPtOnH6K3q4GbQ0jftUNDW9Oa861IRew= github.com/fiatjaf/eventstore v0.2.16 h1:NR64mnyUT5nJR8Sj2AwJTd1Hqs5kKJcCFO21ggUkvWg= github.com/fiatjaf/eventstore v0.2.16/go.mod h1:rUc1KhVufVmC+HUOiuPweGAcvG6lEOQCkRCn2Xn5VRA= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= diff --git a/helpers.go b/helpers.go index 830fe7f..b15f12a 100644 --- a/helpers.go +++ b/helpers.go @@ -17,7 +17,7 @@ import ( "github.com/nbd-wtf/go-nostr/nip19" "github.com/nbd-wtf/go-nostr/nip46" "github.com/nbd-wtf/go-nostr/nip49" - "github.com/urfave/cli/v3" + "github.com/fiatjaf/cli/v3" ) const ( diff --git a/key.go b/key.go index ad66e63..86fee3b 100644 --- a/key.go +++ b/key.go @@ -14,7 +14,7 @@ import ( "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/nip19" "github.com/nbd-wtf/go-nostr/nip49" - "github.com/urfave/cli/v3" + "github.com/fiatjaf/cli/v3" ) var key = &cli.Command{ diff --git a/main.go b/main.go index 9226f3f..3861933 100644 --- a/main.go +++ b/main.go @@ -4,7 +4,7 @@ import ( "context" "os" - "github.com/urfave/cli/v3" + "github.com/fiatjaf/cli/v3" ) var app = &cli.Command{ diff --git a/relay.go b/relay.go index 703898b..9e674d8 100644 --- a/relay.go +++ b/relay.go @@ -14,7 +14,7 @@ import ( "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/nip11" "github.com/nbd-wtf/go-nostr/nip86" - "github.com/urfave/cli/v3" + "github.com/fiatjaf/cli/v3" ) var relay = &cli.Command{ diff --git a/req.go b/req.go index d00ce37..61232a8 100644 --- a/req.go +++ b/req.go @@ -9,7 +9,7 @@ import ( "github.com/mailru/easyjson" "github.com/nbd-wtf/go-nostr" - "github.com/urfave/cli/v3" + "github.com/fiatjaf/cli/v3" ) const CATEGORY_FILTER_ATTRIBUTES = "FILTER ATTRIBUTES" diff --git a/verify.go b/verify.go index 7860b8d..f019f8e 100644 --- a/verify.go +++ b/verify.go @@ -5,7 +5,7 @@ import ( "encoding/json" "github.com/nbd-wtf/go-nostr" - "github.com/urfave/cli/v3" + "github.com/fiatjaf/cli/v3" ) var verify = &cli.Command{ From a36142604d0687c80eb56904080d4cf65df7fc0b Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sat, 27 Jul 2024 10:32:32 -0300 Subject: [PATCH 129/401] compile to riscv64. --- .github/workflows/release-cli.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml index 32dd6b7..f5c90a8 100644 --- a/.github/workflows/release-cli.yml +++ b/.github/workflows/release-cli.yml @@ -25,10 +25,14 @@ jobs: strategy: matrix: goos: [linux, freebsd, darwin, windows] - goarch: [amd64, arm64] + goarch: [amd64, arm64, riscv64] exclude: - goarch: arm64 goos: windows + - goarch: riscv64 + goos: windows + - goarch: riscv64 + goos: darwin steps: - uses: actions/checkout@v3 - uses: wangyoucao577/go-release-action@v1.40 From 928c73513ca70c4ef168e6cfd64a9ac7fca58cd0 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 30 Jul 2024 11:43:14 -0300 Subject: [PATCH 130/401] just move imports around. --- bunker.go | 2 +- count.go | 2 +- decode.go | 2 +- encode.go | 2 +- event.go | 2 +- fetch.go | 2 +- flags.go | 2 +- go.mod | 2 +- go.sum | 2 -- helpers.go | 2 +- key.go | 2 +- relay.go | 2 +- req.go | 2 +- verify.go | 2 +- 14 files changed, 13 insertions(+), 15 deletions(-) diff --git a/bunker.go b/bunker.go index b5e38de..7d8a1c8 100644 --- a/bunker.go +++ b/bunker.go @@ -11,10 +11,10 @@ import ( "time" "github.com/fatih/color" + "github.com/fiatjaf/cli/v3" "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/nip19" "github.com/nbd-wtf/go-nostr/nip46" - "github.com/fiatjaf/cli/v3" "golang.org/x/exp/slices" ) diff --git a/count.go b/count.go index 5b5ebe3..d1d2286 100644 --- a/count.go +++ b/count.go @@ -7,8 +7,8 @@ import ( "fmt" "strings" - "github.com/nbd-wtf/go-nostr" "github.com/fiatjaf/cli/v3" + "github.com/nbd-wtf/go-nostr" ) var count = &cli.Command{ diff --git a/decode.go b/decode.go index d57c80e..d27fe68 100644 --- a/decode.go +++ b/decode.go @@ -6,10 +6,10 @@ import ( "encoding/json" "strings" + "github.com/fiatjaf/cli/v3" "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/nip19" sdk "github.com/nbd-wtf/nostr-sdk" - "github.com/fiatjaf/cli/v3" ) var decode = &cli.Command{ diff --git a/encode.go b/encode.go index 027193a..3506ad4 100644 --- a/encode.go +++ b/encode.go @@ -4,9 +4,9 @@ import ( "context" "fmt" + "github.com/fiatjaf/cli/v3" "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/nip19" - "github.com/fiatjaf/cli/v3" ) var encode = &cli.Command{ diff --git a/event.go b/event.go index 443e6ea..28db9bb 100644 --- a/event.go +++ b/event.go @@ -8,10 +8,10 @@ import ( "strings" "time" + "github.com/fiatjaf/cli/v3" "github.com/mailru/easyjson" "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/nip19" - "github.com/fiatjaf/cli/v3" "golang.org/x/exp/slices" ) diff --git a/fetch.go b/fetch.go index 2ddc4b9..88009d7 100644 --- a/fetch.go +++ b/fetch.go @@ -3,10 +3,10 @@ package main import ( "context" + "github.com/fiatjaf/cli/v3" "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/nip19" sdk "github.com/nbd-wtf/nostr-sdk" - "github.com/fiatjaf/cli/v3" ) var fetch = &cli.Command{ diff --git a/flags.go b/flags.go index 1f332af..3de3d8f 100644 --- a/flags.go +++ b/flags.go @@ -6,9 +6,9 @@ import ( "strconv" "time" + "github.com/fiatjaf/cli/v3" "github.com/markusmobius/go-dateparser" "github.com/nbd-wtf/go-nostr" - "github.com/fiatjaf/cli/v3" ) type NaturalTimeFlag = cli.FlagBase[nostr.Timestamp, struct{}, naturalTimeValue] diff --git a/go.mod b/go.mod index 636588e..a47bfa4 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 github.com/fatih/color v1.16.0 + github.com/fiatjaf/cli/v3 v3.0.0-20240723181502-e7dd498b16ae github.com/mailru/easyjson v0.7.7 github.com/markusmobius/go-dateparser v1.2.3 github.com/nbd-wtf/go-nostr v0.34.2 @@ -23,7 +24,6 @@ require ( github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect github.com/elliotchance/pie/v2 v2.7.0 // indirect - github.com/fiatjaf/cli/v3 v3.0.0-20240723181502-e7dd498b16ae // indirect github.com/fiatjaf/eventstore v0.2.16 // indirect github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/pool v0.2.1 // indirect diff --git a/go.sum b/go.sum index 2e3f780..ddef5db 100644 --- a/go.sum +++ b/go.sum @@ -44,8 +44,6 @@ github.com/elliotchance/pie/v2 v2.7.0 h1:FqoIKg4uj0G/CrLGuMS9ejnFKa92lxE1dEgBD3p github.com/elliotchance/pie/v2 v2.7.0/go.mod h1:18t0dgGFH006g4eVdDtWfgFZPQEgl10IoEO8YWEq3Og= 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/cli/v3 v3.0.0-20240714232133-bb036558919f h1:SQ5W4q4HfpAPA8e8uPADqs4dhI6VvVwUq00DtlpHIt0= -github.com/fiatjaf/cli/v3 v3.0.0-20240714232133-bb036558919f/go.mod h1:Z1ItyMma7t6I7zHG9OpbExhHQOSkFf/96n+mAZ9MtVI= github.com/fiatjaf/cli/v3 v3.0.0-20240723181502-e7dd498b16ae h1:0B/1dU3YECIbPoBIRTQ4c0scZCNz9TVHtQpiODGrTTo= github.com/fiatjaf/cli/v3 v3.0.0-20240723181502-e7dd498b16ae/go.mod h1:aAWPO4bixZZxPtOnH6K3q4GbQ0jftUNDW9Oa861IRew= github.com/fiatjaf/eventstore v0.2.16 h1:NR64mnyUT5nJR8Sj2AwJTd1Hqs5kKJcCFO21ggUkvWg= diff --git a/helpers.go b/helpers.go index b15f12a..bd2d113 100644 --- a/helpers.go +++ b/helpers.go @@ -13,11 +13,11 @@ import ( "github.com/chzyer/readline" "github.com/fatih/color" + "github.com/fiatjaf/cli/v3" "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/nip19" "github.com/nbd-wtf/go-nostr/nip46" "github.com/nbd-wtf/go-nostr/nip49" - "github.com/fiatjaf/cli/v3" ) const ( diff --git a/key.go b/key.go index 86fee3b..21f3aa6 100644 --- a/key.go +++ b/key.go @@ -11,10 +11,10 @@ import ( "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" "github.com/decred/dcrd/dcrec/secp256k1/v4" + "github.com/fiatjaf/cli/v3" "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/nip19" "github.com/nbd-wtf/go-nostr/nip49" - "github.com/fiatjaf/cli/v3" ) var key = &cli.Command{ diff --git a/relay.go b/relay.go index 9e674d8..07d6391 100644 --- a/relay.go +++ b/relay.go @@ -11,10 +11,10 @@ import ( "io" "net/http" + "github.com/fiatjaf/cli/v3" "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/nip11" "github.com/nbd-wtf/go-nostr/nip86" - "github.com/fiatjaf/cli/v3" ) var relay = &cli.Command{ diff --git a/req.go b/req.go index 61232a8..3f9f740 100644 --- a/req.go +++ b/req.go @@ -7,9 +7,9 @@ import ( "os" "strings" + "github.com/fiatjaf/cli/v3" "github.com/mailru/easyjson" "github.com/nbd-wtf/go-nostr" - "github.com/fiatjaf/cli/v3" ) const CATEGORY_FILTER_ATTRIBUTES = "FILTER ATTRIBUTES" diff --git a/verify.go b/verify.go index f019f8e..1bfc979 100644 --- a/verify.go +++ b/verify.go @@ -4,8 +4,8 @@ import ( "context" "encoding/json" - "github.com/nbd-wtf/go-nostr" "github.com/fiatjaf/cli/v3" + "github.com/nbd-wtf/go-nostr" ) var verify = &cli.Command{ From 84965f22533c2616839388a2e3b8ce7d8cb77181 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sat, 3 Aug 2024 10:52:46 -0300 Subject: [PATCH 131/401] don't set limit to zero on --stream --- req.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/req.go b/req.go index 3f9f740..7835d9f 100644 --- a/req.go +++ b/req.go @@ -240,7 +240,7 @@ example: if limit := c.Uint("limit"); limit != 0 { filter.Limit = int(limit) - } else if c.IsSet("limit") || c.Bool("stream") { + } else if c.IsSet("limit") { filter.LimitZero = true } From 3d78e91f62291950bdc08bcabf67eb2d49da3d2f Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 6 Aug 2024 10:56:06 -0300 Subject: [PATCH 132/401] bunker: deny getPublicKey() even though it's harmless. fixes https://github.com/fiatjaf/nak/issues/27 --- bunker.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bunker.go b/bunker.go index 7d8a1c8..5de6ec6 100644 --- a/bunker.go +++ b/bunker.go @@ -177,7 +177,7 @@ var bunker = &cli.Command{ }() } - return harmless || slices.Contains(authorizedKeys, from) || slices.Contains(authorizedSecrets, secret) + return slices.Contains(authorizedKeys, from) || slices.Contains(authorizedSecrets, secret) } for ie := range events { From d226cd6ce4f8c00042030e7fdc88eaa0933e1aac Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 6 Aug 2024 11:05:07 -0300 Subject: [PATCH 133/401] fix password input lowercasing characters. fixes https://github.com/fiatjaf/nak/issues/28 --- helpers.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/helpers.go b/helpers.go index bd2d113..1b4852f 100644 --- a/helpers.go +++ b/helpers.go @@ -261,22 +261,18 @@ func askPassword(msg string, shouldAskAgain func(answer string) bool) (string, e EnableMask: true, MaskRune: '*', } - return _ask(config, msg, "", shouldAskAgain) -} -func _ask(config *readline.Config, msg string, defaultValue string, shouldAskAgain func(answer string) bool) (string, error) { rl, err := readline.NewEx(config) if err != nil { return "", err } - rl.WriteStdin([]byte(defaultValue)) for { answer, err := rl.Readline() if err != nil { return "", err } - answer = strings.TrimSpace(strings.ToLower(answer)) + answer = strings.TrimSpace(answer) if shouldAskAgain != nil && shouldAskAgain(answer) { continue } From c90e61dbec24db08a6dadf288aed10c572efd923 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Wed, 7 Aug 2024 11:46:07 -0300 Subject: [PATCH 134/401] set .DisableSliceFlagSeparator to true. fixes nostr:nevent1qqs9qwgwnr2rzguzrgt99hhhyv8e84mcdr4mnk86uvm6ndjvzl4rjxqpzpmhxue69uhkztnwdaejumr0dshsz9mhwden5te0vf5hgcm0d9hx2u3wwdhkx6tpdshszxnhwden5te0vfhhxarj9ekx2cm5w4exjene9ehx2ap0j8u0fj --- bunker.go | 9 +++++---- count.go | 7 ++++--- decode.go | 1 + encode.go | 19 +++++++++++++------ event.go | 1 + fetch.go | 1 + key.go | 44 +++++++++++++++++++++++++------------------- main.go | 11 ++++++----- musig2.go | 2 +- relay.go | 3 ++- req.go | 1 + verify.go | 1 + 12 files changed, 61 insertions(+), 39 deletions(-) diff --git a/bunker.go b/bunker.go index 5de6ec6..47ff50c 100644 --- a/bunker.go +++ b/bunker.go @@ -19,10 +19,11 @@ import ( ) var bunker = &cli.Command{ - Name: "bunker", - Usage: "starts a NIP-46 signer daemon with the given --sec key", - ArgsUsage: "[relay...]", - Description: ``, + Name: "bunker", + Usage: "starts a NIP-46 signer daemon with the given --sec key", + ArgsUsage: "[relay...]", + Description: ``, + DisableSliceFlagSeparator: true, Flags: []cli.Flag{ &cli.StringFlag{ Name: "sec", diff --git a/count.go b/count.go index d1d2286..906b516 100644 --- a/count.go +++ b/count.go @@ -12,9 +12,10 @@ import ( ) var count = &cli.Command{ - Name: "count", - Usage: "generates encoded COUNT messages and optionally use them to talk to relays", - Description: `outputs a NIP-45 request (the flags are mostly the same as 'nak req').`, + Name: "count", + Usage: "generates encoded COUNT messages and optionally use them to talk to relays", + Description: `outputs a NIP-45 request (the flags are mostly the same as 'nak req').`, + DisableSliceFlagSeparator: true, Flags: []cli.Flag{ &cli.StringSliceFlag{ Name: "author", diff --git a/decode.go b/decode.go index d27fe68..95aa1b8 100644 --- a/decode.go +++ b/decode.go @@ -20,6 +20,7 @@ var decode = &cli.Command{ nak decode nevent1qqs29yet5tp0qq5xu5qgkeehkzqh5qu46739axzezcxpj4tjlkx9j7gpr4mhxue69uhkummnw3ez6ur4vgh8wetvd3hhyer9wghxuet5sh59ud nak decode nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpz4mhxue69uhk2er9dchxummnw3ezumrpdejqz8thwden5te0dehhxarj94c82c3wwajkcmr0wfjx2u3wdejhgqgcwaehxw309aex2mrp0yhxummnw3exzarf9e3k7mgnp0sh5 nak decode nsec1jrmyhtjhgd9yqalps8hf9mayvd58852gtz66m7tqpacjedkp6kxq4dyxsr`, + DisableSliceFlagSeparator: true, Flags: []cli.Flag{ &cli.BoolFlag{ Name: "id", diff --git a/encode.go b/encode.go index 3506ad4..17d097d 100644 --- a/encode.go +++ b/encode.go @@ -25,10 +25,12 @@ var encode = &cli.Command{ } return nil }, + DisableSliceFlagSeparator: true, Commands: []*cli.Command{ { - Name: "npub", - Usage: "encode a hex public key into bech32 'npub' format", + Name: "npub", + Usage: "encode a hex public key into bech32 'npub' format", + DisableSliceFlagSeparator: true, Action: func(ctx context.Context, c *cli.Command) error { for target := range getStdinLinesOrArguments(c.Args()) { if ok := nostr.IsValidPublicKey(target); !ok { @@ -48,8 +50,9 @@ var encode = &cli.Command{ }, }, { - Name: "nsec", - Usage: "encode a hex private key into bech32 'nsec' format", + Name: "nsec", + Usage: "encode a hex private key into bech32 'nsec' format", + DisableSliceFlagSeparator: true, Action: func(ctx context.Context, c *cli.Command) error { for target := range getStdinLinesOrArguments(c.Args()) { if ok := nostr.IsValid32ByteHex(target); !ok { @@ -78,6 +81,7 @@ var encode = &cli.Command{ Usage: "attach relay hints to nprofile code", }, }, + DisableSliceFlagSeparator: true, Action: func(ctx context.Context, c *cli.Command) error { for target := range getStdinLinesOrArguments(c.Args()) { if ok := nostr.IsValid32ByteHex(target); !ok { @@ -116,6 +120,7 @@ var encode = &cli.Command{ Usage: "attach an author pubkey as a hint to the nevent code", }, }, + DisableSliceFlagSeparator: true, Action: func(ctx context.Context, c *cli.Command) error { for target := range getStdinLinesOrArguments(c.Args()) { if ok := nostr.IsValid32ByteHex(target); !ok { @@ -174,6 +179,7 @@ var encode = &cli.Command{ Usage: "attach relay hints to naddr code", }, }, + DisableSliceFlagSeparator: true, Action: func(ctx context.Context, c *cli.Command) error { for d := range getStdinLinesOrBlank() { pubkey := c.String("pubkey") @@ -211,8 +217,9 @@ var encode = &cli.Command{ }, }, { - Name: "note", - Usage: "generate note1 event codes (not recommended)", + Name: "note", + Usage: "generate note1 event codes (not recommended)", + DisableSliceFlagSeparator: true, Action: func(ctx context.Context, c *cli.Command) error { for target := range getStdinLinesOrArguments(c.Args()) { if ok := nostr.IsValid32ByteHex(target); !ok { diff --git a/event.go b/event.go index 28db9bb..df1dbb0 100644 --- a/event.go +++ b/event.go @@ -31,6 +31,7 @@ if an event -- or a partial event -- is given on stdin, the flags can be used to example: echo '{"id":"a889df6a387419ff204305f4c2d296ee328c3cd4f8b62f205648a541b4554dfb","pubkey":"c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5","created_at":1698623783,"kind":1,"tags":[],"content":"hello from the nostr army knife","sig":"84876e1ee3e726da84e5d195eb79358b2b3eaa4d9bd38456fde3e8a2af3f1cd4cda23f23fda454869975b3688797d4c66e12f4c51c1b43c6d2997c5e61865661"}' | nak event wss://offchain.pub echo '{"tags": [["t", "spam"]]}' | nak event -c 'this is spam'`, + DisableSliceFlagSeparator: true, Flags: []cli.Flag{ &cli.StringFlag{ Name: "sec", diff --git a/fetch.go b/fetch.go index 88009d7..014f8a9 100644 --- a/fetch.go +++ b/fetch.go @@ -15,6 +15,7 @@ var fetch = &cli.Command{ Description: `example usage: nak fetch nevent1qqsxrwm0hd3s3fddh4jc2574z3xzufq6qwuyz2rvv3n087zvym3dpaqprpmhxue69uhhqatzd35kxtnjv4kxz7tfdenju6t0xpnej4 echo npub1h8spmtw9m2huyv6v2j2qd5zv956z2zdugl6mgx02f2upffwpm3nqv0j4ps | nak fetch --relay wss://relay.nostr.band`, + DisableSliceFlagSeparator: true, Flags: []cli.Flag{ &cli.StringSliceFlag{ Name: "relay", diff --git a/key.go b/key.go index 21f3aa6..d925ba6 100644 --- a/key.go +++ b/key.go @@ -18,9 +18,10 @@ import ( ) var key = &cli.Command{ - Name: "key", - Usage: "operations on secret keys: generate, derive, encrypt, decrypt.", - Description: ``, + Name: "key", + Usage: "operations on secret keys: generate, derive, encrypt, decrypt.", + Description: ``, + DisableSliceFlagSeparator: true, Commands: []*cli.Command{ generate, public, @@ -31,9 +32,10 @@ var key = &cli.Command{ } var generate = &cli.Command{ - Name: "generate", - Usage: "generates a secret key", - Description: ``, + Name: "generate", + Usage: "generates a secret key", + Description: ``, + DisableSliceFlagSeparator: true, Action: func(ctx context.Context, c *cli.Command) error { sec := nostr.GeneratePrivateKey() stdout(sec) @@ -42,10 +44,11 @@ var generate = &cli.Command{ } var public = &cli.Command{ - Name: "public", - Usage: "computes a public key from a secret key", - Description: ``, - ArgsUsage: "[secret]", + Name: "public", + Usage: "computes a public key from a secret key", + Description: ``, + ArgsUsage: "[secret]", + DisableSliceFlagSeparator: true, Action: func(ctx context.Context, c *cli.Command) error { for sec := range getSecretKeysFromStdinLinesOrSlice(ctx, c, c.Args().Slice()) { pubkey, err := nostr.GetPublicKey(sec) @@ -60,10 +63,11 @@ var public = &cli.Command{ } var encrypt = &cli.Command{ - Name: "encrypt", - Usage: "encrypts a secret key and prints an ncryptsec code", - Description: `uses the NIP-49 standard.`, - ArgsUsage: " ", + Name: "encrypt", + Usage: "encrypts a secret key and prints an ncryptsec code", + Description: `uses the NIP-49 standard.`, + ArgsUsage: " ", + DisableSliceFlagSeparator: true, Flags: []cli.Flag{ &cli.IntFlag{ Name: "logn", @@ -98,10 +102,11 @@ var encrypt = &cli.Command{ } var decrypt = &cli.Command{ - Name: "decrypt", - Usage: "takes an ncrypsec and a password and decrypts it into an nsec", - Description: `uses the NIP-49 standard.`, - ArgsUsage: " ", + Name: "decrypt", + Usage: "takes an ncrypsec and a password and decrypts it into an nsec", + Description: `uses the NIP-49 standard.`, + ArgsUsage: " ", + DisableSliceFlagSeparator: true, Action: func(ctx context.Context, c *cli.Command) error { var ncryptsec string var password string @@ -151,7 +156,8 @@ var combine = &cli.Command{ Description: `The public keys must have 33 bytes (66 characters hex), with the 02 or 03 prefix. It is common in Nostr to drop that first byte, so you'll have to derive the public keys again from the private keys in order to get it back. However, if the intent is to check if two existing Nostr pubkeys match a given combined pubkey, then it might be sufficient to calculate the combined key for all the possible combinations of pubkeys in the input.`, - ArgsUsage: "[pubkey...]", + ArgsUsage: "[pubkey...]", + DisableSliceFlagSeparator: true, Action: func(ctx context.Context, c *cli.Command) error { type Combination struct { Variants []string `json:"input_variants"` diff --git a/main.go b/main.go index 3861933..be8780a 100644 --- a/main.go +++ b/main.go @@ -8,11 +8,12 @@ import ( ) var app = &cli.Command{ - Name: "nak", - Suggest: true, - UseShortOptionHandling: true, - AllowFlagsAfterArguments: true, - Usage: "the nostr army knife command-line tool", + Name: "nak", + Suggest: true, + UseShortOptionHandling: true, + AllowFlagsAfterArguments: true, + Usage: "the nostr army knife command-line tool", + DisableSliceFlagSeparator: true, Commands: []*cli.Command{ req, count, diff --git a/musig2.go b/musig2.go index 201c057..1775125 100644 --- a/musig2.go +++ b/musig2.go @@ -16,7 +16,7 @@ import ( ) func performMusig( - ctx context.Context, + _ context.Context, sec string, evt *nostr.Event, numSigners int, diff --git a/relay.go b/relay.go index 07d6391..f663bc1 100644 --- a/relay.go +++ b/relay.go @@ -22,7 +22,8 @@ var relay = &cli.Command{ Usage: "gets the relay information document for the given relay, as JSON", Description: `example: nak relay nostr.wine`, - ArgsUsage: "", + ArgsUsage: "", + DisableSliceFlagSeparator: true, Action: func(ctx context.Context, c *cli.Command) error { for url := range getStdinLinesOrArguments(c.Args()) { if url == "" { diff --git a/req.go b/req.go index 7835d9f..10713a8 100644 --- a/req.go +++ b/req.go @@ -27,6 +27,7 @@ it can also take a filter from stdin, optionally modify it with flags and send i example: echo '{"kinds": [1], "#t": ["test"]}' | nak req -l 5 -k 4549 --tag t=spam wss://nostr-pub.wellorder.net`, + DisableSliceFlagSeparator: true, Flags: []cli.Flag{ &cli.StringSliceFlag{ Name: "author", diff --git a/verify.go b/verify.go index 1bfc979..e71fc47 100644 --- a/verify.go +++ b/verify.go @@ -15,6 +15,7 @@ var verify = &cli.Command{ echo '{"id":"a889df6a387419ff204305f4c2d296ee328c3cd4f8b62f205648a541b4554dfb","pubkey":"c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5","created_at":1698623783,"kind":1,"tags":[],"content":"hello from the nostr army knife","sig":"84876e1ee3e726da84e5d195eb79358b2b3eaa4d9bd38456fde3e8a2af3f1cd4cda23f23fda454869975b3688797d4c66e12f4c51c1b43c6d2997c5e61865661"}' | nak verify it outputs nothing if the verification is successful.`, + DisableSliceFlagSeparator: true, Action: func(ctx context.Context, c *cli.Command) error { for stdinEvent := range getStdinLinesOrArguments(c.Args()) { evt := nostr.Event{} From 9690dc70cb310a672593f3b157ae35b9b4e69d17 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sun, 18 Aug 2024 23:38:03 -0300 Subject: [PATCH 135/401] nak req --paginate --- README.md | 7 +++++ paginate.go | 73 +++++++++++++++++++++++++++++++++++++++++++++++++++++ req.go | 18 ++++++++++++- 3 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 paginate.go diff --git a/README.md b/README.md index 245531b..8884642 100644 --- a/README.md +++ b/README.md @@ -173,6 +173,13 @@ listening at [wss://relay.damus.io wss://nos.lol wss://relay.nsecbunker.com]: {"kind":1,"id":"f030fccd90c783858dfcee204af94826cf0f1c85d6fc85a0087e9e5172419393","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1719677535,"tags":[["-"],["e","f59911b561c37c90b01e9e5c2557307380835c83399756f4d62d8167227e420a","wss://relay.whatever.com","root","a9e0f110f636f3191644110c19a33448daf09d7cda9708a769e91b7e91340208"],["p","a9e0f110f636f3191644110c19a33448daf09d7cda9708a769e91b7e91340208","wss://p-relay.com"]],"content":"I know the future","sig":"8b36a74e29df8bc12bed66896820da6940d4d9409721b3ed2e910c838833a178cb45fd5bb1c6eb6adc66ab2808bfac9f6644a2c55a6570bb2ad90f221c9c7551"} ``` +### download the latest 50000 notes from a relay, regardless of their natural query limits, by paginating requests +```shell +~> nak req -k 1 --limit 50000 --paginate --paginate-interval 2s nos.lol > events.jsonl +~> wc -l events.jsonl +50000 events.jsonl +``` + ## contributing to this repository Use NIP-34 to send your patches to `naddr1qqpkucttqy28wumn8ghj7un9d3shjtnwdaehgu3wvfnsz9nhwden5te0wfjkccte9ehx7um5wghxyctwvsq3gamnwvaz7tmjv4kxz7fwv3sk6atn9e5k7q3q80cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsxpqqqpmej2wctpn`. diff --git a/paginate.go b/paginate.go new file mode 100644 index 0000000..f8babd8 --- /dev/null +++ b/paginate.go @@ -0,0 +1,73 @@ +package main + +import ( + "context" + "math" + "slices" + "time" + + "github.com/nbd-wtf/go-nostr" +) + +func paginateWithPoolAndParams(pool *nostr.SimplePool, interval time.Duration, globalLimit uint64) func(ctx context.Context, urls []string, filters nostr.Filters) chan nostr.IncomingEvent { + return func(ctx context.Context, urls []string, filters nostr.Filters) chan nostr.IncomingEvent { + // filters will always be just one + filter := filters[0] + + nextUntil := nostr.Now() + if filter.Until != nil { + nextUntil = *filter.Until + } + + if globalLimit == 0 { + globalLimit = uint64(filter.Limit) + if globalLimit == 0 && !filter.LimitZero { + globalLimit = math.MaxUint64 + } + } + var globalCount uint64 = 0 + globalCh := make(chan nostr.IncomingEvent) + + repeatedCache := make([]string, 0, 300) + nextRepeatedCache := make([]string, 0, 300) + + go func() { + defer close(globalCh) + + for { + filter.Until = &nextUntil + time.Sleep(interval) + + keepGoing := false + for evt := range pool.SubManyEose(ctx, urls, nostr.Filters{filter}) { + if slices.Contains(repeatedCache, evt.ID) { + continue + } + + keepGoing = true // if we get one that isn't repeated, then keep trying to get more + nextRepeatedCache = append(nextRepeatedCache, evt.ID) + + globalCh <- evt + + globalCount++ + if globalCount >= globalLimit { + return + } + + if evt.CreatedAt < *filter.Until { + nextUntil = evt.CreatedAt + } + } + + if !keepGoing { + return + } + + repeatedCache = nextRepeatedCache + nextRepeatedCache = nextRepeatedCache[:0] + } + }() + + return globalCh + } +} diff --git a/req.go b/req.go index 10713a8..627ec05 100644 --- a/req.go +++ b/req.go @@ -96,6 +96,20 @@ example: Usage: "keep the subscription open, printing all events as they are returned", DefaultText: "false, will close on EOSE", }, + &cli.BoolFlag{ + Name: "paginate", + Usage: "make multiple REQs to the relay decreasing the value of 'until' until 'limit' or 'since' conditions are met", + DefaultText: "false", + }, + &cli.DurationFlag{ + Name: "paginate-interval", + Usage: "time between queries when using --paginate", + }, + &cli.UintFlag{ + Name: "paginate-global-limit", + Usage: "global limit at which --paginate should stop", + DefaultText: "uses the value given by --limit/-l or infinite", + }, &cli.BoolFlag{ Name: "bare", Usage: "when printing the filter, print just the filter, not enveloped in a [\"REQ\", ...] array", @@ -247,7 +261,9 @@ example: if len(relayUrls) > 0 { fn := pool.SubManyEose - if c.Bool("stream") { + if c.Bool("paginate") { + fn = paginateWithPoolAndParams(pool, c.Duration("paginate-interval"), c.Uint("paginate-global-limit")) + } else if c.Bool("stream") { fn = pool.SubMany } From 2edfa5cbea2fe320931c0380fda71db0813664e6 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Mon, 19 Aug 2024 12:43:22 -0300 Subject: [PATCH 136/401] nak serve --- README.md | 14 ++++++ bunker.go | 6 +-- fetch.go | 16 +++---- go.mod | 22 ++++++++-- go.sum | 44 ++++++++++++++++--- main.go | 3 +- serve.go | 126 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 208 insertions(+), 23 deletions(-) create mode 100644 serve.go diff --git a/README.md b/README.md index 8884642..c1d86ae 100644 --- a/README.md +++ b/README.md @@ -180,6 +180,20 @@ listening at [wss://relay.damus.io wss://nos.lol wss://relay.nsecbunker.com]: 50000 events.jsonl ``` +### run a somewhat verbose local relay for test purposes +```shell +~> nak serve +> relay running at ws://localhost:10547 + got request {"kinds":[1],"authors":["79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"],"since":1724082362} + got event {"kind":1,"id":"e3c6bf630d6deea74c0ee2f7f7ba6da55a627498a32f1e72029229bb1810bce3","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1724082366,"tags":[],"content":"two","sig":"34261cf226c3fee2df24e55a89f43f5349c98a64bce46bdc46807b0329f334cea93e9e8bc285c1259a5684cf23f5e507c8e6dad47a31a6615d706b1130d09e69"} + got event {"kind":1,"id":"0bbb397c8f87ae557650b9d6ee847292df8e530c458ffea1b24bdcb7bed0ec5e","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1724082369,"tags":[],"content":"three","sig":"aa1cb7d5f0f03f358fc4c0a4351a4f1c66e3a7627021b618601c56ba598b825b6d95d9c8720a4c60666a7eb21e17018cf326222f9f574a9396f2f2da7f007546"} + • events stored: 2, subscriptions opened: 1 + got event {"kind":1,"id":"029ebff759dd54dbd01b929f879fea5802de297e1c3768ca16d9b97cc8bca38f","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1724082371,"tags":[],"content":"four","sig":"9816de517d87d4c3ede57c1c50e3c237486794241afadcd891e1acbba2c5e672286090e6ad3402b047d69bae8095bc4e20e57ac70d92386dfa26db216379330f"} + got event {"kind":1,"id":"fe6489fa6fbb925be839377b9b7049d73be755dc2bdad97ff6dd9eecbf8b3a32","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1724082383,"tags":[],"content":"five","sig":"865ce5e32eead5bdb950ac1fbc55bc92dde26818ee3136634538ec42914de179a51e672c2d4269d4362176e5e8cd5e08e69b35b91c6c2af867e129b93d607635"} + got request {"kinds":[30818]} + • events stored: 4, subscriptions opened: 1 +``` + ## contributing to this repository Use NIP-34 to send your patches to `naddr1qqpkucttqy28wumn8ghj7un9d3shjtnwdaehgu3wvfnsz9nhwden5te0wfjkccte9ehx7um5wghxyctwvsq3gamnwvaz7tmjv4kxz7fwv3sk6atn9e5k7q3q80cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsxpqqqpmej2wctpn`. diff --git a/bunker.go b/bunker.go index 47ff50c..d2ea672 100644 --- a/bunker.go +++ b/bunker.go @@ -85,8 +85,8 @@ var bunker = &cli.Command{ return err } npub, _ := nip19.EncodePublicKey(pubkey) - bold := color.New(color.Bold).Sprint - italic := color.New(color.Italic).Sprint + bold := color.New(color.Bold).Sprintf + italic := color.New(color.Italic).Sprintf // this function will be called every now and then printBunkerInfo := func() { @@ -132,7 +132,7 @@ var bunker = &cli.Command{ ) log("listening at %v:\n pubkey: %s \n npub: %s%s%s\n to restart: %s\n bunker: %s\n\n", - bold(relayURLs), + bold("%v", relayURLs), bold(pubkey), bold(npub), authorizedKeysStr, diff --git a/fetch.go b/fetch.go index 014f8a9..9952b81 100644 --- a/fetch.go +++ b/fetch.go @@ -25,10 +25,10 @@ var fetch = &cli.Command{ }, ArgsUsage: "[nip19code]", Action: func(ctx context.Context, c *cli.Command) error { - pool := nostr.NewSimplePool(ctx) + sys := sdk.NewSystem() defer func() { - pool.Relays.Range(func(_ string, relay *nostr.Relay) bool { + sys.Pool.Relays.Range(func(_ string, relay *nostr.Relay) bool { relay.Close() return true }) @@ -78,13 +78,9 @@ var fetch = &cli.Command{ } if authorHint != "" { - relayList := sdk.FetchRelaysForPubkey(ctx, pool, authorHint, - "wss://purplepag.es", "wss://relay.damus.io", "wss://relay.noswhere.com", - "wss://nos.lol", "wss://public.relaying.io", "wss://relay.nostr.band") - for _, relayListItem := range relayList { - if relayListItem.Outbox { - relays = append(relays, relayListItem.URL) - } + relays := sys.FetchOutboxRelays(ctx, authorHint, 3) + for _, url := range relays { + relays = append(relays, url) } } @@ -93,7 +89,7 @@ var fetch = &cli.Command{ continue } - for ie := range pool.SubManyEose(ctx, relays, nostr.Filters{filter}) { + for ie := range sys.Pool.SubManyEose(ctx, relays, nostr.Filters{filter}) { stdout(ie.Event) } } diff --git a/go.mod b/go.mod index a47bfa4..cff7aec 100644 --- a/go.mod +++ b/go.mod @@ -5,43 +5,59 @@ go 1.22 toolchain go1.22.4 require ( + github.com/bep/debounce v1.2.1 github.com/btcsuite/btcd/btcec/v2 v2.3.3 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 github.com/fatih/color v1.16.0 github.com/fiatjaf/cli/v3 v3.0.0-20240723181502-e7dd498b16ae + github.com/fiatjaf/eventstore v0.7.1 + github.com/fiatjaf/khatru v0.7.5 github.com/mailru/easyjson v0.7.7 github.com/markusmobius/go-dateparser v1.2.3 - github.com/nbd-wtf/go-nostr v0.34.2 - github.com/nbd-wtf/nostr-sdk v0.0.5 + github.com/nbd-wtf/go-nostr v0.34.5 + github.com/nbd-wtf/nostr-sdk v0.5.0 golang.org/x/exp v0.0.0-20240707233637-46b078467d37 ) require ( + github.com/andybalholm/brotli v1.0.5 // indirect github.com/btcsuite/btcd/btcutil v1.1.3 // indirect github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/chzyer/logex v1.1.10 // indirect github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect github.com/elliotchance/pie/v2 v2.7.0 // indirect - github.com/fiatjaf/eventstore v0.2.16 // indirect + github.com/fasthttp/websocket v1.5.7 // indirect + github.com/fiatjaf/generic-ristretto v0.0.1 // indirect github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/pool v0.2.1 // indirect github.com/gobwas/ws v1.4.0 // indirect + github.com/golang/glog v1.1.2 // indirect + github.com/graph-gophers/dataloader/v7 v7.1.0 // indirect github.com/hablullah/go-hijri v1.0.2 // indirect github.com/hablullah/go-juliandays v1.0.0 // indirect github.com/jalaali/go-jalaali v0.0.0-20210801064154-80525e88d958 // indirect github.com/josharian/intern v1.0.0 // indirect + github.com/klauspost/compress v1.17.8 // indirect github.com/magefile/mage v1.14.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/puzpuzpuz/xsync/v3 v3.1.0 // indirect + github.com/rs/cors v1.7.0 // indirect + github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect github.com/tetratelabs/wazero v1.2.1 // indirect github.com/tidwall/gjson v1.17.1 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasthttp v1.51.0 // indirect github.com/wasilibs/go-re2 v1.3.0 // indirect golang.org/x/crypto v0.21.0 // indirect + golang.org/x/net v0.22.0 // indirect golang.org/x/sys v0.22.0 // indirect golang.org/x/text v0.16.0 // indirect ) diff --git a/go.sum b/go.sum index ddef5db..3223daf 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,8 @@ github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= +github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= +github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= +github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M= github.com/btcsuite/btcd v0.23.0/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY= @@ -23,6 +27,8 @@ github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= @@ -40,14 +46,24 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeC github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/elliotchance/pie/v2 v2.7.0 h1:FqoIKg4uj0G/CrLGuMS9ejnFKa92lxE1dEgBD3pShXg= github.com/elliotchance/pie/v2 v2.7.0/go.mod h1:18t0dgGFH006g4eVdDtWfgFZPQEgl10IoEO8YWEq3Og= +github.com/fasthttp/websocket v1.5.7 h1:0a6o2OfeATvtGgoMKleURhLT6JqWPg7fYfWnH4KHau4= +github.com/fasthttp/websocket v1.5.7/go.mod h1:bC4fxSono9czeXHQUVKxsC0sNjbm7lPJR04GDFqClfU= 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/cli/v3 v3.0.0-20240723181502-e7dd498b16ae h1:0B/1dU3YECIbPoBIRTQ4c0scZCNz9TVHtQpiODGrTTo= github.com/fiatjaf/cli/v3 v3.0.0-20240723181502-e7dd498b16ae/go.mod h1:aAWPO4bixZZxPtOnH6K3q4GbQ0jftUNDW9Oa861IRew= -github.com/fiatjaf/eventstore v0.2.16 h1:NR64mnyUT5nJR8Sj2AwJTd1Hqs5kKJcCFO21ggUkvWg= -github.com/fiatjaf/eventstore v0.2.16/go.mod h1:rUc1KhVufVmC+HUOiuPweGAcvG6lEOQCkRCn2Xn5VRA= +github.com/fiatjaf/eventstore v0.7.1 h1:5f2yvEtYvsvMBNttysmXhSSum5M1qwvPzjEQ/BFue7Q= +github.com/fiatjaf/eventstore v0.7.1/go.mod h1:ek/yWbanKVG767fK51Q3+6Mvi5oEHYSsdPym40nZexw= +github.com/fiatjaf/generic-ristretto v0.0.1 h1:LUJSU87X/QWFsBXTwnH3moFe4N8AjUxT+Rfa0+bo6YM= +github.com/fiatjaf/generic-ristretto v0.0.1/go.mod h1:cvV6ANHDA/GrfzVrig7N7i6l8CWnkVZvtQ2/wk9DPVE= +github.com/fiatjaf/khatru v0.7.5 h1:UFo+cdbqHDn1W4Q4h03y3vzh1BiU+6fLYK48oWU2K34= +github.com/fiatjaf/khatru v0.7.5/go.mod h1:WVqij7X9Vr9UAMIwafQbKVFKxc42Np37vyficwUr/nQ= 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/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= @@ -56,6 +72,8 @@ github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs= github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc= +github.com/golang/glog v1.1.2 h1:DVjP2PbBOzHyzA+dn3WhHIq4NdVu3Q+pvivFICf/7fo= +github.com/golang/glog v1.1.2/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= @@ -67,6 +85,8 @@ github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/graph-gophers/dataloader/v7 v7.1.0 h1:Wn8HGF/q7MNXcvfaBnLEPEFJttVHR8zuEqP1obys/oc= +github.com/graph-gophers/dataloader/v7 v7.1.0/go.mod h1:1bKE0Dm6OUcTB/OAuYVOZctgIz7Q3d0XrYtlIzTgg6Q= github.com/hablullah/go-hijri v1.0.2 h1:drT/MZpSZJQXo7jftf5fthArShcaMtsal0Zf/dnmp6k= github.com/hablullah/go-hijri v1.0.2/go.mod h1:OS5qyYLDjORXzK4O1adFw9Q5WfhOcMdAKglDkcTxgWQ= github.com/hablullah/go-juliandays v1.0.0 h1:A8YM7wIj16SzlKT0SRJc9CD29iiaUzpBLzh5hr0/5p0= @@ -80,6 +100,8 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= +github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU= +github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo= github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= @@ -91,10 +113,10 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/nbd-wtf/go-nostr v0.34.2 h1:9b4qZ29DhQf9xEWN8/7zfDD868r1jFbpjrR3c+BHc+E= -github.com/nbd-wtf/go-nostr v0.34.2/go.mod h1:NZQkxl96ggbO8rvDpVjcsojJqKTPwqhP4i82O7K5DJs= -github.com/nbd-wtf/nostr-sdk v0.0.5 h1:rec+FcDizDVO0W25PX0lgYMXvP7zNNOgI3Fu9UCm4BY= -github.com/nbd-wtf/nostr-sdk v0.0.5/go.mod h1:iJJsikesCGLNFZ9dLqhLPDzdt924EagUmdQxT3w2Lmk= +github.com/nbd-wtf/go-nostr v0.34.5 h1:vti8WqvGWbVoWAPniaz7li2TpCyC+7ZS62Gmy7ib/z0= +github.com/nbd-wtf/go-nostr v0.34.5/go.mod h1:NZQkxl96ggbO8rvDpVjcsojJqKTPwqhP4i82O7K5DJs= +github.com/nbd-wtf/nostr-sdk v0.5.0 h1:zrMxcvMSxkw29RyfXEdF3XW5rUWLuT5Q9oBAhd5dyew= +github.com/nbd-wtf/nostr-sdk v0.5.0/go.mod h1:MJ7gYv3XiZKU6MHSM0N7oHqQAQhbvpgGQk4Q+XUdIUs= 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= @@ -104,10 +126,16 @@ github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5 github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/puzpuzpuz/xsync/v3 v3.1.0 h1:EewKT7/LNac5SLiEblJeUu8z5eERHrmRLnMQL2d7qX4= github.com/puzpuzpuz/xsync/v3 v3.1.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= +github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= +github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk= +github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= @@ -122,6 +150,10 @@ github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JT github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA= +github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g= github.com/wasilibs/go-re2 v1.3.0 h1:LFhBNzoStM3wMie6rN2slD1cuYH2CGiHpvNL3UtcsMw= github.com/wasilibs/go-re2 v1.3.0/go.mod h1:AafrCXVvGRJJOImMajgJ2M7rVmWyisVK7sFshbxnVrg= github.com/wasilibs/nottinygc v0.4.0 h1:h1TJMihMC4neN6Zq+WKpLxgd9xCFMw7O9ETLwY2exJQ= diff --git a/main.go b/main.go index be8780a..9e236da 100644 --- a/main.go +++ b/main.go @@ -25,6 +25,7 @@ var app = &cli.Command{ verify, relay, bunker, + serve, }, Flags: []cli.Flag{ &cli.BoolFlag{ @@ -37,7 +38,7 @@ var app = &cli.Command{ if q >= 1 { log = func(msg string, args ...any) {} if q >= 2 { - stdout = func(a ...any) (int, error) { return 0, nil } + stdout = func(_ ...any) (int, error) { return 0, nil } } } return nil diff --git a/serve.go b/serve.go new file mode 100644 index 0000000..07e7722 --- /dev/null +++ b/serve.go @@ -0,0 +1,126 @@ +package main + +import ( + "bufio" + "context" + "encoding/json" + "fmt" + "math" + "os" + "time" + + "github.com/bep/debounce" + "github.com/fatih/color" + "github.com/fiatjaf/cli/v3" + "github.com/fiatjaf/eventstore/slicestore" + "github.com/fiatjaf/khatru" + "github.com/nbd-wtf/go-nostr" +) + +var serve = &cli.Command{ + Name: "serve", + Usage: "starts an in-memory relay for testing purposes", + DisableSliceFlagSeparator: true, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "hostname", + Usage: "hostname where to listen for connections", + Value: "localhost", + }, + &cli.UintFlag{ + Name: "port", + Usage: "port where to listen for connections", + Value: 10547, + }, + &cli.StringFlag{ + Name: "events", + Usage: "file containing the initial batch of events that will be served by the relay as newline-separated JSON (jsonl)", + DefaultText: "the relay will start empty", + }, + }, + Action: func(ctx context.Context, c *cli.Command) error { + db := slicestore.SliceStore{MaxLimit: math.MaxInt} + + var scanner *bufio.Scanner + if path := c.String("events"); path != "" { + f, err := os.Open(path) + if err != nil { + return fmt.Errorf("failed to file at '%s': %w", path, err) + } + scanner = bufio.NewScanner(f) + } else if isPiped() { + scanner = bufio.NewScanner(os.Stdin) + } + + if scanner != nil { + scanner.Buffer(make([]byte, 16*1024*1024), 256*1024*1024) + i := 0 + for scanner.Scan() { + var evt nostr.Event + 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) + i++ + } + } + + rl := khatru.NewRelay() + 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) + + started := make(chan bool) + exited := make(chan error) + + hostname := c.String("hostname") + port := int(c.Uint("port")) + + go func() { + err := rl.Start(hostname, port, started) + exited <- err + }() + + bold := color.New(color.Bold).Sprintf + italic := color.New(color.Italic).Sprint + + var printStatus func() + + // relay logging + rl.RejectFilter = append(rl.RejectFilter, func(ctx context.Context, filter nostr.Filter) (reject bool, msg string) { + log(" got %s %v\n", color.HiYellowString("request"), italic(filter)) + printStatus() + return false, "" + }) + rl.RejectCountFilter = append(rl.RejectCountFilter, func(ctx context.Context, filter nostr.Filter) (reject bool, msg string) { + log(" got %s %v\n", color.HiCyanString("count request"), italic(filter)) + printStatus() + return false, "" + }) + rl.RejectEvent = append(rl.RejectEvent, func(ctx context.Context, event *nostr.Event) (reject bool, msg string) { + log(" got %s %v\n", color.BlueString("event"), 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 { + totalEvents++ + } + subs := rl.GetListeningFilters() + + log(" %s events stored: %s, subscriptions opened: %s\n", color.HiMagentaString("•"), color.HiMagentaString("%d", totalEvents), color.HiMagentaString("%d", len(subs))) + }) + } + + <-started + log("%s relay running at %s\n", color.HiRedString(">"), bold("ws://%s:%d", hostname, port)) + + return <-exited + }, +} From 85e96102655ae2ef19cae1ec5fd03ccbe7e5f4bf Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 20 Aug 2024 10:29:00 -0300 Subject: [PATCH 137/401] test natural timestamps. --- example_test.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/example_test.go b/example_test.go index f7b2b34..b94a321 100644 --- a/example_test.go +++ b/example_test.go @@ -116,3 +116,9 @@ func ExampleReqWithFlagsAfter3() { // Output: // {"kind":1,"id":"101572c80ebdc963dab8440f6307387a3023b6d90f7e495d6c5ee1ef77045a67","pubkey":"3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24","created_at":1720987305,"tags":[["e","ceacdc29fa7a0b51640b30d2424e188215460617db5ba5bb52d3fbf0094eebb3","","root"],["e","9f3c1121c96edf17d84b9194f74d66d012b28c4e25b3ef190582c76b8546a188","","reply"],["p","3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24"],["p","6b96c3eb36c6cd457d906bbaafe7b36cacfb8bcc4ab235be6eab3b71c6669251"]],"content":"Nope. I grew up playing in the woods. Never once saw a bear in the woods. If I did, I'd probably shiy my pants, then scream at it like I was a crazy person with my arms above my head to make me seem huge.","sig":"b098820b4a5635865cada9f9a5813be2bc6dd7180e16e590cf30e07916d8ed6ed98ab38b64f3bfba12d88d37335f229f7ef8c084bc48132e936c664a54d3e650"} } + +func ExampleNaturalTimestamps() { + app.Run(ctx, []string{"nak", "event", "-t", "plu=pla", "-e", "3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24", "--ts", "2018-05-19 03:37:19", "-c", "nn"}) + // Output: + // {"kind":1,"id":"0000d199127d5e15046b0a3f2885d464ee18f70968303665ef76326a7d828312","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1724160467,"tags":[["plu","pla"],["e","3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24"],["nonce","24783","16"]],"content":"nn","sig":"99471b43ce82ca01fb9b61f36b45ca542870854b2466a9d3884891598f7d7baef36d07f4b02bb194f2f6f781973f24c3d946f702c82321c6cb0c564e76cf43db"} +} From 9d43e66fac764c667257db785c3e1427fb642791 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 20 Aug 2024 10:29:18 -0300 Subject: [PATCH 138/401] nak event --pow closes https://github.com/fiatjaf/nak/issues/29 --- README.md | 6 +++++ event.go | 77 ++++++++++++++++++++++++++++++++++++++++++------------- musig2.go | 25 ++++++++++++++++++ 3 files changed, 90 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index c1d86ae..fc7c512 100644 --- a/README.md +++ b/README.md @@ -194,6 +194,12 @@ listening at [wss://relay.damus.io wss://nos.lol wss://relay.nsecbunker.com]: • events stored: 4, subscriptions opened: 1 ``` +### make an event with a PoW target +```shell +~> nak event -c 'hello getwired.app and labour.fiatjaf.com' --pow 24 +{"kind":1,"id":"0000009dcc7c62056eafdb41fac817379ec2becf0ce27c5fbe98d0735d968147","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1724160828,"tags":[["nonce","515504","24"]],"content":"hello getwired.app and labour.fiatjaf.com","sig":"7edb988065ccc12779fe99270945b212f3723838f315d76d5e90e9ffa27198f13fa556614295f518d968d55bab81878167d4162b3a7cf81a6b423c6761bd504c"} +``` + ## contributing to this repository Use NIP-34 to send your patches to `naddr1qqpkucttqy28wumn8ghj7un9d3shjtnwdaehgu3wvfnsz9nhwden5te0wfjkccte9ehx7um5wghxyctwvsq3gamnwvaz7tmjv4kxz7fwv3sk6atn9e5k7q3q80cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsxpqqqpmej2wctpn`. diff --git a/event.go b/event.go index df1dbb0..cedb0b8 100644 --- a/event.go +++ b/event.go @@ -11,11 +11,16 @@ import ( "github.com/fiatjaf/cli/v3" "github.com/mailru/easyjson" "github.com/nbd-wtf/go-nostr" + "github.com/nbd-wtf/go-nostr/nip13" "github.com/nbd-wtf/go-nostr/nip19" "golang.org/x/exp/slices" ) -const CATEGORY_EVENT_FIELDS = "EVENT FIELDS" +const ( + CATEGORY_EVENT_FIELDS = "EVENT FIELDS" + CATEGORY_SIGNER = "SIGNER OPTIONS" + CATEGORY_EXTRAS = "EXTRAS" +) var event = &cli.Command{ Name: "event", @@ -38,19 +43,23 @@ example: Usage: "secret key to sign the event, as nsec, ncryptsec or hex", DefaultText: "the key '1'", Value: "0000000000000000000000000000000000000000000000000000000000000001", + Category: CATEGORY_SIGNER, }, &cli.BoolFlag{ - Name: "prompt-sec", - Usage: "prompt the user to paste a hex or nsec with which to sign the event", + Name: "prompt-sec", + Usage: "prompt the user to paste a hex or nsec with which to sign the event", + Category: CATEGORY_SIGNER, }, &cli.StringFlag{ - Name: "connect", - Usage: "sign event using NIP-46, expects a bunker://... URL", + Name: "connect", + Usage: "sign event using NIP-46, expects a bunker://... URL", + Category: CATEGORY_SIGNER, }, &cli.StringFlag{ Name: "connect-as", Usage: "private key to when communicating with the bunker given on --connect", DefaultText: "a random key", + Category: CATEGORY_SIGNER, }, // ~ these args are only for the convoluted musig2 signing process // they will be generally copy-shared-pasted across some manual coordination method between participants @@ -59,6 +68,7 @@ example: Usage: "number of signers to use for musig2", Value: 1, DefaultText: "1 -- i.e. do not use musig2 at all", + Category: CATEGORY_SIGNER, }, &cli.StringSliceFlag{ Name: "musig-pubkey", @@ -77,17 +87,25 @@ example: Hidden: true, }, // ~~~ - &cli.BoolFlag{ - Name: "envelope", - Usage: "print the event enveloped in a [\"EVENT\", ...] message ready to be sent to a relay", + &cli.UintFlag{ + Name: "pow", + Usage: "NIP-13 difficulty to target when doing hash work on the event id", + Category: CATEGORY_EXTRAS, }, &cli.BoolFlag{ - Name: "auth", - Usage: "always perform NIP-42 \"AUTH\" when facing an \"auth-required: \" rejection and try again", + Name: "envelope", + Usage: "print the event enveloped in a [\"EVENT\", ...] message ready to be sent to a relay", + Category: CATEGORY_EXTRAS, }, &cli.BoolFlag{ - Name: "nevent", - Usage: "print the nevent code (to stderr) after the event is published", + Name: "auth", + Usage: "always perform NIP-42 \"AUTH\" when facing an \"auth-required: \" rejection and try again", + Category: CATEGORY_EXTRAS, + }, + &cli.BoolFlag{ + Name: "nevent", + Usage: "print the nevent code (to stderr) after the event is published", + Category: CATEGORY_EXTRAS, }, &cli.UintFlag{ Name: "kind", @@ -193,8 +211,9 @@ example: mustRehashAndResign = true } - tags := make(nostr.Tags, 0, 5) - for _, tagFlag := range c.StringSlice("tag") { + tagFlags := c.StringSlice("tag") + tags := make(nostr.Tags, 0, len(tagFlags)+2) + for _, tagFlag := range tagFlags { // tags are in the format key=value tagName, tagValue, found := strings.Cut(tagFlag, "=") tag := []string{tagName} @@ -203,20 +222,17 @@ example: tagValues := strings.Split(tagValue, ";") tag = append(tag, tagValues...) } - tags = tags.AppendUnique(tag) + tags = append(tags, tag) } for _, etag := range c.StringSlice("e") { tags = tags.AppendUnique([]string{"e", etag}) - mustRehashAndResign = true } for _, ptag := range c.StringSlice("p") { tags = tags.AppendUnique([]string{"p", ptag}) - mustRehashAndResign = true } for _, dtag := range c.StringSlice("d") { tags = tags.AppendUnique([]string{"d", dtag}) - mustRehashAndResign = true } if len(tags) > 0 { for _, tag := range tags { @@ -233,6 +249,31 @@ example: mustRehashAndResign = true } + if difficulty := c.Uint("pow"); difficulty > 0 { + // before doing pow we need the pubkey + if bunker != nil { + evt.PubKey, err = bunker.GetPublicKey(ctx) + if err != nil { + return fmt.Errorf("can't pow: failed to get public key from bunker: %w", err) + } + } else if numSigners := c.Uint("musig"); numSigners > 1 && sec != "" { + pubkeys := c.StringSlice("musig-pubkey") + if int(numSigners) != len(pubkeys) { + return fmt.Errorf("when doing a pow with musig we must know all signer pubkeys upfront") + } + evt.PubKey, err = getMusigAggregatedKey(ctx, pubkeys) + if err != nil { + return err + } + } else { + evt.PubKey, _ = nostr.GetPublicKey(sec) + } + + // try to generate work with this difficulty -- essentially forever + nip13.Generate(&evt, int(difficulty), time.Hour*24*365) + mustRehashAndResign = true + } + if evt.Sig == "" || mustRehashAndResign { if bunker != nil { if err := bunker.SignEvent(ctx, &evt); err != nil { diff --git a/musig2.go b/musig2.go index 1775125..7e0a42e 100644 --- a/musig2.go +++ b/musig2.go @@ -15,6 +15,31 @@ import ( "github.com/nbd-wtf/go-nostr" ) +func getMusigAggregatedKey(_ context.Context, keys []string) (string, 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) + } + if len(bpk) == 32 { + return "", 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) + } + knownSigners[i] = pk + } + + aggpk, _, _, err := musig2.AggregateKeys(knownSigners, true) + if err != nil { + return "", fmt.Errorf("aggregation failed: %w", err) + } + + return hex.EncodeToString(aggpk.FinalKey.SerializeCompressed()[1:]), nil +} + func performMusig( _ context.Context, sec string, From 2042b14578f60c352eaafb595d9ac3cd7fde22aa Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 20 Aug 2024 10:48:08 -0300 Subject: [PATCH 139/401] nak fetch: support nip05 codes. addresses https://github.com/fiatjaf/nak/issues/19 --- fetch.go | 87 +++++++++++++++++++++++++++++++------------------------- 1 file changed, 49 insertions(+), 38 deletions(-) diff --git a/fetch.go b/fetch.go index 9952b81..49b27d1 100644 --- a/fetch.go +++ b/fetch.go @@ -5,13 +5,14 @@ import ( "github.com/fiatjaf/cli/v3" "github.com/nbd-wtf/go-nostr" + "github.com/nbd-wtf/go-nostr/nip05" "github.com/nbd-wtf/go-nostr/nip19" sdk "github.com/nbd-wtf/nostr-sdk" ) var fetch = &cli.Command{ Name: "fetch", - Usage: "fetches events related to the given nip19 code from the included relay hints or the author's NIP-65 relays.", + Usage: "fetches events related to the given nip19 or nip05 code from the included relay hints or the author's NIP-65 relays.", Description: `example usage: nak fetch nevent1qqsxrwm0hd3s3fddh4jc2574z3xzufq6qwuyz2rvv3n087zvym3dpaqprpmhxue69uhhqatzd35kxtnjv4kxz7tfdenju6t0xpnej4 echo npub1h8spmtw9m2huyv6v2j2qd5zv956z2zdugl6mgx02f2upffwpm3nqv0j4ps | nak fetch --relay wss://relay.nostr.band`, @@ -23,7 +24,7 @@ var fetch = &cli.Command{ Usage: "also use these relays to fetch from", }, }, - ArgsUsage: "[nip19code]", + ArgsUsage: "[nip05_or_nip19_code]", Action: func(ctx context.Context, c *cli.Command) error { sys := sdk.NewSystem() @@ -36,45 +37,55 @@ var fetch = &cli.Command{ for code := range getStdinLinesOrArguments(c.Args()) { filter := nostr.Filter{} - - prefix, value, err := nip19.Decode(code) - if err != nil { - ctx = lineProcessingError(ctx, "failed to decode: %s", err) - continue - } - - relays := c.StringSlice("relay") - if err := normalizeAndValidateRelayURLs(relays); err != nil { - return err - } var authorHint string + relays := c.StringSlice("relay") - switch prefix { - case "nevent": - v := value.(nostr.EventPointer) - filter.IDs = append(filter.IDs, v.ID) - if v.Author != "" { - authorHint = v.Author + if nip05.IsValidIdentifier(code) { + pp, err := nip05.QueryIdentifier(ctx, code) + if err != nil { + ctx = lineProcessingError(ctx, "failed to fetch nip05: %s", err) + continue + } + authorHint = pp.PublicKey + relays = append(relays, pp.Relays...) + } else { + prefix, value, err := nip19.Decode(code) + if err != nil { + ctx = lineProcessingError(ctx, "failed to decode: %s", err) + continue + } + + if err := normalizeAndValidateRelayURLs(relays); err != nil { + return err + } + + switch prefix { + case "nevent": + v := value.(nostr.EventPointer) + filter.IDs = append(filter.IDs, v.ID) + if v.Author != "" { + authorHint = v.Author + } + relays = append(relays, v.Relays...) + case "naddr": + v := value.(nostr.EntityPointer) + filter.Tags = nostr.TagMap{"d": []string{v.Identifier}} + filter.Kinds = append(filter.Kinds, v.Kind) + filter.Authors = append(filter.Authors, v.PublicKey) + authorHint = v.PublicKey + relays = append(relays, v.Relays...) + case "nprofile": + v := value.(nostr.ProfilePointer) + filter.Authors = append(filter.Authors, v.PublicKey) + filter.Kinds = append(filter.Kinds, 0) + authorHint = v.PublicKey + relays = append(relays, v.Relays...) + case "npub": + v := value.(string) + filter.Authors = append(filter.Authors, v) + filter.Kinds = append(filter.Kinds, 0) + authorHint = v } - relays = append(relays, v.Relays...) - case "naddr": - v := value.(nostr.EntityPointer) - filter.Tags = nostr.TagMap{"d": []string{v.Identifier}} - filter.Kinds = append(filter.Kinds, v.Kind) - filter.Authors = append(filter.Authors, v.PublicKey) - authorHint = v.PublicKey - relays = append(relays, v.Relays...) - case "nprofile": - v := value.(nostr.ProfilePointer) - filter.Authors = append(filter.Authors, v.PublicKey) - filter.Kinds = append(filter.Kinds, 0) - authorHint = v.PublicKey - relays = append(relays, v.Relays...) - case "npub": - v := value.(string) - filter.Authors = append(filter.Authors, v) - filter.Kinds = append(filter.Kinds, 0) - authorHint = v } if authorHint != "" { From ea7b88cfd70d735263c956e81a04f7e23998ec33 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 20 Aug 2024 10:59:28 -0300 Subject: [PATCH 140/401] fix `fetch` with nip05 filter and make `req` filter options generalize to `fetch`. related: https://github.com/fiatjaf/nak/issues/19 --- fetch.go | 16 +++- req.go | 275 +++++++++++++++++++++++++++++-------------------------- 2 files changed, 158 insertions(+), 133 deletions(-) diff --git a/fetch.go b/fetch.go index 49b27d1..d06723d 100644 --- a/fetch.go +++ b/fetch.go @@ -17,13 +17,13 @@ var fetch = &cli.Command{ nak fetch nevent1qqsxrwm0hd3s3fddh4jc2574z3xzufq6qwuyz2rvv3n087zvym3dpaqprpmhxue69uhhqatzd35kxtnjv4kxz7tfdenju6t0xpnej4 echo npub1h8spmtw9m2huyv6v2j2qd5zv956z2zdugl6mgx02f2upffwpm3nqv0j4ps | nak fetch --relay wss://relay.nostr.band`, DisableSliceFlagSeparator: true, - Flags: []cli.Flag{ + Flags: append(reqFilterFlags, &cli.StringSliceFlag{ Name: "relay", Aliases: []string{"r"}, Usage: "also use these relays to fetch from", }, - }, + ), ArgsUsage: "[nip05_or_nip19_code]", Action: func(ctx context.Context, c *cli.Command) error { sys := sdk.NewSystem() @@ -48,6 +48,7 @@ var fetch = &cli.Command{ } authorHint = pp.PublicKey relays = append(relays, pp.Relays...) + filter.Authors = append(filter.Authors, pp.PublicKey) } else { prefix, value, err := nip19.Decode(code) if err != nil { @@ -70,20 +71,17 @@ var fetch = &cli.Command{ case "naddr": v := value.(nostr.EntityPointer) filter.Tags = nostr.TagMap{"d": []string{v.Identifier}} - filter.Kinds = append(filter.Kinds, v.Kind) filter.Authors = append(filter.Authors, v.PublicKey) authorHint = v.PublicKey relays = append(relays, v.Relays...) case "nprofile": v := value.(nostr.ProfilePointer) filter.Authors = append(filter.Authors, v.PublicKey) - filter.Kinds = append(filter.Kinds, 0) authorHint = v.PublicKey relays = append(relays, v.Relays...) case "npub": v := value.(string) filter.Authors = append(filter.Authors, v) - filter.Kinds = append(filter.Kinds, 0) authorHint = v } } @@ -93,6 +91,14 @@ var fetch = &cli.Command{ for _, url := range relays { relays = append(relays, url) } + + if len(filter.Kinds) == 0 { + filter.Kinds = append(filter.Kinds, 0) + } + } + + if err := applyFlagsToFilter(c, &filter); err != nil { + return err } if len(relays) == 0 { diff --git a/req.go b/req.go index 627ec05..4d96c56 100644 --- a/req.go +++ b/req.go @@ -12,7 +12,10 @@ import ( "github.com/nbd-wtf/go-nostr" ) -const CATEGORY_FILTER_ATTRIBUTES = "FILTER ATTRIBUTES" +const ( + CATEGORY_FILTER_ATTRIBUTES = "FILTER ATTRIBUTES" + // CATEGORY_SIGNER = "SIGNER OPTIONS" -- defined at event.go as the same (yes, I know) +) var req = &cli.Command{ Name: "req", @@ -28,69 +31,7 @@ it can also take a filter from stdin, optionally modify it with flags and send i example: echo '{"kinds": [1], "#t": ["test"]}' | nak req -l 5 -k 4549 --tag t=spam wss://nostr-pub.wellorder.net`, DisableSliceFlagSeparator: true, - Flags: []cli.Flag{ - &cli.StringSliceFlag{ - Name: "author", - Aliases: []string{"a"}, - Usage: "only accept events from these authors (pubkey as hex)", - Category: CATEGORY_FILTER_ATTRIBUTES, - }, - &cli.StringSliceFlag{ - Name: "id", - Aliases: []string{"i"}, - Usage: "only accept events with these ids (hex)", - Category: CATEGORY_FILTER_ATTRIBUTES, - }, - &cli.IntSliceFlag{ - Name: "kind", - Aliases: []string{"k"}, - Usage: "only accept events with these kind numbers", - Category: CATEGORY_FILTER_ATTRIBUTES, - }, - &cli.StringSliceFlag{ - Name: "tag", - Aliases: []string{"t"}, - Usage: "takes a tag like -t e=, only accept events with these tags", - Category: CATEGORY_FILTER_ATTRIBUTES, - }, - &cli.StringSliceFlag{ - Name: "e", - Usage: "shortcut for --tag e=", - Category: CATEGORY_FILTER_ATTRIBUTES, - }, - &cli.StringSliceFlag{ - Name: "p", - Usage: "shortcut for --tag p=", - Category: CATEGORY_FILTER_ATTRIBUTES, - }, - &cli.StringSliceFlag{ - Name: "d", - Usage: "shortcut for --tag d=", - Category: CATEGORY_FILTER_ATTRIBUTES, - }, - &NaturalTimeFlag{ - Name: "since", - Aliases: []string{"s"}, - Usage: "only accept events newer than this (unix timestamp)", - Category: CATEGORY_FILTER_ATTRIBUTES, - }, - &NaturalTimeFlag{ - Name: "until", - Aliases: []string{"u"}, - Usage: "only accept events older than this (unix timestamp)", - Category: CATEGORY_FILTER_ATTRIBUTES, - }, - &cli.UintFlag{ - Name: "limit", - Aliases: []string{"l"}, - Usage: "only accept up to this number of events", - Category: CATEGORY_FILTER_ATTRIBUTES, - }, - &cli.StringFlag{ - Name: "search", - Usage: "a NIP-50 search query, use it only with relays that explicitly support it", - Category: CATEGORY_FILTER_ATTRIBUTES, - }, + Flags: append(reqFilterFlags, &cli.BoolFlag{ Name: "stream", Usage: "keep the subscription open, printing all events as they are returned", @@ -119,30 +60,35 @@ example: Usage: "always perform NIP-42 \"AUTH\" when facing an \"auth-required: \" rejection and try again", }, &cli.BoolFlag{ - Name: "force-pre-auth", - Aliases: []string{"fpa"}, - Usage: "after connecting, for a NIP-42 \"AUTH\" message to be received, act on it and only then send the \"REQ\"", + Name: "force-pre-auth", + Aliases: []string{"fpa"}, + Usage: "after connecting, for a NIP-42 \"AUTH\" message to be received, act on it and only then send the \"REQ\"", + Category: CATEGORY_SIGNER, }, &cli.StringFlag{ Name: "sec", Usage: "secret key to sign the AUTH challenge, as hex or nsec", DefaultText: "the key '1'", Value: "0000000000000000000000000000000000000000000000000000000000000001", + Category: CATEGORY_SIGNER, }, &cli.BoolFlag{ - Name: "prompt-sec", - Usage: "prompt the user to paste a hex or nsec with which to sign the AUTH challenge", + Name: "prompt-sec", + Usage: "prompt the user to paste a hex or nsec with which to sign the AUTH challenge", + Category: CATEGORY_SIGNER, }, &cli.StringFlag{ - Name: "connect", - Usage: "sign AUTH using NIP-46, expects a bunker://... URL", + Name: "connect", + Usage: "sign AUTH using NIP-46, expects a bunker://... URL", + Category: CATEGORY_SIGNER, }, &cli.StringFlag{ Name: "connect-as", Usage: "private key to when communicating with the bunker given on --connect", DefaultText: "a random key", + Category: CATEGORY_SIGNER, }, - }, + ), ArgsUsage: "[relay...]", Action: func(ctx context.Context, c *cli.Command) error { var pool *nostr.SimplePool @@ -201,62 +147,8 @@ example: } } - if authors := c.StringSlice("author"); len(authors) > 0 { - filter.Authors = append(filter.Authors, authors...) - } - if ids := c.StringSlice("id"); len(ids) > 0 { - filter.IDs = append(filter.IDs, ids...) - } - for _, kind64 := range c.IntSlice("kind") { - filter.Kinds = append(filter.Kinds, int(kind64)) - } - if search := c.String("search"); search != "" { - filter.Search = search - } - tags := make([][]string, 0, 5) - for _, tagFlag := range c.StringSlice("tag") { - spl := strings.Split(tagFlag, "=") - if len(spl) == 2 && len(spl[0]) == 1 { - tags = append(tags, spl) - } else { - return fmt.Errorf("invalid --tag '%s'", tagFlag) - } - } - for _, etag := range c.StringSlice("e") { - tags = append(tags, []string{"e", etag}) - } - for _, ptag := range c.StringSlice("p") { - tags = append(tags, []string{"p", ptag}) - } - for _, dtag := range c.StringSlice("d") { - tags = append(tags, []string{"d", dtag}) - } - - if len(tags) > 0 && filter.Tags == nil { - filter.Tags = make(nostr.TagMap) - } - - for _, tag := range tags { - if _, ok := filter.Tags[tag[0]]; !ok { - filter.Tags[tag[0]] = make([]string, 0, 3) - } - filter.Tags[tag[0]] = append(filter.Tags[tag[0]], tag[1]) - } - - if c.IsSet("since") { - nts := getNaturalDate(c, "since") - filter.Since = &nts - } - - if c.IsSet("until") { - nts := getNaturalDate(c, "until") - filter.Until = &nts - } - - if limit := c.Uint("limit"); limit != 0 { - filter.Limit = int(limit) - } else if c.IsSet("limit") { - filter.LimitZero = true + if err := applyFlagsToFilter(c, &filter); err != nil { + return err } if len(relayUrls) > 0 { @@ -288,3 +180,130 @@ example: return nil }, } + +var reqFilterFlags = []cli.Flag{ + &cli.StringSliceFlag{ + Name: "author", + Aliases: []string{"a"}, + Usage: "only accept events from these authors (pubkey as hex)", + Category: CATEGORY_FILTER_ATTRIBUTES, + }, + &cli.StringSliceFlag{ + Name: "id", + Aliases: []string{"i"}, + Usage: "only accept events with these ids (hex)", + Category: CATEGORY_FILTER_ATTRIBUTES, + }, + &cli.IntSliceFlag{ + Name: "kind", + Aliases: []string{"k"}, + Usage: "only accept events with these kind numbers", + Category: CATEGORY_FILTER_ATTRIBUTES, + }, + &cli.StringSliceFlag{ + Name: "tag", + Aliases: []string{"t"}, + Usage: "takes a tag like -t e=, only accept events with these tags", + Category: CATEGORY_FILTER_ATTRIBUTES, + }, + &cli.StringSliceFlag{ + Name: "e", + Usage: "shortcut for --tag e=", + Category: CATEGORY_FILTER_ATTRIBUTES, + }, + &cli.StringSliceFlag{ + Name: "p", + Usage: "shortcut for --tag p=", + Category: CATEGORY_FILTER_ATTRIBUTES, + }, + &cli.StringSliceFlag{ + Name: "d", + Usage: "shortcut for --tag d=", + Category: CATEGORY_FILTER_ATTRIBUTES, + }, + &NaturalTimeFlag{ + Name: "since", + Aliases: []string{"s"}, + Usage: "only accept events newer than this (unix timestamp)", + Category: CATEGORY_FILTER_ATTRIBUTES, + }, + &NaturalTimeFlag{ + Name: "until", + Aliases: []string{"u"}, + Usage: "only accept events older than this (unix timestamp)", + Category: CATEGORY_FILTER_ATTRIBUTES, + }, + &cli.UintFlag{ + Name: "limit", + Aliases: []string{"l"}, + Usage: "only accept up to this number of events", + Category: CATEGORY_FILTER_ATTRIBUTES, + }, + &cli.StringFlag{ + Name: "search", + Usage: "a NIP-50 search query, use it only with relays that explicitly support it", + Category: CATEGORY_FILTER_ATTRIBUTES, + }, +} + +func applyFlagsToFilter(c *cli.Command, filter *nostr.Filter) error { + if authors := c.StringSlice("author"); len(authors) > 0 { + filter.Authors = append(filter.Authors, authors...) + } + if ids := c.StringSlice("id"); len(ids) > 0 { + filter.IDs = append(filter.IDs, ids...) + } + for _, kind64 := range c.IntSlice("kind") { + filter.Kinds = append(filter.Kinds, int(kind64)) + } + if search := c.String("search"); search != "" { + filter.Search = search + } + tags := make([][]string, 0, 5) + for _, tagFlag := range c.StringSlice("tag") { + spl := strings.Split(tagFlag, "=") + if len(spl) == 2 && len(spl[0]) == 1 { + tags = append(tags, spl) + } else { + return fmt.Errorf("invalid --tag '%s'", tagFlag) + } + } + for _, etag := range c.StringSlice("e") { + tags = append(tags, []string{"e", etag}) + } + for _, ptag := range c.StringSlice("p") { + tags = append(tags, []string{"p", ptag}) + } + for _, dtag := range c.StringSlice("d") { + tags = append(tags, []string{"d", dtag}) + } + + if len(tags) > 0 && filter.Tags == nil { + filter.Tags = make(nostr.TagMap) + } + + for _, tag := range tags { + if _, ok := filter.Tags[tag[0]]; !ok { + filter.Tags[tag[0]] = make([]string, 0, 3) + } + filter.Tags[tag[0]] = append(filter.Tags[tag[0]], tag[1]) + } + + if c.IsSet("since") { + nts := getNaturalDate(c, "since") + filter.Since = &nts + } + + if c.IsSet("until") { + nts := getNaturalDate(c, "until") + filter.Until = &nts + } + + if limit := c.Uint("limit"); limit != 0 { + filter.Limit = int(limit) + } else if c.IsSet("limit") { + filter.LimitZero = true + } + + return nil +} From 56657d8aa9f84c5ebb5f4a4998f4c917cab42e69 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 20 Aug 2024 15:10:18 -0300 Subject: [PATCH 141/401] update go-nostr. --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index cff7aec..c047c13 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/fiatjaf/khatru v0.7.5 github.com/mailru/easyjson v0.7.7 github.com/markusmobius/go-dateparser v1.2.3 - github.com/nbd-wtf/go-nostr v0.34.5 + github.com/nbd-wtf/go-nostr v0.34.7 github.com/nbd-wtf/nostr-sdk v0.5.0 golang.org/x/exp v0.0.0-20240707233637-46b078467d37 ) diff --git a/go.sum b/go.sum index 3223daf..3e61686 100644 --- a/go.sum +++ b/go.sum @@ -113,8 +113,8 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/nbd-wtf/go-nostr v0.34.5 h1:vti8WqvGWbVoWAPniaz7li2TpCyC+7ZS62Gmy7ib/z0= -github.com/nbd-wtf/go-nostr v0.34.5/go.mod h1:NZQkxl96ggbO8rvDpVjcsojJqKTPwqhP4i82O7K5DJs= +github.com/nbd-wtf/go-nostr v0.34.7 h1:gQP3rHC+aBw3dsu9ubZn8tV0eDBmLrMpmNCjj5nFUTE= +github.com/nbd-wtf/go-nostr v0.34.7/go.mod h1:NZQkxl96ggbO8rvDpVjcsojJqKTPwqhP4i82O7K5DJs= github.com/nbd-wtf/nostr-sdk v0.5.0 h1:zrMxcvMSxkw29RyfXEdF3XW5rUWLuT5Q9oBAhd5dyew= github.com/nbd-wtf/nostr-sdk v0.5.0/go.mod h1:MJ7gYv3XiZKU6MHSM0N7oHqQAQhbvpgGQk4Q+XUdIUs= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= From a4d9ceecfab2bd99e6dc67364411e0bcac18e234 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 20 Aug 2024 17:13:01 -0300 Subject: [PATCH 142/401] do it again because blergh. --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index c047c13..2ca87ff 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/fiatjaf/khatru v0.7.5 github.com/mailru/easyjson v0.7.7 github.com/markusmobius/go-dateparser v1.2.3 - github.com/nbd-wtf/go-nostr v0.34.7 + github.com/nbd-wtf/go-nostr v0.34.8 github.com/nbd-wtf/nostr-sdk v0.5.0 golang.org/x/exp v0.0.0-20240707233637-46b078467d37 ) diff --git a/go.sum b/go.sum index 3e61686..2142d98 100644 --- a/go.sum +++ b/go.sum @@ -113,8 +113,8 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/nbd-wtf/go-nostr v0.34.7 h1:gQP3rHC+aBw3dsu9ubZn8tV0eDBmLrMpmNCjj5nFUTE= -github.com/nbd-wtf/go-nostr v0.34.7/go.mod h1:NZQkxl96ggbO8rvDpVjcsojJqKTPwqhP4i82O7K5DJs= +github.com/nbd-wtf/go-nostr v0.34.8 h1:wExcoeaFX5DZKzZruQlrEYVd59Z6GmRyPCltTuoyn9g= +github.com/nbd-wtf/go-nostr v0.34.8/go.mod h1:NZQkxl96ggbO8rvDpVjcsojJqKTPwqhP4i82O7K5DJs= github.com/nbd-wtf/nostr-sdk v0.5.0 h1:zrMxcvMSxkw29RyfXEdF3XW5rUWLuT5Q9oBAhd5dyew= github.com/nbd-wtf/nostr-sdk v0.5.0/go.mod h1:MJ7gYv3XiZKU6MHSM0N7oHqQAQhbvpgGQk4Q+XUdIUs= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= From 0240866fa1c118e39d1a837296dd10374feb5ee2 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 20 Aug 2024 18:39:17 -0300 Subject: [PATCH 143/401] fix fetch for non-pubkey cases. --- fetch.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/fetch.go b/fetch.go index d06723d..dbb0938 100644 --- a/fetch.go +++ b/fetch.go @@ -91,10 +91,10 @@ var fetch = &cli.Command{ for _, url := range relays { relays = append(relays, url) } + } - if len(filter.Kinds) == 0 { - filter.Kinds = append(filter.Kinds, 0) - } + if len(filter.Authors) > 0 && len(filter.Kinds) == 0 { + filter.Kinds = append(filter.Kinds, 0) } if err := applyFlagsToFilter(c, &filter); err != nil { From 014c6bc11dd2a8ea763214984a4317773ed495e2 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 20 Aug 2024 23:06:14 -0300 Subject: [PATCH 144/401] --pow: parallel work. --- event.go | 6 ++++-- go.mod | 2 +- go.sum | 4 ++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/event.go b/event.go index cedb0b8..10dec32 100644 --- a/event.go +++ b/event.go @@ -269,8 +269,10 @@ example: evt.PubKey, _ = nostr.GetPublicKey(sec) } - // try to generate work with this difficulty -- essentially forever - nip13.Generate(&evt, int(difficulty), time.Hour*24*365) + // try to generate work with this difficulty -- runs forever + nonceTag, _ := nip13.DoWork(ctx, evt, int(difficulty)) + evt.Tags = append(evt.Tags, nonceTag) + mustRehashAndResign = true } diff --git a/go.mod b/go.mod index 2ca87ff..c8c5956 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/fiatjaf/khatru v0.7.5 github.com/mailru/easyjson v0.7.7 github.com/markusmobius/go-dateparser v1.2.3 - github.com/nbd-wtf/go-nostr v0.34.8 + github.com/nbd-wtf/go-nostr v0.34.9 github.com/nbd-wtf/nostr-sdk v0.5.0 golang.org/x/exp v0.0.0-20240707233637-46b078467d37 ) diff --git a/go.sum b/go.sum index 2142d98..b79c403 100644 --- a/go.sum +++ b/go.sum @@ -113,8 +113,8 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/nbd-wtf/go-nostr v0.34.8 h1:wExcoeaFX5DZKzZruQlrEYVd59Z6GmRyPCltTuoyn9g= -github.com/nbd-wtf/go-nostr v0.34.8/go.mod h1:NZQkxl96ggbO8rvDpVjcsojJqKTPwqhP4i82O7K5DJs= +github.com/nbd-wtf/go-nostr v0.34.9 h1:kM/+nCEKEUwHT9LhSPnS1ucS9yYG6t5VkxlYdXdMchU= +github.com/nbd-wtf/go-nostr v0.34.9/go.mod h1:NZQkxl96ggbO8rvDpVjcsojJqKTPwqhP4i82O7K5DJs= github.com/nbd-wtf/nostr-sdk v0.5.0 h1:zrMxcvMSxkw29RyfXEdF3XW5rUWLuT5Q9oBAhd5dyew= github.com/nbd-wtf/nostr-sdk v0.5.0/go.mod h1:MJ7gYv3XiZKU6MHSM0N7oHqQAQhbvpgGQk4Q+XUdIUs= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= From cfdea699bc8efb448d042ea7598ccb9bed7392de Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Wed, 21 Aug 2024 10:41:19 -0300 Subject: [PATCH 145/401] fix using NOSTR_SECRET_KEY environment variable. --- README.md | 9 +++++++++ bunker.go | 1 - event.go | 1 - helpers.go | 9 ++++++--- relay.go | 1 - req.go | 1 - 6 files changed, 15 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index fc7c512..050b921 100644 --- a/README.md +++ b/README.md @@ -200,6 +200,15 @@ listening at [wss://relay.damus.io wss://nos.lol wss://relay.nsecbunker.com]: {"kind":1,"id":"0000009dcc7c62056eafdb41fac817379ec2becf0ce27c5fbe98d0735d968147","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1724160828,"tags":[["nonce","515504","24"]],"content":"hello getwired.app and labour.fiatjaf.com","sig":"7edb988065ccc12779fe99270945b212f3723838f315d76d5e90e9ffa27198f13fa556614295f518d968d55bab81878167d4162b3a7cf81a6b423c6761bd504c"} ``` +### make a nostr event signed with a key given as an environment variable + +```shell +~> export NOSTR_SECRET_KEY=ncryptsec1qggyy9vw0nclmw8ly9caz6aa7f85a4ufhsct64uva337pulsdw00n6twa2lzhzk2znzsyu60urx9s08lx00ke6ual3lszyn5an9zarm6s70lw5lj6dv3mj3f9p4tvp0we6qyz4gp420mapfmvqheuttv +~> nak event -c 'it supports keys as hex, nsec or ncryptsec' +type the password to decrypt your secret key: ******** +{"kind":1,"id":"5cbf3feb9a7d99c3ee2a88693a591caca1a8348fea427b3652c27f7a8a76af48","pubkey":"b00bcab55375d8c7b731dd9841f6d805ff1cf6fdc945e7326786deb5ddac6ce4","created_at":1724247924,"tags":[],"content":"it supports keys as hex, nsec or ncryptsec","sig":"fb3fd170bc10e5042322c7a05dd4bbd8ac9947b39026b8a7afd1ee02524e8e3aa1d9554e9c7b6181ca1b45cab01cd06643bdffa5ce678b475e6b185e1c14b085"} +``` + ## contributing to this repository Use NIP-34 to send your patches to `naddr1qqpkucttqy28wumn8ghj7un9d3shjtnwdaehgu3wvfnsz9nhwden5te0wfjkccte9ehx7um5wghxyctwvsq3gamnwvaz7tmjv4kxz7fwv3sk6atn9e5k7q3q80cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsxpqqqpmej2wctpn`. diff --git a/bunker.go b/bunker.go index d2ea672..382310e 100644 --- a/bunker.go +++ b/bunker.go @@ -29,7 +29,6 @@ var bunker = &cli.Command{ Name: "sec", Usage: "secret key to sign the event, as hex or nsec", DefaultText: "the key '1'", - Value: "0000000000000000000000000000000000000000000000000000000000000001", }, &cli.BoolFlag{ Name: "prompt-sec", diff --git a/event.go b/event.go index 10dec32..9925244 100644 --- a/event.go +++ b/event.go @@ -42,7 +42,6 @@ example: Name: "sec", Usage: "secret key to sign the event, as nsec, ncryptsec or hex", DefaultText: "the key '1'", - Value: "0000000000000000000000000000000000000000000000000000000000000001", Category: CATEGORY_SIGNER, }, &cli.BoolFlag{ diff --git a/helpers.go b/helpers.go index 1b4852f..fea23f7 100644 --- a/helpers.go +++ b/helpers.go @@ -196,12 +196,13 @@ func gatherSecretKeyOrBunkerFromArguments(ctx context.Context, c *cli.Command) ( return "", bunker, err } + // take private from flags, environment variable or default to 1 sec := c.String("sec") - - // check in the environment for the secret key if sec == "" { - if key, ok := os.LookupEnv("NOSTR_PRIVATE_KEY"); ok { + if key, ok := os.LookupEnv("NOSTR_SECRET_KEY"); ok { sec = key + } else { + sec = "0000000000000000000000000000000000000000000000000000000000000001" } } @@ -214,6 +215,7 @@ func gatherSecretKeyOrBunkerFromArguments(ctx context.Context, c *cli.Command) ( return "", nil, fmt.Errorf("failed to get secret key: %w", err) } } + if strings.HasPrefix(sec, "ncryptsec1") { sec, err = promptDecrypt(sec) if err != nil { @@ -230,6 +232,7 @@ func gatherSecretKeyOrBunkerFromArguments(ctx context.Context, c *cli.Command) ( if ok := nostr.IsValid32ByteHex(sec); !ok { return "", nil, fmt.Errorf("invalid secret key") } + return sec, nil, nil } diff --git a/relay.go b/relay.go index f663bc1..066e78c 100644 --- a/relay.go +++ b/relay.go @@ -79,7 +79,6 @@ var relay = &cli.Command{ Name: "sec", Usage: "secret key to sign the event, as nsec, ncryptsec or hex", DefaultText: "the key '1'", - Value: "0000000000000000000000000000000000000000000000000000000000000001", }, &cli.BoolFlag{ Name: "prompt-sec", diff --git a/req.go b/req.go index 4d96c56..580ae4e 100644 --- a/req.go +++ b/req.go @@ -69,7 +69,6 @@ example: Name: "sec", Usage: "secret key to sign the AUTH challenge, as hex or nsec", DefaultText: "the key '1'", - Value: "0000000000000000000000000000000000000000000000000000000000000001", Category: CATEGORY_SIGNER, }, &cli.BoolFlag{ From b3ef2c128970357f92c9009f6e0bbe20469d5157 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Wed, 21 Aug 2024 17:09:14 -0300 Subject: [PATCH 146/401] update go-nostr because parallel work generation was broken. --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index c8c5956..83d8374 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/fiatjaf/khatru v0.7.5 github.com/mailru/easyjson v0.7.7 github.com/markusmobius/go-dateparser v1.2.3 - github.com/nbd-wtf/go-nostr v0.34.9 + github.com/nbd-wtf/go-nostr v0.34.10 github.com/nbd-wtf/nostr-sdk v0.5.0 golang.org/x/exp v0.0.0-20240707233637-46b078467d37 ) diff --git a/go.sum b/go.sum index b79c403..1347bd0 100644 --- a/go.sum +++ b/go.sum @@ -113,8 +113,8 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/nbd-wtf/go-nostr v0.34.9 h1:kM/+nCEKEUwHT9LhSPnS1ucS9yYG6t5VkxlYdXdMchU= -github.com/nbd-wtf/go-nostr v0.34.9/go.mod h1:NZQkxl96ggbO8rvDpVjcsojJqKTPwqhP4i82O7K5DJs= +github.com/nbd-wtf/go-nostr v0.34.10 h1:scJH45sFk5LOzHJNLw0EFTknCCKfKlo3tK+vdpTHz3Q= +github.com/nbd-wtf/go-nostr v0.34.10/go.mod h1:NZQkxl96ggbO8rvDpVjcsojJqKTPwqhP4i82O7K5DJs= github.com/nbd-wtf/nostr-sdk v0.5.0 h1:zrMxcvMSxkw29RyfXEdF3XW5rUWLuT5Q9oBAhd5dyew= github.com/nbd-wtf/nostr-sdk v0.5.0/go.mod h1:MJ7gYv3XiZKU6MHSM0N7oHqQAQhbvpgGQk4Q+XUdIUs= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= From cf1694704ea6942c68e99136bccafdc5106dbdcc Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Fri, 23 Aug 2024 16:17:17 -0300 Subject: [PATCH 147/401] bunker: fix printing bunker uri. --- bunker.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bunker.go b/bunker.go index 382310e..452912c 100644 --- a/bunker.go +++ b/bunker.go @@ -84,8 +84,8 @@ var bunker = &cli.Command{ return err } npub, _ := nip19.EncodePublicKey(pubkey) - bold := color.New(color.Bold).Sprintf - italic := color.New(color.Italic).Sprintf + bold := color.New(color.Bold).Sprint + italic := color.New(color.Italic).Sprint // this function will be called every now and then printBunkerInfo := func() { @@ -131,7 +131,7 @@ var bunker = &cli.Command{ ) log("listening at %v:\n pubkey: %s \n npub: %s%s%s\n to restart: %s\n bunker: %s\n\n", - bold("%v", relayURLs), + bold(relayURLs), bold(pubkey), bold(npub), authorizedKeysStr, From 11f37afa5be34fb345001763c3b5da22f3631da4 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sat, 24 Aug 2024 21:40:08 -0300 Subject: [PATCH 148/401] readme: how to watch livestreams from your terminal. --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.md b/README.md index 050b921..9e2ed65 100644 --- a/README.md +++ b/README.md @@ -209,6 +209,20 @@ type the password to decrypt your secret key: ******** {"kind":1,"id":"5cbf3feb9a7d99c3ee2a88693a591caca1a8348fea427b3652c27f7a8a76af48","pubkey":"b00bcab55375d8c7b731dd9841f6d805ff1cf6fdc945e7326786deb5ddac6ce4","created_at":1724247924,"tags":[],"content":"it supports keys as hex, nsec or ncryptsec","sig":"fb3fd170bc10e5042322c7a05dd4bbd8ac9947b39026b8a7afd1ee02524e8e3aa1d9554e9c7b6181ca1b45cab01cd06643bdffa5ce678b475e6b185e1c14b085"} ``` +### download some helpful `jq` functions for dealing with nostr events +```shell +~> nak req -i 412f2d3e73acc312942c055ac2a695dc60bf58ff97e06689a8a79e97796c4cdb relay.westernbtc.com | jq -r .content > ~/.jq +``` + +### watch a NIP-53 livestream (zap.stream etc) +```shell +~> # this requires the jq utils from the step above +~> mpv $(nak fetch naddr1qqjxvvm9xscnsdtx95cxvcfk956rsvtx943rje3k95mx2dp389jnwwrp8ymxgqg4waehxw309aex2mrp0yhxgctdw4eju6t09upzpn6956apxcad0mfp8grcuugdysg44eepex68h50t73zcathmfs49qvzqqqrkvu7ed38k | jq -r 'tag_value("streaming")') +~> +~> # or without the utils +~> mpv $(nak fetch naddr1qqjxvvm9xscnsdtx95cxvcfk956rsvtx943rje3k95mx2dp389jnwwrp8ymxgqg4waehxw309aex2mrp0yhxgctdw4eju6t09upzpn6956apxcad0mfp8grcuugdysg44eepex68h50t73zcathmfs49qvzqqqrkvu7ed38k | jq -r '.tags | map(select(.[0] == "streaming") | .[1])[0]') +``` + ## contributing to this repository Use NIP-34 to send your patches to `naddr1qqpkucttqy28wumn8ghj7un9d3shjtnwdaehgu3wvfnsz9nhwden5te0wfjkccte9ehx7um5wghxyctwvsq3gamnwvaz7tmjv4kxz7fwv3sk6atn9e5k7q3q80cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsxpqqqpmej2wctpn`. From 29b6ecbafed8b39044f06fc943c7bfc22f6d0231 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sun, 25 Aug 2024 00:23:46 -0300 Subject: [PATCH 149/401] readme: how to download torrents. --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 9e2ed65..0ddabc7 100644 --- a/README.md +++ b/README.md @@ -223,6 +223,12 @@ type the password to decrypt your secret key: ******** ~> mpv $(nak fetch naddr1qqjxvvm9xscnsdtx95cxvcfk956rsvtx943rje3k95mx2dp389jnwwrp8ymxgqg4waehxw309aex2mrp0yhxgctdw4eju6t09upzpn6956apxcad0mfp8grcuugdysg44eepex68h50t73zcathmfs49qvzqqqrkvu7ed38k | jq -r '.tags | map(select(.[0] == "streaming") | .[1])[0]') ``` +### download a NIP-35 torrent from an `nevent` +```shell +~> # this requires the jq utils from two steps above +~> aria2c $(nak fetch nevent1qqsdsg6x7uujekac4ga7k7qa9q9sx8gqj7xzjf5w9us0dm0ghvf4ugspp4mhxue69uhkummn9ekx7mq6dw9y4 | jq -r '"magnet:?xt=urn:btih:\(tag_value("x"))&dn=\(tag_value("title"))&tr=http%3A%2F%2Ftracker.loadpeers.org%3A8080%2FxvRKfvAlnfuf5EfxTT5T0KIVPtbqAHnX%2Fannounce&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A6969%2Fannounce&tr=udp%3A%2F%2Fopen.stealth.si%3A80%2Fannounce&tr=udp%3A%2F%2Ftracker.torrent.eu.org%3A451%2Fannounce&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337&tr=\(tags("tracker") | map(.[1] | @uri) | join("&tr="))"') +``` + ## contributing to this repository Use NIP-34 to send your patches to `naddr1qqpkucttqy28wumn8ghj7un9d3shjtnwdaehgu3wvfnsz9nhwden5te0wfjkccte9ehx7um5wghxyctwvsq3gamnwvaz7tmjv4kxz7fwv3sk6atn9e5k7q3q80cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsxpqqqpmej2wctpn`. From 6d23509d8c0fc4af34ad444cd2a3d0ef2b10af3f Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sun, 25 Aug 2024 17:09:42 -0300 Subject: [PATCH 150/401] fetch: handle note1 case. --- fetch.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/fetch.go b/fetch.go index dbb0938..4343106 100644 --- a/fetch.go +++ b/fetch.go @@ -2,6 +2,7 @@ package main import ( "context" + "fmt" "github.com/fiatjaf/cli/v3" "github.com/nbd-wtf/go-nostr" @@ -68,6 +69,8 @@ var fetch = &cli.Command{ authorHint = v.Author } relays = append(relays, v.Relays...) + case "note": + filter.IDs = append(filter.IDs, value.(string)) case "naddr": v := value.(nostr.EntityPointer) filter.Tags = nostr.TagMap{"d": []string{v.Identifier}} @@ -83,6 +86,8 @@ var fetch = &cli.Command{ v := value.(string) filter.Authors = append(filter.Authors, v) authorHint = v + default: + return fmt.Errorf("unexpected prefix %s", prefix) } } From 36c32ae308d0f26dc1ad065730768ee600c41511 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Mon, 26 Aug 2024 15:48:06 -0300 Subject: [PATCH 151/401] make it possible to have empty content on kind 1. fixes https://github.com/fiatjaf/nak/issues/32 --- event.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/event.go b/event.go index 9925244..586f0d2 100644 --- a/event.go +++ b/event.go @@ -202,8 +202,8 @@ example: mustRehashAndResign = true } - if content := c.String("content"); content != "" { - evt.Content = content + if c.IsSet("content") { + evt.Content = c.String("content") mustRehashAndResign = true } else if evt.Content == "" && evt.Kind == 1 { evt.Content = "hello from the nostr army knife" From e0c967efa985a4c0b387db3a885ffd1f5daa2552 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Mon, 26 Aug 2024 15:59:48 -0300 Subject: [PATCH 152/401] fix natural timestamps test. --- example_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/example_test.go b/example_test.go index b94a321..d713dee 100644 --- a/example_test.go +++ b/example_test.go @@ -118,7 +118,7 @@ func ExampleReqWithFlagsAfter3() { } func ExampleNaturalTimestamps() { - app.Run(ctx, []string{"nak", "event", "-t", "plu=pla", "-e", "3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24", "--ts", "2018-05-19 03:37:19", "-c", "nn"}) + app.Run(ctx, []string{"nak", "event", "-t", "plu=pla", "-e", "3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24", "--ts", "May 19 2018 03:37:19", "-c", "nn"}) // Output: - // {"kind":1,"id":"0000d199127d5e15046b0a3f2885d464ee18f70968303665ef76326a7d828312","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1724160467,"tags":[["plu","pla"],["e","3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24"],["nonce","24783","16"]],"content":"nn","sig":"99471b43ce82ca01fb9b61f36b45ca542870854b2466a9d3884891598f7d7baef36d07f4b02bb194f2f6f781973f24c3d946f702c82321c6cb0c564e76cf43db"} + // {"kind":0,"id":"b10da0095f96aa2accd99fa3d93bf29a76f51d2594cf5a0a52f8e961aecd0b67","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1526711839,"tags":[["plu","pla"],["e","3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24"]],"content":"nn","sig":"988442c97064a041ba5e2bfbd64e84d3f819b2169e865511d9d53e74667949ff165325942acaa2ca233c8b529adedf12cf44088cf04081b56d098c5f4d52dd8f"} } From 8a934cc76bb36165b7d7e87cf27d425af5a1c203 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Wed, 28 Aug 2024 16:13:19 -0300 Subject: [PATCH 153/401] fix fetch naddr missing kind. --- fetch.go | 1 + 1 file changed, 1 insertion(+) diff --git a/fetch.go b/fetch.go index 4343106..380b93e 100644 --- a/fetch.go +++ b/fetch.go @@ -73,6 +73,7 @@ var fetch = &cli.Command{ filter.IDs = append(filter.IDs, value.(string)) case "naddr": v := value.(nostr.EntityPointer) + filter.Kinds = []int{v.Kind} filter.Tags = nostr.TagMap{"d": []string{v.Identifier}} filter.Authors = append(filter.Authors, v.PublicKey) authorHint = v.PublicKey From 88a07a35043fdb3df38e7f9d20f305121011c75f Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Thu, 5 Sep 2024 14:43:34 -0300 Subject: [PATCH 154/401] update go-nostr and nostr-sdk to fix bad nevent/naddr parsing bug. --- decode.go | 7 +++++-- go.mod | 8 ++++---- go.sum | 8 ++++---- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/decode.go b/decode.go index 95aa1b8..dac7260 100644 --- a/decode.go +++ b/decode.go @@ -59,8 +59,11 @@ var decode = &cli.Command{ } else if pp := sdk.InputToProfile(ctx, input); pp != nil { decodeResult = DecodeResult{ProfilePointer: pp} } else if prefix, value, err := nip19.Decode(input); err == nil && prefix == "naddr" { - ep := value.(nostr.EntityPointer) - decodeResult = DecodeResult{EntityPointer: &ep} + if ep, ok := value.(nostr.EntityPointer); ok { + decodeResult = DecodeResult{EntityPointer: &ep} + } else { + ctx = lineProcessingError(ctx, "couldn't decode naddr: %s", err) + } } else if prefix, value, err := nip19.Decode(input); err == nil && prefix == "nsec" { decodeResult.PrivateKey.PrivateKey = value.(string) decodeResult.PrivateKey.PublicKey, _ = nostr.GetPublicKey(value.(string)) diff --git a/go.mod b/go.mod index 83d8374..17b4743 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,8 @@ module github.com/fiatjaf/nak -go 1.22 +go 1.23 -toolchain go1.22.4 +toolchain go1.23.0 require ( github.com/bep/debounce v1.2.1 @@ -15,8 +15,8 @@ require ( github.com/fiatjaf/khatru v0.7.5 github.com/mailru/easyjson v0.7.7 github.com/markusmobius/go-dateparser v1.2.3 - github.com/nbd-wtf/go-nostr v0.34.10 - github.com/nbd-wtf/nostr-sdk v0.5.0 + github.com/nbd-wtf/go-nostr v0.34.14 + github.com/nbd-wtf/nostr-sdk v0.5.3 golang.org/x/exp v0.0.0-20240707233637-46b078467d37 ) diff --git a/go.sum b/go.sum index 1347bd0..c3151aa 100644 --- a/go.sum +++ b/go.sum @@ -113,10 +113,10 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/nbd-wtf/go-nostr v0.34.10 h1:scJH45sFk5LOzHJNLw0EFTknCCKfKlo3tK+vdpTHz3Q= -github.com/nbd-wtf/go-nostr v0.34.10/go.mod h1:NZQkxl96ggbO8rvDpVjcsojJqKTPwqhP4i82O7K5DJs= -github.com/nbd-wtf/nostr-sdk v0.5.0 h1:zrMxcvMSxkw29RyfXEdF3XW5rUWLuT5Q9oBAhd5dyew= -github.com/nbd-wtf/nostr-sdk v0.5.0/go.mod h1:MJ7gYv3XiZKU6MHSM0N7oHqQAQhbvpgGQk4Q+XUdIUs= +github.com/nbd-wtf/go-nostr v0.34.14 h1:o4n2LkuAtdIjNYJ23sFbcx68UXLnji4j8hYR1Sd2wgI= +github.com/nbd-wtf/go-nostr v0.34.14/go.mod h1:NZQkxl96ggbO8rvDpVjcsojJqKTPwqhP4i82O7K5DJs= +github.com/nbd-wtf/nostr-sdk v0.5.3 h1:jaiT7xm2h3iksM96PQKlbl5zDpMuZFF3fVj8TknWoJU= +github.com/nbd-wtf/nostr-sdk v0.5.3/go.mod h1:9zlqzVbIczMHeN3fy3Ib2/fStRupTPVLvy54Htd9FCE= 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= From 9bbc87b27afd74a4186c16cdb339fb029e6a45d2 Mon Sep 17 00:00:00 2001 From: arkinox <99223753+arkin0x@users.noreply.github.com> Date: Tue, 10 Sep 2024 15:44:33 -0500 Subject: [PATCH 155/401] specify how ; can separate multiple tag values --- event.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/event.go b/event.go index 586f0d2..e0ecee7 100644 --- a/event.go +++ b/event.go @@ -125,7 +125,7 @@ example: &cli.StringSliceFlag{ Name: "tag", Aliases: []string{"t"}, - Usage: "sets a tag field on the event, takes a value like -t e=", + Usage: "sets a tag field on the event, takes a value like -t e= or -t sometag=\"value one;value two;value three\"", Category: CATEGORY_EVENT_FIELDS, }, &cli.StringSliceFlag{ From 9d02301b2dc517ae93d9141a6421cf7f7fb4902d Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sun, 15 Sep 2024 08:57:53 -0300 Subject: [PATCH 156/401] support --version using -X --- .github/workflows/release-cli.yml | 1 + main.go | 3 +++ 2 files changed, 4 insertions(+) diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml index f5c90a8..a515176 100644 --- a/.github/workflows/release-cli.yml +++ b/.github/workflows/release-cli.yml @@ -40,6 +40,7 @@ jobs: github_token: ${{ secrets.GITHUB_TOKEN }} goos: ${{ matrix.goos }} goarch: ${{ matrix.goarch }} + ldflags: -X main.version=${{ github.ref }} overwrite: true md5sum: false sha256sum: false diff --git a/main.go b/main.go index 9e236da..218e5f2 100644 --- a/main.go +++ b/main.go @@ -7,6 +7,8 @@ import ( "github.com/fiatjaf/cli/v3" ) +var version string = "debug" + var app = &cli.Command{ Name: "nak", Suggest: true, @@ -27,6 +29,7 @@ var app = &cli.Command{ bunker, serve, }, + Version: version, Flags: []cli.Flag{ &cli.BoolFlag{ Name: "quiet", From bd5ca276616996355e546b0f33313a7f2b8e64ab Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sun, 15 Sep 2024 09:04:52 -0300 Subject: [PATCH 157/401] github.ref->github.ref_name as version variable. --- .github/workflows/release-cli.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml index a515176..d794d94 100644 --- a/.github/workflows/release-cli.yml +++ b/.github/workflows/release-cli.yml @@ -40,7 +40,7 @@ jobs: github_token: ${{ secrets.GITHUB_TOKEN }} goos: ${{ matrix.goos }} goarch: ${{ matrix.goarch }} - ldflags: -X main.version=${{ github.ref }} + ldflags: -X main.version=${{ github.ref_name }} overwrite: true md5sum: false sha256sum: false From 2b5f3355bc61e4fe3d741dc24b7061bea90586ba Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 17 Sep 2024 08:09:20 -0300 Subject: [PATCH 158/401] use a single global sdk.System and its Pool. --- bunker.go | 7 +++---- common.go | 7 +++++++ decode.go | 2 +- event.go | 2 +- fetch.go | 3 --- go.mod | 27 ++++++++++++--------------- go.sum | 46 ++++++++++++++++++++++------------------------ helpers.go | 7 +++---- paginate.go | 7 +++++-- req.go | 11 ++++------- 10 files changed, 58 insertions(+), 61 deletions(-) create mode 100644 common.go diff --git a/bunker.go b/bunker.go index 452912c..4297e02 100644 --- a/bunker.go +++ b/bunker.go @@ -50,7 +50,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, relayUrls, false) + relays := connectToAllRelays(ctx, relayUrls, false) if len(relays) == 0 { log("failed to connect to any of the given relays.\n") os.Exit(3) @@ -143,9 +143,8 @@ var bunker = &cli.Command{ printBunkerInfo() // subscribe to relays - pool := nostr.NewSimplePool(ctx) now := nostr.Now() - events := pool.SubMany(ctx, relayURLs, nostr.Filters{ + events := sys.Pool.SubMany(ctx, relayURLs, nostr.Filters{ { Kinds: []int{nostr.KindNostrConnect}, Tags: nostr.TagMap{"p": []string{pubkey}}, @@ -198,7 +197,7 @@ var bunker = &cli.Command{ handlerWg.Add(len(relayURLs)) for _, relayURL := range relayURLs { go func(relayURL string) { - if relay, _ := pool.EnsureRelay(relayURL); relay != nil { + if relay, _ := sys.Pool.EnsureRelay(relayURL); relay != nil { err := relay.Publish(ctx, eventResponse) printLock.Lock() if err == nil { diff --git a/common.go b/common.go new file mode 100644 index 0000000..f4b3335 --- /dev/null +++ b/common.go @@ -0,0 +1,7 @@ +package main + +import ( + "github.com/nbd-wtf/go-nostr/sdk" +) + +var sys = sdk.NewSystem() diff --git a/decode.go b/decode.go index dac7260..9da34ad 100644 --- a/decode.go +++ b/decode.go @@ -9,7 +9,7 @@ import ( "github.com/fiatjaf/cli/v3" "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/nip19" - sdk "github.com/nbd-wtf/nostr-sdk" + "github.com/nbd-wtf/go-nostr/sdk" ) var decode = &cli.Command{ diff --git a/event.go b/event.go index e0ecee7..d2aa549 100644 --- a/event.go +++ b/event.go @@ -157,7 +157,7 @@ example: // try to connect to the relays here var relays []*nostr.Relay if relayUrls := c.Args().Slice(); len(relayUrls) > 0 { - _, relays = connectToAllRelays(ctx, relayUrls, false) + relays = connectToAllRelays(ctx, relayUrls, false) if len(relays) == 0 { log("failed to connect to any of the given relays.\n") os.Exit(3) diff --git a/fetch.go b/fetch.go index 380b93e..6bd40b9 100644 --- a/fetch.go +++ b/fetch.go @@ -8,7 +8,6 @@ import ( "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/nip05" "github.com/nbd-wtf/go-nostr/nip19" - sdk "github.com/nbd-wtf/nostr-sdk" ) var fetch = &cli.Command{ @@ -27,8 +26,6 @@ var fetch = &cli.Command{ ), ArgsUsage: "[nip05_or_nip19_code]", Action: func(ctx context.Context, c *cli.Command) error { - sys := sdk.NewSystem() - defer func() { sys.Pool.Relays.Range(func(_ string, relay *nostr.Relay) bool { relay.Close() diff --git a/go.mod b/go.mod index 17b4743..9d43b24 100644 --- a/go.mod +++ b/go.mod @@ -1,23 +1,20 @@ module github.com/fiatjaf/nak -go 1.23 - -toolchain go1.23.0 +go 1.23.0 require ( github.com/bep/debounce v1.2.1 - github.com/btcsuite/btcd/btcec/v2 v2.3.3 + 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.3.0 github.com/fatih/color v1.16.0 github.com/fiatjaf/cli/v3 v3.0.0-20240723181502-e7dd498b16ae - github.com/fiatjaf/eventstore v0.7.1 + github.com/fiatjaf/eventstore v0.9.0 github.com/fiatjaf/khatru v0.7.5 github.com/mailru/easyjson v0.7.7 github.com/markusmobius/go-dateparser v1.2.3 - github.com/nbd-wtf/go-nostr v0.34.14 - github.com/nbd-wtf/nostr-sdk v0.5.3 - golang.org/x/exp v0.0.0-20240707233637-46b078467d37 + github.com/nbd-wtf/go-nostr v0.36.2 + golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 ) require ( @@ -27,7 +24,7 @@ require ( github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/chzyer/logex v1.1.10 // indirect github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect - github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect + github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/elliotchance/pie/v2 v2.7.0 // indirect github.com/fasthttp/websocket v1.5.7 // indirect @@ -46,18 +43,18 @@ require ( github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/puzpuzpuz/xsync/v3 v3.1.0 // indirect + github.com/puzpuzpuz/xsync/v3 v3.4.0 // indirect github.com/rs/cors v1.7.0 // indirect github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect - github.com/tetratelabs/wazero v1.2.1 // indirect - github.com/tidwall/gjson v1.17.1 // indirect + github.com/tetratelabs/wazero v1.8.0 // indirect + github.com/tidwall/gjson v1.17.3 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.51.0 // indirect github.com/wasilibs/go-re2 v1.3.0 // indirect - golang.org/x/crypto v0.21.0 // indirect + golang.org/x/crypto v0.27.0 // indirect golang.org/x/net v0.22.0 // indirect - golang.org/x/sys v0.22.0 // indirect - golang.org/x/text v0.16.0 // indirect + golang.org/x/sys v0.25.0 // indirect + golang.org/x/text v0.18.0 // indirect ) diff --git a/go.sum b/go.sum index c3151aa..5b9e04d 100644 --- a/go.sum +++ b/go.sum @@ -8,8 +8,8 @@ github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tj github.com/btcsuite/btcd v0.23.0/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY= github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA= github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= -github.com/btcsuite/btcd/btcec/v2 v2.3.3 h1:6+iXlDKE8RMtKsvK0gshlXIuPbyWM/h84Ensb7o3sC0= -github.com/btcsuite/btcd/btcec/v2 v2.3.3/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= +github.com/btcsuite/btcd/btcec/v2 v2.3.4 h1:3EJjcN70HCu/mwqlUsGK8GcNVyLVxFDlWurTXGPFfiQ= +github.com/btcsuite/btcd/btcec/v2 v2.3.4/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A= github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE= github.com/btcsuite/btcd/btcutil v1.1.3 h1:xfbtw8lwpp0G6NwSHb+UE67ryTFHJAiNuipusjXSohQ= @@ -40,8 +40,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= -github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y= -github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= +github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8= +github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= @@ -58,8 +58,8 @@ 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/cli/v3 v3.0.0-20240723181502-e7dd498b16ae h1:0B/1dU3YECIbPoBIRTQ4c0scZCNz9TVHtQpiODGrTTo= github.com/fiatjaf/cli/v3 v3.0.0-20240723181502-e7dd498b16ae/go.mod h1:aAWPO4bixZZxPtOnH6K3q4GbQ0jftUNDW9Oa861IRew= -github.com/fiatjaf/eventstore v0.7.1 h1:5f2yvEtYvsvMBNttysmXhSSum5M1qwvPzjEQ/BFue7Q= -github.com/fiatjaf/eventstore v0.7.1/go.mod h1:ek/yWbanKVG767fK51Q3+6Mvi5oEHYSsdPym40nZexw= +github.com/fiatjaf/eventstore v0.9.0 h1:WsGDVAaRaVaV/J8PdrQDGfzChrL13q+lTO4C44rhu3E= +github.com/fiatjaf/eventstore v0.9.0/go.mod h1:JrAce5h0wi79+Sw4gsEq5kz0NtUxbVkOZ7lAo7ay6R8= github.com/fiatjaf/generic-ristretto v0.0.1 h1:LUJSU87X/QWFsBXTwnH3moFe4N8AjUxT+Rfa0+bo6YM= github.com/fiatjaf/generic-ristretto v0.0.1/go.mod h1:cvV6ANHDA/GrfzVrig7N7i6l8CWnkVZvtQ2/wk9DPVE= github.com/fiatjaf/khatru v0.7.5 h1:UFo+cdbqHDn1W4Q4h03y3vzh1BiU+6fLYK48oWU2K34= @@ -113,10 +113,8 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/nbd-wtf/go-nostr v0.34.14 h1:o4n2LkuAtdIjNYJ23sFbcx68UXLnji4j8hYR1Sd2wgI= -github.com/nbd-wtf/go-nostr v0.34.14/go.mod h1:NZQkxl96ggbO8rvDpVjcsojJqKTPwqhP4i82O7K5DJs= -github.com/nbd-wtf/nostr-sdk v0.5.3 h1:jaiT7xm2h3iksM96PQKlbl5zDpMuZFF3fVj8TknWoJU= -github.com/nbd-wtf/nostr-sdk v0.5.3/go.mod h1:9zlqzVbIczMHeN3fy3Ib2/fStRupTPVLvy54Htd9FCE= +github.com/nbd-wtf/go-nostr v0.36.2 h1:c79JA5FOsNeVFdPUqP9dAA5xRw1qYcwaweKU/U8YyhE= +github.com/nbd-wtf/go-nostr v0.36.2/go.mod h1:TGKGj00BmJRXvRe0LlpDN3KKbELhhPXgBwUEhzu3Oq0= 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= @@ -130,8 +128,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/puzpuzpuz/xsync/v3 v3.1.0 h1:EewKT7/LNac5SLiEblJeUu8z5eERHrmRLnMQL2d7qX4= -github.com/puzpuzpuz/xsync/v3 v3.1.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= +github.com/puzpuzpuz/xsync/v3 v3.4.0 h1:DuVBAdXuGFHv8adVXjWWZ63pJq+NRXOWVXlKDBZ+mJ4= +github.com/puzpuzpuz/xsync/v3 v3.4.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk= @@ -141,10 +139,10 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= -github.com/tetratelabs/wazero v1.2.1 h1:J4X2hrGzJvt+wqltuvcSjHQ7ujQxA9gb6PeMs4qlUWs= -github.com/tetratelabs/wazero v1.2.1/go.mod h1:wYx2gNRg8/WihJfSDxA1TIL8H+GkfLYm+bIfbblu9VQ= -github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U= -github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tetratelabs/wazero v1.8.0 h1:iEKu0d4c2Pd+QSRieYbnQC9yiFlMS9D+Jr0LsRmcF4g= +github.com/tetratelabs/wazero v1.8.0/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs= +github.com/tidwall/gjson v1.17.3 h1:bwWLZU7icoKRG+C+0PNwIKC6FCJO/Q3p2pZvuP0jN94= +github.com/tidwall/gjson v1.17.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= @@ -161,10 +159,10 @@ github.com/wasilibs/nottinygc v0.4.0/go.mod h1:oDcIotskuYNMpqMF23l7Z8uzD4TC0WXHK golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= -golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= -golang.org/x/exp v0.0.0-20240707233637-46b078467d37 h1:uLDX+AfeFCct3a2C7uIWBKMJIR3CJMhcgfrUAqjRK6w= -golang.org/x/exp v0.0.0-20240707233637-46b078467d37/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= +golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= +golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= +golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -184,13 +182,13 @@ golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= -golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/helpers.go b/helpers.go index fea23f7..dfd9d93 100644 --- a/helpers.go +++ b/helpers.go @@ -120,13 +120,12 @@ func connectToAllRelays( relayUrls []string, forcePreAuth bool, opts ...nostr.PoolOption, -) (*nostr.SimplePool, []*nostr.Relay) { +) []*nostr.Relay { relays := make([]*nostr.Relay, 0, len(relayUrls)) - pool := nostr.NewSimplePool(ctx, opts...) relayLoop: for _, url := range relayUrls { log("connecting to %s... ", url) - if relay, err := pool.EnsureRelay(url); err == nil { + if relay, err := sys.Pool.EnsureRelay(url); err == nil { if forcePreAuth { log("waiting for auth challenge... ") signer := opts[0].(nostr.WithAuthHandler) @@ -166,7 +165,7 @@ relayLoop: log(err.Error() + "\n") } } - return pool, relays + return relays } func lineProcessingError(ctx context.Context, msg string, args ...any) context.Context { diff --git a/paginate.go b/paginate.go index f8babd8..9d796f0 100644 --- a/paginate.go +++ b/paginate.go @@ -9,7 +9,10 @@ import ( "github.com/nbd-wtf/go-nostr" ) -func paginateWithPoolAndParams(pool *nostr.SimplePool, interval time.Duration, globalLimit uint64) func(ctx context.Context, urls []string, filters nostr.Filters) chan nostr.IncomingEvent { +func paginateWithParams( + interval time.Duration, + globalLimit uint64, +) func(ctx context.Context, urls []string, filters nostr.Filters) chan nostr.IncomingEvent { return func(ctx context.Context, urls []string, filters nostr.Filters) chan nostr.IncomingEvent { // filters will always be just one filter := filters[0] @@ -39,7 +42,7 @@ func paginateWithPoolAndParams(pool *nostr.SimplePool, interval time.Duration, g time.Sleep(interval) keepGoing := false - for evt := range pool.SubManyEose(ctx, urls, nostr.Filters{filter}) { + for evt := range sys.Pool.SubManyEose(ctx, urls, nostr.Filters{filter}) { if slices.Contains(repeatedCache, evt.ID) { continue } diff --git a/req.go b/req.go index 580ae4e..303367b 100644 --- a/req.go +++ b/req.go @@ -90,12 +90,9 @@ example: ), ArgsUsage: "[relay...]", Action: func(ctx context.Context, c *cli.Command) error { - var pool *nostr.SimplePool - relayUrls := c.Args().Slice() if len(relayUrls) > 0 { - var relays []*nostr.Relay - pool, relays = connectToAllRelays(ctx, relayUrls, c.Bool("force-pre-auth"), nostr.WithAuthHandler(func(evt *nostr.Event) error { + relays := connectToAllRelays(ctx, relayUrls, c.Bool("force-pre-auth"), nostr.WithAuthHandler(func(evt *nostr.Event) error { if !c.Bool("auth") && !c.Bool("force-pre-auth") { return fmt.Errorf("auth not authorized") } @@ -151,11 +148,11 @@ example: } if len(relayUrls) > 0 { - fn := pool.SubManyEose + fn := sys.Pool.SubManyEose if c.Bool("paginate") { - fn = paginateWithPoolAndParams(pool, c.Duration("paginate-interval"), c.Uint("paginate-global-limit")) + fn = paginateWithParams(c.Duration("paginate-interval"), c.Uint("paginate-global-limit")) } else if c.Bool("stream") { - fn = pool.SubMany + fn = sys.Pool.SubMany } for ie := range fn(ctx, relayUrls, nostr.Filters{filter}) { From dae7eba8caeedcbbf71dc044841957e45e7ad82f Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 17 Sep 2024 11:27:59 -0300 Subject: [PATCH 159/401] use keyer.Keyer in most places instead of raw bunkers and plaintext keys, simplifies the code a little at the cost of some abstraction but I think it's strictly good this time. --- common.go | 7 --- event.go | 64 ++++---------------- go.mod | 2 +- go.sum | 4 +- helpers.go | 111 +--------------------------------- helpers_key.go | 160 +++++++++++++++++++++++++++++++++++++++++++++++++ relay.go | 35 ++--------- req.go | 134 +++++++++++++++++------------------------ 8 files changed, 237 insertions(+), 280 deletions(-) delete mode 100644 common.go create mode 100644 helpers_key.go diff --git a/common.go b/common.go deleted file mode 100644 index f4b3335..0000000 --- a/common.go +++ /dev/null @@ -1,7 +0,0 @@ -package main - -import ( - "github.com/nbd-wtf/go-nostr/sdk" -) - -var sys = sdk.NewSystem() diff --git a/event.go b/event.go index d2aa549..2197abc 100644 --- a/event.go +++ b/event.go @@ -37,29 +37,7 @@ example: echo '{"id":"a889df6a387419ff204305f4c2d296ee328c3cd4f8b62f205648a541b4554dfb","pubkey":"c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5","created_at":1698623783,"kind":1,"tags":[],"content":"hello from the nostr army knife","sig":"84876e1ee3e726da84e5d195eb79358b2b3eaa4d9bd38456fde3e8a2af3f1cd4cda23f23fda454869975b3688797d4c66e12f4c51c1b43c6d2997c5e61865661"}' | nak event wss://offchain.pub echo '{"tags": [["t", "spam"]]}' | nak event -c 'this is spam'`, DisableSliceFlagSeparator: true, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "sec", - Usage: "secret key to sign the event, as nsec, ncryptsec or hex", - DefaultText: "the key '1'", - Category: CATEGORY_SIGNER, - }, - &cli.BoolFlag{ - Name: "prompt-sec", - Usage: "prompt the user to paste a hex or nsec with which to sign the event", - Category: CATEGORY_SIGNER, - }, - &cli.StringFlag{ - Name: "connect", - Usage: "sign event using NIP-46, expects a bunker://... URL", - Category: CATEGORY_SIGNER, - }, - &cli.StringFlag{ - Name: "connect-as", - Usage: "private key to when communicating with the bunker given on --connect", - DefaultText: "a random key", - Category: CATEGORY_SIGNER, - }, + Flags: append(defaultKeyFlags, // ~ these args are only for the convoluted musig2 signing process // they will be generally copy-shared-pasted across some manual coordination method between participants &cli.UintFlag{ @@ -151,7 +129,7 @@ example: Value: nostr.Now(), Category: CATEGORY_EVENT_FIELDS, }, - }, + ), ArgsUsage: "[relay...]", Action: func(ctx context.Context, c *cli.Command) error { // try to connect to the relays here @@ -170,10 +148,11 @@ example: } }() - sec, bunker, err := gatherSecretKeyOrBunkerFromArguments(ctx, c) + kr, err := gatherKeyerFromArguments(ctx, c) if err != nil { return err } + sec, _, _ := gatherSecretKeyOrBunkerFromArguments(ctx, c) doAuth := c.Bool("auth") @@ -250,12 +229,7 @@ example: if difficulty := c.Uint("pow"); difficulty > 0 { // before doing pow we need the pubkey - if bunker != nil { - evt.PubKey, err = bunker.GetPublicKey(ctx) - if err != nil { - return fmt.Errorf("can't pow: failed to get public key from bunker: %w", err) - } - } else if numSigners := c.Uint("musig"); numSigners > 1 && sec != "" { + if numSigners := c.Uint("musig"); numSigners > 1 { pubkeys := c.StringSlice("musig-pubkey") if int(numSigners) != len(pubkeys) { return fmt.Errorf("when doing a pow with musig we must know all signer pubkeys upfront") @@ -265,7 +239,7 @@ example: return err } } else { - evt.PubKey, _ = nostr.GetPublicKey(sec) + evt.PubKey = kr.GetPublicKey(ctx) } // try to generate work with this difficulty -- runs forever @@ -276,11 +250,7 @@ example: } if evt.Sig == "" || mustRehashAndResign { - if bunker != nil { - if err := bunker.SignEvent(ctx, &evt); err != nil { - return fmt.Errorf("failed to sign with bunker: %w", err) - } - } else if numSigners := c.Uint("musig"); numSigners > 1 && sec != "" { + if numSigners := c.Uint("musig"); numSigners > 1 && sec != "" { pubkeys := c.StringSlice("musig-pubkey") secNonce := c.String("musig-nonce-secret") pubNonces := c.StringSlice("musig-nonce") @@ -295,7 +265,7 @@ example: // instructions for what to do should have been printed by the performMusig() function return nil } - } else if err := evt.Sign(sec); err != nil { + } else if err := kr.SignEvent(ctx, &evt); err != nil { return fmt.Errorf("error signing with provided key: %w", err) } } @@ -332,21 +302,9 @@ example: // error publishing if strings.HasPrefix(err.Error(), "msg: auth-required:") && (sec != "" || bunker != nil) && doAuth { // if the relay is requesting auth and we can auth, let's do it - var pk string - if bunker != nil { - pk, err = bunker.GetPublicKey(ctx) - if err != nil { - return fmt.Errorf("failed to get public key from bunker: %w", err) - } - } else { - pk, _ = nostr.GetPublicKey(sec) - } - log("performing auth as %s... ", pk) - if err := relay.Auth(ctx, func(evt *nostr.Event) error { - if bunker != nil { - return bunker.SignEvent(ctx, evt) - } - return evt.Sign(sec) + log("performing auth as %s... ", kr.GetPublicKey(ctx)) + if err := relay.Auth(ctx, func(authEvent *nostr.Event) error { + return kr.SignEvent(ctx, authEvent) }); err == nil { // try to publish again, but this time don't try to auth again doAuth = false diff --git a/go.mod b/go.mod index 9d43b24..5ae81bc 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/fiatjaf/khatru v0.7.5 github.com/mailru/easyjson v0.7.7 github.com/markusmobius/go-dateparser v1.2.3 - github.com/nbd-wtf/go-nostr v0.36.2 + github.com/nbd-wtf/go-nostr v0.36.3 golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 ) diff --git a/go.sum b/go.sum index 5b9e04d..c6a7a94 100644 --- a/go.sum +++ b/go.sum @@ -113,8 +113,8 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/nbd-wtf/go-nostr v0.36.2 h1:c79JA5FOsNeVFdPUqP9dAA5xRw1qYcwaweKU/U8YyhE= -github.com/nbd-wtf/go-nostr v0.36.2/go.mod h1:TGKGj00BmJRXvRe0LlpDN3KKbELhhPXgBwUEhzu3Oq0= +github.com/nbd-wtf/go-nostr v0.36.3 h1:50fNFO8vQNMEIZ+6qUq0M5hlqEtA13WrtrKcz10eg9k= +github.com/nbd-wtf/go-nostr v0.36.3/go.mod h1:TGKGj00BmJRXvRe0LlpDN3KKbELhhPXgBwUEhzu3Oq0= 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= diff --git a/helpers.go b/helpers.go index dfd9d93..8df8381 100644 --- a/helpers.go +++ b/helpers.go @@ -3,7 +3,6 @@ package main import ( "bufio" "context" - "encoding/hex" "fmt" "math/rand" "net/url" @@ -11,15 +10,14 @@ import ( "strings" "time" - "github.com/chzyer/readline" "github.com/fatih/color" "github.com/fiatjaf/cli/v3" "github.com/nbd-wtf/go-nostr" - "github.com/nbd-wtf/go-nostr/nip19" - "github.com/nbd-wtf/go-nostr/nip46" - "github.com/nbd-wtf/go-nostr/nip49" + "github.com/nbd-wtf/go-nostr/sdk" ) +var sys = sdk.NewSystem() + const ( LINE_PROCESSING_ERROR = iota ) @@ -179,109 +177,6 @@ func exitIfLineProcessingError(ctx context.Context) { } } -func gatherSecretKeyOrBunkerFromArguments(ctx context.Context, c *cli.Command) (string, *nip46.BunkerClient, error) { - var err error - - if bunkerURL := c.String("connect"); bunkerURL != "" { - clientKey := c.String("connect-as") - if clientKey != "" { - clientKey = strings.Repeat("0", 64-len(clientKey)) + clientKey - } else { - clientKey = nostr.GeneratePrivateKey() - } - bunker, err := nip46.ConnectBunker(ctx, clientKey, bunkerURL, nil, func(s string) { - fmt.Fprintf(color.Error, color.CyanString("[nip46]: open the following URL: %s"), s) - }) - return "", bunker, err - } - - // take private from flags, environment variable or default to 1 - sec := c.String("sec") - if sec == "" { - if key, ok := os.LookupEnv("NOSTR_SECRET_KEY"); ok { - sec = key - } else { - sec = "0000000000000000000000000000000000000000000000000000000000000001" - } - } - - 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") - } - 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) - } - } - - if strings.HasPrefix(sec, "ncryptsec1") { - sec, err = promptDecrypt(sec) - if err != nil { - return "", 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) - } - - if ok := nostr.IsValid32ByteHex(sec); !ok { - return "", nil, fmt.Errorf("invalid secret key") - } - - return sec, nil, nil -} - -func promptDecrypt(ncryptsec string) (string, error) { - for i := 1; i < 4; i++ { - var attemptStr string - if i > 1 { - attemptStr = fmt.Sprintf(" [%d/3]", i) - } - password, err := askPassword("type the password to decrypt your secret key"+attemptStr+": ", nil) - if err != nil { - return "", err - } - sec, err := nip49.Decrypt(ncryptsec, password) - if err != nil { - continue - } - return sec, nil - } - return "", fmt.Errorf("couldn't decrypt private key") -} - -func askPassword(msg string, shouldAskAgain func(answer string) bool) (string, error) { - config := &readline.Config{ - Stdout: color.Error, - Prompt: color.YellowString(msg), - InterruptPrompt: "^C", - DisableAutoSaveHistory: true, - EnableMask: true, - MaskRune: '*', - } - - rl, err := readline.NewEx(config) - if err != nil { - return "", err - } - - for { - answer, err := rl.Readline() - if err != nil { - return "", err - } - answer = strings.TrimSpace(answer) - if shouldAskAgain != nil && shouldAskAgain(answer) { - continue - } - return answer, err - } -} - const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" func randString(n int) string { diff --git a/helpers_key.go b/helpers_key.go new file mode 100644 index 0000000..2afb0f5 --- /dev/null +++ b/helpers_key.go @@ -0,0 +1,160 @@ +package main + +import ( + "context" + "encoding/hex" + "fmt" + "os" + "strings" + + "github.com/chzyer/readline" + "github.com/fatih/color" + "github.com/fiatjaf/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{ + &cli.StringFlag{ + Name: "sec", + Usage: "secret key to sign the event, as nsec, ncryptsec or hex, or a bunker URL", + DefaultText: "the key '1'", + Aliases: []string{"connect"}, + Category: CATEGORY_SIGNER, + }, + &cli.BoolFlag{ + Name: "prompt-sec", + Usage: "prompt the user to paste a hex or nsec with which to sign the event", + Category: CATEGORY_SIGNER, + }, + &cli.StringFlag{ + Name: "connect-as", + Usage: "private key to when communicating with the bunker given on --connect", + DefaultText: "a random key", + Category: CATEGORY_SIGNER, + }, +} + +func gatherKeyerFromArguments(ctx context.Context, c *cli.Command) (keyer.Keyer, error) { + key, bunker, err := gatherSecretKeyOrBunkerFromArguments(ctx, c) + if err != nil { + return nil, err + } + + var kr keyer.Keyer + if bunker != nil { + kr = keyer.NewBunkerSignerFromBunkerClient(bunker) + } else { + kr = keyer.NewPlainKeySigner(key) + } + + return kr, nil +} + +func gatherSecretKeyOrBunkerFromArguments(ctx context.Context, c *cli.Command) (string, *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 + } else { + clientKey = nostr.GeneratePrivateKey() + } + bunker, err := nip46.ConnectBunker(ctx, clientKey, bunkerURL, nil, func(s string) { + fmt.Fprintf(color.Error, color.CyanString("[nip46]: open the following URL: %s"), s) + }) + return "", bunker, err + } + + // take private from flags, environment variable or default to 1 + if sec == "" { + if key, ok := os.LookupEnv("NOSTR_SECRET_KEY"); ok { + sec = key + } else { + sec = "0000000000000000000000000000000000000000000000000000000000000001" + } + } + + 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") + } + 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) + } + } + + if strings.HasPrefix(sec, "ncryptsec1") { + sec, err = promptDecrypt(sec) + if err != nil { + return "", 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) + } + + if ok := nostr.IsValid32ByteHex(sec); !ok { + return "", nil, fmt.Errorf("invalid secret key") + } + + return sec, nil, nil +} + +func promptDecrypt(ncryptsec string) (string, error) { + for i := 1; i < 4; i++ { + var attemptStr string + if i > 1 { + attemptStr = fmt.Sprintf(" [%d/3]", i) + } + password, err := askPassword("type the password to decrypt your secret key"+attemptStr+": ", nil) + if err != nil { + return "", err + } + sec, err := nip49.Decrypt(ncryptsec, password) + if err != nil { + continue + } + return sec, nil + } + return "", fmt.Errorf("couldn't decrypt private key") +} + +func askPassword(msg string, shouldAskAgain func(answer string) bool) (string, error) { + config := &readline.Config{ + Stdout: color.Error, + Prompt: color.YellowString(msg), + InterruptPrompt: "^C", + DisableAutoSaveHistory: true, + EnableMask: true, + MaskRune: '*', + } + + rl, err := readline.NewEx(config) + if err != nil { + return "", err + } + + for { + answer, err := rl.Readline() + if err != nil { + return "", err + } + answer = strings.TrimSpace(answer) + if shouldAskAgain != nil && shouldAskAgain(answer) { + continue + } + return answer, err + } +} diff --git a/relay.go b/relay.go index 066e78c..3e8ce9f 100644 --- a/relay.go +++ b/relay.go @@ -74,26 +74,7 @@ var relay = &cli.Command{ flags[i] = declareFlag(argName) } - flags = append(flags, - &cli.StringFlag{ - Name: "sec", - Usage: "secret key to sign the event, as nsec, ncryptsec or hex", - DefaultText: "the key '1'", - }, - &cli.BoolFlag{ - Name: "prompt-sec", - Usage: "prompt the user to paste a hex or nsec with which to sign the event", - }, - &cli.StringFlag{ - Name: "connect", - Usage: "sign event using NIP-46, expects a bunker://... URL", - }, - &cli.StringFlag{ - Name: "connect-as", - Usage: "private key to when communicating with the bunker given on --connect", - DefaultText: "a random key", - }, - ) + flags = append(flags, defaultKeyFlags...) cmd := &cli.Command{ Name: def.method, @@ -114,7 +95,7 @@ var relay = &cli.Command{ return nil } - sec, bunker, err := gatherSecretKeyOrBunkerFromArguments(ctx, c) + kr, err := gatherKeyerFromArguments(ctx, c) if err != nil { return err } @@ -131,7 +112,7 @@ var relay = &cli.Command{ // Authorization payloadHash := sha256.Sum256(reqj) - authEvent := nostr.Event{ + tokenEvent := nostr.Event{ Kind: 27235, CreatedAt: nostr.Now(), Tags: nostr.Tags{ @@ -140,14 +121,10 @@ var relay = &cli.Command{ {"payload", hex.EncodeToString(payloadHash[:])}, }, } - if bunker != nil { - if err := bunker.SignEvent(ctx, &authEvent); err != nil { - return fmt.Errorf("failed to sign with bunker: %w", err) - } - } else if err := authEvent.Sign(sec); err != nil { - return fmt.Errorf("error signing with provided key: %w", err) + if err := kr.SignEvent(ctx, &tokenEvent); err != nil { + return fmt.Errorf("failed to sign token event: %w", err) } - evtj, _ := json.Marshal(authEvent) + evtj, _ := json.Marshal(tokenEvent) req.Header.Set("Authorization", "Nostr "+base64.StdEncoding.EncodeToString(evtj)) // Content-Type diff --git a/req.go b/req.go index 303367b..49f5d91 100644 --- a/req.go +++ b/req.go @@ -31,93 +31,67 @@ it can also take a filter from stdin, optionally modify it with flags and send i example: echo '{"kinds": [1], "#t": ["test"]}' | nak req -l 5 -k 4549 --tag t=spam wss://nostr-pub.wellorder.net`, DisableSliceFlagSeparator: true, - Flags: append(reqFilterFlags, - &cli.BoolFlag{ - Name: "stream", - Usage: "keep the subscription open, printing all events as they are returned", - DefaultText: "false, will close on EOSE", - }, - &cli.BoolFlag{ - Name: "paginate", - Usage: "make multiple REQs to the relay decreasing the value of 'until' until 'limit' or 'since' conditions are met", - DefaultText: "false", - }, - &cli.DurationFlag{ - Name: "paginate-interval", - Usage: "time between queries when using --paginate", - }, - &cli.UintFlag{ - Name: "paginate-global-limit", - Usage: "global limit at which --paginate should stop", - DefaultText: "uses the value given by --limit/-l or infinite", - }, - &cli.BoolFlag{ - Name: "bare", - Usage: "when printing the filter, print just the filter, not enveloped in a [\"REQ\", ...] array", - }, - &cli.BoolFlag{ - Name: "auth", - Usage: "always perform NIP-42 \"AUTH\" when facing an \"auth-required: \" rejection and try again", - }, - &cli.BoolFlag{ - Name: "force-pre-auth", - Aliases: []string{"fpa"}, - Usage: "after connecting, for a NIP-42 \"AUTH\" message to be received, act on it and only then send the \"REQ\"", - Category: CATEGORY_SIGNER, - }, - &cli.StringFlag{ - Name: "sec", - Usage: "secret key to sign the AUTH challenge, as hex or nsec", - DefaultText: "the key '1'", - Category: CATEGORY_SIGNER, - }, - &cli.BoolFlag{ - Name: "prompt-sec", - Usage: "prompt the user to paste a hex or nsec with which to sign the AUTH challenge", - Category: CATEGORY_SIGNER, - }, - &cli.StringFlag{ - Name: "connect", - Usage: "sign AUTH using NIP-46, expects a bunker://... URL", - Category: CATEGORY_SIGNER, - }, - &cli.StringFlag{ - Name: "connect-as", - Usage: "private key to when communicating with the bunker given on --connect", - DefaultText: "a random key", - Category: CATEGORY_SIGNER, - }, + Flags: append(defaultKeyFlags, + append(reqFilterFlags, + &cli.BoolFlag{ + Name: "stream", + Usage: "keep the subscription open, printing all events as they are returned", + DefaultText: "false, will close on EOSE", + }, + &cli.BoolFlag{ + Name: "paginate", + Usage: "make multiple REQs to the relay decreasing the value of 'until' until 'limit' or 'since' conditions are met", + DefaultText: "false", + }, + &cli.DurationFlag{ + Name: "paginate-interval", + Usage: "time between queries when using --paginate", + }, + &cli.UintFlag{ + Name: "paginate-global-limit", + Usage: "global limit at which --paginate should stop", + DefaultText: "uses the value given by --limit/-l or infinite", + }, + &cli.BoolFlag{ + Name: "bare", + Usage: "when printing the filter, print just the filter, not enveloped in a [\"REQ\", ...] array", + }, + &cli.BoolFlag{ + Name: "auth", + Usage: "always perform NIP-42 \"AUTH\" when facing an \"auth-required: \" rejection and try again", + }, + &cli.BoolFlag{ + Name: "force-pre-auth", + Aliases: []string{"fpa"}, + Usage: "after connecting, for a NIP-42 \"AUTH\" message to be received, act on it and only then send the \"REQ\"", + Category: CATEGORY_SIGNER, + }, + )..., ), ArgsUsage: "[relay...]", Action: func(ctx context.Context, c *cli.Command) error { relayUrls := c.Args().Slice() if len(relayUrls) > 0 { - relays := connectToAllRelays(ctx, relayUrls, c.Bool("force-pre-auth"), nostr.WithAuthHandler(func(evt *nostr.Event) error { - if !c.Bool("auth") && !c.Bool("force-pre-auth") { - return fmt.Errorf("auth not authorized") - } - sec, bunker, err := gatherSecretKeyOrBunkerFromArguments(ctx, c) - if err != nil { - return err - } + relays := connectToAllRelays(ctx, + relayUrls, + c.Bool("force-pre-auth"), + nostr.WithAuthHandler( + func(authEvent *nostr.Event) error { + if !c.Bool("auth") && !c.Bool("force-pre-auth") { + return fmt.Errorf("auth not authorized") + } + kr, err := gatherKeyerFromArguments(ctx, c) + if err != nil { + return err + } - var pk string - if bunker != nil { - pk, err = bunker.GetPublicKey(ctx) - if err != nil { - return fmt.Errorf("failed to get public key from bunker: %w", err) - } - } else { - pk, _ = nostr.GetPublicKey(sec) - } - log("performing auth as %s... ", pk) + pk := kr.GetPublicKey(ctx) + log("performing auth as %s... ", pk) - if bunker != nil { - return bunker.SignEvent(ctx, evt) - } else { - return evt.Sign(sec) - } - })) + return kr.SignEvent(ctx, authEvent) + }, + ), + ) if len(relays) == 0 { log("failed to connect to any of the given relays.\n") os.Exit(3) From a4886dc445a45ee49d644066a5135dea01e967a3 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 17 Sep 2024 11:28:26 -0300 Subject: [PATCH 160/401] nak encrypt and nak decrypt: nip44 with option to do nip04. closes https://github.com/fiatjaf/nak/issues/36 --- encrypt_decrypt.go | 140 +++++++++++++++++++++++++++++++++++++++++++++ key.go | 8 +-- main.go | 2 + 3 files changed, 146 insertions(+), 4 deletions(-) create mode 100644 encrypt_decrypt.go diff --git a/encrypt_decrypt.go b/encrypt_decrypt.go new file mode 100644 index 0000000..8ef39e7 --- /dev/null +++ b/encrypt_decrypt.go @@ -0,0 +1,140 @@ +package main + +import ( + "context" + "fmt" + + "github.com/fiatjaf/cli/v3" + "github.com/nbd-wtf/go-nostr" + "github.com/nbd-wtf/go-nostr/nip04" +) + +var encrypt = &cli.Command{ + Name: "encrypt", + Usage: "encrypts a string with nip44 (or nip04 if specified using a flag) and returns the resulting ciphertext as base64", + ArgsUsage: "[plaintext string]", + DisableSliceFlagSeparator: true, + Flags: append( + defaultKeyFlags, + &cli.StringFlag{ + Name: "recipient-pubkey", + Aliases: []string{"p", "tgt", "target", "pubkey"}, + Required: true, + }, + &cli.BoolFlag{ + Name: "nip04", + Usage: "use nip04 encryption instead of nip44", + }, + ), + 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) + } + + plaintext := c.Args().First() + + if c.Bool("nip04") { + sec, bunker, err := gatherSecretKeyOrBunkerFromArguments(ctx, c) + if err != nil { + return err + } + + if bunker != nil { + ciphertext, err := bunker.NIP04Encrypt(ctx, target, plaintext) + if err != nil { + return err + } + fmt.Println(ciphertext) + } else { + ss, err := nip04.ComputeSharedSecret(target, sec) + if err != nil { + return fmt.Errorf("failed to compute nip04 shared secret: %w", err) + } + ciphertext, err := nip04.Encrypt(plaintext, ss) + if err != nil { + return fmt.Errorf("failed to encrypt as nip04: %w", err) + } + fmt.Println(ciphertext) + } + } else { + kr, err := gatherKeyerFromArguments(ctx, c) + if err != nil { + return err + } + + res, err := kr.Encrypt(ctx, plaintext, target) + if err != nil { + return fmt.Errorf("failed to encrypt: %w", err) + } + fmt.Println(res) + } + + return nil + }, +} + +var decrypt = &cli.Command{ + Name: "decrypt", + Usage: "decrypts a base64 nip44 ciphertext (or nip04 if specified using a flag) and returns the resulting plaintext", + ArgsUsage: "[ciphertext base64]", + DisableSliceFlagSeparator: true, + Flags: append( + defaultKeyFlags, + &cli.StringFlag{ + Name: "sender-pubkey", + Aliases: []string{"p", "src", "source", "pubkey"}, + Required: true, + }, + &cli.BoolFlag{ + Name: "nip04", + Usage: "use nip04 encryption instead of nip44", + }, + ), + 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) + } + + ciphertext := c.Args().First() + + if c.Bool("nip04") { + sec, bunker, err := gatherSecretKeyOrBunkerFromArguments(ctx, c) + if err != nil { + return err + } + + if bunker != nil { + plaintext, err := bunker.NIP04Decrypt(ctx, source, ciphertext) + if err != nil { + return err + } + fmt.Println(plaintext) + } else { + ss, err := nip04.ComputeSharedSecret(source, sec) + if err != nil { + return fmt.Errorf("failed to compute nip04 shared secret: %w", err) + } + plaintext, err := nip04.Decrypt(ciphertext, ss) + if err != nil { + return fmt.Errorf("failed to encrypt as nip04: %w", err) + } + fmt.Println(plaintext) + } + } else { + kr, err := gatherKeyerFromArguments(ctx, c) + if err != nil { + return err + } + + res, err := kr.Decrypt(ctx, ciphertext, source) + if err != nil { + return fmt.Errorf("failed to encrypt: %w", err) + } + fmt.Println(res) + } + + return nil + }, +} diff --git a/key.go b/key.go index d925ba6..826cb7f 100644 --- a/key.go +++ b/key.go @@ -25,8 +25,8 @@ var key = &cli.Command{ Commands: []*cli.Command{ generate, public, - encrypt, - decrypt, + encryptKey, + decryptKey, combine, }, } @@ -62,7 +62,7 @@ var public = &cli.Command{ }, } -var encrypt = &cli.Command{ +var encryptKey = &cli.Command{ Name: "encrypt", Usage: "encrypts a secret key and prints an ncryptsec code", Description: `uses the NIP-49 standard.`, @@ -101,7 +101,7 @@ var encrypt = &cli.Command{ }, } -var decrypt = &cli.Command{ +var decryptKey = &cli.Command{ Name: "decrypt", Usage: "takes an ncrypsec and a password and decrypts it into an nsec", Description: `uses the NIP-49 standard.`, diff --git a/main.go b/main.go index 218e5f2..aae4e37 100644 --- a/main.go +++ b/main.go @@ -28,6 +28,8 @@ var app = &cli.Command{ relay, bunker, serve, + encrypt, + decrypt, }, Version: version, Flags: []cli.Flag{ From 321572641750582d05588d17c42d2408a75a8958 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sat, 21 Sep 2024 12:02:09 -0300 Subject: [PATCH 161/401] use stdout() function instead of fmt.Println() in some places. --- encrypt_decrypt.go | 12 ++++++------ key.go | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/encrypt_decrypt.go b/encrypt_decrypt.go index 8ef39e7..e517fdf 100644 --- a/encrypt_decrypt.go +++ b/encrypt_decrypt.go @@ -45,7 +45,7 @@ var encrypt = &cli.Command{ if err != nil { return err } - fmt.Println(ciphertext) + stdout(ciphertext) } else { ss, err := nip04.ComputeSharedSecret(target, sec) if err != nil { @@ -55,7 +55,7 @@ var encrypt = &cli.Command{ if err != nil { return fmt.Errorf("failed to encrypt as nip04: %w", err) } - fmt.Println(ciphertext) + stdout(ciphertext) } } else { kr, err := gatherKeyerFromArguments(ctx, c) @@ -67,7 +67,7 @@ var encrypt = &cli.Command{ if err != nil { return fmt.Errorf("failed to encrypt: %w", err) } - fmt.Println(res) + stdout(res) } return nil @@ -110,7 +110,7 @@ var decrypt = &cli.Command{ if err != nil { return err } - fmt.Println(plaintext) + stdout(plaintext) } else { ss, err := nip04.ComputeSharedSecret(source, sec) if err != nil { @@ -120,7 +120,7 @@ var decrypt = &cli.Command{ if err != nil { return fmt.Errorf("failed to encrypt as nip04: %w", err) } - fmt.Println(plaintext) + stdout(plaintext) } } else { kr, err := gatherKeyerFromArguments(ctx, c) @@ -132,7 +132,7 @@ var decrypt = &cli.Command{ if err != nil { return fmt.Errorf("failed to encrypt: %w", err) } - fmt.Println(res) + stdout(res) } return nil diff --git a/key.go b/key.go index 826cb7f..df5ef2a 100644 --- a/key.go +++ b/key.go @@ -252,13 +252,13 @@ However, if the intent is to check if two existing Nostr pubkeys match a given c } res, _ := json.MarshalIndent(result, "", " ") - fmt.Println(string(res)) + stdout(string(res)) return nil }, } -func getSecretKeysFromStdinLinesOrSlice(ctx context.Context, c *cli.Command, keys []string) chan string { +func getSecretKeysFromStdinLinesOrSlice(ctx context.Context, _ *cli.Command, keys []string) chan string { ch := make(chan string) go func() { for sec := range getStdinLinesOrArgumentsFromSlice(keys) { From 43fe41df5d4bb672835ac5dc8256902bc18bf8f2 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sun, 22 Sep 2024 19:04:21 -0300 Subject: [PATCH 162/401] use log() function instead of fmt.Fprintf(os.Stderr) in some places. --- bunker.go | 2 +- helpers_key.go | 2 +- key.go | 5 ++--- musig2.go | 9 ++++----- 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/bunker.go b/bunker.go index 4297e02..1d4422d 100644 --- a/bunker.go +++ b/bunker.go @@ -222,7 +222,7 @@ var bunker = &cli.Command{ select { case <-ctx.Done(): case <-time.After(time.Minute * 5): - fmt.Fprintf(os.Stderr, "\n") + log("\n") printBunkerInfo() } }() diff --git a/helpers_key.go b/helpers_key.go index 2afb0f5..977d677 100644 --- a/helpers_key.go +++ b/helpers_key.go @@ -68,7 +68,7 @@ func gatherSecretKeyOrBunkerFromArguments(ctx context.Context, c *cli.Command) ( clientKey = nostr.GeneratePrivateKey() } bunker, err := nip46.ConnectBunker(ctx, clientKey, bunkerURL, nil, func(s string) { - fmt.Fprintf(color.Error, color.CyanString("[nip46]: open the following URL: %s"), s) + log(color.CyanString("[nip46]: open the following URL: %s"), s) }) return "", bunker, err } diff --git a/key.go b/key.go index df5ef2a..670319d 100644 --- a/key.go +++ b/key.go @@ -5,7 +5,6 @@ import ( "encoding/hex" "encoding/json" "fmt" - "os" "strings" "github.com/btcsuite/btcd/btcec/v2" @@ -188,7 +187,7 @@ However, if the intent is to check if two existing Nostr pubkeys match a given c for i, prefix := range []byte{0x02, 0x03} { pubk, err := btcec.ParsePubKey(append([]byte{prefix}, keyb...)) if err != nil { - fmt.Fprintf(os.Stderr, "error parsing key %s: %s", keyhex, err) + log("error parsing key %s: %s", keyhex, err) continue } group[i] = pubk @@ -229,7 +228,7 @@ However, if the intent is to check if two existing Nostr pubkeys match a given c agg, _, _, err := musig2.AggregateKeys(combining, true) if err != nil { - fmt.Fprintf(os.Stderr, "error aggregating: %s", err) + log("error aggregating: %s", err) return } diff --git a/musig2.go b/musig2.go index 7e0a42e..1d87f32 100644 --- a/musig2.go +++ b/musig2.go @@ -6,7 +6,6 @@ import ( "encoding/base64" "encoding/hex" "fmt" - "os" "strconv" "strings" @@ -135,8 +134,8 @@ func performMusig( if err != nil { return false, err } - fmt.Fprintf(os.Stderr, "the following code should be saved secretly until the next step an included with --musig-nonce-secret:\n") - fmt.Fprintf(os.Stderr, "%s\n\n", base64.StdEncoding.EncodeToString(nonce.SecNonce[:])) + log("the following code should be saved secretly until the next step an included with --musig-nonce-secret:\n") + log("%s\n\n", base64.StdEncoding.EncodeToString(nonce.SecNonce[:])) knownNonces = append(knownNonces, nonce.PubNonce) printPublicCommandForNextPeer(evt, numSigners, knownSigners, knownNonces, nil, false) @@ -149,7 +148,7 @@ func performMusig( } else { evt.PubKey = hex.EncodeToString(comb.SerializeCompressed()[1:]) evt.ID = evt.GetID() - fmt.Fprintf(os.Stderr, "combined key: %x\n\n", comb.SerializeCompressed()) + log("combined key: %x\n\n", comb.SerializeCompressed()) } // we have all the signers, which means we must also have all the nonces @@ -244,7 +243,7 @@ func printPublicCommandForNextPeer( maybeNonceSecret = " --musig-nonce-secret ''" } - fmt.Fprintf(os.Stderr, "the next signer and they should call this on their side:\nnak event --sec --musig %d %s%s%s%s%s\n", + log("the next signer and they should call this on their side:\nnak event --sec --musig %d %s%s%s%s%s\n", numSigners, eventToCliArgs(evt), signersToCliArgs(knownSigners), From d7c0ff2bb7256565aec02b6044f00411d8902ebd Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sun, 22 Sep 2024 19:21:41 -0300 Subject: [PATCH 163/401] update go-nostr keyer interface and make req --auth work again. --- event.go | 5 +++-- go.mod | 2 +- go.sum | 4 ++-- helpers.go | 9 ++++++++- helpers_key.go | 4 ++-- paginate.go | 6 +++--- req.go | 6 +++--- 7 files changed, 22 insertions(+), 14 deletions(-) diff --git a/event.go b/event.go index 2197abc..cbc0ec5 100644 --- a/event.go +++ b/event.go @@ -239,7 +239,7 @@ example: return err } } else { - evt.PubKey = kr.GetPublicKey(ctx) + evt.PubKey, _ = kr.GetPublicKey(ctx) } // try to generate work with this difficulty -- runs forever @@ -302,7 +302,8 @@ example: // error publishing if strings.HasPrefix(err.Error(), "msg: auth-required:") && (sec != "" || bunker != nil) && doAuth { // if the relay is requesting auth and we can auth, let's do it - log("performing auth as %s... ", kr.GetPublicKey(ctx)) + pk, _ := kr.GetPublicKey(ctx) + log("performing auth as %s... ", pk) if err := relay.Auth(ctx, func(authEvent *nostr.Event) error { return kr.SignEvent(ctx, authEvent) }); err == nil { diff --git a/go.mod b/go.mod index 5ae81bc..384eea4 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/fiatjaf/khatru v0.7.5 github.com/mailru/easyjson v0.7.7 github.com/markusmobius/go-dateparser v1.2.3 - github.com/nbd-wtf/go-nostr v0.36.3 + github.com/nbd-wtf/go-nostr v0.37.2 golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 ) diff --git a/go.sum b/go.sum index c6a7a94..b3c3565 100644 --- a/go.sum +++ b/go.sum @@ -113,8 +113,8 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/nbd-wtf/go-nostr v0.36.3 h1:50fNFO8vQNMEIZ+6qUq0M5hlqEtA13WrtrKcz10eg9k= -github.com/nbd-wtf/go-nostr v0.36.3/go.mod h1:TGKGj00BmJRXvRe0LlpDN3KKbELhhPXgBwUEhzu3Oq0= +github.com/nbd-wtf/go-nostr v0.37.2 h1:42rriFqqz07EdydERwYeQnewl+Rah1Gq46I+Wh0KYYg= +github.com/nbd-wtf/go-nostr v0.37.2/go.mod h1:TGKGj00BmJRXvRe0LlpDN3KKbELhhPXgBwUEhzu3Oq0= 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= diff --git a/helpers.go b/helpers.go index 8df8381..d8eb013 100644 --- a/helpers.go +++ b/helpers.go @@ -119,6 +119,13 @@ func connectToAllRelays( forcePreAuth bool, opts ...nostr.PoolOption, ) []*nostr.Relay { + sys.Pool = nostr.NewSimplePool(context.Background(), + append(opts, + nostr.WithEventMiddleware(sys.TrackEventHints), + nostr.WithPenaltyBox(), + )..., + ) + relays := make([]*nostr.Relay, 0, len(relayUrls)) relayLoop: for _, url := range relayUrls { @@ -137,7 +144,7 @@ relayLoop: if (*challengeTag)[1] == "" { return fmt.Errorf("auth not received yet *****") } - return signer(authEvent) + return signer(ctx, nostr.RelayEvent{Event: authEvent, Relay: relay}) }); err == nil { // auth succeeded break challengeWaitLoop diff --git a/helpers_key.go b/helpers_key.go index 977d677..62a6f80 100644 --- a/helpers_key.go +++ b/helpers_key.go @@ -48,10 +48,10 @@ func gatherKeyerFromArguments(ctx context.Context, c *cli.Command) (keyer.Keyer, if bunker != nil { kr = keyer.NewBunkerSignerFromBunkerClient(bunker) } else { - kr = keyer.NewPlainKeySigner(key) + kr, err = keyer.NewPlainKeySigner(key) } - return kr, nil + return kr, err } func gatherSecretKeyOrBunkerFromArguments(ctx context.Context, c *cli.Command) (string, *nip46.BunkerClient, error) { diff --git a/paginate.go b/paginate.go index 9d796f0..77ee9a7 100644 --- a/paginate.go +++ b/paginate.go @@ -12,8 +12,8 @@ import ( func paginateWithParams( interval time.Duration, globalLimit uint64, -) func(ctx context.Context, urls []string, filters nostr.Filters) chan nostr.IncomingEvent { - return func(ctx context.Context, urls []string, filters nostr.Filters) chan nostr.IncomingEvent { +) func(ctx context.Context, urls []string, filters nostr.Filters) chan nostr.RelayEvent { + return func(ctx context.Context, urls []string, filters nostr.Filters) chan nostr.RelayEvent { // filters will always be just one filter := filters[0] @@ -29,7 +29,7 @@ func paginateWithParams( } } var globalCount uint64 = 0 - globalCh := make(chan nostr.IncomingEvent) + globalCh := make(chan nostr.RelayEvent) repeatedCache := make([]string, 0, 300) nextRepeatedCache := make([]string, 0, 300) diff --git a/req.go b/req.go index 49f5d91..99dd1fb 100644 --- a/req.go +++ b/req.go @@ -76,7 +76,7 @@ example: relayUrls, c.Bool("force-pre-auth"), nostr.WithAuthHandler( - func(authEvent *nostr.Event) error { + func(ctx context.Context, authEvent nostr.RelayEvent) error { if !c.Bool("auth") && !c.Bool("force-pre-auth") { return fmt.Errorf("auth not authorized") } @@ -85,10 +85,10 @@ example: return err } - pk := kr.GetPublicKey(ctx) + pk, _ := kr.GetPublicKey(ctx) log("performing auth as %s... ", pk) - return kr.SignEvent(ctx, authEvent) + return kr.SignEvent(ctx, authEvent.Event) }, ), ) From 2988c71ccba5a5a7028e36cd5d6032d95b0dfb53 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Thu, 26 Sep 2024 22:17:31 -0300 Subject: [PATCH 164/401] nak/b and nak/s user-agents. --- go.mod | 2 +- go.sum | 4 ++-- helpers.go | 7 +++++++ paginate.go | 6 +++--- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 384eea4..2ecd18e 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/fiatjaf/khatru v0.7.5 github.com/mailru/easyjson v0.7.7 github.com/markusmobius/go-dateparser v1.2.3 - github.com/nbd-wtf/go-nostr v0.37.2 + github.com/nbd-wtf/go-nostr v0.37.5 golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 ) diff --git a/go.sum b/go.sum index b3c3565..f64d5e8 100644 --- a/go.sum +++ b/go.sum @@ -113,8 +113,8 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/nbd-wtf/go-nostr v0.37.2 h1:42rriFqqz07EdydERwYeQnewl+Rah1Gq46I+Wh0KYYg= -github.com/nbd-wtf/go-nostr v0.37.2/go.mod h1:TGKGj00BmJRXvRe0LlpDN3KKbELhhPXgBwUEhzu3Oq0= +github.com/nbd-wtf/go-nostr v0.37.5 h1:w/8aBgSf3lC2OoqAJXnYUO0Nxqv+YAdxDC8X3FbLYS8= +github.com/nbd-wtf/go-nostr v0.37.5/go.mod h1:TGKGj00BmJRXvRe0LlpDN3KKbELhhPXgBwUEhzu3Oq0= 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= diff --git a/helpers.go b/helpers.go index d8eb013..f84967f 100644 --- a/helpers.go +++ b/helpers.go @@ -18,6 +18,12 @@ import ( var sys = sdk.NewSystem() +func init() { + sys.Pool = nostr.NewSimplePool(context.Background(), + nostr.WithUserAgent("nak/b"), + ) +} + const ( LINE_PROCESSING_ERROR = iota ) @@ -123,6 +129,7 @@ func connectToAllRelays( append(opts, nostr.WithEventMiddleware(sys.TrackEventHints), nostr.WithPenaltyBox(), + nostr.WithUserAgent("nak/s"), )..., ) diff --git a/paginate.go b/paginate.go index 77ee9a7..0f871e3 100644 --- a/paginate.go +++ b/paginate.go @@ -12,8 +12,8 @@ import ( func paginateWithParams( interval time.Duration, globalLimit uint64, -) func(ctx context.Context, urls []string, filters nostr.Filters) chan nostr.RelayEvent { - return func(ctx context.Context, urls []string, filters nostr.Filters) chan nostr.RelayEvent { +) func(ctx context.Context, urls []string, filters nostr.Filters, opts ...nostr.SubscriptionOption) chan nostr.RelayEvent { + return func(ctx context.Context, urls []string, filters nostr.Filters, opts ...nostr.SubscriptionOption) chan nostr.RelayEvent { // filters will always be just one filter := filters[0] @@ -42,7 +42,7 @@ func paginateWithParams( time.Sleep(interval) keepGoing := false - for evt := range sys.Pool.SubManyEose(ctx, urls, nostr.Filters{filter}) { + for evt := range sys.Pool.SubManyEose(ctx, urls, nostr.Filters{filter}, opts...) { if slices.Contains(repeatedCache, evt.ID) { continue } From 5b04bc485900dc196af1d123f00b4e037297d28a Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 8 Oct 2024 09:08:50 -0300 Subject: [PATCH 165/401] nak key public --with-parity --- key.go | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/key.go b/key.go index 670319d..7d399b4 100644 --- a/key.go +++ b/key.go @@ -48,14 +48,22 @@ var public = &cli.Command{ Description: ``, ArgsUsage: "[secret]", DisableSliceFlagSeparator: true, + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "with-parity", + Usage: "output 33 bytes instead of 32, the first one being either '02' or '03', a prefix indicating whether this pubkey is even or odd.", + }, + }, Action: func(ctx context.Context, c *cli.Command) error { for sec := range getSecretKeysFromStdinLinesOrSlice(ctx, c, c.Args().Slice()) { - pubkey, err := nostr.GetPublicKey(sec) - if err != nil { - ctx = lineProcessingError(ctx, "failed to derive public key: %s", err) - continue + b, _ := hex.DecodeString(sec) + _, pk := btcec.PrivKeyFromBytes(b) + + if c.Bool("with-parity") { + stdout(hex.EncodeToString(pk.SerializeCompressed())) + } else { + stdout(hex.EncodeToString(pk.SerializeCompressed()[1:])) } - stdout(pubkey) } return nil }, From 38ed370c5925f0315683353c143b2615772017ad Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sun, 29 Sep 2024 10:30:31 -0300 Subject: [PATCH 166/401] slightly improve verify error message. --- verify.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/verify.go b/verify.go index e71fc47..69305e6 100644 --- a/verify.go +++ b/verify.go @@ -32,7 +32,7 @@ it outputs nothing if the verification is successful.`, } if ok, err := evt.CheckSignature(); !ok { - ctx = lineProcessingError(ctx, "invalid signature: %s", err) + ctx = lineProcessingError(ctx, "invalid signature: %v", err) continue } } From ea53eca74f46b2087f39928c1b66facf77140090 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sun, 27 Oct 2024 09:56:49 -0300 Subject: [PATCH 167/401] update go-nostr for nip44-on-nip46 fixes. --- bunker.go | 2 +- go.mod | 15 +++++++-------- go.sum | 30 ++++++++++++++---------------- 3 files changed, 22 insertions(+), 25 deletions(-) diff --git a/bunker.go b/bunker.go index 1d4422d..a74c8da 100644 --- a/bunker.go +++ b/bunker.go @@ -183,7 +183,7 @@ var bunker = &cli.Command{ cancelPreviousBunkerInfoPrint() // this prevents us from printing a million bunker info blocks // handle the NIP-46 request event - req, resp, eventResponse, err := signer.HandleRequest(ie.Event) + req, resp, eventResponse, err := signer.HandleRequest(ctx, ie.Event) if err != nil { log("< failed to handle request from %s: %s\n", ie.Event.PubKey, err.Error()) continue diff --git a/go.mod b/go.mod index 2ecd18e..e5f2800 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/fiatjaf/nak -go 1.23.0 +go 1.23.1 require ( github.com/bep/debounce v1.2.1 @@ -13,7 +13,7 @@ require ( github.com/fiatjaf/khatru v0.7.5 github.com/mailru/easyjson v0.7.7 github.com/markusmobius/go-dateparser v1.2.3 - github.com/nbd-wtf/go-nostr v0.37.5 + github.com/nbd-wtf/go-nostr v0.40.1 golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 ) @@ -21,18 +21,17 @@ require ( github.com/andybalholm/brotli v1.0.5 // indirect github.com/btcsuite/btcd/btcutil v1.1.3 // indirect github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect - github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chzyer/logex v1.1.10 // indirect github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect + github.com/dgraph-io/ristretto v1.0.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/elliotchance/pie/v2 v2.7.0 // indirect github.com/fasthttp/websocket v1.5.7 // indirect - github.com/fiatjaf/generic-ristretto v0.0.1 // indirect github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/pool v0.2.1 // indirect github.com/gobwas/ws v1.4.0 // indirect - github.com/golang/glog v1.1.2 // indirect github.com/graph-gophers/dataloader/v7 v7.1.0 // indirect github.com/hablullah/go-hijri v1.0.2 // indirect github.com/hablullah/go-juliandays v1.0.0 // indirect @@ -53,8 +52,8 @@ require ( github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.51.0 // indirect github.com/wasilibs/go-re2 v1.3.0 // indirect - golang.org/x/crypto v0.27.0 // indirect + golang.org/x/crypto v0.28.0 // indirect golang.org/x/net v0.22.0 // indirect - golang.org/x/sys v0.25.0 // indirect - golang.org/x/text v0.18.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/text v0.19.0 // indirect ) diff --git a/go.sum b/go.sum index f64d5e8..6de83f3 100644 --- a/go.sum +++ b/go.sum @@ -27,8 +27,8 @@ github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +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= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= @@ -46,8 +46,10 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeC github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= -github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= -github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dgraph-io/ristretto v1.0.0 h1:SYG07bONKMlFDUYu5pEu3DGAh8c2OFNzKm6G9J4Si84= +github.com/dgraph-io/ristretto v1.0.0/go.mod h1:jTi2FiYEhQ1NsMmA7DeBykizjOuY88NhKBkepyu1jPc= +github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y= +github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/elliotchance/pie/v2 v2.7.0 h1:FqoIKg4uj0G/CrLGuMS9ejnFKa92lxE1dEgBD3pShXg= @@ -60,8 +62,6 @@ github.com/fiatjaf/cli/v3 v3.0.0-20240723181502-e7dd498b16ae h1:0B/1dU3YECIbPoBI github.com/fiatjaf/cli/v3 v3.0.0-20240723181502-e7dd498b16ae/go.mod h1:aAWPO4bixZZxPtOnH6K3q4GbQ0jftUNDW9Oa861IRew= github.com/fiatjaf/eventstore v0.9.0 h1:WsGDVAaRaVaV/J8PdrQDGfzChrL13q+lTO4C44rhu3E= github.com/fiatjaf/eventstore v0.9.0/go.mod h1:JrAce5h0wi79+Sw4gsEq5kz0NtUxbVkOZ7lAo7ay6R8= -github.com/fiatjaf/generic-ristretto v0.0.1 h1:LUJSU87X/QWFsBXTwnH3moFe4N8AjUxT+Rfa0+bo6YM= -github.com/fiatjaf/generic-ristretto v0.0.1/go.mod h1:cvV6ANHDA/GrfzVrig7N7i6l8CWnkVZvtQ2/wk9DPVE= github.com/fiatjaf/khatru v0.7.5 h1:UFo+cdbqHDn1W4Q4h03y3vzh1BiU+6fLYK48oWU2K34= github.com/fiatjaf/khatru v0.7.5/go.mod h1:WVqij7X9Vr9UAMIwafQbKVFKxc42Np37vyficwUr/nQ= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= @@ -72,8 +72,6 @@ github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs= github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc= -github.com/golang/glog v1.1.2 h1:DVjP2PbBOzHyzA+dn3WhHIq4NdVu3Q+pvivFICf/7fo= -github.com/golang/glog v1.1.2/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= @@ -113,8 +111,8 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/nbd-wtf/go-nostr v0.37.5 h1:w/8aBgSf3lC2OoqAJXnYUO0Nxqv+YAdxDC8X3FbLYS8= -github.com/nbd-wtf/go-nostr v0.37.5/go.mod h1:TGKGj00BmJRXvRe0LlpDN3KKbELhhPXgBwUEhzu3Oq0= +github.com/nbd-wtf/go-nostr v0.40.1 h1:+ogxn+CeRwjQSMSU161fOxKWtVWTEz/p++X4O8VKhMw= +github.com/nbd-wtf/go-nostr v0.40.1/go.mod h1:FBa4FBJO7NuANvkeKSlrf0BIyxGufmrUbuelr6Q4Ick= 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= @@ -159,8 +157,8 @@ github.com/wasilibs/nottinygc v0.4.0/go.mod h1:oDcIotskuYNMpqMF23l7Z8uzD4TC0WXHK golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= -golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -182,13 +180,13 @@ golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= -golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= -golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= From 464766a8365dc7f10e152cb5ace46f2a50378e1e Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sun, 27 Oct 2024 11:01:29 -0300 Subject: [PATCH 168/401] allow "=" in tag value. fixes https://github.com/fiatjaf/nak/issues/40 --- req.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/req.go b/req.go index 99dd1fb..62f666f 100644 --- a/req.go +++ b/req.go @@ -231,7 +231,7 @@ func applyFlagsToFilter(c *cli.Command, filter *nostr.Filter) error { } tags := make([][]string, 0, 5) for _, tagFlag := range c.StringSlice("tag") { - spl := strings.Split(tagFlag, "=") + spl := strings.SplitN(tagFlag, "=", 2) if len(spl) == 2 && len(spl[0]) == 1 { tags = append(tags, spl) } else { From 134d1225d66666bc5ce83c3a113374c0b062a19f Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 29 Oct 2024 13:33:24 -0300 Subject: [PATCH 169/401] nak event: presence of key flags indicates the need to resign a given event. fixes https://github.com/fiatjaf/nak/issues/41 --- event.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/event.go b/event.go index cbc0ec5..909803f 100644 --- a/event.go +++ b/event.go @@ -227,6 +227,10 @@ example: mustRehashAndResign = true } + if c.IsSet("musig") || c.IsSet("sec") || c.IsSet("prompt-sec") { + mustRehashAndResign = true + } + if difficulty := c.Uint("pow"); difficulty > 0 { // before doing pow we need the pubkey if numSigners := c.Uint("musig"); numSigners > 1 { From 847f8aaa695b27e134c5c9d61676294e7d42ac61 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 29 Oct 2024 21:11:15 -0300 Subject: [PATCH 170/401] remove duplicated password decryption prompts by returning the bare key together with the Keyer when it is given. --- encrypt_decrypt.go | 4 ++-- event.go | 8 ++++---- helpers_key.go | 8 ++++---- relay.go | 2 +- req.go | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/encrypt_decrypt.go b/encrypt_decrypt.go index e517fdf..b4cc5c5 100644 --- a/encrypt_decrypt.go +++ b/encrypt_decrypt.go @@ -58,7 +58,7 @@ var encrypt = &cli.Command{ stdout(ciphertext) } } else { - kr, err := gatherKeyerFromArguments(ctx, c) + kr, _, err := gatherKeyerFromArguments(ctx, c) if err != nil { return err } @@ -123,7 +123,7 @@ var decrypt = &cli.Command{ stdout(plaintext) } } else { - kr, err := gatherKeyerFromArguments(ctx, c) + kr, _, err := gatherKeyerFromArguments(ctx, c) if err != nil { return err } diff --git a/event.go b/event.go index 909803f..6a93c7a 100644 --- a/event.go +++ b/event.go @@ -148,11 +148,10 @@ example: } }() - kr, err := gatherKeyerFromArguments(ctx, c) + kr, sec, err := gatherKeyerFromArguments(ctx, c) if err != nil { return err } - sec, _, _ := gatherSecretKeyOrBunkerFromArguments(ctx, c) doAuth := c.Bool("auth") @@ -254,7 +253,8 @@ example: } if evt.Sig == "" || mustRehashAndResign { - if numSigners := c.Uint("musig"); numSigners > 1 && sec != "" { + if numSigners := c.Uint("musig"); numSigners > 1 { + // must do musig pubkeys := c.StringSlice("musig-pubkey") secNonce := c.String("musig-nonce-secret") pubNonces := c.StringSlice("musig-nonce") @@ -304,7 +304,7 @@ example: } // error publishing - if strings.HasPrefix(err.Error(), "msg: auth-required:") && (sec != "" || bunker != 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 pk, _ := kr.GetPublicKey(ctx) log("performing auth as %s... ", pk) diff --git a/helpers_key.go b/helpers_key.go index 62a6f80..19f35c3 100644 --- a/helpers_key.go +++ b/helpers_key.go @@ -38,20 +38,20 @@ var defaultKeyFlags = []cli.Flag{ }, } -func gatherKeyerFromArguments(ctx context.Context, c *cli.Command) (keyer.Keyer, error) { +func gatherKeyerFromArguments(ctx context.Context, c *cli.Command) (nostr.Keyer, string, error) { key, bunker, err := gatherSecretKeyOrBunkerFromArguments(ctx, c) if err != nil { - return nil, err + return nil, "", err } - var kr keyer.Keyer + var kr nostr.Keyer if bunker != nil { kr = keyer.NewBunkerSignerFromBunkerClient(bunker) } else { kr, err = keyer.NewPlainKeySigner(key) } - return kr, err + return kr, key, err } func gatherSecretKeyOrBunkerFromArguments(ctx context.Context, c *cli.Command) (string, *nip46.BunkerClient, error) { diff --git a/relay.go b/relay.go index 3e8ce9f..381bf65 100644 --- a/relay.go +++ b/relay.go @@ -95,7 +95,7 @@ var relay = &cli.Command{ return nil } - kr, err := gatherKeyerFromArguments(ctx, c) + kr, _, err := gatherKeyerFromArguments(ctx, c) if err != nil { return err } diff --git a/req.go b/req.go index 62f666f..f4a143a 100644 --- a/req.go +++ b/req.go @@ -80,7 +80,7 @@ example: if !c.Bool("auth") && !c.Bool("force-pre-auth") { return fmt.Errorf("auth not authorized") } - kr, err := gatherKeyerFromArguments(ctx, c) + kr, _, err := gatherKeyerFromArguments(ctx, c) if err != nil { return err } From 40892c1228344a275a7f645a457e3152087806e5 Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Wed, 30 Oct 2024 10:41:22 +0900 Subject: [PATCH 171/401] build arm binary for Raspberry Pi 32bit --- .github/workflows/release-cli.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml index d794d94..87a941c 100644 --- a/.github/workflows/release-cli.yml +++ b/.github/workflows/release-cli.yml @@ -25,7 +25,7 @@ jobs: strategy: matrix: goos: [linux, freebsd, darwin, windows] - goarch: [amd64, arm64, riscv64] + goarch: [arm, amd64, arm64, riscv64] exclude: - goarch: arm64 goos: windows @@ -33,6 +33,12 @@ jobs: goos: windows - goarch: riscv64 goos: darwin + - goarch: arm + goos: windows + - goarch: arm + goos: darwin + - goarch: arm + goos: freebsd steps: - uses: actions/checkout@v3 - uses: wangyoucao577/go-release-action@v1.40 From 71b106fd459549446bed9903bccd2e517689d1d7 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Wed, 30 Oct 2024 10:38:59 -0300 Subject: [PATCH 172/401] update go-nostr so we always encrypt nip46 messages with nip44. --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index e5f2800..e3a5e04 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/fiatjaf/khatru v0.7.5 github.com/mailru/easyjson v0.7.7 github.com/markusmobius/go-dateparser v1.2.3 - github.com/nbd-wtf/go-nostr v0.40.1 + github.com/nbd-wtf/go-nostr v0.41.0 golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 ) diff --git a/go.sum b/go.sum index 6de83f3..cb1a3ef 100644 --- a/go.sum +++ b/go.sum @@ -111,8 +111,8 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/nbd-wtf/go-nostr v0.40.1 h1:+ogxn+CeRwjQSMSU161fOxKWtVWTEz/p++X4O8VKhMw= -github.com/nbd-wtf/go-nostr v0.40.1/go.mod h1:FBa4FBJO7NuANvkeKSlrf0BIyxGufmrUbuelr6Q4Ick= +github.com/nbd-wtf/go-nostr v0.41.0 h1:NNvFO3zEIgA8EQMIhIMtUUTNSce1eVCXbzydpCw8qpM= +github.com/nbd-wtf/go-nostr v0.41.0/go.mod h1:FBa4FBJO7NuANvkeKSlrf0BIyxGufmrUbuelr6Q4Ick= 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= From 4c6181d6493f67574828c36e99002dd940e73b7d Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Fri, 1 Nov 2024 08:44:15 -0300 Subject: [PATCH 173/401] update go-nostr so all bunkers are nip44 maximalists. --- go.mod | 10 ++++++---- go.sum | 27 +++++++++++++++++++-------- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index e3a5e04..9d4f2bb 100644 --- a/go.mod +++ b/go.mod @@ -9,8 +9,8 @@ require ( github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 github.com/fatih/color v1.16.0 github.com/fiatjaf/cli/v3 v3.0.0-20240723181502-e7dd498b16ae - github.com/fiatjaf/eventstore v0.9.0 - github.com/fiatjaf/khatru v0.7.5 + github.com/fiatjaf/eventstore v0.12.0 + github.com/fiatjaf/khatru v0.10.0 github.com/mailru/easyjson v0.7.7 github.com/markusmobius/go-dateparser v1.2.3 github.com/nbd-wtf/go-nostr v0.41.0 @@ -21,6 +21,7 @@ require ( github.com/andybalholm/brotli v1.0.5 // indirect github.com/btcsuite/btcd/btcutil v1.1.3 // indirect github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect + github.com/cespare/xxhash v1.1.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chzyer/logex v1.1.10 // indirect github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect @@ -33,11 +34,12 @@ require ( github.com/gobwas/pool v0.2.1 // indirect github.com/gobwas/ws v1.4.0 // indirect github.com/graph-gophers/dataloader/v7 v7.1.0 // indirect + github.com/greatroar/blobloom v0.8.0 // indirect github.com/hablullah/go-hijri v1.0.2 // indirect github.com/hablullah/go-juliandays v1.0.0 // indirect github.com/jalaali/go-jalaali v0.0.0-20210801064154-80525e88d958 // indirect github.com/josharian/intern v1.0.0 // indirect - github.com/klauspost/compress v1.17.8 // indirect + github.com/klauspost/compress v1.17.10 // indirect github.com/magefile/mage v1.14.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect @@ -53,7 +55,7 @@ require ( github.com/valyala/fasthttp v1.51.0 // indirect github.com/wasilibs/go-re2 v1.3.0 // indirect golang.org/x/crypto v0.28.0 // indirect - golang.org/x/net v0.22.0 // indirect + golang.org/x/net v0.30.0 // indirect golang.org/x/sys v0.26.0 // indirect golang.org/x/text v0.19.0 // indirect ) diff --git a/go.sum b/go.sum index cb1a3ef..7b9454a 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= @@ -27,6 +29,8 @@ github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= +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= @@ -60,10 +64,10 @@ 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/cli/v3 v3.0.0-20240723181502-e7dd498b16ae h1:0B/1dU3YECIbPoBIRTQ4c0scZCNz9TVHtQpiODGrTTo= github.com/fiatjaf/cli/v3 v3.0.0-20240723181502-e7dd498b16ae/go.mod h1:aAWPO4bixZZxPtOnH6K3q4GbQ0jftUNDW9Oa861IRew= -github.com/fiatjaf/eventstore v0.9.0 h1:WsGDVAaRaVaV/J8PdrQDGfzChrL13q+lTO4C44rhu3E= -github.com/fiatjaf/eventstore v0.9.0/go.mod h1:JrAce5h0wi79+Sw4gsEq5kz0NtUxbVkOZ7lAo7ay6R8= -github.com/fiatjaf/khatru v0.7.5 h1:UFo+cdbqHDn1W4Q4h03y3vzh1BiU+6fLYK48oWU2K34= -github.com/fiatjaf/khatru v0.7.5/go.mod h1:WVqij7X9Vr9UAMIwafQbKVFKxc42Np37vyficwUr/nQ= +github.com/fiatjaf/eventstore v0.12.0 h1:ZdL+dZkIgBgIp5A3+3XLdPg/uucv5Tiws6DHzNfZG4M= +github.com/fiatjaf/eventstore v0.12.0/go.mod h1:PxeYbZ3MsH0XLobANsp6c0cJjJYkfmBJ3TwrplFy/08= +github.com/fiatjaf/khatru v0.10.0 h1:f43om33RZfkIAIW9vhHelFJXp8XCij/Jh30AmJ0AVF8= +github.com/fiatjaf/khatru v0.10.0/go.mod h1:iCLz0bPcFSBHJrY2kZ1lji5IrIsv9YFwRS7aaXIPv+o= 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/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= @@ -85,6 +89,8 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/graph-gophers/dataloader/v7 v7.1.0 h1:Wn8HGF/q7MNXcvfaBnLEPEFJttVHR8zuEqP1obys/oc= github.com/graph-gophers/dataloader/v7 v7.1.0/go.mod h1:1bKE0Dm6OUcTB/OAuYVOZctgIz7Q3d0XrYtlIzTgg6Q= +github.com/greatroar/blobloom v0.8.0 h1:I9RlEkfqK9/6f1v9mFmDYegDQ/x0mISCpiNpAm23Pt4= +github.com/greatroar/blobloom v0.8.0/go.mod h1:mjMJ1hh1wjGVfr93QIHJ6FfDNVrA0IELv8OvMHJxHKs= github.com/hablullah/go-hijri v1.0.2 h1:drT/MZpSZJQXo7jftf5fthArShcaMtsal0Zf/dnmp6k= github.com/hablullah/go-hijri v1.0.2/go.mod h1:OS5qyYLDjORXzK4O1adFw9Q5WfhOcMdAKglDkcTxgWQ= github.com/hablullah/go-juliandays v1.0.0 h1:A8YM7wIj16SzlKT0SRJc9CD29iiaUzpBLzh5hr0/5p0= @@ -98,8 +104,8 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= -github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU= -github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/klauspost/compress v1.17.10 h1:oXAz+Vh0PMUvJczoi+flxpnBEPxoER1IaAnU/NMPtT0= +github.com/klauspost/compress v1.17.10/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo= github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= @@ -132,8 +138,13 @@ github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk= github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= @@ -166,8 +177,8 @@ golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= -golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 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= From 9a9e96a829e8dd5ed6d986cd2f1c62dc126ba9b9 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Mon, 11 Nov 2024 22:33:17 -0300 Subject: [PATCH 174/401] support $NOSTR_CLIENT_KEY environment variable for --connect-as --- helpers_key.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/helpers_key.go b/helpers_key.go index 19f35c3..36b929f 100644 --- a/helpers_key.go +++ b/helpers_key.go @@ -32,9 +32,10 @@ var defaultKeyFlags = []cli.Flag{ }, &cli.StringFlag{ Name: "connect-as", - Usage: "private key to when communicating with the bunker given on --connect", + Usage: "private key to use when communicating with NIP-46 bunkers", DefaultText: "a random key", Category: CATEGORY_SIGNER, + Sources: cli.EnvVars("NOSTR_CLIENT_KEY"), }, } From a187e448f27c9a3e0392b430e748d364889e87a3 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Mon, 11 Nov 2024 23:09:15 -0300 Subject: [PATCH 175/401] get rid of some of the HTML escaping that plagues golang json. --- event.go | 3 +-- go.mod | 2 +- go.sum | 4 ++-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/event.go b/event.go index 6a93c7a..26c1250 100644 --- a/event.go +++ b/event.go @@ -2,7 +2,6 @@ package main import ( "context" - "encoding/json" "fmt" "os" "strings" @@ -277,7 +276,7 @@ example: // print event as json var result string if c.Bool("envelope") { - j, _ := json.Marshal(nostr.EventEnvelope{Event: evt}) + j, _ := easyjson.Marshal(nostr.EventEnvelope{Event: evt}) result = string(j) } else { j, _ := easyjson.Marshal(&evt) diff --git a/go.mod b/go.mod index 9d4f2bb..8738504 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/fiatjaf/khatru v0.10.0 github.com/mailru/easyjson v0.7.7 github.com/markusmobius/go-dateparser v1.2.3 - github.com/nbd-wtf/go-nostr v0.41.0 + github.com/nbd-wtf/go-nostr v0.42.1 golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 ) diff --git a/go.sum b/go.sum index 7b9454a..50cd0d0 100644 --- a/go.sum +++ b/go.sum @@ -117,8 +117,8 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/nbd-wtf/go-nostr v0.41.0 h1:NNvFO3zEIgA8EQMIhIMtUUTNSce1eVCXbzydpCw8qpM= -github.com/nbd-wtf/go-nostr v0.41.0/go.mod h1:FBa4FBJO7NuANvkeKSlrf0BIyxGufmrUbuelr6Q4Ick= +github.com/nbd-wtf/go-nostr v0.42.1 h1:zQksbvj+EsLgZBuKugvMzMpzXZuIp1WSx3BvgZ5ZY6A= +github.com/nbd-wtf/go-nostr v0.42.1/go.mod h1:FBa4FBJO7NuANvkeKSlrf0BIyxGufmrUbuelr6Q4Ick= 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= From 5d32739573b44dd04c6e1e82057716058fbfe4e1 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 12 Nov 2024 18:46:38 -0300 Subject: [PATCH 176/401] update go-nostr again, apparently this was necessary. --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 8738504..4cc8204 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/fiatjaf/khatru v0.10.0 github.com/mailru/easyjson v0.7.7 github.com/markusmobius/go-dateparser v1.2.3 - github.com/nbd-wtf/go-nostr v0.42.1 + github.com/nbd-wtf/go-nostr v0.42.2 golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 ) diff --git a/go.sum b/go.sum index 50cd0d0..cadffd1 100644 --- a/go.sum +++ b/go.sum @@ -117,8 +117,8 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/nbd-wtf/go-nostr v0.42.1 h1:zQksbvj+EsLgZBuKugvMzMpzXZuIp1WSx3BvgZ5ZY6A= -github.com/nbd-wtf/go-nostr v0.42.1/go.mod h1:FBa4FBJO7NuANvkeKSlrf0BIyxGufmrUbuelr6Q4Ick= +github.com/nbd-wtf/go-nostr v0.42.2 h1:X8vpfLutvmyxqjsroKPHdIyPliNa6sYD8+CA0kDVySw= +github.com/nbd-wtf/go-nostr v0.42.2/go.mod h1:FBa4FBJO7NuANvkeKSlrf0BIyxGufmrUbuelr6Q4Ick= 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= From 9d619ddf00cdec3d8050bf8bf449d1f8a814590b Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 19 Nov 2024 07:47:11 -0300 Subject: [PATCH 177/401] remove note1 encoding. --- encode.go | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/encode.go b/encode.go index 17d097d..f30feed 100644 --- a/encode.go +++ b/encode.go @@ -212,28 +212,6 @@ var encode = &cli.Command{ } } - exitIfLineProcessingError(ctx) - return nil - }, - }, - { - Name: "note", - Usage: "generate note1 event codes (not recommended)", - 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 event id: %s", target) - continue - } - - if note, err := nip19.EncodeNote(target); err == nil { - stdout(note) - } else { - return err - } - } - exitIfLineProcessingError(ctx) return nil }, From 491a094e0742c0d506dd8bf16af4f9c12b91fa92 Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Sat, 23 Nov 2024 17:33:01 +0900 Subject: [PATCH 178/401] close ch --- helpers.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/helpers.go b/helpers.go index f84967f..6cf5908 100644 --- a/helpers.go +++ b/helpers.go @@ -70,7 +70,9 @@ func getStdinLinesOrArgumentsFromSlice(args []string) chan string { // try the stdin multi := make(chan string) - writeStdinLinesOrNothing(multi) + if !writeStdinLinesOrNothing(multi) { + close(multi) + } return multi } From dd0ef2ca64e94cf6ce908d4b229fe4e518cdeb3c Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Mon, 25 Nov 2024 12:31:21 -0300 Subject: [PATCH 179/401] relay management examples on help. --- relay.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/relay.go b/relay.go index 381bf65..ac69a33 100644 --- a/relay.go +++ b/relay.go @@ -19,9 +19,13 @@ import ( var relay = &cli.Command{ Name: "relay", - Usage: "gets the relay information document for the given relay, as JSON", - Description: `example: - nak relay nostr.wine`, + Usage: "gets the relay information document for the given relay, as JSON -- or allows usage of the relay management API.", + Description: `examples: + fetching relay information: + nak relay nostr.wine + + managing a relay + nak relay nostr.wine banevent --sec 1234 --id 037eb3751073770ff17483b1b1ff125866cd5147668271975ef0a8a8e7ee184a --reason "I don't like it"`, ArgsUsage: "", DisableSliceFlagSeparator: true, Action: func(ctx context.Context, c *cli.Command) error { From f425097c5a451d12e9eb523e6abf47956de70d9a Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 26 Nov 2024 12:05:38 -0300 Subject: [PATCH 180/401] allow filters with long tags (the 1-char restriction is only a convention, not a rule). fixes https://github.com/fiatjaf/nak/issues/44 --- count.go | 2 +- req.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/count.go b/count.go index 906b516..36c0bd5 100644 --- a/count.go +++ b/count.go @@ -85,7 +85,7 @@ var count = &cli.Command{ tags := make([][]string, 0, 5) for _, tagFlag := range c.StringSlice("tag") { spl := strings.SplitN(tagFlag, "=", 2) - if len(spl) == 2 && len(spl[0]) == 1 { + if len(spl) == 2 { tags = append(tags, spl) } else { return fmt.Errorf("invalid --tag '%s'", tagFlag) diff --git a/req.go b/req.go index f4a143a..4b0b92a 100644 --- a/req.go +++ b/req.go @@ -232,7 +232,7 @@ func applyFlagsToFilter(c *cli.Command, filter *nostr.Filter) error { tags := make([][]string, 0, 5) for _, tagFlag := range c.StringSlice("tag") { spl := strings.SplitN(tagFlag, "=", 2) - if len(spl) == 2 && len(spl[0]) == 1 { + if len(spl) == 2 { tags = append(tags, spl) } else { return fmt.Errorf("invalid --tag '%s'", tagFlag) From 7033bfee1922a4a97ccf12d0f1d9d4d4e2216986 Mon Sep 17 00:00:00 2001 From: redraw Date: Sun, 1 Dec 2024 23:17:09 -0300 Subject: [PATCH 181/401] fix(decode): handle pubkey flag --- decode.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/decode.go b/decode.go index 9da34ad..314628d 100644 --- a/decode.go +++ b/decode.go @@ -58,6 +58,10 @@ var decode = &cli.Command{ decodeResult = DecodeResult{EventPointer: evp} } else if pp := sdk.InputToProfile(ctx, input); pp != nil { decodeResult = DecodeResult{ProfilePointer: pp} + if c.Bool("pubkey") { + stdout(pp.PublicKey) + return nil + } } else if prefix, value, err := nip19.Decode(input); err == nil && prefix == "naddr" { if ep, ok := value.(nostr.EntityPointer); ok { decodeResult = DecodeResult{EntityPointer: &ep} From 11ae7bc4d343ac265a2fbb1ed53d45d4f4a3c61a Mon Sep 17 00:00:00 2001 From: redraw Date: Sun, 1 Dec 2024 23:27:35 -0300 Subject: [PATCH 182/401] add test ExampleDecodePubkey --- example_test.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/example_test.go b/example_test.go index d713dee..bc2ae33 100644 --- a/example_test.go +++ b/example_test.go @@ -67,6 +67,12 @@ func ExampleDecode() { // } } +func ExampleDecodePubkey() { + app.Run(ctx, []string{"nak", "decode", "-p", "npub10xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqpkge6d"}) + // Output: + // 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 +} + func ExampleReq() { app.Run(ctx, []string{"nak", "req", "-k", "1", "-l", "18", "-a", "2fa2104d6b38d11b0230010559879124e42ab8dfeff5ff29dc9cdadd4ecacc3f", "-e", "aec4de6d051a7c2b6ca2d087903d42051a31e07fb742f1240970084822de10a6"}) // Output: From 932361fe8f3021afb2a8fcfa084f9eed2c3ecc1b Mon Sep 17 00:00:00 2001 From: redraw Date: Mon, 2 Dec 2024 10:00:52 -0300 Subject: [PATCH 183/401] fix(decode): handle event id flag --- decode.go | 10 +++++++++- example_test.go | 10 +++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/decode.go b/decode.go index 314628d..561ba92 100644 --- a/decode.go +++ b/decode.go @@ -56,11 +56,15 @@ var decode = &cli.Command{ } } else if evp := sdk.InputToEventPointer(input); evp != nil { decodeResult = DecodeResult{EventPointer: evp} + if c.Bool("id") { + stdout(evp.ID) + continue + } } else if pp := sdk.InputToProfile(ctx, input); pp != nil { decodeResult = DecodeResult{ProfilePointer: pp} if c.Bool("pubkey") { stdout(pp.PublicKey) - return nil + continue } } else if prefix, value, err := nip19.Decode(input); err == nil && prefix == "naddr" { if ep, ok := value.(nostr.EntityPointer); ok { @@ -76,6 +80,10 @@ var decode = &cli.Command{ continue } + if c.Bool("pubkey") || c.Bool("id") { + return nil + } + stdout(decodeResult.JSON()) } diff --git a/example_test.go b/example_test.go index bc2ae33..e77c4eb 100644 --- a/example_test.go +++ b/example_test.go @@ -68,9 +68,17 @@ func ExampleDecode() { } func ExampleDecodePubkey() { - app.Run(ctx, []string{"nak", "decode", "-p", "npub10xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqpkge6d"}) + app.Run(ctx, []string{"nak", "decode", "-p", "npub10xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqpkge6d", "npub1ccz8l9zpa47k6vz9gphftsrumpw80rjt3nhnefat4symjhrsnmjs38mnyd"}) // Output: // 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 + // c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5 +} + +func ExampleDecodeEventId() { + app.Run(ctx, []string{"nak", "decode", "-e", "nevent1qyd8wumn8ghj7urewfsk66ty9enxjct5dfskvtnrdakj7qgmwaehxw309aex2mrp0yh8wetnw3jhymnzw33jucm0d5hszxthwden5te0wfjkccte9eekummjwsh8xmmrd9skctcpzamhxue69uhkzarvv9ejumn0wd68ytnvv9hxgtcqyqllp5v5j0nxr74fptqxkhvfv0h3uj870qpk3ln8a58agyxl3fka296ewr8", "nevent1qqswh48lurxs8u0pll9qj2rzctvjncwhstpzlstq59rdtzlty79awns5hl5uf"}) + // Output: + // 3ff0d19493e661faa90ac06b5d8963ef1e48fe780368fe67ed0fd410df8a6dd5 + // ebd4ffe0cd03f1e1ffca092862c2d929e1d782c22fc160a146d58beb278bd74e } func ExampleReq() { From fd5cd55f6f04209130474b997658941a1ccb54a7 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 3 Dec 2024 00:42:41 -0300 Subject: [PATCH 184/401] replace encoding/json with json-iterator everywhere so we get rid of HTML encoding and maybe be faster. --- bunker.go | 3 +-- count.go | 1 - decode.go | 1 - event.go | 4 ++-- go.mod | 5 ++++- go.sum | 9 +++++++++ helpers.go | 3 +++ key.go | 1 - relay.go | 1 - req.go | 1 - serve.go | 1 - verify.go | 1 - 12 files changed, 19 insertions(+), 12 deletions(-) diff --git a/bunker.go b/bunker.go index a74c8da..177ebc3 100644 --- a/bunker.go +++ b/bunker.go @@ -2,10 +2,10 @@ package main import ( "context" - "encoding/json" "fmt" "net/url" "os" + "slices" "strings" "sync" "time" @@ -15,7 +15,6 @@ import ( "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/nip19" "github.com/nbd-wtf/go-nostr/nip46" - "golang.org/x/exp/slices" ) var bunker = &cli.Command{ diff --git a/count.go b/count.go index 36c0bd5..6333d30 100644 --- a/count.go +++ b/count.go @@ -2,7 +2,6 @@ package main import ( "context" - "encoding/json" "errors" "fmt" "strings" diff --git a/decode.go b/decode.go index 561ba92..4b9a139 100644 --- a/decode.go +++ b/decode.go @@ -3,7 +3,6 @@ package main import ( "context" "encoding/hex" - "encoding/json" "strings" "github.com/fiatjaf/cli/v3" diff --git a/event.go b/event.go index 26c1250..b87a34c 100644 --- a/event.go +++ b/event.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "os" + "slices" "strings" "time" @@ -12,7 +13,6 @@ import ( "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/nip13" "github.com/nbd-wtf/go-nostr/nip19" - "golang.org/x/exp/slices" ) const ( @@ -276,7 +276,7 @@ example: // print event as json var result string if c.Bool("envelope") { - j, _ := easyjson.Marshal(nostr.EventEnvelope{Event: evt}) + j, _ := json.Marshal(nostr.EventEnvelope{Event: evt}) result = string(j) } else { j, _ := easyjson.Marshal(&evt) diff --git a/go.mod b/go.mod index 4cc8204..72da3b4 100644 --- a/go.mod +++ b/go.mod @@ -11,10 +11,10 @@ require ( github.com/fiatjaf/cli/v3 v3.0.0-20240723181502-e7dd498b16ae github.com/fiatjaf/eventstore v0.12.0 github.com/fiatjaf/khatru v0.10.0 + github.com/json-iterator/go v1.1.12 github.com/mailru/easyjson v0.7.7 github.com/markusmobius/go-dateparser v1.2.3 github.com/nbd-wtf/go-nostr v0.42.2 - golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 ) require ( @@ -43,6 +43,8 @@ require ( github.com/magefile/mage v1.14.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/puzpuzpuz/xsync/v3 v3.4.0 // indirect github.com/rs/cors v1.7.0 // indirect @@ -55,6 +57,7 @@ require ( github.com/valyala/fasthttp v1.51.0 // indirect github.com/wasilibs/go-re2 v1.3.0 // indirect golang.org/x/crypto v0.28.0 // indirect + golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect golang.org/x/net v0.30.0 // indirect golang.org/x/sys v0.26.0 // indirect golang.org/x/text v0.19.0 // indirect diff --git a/go.sum b/go.sum index cadffd1..b496dc8 100644 --- a/go.sum +++ b/go.sum @@ -87,6 +87,7 @@ github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/graph-gophers/dataloader/v7 v7.1.0 h1:Wn8HGF/q7MNXcvfaBnLEPEFJttVHR8zuEqP1obys/oc= github.com/graph-gophers/dataloader/v7 v7.1.0/go.mod h1:1bKE0Dm6OUcTB/OAuYVOZctgIz7Q3d0XrYtlIzTgg6Q= github.com/greatroar/blobloom v0.8.0 h1:I9RlEkfqK9/6f1v9mFmDYegDQ/x0mISCpiNpAm23Pt4= @@ -103,6 +104,8 @@ github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJS github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= github.com/klauspost/compress v1.17.10 h1:oXAz+Vh0PMUvJczoi+flxpnBEPxoER1IaAnU/NMPtT0= github.com/klauspost/compress v1.17.10/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= @@ -117,6 +120,11 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +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.42.2 h1:X8vpfLutvmyxqjsroKPHdIyPliNa6sYD8+CA0kDVySw= github.com/nbd-wtf/go-nostr v0.42.2/go.mod h1:FBa4FBJO7NuANvkeKSlrf0BIyxGufmrUbuelr6Q4Ick= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= @@ -142,6 +150,7 @@ github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1 github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= diff --git a/helpers.go b/helpers.go index 6cf5908..f276682 100644 --- a/helpers.go +++ b/helpers.go @@ -12,12 +12,15 @@ import ( "github.com/fatih/color" "github.com/fiatjaf/cli/v3" + jsoniter "github.com/json-iterator/go" "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/sdk" ) var sys = sdk.NewSystem() +var json = jsoniter.ConfigFastest + func init() { sys.Pool = nostr.NewSimplePool(context.Background(), nostr.WithUserAgent("nak/b"), diff --git a/key.go b/key.go index 7d399b4..4a160d0 100644 --- a/key.go +++ b/key.go @@ -3,7 +3,6 @@ package main import ( "context" "encoding/hex" - "encoding/json" "fmt" "strings" diff --git a/relay.go b/relay.go index ac69a33..afc9a44 100644 --- a/relay.go +++ b/relay.go @@ -6,7 +6,6 @@ import ( "crypto/sha256" "encoding/base64" "encoding/hex" - "encoding/json" "fmt" "io" "net/http" diff --git a/req.go b/req.go index 4b0b92a..e0ea0f9 100644 --- a/req.go +++ b/req.go @@ -2,7 +2,6 @@ package main import ( "context" - "encoding/json" "fmt" "os" "strings" diff --git a/serve.go b/serve.go index 07e7722..c2a0a7c 100644 --- a/serve.go +++ b/serve.go @@ -3,7 +3,6 @@ package main import ( "bufio" "context" - "encoding/json" "fmt" "math" "os" diff --git a/verify.go b/verify.go index 69305e6..5b584cb 100644 --- a/verify.go +++ b/verify.go @@ -2,7 +2,6 @@ package main import ( "context" - "encoding/json" "github.com/fiatjaf/cli/v3" "github.com/nbd-wtf/go-nostr" From 7f608588a2616c52f93aab1becfb6aca954e6c35 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 10 Dec 2024 23:28:36 -0300 Subject: [PATCH 185/401] improve count and support hyperloglog aggregation. --- count.go | 63 ++++++++++++++++++++++++++++++++++++++++++++------------ go.mod | 25 +++++++++++----------- go.sum | 61 +++++++++++++++++++++++------------------------------- 3 files changed, 88 insertions(+), 61 deletions(-) diff --git a/count.go b/count.go index 6333d30..704a6a7 100644 --- a/count.go +++ b/count.go @@ -2,12 +2,14 @@ package main import ( "context" - "errors" "fmt" + "os" "strings" "github.com/fiatjaf/cli/v3" "github.com/nbd-wtf/go-nostr" + "github.com/nbd-wtf/go-nostr/nip45" + "github.com/nbd-wtf/go-nostr/nip45/hyperloglog" ) var count = &cli.Command{ @@ -65,6 +67,32 @@ var count = &cli.Command{ }, ArgsUsage: "[relay...]", Action: func(ctx context.Context, c *cli.Command) error { + biggerUrlSize := 0 + relayUrls := c.Args().Slice() + if len(relayUrls) > 0 { + relays := connectToAllRelays(ctx, + relayUrls, + false, + ) + if len(relays) == 0 { + log("failed to connect to any of the given relays.\n") + os.Exit(3) + } + relayUrls = make([]string, len(relays)) + for i, relay := range relays { + relayUrls[i] = relay.URL + if len(relay.URL) > biggerUrlSize { + biggerUrlSize = len(relay.URL) + } + } + + defer func() { + for _, relay := range relays { + relay.Close() + } + }() + } + filter := nostr.Filter{} if authors := c.StringSlice("author"); len(authors) > 0 { @@ -118,26 +146,35 @@ var count = &cli.Command{ filter.Limit = int(limit) } - relays := c.Args().Slice() successes := 0 - failures := make([]error, 0, len(relays)) - if len(relays) > 0 { - for _, relayUrl := range relays { - relay, err := nostr.RelayConnect(ctx, relayUrl) + if len(relayUrls) > 0 { + var hll *hyperloglog.HyperLogLog + if offset := nip45.HyperLogLogEventPubkeyOffsetForFilter(filter); offset != -1 && len(relayUrls) > 1 { + hll = hyperloglog.New(offset) + } + for _, relayUrl := range relayUrls { + relay, _ := sys.Pool.EnsureRelay(relayUrl) + count, hllRegisters, err := relay.Count(ctx, nostr.Filters{filter}) + fmt.Fprintf(os.Stderr, "%s%s: ", strings.Repeat(" ", biggerUrlSize-len(relayUrl)), relayUrl) + if err != nil { - failures = append(failures, err) + fmt.Fprintf(os.Stderr, "❌ %s\n", err) continue } - count, err := relay.Count(ctx, nostr.Filters{filter}) - if err != nil { - failures = append(failures, err) - continue + + var hasHLLStr string + if hll != nil && len(hllRegisters) == 256 { + hll.MergeRegisters(hllRegisters) + hasHLLStr = " 📋" } - fmt.Printf("%s: %d\n", relay.URL, count) + + fmt.Fprintf(os.Stderr, "%d%s\n", count, hasHLLStr) successes++ } if successes == 0 { - return errors.Join(failures...) + return fmt.Errorf("all relays have failed") + } else if hll != nil { + fmt.Fprintf(os.Stderr, "📋 HyperLogLog sum: %d\n", hll.Count()) } } else { // no relays given, will just print the filter diff --git a/go.mod b/go.mod index 72da3b4..69b9fd9 100644 --- a/go.mod +++ b/go.mod @@ -9,19 +9,19 @@ require ( github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 github.com/fatih/color v1.16.0 github.com/fiatjaf/cli/v3 v3.0.0-20240723181502-e7dd498b16ae - github.com/fiatjaf/eventstore v0.12.0 - github.com/fiatjaf/khatru v0.10.0 + github.com/fiatjaf/eventstore v0.14.2 + github.com/fiatjaf/khatru v0.14.0 github.com/json-iterator/go v1.1.12 github.com/mailru/easyjson v0.7.7 github.com/markusmobius/go-dateparser v1.2.3 - github.com/nbd-wtf/go-nostr v0.42.2 + github.com/nbd-wtf/go-nostr v0.44.0 ) require ( + fiatjaf.com/lib v0.2.0 // indirect github.com/andybalholm/brotli v1.0.5 // indirect github.com/btcsuite/btcd/btcutil v1.1.3 // indirect github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect - github.com/cespare/xxhash v1.1.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chzyer/logex v1.1.10 // indirect github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect @@ -34,12 +34,11 @@ require ( github.com/gobwas/pool v0.2.1 // indirect github.com/gobwas/ws v1.4.0 // indirect github.com/graph-gophers/dataloader/v7 v7.1.0 // indirect - github.com/greatroar/blobloom v0.8.0 // indirect github.com/hablullah/go-hijri v1.0.2 // indirect github.com/hablullah/go-juliandays v1.0.0 // indirect github.com/jalaali/go-jalaali v0.0.0-20210801064154-80525e88d958 // indirect github.com/josharian/intern v1.0.0 // indirect - github.com/klauspost/compress v1.17.10 // indirect + github.com/klauspost/compress v1.17.11 // indirect github.com/magefile/mage v1.14.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect @@ -47,18 +46,18 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/puzpuzpuz/xsync/v3 v3.4.0 // indirect - github.com/rs/cors v1.7.0 // indirect + github.com/rs/cors v1.11.1 // indirect github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect github.com/tetratelabs/wazero v1.8.0 // indirect - github.com/tidwall/gjson v1.17.3 // indirect + github.com/tidwall/gjson v1.18.0 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.51.0 // indirect github.com/wasilibs/go-re2 v1.3.0 // indirect - golang.org/x/crypto v0.28.0 // indirect - golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect - golang.org/x/net v0.30.0 // indirect - golang.org/x/sys v0.26.0 // indirect - golang.org/x/text v0.19.0 // indirect + golang.org/x/crypto v0.30.0 // indirect + golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d // indirect + golang.org/x/net v0.32.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/text v0.21.0 // indirect ) diff --git a/go.sum b/go.sum index b496dc8..7ee1706 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= -github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +fiatjaf.com/lib v0.2.0 h1:TgIJESbbND6GjOgGHxF5jsO6EMjuAxIzZHPo5DXYexs= +fiatjaf.com/lib v0.2.0/go.mod h1:Ycqq3+mJ9jAWu7XjbQI1cVr+OFgnHn79dQR5oTII47g= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= @@ -29,8 +29,6 @@ github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= -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= @@ -64,10 +62,10 @@ 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/cli/v3 v3.0.0-20240723181502-e7dd498b16ae h1:0B/1dU3YECIbPoBIRTQ4c0scZCNz9TVHtQpiODGrTTo= github.com/fiatjaf/cli/v3 v3.0.0-20240723181502-e7dd498b16ae/go.mod h1:aAWPO4bixZZxPtOnH6K3q4GbQ0jftUNDW9Oa861IRew= -github.com/fiatjaf/eventstore v0.12.0 h1:ZdL+dZkIgBgIp5A3+3XLdPg/uucv5Tiws6DHzNfZG4M= -github.com/fiatjaf/eventstore v0.12.0/go.mod h1:PxeYbZ3MsH0XLobANsp6c0cJjJYkfmBJ3TwrplFy/08= -github.com/fiatjaf/khatru v0.10.0 h1:f43om33RZfkIAIW9vhHelFJXp8XCij/Jh30AmJ0AVF8= -github.com/fiatjaf/khatru v0.10.0/go.mod h1:iCLz0bPcFSBHJrY2kZ1lji5IrIsv9YFwRS7aaXIPv+o= +github.com/fiatjaf/eventstore v0.14.2 h1:1XOLLEBCGQNQ1rLaO8mcfTQydcetPOqb/uRK8zOnOSI= +github.com/fiatjaf/eventstore v0.14.2/go.mod h1:XmYZSdFxsY+cwfpdzVG61M7pemcmqlZwDfDGFC8WwWo= +github.com/fiatjaf/khatru v0.14.0 h1:zpWlAA87XBpDKBPIDbAuNw/HpKXzyt5XHVDbSvUbmDo= +github.com/fiatjaf/khatru v0.14.0/go.mod h1:uxE5e8DBXPZqbHjr/gfatQas5bEJIMmsOCDcdF4LoRQ= 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/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= @@ -90,8 +88,6 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/graph-gophers/dataloader/v7 v7.1.0 h1:Wn8HGF/q7MNXcvfaBnLEPEFJttVHR8zuEqP1obys/oc= github.com/graph-gophers/dataloader/v7 v7.1.0/go.mod h1:1bKE0Dm6OUcTB/OAuYVOZctgIz7Q3d0XrYtlIzTgg6Q= -github.com/greatroar/blobloom v0.8.0 h1:I9RlEkfqK9/6f1v9mFmDYegDQ/x0mISCpiNpAm23Pt4= -github.com/greatroar/blobloom v0.8.0/go.mod h1:mjMJ1hh1wjGVfr93QIHJ6FfDNVrA0IELv8OvMHJxHKs= github.com/hablullah/go-hijri v1.0.2 h1:drT/MZpSZJQXo7jftf5fthArShcaMtsal0Zf/dnmp6k= github.com/hablullah/go-hijri v1.0.2/go.mod h1:OS5qyYLDjORXzK4O1adFw9Q5WfhOcMdAKglDkcTxgWQ= github.com/hablullah/go-juliandays v1.0.0 h1:A8YM7wIj16SzlKT0SRJc9CD29iiaUzpBLzh5hr0/5p0= @@ -107,8 +103,8 @@ github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlT github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= -github.com/klauspost/compress v1.17.10 h1:oXAz+Vh0PMUvJczoi+flxpnBEPxoER1IaAnU/NMPtT0= -github.com/klauspost/compress v1.17.10/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= +github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo= github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= @@ -125,8 +121,8 @@ 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.42.2 h1:X8vpfLutvmyxqjsroKPHdIyPliNa6sYD8+CA0kDVySw= -github.com/nbd-wtf/go-nostr v0.42.2/go.mod h1:FBa4FBJO7NuANvkeKSlrf0BIyxGufmrUbuelr6Q4Ick= +github.com/nbd-wtf/go-nostr v0.44.0 h1:qBC7/Ze9qT0MLimFOombcbCbLwbaSvXNoFdjlP3uzYg= +github.com/nbd-wtf/go-nostr v0.44.0/go.mod h1:8YfmT9tBuRT+4nWHuMBDh+xSIZqAdZC6QIOgQfBgWxU= 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= @@ -142,25 +138,20 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/puzpuzpuz/xsync/v3 v3.4.0 h1:DuVBAdXuGFHv8adVXjWWZ63pJq+NRXOWVXlKDBZ+mJ4= github.com/puzpuzpuz/xsync/v3 v3.4.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= -github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= -github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= +github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk= github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g= -github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ= -github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= github.com/tetratelabs/wazero v1.8.0 h1:iEKu0d4c2Pd+QSRieYbnQC9yiFlMS9D+Jr0LsRmcF4g= github.com/tetratelabs/wazero v1.8.0/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs= -github.com/tidwall/gjson v1.17.3 h1:bwWLZU7icoKRG+C+0PNwIKC6FCJO/Q3p2pZvuP0jN94= -github.com/tidwall/gjson v1.17.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= @@ -177,17 +168,17 @@ github.com/wasilibs/nottinygc v0.4.0/go.mod h1:oDcIotskuYNMpqMF23l7Z8uzD4TC0WXHK golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= -golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= -golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= -golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= +golang.org/x/crypto v0.30.0 h1:RwoQn3GkWiMkzlX562cLB7OxWvjH1L8xutO2WoJcRoY= +golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d h1:0olWaB5pg3+oychR51GUVCEsGkeCU/2JxjBgIo4f3M0= +golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= -golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI= +golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 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= @@ -200,13 +191,13 @@ golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= -golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= -golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= From 767592905653b6c31eab88db2f588bf7d14ea853 Mon Sep 17 00:00:00 2001 From: franzap <_@franzap.com> Date: Wed, 11 Dec 2024 11:17:39 -0300 Subject: [PATCH 186/401] Add zapstore.yaml --- zapstore.yaml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 zapstore.yaml diff --git a/zapstore.yaml b/zapstore.yaml new file mode 100644 index 0000000..9d70234 --- /dev/null +++ b/zapstore.yaml @@ -0,0 +1,14 @@ +nak: + cli: + name: nak + summary: a command line tool for doing all things nostr + repository: https://github.com/fiatjaf/nak + artifacts: + nak-v%v-darwin-arm64: + platforms: [darwin-arm64] + nak-v%v-darwin-amd64: + platforms: [darwin-x86_64] + nak-v%v-linux-arm64: + platforms: [linux-aarch64] + nak-v%v-linux-amd64: + platforms: [linux-x86_64] \ No newline at end of file From 2d992f235e687565d33f80dd6f7b2bd8e1f263ca Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Fri, 13 Dec 2024 23:27:08 -0300 Subject: [PATCH 187/401] bunker: fix MarshalIndent() is not allowed to have a prefix on json-iterator. --- bunker.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bunker.go b/bunker.go index 177ebc3..179ba9e 100644 --- a/bunker.go +++ b/bunker.go @@ -188,9 +188,9 @@ var bunker = &cli.Command{ continue } - jreq, _ := json.MarshalIndent(req, " ", " ") + jreq, _ := json.MarshalIndent(req, "", " ") log("- got request from '%s': %s\n", color.New(color.Bold, color.FgBlue).Sprint(ie.Event.PubKey), string(jreq)) - jresp, _ := json.MarshalIndent(resp, " ", " ") + jresp, _ := json.MarshalIndent(resp, "", " ") log("~ responding with %s\n", string(jresp)) handlerWg.Add(len(relayURLs)) From 53a24513032ac38a6dd252274edf5eb4df581603 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Thu, 16 Jan 2025 16:02:03 -0300 Subject: [PATCH 188/401] update go-nostr and adjust user-agent stuff. this changes the websocket library we were using. --- go.mod | 6 ++---- go.sum | 12 ++++-------- helpers.go | 10 ++++++++-- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/go.mod b/go.mod index 69b9fd9..353e8bb 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/json-iterator/go v1.1.12 github.com/mailru/easyjson v0.7.7 github.com/markusmobius/go-dateparser v1.2.3 - github.com/nbd-wtf/go-nostr v0.44.0 + github.com/nbd-wtf/go-nostr v0.46.0 ) require ( @@ -25,14 +25,12 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chzyer/logex v1.1.10 // indirect github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect + github.com/coder/websocket v1.8.12 // indirect github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect github.com/dgraph-io/ristretto v1.0.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/elliotchance/pie/v2 v2.7.0 // indirect github.com/fasthttp/websocket v1.5.7 // indirect - github.com/gobwas/httphead v0.1.0 // indirect - github.com/gobwas/pool v0.2.1 // indirect - github.com/gobwas/ws v1.4.0 // indirect github.com/graph-gophers/dataloader/v7 v7.1.0 // indirect github.com/hablullah/go-hijri v1.0.2 // indirect github.com/hablullah/go-juliandays v1.0.0 // indirect diff --git a/go.sum b/go.sum index 7ee1706..fd00dd3 100644 --- a/go.sum +++ b/go.sum @@ -37,6 +37,8 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5O github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo= +github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -68,12 +70,6 @@ github.com/fiatjaf/khatru v0.14.0 h1:zpWlAA87XBpDKBPIDbAuNw/HpKXzyt5XHVDbSvUbmDo github.com/fiatjaf/khatru v0.14.0/go.mod h1:uxE5e8DBXPZqbHjr/gfatQas5bEJIMmsOCDcdF4LoRQ= 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/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= -github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= -github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= -github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= -github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs= -github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= @@ -121,8 +117,8 @@ 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.44.0 h1:qBC7/Ze9qT0MLimFOombcbCbLwbaSvXNoFdjlP3uzYg= -github.com/nbd-wtf/go-nostr v0.44.0/go.mod h1:8YfmT9tBuRT+4nWHuMBDh+xSIZqAdZC6QIOgQfBgWxU= +github.com/nbd-wtf/go-nostr v0.46.0 h1:aR+xXEC6MPutNMIRhNdi+2iBPEHW7SO10sFaOAVSz3Y= +github.com/nbd-wtf/go-nostr v0.46.0/go.mod h1:xVNOqkn0GImeTmaF6VDwgYsuSkfG3yrIbd0dT6NZDIQ= 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= diff --git a/helpers.go b/helpers.go index f276682..40ce39b 100644 --- a/helpers.go +++ b/helpers.go @@ -5,6 +5,8 @@ import ( "context" "fmt" "math/rand" + "net/http" + "net/textproto" "net/url" "os" "strings" @@ -23,7 +25,9 @@ var json = jsoniter.ConfigFastest func init() { sys.Pool = nostr.NewSimplePool(context.Background(), - nostr.WithUserAgent("nak/b"), + nostr.WithRelayOptions( + nostr.WithRequestHeader(http.Header{textproto.CanonicalMIMEHeaderKey("user-agent"): {"nak/b"}}), + ), ) } @@ -134,7 +138,9 @@ func connectToAllRelays( append(opts, nostr.WithEventMiddleware(sys.TrackEventHints), nostr.WithPenaltyBox(), - nostr.WithUserAgent("nak/s"), + nostr.WithRelayOptions( + nostr.WithRequestHeader(http.Header{textproto.CanonicalMIMEHeaderKey("user-agent"): {"nak/s"}}), + ), )..., ) From df20a3241aeec638b3b6d98a61e1a150dd8f4baa Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Thu, 16 Jan 2025 21:46:43 -0300 Subject: [PATCH 189/401] update go-nostr to fix url path normalization (@pablof7z's @primalhq NWC bug). --- go.mod | 8 ++++---- go.sum | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index 353e8bb..d499ae1 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/json-iterator/go v1.1.12 github.com/mailru/easyjson v0.7.7 github.com/markusmobius/go-dateparser v1.2.3 - github.com/nbd-wtf/go-nostr v0.46.0 + github.com/nbd-wtf/go-nostr v0.47.0 ) require ( @@ -53,9 +53,9 @@ require ( github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.51.0 // indirect github.com/wasilibs/go-re2 v1.3.0 // indirect - golang.org/x/crypto v0.30.0 // indirect + golang.org/x/crypto v0.32.0 // indirect golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d // indirect - golang.org/x/net v0.32.0 // indirect - golang.org/x/sys v0.28.0 // indirect + golang.org/x/net v0.34.0 // indirect + golang.org/x/sys v0.29.0 // indirect golang.org/x/text v0.21.0 // indirect ) diff --git a/go.sum b/go.sum index fd00dd3..adbd5dd 100644 --- a/go.sum +++ b/go.sum @@ -117,8 +117,8 @@ 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.46.0 h1:aR+xXEC6MPutNMIRhNdi+2iBPEHW7SO10sFaOAVSz3Y= -github.com/nbd-wtf/go-nostr v0.46.0/go.mod h1:xVNOqkn0GImeTmaF6VDwgYsuSkfG3yrIbd0dT6NZDIQ= +github.com/nbd-wtf/go-nostr v0.47.0 h1:TM3kf3arDoyVqTmfIbYkgYsKJtIWydMF0zvAbaTenk4= +github.com/nbd-wtf/go-nostr v0.47.0/go.mod h1:O6n8bv+KktkEs+4svL7KN/OSnOWB5LzcZbuKjxnpRD0= 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= @@ -164,8 +164,8 @@ github.com/wasilibs/nottinygc v0.4.0/go.mod h1:oDcIotskuYNMpqMF23l7Z8uzD4TC0WXHK golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.30.0 h1:RwoQn3GkWiMkzlX562cLB7OxWvjH1L8xutO2WoJcRoY= -golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= +golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d h1:0olWaB5pg3+oychR51GUVCEsGkeCU/2JxjBgIo4f3M0= golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -173,8 +173,8 @@ golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI= -golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs= +golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= +golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 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= @@ -187,8 +187,8 @@ golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= From a3ef9b45de881af3e0d23f94949ddfb79507d603 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sat, 18 Jan 2025 15:45:08 -0300 Subject: [PATCH 190/401] update go-nostr to solve some websocket things like a stupid limit to the websocket message. --- go.mod | 6 +++--- go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index d499ae1..16a0e3f 100644 --- a/go.mod +++ b/go.mod @@ -12,9 +12,9 @@ require ( github.com/fiatjaf/eventstore v0.14.2 github.com/fiatjaf/khatru v0.14.0 github.com/json-iterator/go v1.1.12 - github.com/mailru/easyjson v0.7.7 + github.com/mailru/easyjson v0.9.0 github.com/markusmobius/go-dateparser v1.2.3 - github.com/nbd-wtf/go-nostr v0.47.0 + github.com/nbd-wtf/go-nostr v0.47.3 ) require ( @@ -54,7 +54,7 @@ require ( github.com/valyala/fasthttp v1.51.0 // indirect github.com/wasilibs/go-re2 v1.3.0 // indirect golang.org/x/crypto v0.32.0 // indirect - golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d // indirect + golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect golang.org/x/net v0.34.0 // indirect golang.org/x/sys v0.29.0 // indirect golang.org/x/text v0.21.0 // indirect diff --git a/go.sum b/go.sum index adbd5dd..7143878 100644 --- a/go.sum +++ b/go.sum @@ -103,8 +103,8 @@ github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IX github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo= github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= +github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/markusmobius/go-dateparser v1.2.3 h1:TvrsIvr5uk+3v6poDjaicnAFJ5IgtFHgLiuMY2Eb7Nw= github.com/markusmobius/go-dateparser v1.2.3/go.mod h1:cMwQRrBUQlK1UI5TIFHEcvpsMbkWrQLXuaPNMFzuYLk= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= @@ -117,8 +117,8 @@ 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.47.0 h1:TM3kf3arDoyVqTmfIbYkgYsKJtIWydMF0zvAbaTenk4= -github.com/nbd-wtf/go-nostr v0.47.0/go.mod h1:O6n8bv+KktkEs+4svL7KN/OSnOWB5LzcZbuKjxnpRD0= +github.com/nbd-wtf/go-nostr v0.47.3 h1:KM86+0rLa8jPvt11akST1T0ffhrs6KpzvJ2CLYo765c= +github.com/nbd-wtf/go-nostr v0.47.3/go.mod h1:O6n8bv+KktkEs+4svL7KN/OSnOWB5LzcZbuKjxnpRD0= 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= @@ -166,8 +166,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= -golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d h1:0olWaB5pg3+oychR51GUVCEsGkeCU/2JxjBgIo4f3M0= -golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= +golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA= +golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU= golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= From 5509095277ae67058fa6bc6fc2865a829c69e4b7 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sat, 18 Jan 2025 18:33:42 -0300 Subject: [PATCH 191/401] nak outbox --- fetch.go | 8 +++++-- go.mod | 2 +- go.sum | 4 ++-- helpers.go | 15 +++++------- main.go | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++ outbox.go | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 148 insertions(+), 14 deletions(-) create mode 100644 outbox.go diff --git a/fetch.go b/fetch.go index 6bd40b9..ea84418 100644 --- a/fetch.go +++ b/fetch.go @@ -8,11 +8,12 @@ import ( "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{ Name: "fetch", - Usage: "fetches events related to the given nip19 or nip05 code from the included relay hints or the author's NIP-65 relays.", + Usage: "fetches events related to the given nip19 or nip05 code from the included relay hints or the author's outbox relays.", Description: `example usage: nak fetch nevent1qqsxrwm0hd3s3fddh4jc2574z3xzufq6qwuyz2rvv3n087zvym3dpaqprpmhxue69uhhqatzd35kxtnjv4kxz7tfdenju6t0xpnej4 echo npub1h8spmtw9m2huyv6v2j2qd5zv956z2zdugl6mgx02f2upffwpm3nqv0j4ps | nak fetch --relay wss://relay.nostr.band`, @@ -90,8 +91,11 @@ var fetch = &cli.Command{ } if authorHint != "" { - relays := sys.FetchOutboxRelays(ctx, authorHint, 3) for _, url := range relays { + sys.Hints.Save(authorHint, nostr.NormalizeURL(url), hints.LastInHint, nostr.Now()) + } + + for _, url := range sys.FetchOutboxRelays(ctx, authorHint, 3) { relays = append(relays, url) } } diff --git a/go.mod b/go.mod index 16a0e3f..13603d8 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/json-iterator/go v1.1.12 github.com/mailru/easyjson v0.9.0 github.com/markusmobius/go-dateparser v1.2.3 - github.com/nbd-wtf/go-nostr v0.47.3 + github.com/nbd-wtf/go-nostr v0.48.0 ) require ( diff --git a/go.sum b/go.sum index 7143878..7abdca9 100644 --- a/go.sum +++ b/go.sum @@ -117,8 +117,8 @@ 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.47.3 h1:KM86+0rLa8jPvt11akST1T0ffhrs6KpzvJ2CLYo765c= -github.com/nbd-wtf/go-nostr v0.47.3/go.mod h1:O6n8bv+KktkEs+4svL7KN/OSnOWB5LzcZbuKjxnpRD0= +github.com/nbd-wtf/go-nostr v0.48.0 h1:GYu6k6wRzSxYpra4pzMRk3R8xdUW8fac+trQtt6YD0o= +github.com/nbd-wtf/go-nostr v0.48.0/go.mod h1:O6n8bv+KktkEs+4svL7KN/OSnOWB5LzcZbuKjxnpRD0= 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= diff --git a/helpers.go b/helpers.go index 40ce39b..e364d8a 100644 --- a/helpers.go +++ b/helpers.go @@ -19,18 +19,15 @@ import ( "github.com/nbd-wtf/go-nostr/sdk" ) -var sys = sdk.NewSystem() +var sys *sdk.System + +var ( + hintsFilePath string + hintsFileExists bool +) var json = jsoniter.ConfigFastest -func init() { - sys.Pool = nostr.NewSimplePool(context.Background(), - nostr.WithRelayOptions( - nostr.WithRequestHeader(http.Header{textproto.CanonicalMIMEHeaderKey("user-agent"): {"nak/b"}}), - ), - ) -} - const ( LINE_PROCESSING_ERROR = iota ) diff --git a/main.go b/main.go index aae4e37..9b4e98d 100644 --- a/main.go +++ b/main.go @@ -2,9 +2,15 @@ package main import ( "context" + "net/http" + "net/textproto" "os" + "path/filepath" "github.com/fiatjaf/cli/v3" + "github.com/nbd-wtf/go-nostr" + "github.com/nbd-wtf/go-nostr/sdk" + "github.com/nbd-wtf/go-nostr/sdk/hints/memoryh" ) var version string = "debug" @@ -30,9 +36,15 @@ var app = &cli.Command{ serve, encrypt, decrypt, + outbox, }, Version: version, Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "config-path", + Hidden: true, + Persistent: true, + }, &cli.BoolFlag{ Name: "quiet", Usage: "do not print logs and info messages to stderr, use -qq to also not print anything to stdout", @@ -50,6 +62,59 @@ var app = &cli.Command{ }, }, }, + Before: func(ctx context.Context, c *cli.Command) error { + configPath := c.String("config-path") + if configPath == "" { + if home, err := os.UserHomeDir(); err == nil { + configPath = filepath.Join(home, ".config/nak") + } + } + if configPath != "" { + hintsFilePath = filepath.Join(configPath, "outbox/hints.db") + } + if hintsFilePath != "" { + if _, err := os.Stat(hintsFilePath); !os.IsNotExist(err) { + hintsFileExists = true + } + } + + if hintsFilePath != "" { + if data, err := os.ReadFile(hintsFilePath); err == nil { + hintsdb := memoryh.NewHintDB() + if err := json.Unmarshal(data, &hintsdb); err == nil { + sys = sdk.NewSystem( + sdk.WithHintsDB(hintsdb), + ) + goto systemOperational + } + } + } + + 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"}}), + ), + ) + + return nil + }, + After: func(ctx context.Context, c *cli.Command) error { + // save hints database on exit + if hintsFileExists { + data, err := json.Marshal(sys.Hints) + if err != nil { + return err + } + return os.WriteFile(hintsFilePath, data, 0644) + } + + return nil + }, } func main() { diff --git a/outbox.go b/outbox.go new file mode 100644 index 0000000..7ee859e --- /dev/null +++ b/outbox.go @@ -0,0 +1,68 @@ +package main + +import ( + "context" + "fmt" + "os" + "path/filepath" + + "github.com/fiatjaf/cli/v3" + "github.com/nbd-wtf/go-nostr" +) + +var outbox = &cli.Command{ + Name: "outbox", + Usage: "manage outbox relay hints database", + DisableSliceFlagSeparator: true, + Commands: []*cli.Command{ + { + Name: "init", + Usage: "initialize the outbox hints database", + DisableSliceFlagSeparator: true, + Action: func(ctx context.Context, c *cli.Command) error { + if hintsFileExists { + return nil + } + if hintsFilePath == "" { + return fmt.Errorf("couldn't find a place to store the hints, pass --config-path to fix.") + } + + if err := os.MkdirAll(filepath.Dir(hintsFilePath), 0777); err == nil { + if err := os.WriteFile(hintsFilePath, []byte("{}"), 0644); err != nil { + return fmt.Errorf("failed to create hints database: %w", err) + } + } + + log("initialized hints database at %s\n", hintsFilePath) + return nil + }, + }, + { + Name: "list", + Usage: "list outbox relays for a given pubkey", + ArgsUsage: "", + DisableSliceFlagSeparator: true, + Action: func(ctx context.Context, c *cli.Command) error { + if !hintsFileExists { + log("running with temporary fragile data.\n") + log("call `nak outbox init` to setup persistence.\n") + } + + if c.Args().Len() != 1 { + return fmt.Errorf("expected exactly one argument (pubkey)") + } + + pubkey := c.Args().First() + if !nostr.IsValidPublicKey(pubkey) { + return fmt.Errorf("invalid public key: %s", pubkey) + } + + for _, relay := range sys.FetchOutboxRelays(ctx, pubkey, 6) { + stdout(relay) + } + + return nil + }, + }, + }, +} From aa53f2cd60d56e68f619c74c318e33d6a6e83482 Mon Sep 17 00:00:00 2001 From: mleku Date: Tue, 21 Jan 2025 16:13:28 -0106 Subject: [PATCH 192/401] show env var in help, reset terminal mode correctly --- helpers_key.go | 2 +- main.go | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/helpers_key.go b/helpers_key.go index 36b929f..b100e75 100644 --- a/helpers_key.go +++ b/helpers_key.go @@ -20,7 +20,7 @@ import ( var defaultKeyFlags = []cli.Flag{ &cli.StringFlag{ Name: "sec", - Usage: "secret key to sign the event, as nsec, ncryptsec or hex, or a bunker URL", + Usage: "secret key to sign the event, as nsec, ncryptsec or hex, or a bunker URL, it is more secure to use the environment variable NOSTR_SECRET_KEY than this flag", DefaultText: "the key '1'", Aliases: []string{"connect"}, Category: CATEGORY_SIGNER, diff --git a/main.go b/main.go index 9b4e98d..f8e5a77 100644 --- a/main.go +++ b/main.go @@ -11,6 +11,7 @@ import ( "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/sdk" "github.com/nbd-wtf/go-nostr/sdk/hints/memoryh" + "github.com/fatih/color" ) var version string = "debug" @@ -118,8 +119,12 @@ var app = &cli.Command{ } func main() { + defer func() { + color.New(color.Reset).Println() + }() if err := app.Run(context.Background(), os.Args); err != nil { stdout(err) + color.New(color.Reset).Println() os.Exit(1) } } From 6b659c155243243531066a82fb1d80d37cc5b5cc Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 28 Jan 2025 23:43:47 -0300 Subject: [PATCH 193/401] fix @mleku unnecessary newlines. --- main.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/main.go b/main.go index f8e5a77..6d62471 100644 --- a/main.go +++ b/main.go @@ -7,11 +7,11 @@ import ( "os" "path/filepath" + "github.com/fatih/color" "github.com/fiatjaf/cli/v3" "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/sdk" "github.com/nbd-wtf/go-nostr/sdk/hints/memoryh" - "github.com/fatih/color" ) var version string = "debug" @@ -120,11 +120,11 @@ var app = &cli.Command{ func main() { defer func() { - color.New(color.Reset).Println() + color.New(color.Reset).Print() }() if err := app.Run(context.Background(), os.Args); err != nil { stdout(err) - color.New(color.Reset).Println() + color.New(color.Reset).Print() os.Exit(1) } } From 943e8835f9da896d03bd02c755f635892954ed8b Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 28 Jan 2025 19:25:39 -0300 Subject: [PATCH 194/401] nak wallet --- go.mod | 12 ++-- go.sum | 27 ++++++--- main.go | 1 + wallet.go | 162 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 189 insertions(+), 13 deletions(-) create mode 100644 wallet.go diff --git a/go.mod b/go.mod index 13603d8..2d4555f 100644 --- a/go.mod +++ b/go.mod @@ -9,18 +9,19 @@ require ( github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 github.com/fatih/color v1.16.0 github.com/fiatjaf/cli/v3 v3.0.0-20240723181502-e7dd498b16ae - github.com/fiatjaf/eventstore v0.14.2 - github.com/fiatjaf/khatru v0.14.0 + github.com/fiatjaf/eventstore v0.15.0 + github.com/fiatjaf/khatru v0.15.0 github.com/json-iterator/go v1.1.12 github.com/mailru/easyjson v0.9.0 github.com/markusmobius/go-dateparser v1.2.3 - github.com/nbd-wtf/go-nostr v0.48.0 + github.com/nbd-wtf/go-nostr v0.49.0 ) require ( fiatjaf.com/lib v0.2.0 // indirect github.com/andybalholm/brotli v1.0.5 // indirect - github.com/btcsuite/btcd/btcutil v1.1.3 // indirect + github.com/btcsuite/btcd v0.24.2 // indirect + github.com/btcsuite/btcd/btcutil v1.1.5 // indirect github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chzyer/logex v1.1.10 // indirect @@ -30,7 +31,9 @@ require ( github.com/dgraph-io/ristretto v1.0.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/elliotchance/pie/v2 v2.7.0 // indirect + github.com/elnosh/gonuts v0.3.1-0.20250123162555-7c0381a585e3 // indirect github.com/fasthttp/websocket v1.5.7 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/graph-gophers/dataloader/v7 v7.1.0 // indirect github.com/hablullah/go-hijri v1.0.2 // indirect github.com/hablullah/go-juliandays v1.0.0 // indirect @@ -53,6 +56,7 @@ require ( github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.51.0 // indirect github.com/wasilibs/go-re2 v1.3.0 // indirect + github.com/x448/float16 v0.8.4 // indirect golang.org/x/crypto v0.32.0 // indirect golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect golang.org/x/net v0.34.0 // indirect diff --git a/go.sum b/go.sum index 7abdca9..0f3a4f7 100644 --- a/go.sum +++ b/go.sum @@ -7,15 +7,17 @@ github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M= -github.com/btcsuite/btcd v0.23.0/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY= +github.com/btcsuite/btcd v0.23.5-0.20231215221805-96c9fd8078fd/go.mod h1:nm3Bko6zh6bWP60UxwoT5LzdGJsQJaPo6HjduXq9p6A= +github.com/btcsuite/btcd v0.24.2 h1:aLmxPguqxza+4ag8R1I2nnJjSu2iFn/kqtHTIImswcY= +github.com/btcsuite/btcd v0.24.2/go.mod h1:5C8ChTkl5ejr3WHj8tkQSCmydiMEPB0ZhQhehpq7Dgg= github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA= github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= github.com/btcsuite/btcd/btcec/v2 v2.3.4 h1:3EJjcN70HCu/mwqlUsGK8GcNVyLVxFDlWurTXGPFfiQ= github.com/btcsuite/btcd/btcec/v2 v2.3.4/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A= github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE= -github.com/btcsuite/btcd/btcutil v1.1.3 h1:xfbtw8lwpp0G6NwSHb+UE67ryTFHJAiNuipusjXSohQ= -github.com/btcsuite/btcd/btcutil v1.1.3/go.mod h1:UR7dsSJzJUfMmFiiLlIrMq1lS9jh9EdCV7FStZSnpi0= +github.com/btcsuite/btcd/btcutil v1.1.5 h1:+wER79R5670vs/ZusMTF1yTcRYE5GUsFbdjdisflzM8= +github.com/btcsuite/btcd/btcutil v1.1.5/go.mod h1:PSZZ4UitpLBWzxGd5VGOrLnmOjtPP/a6HaFo12zMs00= github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 h1:59Kx4K6lzOW5w6nFlA0v5+lk/6sjybR934QNHSJZPTQ= @@ -58,18 +60,22 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/elliotchance/pie/v2 v2.7.0 h1:FqoIKg4uj0G/CrLGuMS9ejnFKa92lxE1dEgBD3pShXg= github.com/elliotchance/pie/v2 v2.7.0/go.mod h1:18t0dgGFH006g4eVdDtWfgFZPQEgl10IoEO8YWEq3Og= +github.com/elnosh/gonuts v0.3.1-0.20250123162555-7c0381a585e3 h1:k7evIqJ2BtFn191DgY/b03N2bMYA/iQwzr4f/uHYn20= +github.com/elnosh/gonuts v0.3.1-0.20250123162555-7c0381a585e3/go.mod h1:vgZomh4YQk7R3w4ltZc0sHwCmndfHkuX6V4sga/8oNs= github.com/fasthttp/websocket v1.5.7 h1:0a6o2OfeATvtGgoMKleURhLT6JqWPg7fYfWnH4KHau4= github.com/fasthttp/websocket v1.5.7/go.mod h1:bC4fxSono9czeXHQUVKxsC0sNjbm7lPJR04GDFqClfU= 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/cli/v3 v3.0.0-20240723181502-e7dd498b16ae h1:0B/1dU3YECIbPoBIRTQ4c0scZCNz9TVHtQpiODGrTTo= github.com/fiatjaf/cli/v3 v3.0.0-20240723181502-e7dd498b16ae/go.mod h1:aAWPO4bixZZxPtOnH6K3q4GbQ0jftUNDW9Oa861IRew= -github.com/fiatjaf/eventstore v0.14.2 h1:1XOLLEBCGQNQ1rLaO8mcfTQydcetPOqb/uRK8zOnOSI= -github.com/fiatjaf/eventstore v0.14.2/go.mod h1:XmYZSdFxsY+cwfpdzVG61M7pemcmqlZwDfDGFC8WwWo= -github.com/fiatjaf/khatru v0.14.0 h1:zpWlAA87XBpDKBPIDbAuNw/HpKXzyt5XHVDbSvUbmDo= -github.com/fiatjaf/khatru v0.14.0/go.mod h1:uxE5e8DBXPZqbHjr/gfatQas5bEJIMmsOCDcdF4LoRQ= +github.com/fiatjaf/eventstore v0.15.0 h1:5UXe0+vIb30/cYcOWipks8nR3g+X8W224TFy5yPzivk= +github.com/fiatjaf/eventstore v0.15.0/go.mod h1:KAsld5BhkmSck48aF11Txu8X+OGNmoabw4TlYVWqInc= +github.com/fiatjaf/khatru v0.15.0 h1:0aLWiTrdzoKD4WmW35GWL/Jsn4dACCUw325JKZg/AmI= +github.com/fiatjaf/khatru v0.15.0/go.mod h1:GBQJXZpitDatXF9RookRXcWB5zCJclCE4ufDK3jk80g= 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= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= @@ -82,6 +88,7 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/graph-gophers/dataloader/v7 v7.1.0 h1:Wn8HGF/q7MNXcvfaBnLEPEFJttVHR8zuEqP1obys/oc= github.com/graph-gophers/dataloader/v7 v7.1.0/go.mod h1:1bKE0Dm6OUcTB/OAuYVOZctgIz7Q3d0XrYtlIzTgg6Q= github.com/hablullah/go-hijri v1.0.2 h1:drT/MZpSZJQXo7jftf5fthArShcaMtsal0Zf/dnmp6k= @@ -117,8 +124,8 @@ 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.48.0 h1:GYu6k6wRzSxYpra4pzMRk3R8xdUW8fac+trQtt6YD0o= -github.com/nbd-wtf/go-nostr v0.48.0/go.mod h1:O6n8bv+KktkEs+4svL7KN/OSnOWB5LzcZbuKjxnpRD0= +github.com/nbd-wtf/go-nostr v0.49.0 h1:/oDdOaZf2oFP0NDh48MFNMjiEO99hNxS+P+OrvIDRsc= +github.com/nbd-wtf/go-nostr v0.49.0/go.mod h1:M50QnhkraC5Ol93v3jqxSMm1aGxUQm5mlmkYw5DJzh8= 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= @@ -161,6 +168,8 @@ github.com/wasilibs/go-re2 v1.3.0 h1:LFhBNzoStM3wMie6rN2slD1cuYH2CGiHpvNL3UtcsMw github.com/wasilibs/go-re2 v1.3.0/go.mod h1:AafrCXVvGRJJOImMajgJ2M7rVmWyisVK7sFshbxnVrg= github.com/wasilibs/nottinygc v0.4.0 h1:h1TJMihMC4neN6Zq+WKpLxgd9xCFMw7O9ETLwY2exJQ= github.com/wasilibs/nottinygc v0.4.0/go.mod h1:oDcIotskuYNMpqMF23l7Z8uzD4TC0WXHK8jetlB3HIo= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= diff --git a/main.go b/main.go index 6d62471..390548c 100644 --- a/main.go +++ b/main.go @@ -38,6 +38,7 @@ var app = &cli.Command{ encrypt, decrypt, outbox, + wallet, }, Version: version, Flags: []cli.Flag{ diff --git a/wallet.go b/wallet.go new file mode 100644 index 0000000..6f11fa4 --- /dev/null +++ b/wallet.go @@ -0,0 +1,162 @@ +package main + +import ( + "context" + "fmt" + "strconv" + + "github.com/fiatjaf/cli/v3" + "github.com/nbd-wtf/go-nostr/nip60" +) + +func prepareWallet(ctx context.Context, c *cli.Command) (*nip60.WalletStash, error) { + kr, _, err := gatherKeyerFromArguments(ctx, c) + if err != nil { + return nil, err + } + + pk, err := kr.GetPublicKey(ctx) + if err != nil { + return nil, err + } + + relays := sys.FetchOutboxRelays(ctx, pk, 3) + wl := nip60.LoadStash(ctx, kr, sys.Pool, relays) + if wl == nil { + return nil, fmt.Errorf("error loading wallet stash") + } + + go func() { + for err := range wl.Processed { + if err == nil { + // event processed ok + } else { + log("processing error: %s\n", err) + } + } + }() + + go func() { + for evt := range wl.Changes { + for res := range sys.Pool.PublishMany(ctx, relays, evt) { + if res.Error != nil { + log("error saving kind:%d event to %s: %s\n", evt.Kind, res.RelayURL, err) + } else { + log("saved kind:%d event to %s\n", evt.Kind, res.RelayURL) + } + } + } + }() + + <-wl.Stable + + return wl, nil +} + +var wallet = &cli.Command{ + Name: "wallet", + Usage: "manage NIP-60 Cashu wallets", + DisableSliceFlagSeparator: true, + Flags: defaultKeyFlags, + ArgsUsage: "", + Commands: []*cli.Command{ + { + Name: "list", + Usage: "list existing Cashu wallets", + DisableSliceFlagSeparator: true, + Action: func(ctx context.Context, c *cli.Command) error { + wl, err := prepareWallet(ctx, c) + if err != nil { + return err + } + + for w := range wl.Wallets() { + stdout(w.DisplayName(), w.Balance()) + } + + return nil + }, + }, + { + Name: "tokens", + Usage: "list existing tokens in this wallet", + DisableSliceFlagSeparator: true, + Action: func(ctx context.Context, c *cli.Command) error { + wl, err := prepareWallet(ctx, c) + if err != nil { + return err + } + + args := c.Args().Slice() + if len(args) != 1 { + return fmt.Errorf("must be called as `nak wallet tokens") + } + + w := wl.EnsureWallet(args[0]) + + for _, token := range w.Tokens { + stdout(token.ID(), token.Proofs.Amount(), token.Mint) + } + + return nil + }, + }, + { + Name: "receive", + Usage: "receive a cashu token", + ArgsUsage: "", + DisableSliceFlagSeparator: true, + Action: func(ctx context.Context, c *cli.Command) error { + args := c.Args().Slice() + if len(args) != 2 { + return fmt.Errorf("must be called as `nak wallet receive ") + } + + wl, err := prepareWallet(ctx, c) + if err != nil { + return err + } + + w := wl.EnsureWallet(args[0]) + + if err := w.ReceiveToken(ctx, args[1]); err != nil { + return err + } + + return nil + }, + }, + { + Name: "send", + Usage: "send a cashu token", + ArgsUsage: "", + DisableSliceFlagSeparator: true, + Action: func(ctx context.Context, c *cli.Command) error { + args := c.Args().Slice() + if len(args) != 2 { + return fmt.Errorf("must be called as `nak wallet send ") + } + amount, err := strconv.ParseUint(args[1], 10, 64) + if err != nil { + return fmt.Errorf("amount '%s' is invalid", args[1]) + } + + wl, err := prepareWallet(ctx, c) + if err != nil { + return err + } + + w := wl.EnsureWallet(args[0]) + + token, err := w.SendToken(ctx, amount) + if err != nil { + return err + } + + stdout(token) + + return nil + }, + }, + }, +} From 6e43a6b7334e23bce394d21f2e3b6874ca1b6cfb Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Wed, 29 Jan 2025 19:13:30 -0300 Subject: [PATCH 195/401] reword NIP-XX to nipXX everywhere. --- bunker.go | 2 +- count.go | 2 +- encode.go | 4 ++-- event.go | 4 ++-- helpers_key.go | 2 +- key.go | 4 ++-- req.go | 8 ++++---- wallet.go | 2 +- 8 files changed, 14 insertions(+), 14 deletions(-) diff --git a/bunker.go b/bunker.go index 179ba9e..bc2b179 100644 --- a/bunker.go +++ b/bunker.go @@ -19,7 +19,7 @@ import ( var bunker = &cli.Command{ Name: "bunker", - Usage: "starts a NIP-46 signer daemon with the given --sec key", + Usage: "starts a nip46 signer daemon with the given --sec key", ArgsUsage: "[relay...]", Description: ``, DisableSliceFlagSeparator: true, diff --git a/count.go b/count.go index 704a6a7..1ca9596 100644 --- a/count.go +++ b/count.go @@ -15,7 +15,7 @@ import ( var count = &cli.Command{ Name: "count", Usage: "generates encoded COUNT messages and optionally use them to talk to relays", - Description: `outputs a NIP-45 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, Flags: []cli.Flag{ &cli.StringSliceFlag{ diff --git a/encode.go b/encode.go index f30feed..e1e44a0 100644 --- a/encode.go +++ b/encode.go @@ -153,7 +153,7 @@ var encode = &cli.Command{ }, { Name: "naddr", - Usage: "generate codes for NIP-33 parameterized replaceable events", + Usage: "generate codes for addressable events", Flags: []cli.Flag{ &cli.StringFlag{ Name: "identifier", @@ -189,7 +189,7 @@ var encode = &cli.Command{ kind := c.Int("kind") if kind < 30000 || kind >= 40000 { - return fmt.Errorf("kind must be between 30000 and 39999, as per NIP-16, got %d", kind) + return fmt.Errorf("kind must be between 30000 and 39999, got %d", kind) } if d == "" { diff --git a/event.go b/event.go index b87a34c..2c5f590 100644 --- a/event.go +++ b/event.go @@ -65,7 +65,7 @@ example: // ~~~ &cli.UintFlag{ Name: "pow", - Usage: "NIP-13 difficulty to target when doing hash work on the event id", + Usage: "nip13 difficulty to target when doing hash work on the event id", Category: CATEGORY_EXTRAS, }, &cli.BoolFlag{ @@ -75,7 +75,7 @@ example: }, &cli.BoolFlag{ Name: "auth", - Usage: "always perform NIP-42 \"AUTH\" when facing an \"auth-required: \" rejection and try again", + Usage: "always perform nip42 \"AUTH\" when facing an \"auth-required: \" rejection and try again", Category: CATEGORY_EXTRAS, }, &cli.BoolFlag{ diff --git a/helpers_key.go b/helpers_key.go index b100e75..35511ab 100644 --- a/helpers_key.go +++ b/helpers_key.go @@ -32,7 +32,7 @@ var defaultKeyFlags = []cli.Flag{ }, &cli.StringFlag{ Name: "connect-as", - Usage: "private key to use when communicating with NIP-46 bunkers", + Usage: "private key to use when communicating with nip46 bunkers", DefaultText: "a random key", Category: CATEGORY_SIGNER, Sources: cli.EnvVars("NOSTR_CLIENT_KEY"), diff --git a/key.go b/key.go index 4a160d0..90ac4c9 100644 --- a/key.go +++ b/key.go @@ -71,7 +71,7 @@ var public = &cli.Command{ var encryptKey = &cli.Command{ Name: "encrypt", Usage: "encrypts a secret key and prints an ncryptsec code", - Description: `uses the NIP-49 standard.`, + Description: `uses the nip49 standard.`, ArgsUsage: " ", DisableSliceFlagSeparator: true, Flags: []cli.Flag{ @@ -110,7 +110,7 @@ var encryptKey = &cli.Command{ var decryptKey = &cli.Command{ Name: "decrypt", Usage: "takes an ncrypsec and a password and decrypts it into an nsec", - Description: `uses the NIP-49 standard.`, + Description: `uses the nip49 standard.`, ArgsUsage: " ", DisableSliceFlagSeparator: true, Action: func(ctx context.Context, c *cli.Command) error { diff --git a/req.go b/req.go index e0ea0f9..09cae23 100644 --- a/req.go +++ b/req.go @@ -19,7 +19,7 @@ const ( var req = &cli.Command{ Name: "req", Usage: "generates encoded REQ messages and optionally use them to talk to relays", - Description: `outputs a NIP-01 Nostr filter. when a relay is not given, will print the filter, otherwise will connect to the given relay and send the filter. + Description: `outputs a nip01 Nostr filter. when a relay is not given, will print the filter, otherwise will connect to the given relay and send the filter. example: nak req -k 1 -l 15 wss://nostr.wine wss://nostr-pub.wellorder.net @@ -57,12 +57,12 @@ example: }, &cli.BoolFlag{ Name: "auth", - Usage: "always perform NIP-42 \"AUTH\" when facing an \"auth-required: \" rejection and try again", + Usage: "always perform nip42 \"AUTH\" when facing an \"auth-required: \" rejection and try again", }, &cli.BoolFlag{ Name: "force-pre-auth", Aliases: []string{"fpa"}, - Usage: "after connecting, for a NIP-42 \"AUTH\" message to be received, act on it and only then send the \"REQ\"", + Usage: "after connecting, for a nip42 \"AUTH\" message to be received, act on it and only then send the \"REQ\"", Category: CATEGORY_SIGNER, }, )..., @@ -210,7 +210,7 @@ var reqFilterFlags = []cli.Flag{ }, &cli.StringFlag{ Name: "search", - Usage: "a NIP-50 search query, use it only with relays that explicitly support it", + Usage: "a nip50 search query, use it only with relays that explicitly support it", Category: CATEGORY_FILTER_ATTRIBUTES, }, } diff --git a/wallet.go b/wallet.go index 6f11fa4..6f158e3 100644 --- a/wallet.go +++ b/wallet.go @@ -55,7 +55,7 @@ func prepareWallet(ctx context.Context, c *cli.Command) (*nip60.WalletStash, err var wallet = &cli.Command{ Name: "wallet", - Usage: "manage NIP-60 Cashu wallets", + Usage: "manage nip60 Cashu wallets", DisableSliceFlagSeparator: true, Flags: defaultKeyFlags, ArgsUsage: "", From 81571c69529b875a1421e28c9b079b202854cb9b Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Thu, 30 Jan 2025 16:05:51 -0300 Subject: [PATCH 196/401] global bold/italic helpers. --- bunker.go | 14 ++++++-------- helpers.go | 12 ++++++++++++ serve.go | 11 ++++------- 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/bunker.go b/bunker.go index bc2b179..29eaa69 100644 --- a/bunker.go +++ b/bunker.go @@ -83,8 +83,6 @@ var bunker = &cli.Command{ return err } npub, _ := nip19.EncodePublicKey(pubkey) - bold := color.New(color.Bold).Sprint - italic := color.New(color.Italic).Sprint // this function will be called every now and then printBunkerInfo := func() { @@ -93,12 +91,12 @@ var bunker = &cli.Command{ authorizedKeysStr := "" if len(authorizedKeys) != 0 { - authorizedKeysStr = "\n authorized keys:\n - " + italic(strings.Join(authorizedKeys, "\n - ")) + authorizedKeysStr = "\n authorized keys:\n - " + colors.italic(strings.Join(authorizedKeys, "\n - ")) } authorizedSecretsStr := "" if len(authorizedSecrets) != 0 { - authorizedSecretsStr = "\n authorized secrets:\n - " + italic(strings.Join(authorizedSecrets, "\n - ")) + authorizedSecretsStr = "\n authorized secrets:\n - " + colors.italic(strings.Join(authorizedSecrets, "\n - ")) } preauthorizedFlags := "" @@ -130,13 +128,13 @@ var bunker = &cli.Command{ ) log("listening at %v:\n pubkey: %s \n npub: %s%s%s\n to restart: %s\n bunker: %s\n\n", - bold(relayURLs), - bold(pubkey), - bold(npub), + colors.bold(relayURLs), + colors.bold(pubkey), + colors.bold(npub), authorizedKeysStr, authorizedSecretsStr, color.CyanString(restartCommand), - bold(bunkerURI), + colors.bold(bunkerURI), ) } printBunkerInfo() diff --git a/helpers.go b/helpers.go index e364d8a..38ab879 100644 --- a/helpers.go +++ b/helpers.go @@ -212,3 +212,15 @@ func randString(n int) string { func leftPadKey(k string) string { return strings.Repeat("0", 64-len(k)) + k } + +var colors = struct { + italic func(...any) string + italicf func(string, ...any) string + bold func(...any) string + boldf func(string, ...any) string +}{ + color.New(color.Italic).Sprint, + color.New(color.Italic).Sprintf, + color.New(color.Bold).Sprint, + color.New(color.Bold).Sprintf, +} diff --git a/serve.go b/serve.go index c2a0a7c..b06f61c 100644 --- a/serve.go +++ b/serve.go @@ -81,24 +81,21 @@ var serve = &cli.Command{ exited <- err }() - bold := color.New(color.Bold).Sprintf - italic := color.New(color.Italic).Sprint - var printStatus func() // relay logging rl.RejectFilter = append(rl.RejectFilter, func(ctx context.Context, filter nostr.Filter) (reject bool, msg string) { - log(" got %s %v\n", color.HiYellowString("request"), italic(filter)) + 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) { - log(" got %s %v\n", color.HiCyanString("count request"), italic(filter)) + 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) { - log(" got %s %v\n", color.BlueString("event"), italic(event)) + log(" got %s %v\n", color.BlueString("event"), colors.italic(event)) printStatus() return false, "" }) @@ -118,7 +115,7 @@ var serve = &cli.Command{ } <-started - log("%s relay running at %s\n", color.HiRedString(">"), bold("ws://%s:%d", hostname, port)) + log("%s relay running at %s\n", color.HiRedString(">"), colors.boldf("ws://%s:%d", hostname, port)) return <-exited }, From df5ebd3f56a4e80ac8005a0d7f58bebc3d51eee9 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Thu, 30 Jan 2025 16:06:16 -0300 Subject: [PATCH 197/401] global verbose flag. --- helpers.go | 10 +++++----- main.go | 13 +++++++++++++ 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/helpers.go b/helpers.go index 38ab879..8caaef4 100644 --- a/helpers.go +++ b/helpers.go @@ -32,11 +32,11 @@ const ( LINE_PROCESSING_ERROR = iota ) -var log = func(msg string, args ...any) { - fmt.Fprintf(color.Error, msg, args...) -} - -var stdout = fmt.Println +var ( + log = func(msg string, args ...any) { fmt.Fprintf(color.Error, msg, args...) } + logverbose = func(msg string, args ...any) {} // by default do nothing + stdout = fmt.Println +) func isPiped() bool { stat, _ := os.Stdin.Stat() diff --git a/main.go b/main.go index 390548c..e08b059 100644 --- a/main.go +++ b/main.go @@ -63,6 +63,19 @@ var app = &cli.Command{ return nil }, }, + &cli.BoolFlag{ + Name: "verbose", + Usage: "print more stuff than normally", + Aliases: []string{"v"}, + Persistent: true, + Action: func(ctx context.Context, c *cli.Command, b bool) error { + v := c.Count("verbose") + if v >= 1 { + logverbose = log + } + return nil + }, + }, }, Before: func(ctx context.Context, c *cli.Command) error { configPath := c.String("config-path") From e2dd3ca544d1420543f49752a6da5eb8dcc578e5 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Thu, 30 Jan 2025 19:58:17 -0300 Subject: [PATCH 198/401] fix -v verbose flag (it was being used by the default --version flag). --- helpers.go | 2 ++ main.go | 13 ++++++++----- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/helpers.go b/helpers.go index 8caaef4..3a3d9ce 100644 --- a/helpers.go +++ b/helpers.go @@ -214,11 +214,13 @@ func leftPadKey(k string) string { } var colors = struct { + reset func(...any) (int, error) italic func(...any) string italicf func(string, ...any) string bold func(...any) string boldf func(string, ...any) string }{ + color.New(color.Reset).Print, color.New(color.Italic).Sprint, color.New(color.Italic).Sprintf, color.New(color.Bold).Sprint, diff --git a/main.go b/main.go index e08b059..96d05c3 100644 --- a/main.go +++ b/main.go @@ -7,7 +7,6 @@ import ( "os" "path/filepath" - "github.com/fatih/color" "github.com/fiatjaf/cli/v3" "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/sdk" @@ -133,12 +132,16 @@ var app = &cli.Command{ } func main() { - defer func() { - color.New(color.Reset).Print() - }() + defer colors.reset() + + cli.VersionFlag = &cli.BoolFlag{ + Name: "version", + Usage: "prints the version", + } + if err := app.Run(context.Background(), os.Args); err != nil { stdout(err) - color.New(color.Reset).Print() + colors.reset() os.Exit(1) } } From 12a1f1563edaaafa3d947954661d487c8f8af8fa Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Thu, 30 Jan 2025 19:59:54 -0300 Subject: [PATCH 199/401] wallet pay and fix missing tokens because the program was exiting before they were saved. --- go.mod | 2 +- go.sum | 4 +- wallet.go | 178 +++++++++++++++++++++++++++++++++++++++++++----------- 3 files changed, 145 insertions(+), 39 deletions(-) diff --git a/go.mod b/go.mod index 2d4555f..3a7d4da 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/json-iterator/go v1.1.12 github.com/mailru/easyjson v0.9.0 github.com/markusmobius/go-dateparser v1.2.3 - github.com/nbd-wtf/go-nostr v0.49.0 + github.com/nbd-wtf/go-nostr v0.49.2 ) require ( diff --git a/go.sum b/go.sum index 0f3a4f7..82f770f 100644 --- a/go.sum +++ b/go.sum @@ -124,8 +124,8 @@ 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.49.0 h1:/oDdOaZf2oFP0NDh48MFNMjiEO99hNxS+P+OrvIDRsc= -github.com/nbd-wtf/go-nostr v0.49.0/go.mod h1:M50QnhkraC5Ol93v3jqxSMm1aGxUQm5mlmkYw5DJzh8= +github.com/nbd-wtf/go-nostr v0.49.2 h1:nOiwo/M20bYczU8pwLtosR+hGcSIaCdgoqiy6DTYJ+M= +github.com/nbd-wtf/go-nostr v0.49.2/go.mod h1:M50QnhkraC5Ol93v3jqxSMm1aGxUQm5mlmkYw5DJzh8= 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= diff --git a/wallet.go b/wallet.go index 6f158e3..b2bfa88 100644 --- a/wallet.go +++ b/wallet.go @@ -4,68 +4,118 @@ import ( "context" "fmt" "strconv" + "strings" + "github.com/fatih/color" "github.com/fiatjaf/cli/v3" + "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/nip60" ) -func prepareWallet(ctx context.Context, c *cli.Command) (*nip60.WalletStash, error) { +func prepareWallet(ctx context.Context, c *cli.Command) (*nip60.WalletStash, func(), error) { kr, _, err := gatherKeyerFromArguments(ctx, c) if err != nil { - return nil, err + return nil, nil, err } pk, err := kr.GetPublicKey(ctx) if err != nil { - return nil, err + return nil, nil, err } relays := sys.FetchOutboxRelays(ctx, pk, 3) wl := nip60.LoadStash(ctx, kr, sys.Pool, relays) if wl == nil { - return nil, fmt.Errorf("error loading wallet stash") + return nil, nil, fmt.Errorf("error loading wallet stash") } - go func() { - for err := range wl.Processed { - if err == nil { - // event processed ok - } else { - log("processing error: %s\n", err) - } + wl.Processed = func(evt *nostr.Event, err error) { + if err == nil { + logverbose("processed event %s\n", evt) + } else { + log("error processing event %s: %s\n", evt, err) } - }() + } - go func() { - for evt := range wl.Changes { - for res := range sys.Pool.PublishMany(ctx, relays, evt) { - if res.Error != nil { - log("error saving kind:%d event to %s: %s\n", evt.Kind, res.RelayURL, err) - } else { - log("saved kind:%d event to %s\n", evt.Kind, res.RelayURL) - } + wl.PublishUpdate = func(event nostr.Event, deleted, received, change *nip60.Token, isHistory bool) { + desc := "wallet" + if received != nil { + desc = fmt.Sprintf("received from %s with %d proofs totalling %d", + received.Mint, len(received.Proofs), received.Proofs.Amount()) + } else if change != nil { + desc = fmt.Sprintf("change from %s with %d proofs totalling %d", + change.Mint, len(change.Proofs), change.Proofs.Amount()) + } else if deleted != nil { + desc = fmt.Sprintf("deleting a used token from %s with %d proofs totalling %d", + deleted.Mint, len(deleted.Proofs), deleted.Proofs.Amount()) + } else if isHistory { + desc = "history entry" + } + + log("- saving kind:%d event (%s)... ", event.Kind, desc) + first := true + for res := range sys.Pool.PublishMany(ctx, relays, event) { + cleanUrl, ok := strings.CutPrefix(res.RelayURL, "wss://") + if !ok { + cleanUrl = res.RelayURL + } + + if !first { + log(", ") + } + first = false + + if res.Error != nil { + log("%s: %s", color.RedString(cleanUrl), res.Error) + } else { + log("%s: ok", color.GreenString(cleanUrl)) } } - }() + log("\n") + } <-wl.Stable - return wl, nil + return wl, func() { + wl.Close() + }, nil } var wallet = &cli.Command{ Name: "wallet", - Usage: "manage nip60 Cashu wallets", + Usage: "manage nip60 cashu wallets (see subcommands)", + Description: "all wallet data is stored on Nostr relays, signed and encrypted with the given key, and reloaded again from relays on every call.\n\nthe same data can be accessed by other compatible nip60 clients.", DisableSliceFlagSeparator: true, Flags: defaultKeyFlags, ArgsUsage: "", + Action: func(ctx context.Context, c *cli.Command) error { + args := c.Args().Slice() + if len(args) != 1 { + return fmt.Errorf("must be called as `nak wallet ") + } + + wl, closewl, err := prepareWallet(ctx, c) + if err != nil { + return err + } + + for w := range wl.Wallets() { + if w.Identifier == args[0] { + stdout(w.DisplayName(), w.Balance()) + break + } + } + + closewl() + return nil + }, Commands: []*cli.Command{ { Name: "list", - Usage: "list existing Cashu wallets", + Usage: "lists existing wallets with their balances", DisableSliceFlagSeparator: true, Action: func(ctx context.Context, c *cli.Command) error { - wl, err := prepareWallet(ctx, c) + wl, closewl, err := prepareWallet(ctx, c) if err != nil { return err } @@ -74,15 +124,16 @@ var wallet = &cli.Command{ stdout(w.DisplayName(), w.Balance()) } + closewl() return nil }, }, { Name: "tokens", - Usage: "list existing tokens in this wallet", + Usage: "lists existing tokens in this wallet with their mints and aggregated amounts", DisableSliceFlagSeparator: true, Action: func(ctx context.Context, c *cli.Command) error { - wl, err := prepareWallet(ctx, c) + wl, closewl, err := prepareWallet(ctx, c) if err != nil { return err } @@ -92,18 +143,19 @@ var wallet = &cli.Command{ return fmt.Errorf("must be called as `nak wallet tokens") } - w := wl.EnsureWallet(args[0]) + w := wl.EnsureWallet(ctx, args[0]) for _, token := range w.Tokens { - stdout(token.ID(), token.Proofs.Amount(), token.Mint) + stdout(token.ID(), token.Proofs.Amount(), strings.Split(token.Mint, "://")[1]) } + closewl() return nil }, }, { Name: "receive", - Usage: "receive a cashu token", + Usage: "takes a cashu token string as an argument and adds it to the wallet", ArgsUsage: "", DisableSliceFlagSeparator: true, Action: func(ctx context.Context, c *cli.Command) error { @@ -112,25 +164,32 @@ var wallet = &cli.Command{ return fmt.Errorf("must be called as `nak wallet receive ") } - wl, err := prepareWallet(ctx, c) + wl, closewl, err := prepareWallet(ctx, c) if err != nil { return err } - w := wl.EnsureWallet(args[0]) + w := wl.EnsureWallet(ctx, args[0]) if err := w.ReceiveToken(ctx, args[1]); err != nil { return err } + closewl() return nil }, }, { Name: "send", - Usage: "send a cashu token", + Usage: "prints a cashu token with the given amount for sending to someone else", ArgsUsage: "", DisableSliceFlagSeparator: true, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "mint", + Usage: "send from a specific mint", + }, + }, Action: func(ctx context.Context, c *cli.Command) error { args := c.Args().Slice() if len(args) != 2 { @@ -141,20 +200,67 @@ var wallet = &cli.Command{ return fmt.Errorf("amount '%s' is invalid", args[1]) } - wl, err := prepareWallet(ctx, c) + wl, closewl, err := prepareWallet(ctx, c) if err != nil { return err } - w := wl.EnsureWallet(args[0]) + w := wl.EnsureWallet(ctx, args[0]) - token, err := w.SendToken(ctx, amount) + opts := make([]nip60.SendOption, 0, 1) + if mint := c.String("mint"); mint != "" { + mint = "http" + nostr.NormalizeURL(mint)[2:] + opts = append(opts, nip60.WithMint(mint)) + } + token, err := w.SendToken(ctx, amount, opts...) if err != nil { return err } stdout(token) + closewl() + return nil + }, + }, + { + Name: "pay", + Usage: "pays a bolt11 lightning invoice and outputs the preimage", + ArgsUsage: "", + DisableSliceFlagSeparator: true, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "mint", + Usage: "pay from a specific mint", + }, + }, + Action: func(ctx context.Context, c *cli.Command) error { + args := c.Args().Slice() + if len(args) != 2 { + return fmt.Errorf("must be called as `nak wallet pay ") + } + + wl, closewl, err := prepareWallet(ctx, c) + if err != nil { + return err + } + + w := wl.EnsureWallet(ctx, args[0]) + + opts := make([]nip60.SendOption, 0, 1) + if mint := c.String("mint"); mint != "" { + mint = "http" + nostr.NormalizeURL(mint)[2:] + opts = append(opts, nip60.WithMint(mint)) + } + + preimage, err := w.PayBolt11(ctx, args[1], opts...) + if err != nil { + return err + } + + stdout(preimage) + + closewl() return nil }, }, From dba3f648adb8ccd52997d8767d06783096fddf31 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Fri, 31 Jan 2025 23:44:03 -0300 Subject: [PATCH 200/401] experimental mcp server. --- go.mod | 6 ++- go.sum | 4 ++ main.go | 1 + mcp.go | 164 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 mcp.go diff --git a/go.mod b/go.mod index 3a7d4da..34a475a 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,8 @@ module github.com/fiatjaf/nak -go 1.23.1 +go 1.23.3 + +toolchain go1.23.4 require ( github.com/bep/debounce v1.2.1 @@ -13,6 +15,7 @@ require ( github.com/fiatjaf/khatru v0.15.0 github.com/json-iterator/go v1.1.12 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.49.2 ) @@ -34,6 +37,7 @@ require ( github.com/elnosh/gonuts v0.3.1-0.20250123162555-7c0381a585e3 // indirect github.com/fasthttp/websocket v1.5.7 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/graph-gophers/dataloader/v7 v7.1.0 // indirect github.com/hablullah/go-hijri v1.0.2 // indirect github.com/hablullah/go-juliandays v1.0.0 // indirect diff --git a/go.sum b/go.sum index 82f770f..813b5dd 100644 --- a/go.sum +++ b/go.sum @@ -88,6 +88,8 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/graph-gophers/dataloader/v7 v7.1.0 h1:Wn8HGF/q7MNXcvfaBnLEPEFJttVHR8zuEqP1obys/oc= github.com/graph-gophers/dataloader/v7 v7.1.0/go.mod h1:1bKE0Dm6OUcTB/OAuYVOZctgIz7Q3d0XrYtlIzTgg6Q= @@ -112,6 +114,8 @@ github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo= github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/mark3labs/mcp-go v0.8.3 h1:IzlyN8BaP4YwUMUDqxOGJhGdZXEDQiAPX43dNPgnzrg= +github.com/mark3labs/mcp-go v0.8.3/go.mod h1:cjMlBU0cv/cj9kjlgmRhoJ5JREdS7YX83xeIG9Ko/jE= github.com/markusmobius/go-dateparser v1.2.3 h1:TvrsIvr5uk+3v6poDjaicnAFJ5IgtFHgLiuMY2Eb7Nw= github.com/markusmobius/go-dateparser v1.2.3/go.mod h1:cMwQRrBUQlK1UI5TIFHEcvpsMbkWrQLXuaPNMFzuYLk= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= diff --git a/main.go b/main.go index 96d05c3..f2934fa 100644 --- a/main.go +++ b/main.go @@ -38,6 +38,7 @@ var app = &cli.Command{ decrypt, outbox, wallet, + mcpServer, }, Version: version, Flags: []cli.Flag{ diff --git a/mcp.go b/mcp.go new file mode 100644 index 0000000..0538b18 --- /dev/null +++ b/mcp.go @@ -0,0 +1,164 @@ +package main + +import ( + "context" + "fmt" + "os" + "strings" + + "github.com/fiatjaf/cli/v3" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" + "github.com/nbd-wtf/go-nostr" +) + +var mcpServer = &cli.Command{ + Name: "mcp", + Usage: "pander to the AI gods", + Description: ``, + DisableSliceFlagSeparator: true, + Flags: []cli.Flag{}, + Action: func(ctx context.Context, c *cli.Command) error { + s := server.NewMCPServer( + "nak", + version, + ) + + s.AddTool(mcp.NewTool("publish_note", + mcp.WithDescription("Publish a short note event to Nostr with the given text content"), + mcp.WithString("content", + mcp.Required(), + mcp.Description("Arbitrary string to be published"), + ), + mcp.WithString("mention", + mcp.Required(), + mcp.Description("Nostr user's public key to be mentioned"), + ), + ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + content, _ := request.Params.Arguments["content"].(string) + mention, _ := request.Params.Arguments["mention"].(string) + + 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" + } + var relays []string + + evt := nostr.Event{ + Kind: 1, + Tags: nostr.Tags{{"client", "goose/nak"}}, + Content: content, + CreatedAt: nostr.Now(), + } + + if mention != "" { + evt.Tags = append(evt.Tags, nostr.Tag{"p", mention}) + // their inbox relays + relays = sys.FetchInboxRelays(ctx, mention, 3) + } + + evt.Sign(sk) + + // our write relays + relays = append(relays, sys.FetchOutboxRelays(ctx, evt.PubKey, 3)...) + + if len(relays) == 0 { + relays = []string{"nos.lol", "relay.damus.io"} + } + + for res := range sys.Pool.PublishMany(ctx, []string{"nos.lol"}, evt) { + if res.Error != nil { + return mcp.NewToolResultError( + fmt.Sprintf("there was an error publishing the event to the relay %s", + res.RelayURL), + ), nil + } + } + + return mcp.NewToolResultText("event was successfully published with id " + evt.ID), nil + }) + + s.AddTool(mcp.NewTool("search_profile", + mcp.WithDescription("Search for the public key of a Nostr user given their name"), + mcp.WithString("name", + mcp.Required(), + mcp.Description("Name to be searched"), + ), + ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + name, _ := request.Params.Arguments["name"].(string) + re := sys.Pool.QuerySingle(ctx, []string{"relay.nostr.band", "nostr.wine"}, nostr.Filter{Search: name, Kinds: []int{0}}) + if re == nil { + return mcp.NewToolResultError("couldn't find anyone with that name"), nil + } + + return mcp.NewToolResultText(re.PubKey), 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.Required(), + mcp.Description("Public key of Nostr user we want to know the relay from where to read"), + ), + ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + name, _ := request.Params.Arguments["name"].(string) + re := sys.Pool.QuerySingle(ctx, []string{"relay.nostr.band", "nostr.wine"}, nostr.Filter{Search: name, Kinds: []int{0}}) + if re == nil { + return mcp.NewToolResultError("couldn't find anyone with that name"), nil + } + + return mcp.NewToolResultText(re.PubKey), nil + }) + + s.AddTool(mcp.NewTool("read_events_from_relay", + mcp.WithDescription("Makes a REQ query to one relay using the specified parameters"), + mcp.WithNumber("kind", + mcp.Required(), + mcp.Description("event kind number to include in the 'kinds' field"), + ), + mcp.WithString("pubkey", + mcp.Description("pubkey to include in the 'authors' field"), + ), + mcp.WithNumber("limit", + mcp.Required(), + mcp.Description("maximum number of events to query"), + ), + mcp.WithString("relay", + mcp.Required(), + mcp.Description("relay URL to send the query to"), + ), + ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + relay, _ := request.Params.Arguments["relay"].(string) + limit, _ := request.Params.Arguments["limit"].(int) + kind, _ := request.Params.Arguments["kind"].(int) + pubkey, _ := request.Params.Arguments["pubkey"].(string) + + 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{ + Limit: limit, + Kinds: []int{kind}, + } + if pubkey != "" { + filter.Authors = []string{pubkey} + } + + events := sys.Pool.SubManyEose(ctx, []string{relay}, nostr.Filters{filter}) + + results := make([]string, 0, limit) + for ie := range events { + results = append(results, ie.String()) + } + + return mcp.NewToolResultText(strings.Join(results, "\n\n")), nil + }) + + return server.ServeStdio(s) + }, +} From ad6b8c4ba566cd08c1d4ac0341f8d547421c5443 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sun, 2 Feb 2025 00:21:25 -0300 Subject: [PATCH 201/401] more mcp stuff. --- mcp.go | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 57 insertions(+), 4 deletions(-) diff --git a/mcp.go b/mcp.go index 0538b18..d310ba4 100644 --- a/mcp.go +++ b/mcp.go @@ -10,6 +10,7 @@ import ( "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" ) var mcpServer = &cli.Command{ @@ -82,6 +83,52 @@ var mcpServer = &cli.Command{ return mcp.NewToolResultText("event was successfully published with id " + evt.ID), nil }) + s.AddTool(mcp.NewTool("resolve_nostr_uri", + mcp.WithDescription("Resolve URIs prefixed with nostr:, including nostr:nevent1..., nostr:npub1..., nostr:nprofile1... and nostr:naddr1..."), + mcp.WithString("uri", + mcp.Required(), + mcp.Description("URI to be resolved"), + ), + ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + uri, _ := request.Params.Arguments["uri"].(string) + if strings.HasPrefix(uri, "nostr:") { + uri = uri[6:] + } + + prefix, data, err := nip19.Decode(uri) + if err != nil { + return mcp.NewToolResultError("this Nostr uri is invalid"), nil + } + + switch prefix { + case "npub": + pm := sys.FetchProfileMetadata(ctx, data.(string)) + return mcp.NewToolResultText( + fmt.Sprintf("this is a Nostr profile named '%s', their public key is '%s'", + pm.ShortName(), pm.PubKey), + ), nil + case "nprofile": + pm, _ := sys.FetchProfileFromInput(ctx, uri) + return mcp.NewToolResultText( + fmt.Sprintf("this is a Nostr profile named '%s', their public key is '%s'", + pm.ShortName(), pm.PubKey), + ), nil + case "nevent": + event, _, err := sys.FetchSpecificEventFromInput(ctx, uri, false) + if err != nil { + return mcp.NewToolResultError("Couldn't find this event anywhere"), nil + } + + return mcp.NewToolResultText( + fmt.Sprintf("this is a Nostr event: %s", event), + ), nil + case "naddr": + return mcp.NewToolResultError("For now we can't handle this kind of Nostr uri"), nil + default: + return mcp.NewToolResultError("We don't know how to handle this Nostr uri"), nil + } + }) + s.AddTool(mcp.NewTool("search_profile", mcp.WithDescription("Search for the public key of a Nostr user given their name"), mcp.WithString("name", @@ -115,7 +162,7 @@ var mcpServer = &cli.Command{ }) s.AddTool(mcp.NewTool("read_events_from_relay", - mcp.WithDescription("Makes a REQ query to one relay using the specified parameters"), + mcp.WithDescription("Makes a REQ query to one relay using the specified parameters, this can be used to fetch notes from a profile"), mcp.WithNumber("kind", mcp.Required(), mcp.Description("event kind number to include in the 'kinds' field"), @@ -151,12 +198,18 @@ var mcpServer = &cli.Command{ events := sys.Pool.SubManyEose(ctx, []string{relay}, nostr.Filters{filter}) - results := make([]string, 0, limit) + + result := strings.Builder{} for ie := range events { - results = append(results, ie.String()) + result.WriteString("author public key: ") + result.WriteString(ie.PubKey) + result.WriteString("content: '") + result.WriteString(ie.Content) + result.WriteString("'") + result.WriteString("\n---\n") } - return mcp.NewToolResultText(strings.Join(results, "\n\n")), nil + return mcp.NewToolResultText(result.String()), nil }) return server.ServeStdio(s) From ff8701a3b001580ddebfa74b88ec9b07431cb2ac Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Mon, 3 Feb 2025 15:54:12 -0300 Subject: [PATCH 202/401] fetch: stop adding kind:0 to all requests when they already have a kind specified. --- fetch.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/fetch.go b/fetch.go index ea84418..5972415 100644 --- a/fetch.go +++ b/fetch.go @@ -100,14 +100,14 @@ var fetch = &cli.Command{ } } - if len(filter.Authors) > 0 && len(filter.Kinds) == 0 { - filter.Kinds = append(filter.Kinds, 0) - } - if err := applyFlagsToFilter(c, &filter); err != nil { return err } + if len(filter.Authors) > 0 && len(filter.Kinds) == 0 { + filter.Kinds = append(filter.Kinds, 0) + } + if len(relays) == 0 { ctx = lineProcessingError(ctx, "no relay hints found") continue From 1e353680bc066ea2fc932daba6aee1f3e4d26d10 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 4 Feb 2025 22:38:32 -0300 Subject: [PATCH 203/401] wallet: adapt to single-wallet nip60 mode and implement nutzapping. --- go.mod | 2 +- go.sum | 4 +- key.go | 2 +- wallet.go | 353 +++++++++++++++++++++++++++++++++++++++++++----------- 4 files changed, 290 insertions(+), 71 deletions(-) diff --git a/go.mod b/go.mod index 34a475a..d4264cb 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,7 @@ require ( 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.49.2 + github.com/nbd-wtf/go-nostr v0.49.3 ) require ( diff --git a/go.sum b/go.sum index 813b5dd..0b1a34a 100644 --- a/go.sum +++ b/go.sum @@ -128,8 +128,8 @@ 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.49.2 h1:nOiwo/M20bYczU8pwLtosR+hGcSIaCdgoqiy6DTYJ+M= -github.com/nbd-wtf/go-nostr v0.49.2/go.mod h1:M50QnhkraC5Ol93v3jqxSMm1aGxUQm5mlmkYw5DJzh8= +github.com/nbd-wtf/go-nostr v0.49.3 h1:7tsEdMZOtJ764JuMLffkbhVUi4yyf688dbqArLvItPs= +github.com/nbd-wtf/go-nostr v0.49.3/go.mod h1:M50QnhkraC5Ol93v3jqxSMm1aGxUQm5mlmkYw5DJzh8= 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= diff --git a/key.go b/key.go index 90ac4c9..2be2b8f 100644 --- a/key.go +++ b/key.go @@ -17,7 +17,7 @@ import ( var key = &cli.Command{ Name: "key", - Usage: "operations on secret keys: generate, derive, encrypt, decrypt.", + Usage: "operations on secret keys: generate, derive, encrypt, decrypt", Description: ``, DisableSliceFlagSeparator: true, Commands: []*cli.Command{ diff --git a/wallet.go b/wallet.go index b2bfa88..4baf86e 100644 --- a/wallet.go +++ b/wallet.go @@ -10,9 +10,10 @@ import ( "github.com/fiatjaf/cli/v3" "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/nip60" + "github.com/nbd-wtf/go-nostr/nip61" ) -func prepareWallet(ctx context.Context, c *cli.Command) (*nip60.WalletStash, func(), error) { +func prepareWallet(ctx context.Context, c *cli.Command) (*nip60.Wallet, func(), error) { kr, _, err := gatherKeyerFromArguments(ctx, c) if err != nil { return nil, nil, err @@ -24,12 +25,12 @@ func prepareWallet(ctx context.Context, c *cli.Command) (*nip60.WalletStash, fun } relays := sys.FetchOutboxRelays(ctx, pk, 3) - wl := nip60.LoadStash(ctx, kr, sys.Pool, relays) - if wl == nil { - return nil, nil, fmt.Errorf("error loading wallet stash") + w := nip60.LoadWallet(ctx, kr, sys.Pool, relays) + if w == nil { + return nil, nil, fmt.Errorf("error loading walle") } - wl.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 { @@ -37,17 +38,20 @@ func prepareWallet(ctx context.Context, c *cli.Command) (*nip60.WalletStash, fun } } - wl.PublishUpdate = func(event nostr.Event, deleted, received, change *nip60.Token, isHistory bool) { + w.PublishUpdate = func(event nostr.Event, deleted, received, change *nip60.Token, isHistory bool) { desc := "wallet" if received != nil { + mint, _ := strings.CutPrefix(received.Mint, "https://") desc = fmt.Sprintf("received from %s with %d proofs totalling %d", - received.Mint, len(received.Proofs), received.Proofs.Amount()) + mint, len(received.Proofs), received.Proofs.Amount()) } else if change != nil { + mint, _ := strings.CutPrefix(change.Mint, "https://") desc = fmt.Sprintf("change from %s with %d proofs totalling %d", - change.Mint, len(change.Proofs), change.Proofs.Amount()) + mint, len(change.Proofs), change.Proofs.Amount()) } else if deleted != nil { + mint, _ := strings.CutPrefix(deleted.Mint, "https://") desc = fmt.Sprintf("deleting a used token from %s with %d proofs totalling %d", - deleted.Mint, len(deleted.Proofs), deleted.Proofs.Amount()) + mint, len(deleted.Proofs), deleted.Proofs.Amount()) } else if isHistory { desc = "history entry" } @@ -74,82 +78,102 @@ func prepareWallet(ctx context.Context, c *cli.Command) (*nip60.WalletStash, fun log("\n") } - <-wl.Stable + <-w.Stable - return wl, func() { - wl.Close() + return w, func() { + w.Close() }, nil } var wallet = &cli.Command{ Name: "wallet", - Usage: "manage nip60 cashu wallets (see subcommands)", + Usage: "displays the current wallet balance", Description: "all wallet data is stored on Nostr relays, signed and encrypted with the given key, and reloaded again from relays on every call.\n\nthe same data can be accessed by other compatible nip60 clients.", DisableSliceFlagSeparator: true, Flags: defaultKeyFlags, - ArgsUsage: "", Action: func(ctx context.Context, c *cli.Command) error { - args := c.Args().Slice() - if len(args) != 1 { - return fmt.Errorf("must be called as `nak wallet ") - } - - wl, closewl, err := prepareWallet(ctx, c) + w, closew, err := prepareWallet(ctx, c) if err != nil { return err } - for w := range wl.Wallets() { - if w.Identifier == args[0] { - stdout(w.DisplayName(), w.Balance()) - break - } - } + stdout(w.Balance()) - closewl() + closew() return nil }, Commands: []*cli.Command{ { - Name: "list", - Usage: "lists existing wallets with their balances", + Name: "mints", + Usage: "lists, adds or remove default mints from the wallet", DisableSliceFlagSeparator: true, Action: func(ctx context.Context, c *cli.Command) error { - wl, closewl, err := prepareWallet(ctx, c) + w, closew, err := prepareWallet(ctx, c) if err != nil { return err } - for w := range wl.Wallets() { - stdout(w.DisplayName(), w.Balance()) + for _, url := range w.Mints { + stdout(strings.Split(url, "://")[1]) } - closewl() + closew() return nil }, + Commands: []*cli.Command{ + { + Name: "add", + DisableSliceFlagSeparator: true, + ArgsUsage: "...", + Action: func(ctx context.Context, c *cli.Command) error { + w, closew, err := prepareWallet(ctx, c) + if err != nil { + return err + } + + if err := w.AddMint(ctx, c.Args().Slice()...); err != nil { + return err + } + + closew() + return nil + }, + }, + { + Name: "remove", + DisableSliceFlagSeparator: true, + ArgsUsage: "...", + Action: func(ctx context.Context, c *cli.Command) error { + w, closew, err := prepareWallet(ctx, c) + if err != nil { + return err + } + + if err := w.RemoveMint(ctx, c.Args().Slice()...); err != nil { + return err + } + + closew() + return nil + }, + }, + }, }, { Name: "tokens", - Usage: "lists existing tokens in this wallet with their mints and aggregated amounts", + Usage: "lists existing tokens with their mints and aggregated amounts", DisableSliceFlagSeparator: true, Action: func(ctx context.Context, c *cli.Command) error { - wl, closewl, err := prepareWallet(ctx, c) + w, closew, err := prepareWallet(ctx, c) if err != nil { return err } - args := c.Args().Slice() - if len(args) != 1 { - return fmt.Errorf("must be called as `nak wallet tokens") - } - - w := wl.EnsureWallet(ctx, args[0]) - for _, token := range w.Tokens { stdout(token.ID(), token.Proofs.Amount(), strings.Split(token.Mint, "://")[1]) } - closewl() + closew() return nil }, }, @@ -158,24 +182,38 @@ var wallet = &cli.Command{ Usage: "takes a cashu token string as an argument and adds it to the wallet", ArgsUsage: "", DisableSliceFlagSeparator: true, + Flags: []cli.Flag{ + &cli.StringSliceFlag{ + Name: "mint", + Usage: "mint to swap the token into", + }, + }, Action: func(ctx context.Context, c *cli.Command) error { args := c.Args().Slice() - if len(args) != 2 { - return fmt.Errorf("must be called as `nak wallet receive ") + if len(args) != 1 { + return fmt.Errorf("must be called as `nak wallet receive ") } - wl, closewl, err := prepareWallet(ctx, c) + w, closew, err := prepareWallet(ctx, c) if err != nil { return err } - w := wl.EnsureWallet(ctx, args[0]) - - if err := w.ReceiveToken(ctx, args[1]); err != nil { + proofs, mint, err := nip60.GetProofsAndMint(args[0]) + if err != nil { return err } - closewl() + opts := make([]nip60.ReceiveOption, 0, 1) + for _, url := range c.StringSlice("mint") { + opts = append(opts, nip60.WithMintDestination(url)) + } + + if err := w.Receive(ctx, proofs, mint, opts...); err != nil { + return err + } + + closew() return nil }, }, @@ -192,34 +230,32 @@ var wallet = &cli.Command{ }, Action: func(ctx context.Context, c *cli.Command) error { args := c.Args().Slice() - if len(args) != 2 { - return fmt.Errorf("must be called as `nak wallet send ") + if len(args) != 1 { + return fmt.Errorf("must be called as `nak wallet send ") } - amount, err := strconv.ParseUint(args[1], 10, 64) + amount, err := strconv.ParseUint(args[0], 10, 64) if err != nil { - return fmt.Errorf("amount '%s' is invalid", args[1]) + return fmt.Errorf("amount '%s' is invalid", args[0]) } - wl, closewl, err := prepareWallet(ctx, c) + w, closew, err := prepareWallet(ctx, c) if err != nil { return err } - w := wl.EnsureWallet(ctx, args[0]) - opts := make([]nip60.SendOption, 0, 1) if mint := c.String("mint"); mint != "" { mint = "http" + nostr.NormalizeURL(mint)[2:] opts = append(opts, nip60.WithMint(mint)) } - token, err := w.SendToken(ctx, amount, opts...) + proofs, mint, err := w.Send(ctx, amount, opts...) if err != nil { return err } - stdout(token) + stdout(nip60.MakeTokenString(proofs, mint)) - closewl() + closew() return nil }, }, @@ -236,33 +272,216 @@ var wallet = &cli.Command{ }, Action: func(ctx context.Context, c *cli.Command) error { args := c.Args().Slice() - if len(args) != 2 { - return fmt.Errorf("must be called as `nak wallet pay ") + if len(args) != 1 { + return fmt.Errorf("must be called as `nak wallet pay ") } - wl, closewl, err := prepareWallet(ctx, c) + w, closew, err := prepareWallet(ctx, c) if err != nil { return err } - w := wl.EnsureWallet(ctx, args[0]) - opts := make([]nip60.SendOption, 0, 1) if mint := c.String("mint"); mint != "" { mint = "http" + nostr.NormalizeURL(mint)[2:] opts = append(opts, nip60.WithMint(mint)) } - preimage, err := w.PayBolt11(ctx, args[1], opts...) + preimage, err := w.PayBolt11(ctx, args[0], opts...) if err != nil { return err } stdout(preimage) - closewl() + closew() return nil }, }, + { + Name: "nutzap", + Usage: "sends a nip61 nutzap to one or more Nostr profiles and/or events", + ArgsUsage: " ", + Description: " is in satoshis, can be an npub, nprofile, nevent or hex pubkey.", + DisableSliceFlagSeparator: true, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "mint", + Usage: "send from a specific mint", + }, + &cli.StringFlag{ + Name: "message", + Usage: "attach a message to the nutzap", + }, + }, + Action: func(ctx context.Context, c *cli.Command) error { + args := c.Args().Slice() + if len(args) >= 2 { + return fmt.Errorf("must be called as `nak wallet send ...") + } + + w, closew, err := prepareWallet(ctx, c) + if err != nil { + return err + } + + amount := c.Uint("amount") + target := c.String("target") + + var evt *nostr.Event + var eventId string + + if strings.HasPrefix(target, "nevent1") { + evt, _, err = sys.FetchSpecificEventFromInput(ctx, target, false) + if err != nil { + return err + } + eventId = evt.ID + target = evt.PubKey + } + + pm, err := sys.FetchProfileFromInput(ctx, target) + if err != nil { + return err + } + + log("sending %d sat to '%s' (%s)", amount, pm.ShortName(), pm.Npub()) + + opts := make([]nip60.SendOption, 0, 1) + if mint := c.String("mint"); mint != "" { + mint = "http" + nostr.NormalizeURL(mint)[2:] + opts = append(opts, nip60.WithMint(mint)) + } + + kr, _, _ := gatherKeyerFromArguments(ctx, c) + results, err := nip61.SendNutzap( + ctx, + kr, + w, + sys.Pool, + pm.PubKey, + sys.FetchInboxRelays, + sys.FetchOutboxRelays(ctx, pm.PubKey, 3), + eventId, + amount, + c.String("message"), + ) + if err != nil { + return err + } + + log("- publishing nutzap... ") + first := true + for res := range results { + cleanUrl, ok := strings.CutPrefix(res.RelayURL, "wss://") + if !ok { + cleanUrl = res.RelayURL + } + + if !first { + log(", ") + } + first = false + if res.Error != nil { + log("%s: %s", color.RedString(cleanUrl), res.Error) + } else { + log("%s: ok", color.GreenString(cleanUrl)) + } + } + + closew() + return nil + }, + Commands: []*cli.Command{ + { + Name: "setup", + Usage: "setup your wallet private key and kind:10019 event for receiving nutzaps", + DisableSliceFlagSeparator: true, + Flags: []cli.Flag{ + &cli.StringSliceFlag{ + Name: "mint", + Usage: "mints to receive nutzaps in", + }, + &cli.StringFlag{ + Name: "private-key", + Usage: "private key used for receiving nutzaps", + }, + &cli.BoolFlag{ + Name: "force", + Aliases: []string{"f"}, + Usage: "forces replacement of private-key", + }, + }, + Action: func(ctx context.Context, c *cli.Command) error { + w, closew, err := prepareWallet(ctx, c) + if err != nil { + return err + } + + if w.PrivateKey == nil { + if sk := c.String("private-key"); sk != "" { + if err := w.SetPrivateKey(ctx, sk); err != nil { + return err + } + } else { + return fmt.Errorf("missing --private-key") + } + } else if sk := c.String("private-key"); sk != "" && !c.Bool("force") { + return fmt.Errorf("refusing to replace existing private key, use the --force flag") + } + + kr, _, _ := gatherKeyerFromArguments(ctx, c) + pk, _ := kr.GetPublicKey(ctx) + relays := sys.FetchWriteRelays(ctx, pk, 6) + + info := nip61.Info{} + ie := sys.Pool.QuerySingle(ctx, relays, nostr.Filter{ + Kinds: []int{10019}, + Authors: []string{pk}, + Limit: 1, + }) + if ie != nil { + info.ParseEvent(ie.Event) + } + + if mints := c.StringSlice("mints"); len(mints) == 0 && len(info.Mints) == 0 { + info.Mints = w.Mints + } + if len(info.Mints) == 0 { + return fmt.Errorf("missing --mint") + } + + evt := nostr.Event{} + if err := info.ToEvent(ctx, kr, &evt); err != nil { + return err + } + + stdout(evt) + log("- saving kind:10019 event... ") + first := true + for res := range sys.Pool.PublishMany(ctx, relays, evt) { + cleanUrl, ok := strings.CutPrefix(res.RelayURL, "wss://") + if !ok { + cleanUrl = res.RelayURL + } + + if !first { + log(", ") + } + first = false + + if res.Error != nil { + log("%s: %s", color.RedString(cleanUrl), res.Error) + } else { + log("%s: ok", color.GreenString(cleanUrl)) + } + } + + closew() + return nil + }, + }, + }, + }, }, } From 6c634d8081a1bb3cc9ee5ebe37a4fefdce975f1d Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 4 Feb 2025 23:20:35 -0300 Subject: [PATCH 204/401] nak curl --- curl.go | 102 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ main.go | 11 ++++++ 2 files changed, 113 insertions(+) create mode 100644 curl.go diff --git a/curl.go b/curl.go new file mode 100644 index 0000000..a039f41 --- /dev/null +++ b/curl.go @@ -0,0 +1,102 @@ +package main + +import ( + "context" + "encoding/base64" + "fmt" + "os" + "os/exec" + "strings" + + "github.com/fiatjaf/cli/v3" + "github.com/nbd-wtf/go-nostr" + "golang.org/x/exp/slices" +) + +var curlFlags []string + +var curl = &cli.Command{ + Name: "curl", + Usage: "calls curl but with a nip98 header", + Description: "accepts all flags and arguments exactly as they would be passed to curl.", + Flags: defaultKeyFlags, + Action: func(ctx context.Context, c *cli.Command) error { + kr, _, err := gatherKeyerFromArguments(ctx, c) + if err != nil { + return err + } + + // cowboy parsing of curl flags to get the data we need for nip98 + var url string + method := "GET" + + nextIsMethod := false + for _, f := range curlFlags { + if strings.HasPrefix(f, "https://") || strings.HasPrefix(f, "http://") { + url = f + } else if f == "--request" || f == "-X" { + nextIsMethod = true + } else if nextIsMethod { + method = f + method, _ = strings.CutPrefix(method, `"`) + method, _ = strings.CutSuffix(method, `"`) + method = strings.ToUpper(method) + } + nextIsMethod = false + } + + if url == "" { + return fmt.Errorf("can't create nip98 event: target url is empty") + } + + // make and sign event + evt := nostr.Event{ + Kind: 27235, + CreatedAt: nostr.Now(), + Tags: nostr.Tags{ + {"u", url}, + {"method", method}, + }, + } + if err := kr.SignEvent(ctx, &evt); err != nil { + return err + } + + // the first 2 indexes of curlFlags were reserved for this + curlFlags[0] = "-H" + curlFlags[1] = fmt.Sprintf("Authorization: Nostr %s", base64.StdEncoding.EncodeToString([]byte(evt.String()))) + + // call curl + cmd := exec.Command("curl", curlFlags...) + cmd.Stderr = os.Stderr + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Run() + return nil + }, +} + +func realCurl() error { + curlFlags = make([]string, 2, len(os.Args)-4) + keyFlags := make([]string, 0, 5) + + for i := 0; i < len(os.Args[2:]); i++ { + arg := os.Args[i+2] + if slices.ContainsFunc(defaultKeyFlags, func(f cli.Flag) bool { + bareArg, _ := strings.CutPrefix(arg, "-") + bareArg, _ = strings.CutPrefix(bareArg, "-") + return slices.Contains(f.Names(), bareArg) + }) { + keyFlags = append(keyFlags, arg) + if arg != "--prompt-sec" { + i++ + val := os.Args[i+2] + keyFlags = append(keyFlags, val) + } + } else { + curlFlags = append(curlFlags, arg) + } + } + + return curl.Run(context.Background(), keyFlags) +} diff --git a/main.go b/main.go index f2934fa..91aa68f 100644 --- a/main.go +++ b/main.go @@ -39,6 +39,7 @@ var app = &cli.Command{ outbox, wallet, mcpServer, + curl, }, Version: version, Flags: []cli.Flag{ @@ -140,6 +141,16 @@ func main() { Usage: "prints the version", } + // a megahack to enable this curl command proxy + if len(os.Args) > 2 && os.Args[1] == "curl" { + if err := realCurl(); err != nil { + stdout(err) + colors.reset() + os.Exit(1) + } + return + } + if err := app.Run(context.Background(), os.Args); err != nil { stdout(err) colors.reset() From 60d1292f80950d5177d3f8afb60eeaba9a63dde0 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Wed, 5 Feb 2025 09:44:16 -0300 Subject: [PATCH 205/401] parse multiline json from input on nak event and nak req, use iterators instead of channels for more efficient stdin parsing. --- event.go | 31 ++++++++++++-------- helpers.go | 86 +++++++++++++++++++++++++++++++----------------------- req.go | 2 +- 3 files changed, 70 insertions(+), 49 deletions(-) diff --git a/event.go b/event.go index 2c5f590..d0ba01d 100644 --- a/event.go +++ b/event.go @@ -154,21 +154,20 @@ example: doAuth := c.Bool("auth") - // then process input and generate events - for stdinEvent := range getStdinLinesOrBlank() { - evt := nostr.Event{ - Tags: make(nostr.Tags, 0, 3), - } + // then process input and generate events: - kindWasSupplied := false + // will reuse this + var evt nostr.Event + + // this is called when we have a valid json from stdin + handleEvent := func(stdinEvent string) error { + evt.Content = "" + + kindWasSupplied := strings.Contains(stdinEvent, `"kind"`) mustRehashAndResign := false - if stdinEvent != "" { - if err := easyjson.Unmarshal([]byte(stdinEvent), &evt); err != nil { - ctx = lineProcessingError(ctx, "invalid event received from stdin: %s", err) - continue - } - kindWasSupplied = strings.Contains(stdinEvent, `"kind"`) + if err := easyjson.Unmarshal([]byte(stdinEvent), &evt); err != nil { + return fmt.Errorf("invalid event received from stdin: %s", err) } if kind := c.Uint("kind"); slices.Contains(c.FlagNames(), "kind") { @@ -324,6 +323,14 @@ example: log(nevent + "\n") } } + + return nil + } + + for stdinEvent := range getJsonsOrBlank() { + if err := handleEvent(stdinEvent); err != nil { + ctx = lineProcessingError(ctx, err.Error()) + } } exitIfLineProcessingError(ctx) diff --git a/helpers.go b/helpers.go index 3a3d9ce..73c13cb 100644 --- a/helpers.go +++ b/helpers.go @@ -4,11 +4,13 @@ import ( "bufio" "context" "fmt" + "iter" "math/rand" "net/http" "net/textproto" "net/url" "os" + "slices" "strings" "time" @@ -43,59 +45,71 @@ func isPiped() bool { return stat.Mode()&os.ModeCharDevice == 0 } -func getStdinLinesOrBlank() chan string { - multi := make(chan string) - if hasStdinLines := writeStdinLinesOrNothing(multi); !hasStdinLines { - single := make(chan string, 1) - single <- "" - close(single) - return single - } else { - return multi +func getJsonsOrBlank() iter.Seq[string] { + var curr strings.Builder + + return func(yield func(string) bool) { + for stdinLine := range getStdinLinesOrBlank() { + // we're look for an event, but it may be in multiple lines, so if json parsing fails + // we'll try the next line until we're successful + curr.WriteString(stdinLine) + stdinEvent := curr.String() + + var dummy any + if err := json.Unmarshal([]byte(stdinEvent), &dummy); err != nil { + continue + } + + if !yield(stdinEvent) { + return + } + + curr.Reset() + } } } -func getStdinLinesOrArguments(args cli.Args) chan string { +func getStdinLinesOrBlank() iter.Seq[string] { + return func(yield func(string) bool) { + if hasStdinLines := writeStdinLinesOrNothing(yield); !hasStdinLines { + return + } else { + return + } + } +} + +func getStdinLinesOrArguments(args cli.Args) iter.Seq[string] { return getStdinLinesOrArgumentsFromSlice(args.Slice()) } -func getStdinLinesOrArgumentsFromSlice(args []string) chan string { +func getStdinLinesOrArgumentsFromSlice(args []string) iter.Seq[string] { // try the first argument if len(args) > 0 { - argsCh := make(chan string, 1) - go func() { - for _, arg := range args { - argsCh <- arg - } - close(argsCh) - }() - return argsCh + return slices.Values(args) } // try the stdin - multi := make(chan string) - if !writeStdinLinesOrNothing(multi) { - close(multi) + return func(yield func(string) bool) { + writeStdinLinesOrNothing(yield) } - return multi } -func writeStdinLinesOrNothing(ch chan string) (hasStdinLines bool) { +func writeStdinLinesOrNothing(yield func(string) bool) (hasStdinLines bool) { if isPiped() { // piped - go func() { - scanner := bufio.NewScanner(os.Stdin) - scanner.Buffer(make([]byte, 16*1024*1024), 256*1024*1024) - hasEmittedAtLeastOne := false - for scanner.Scan() { - ch <- strings.TrimSpace(scanner.Text()) - hasEmittedAtLeastOne = true + scanner := bufio.NewScanner(os.Stdin) + scanner.Buffer(make([]byte, 16*1024*1024), 256*1024*1024) + hasEmittedAtLeastOne := false + for scanner.Scan() { + if !yield(strings.TrimSpace(scanner.Text())) { + return } - if !hasEmittedAtLeastOne { - ch <- "" - } - close(ch) - }() + hasEmittedAtLeastOne = true + } + if !hasEmittedAtLeastOne { + yield("") + } return true } else { // not piped diff --git a/req.go b/req.go index 09cae23..a5442da 100644 --- a/req.go +++ b/req.go @@ -107,7 +107,7 @@ example: }() } - for stdinFilter := range getStdinLinesOrBlank() { + for stdinFilter := range getJsonsOrBlank() { filter := nostr.Filter{} if stdinFilter != "" { if err := easyjson.Unmarshal([]byte(stdinFilter), &filter); err != nil { From 4392293ed62a7651edc476727727da941339a36a Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Wed, 5 Feb 2025 10:22:04 -0300 Subject: [PATCH 206/401] curl method and negative make fixes. --- curl.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/curl.go b/curl.go index a039f41..25c4525 100644 --- a/curl.go +++ b/curl.go @@ -32,15 +32,16 @@ var curl = &cli.Command{ nextIsMethod := false for _, f := range curlFlags { - if strings.HasPrefix(f, "https://") || strings.HasPrefix(f, "http://") { - url = f - } else if f == "--request" || f == "-X" { - nextIsMethod = true - } else if nextIsMethod { + if nextIsMethod { method = f method, _ = strings.CutPrefix(method, `"`) method, _ = strings.CutSuffix(method, `"`) method = strings.ToUpper(method) + } else if strings.HasPrefix(f, "https://") || strings.HasPrefix(f, "http://") { + url = f + } else if f == "--request" || f == "-X" { + nextIsMethod = true + continue } nextIsMethod = false } @@ -77,7 +78,7 @@ var curl = &cli.Command{ } func realCurl() error { - curlFlags = make([]string, 2, len(os.Args)-4) + curlFlags = make([]string, 2, max(len(os.Args)-4, 2)) keyFlags := make([]string, 0, 5) for i := 0; i < len(os.Args[2:]); i++ { From d00976a66955ef02fd78966217cd920991b44eb2 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Wed, 5 Feb 2025 10:37:15 -0300 Subject: [PATCH 207/401] curl: assume POST when there is data and no method is specified. --- curl.go | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/curl.go b/curl.go index 25c4525..52fe474 100644 --- a/curl.go +++ b/curl.go @@ -28,7 +28,22 @@ var curl = &cli.Command{ // cowboy parsing of curl flags to get the data we need for nip98 var url string - method := "GET" + var method string + var presumedMethod string + + curlBodyBuildingFlags := []string{ + "-d", + "--data", + "--data-binary", + "--data-ascii", + "--data-raw", + "--data-urlencode", + "-F", + "--form", + "--form-string", + "--form-escape", + "--upload-file", + } nextIsMethod := false for _, f := range curlFlags { @@ -42,6 +57,11 @@ var curl = &cli.Command{ } else if f == "--request" || f == "-X" { nextIsMethod = true continue + } else if slices.Contains(curlBodyBuildingFlags, f) || + slices.ContainsFunc(curlBodyBuildingFlags, func(s string) bool { + return strings.HasPrefix(f, s) + }) { + presumedMethod = "POST" } nextIsMethod = false } @@ -50,6 +70,14 @@ var curl = &cli.Command{ return fmt.Errorf("can't create nip98 event: target url is empty") } + if method == "" { + if presumedMethod != "" { + method = presumedMethod + } else { + method = "GET" + } + } + // make and sign event evt := nostr.Event{ Kind: 27235, From 1f2492c9b1cadd4b16e73d89c6accde1010cbd52 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Wed, 5 Feb 2025 20:42:55 -0300 Subject: [PATCH 208/401] fix multiline handler thing for when we're don't have any stdin. --- helpers.go | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/helpers.go b/helpers.go index 73c13cb..9ae8ad5 100644 --- a/helpers.go +++ b/helpers.go @@ -49,7 +49,7 @@ func getJsonsOrBlank() iter.Seq[string] { var curr strings.Builder return func(yield func(string) bool) { - for stdinLine := range getStdinLinesOrBlank() { + hasStdin := writeStdinLinesOrNothing(func(stdinLine string) bool { // we're look for an event, but it may be in multiple lines, so if json parsing fails // we'll try the next line until we're successful curr.WriteString(stdinLine) @@ -57,24 +57,34 @@ func getJsonsOrBlank() iter.Seq[string] { var dummy any if err := json.Unmarshal([]byte(stdinEvent), &dummy); err != nil { - continue + return true } if !yield(stdinEvent) { - return + return false } curr.Reset() + return true + }) + + if !hasStdin { + yield("{}") } } } func getStdinLinesOrBlank() iter.Seq[string] { return func(yield func(string) bool) { - if hasStdinLines := writeStdinLinesOrNothing(yield); !hasStdinLines { - return - } else { - return + hasStdin := writeStdinLinesOrNothing(func(stdinLine string) bool { + if !yield(stdinLine) { + return false + } + return true + }) + + if !hasStdin { + yield("") } } } @@ -107,10 +117,7 @@ func writeStdinLinesOrNothing(yield func(string) bool) (hasStdinLines bool) { } hasEmittedAtLeastOne = true } - if !hasEmittedAtLeastOne { - yield("") - } - return true + return hasEmittedAtLeastOne } else { // not piped return false From 55c6f75b8a4741418f8a01a04f93da8f8333eafd Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Fri, 7 Feb 2025 16:54:23 -0300 Subject: [PATCH 209/401] mcp: fix a bunch of stupid bugs. --- mcp.go | 52 ++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 38 insertions(+), 14 deletions(-) diff --git a/mcp.go b/mcp.go index d310ba4..daf36ed 100644 --- a/mcp.go +++ b/mcp.go @@ -27,6 +27,9 @@ var mcpServer = &cli.Command{ s.AddTool(mcp.NewTool("publish_note", mcp.WithDescription("Publish a short note event to Nostr with the given text content"), + mcp.WithString("relay", + mcp.Description("Relay to publish the note to"), + ), mcp.WithString("content", mcp.Required(), mcp.Description("Arbitrary string to be published"), @@ -38,6 +41,11 @@ var mcpServer = &cli.Command{ ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { content, _ := request.Params.Arguments["content"].(string) mention, _ := request.Params.Arguments["mention"].(string) + relayI, ok := request.Params.Arguments["relay"] + var relay string + if ok { + relay, _ = relayI.(string) + } 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 @@ -71,16 +79,33 @@ var mcpServer = &cli.Command{ relays = []string{"nos.lol", "relay.damus.io"} } - for res := range sys.Pool.PublishMany(ctx, []string{"nos.lol"}, evt) { + // extra relay specified + relays = append(relays, relay) + + result := strings.Builder{} + result.WriteString( + fmt.Sprintf("the event we generated has id '%s', kind '%d' and is signed by pubkey '%s'. ", + evt.ID, + evt.Kind, + evt.PubKey, + ), + ) + + for res := range sys.Pool.PublishMany(ctx, relays, evt) { if res.Error != nil { - return mcp.NewToolResultError( - fmt.Sprintf("there was an error publishing the event to the relay %s", + result.WriteString( + fmt.Sprintf("there was an error publishing the event to the relay %s. ", res.RelayURL), - ), nil + ) + } else { + result.WriteString( + fmt.Sprintf("the event was successfully published to the relay %s. ", + res.RelayURL), + ) } } - return mcp.NewToolResultText("event was successfully published with id " + evt.ID), nil + return mcp.NewToolResultText(result.String()), nil }) s.AddTool(mcp.NewTool("resolve_nostr_uri", @@ -152,13 +177,9 @@ var mcpServer = &cli.Command{ mcp.Description("Public key of Nostr user we want to know the relay from where to read"), ), ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - name, _ := request.Params.Arguments["name"].(string) - re := sys.Pool.QuerySingle(ctx, []string{"relay.nostr.band", "nostr.wine"}, nostr.Filter{Search: name, Kinds: []int{0}}) - if re == nil { - return mcp.NewToolResultError("couldn't find anyone with that name"), nil - } - - return mcp.NewToolResultText(re.PubKey), nil + pubkey, _ := request.Params.Arguments["pubkey"].(string) + res := sys.FetchOutboxRelays(ctx, pubkey, 1) + return mcp.NewToolResultText(res[0]), nil }) s.AddTool(mcp.NewTool("read_events_from_relay", @@ -182,7 +203,11 @@ var mcpServer = &cli.Command{ relay, _ := request.Params.Arguments["relay"].(string) limit, _ := request.Params.Arguments["limit"].(int) kind, _ := request.Params.Arguments["kind"].(int) - pubkey, _ := request.Params.Arguments["pubkey"].(string) + pubkeyI, ok := request.Params.Arguments["pubkey"] + var pubkey string + if ok { + pubkey, _ = pubkeyI.(string) + } 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 @@ -198,7 +223,6 @@ var mcpServer = &cli.Command{ events := sys.Pool.SubManyEose(ctx, []string{relay}, nostr.Filters{filter}) - result := strings.Builder{} for ie := range events { result.WriteString("author public key: ") From 2e30dfe2eb2474145a3920c233642a1a8a2a21ba Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Wed, 12 Feb 2025 15:51:00 -0300 Subject: [PATCH 210/401] wallet: fix nutzap error message. --- wallet.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wallet.go b/wallet.go index 4baf86e..87e5822 100644 --- a/wallet.go +++ b/wallet.go @@ -316,8 +316,8 @@ var wallet = &cli.Command{ }, Action: func(ctx context.Context, c *cli.Command) error { args := c.Args().Slice() - if len(args) >= 2 { - return fmt.Errorf("must be called as `nak wallet send ...") + if len(args) < 2 { + return fmt.Errorf("must be called as `nak wallet nutzap ...") } w, closew, err := prepareWallet(ctx, c) From 95bed5d5a84659d55e80992e35797e31d27b8240 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Wed, 12 Feb 2025 16:37:17 -0300 Subject: [PATCH 211/401] nak req --ids-only --- go.mod | 4 ++-- go.sum | 4 ++-- req.go | 39 +++++++++++++++++++++++++++++++-------- 3 files changed, 35 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index d4264cb..beb0af0 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,8 @@ require ( 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.49.3 + github.com/nbd-wtf/go-nostr v0.49.7 + golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 ) require ( @@ -62,7 +63,6 @@ require ( github.com/wasilibs/go-re2 v1.3.0 // indirect github.com/x448/float16 v0.8.4 // indirect golang.org/x/crypto v0.32.0 // indirect - golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect golang.org/x/net v0.34.0 // indirect golang.org/x/sys v0.29.0 // indirect golang.org/x/text v0.21.0 // indirect diff --git a/go.sum b/go.sum index 0b1a34a..29d0121 100644 --- a/go.sum +++ b/go.sum @@ -128,8 +128,8 @@ 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.49.3 h1:7tsEdMZOtJ764JuMLffkbhVUi4yyf688dbqArLvItPs= -github.com/nbd-wtf/go-nostr v0.49.3/go.mod h1:M50QnhkraC5Ol93v3jqxSMm1aGxUQm5mlmkYw5DJzh8= +github.com/nbd-wtf/go-nostr v0.49.7 h1:4D9XCqjTJYqUPMuNJI27W5gaiklnTI12IzzWIAOFepE= +github.com/nbd-wtf/go-nostr v0.49.7/go.mod h1:M50QnhkraC5Ol93v3jqxSMm1aGxUQm5mlmkYw5DJzh8= 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= diff --git a/req.go b/req.go index a5442da..3a1a4bf 100644 --- a/req.go +++ b/req.go @@ -9,6 +9,7 @@ import ( "github.com/fiatjaf/cli/v3" "github.com/mailru/easyjson" "github.com/nbd-wtf/go-nostr" + "github.com/nbd-wtf/go-nostr/nip77" ) const ( @@ -32,6 +33,10 @@ example: DisableSliceFlagSeparator: true, Flags: append(defaultKeyFlags, append(reqFilterFlags, + &cli.BoolFlag{ + Name: "ids-only", + Usage: "use nip77 to fetch just a list of ids", + }, &cli.BoolFlag{ Name: "stream", Usage: "keep the subscription open, printing all events as they are returned", @@ -121,15 +126,33 @@ example: } if len(relayUrls) > 0 { - fn := sys.Pool.SubManyEose - if c.Bool("paginate") { - fn = paginateWithParams(c.Duration("paginate-interval"), c.Uint("paginate-global-limit")) - } else if c.Bool("stream") { - fn = sys.Pool.SubMany - } + if c.Bool("ids-only") { + seen := make(map[string]struct{}, max(500, filter.Limit)) + for _, url := range relayUrls { + ch, err := nip77.FetchIDsOnly(ctx, url, filter) + if err != nil { + log("negentropy call to %s failed: %s", url, err) + continue + } + for id := range ch { + if _, ok := seen[id]; ok { + continue + } + seen[id] = struct{}{} + stdout(id) + } + } + } else { + fn := sys.Pool.SubManyEose + if c.Bool("paginate") { + fn = paginateWithParams(c.Duration("paginate-interval"), c.Uint("paginate-global-limit")) + } else if c.Bool("stream") { + fn = sys.Pool.SubMany + } - for ie := range fn(ctx, relayUrls, nostr.Filters{filter}) { - stdout(ie.Event) + for ie := range fn(ctx, relayUrls, nostr.Filters{filter}) { + stdout(ie.Event) + } } } else { // no relays given, will just print the filter From 17920d8aef97aa74270700db12a7a2d113a1ba11 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Thu, 13 Feb 2025 23:09:56 -0300 Subject: [PATCH 212/401] adapt to go-nostr's new methods that take just one filter (and paginator). --- bunker.go | 31 +++++++++++++++++----- fetch.go | 2 +- go.mod | 2 +- mcp.go | 2 +- paginate.go | 76 ----------------------------------------------------- req.go | 8 +++--- 6 files changed, 31 insertions(+), 90 deletions(-) delete mode 100644 paginate.go diff --git a/bunker.go b/bunker.go index 29eaa69..73fef86 100644 --- a/bunker.go +++ b/bunker.go @@ -141,13 +141,11 @@ var bunker = &cli.Command{ // subscribe to relays now := nostr.Now() - events := sys.Pool.SubMany(ctx, relayURLs, nostr.Filters{ - { - Kinds: []int{nostr.KindNostrConnect}, - Tags: nostr.TagMap{"p": []string{pubkey}}, - Since: &now, - LimitZero: true, - }, + events := sys.Pool.SubscribeMany(ctx, relayURLs, nostr.Filter{ + Kinds: []int{nostr.KindNostrConnect}, + Tags: nostr.TagMap{"p": []string{pubkey}}, + Since: &now, + LimitZero: true, }) signer := nip46.NewStaticKeySigner(sec) @@ -227,4 +225,23 @@ var bunker = &cli.Command{ return nil }, + Commands: []*cli.Command{ + { + Name: "connect", + Usage: "use the client-initiated NostrConnect flow of NIP46", + ArgsUsage: "", + Action: func(ctx context.Context, c *cli.Command) error { + if c.Args().Len() != 1 { + return fmt.Errorf("must be called with a nostrconnect://... uri") + } + + uri, err := url.Parse(c.Args().First()) + if err != nil || uri.Scheme != "nostrconnect" || !nostr.IsValidPublicKey(uri.Host) { + return fmt.Errorf("invalid uri") + } + + return nil + }, + }, + }, } diff --git a/fetch.go b/fetch.go index 5972415..fb4c0ab 100644 --- a/fetch.go +++ b/fetch.go @@ -113,7 +113,7 @@ var fetch = &cli.Command{ continue } - for ie := range sys.Pool.SubManyEose(ctx, relays, nostr.Filters{filter}) { + for ie := range sys.Pool.FetchMany(ctx, relays, filter) { stdout(ie.Event) } } diff --git a/go.mod b/go.mod index beb0af0..e7ec183 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,7 @@ require ( 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.49.7 + github.com/nbd-wtf/go-nostr v0.50.0 golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 ) diff --git a/mcp.go b/mcp.go index daf36ed..bb56a4a 100644 --- a/mcp.go +++ b/mcp.go @@ -221,7 +221,7 @@ var mcpServer = &cli.Command{ filter.Authors = []string{pubkey} } - events := sys.Pool.SubManyEose(ctx, []string{relay}, nostr.Filters{filter}) + events := sys.Pool.FetchMany(ctx, []string{relay}, filter) result := strings.Builder{} for ie := range events { diff --git a/paginate.go b/paginate.go deleted file mode 100644 index 0f871e3..0000000 --- a/paginate.go +++ /dev/null @@ -1,76 +0,0 @@ -package main - -import ( - "context" - "math" - "slices" - "time" - - "github.com/nbd-wtf/go-nostr" -) - -func paginateWithParams( - interval time.Duration, - globalLimit uint64, -) func(ctx context.Context, urls []string, filters nostr.Filters, opts ...nostr.SubscriptionOption) chan nostr.RelayEvent { - return func(ctx context.Context, urls []string, filters nostr.Filters, opts ...nostr.SubscriptionOption) chan nostr.RelayEvent { - // filters will always be just one - filter := filters[0] - - nextUntil := nostr.Now() - if filter.Until != nil { - nextUntil = *filter.Until - } - - if globalLimit == 0 { - globalLimit = uint64(filter.Limit) - if globalLimit == 0 && !filter.LimitZero { - globalLimit = math.MaxUint64 - } - } - var globalCount uint64 = 0 - globalCh := make(chan nostr.RelayEvent) - - repeatedCache := make([]string, 0, 300) - nextRepeatedCache := make([]string, 0, 300) - - go func() { - defer close(globalCh) - - for { - filter.Until = &nextUntil - time.Sleep(interval) - - keepGoing := false - for evt := range sys.Pool.SubManyEose(ctx, urls, nostr.Filters{filter}, opts...) { - if slices.Contains(repeatedCache, evt.ID) { - continue - } - - keepGoing = true // if we get one that isn't repeated, then keep trying to get more - nextRepeatedCache = append(nextRepeatedCache, evt.ID) - - globalCh <- evt - - globalCount++ - if globalCount >= globalLimit { - return - } - - if evt.CreatedAt < *filter.Until { - nextUntil = evt.CreatedAt - } - } - - if !keepGoing { - return - } - - repeatedCache = nextRepeatedCache - nextRepeatedCache = nextRepeatedCache[:0] - } - }() - - return globalCh - } -} diff --git a/req.go b/req.go index 3a1a4bf..dd719fc 100644 --- a/req.go +++ b/req.go @@ -143,14 +143,14 @@ example: } } } else { - fn := sys.Pool.SubManyEose + fn := sys.Pool.FetchMany if c.Bool("paginate") { - fn = paginateWithParams(c.Duration("paginate-interval"), c.Uint("paginate-global-limit")) + fn = sys.Pool.PaginatorWithInterval(c.Duration("paginate-interval")) } else if c.Bool("stream") { - fn = sys.Pool.SubMany + fn = sys.Pool.SubscribeMany } - for ie := range fn(ctx, relayUrls, nostr.Filters{filter}) { + for ie := range fn(ctx, relayUrls, filter) { stdout(ie.Event) } } From 26930d40bc8dde022f468646d65992fcdbc50841 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sun, 16 Feb 2025 13:02:04 -0300 Subject: [PATCH 213/401] migrate to urfave/cli/v3 again now that they have flags after arguments. --- bunker.go | 2 +- count.go | 2 +- curl.go | 2 +- decode.go | 2 +- encode.go | 8 ++++---- encrypt_decrypt.go | 2 +- event.go | 2 +- fetch.go | 2 +- flags.go | 2 +- go.mod | 2 +- go.sum | 4 ++-- helpers.go | 2 +- helpers_key.go | 2 +- key.go | 2 +- main.go | 26 +++++++++++--------------- mcp.go | 2 +- outbox.go | 2 +- relay.go | 2 +- req.go | 2 +- serve.go | 2 +- verify.go | 2 +- wallet.go | 2 +- 22 files changed, 36 insertions(+), 40 deletions(-) diff --git a/bunker.go b/bunker.go index 73fef86..a1eedba 100644 --- a/bunker.go +++ b/bunker.go @@ -11,7 +11,7 @@ import ( "time" "github.com/fatih/color" - "github.com/fiatjaf/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/nip46" diff --git a/count.go b/count.go index 1ca9596..4599c0b 100644 --- a/count.go +++ b/count.go @@ -6,7 +6,7 @@ import ( "os" "strings" - "github.com/fiatjaf/cli/v3" + "github.com/urfave/cli/v3" "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/nip45" "github.com/nbd-wtf/go-nostr/nip45/hyperloglog" diff --git a/curl.go b/curl.go index 52fe474..9250898 100644 --- a/curl.go +++ b/curl.go @@ -8,7 +8,7 @@ import ( "os/exec" "strings" - "github.com/fiatjaf/cli/v3" + "github.com/urfave/cli/v3" "github.com/nbd-wtf/go-nostr" "golang.org/x/exp/slices" ) diff --git a/decode.go b/decode.go index 4b9a139..4ad2b13 100644 --- a/decode.go +++ b/decode.go @@ -5,7 +5,7 @@ import ( "encoding/hex" "strings" - "github.com/fiatjaf/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" diff --git a/encode.go b/encode.go index e1e44a0..c8dd419 100644 --- a/encode.go +++ b/encode.go @@ -4,9 +4,9 @@ import ( "context" "fmt" - "github.com/fiatjaf/cli/v3" "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/nip19" + "github.com/urfave/cli/v3" ) var encode = &cli.Command{ @@ -19,11 +19,11 @@ var encode = &cli.Command{ nak encode nevent nak encode nevent --author --relay --relay nak encode nsec `, - Before: func(ctx context.Context, c *cli.Command) error { + Before: func(ctx context.Context, c *cli.Command) (context.Context, error) { if c.Args().Len() < 1 { - return fmt.Errorf("expected more than 1 argument.") + return ctx, fmt.Errorf("expected more than 1 argument.") } - return nil + return ctx, nil }, DisableSliceFlagSeparator: true, Commands: []*cli.Command{ diff --git a/encrypt_decrypt.go b/encrypt_decrypt.go index b4cc5c5..930948a 100644 --- a/encrypt_decrypt.go +++ b/encrypt_decrypt.go @@ -4,7 +4,7 @@ import ( "context" "fmt" - "github.com/fiatjaf/cli/v3" + "github.com/urfave/cli/v3" "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/nip04" ) diff --git a/event.go b/event.go index d0ba01d..8d32bc1 100644 --- a/event.go +++ b/event.go @@ -8,7 +8,7 @@ import ( "strings" "time" - "github.com/fiatjaf/cli/v3" + "github.com/urfave/cli/v3" "github.com/mailru/easyjson" "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/nip13" diff --git a/fetch.go b/fetch.go index fb4c0ab..a2d141f 100644 --- a/fetch.go +++ b/fetch.go @@ -4,7 +4,7 @@ import ( "context" "fmt" - "github.com/fiatjaf/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" diff --git a/flags.go b/flags.go index 3de3d8f..e9ddbcc 100644 --- a/flags.go +++ b/flags.go @@ -6,7 +6,7 @@ import ( "strconv" "time" - "github.com/fiatjaf/cli/v3" + "github.com/urfave/cli/v3" "github.com/markusmobius/go-dateparser" "github.com/nbd-wtf/go-nostr" ) diff --git a/go.mod b/go.mod index e7ec183..2948186 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,6 @@ require ( github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 github.com/fatih/color v1.16.0 - github.com/fiatjaf/cli/v3 v3.0.0-20240723181502-e7dd498b16ae github.com/fiatjaf/eventstore v0.15.0 github.com/fiatjaf/khatru v0.15.0 github.com/json-iterator/go v1.1.12 @@ -18,6 +17,7 @@ require ( github.com/mark3labs/mcp-go v0.8.3 github.com/markusmobius/go-dateparser v1.2.3 github.com/nbd-wtf/go-nostr v0.50.0 + github.com/urfave/cli/v3 v3.0.0-beta1 golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 ) diff --git a/go.sum b/go.sum index 29d0121..037fea1 100644 --- a/go.sum +++ b/go.sum @@ -66,8 +66,6 @@ github.com/fasthttp/websocket v1.5.7 h1:0a6o2OfeATvtGgoMKleURhLT6JqWPg7fYfWnH4KH github.com/fasthttp/websocket v1.5.7/go.mod h1:bC4fxSono9czeXHQUVKxsC0sNjbm7lPJR04GDFqClfU= 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/cli/v3 v3.0.0-20240723181502-e7dd498b16ae h1:0B/1dU3YECIbPoBIRTQ4c0scZCNz9TVHtQpiODGrTTo= -github.com/fiatjaf/cli/v3 v3.0.0-20240723181502-e7dd498b16ae/go.mod h1:aAWPO4bixZZxPtOnH6K3q4GbQ0jftUNDW9Oa861IRew= github.com/fiatjaf/eventstore v0.15.0 h1:5UXe0+vIb30/cYcOWipks8nR3g+X8W224TFy5yPzivk= github.com/fiatjaf/eventstore v0.15.0/go.mod h1:KAsld5BhkmSck48aF11Txu8X+OGNmoabw4TlYVWqInc= github.com/fiatjaf/khatru v0.15.0 h1:0aLWiTrdzoKD4WmW35GWL/Jsn4dACCUw325JKZg/AmI= @@ -164,6 +162,8 @@ github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JT github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/urfave/cli/v3 v3.0.0-beta1 h1:6DTaaUarcM0wX7qj5Hcvs+5Dm3dyUTBbEwIWAjcw9Zg= +github.com/urfave/cli/v3 v3.0.0-beta1/go.mod h1:FnIeEMYu+ko8zP1F9Ypr3xkZMIDqW3DR92yUtY39q1Y= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA= diff --git a/helpers.go b/helpers.go index 9ae8ad5..106b8d7 100644 --- a/helpers.go +++ b/helpers.go @@ -15,7 +15,7 @@ import ( "time" "github.com/fatih/color" - "github.com/fiatjaf/cli/v3" + "github.com/urfave/cli/v3" jsoniter "github.com/json-iterator/go" "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/sdk" diff --git a/helpers_key.go b/helpers_key.go index 35511ab..d560001 100644 --- a/helpers_key.go +++ b/helpers_key.go @@ -9,7 +9,7 @@ import ( "github.com/chzyer/readline" "github.com/fatih/color" - "github.com/fiatjaf/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" diff --git a/key.go b/key.go index 2be2b8f..4ccd5cb 100644 --- a/key.go +++ b/key.go @@ -9,7 +9,7 @@ import ( "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" "github.com/decred/dcrd/dcrec/secp256k1/v4" - "github.com/fiatjaf/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" diff --git a/main.go b/main.go index 91aa68f..9fb116f 100644 --- a/main.go +++ b/main.go @@ -7,10 +7,10 @@ import ( "os" "path/filepath" - "github.com/fiatjaf/cli/v3" "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/sdk" "github.com/nbd-wtf/go-nostr/sdk/hints/memoryh" + "github.com/urfave/cli/v3" ) var version string = "debug" @@ -19,7 +19,6 @@ var app = &cli.Command{ Name: "nak", Suggest: true, UseShortOptionHandling: true, - AllowFlagsAfterArguments: true, Usage: "the nostr army knife command-line tool", DisableSliceFlagSeparator: true, Commands: []*cli.Command{ @@ -44,15 +43,13 @@ var app = &cli.Command{ Version: version, Flags: []cli.Flag{ &cli.StringFlag{ - Name: "config-path", - Hidden: true, - Persistent: true, + Name: "config-path", + Hidden: true, }, &cli.BoolFlag{ - Name: "quiet", - Usage: "do not print logs and info messages to stderr, use -qq to also not print anything to stdout", - Aliases: []string{"q"}, - Persistent: true, + Name: "quiet", + Usage: "do not print logs and info messages to stderr, use -qq to also not print anything to stdout", + Aliases: []string{"q"}, Action: func(ctx context.Context, c *cli.Command, b bool) error { q := c.Count("quiet") if q >= 1 { @@ -65,10 +62,9 @@ var app = &cli.Command{ }, }, &cli.BoolFlag{ - Name: "verbose", - Usage: "print more stuff than normally", - Aliases: []string{"v"}, - Persistent: true, + Name: "verbose", + Usage: "print more stuff than normally", + Aliases: []string{"v"}, Action: func(ctx context.Context, c *cli.Command, b bool) error { v := c.Count("verbose") if v >= 1 { @@ -78,7 +74,7 @@ var app = &cli.Command{ }, }, }, - Before: func(ctx context.Context, c *cli.Command) error { + Before: func(ctx context.Context, c *cli.Command) (context.Context, error) { configPath := c.String("config-path") if configPath == "" { if home, err := os.UserHomeDir(); err == nil { @@ -117,7 +113,7 @@ var app = &cli.Command{ ), ) - return nil + return ctx, nil }, After: func(ctx context.Context, c *cli.Command) error { // save hints database on exit diff --git a/mcp.go b/mcp.go index bb56a4a..4eba105 100644 --- a/mcp.go +++ b/mcp.go @@ -6,7 +6,7 @@ import ( "os" "strings" - "github.com/fiatjaf/cli/v3" + "github.com/urfave/cli/v3" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" "github.com/nbd-wtf/go-nostr" diff --git a/outbox.go b/outbox.go index 7ee859e..284f148 100644 --- a/outbox.go +++ b/outbox.go @@ -6,7 +6,7 @@ import ( "os" "path/filepath" - "github.com/fiatjaf/cli/v3" + "github.com/urfave/cli/v3" "github.com/nbd-wtf/go-nostr" ) diff --git a/relay.go b/relay.go index afc9a44..36f5729 100644 --- a/relay.go +++ b/relay.go @@ -10,7 +10,7 @@ import ( "io" "net/http" - "github.com/fiatjaf/cli/v3" + "github.com/urfave/cli/v3" "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/nip11" "github.com/nbd-wtf/go-nostr/nip86" diff --git a/req.go b/req.go index dd719fc..d189db0 100644 --- a/req.go +++ b/req.go @@ -6,7 +6,7 @@ import ( "os" "strings" - "github.com/fiatjaf/cli/v3" + "github.com/urfave/cli/v3" "github.com/mailru/easyjson" "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/nip77" diff --git a/serve.go b/serve.go index b06f61c..af0faea 100644 --- a/serve.go +++ b/serve.go @@ -10,7 +10,7 @@ import ( "github.com/bep/debounce" "github.com/fatih/color" - "github.com/fiatjaf/cli/v3" + "github.com/urfave/cli/v3" "github.com/fiatjaf/eventstore/slicestore" "github.com/fiatjaf/khatru" "github.com/nbd-wtf/go-nostr" diff --git a/verify.go b/verify.go index 5b584cb..bccd27e 100644 --- a/verify.go +++ b/verify.go @@ -3,7 +3,7 @@ package main import ( "context" - "github.com/fiatjaf/cli/v3" + "github.com/urfave/cli/v3" "github.com/nbd-wtf/go-nostr" ) diff --git a/wallet.go b/wallet.go index 87e5822..c6faacd 100644 --- a/wallet.go +++ b/wallet.go @@ -7,7 +7,7 @@ import ( "strings" "github.com/fatih/color" - "github.com/fiatjaf/cli/v3" + "github.com/urfave/cli/v3" "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/nip60" "github.com/nbd-wtf/go-nostr/nip61" From faca2e50f0846979e71d3ba2fe13d666e3e8d457 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Mon, 17 Feb 2025 16:54:31 -0300 Subject: [PATCH 214/401] adapt to FetchSpecificEvent() changes. --- mcp.go | 7 +++++-- wallet.go | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/mcp.go b/mcp.go index 4eba105..f664d9b 100644 --- a/mcp.go +++ b/mcp.go @@ -6,11 +6,12 @@ import ( "os" "strings" - "github.com/urfave/cli/v3" "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" ) var mcpServer = &cli.Command{ @@ -139,7 +140,9 @@ var mcpServer = &cli.Command{ pm.ShortName(), pm.PubKey), ), nil case "nevent": - event, _, err := sys.FetchSpecificEventFromInput(ctx, uri, false) + event, _, err := sys.FetchSpecificEventFromInput(ctx, uri, sdk.FetchSpecificEventParameters{ + WithRelays: false, + }) if err != nil { return mcp.NewToolResultError("Couldn't find this event anywhere"), nil } diff --git a/wallet.go b/wallet.go index c6faacd..aebde63 100644 --- a/wallet.go +++ b/wallet.go @@ -7,10 +7,11 @@ import ( "strings" "github.com/fatih/color" - "github.com/urfave/cli/v3" "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" + "github.com/urfave/cli/v3" ) func prepareWallet(ctx context.Context, c *cli.Command) (*nip60.Wallet, func(), error) { @@ -332,7 +333,9 @@ var wallet = &cli.Command{ var eventId string if strings.HasPrefix(target, "nevent1") { - evt, _, err = sys.FetchSpecificEventFromInput(ctx, target, false) + evt, _, err = sys.FetchSpecificEventFromInput(ctx, target, sdk.FetchSpecificEventParameters{ + WithRelays: false, + }) if err != nil { return err } From 707e5b3918cff690c29cd33542c4b7a3070b6de0 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Mon, 17 Feb 2025 17:00:21 -0300 Subject: [PATCH 215/401] req: print at least something when auth fails. --- req.go | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/req.go b/req.go index d189db0..3d3e799 100644 --- a/req.go +++ b/req.go @@ -6,10 +6,10 @@ import ( "os" "strings" - "github.com/urfave/cli/v3" "github.com/mailru/easyjson" "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/nip77" + "github.com/urfave/cli/v3" ) const ( @@ -80,7 +80,16 @@ example: relayUrls, c.Bool("force-pre-auth"), nostr.WithAuthHandler( - func(ctx context.Context, authEvent nostr.RelayEvent) error { + func(ctx context.Context, authEvent nostr.RelayEvent) (err error) { + defer func() { + if err != nil { + log("auth to %s failed: %s\n", + (*authEvent.Tags.GetFirst([]string{"relay", ""}))[1], + err, + ) + } + }() + if !c.Bool("auth") && !c.Bool("force-pre-auth") { return fmt.Errorf("auth not authorized") } From 43a3e5f40d57d5d68f5f81e610044b23a9a90040 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 18 Feb 2025 18:19:36 -0300 Subject: [PATCH 216/401] event: support reading --content from a file if the name starts with @. --- event.go | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/event.go b/event.go index 8d32bc1..d30bec8 100644 --- a/event.go +++ b/event.go @@ -8,11 +8,11 @@ import ( "strings" "time" - "github.com/urfave/cli/v3" "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" ) const ( @@ -94,7 +94,7 @@ example: &cli.StringFlag{ Name: "content", Aliases: []string{"c"}, - Usage: "event content", + Usage: "event content (if it starts with an '@' will read from a file)", DefaultText: "hello from the nostr army knife", Value: "", Category: CATEGORY_EVENT_FIELDS, @@ -179,7 +179,16 @@ example: } if c.IsSet("content") { - evt.Content = c.String("content") + content := c.String("content") + if strings.HasPrefix(content, "@") { + filedata, err := os.ReadFile(content[1:]) + if err != nil { + return fmt.Errorf("failed to read file '%s' for content: %w", content[1:], err) + } + evt.Content = string(filedata) + } else { + evt.Content = content + } mustRehashAndResign = true } else if evt.Content == "" && evt.Kind == 1 { evt.Content = "hello from the nostr army knife" From 7bb7543ef76d7d24a1c6825e47e6cfdeae2cff4d Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Mon, 3 Mar 2025 16:29:20 -0300 Subject: [PATCH 217/401] serve: setup relay info. --- go.sum | 4 ++-- serve.go | 8 +++++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/go.sum b/go.sum index 037fea1..01fa5eb 100644 --- a/go.sum +++ b/go.sum @@ -126,8 +126,8 @@ 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.49.7 h1:4D9XCqjTJYqUPMuNJI27W5gaiklnTI12IzzWIAOFepE= -github.com/nbd-wtf/go-nostr v0.49.7/go.mod h1:M50QnhkraC5Ol93v3jqxSMm1aGxUQm5mlmkYw5DJzh8= +github.com/nbd-wtf/go-nostr v0.50.0 h1:MgL/HPnWSTb5BFCL9RuzYQQpMrTi67MvHem4nWFn47E= +github.com/nbd-wtf/go-nostr v0.50.0/go.mod h1:M50QnhkraC5Ol93v3jqxSMm1aGxUQm5mlmkYw5DJzh8= 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= diff --git a/serve.go b/serve.go index af0faea..28489dc 100644 --- a/serve.go +++ b/serve.go @@ -10,10 +10,10 @@ import ( "github.com/bep/debounce" "github.com/fatih/color" - "github.com/urfave/cli/v3" "github.com/fiatjaf/eventstore/slicestore" "github.com/fiatjaf/khatru" "github.com/nbd-wtf/go-nostr" + "github.com/urfave/cli/v3" ) var serve = &cli.Command{ @@ -65,6 +65,12 @@ var serve = &cli.Command{ } rl := khatru.NewRelay() + + rl.Info.Name = "nak serve" + rl.Info.Description = "a local relay for testing, debugging and development." + 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) From e6448debf23e2d80d9507423777439e339c088a8 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Wed, 5 Mar 2025 00:32:40 -0300 Subject: [PATCH 218/401] blossom command moved into here. --- blossom.go | 222 +++++++++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 9 ++- go.sum | 20 +++-- main.go | 7 +- 4 files changed, 246 insertions(+), 12 deletions(-) create mode 100644 blossom.go diff --git a/blossom.go b/blossom.go new file mode 100644 index 0000000..f4b039f --- /dev/null +++ b/blossom.go @@ -0,0 +1,222 @@ +package main + +import ( + "context" + "fmt" + "os" + + "github.com/nbd-wtf/go-nostr/keyer" + "github.com/nbd-wtf/go-nostr/nipb0/blossom" + "github.com/urfave/cli/v3" +) + +var blossomCmd = &cli.Command{ + Name: "blossom", + Suggest: true, + UseShortOptionHandling: true, + Usage: "an army knife for blossom things", + DisableSliceFlagSeparator: true, + Flags: append(defaultKeyFlags, + &cli.StringFlag{ + Name: "server", + Aliases: []string{"s"}, + Usage: "the hostname of the target mediaserver", + Required: true, + }, + ), + Commands: []*cli.Command{ + { + Name: "list", + Usage: "lists blobs from a pubkey", + Description: `takes one pubkey passed as an argument or derives one from the --sec supplied. if that is given then it will also pre-authorize the list, which some servers may require.`, + DisableSliceFlagSeparator: true, + ArgsUsage: "[pubkey]", + Action: func(ctx context.Context, c *cli.Command) error { + var client *blossom.Client + pubkey := c.Args().First() + if pubkey != "" { + client = blossom.NewClient(client.GetMediaServer(), keyer.NewReadOnlySigner(pubkey)) + } else { + var err error + client, err = getBlossomClient(ctx, c) + if err != nil { + return err + } + } + + bds, err := client.List(ctx) + if err != nil { + return err + } + + for _, bd := range bds { + stdout(bd) + } + + return nil + }, + }, + { + Name: "upload", + Usage: "uploads a file to a specific mediaserver.", + Description: `takes any number of local file paths and uploads them to a mediaserver, printing the resulting blob descriptions when successful.`, + DisableSliceFlagSeparator: true, + ArgsUsage: "[files...]", + Action: func(ctx context.Context, c *cli.Command) error { + client, err := getBlossomClient(ctx, c) + if err != nil { + return err + } + + hasError := false + for _, fpath := range c.Args().Slice() { + bd, err := client.UploadFile(ctx, fpath) + if err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err) + hasError = true + continue + } + + j, _ := json.Marshal(bd) + stdout(string(j)) + } + + if hasError { + os.Exit(3) + } + return nil + }, + }, + { + Name: "download", + Usage: "downloads files from mediaservers", + Description: `takes any number of sha256 hashes as hex, downloads them and prints them to stdout (unless --output is specified).`, + DisableSliceFlagSeparator: true, + ArgsUsage: "[sha256...]", + Flags: []cli.Flag{ + &cli.StringSliceFlag{ + Name: "output", + Aliases: []string{"o"}, + Usage: "file name to save downloaded file to, can be passed multiple times when downloading multiple hashes", + }, + }, + Action: func(ctx context.Context, c *cli.Command) error { + client, err := getBlossomClient(ctx, c) + if err != nil { + return err + } + + outputs := c.StringSlice("output") + + hasError := false + for i, hash := range c.Args().Slice() { + if len(outputs)-1 >= i && outputs[i] != "--" { + // save to this file + err := client.DownloadToFile(ctx, hash, outputs[i]) + if err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err) + hasError = true + } + } else { + // if output wasn't specified, print to stdout + data, err := client.Download(ctx, hash) + if err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err) + hasError = true + continue + } + os.Stdout.Write(data) + } + } + + if hasError { + os.Exit(2) + } + return nil + }, + }, + { + Name: "del", + Aliases: []string{"delete"}, + Usage: "deletes a file from a mediaserver", + Description: `takes any number of sha256 hashes, signs authorizations and deletes them from the current mediaserver. + +if any of the files are not deleted command will fail, otherwise it will succeed. it will also print error messages to stderr and the hashes it successfully deletes to stdout.`, + DisableSliceFlagSeparator: true, + ArgsUsage: "[sha256...]", + Action: func(ctx context.Context, c *cli.Command) error { + client, err := getBlossomClient(ctx, c) + if err != nil { + return err + } + + hasError := false + for _, hash := range c.Args().Slice() { + err := client.Delete(ctx, hash) + if err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err) + hasError = true + continue + } + + stdout(hash) + } + + if hasError { + os.Exit(3) + } + return nil + }, + }, + { + Name: "check", + Usage: "asks the mediaserver if it has the specified hashes.", + Description: `uses the HEAD request to succintly check if the server has the specified sha256 hash. + +if any of the files are not found the command will fail, otherwise it will succeed. it will also print error messages to stderr and the hashes it finds to stdout.`, + DisableSliceFlagSeparator: true, + ArgsUsage: "[sha256...]", + Action: func(ctx context.Context, c *cli.Command) error { + client, err := getBlossomClient(ctx, c) + if err != nil { + return err + } + + hasError := false + for _, hash := range c.Args().Slice() { + err := client.Check(ctx, hash) + if err != nil { + hasError = true + fmt.Fprintf(os.Stderr, "%s\n", err) + continue + } + + stdout(hash) + } + + if hasError { + os.Exit(2) + } + return nil + }, + }, + { + Name: "mirror", + Usage: "", + Description: ``, + DisableSliceFlagSeparator: true, + ArgsUsage: "", + Action: func(ctx context.Context, c *cli.Command) error { + return nil + }, + }, + }, +} + +func getBlossomClient(ctx context.Context, c *cli.Command) (*blossom.Client, error) { + keyer, _, err := gatherKeyerFromArguments(ctx, c) + if err != nil { + return nil, err + } + return blossom.NewClient(c.String("server"), keyer), nil +} diff --git a/go.mod b/go.mod index 2948186..0866e84 100644 --- a/go.mod +++ b/go.mod @@ -16,13 +16,14 @@ require ( 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.50.0 + github.com/nbd-wtf/go-nostr v0.50.1 github.com/urfave/cli/v3 v3.0.0-beta1 golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 ) require ( fiatjaf.com/lib v0.2.0 // indirect + github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3 // indirect github.com/andybalholm/brotli v1.0.5 // indirect github.com/btcsuite/btcd v0.24.2 // indirect github.com/btcsuite/btcd/btcutil v1.1.5 // indirect @@ -44,10 +45,12 @@ require ( github.com/hablullah/go-juliandays v1.0.0 // indirect github.com/jalaali/go-jalaali v0.0.0-20210801064154-80525e88d958 // indirect github.com/josharian/intern v1.0.0 // indirect - github.com/klauspost/compress v1.17.11 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/magefile/mage v1.14.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/minio/simdjson-go v0.4.5 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pkg/errors v0.9.1 // indirect @@ -64,6 +67,6 @@ require ( github.com/x448/float16 v0.8.4 // indirect golang.org/x/crypto v0.32.0 // indirect golang.org/x/net v0.34.0 // indirect - golang.org/x/sys v0.29.0 // indirect + golang.org/x/sys v0.30.0 // indirect golang.org/x/text v0.21.0 // indirect ) diff --git a/go.sum b/go.sum index 01fa5eb..9f4c18c 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ fiatjaf.com/lib v0.2.0 h1:TgIJESbbND6GjOgGHxF5jsO6EMjuAxIzZHPo5DXYexs= fiatjaf.com/lib v0.2.0/go.mod h1:Ycqq3+mJ9jAWu7XjbQI1cVr+OFgnHn79dQR5oTII47g= +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/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= @@ -58,6 +60,7 @@ github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WA github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/dvyukov/go-fuzz v0.0.0-20200318091601-be3528f3a813/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw= github.com/elliotchance/pie/v2 v2.7.0 h1:FqoIKg4uj0G/CrLGuMS9ejnFKa92lxE1dEgBD3pShXg= github.com/elliotchance/pie/v2 v2.7.0/go.mod h1:18t0dgGFH006g4eVdDtWfgFZPQEgl10IoEO8YWEq3Og= github.com/elnosh/gonuts v0.3.1-0.20250123162555-7c0381a585e3 h1:k7evIqJ2BtFn191DgY/b03N2bMYA/iQwzr4f/uHYn20= @@ -106,8 +109,10 @@ github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlT github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= -github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= -github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= +github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo= github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= @@ -121,13 +126,15 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/minio/simdjson-go v0.4.5 h1:r4IQwjRGmWCQ2VeMc7fGiilu1z5du0gJ/I/FsKwgo5A= +github.com/minio/simdjson-go v0.4.5/go.mod h1:eoNz0DcLQRyEDeaPr4Ru6JpjlZPzbA0IodxVJk8lO8E= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 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.50.0 h1:MgL/HPnWSTb5BFCL9RuzYQQpMrTi67MvHem4nWFn47E= -github.com/nbd-wtf/go-nostr v0.50.0/go.mod h1:M50QnhkraC5Ol93v3jqxSMm1aGxUQm5mlmkYw5DJzh8= +github.com/nbd-wtf/go-nostr v0.50.1 h1:l02wKcnYVyvjnj53CNB3I/C161uoH1W51sPWbrLb9C0= +github.com/nbd-wtf/go-nostr v0.50.1/go.mod h1:gOzf8mcTlMs7e+LjYDssRU4Vb/YGeeYO61aDNrxDStY= 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= @@ -149,6 +156,7 @@ github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1Avp github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= @@ -200,8 +208,8 @@ golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= -golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= diff --git a/main.go b/main.go index 9fb116f..495f129 100644 --- a/main.go +++ b/main.go @@ -22,10 +22,10 @@ var app = &cli.Command{ Usage: "the nostr army knife command-line tool", DisableSliceFlagSeparator: true, Commands: []*cli.Command{ - req, - count, - fetch, event, + req, + fetch, + count, decode, encode, key, @@ -33,6 +33,7 @@ var app = &cli.Command{ relay, bunker, serve, + blossomCmd, encrypt, decrypt, outbox, From f5316a0f35a624cc0a56b12bc3da5e2afa65c86d Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Wed, 5 Mar 2025 22:01:24 -0300 Subject: [PATCH 219/401] preliminary (broken) dvm support. --- dvm.go | 134 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ event.go | 1 - go.mod | 4 +- go.sum | 4 +- main.go | 1 + relay.go | 11 ++--- 6 files changed, 146 insertions(+), 9 deletions(-) create mode 100644 dvm.go diff --git a/dvm.go b/dvm.go new file mode 100644 index 0000000..659ca61 --- /dev/null +++ b/dvm.go @@ -0,0 +1,134 @@ +package main + +import ( + "context" + "fmt" + "os" + "strconv" + "strings" + + "github.com/fatih/color" + "github.com/nbd-wtf/go-nostr" + "github.com/nbd-wtf/go-nostr/nip90" + "github.com/urfave/cli/v3" +) + +var dvm = &cli.Command{ + Name: "dvm", + Usage: "deal with nip90 data-vending-machine things (experimental)", + Description: `example usage: + nak dvm 5001 --input "What is the capital of France?" --input-type text --output "text/plain" --bid 1000 wss://relay.example.com`, + DisableSliceFlagSeparator: true, + Flags: append(defaultKeyFlags, + &cli.StringSliceFlag{ + Name: "relay", + Aliases: []string{"r"}, + }, + ), + Commands: append([]*cli.Command{ + { + Name: "list", + Usage: "find DVMs that have announced themselves for a specific kind", + Action: func(ctx context.Context, c *cli.Command) error { + return fmt.Errorf("we don't know how to do this yet") + }, + }, + }, (func() []*cli.Command { + commands := make([]*cli.Command, len(nip90.Jobs)) + for i, job := range nip90.Jobs { + flags := make([]cli.Flag, 0, 2+len(job.Params)) + + if job.InputType != "" { + flags = append(flags, &cli.StringSliceFlag{ + Name: "input", + Category: "INPUT", + }) + } + + for _, param := range job.Params { + flags = append(flags, &cli.StringSliceFlag{ + Name: param, + Category: "PARAMETER", + }) + } + + commands[i] = &cli.Command{ + Name: strconv.Itoa(job.InputKind), + Usage: job.Name, + Description: job.Description, + Flags: flags, + Action: func(ctx context.Context, c *cli.Command) error { + relayUrls := c.StringSlice("relay") + relays := connectToAllRelays(ctx, relayUrls, false) + if len(relays) == 0 { + log("failed to connect to any of the given relays.\n") + os.Exit(3) + } + defer func() { + for _, relay := range relays { + relay.Close() + } + }() + + evt := nostr.Event{ + Kind: job.InputKind, + Tags: make(nostr.Tags, 0, 2+len(job.Params)), + CreatedAt: nostr.Now(), + } + + for _, input := range c.StringSlice("input") { + evt.Tags = append(evt.Tags, nostr.Tag{"i", input, job.InputType}) + } + for _, paramN := range job.Params { + for _, paramV := range c.StringSlice(paramN) { + tag := nostr.Tag{"param", paramN, "", ""}[0:2] + for _, v := range strings.Split(paramV, ";") { + tag = append(tag, v) + } + evt.Tags = append(evt.Tags, tag) + } + } + + kr, _, err := gatherKeyerFromArguments(ctx, c) + if err != nil { + return err + } + if err := kr.SignEvent(ctx, &evt); err != nil { + return err + } + + log("- publishing job request... ") + first := true + for res := range sys.Pool.PublishMany(ctx, relayUrls, evt) { + cleanUrl, ok := strings.CutPrefix(res.RelayURL, "wss://") + if !ok { + cleanUrl = res.RelayURL + } + + if !first { + log(", ") + } + first = false + + if res.Error != nil { + log("%s: %s", color.RedString(cleanUrl), res.Error) + } else { + log("%s: ok", color.GreenString(cleanUrl)) + } + } + + log("\n- waiting for response...") + for ie := range sys.Pool.SubscribeMany(ctx, relayUrls, nostr.Filter{ + Kinds: []int{7000, job.OutputKind}, + Tags: nostr.TagMap{"e": []string{evt.ID}}, + }) { + stdout(ie.Event) + } + + return nil + }, + } + } + return commands + })()...), +} diff --git a/event.go b/event.go index d30bec8..dbc2a7c 100644 --- a/event.go +++ b/event.go @@ -140,7 +140,6 @@ example: os.Exit(3) } } - defer func() { for _, relay := range relays { relay.Close() diff --git a/go.mod b/go.mod index 0866e84..b0bf039 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ require ( 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.50.1 + github.com/nbd-wtf/go-nostr v0.50.2 github.com/urfave/cli/v3 v3.0.0-beta1 golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 ) @@ -70,3 +70,5 @@ require ( golang.org/x/sys v0.30.0 // indirect golang.org/x/text v0.21.0 // indirect ) + +replace github.com/nbd-wtf/go-nostr => ../go-nostr diff --git a/go.sum b/go.sum index 9f4c18c..1ef0112 100644 --- a/go.sum +++ b/go.sum @@ -133,8 +133,8 @@ 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.50.1 h1:l02wKcnYVyvjnj53CNB3I/C161uoH1W51sPWbrLb9C0= -github.com/nbd-wtf/go-nostr v0.50.1/go.mod h1:gOzf8mcTlMs7e+LjYDssRU4Vb/YGeeYO61aDNrxDStY= +github.com/nbd-wtf/go-nostr v0.50.2 h1:OMmxztiAPj9cjZ16C+Z13YTJBF91RKEm4HZm5ogKD/c= +github.com/nbd-wtf/go-nostr v0.50.2/go.mod h1:gOzf8mcTlMs7e+LjYDssRU4Vb/YGeeYO61aDNrxDStY= 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= diff --git a/main.go b/main.go index 495f129..96c4b47 100644 --- a/main.go +++ b/main.go @@ -40,6 +40,7 @@ var app = &cli.Command{ wallet, mcpServer, curl, + dvm, }, Version: version, Flags: []cli.Flag{ diff --git a/relay.go b/relay.go index 36f5729..e464fcb 100644 --- a/relay.go +++ b/relay.go @@ -10,10 +10,10 @@ import ( "io" "net/http" - "github.com/urfave/cli/v3" "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/nip11" "github.com/nbd-wtf/go-nostr/nip86" + "github.com/urfave/cli/v3" ) var relay = &cli.Command{ @@ -45,9 +45,7 @@ var relay = &cli.Command{ return nil }, Commands: (func() []*cli.Command { - commands := make([]*cli.Command, 0, 12) - - for _, def := range []struct { + methods := []struct { method string args []string }{ @@ -69,7 +67,10 @@ var relay = &cli.Command{ {"blockip", []string{"ip", "reason"}}, {"unblockip", []string{"ip", "reason"}}, {"listblockedips", nil}, - } { + } + + commands := make([]*cli.Command, 0, len(methods)) + for _, def := range methods { def := def flags := make([]cli.Flag, len(def.args), len(def.args)+4) From c60bb82be84c64d1c025b0ccff10f0d2a42358fe Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Thu, 6 Mar 2025 08:06:27 -0300 Subject: [PATCH 220/401] small fixes on dvm flags and stuff. --- curl.go | 11 ++++++----- dvm.go | 25 ++++++++++++++----------- relay.go | 3 ++- 3 files changed, 22 insertions(+), 17 deletions(-) diff --git a/curl.go b/curl.go index 9250898..973746d 100644 --- a/curl.go +++ b/curl.go @@ -8,18 +8,19 @@ import ( "os/exec" "strings" - "github.com/urfave/cli/v3" "github.com/nbd-wtf/go-nostr" + "github.com/urfave/cli/v3" "golang.org/x/exp/slices" ) var curlFlags []string var curl = &cli.Command{ - Name: "curl", - Usage: "calls curl but with a nip98 header", - Description: "accepts all flags and arguments exactly as they would be passed to curl.", - Flags: defaultKeyFlags, + Name: "curl", + Usage: "calls curl but with a nip98 header", + Description: "accepts all flags and arguments exactly as they would be passed to curl.", + Flags: defaultKeyFlags, + DisableSliceFlagSeparator: true, Action: func(ctx context.Context, c *cli.Command) error { kr, _, err := gatherKeyerFromArguments(ctx, c) if err != nil { diff --git a/dvm.go b/dvm.go index 659ca61..f4142fc 100644 --- a/dvm.go +++ b/dvm.go @@ -14,10 +14,8 @@ import ( ) var dvm = &cli.Command{ - Name: "dvm", - Usage: "deal with nip90 data-vending-machine things (experimental)", - Description: `example usage: - nak dvm 5001 --input "What is the capital of France?" --input-type text --output "text/plain" --bid 1000 wss://relay.example.com`, + Name: "dvm", + Usage: "deal with nip90 data-vending-machine things (experimental)", DisableSliceFlagSeparator: true, Flags: append(defaultKeyFlags, &cli.StringSliceFlag{ @@ -27,8 +25,9 @@ var dvm = &cli.Command{ ), Commands: append([]*cli.Command{ { - Name: "list", - Usage: "find DVMs that have announced themselves for a specific kind", + Name: "list", + Usage: "find DVMs that have announced themselves for a specific kind", + DisableSliceFlagSeparator: true, Action: func(ctx context.Context, c *cli.Command) error { return fmt.Errorf("we don't know how to do this yet") }, @@ -41,6 +40,7 @@ var dvm = &cli.Command{ if job.InputType != "" { flags = append(flags, &cli.StringSliceFlag{ Name: "input", + Aliases: []string{"i"}, Category: "INPUT", }) } @@ -53,10 +53,11 @@ var dvm = &cli.Command{ } commands[i] = &cli.Command{ - Name: strconv.Itoa(job.InputKind), - Usage: job.Name, - Description: job.Description, - Flags: flags, + Name: strconv.Itoa(job.InputKind), + Usage: job.Name, + Description: job.Description, + DisableSliceFlagSeparator: true, + Flags: flags, Action: func(ctx context.Context, c *cli.Command) error { relayUrls := c.StringSlice("relay") relays := connectToAllRelays(ctx, relayUrls, false) @@ -97,6 +98,8 @@ var dvm = &cli.Command{ return err } + logverbose("%s", evt) + log("- publishing job request... ") first := true for res := range sys.Pool.PublishMany(ctx, relayUrls, evt) { @@ -117,7 +120,7 @@ var dvm = &cli.Command{ } } - log("\n- waiting for response...") + log("\n- waiting for response...\n") for ie := range sys.Pool.SubscribeMany(ctx, relayUrls, nostr.Filter{ Kinds: []int{7000, job.OutputKind}, Tags: nostr.TagMap{"e": []string{evt.ID}}, diff --git a/relay.go b/relay.go index e464fcb..b05f580 100644 --- a/relay.go +++ b/relay.go @@ -85,6 +85,8 @@ var relay = &cli.Command{ Usage: fmt.Sprintf(`the "%s" relay management RPC call`, def.method), Description: fmt.Sprintf( `the "%s" management RPC call, see https://nips.nostr.com/86 for more information`, def.method), + Flags: flags, + DisableSliceFlagSeparator: true, Action: func(ctx context.Context, c *cli.Command) error { params := make([]any, len(def.args)) for i, argName := range def.args { @@ -174,7 +176,6 @@ var relay = &cli.Command{ return nil }, - Flags: flags, } commands = append(commands, cmd) From c1248eb37b0867b2e5b245ff7edf300aa39e771d Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Fri, 7 Mar 2025 10:23:37 -0300 Subject: [PATCH 221/401] update dependencies, includes authenticated blossom blob uploads. --- go.mod | 25 ++++++++++++++----------- go.sum | 58 ++++++++++++++++++++++++++++++++++++++++------------------ 2 files changed, 54 insertions(+), 29 deletions(-) diff --git a/go.mod b/go.mod index b0bf039..04a3fb1 100644 --- a/go.mod +++ b/go.mod @@ -11,33 +11,36 @@ require ( github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 github.com/fatih/color v1.16.0 github.com/fiatjaf/eventstore v0.15.0 - github.com/fiatjaf/khatru v0.15.0 + github.com/fiatjaf/khatru v0.16.0 github.com/json-iterator/go v1.1.12 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.50.2 + github.com/nbd-wtf/go-nostr v0.50.3 github.com/urfave/cli/v3 v3.0.0-beta1 - golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 + golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac ) require ( fiatjaf.com/lib v0.2.0 // indirect github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3 // indirect - github.com/andybalholm/brotli v1.0.5 // indirect + github.com/andybalholm/brotli v1.1.1 // indirect github.com/btcsuite/btcd v0.24.2 // indirect github.com/btcsuite/btcd/btcutil v1.1.5 // indirect github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect + github.com/bytedance/sonic v1.12.10 // indirect + github.com/bytedance/sonic/loader v0.2.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chzyer/logex v1.1.10 // indirect github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect + github.com/cloudwego/base64x v0.1.5 // indirect github.com/coder/websocket v1.8.12 // indirect github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect github.com/dgraph-io/ristretto v1.0.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/elliotchance/pie/v2 v2.7.0 // indirect github.com/elnosh/gonuts v0.3.1-0.20250123162555-7c0381a585e3 // indirect - github.com/fasthttp/websocket v1.5.7 // indirect + github.com/fasthttp/websocket v1.5.12 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/graph-gophers/dataloader/v7 v7.1.0 // indirect @@ -54,21 +57,21 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/puzpuzpuz/xsync/v3 v3.4.0 // indirect + github.com/puzpuzpuz/xsync/v3 v3.5.0 // indirect github.com/rs/cors v1.11.1 // indirect - github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect + github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 // indirect github.com/tetratelabs/wazero v1.8.0 // indirect github.com/tidwall/gjson v1.18.0 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/valyala/fasthttp v1.51.0 // indirect + github.com/valyala/fasthttp v1.58.0 // indirect github.com/wasilibs/go-re2 v1.3.0 // indirect github.com/x448/float16 v0.8.4 // indirect + golang.org/x/arch v0.15.0 // indirect golang.org/x/crypto v0.32.0 // indirect golang.org/x/net v0.34.0 // indirect - golang.org/x/sys v0.30.0 // indirect + golang.org/x/sys v0.31.0 // indirect golang.org/x/text v0.21.0 // indirect ) - -replace github.com/nbd-wtf/go-nostr => ../go-nostr diff --git a/go.sum b/go.sum index 1ef0112..29fc05b 100644 --- a/go.sum +++ b/go.sum @@ -3,8 +3,8 @@ fiatjaf.com/lib v0.2.0/go.mod h1:Ycqq3+mJ9jAWu7XjbQI1cVr+OFgnHn79dQR5oTII47g= 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/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= -github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= -github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= +github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= @@ -33,6 +33,11 @@ github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= +github.com/bytedance/sonic v1.12.10 h1:uVCQr6oS5669E9ZVW0HyksTLfNS7Q/9hV6IVS4nEMsI= +github.com/bytedance/sonic v1.12.10/go.mod h1:uVvFidNmlt9+wa31S1urfwwthTWteBgG0hWuoKAXTx8= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/bytedance/sonic/loader v0.2.3 h1:yctD0Q3v2NOGfSWPLPvG2ggA2kV6TS6s4wioyEqssH0= +github.com/bytedance/sonic/loader v0.2.3/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= 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= @@ -41,6 +46,9 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5O github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= +github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo= github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -65,14 +73,14 @@ github.com/elliotchance/pie/v2 v2.7.0 h1:FqoIKg4uj0G/CrLGuMS9ejnFKa92lxE1dEgBD3p github.com/elliotchance/pie/v2 v2.7.0/go.mod h1:18t0dgGFH006g4eVdDtWfgFZPQEgl10IoEO8YWEq3Og= github.com/elnosh/gonuts v0.3.1-0.20250123162555-7c0381a585e3 h1:k7evIqJ2BtFn191DgY/b03N2bMYA/iQwzr4f/uHYn20= github.com/elnosh/gonuts v0.3.1-0.20250123162555-7c0381a585e3/go.mod h1:vgZomh4YQk7R3w4ltZc0sHwCmndfHkuX6V4sga/8oNs= -github.com/fasthttp/websocket v1.5.7 h1:0a6o2OfeATvtGgoMKleURhLT6JqWPg7fYfWnH4KHau4= -github.com/fasthttp/websocket v1.5.7/go.mod h1:bC4fxSono9czeXHQUVKxsC0sNjbm7lPJR04GDFqClfU= +github.com/fasthttp/websocket v1.5.12 h1:e4RGPpWW2HTbL3zV0Y/t7g0ub294LkiuXXUuTOUInlE= +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.15.0 h1:5UXe0+vIb30/cYcOWipks8nR3g+X8W224TFy5yPzivk= github.com/fiatjaf/eventstore v0.15.0/go.mod h1:KAsld5BhkmSck48aF11Txu8X+OGNmoabw4TlYVWqInc= -github.com/fiatjaf/khatru v0.15.0 h1:0aLWiTrdzoKD4WmW35GWL/Jsn4dACCUw325JKZg/AmI= -github.com/fiatjaf/khatru v0.15.0/go.mod h1:GBQJXZpitDatXF9RookRXcWB5zCJclCE4ufDK3jk80g= +github.com/fiatjaf/khatru v0.16.0 h1:xgGwnnOqE3989wEWm7c/z6Y6g4X92BFe/Xp1UWQ3Zmc= +github.com/fiatjaf/khatru v0.16.0/go.mod h1:TLcMgPy3IAPh40VGYq6m+gxEMpDKHj+sumqcuvbSogc= 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= @@ -111,8 +119,10 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo= github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= @@ -133,8 +143,8 @@ 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.50.2 h1:OMmxztiAPj9cjZ16C+Z13YTJBF91RKEm4HZm5ogKD/c= -github.com/nbd-wtf/go-nostr v0.50.2/go.mod h1:gOzf8mcTlMs7e+LjYDssRU4Vb/YGeeYO61aDNrxDStY= +github.com/nbd-wtf/go-nostr v0.50.3 h1:mRUJLOkCqnNTAwvjtSRogJyN3SUv1lze1UgnmqUBN0Q= +github.com/nbd-wtf/go-nostr v0.50.3/go.mod h1:XJyV09CfSZCtuf1ApdQFc+3RuEYzt4E/pbXn+doA8tQ= 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= @@ -148,16 +158,21 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/puzpuzpuz/xsync/v3 v3.4.0 h1:DuVBAdXuGFHv8adVXjWWZ63pJq+NRXOWVXlKDBZ+mJ4= -github.com/puzpuzpuz/xsync/v3 v3.4.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= +github.com/puzpuzpuz/xsync/v3 v3.5.0 h1:i+cMcpEDY1BkNm7lPDkCtE4oElsYLn+EKF8kAu2vXT4= +github.com/puzpuzpuz/xsync/v3 v3.5.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= -github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk= -github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g= +github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 h1:D0vL7YNisV2yqE55+q0lFuGse6U8lxlg7fYTctlT5Gc= +github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38/go.mod h1:sM7Mt7uEoCeFSCBM+qBrqvEo+/9vdmj19wzp3yzUhmg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= @@ -170,25 +185,31 @@ github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JT github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/urfave/cli/v3 v3.0.0-beta1 h1:6DTaaUarcM0wX7qj5Hcvs+5Dm3dyUTBbEwIWAjcw9Zg= github.com/urfave/cli/v3 v3.0.0-beta1/go.mod h1:FnIeEMYu+ko8zP1F9Ypr3xkZMIDqW3DR92yUtY39q1Y= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA= -github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g= +github.com/valyala/fasthttp v1.58.0 h1:GGB2dWxSbEprU9j0iMJHgdKYJVDyjrOwF9RE59PbRuE= +github.com/valyala/fasthttp v1.58.0/go.mod h1:SYXvHHaFp7QZHGKSHmoMipInhrI5StHrhDTYVEjK/Kw= github.com/wasilibs/go-re2 v1.3.0 h1:LFhBNzoStM3wMie6rN2slD1cuYH2CGiHpvNL3UtcsMw= github.com/wasilibs/go-re2 v1.3.0/go.mod h1:AafrCXVvGRJJOImMajgJ2M7rVmWyisVK7sFshbxnVrg= github.com/wasilibs/nottinygc v0.4.0 h1:h1TJMihMC4neN6Zq+WKpLxgd9xCFMw7O9ETLwY2exJQ= github.com/wasilibs/nottinygc v0.4.0/go.mod h1:oDcIotskuYNMpqMF23l7Z8uzD4TC0WXHK8jetlB3HIo= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +golang.org/x/arch v0.15.0 h1:QtOrQd0bTUnhNVNndMpLHNWrDmYzZ2KDqSrEymqInZw= +golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= -golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA= -golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU= +golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac h1:l5+whBCLH3iH2ZNHYLbAe58bo7yrN4mVcnkHDYz5vvs= +golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac/go.mod h1:hH+7mtFmImwwcMvScyxUhjuVHR3HGaDPMn9rMSUUbxo= golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -208,8 +229,8 @@ golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -233,3 +254,4 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= From d6a23bd00c1ec8f4dce32d11dcc5de70d521a5a0 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sat, 8 Mar 2025 10:51:44 -0300 Subject: [PATCH 222/401] experimental `nak fs` --- .gitignore | 1 + fs.go | 222 +++++++++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 10 ++- go.sum | 22 ++++-- helpers.go | 8 +- main.go | 7 +- 6 files changed, 256 insertions(+), 14 deletions(-) create mode 100644 fs.go diff --git a/.gitignore b/.gitignore index 1e09c80..9f82d74 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ nak +mnt diff --git a/fs.go b/fs.go new file mode 100644 index 0000000..b5845e4 --- /dev/null +++ b/fs.go @@ -0,0 +1,222 @@ +package main + +import ( + "context" + "fmt" + "os" + "os/signal" + "strings" + "sync/atomic" + "syscall" + "time" + + "github.com/colduction/nocopy" + "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/urfave/cli/v3" +) + +var fsCmd = &cli.Command{ + Name: "fs", + Usage: "mount a FUSE filesystem that exposes Nostr events as files.", + Description: `(experimental)`, + ArgsUsage: "", + Flags: []cli.Flag{ + &cli.StringFlag{ + 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) + }, + }, + }, + DisableSliceFlagSeparator: true, + Action: func(ctx context.Context, c *cli.Command) error { + mountpoint := c.Args().First() + if mountpoint == "" { + return fmt.Errorf("must be called with a directory path to serve as the mountpoint as an argument") + } + + root := &NostrRoot{ctx: ctx, rootPubKey: c.String("pubkey")} + + // create the server + log("- mounting at %s... ", color.HiCyanString(mountpoint)) + timeout := time.Second * 120 + server, err := fs.Mount(mountpoint, root, &fs.Options{ + MountOptions: fuse.MountOptions{ + Debug: isVerbose, + Name: "nak", + }, + AttrTimeout: &timeout, + EntryTimeout: &timeout, + Logger: nostr.DebugLogger, + }) + if err != nil { + return fmt.Errorf("mount failed: %w", err) + } + log("ok\n") + + // setup signal handling for clean unmount + ch := make(chan os.Signal, 1) + chErr := make(chan error) + signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM) + go func() { + <-ch + log("- unmounting... ") + err := server.Unmount() + if err != nil { + chErr <- fmt.Errorf("unmount failed: %w", err) + } else { + log("ok\n") + chErr <- nil + } + }() + + // serve the filesystem until unmounted + server.Wait() + return <-chErr + }, +} + +type NostrRoot struct { + fs.Inode + rootPubKey string + ctx context.Context +} + +var _ = (fs.NodeOnAdder)((*NostrRoot)(nil)) + +func (r *NostrRoot) OnAdd(context.Context) { + if r.rootPubKey == "" { + return + } + + fl := sys.FetchFollowList(r.ctx, r.rootPubKey) + + for _, f := range fl.Items { + h := r.NewPersistentInode( + r.ctx, + &NpubDir{pointer: nostr.ProfilePointer{PublicKey: f.Pubkey, Relays: []string{f.Relay}}}, + fs.StableAttr{Mode: syscall.S_IFDIR}, + ) + npub, _ := nip19.EncodePublicKey(f.Pubkey) + r.AddChild(npub, h, true) + } +} + +func (r *NostrRoot) Lookup(ctx context.Context, name string, out *fuse.EntryOut) (*fs.Inode, syscall.Errno) { + // check if we already have this npub + child := r.GetChild(name) + if child != nil { + return child, fs.OK + } + + // if the name starts with "npub1" or "nprofile1", create a new npub directory + if strings.HasPrefix(name, "npub1") || strings.HasPrefix(name, "nprofile1") { + npubdir, err := NewNpubDir(name) + if err != nil { + return nil, syscall.ENOENT + } + + return r.NewPersistentInode( + ctx, + npubdir, + fs.StableAttr{Mode: syscall.S_IFDIR}, + ), 0 + } + + return nil, syscall.ENOENT +} + +type NpubDir struct { + fs.Inode + pointer nostr.ProfilePointer + ctx context.Context + fetched atomic.Bool +} + +func NewNpubDir(npub string) (*NpubDir, error) { + pointer, err := nip19.ToPointer(npub) + if err != nil { + return nil, err + } + + pp, ok := pointer.(nostr.ProfilePointer) + if !ok { + return nil, fmt.Errorf("directory must be npub or nprofile") + } + + return &NpubDir{pointer: pp}, nil +} + +var _ = (fs.NodeOpendirer)((*NpubDir)(nil)) + +func (n *NpubDir) Opendir(ctx context.Context) syscall.Errno { + if n.fetched.CompareAndSwap(true, true) { + return fs.OK + } + + for ie := range sys.Pool.FetchMany(ctx, sys.FetchOutboxRelays(ctx, n.pointer.PublicKey, 2), nostr.Filter{ + Kinds: []int{1}, + Authors: []string{n.pointer.PublicKey}, + }, nostr.WithLabel("nak-fs-feed")) { + h := n.NewPersistentInode( + ctx, + &EventFile{ctx: ctx, evt: *ie.Event}, + fs.StableAttr{ + Mode: syscall.S_IFREG, + Ino: hexToUint64(ie.Event.ID), + }, + ) + n.AddChild(ie.Event.ID, h, true) + } + + return fs.OK +} + +type EventFile struct { + fs.Inode + ctx context.Context + evt nostr.Event +} + +var ( + _ = (fs.NodeOpener)((*EventFile)(nil)) + _ = (fs.NodeGetattrer)((*EventFile)(nil)) +) + +func (c *EventFile) Getattr(ctx context.Context, fh fs.FileHandle, out *fuse.AttrOut) syscall.Errno { + out.Mode = 0444 + out.Size = uint64(len(c.evt.String())) + ts := uint64(c.evt.CreatedAt) + out.Atime = ts + out.Mtime = ts + out.Ctime = ts + + return fs.OK +} + +func (c *EventFile) Open(ctx context.Context, flags uint32) (fs.FileHandle, uint32, syscall.Errno) { + return nil, fuse.FOPEN_KEEP_CACHE, 0 +} + +func (c *EventFile) Read( + ctx context.Context, + fh fs.FileHandle, + dest []byte, + off int64, +) (fuse.ReadResult, syscall.Errno) { + buf := c.evt.String() + + end := int(off) + len(dest) + if end > len(buf) { + end = len(c.evt.Content) + } + return fuse.ReadResultData(nocopy.StringToByteSlice(c.evt.Content[off:end])), fs.OK +} diff --git a/go.mod b/go.mod index 04a3fb1..a2f633d 100644 --- a/go.mod +++ b/go.mod @@ -8,15 +8,17 @@ require ( 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/colduction/nocopy v0.2.0 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 github.com/fatih/color v1.16.0 github.com/fiatjaf/eventstore v0.15.0 github.com/fiatjaf/khatru v0.16.0 + github.com/hanwen/go-fuse/v2 v2.7.2 github.com/json-iterator/go v1.1.12 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.50.3 + github.com/nbd-wtf/go-nostr v0.50.5 github.com/urfave/cli/v3 v3.0.0-beta1 golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac ) @@ -28,8 +30,8 @@ require ( github.com/btcsuite/btcd v0.24.2 // indirect github.com/btcsuite/btcd/btcutil v1.1.5 // indirect github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect - github.com/bytedance/sonic v1.12.10 // indirect - github.com/bytedance/sonic/loader v0.2.3 // indirect + github.com/bytedance/sonic v1.13.1 // indirect + github.com/bytedance/sonic/loader v0.2.4 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chzyer/logex v1.1.10 // indirect github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect @@ -50,10 +52,10 @@ require ( github.com/josharian/intern v1.0.0 // indirect github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect + github.com/kylelemons/godebug v1.1.0 // indirect github.com/magefile/mage v1.14.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/minio/simdjson-go v0.4.5 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pkg/errors v0.9.1 // indirect diff --git a/go.sum b/go.sum index 29fc05b..0fe7b5f 100644 --- a/go.sum +++ b/go.sum @@ -33,11 +33,11 @@ github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= -github.com/bytedance/sonic v1.12.10 h1:uVCQr6oS5669E9ZVW0HyksTLfNS7Q/9hV6IVS4nEMsI= -github.com/bytedance/sonic v1.12.10/go.mod h1:uVvFidNmlt9+wa31S1urfwwthTWteBgG0hWuoKAXTx8= +github.com/bytedance/sonic v1.13.1 h1:Jyd5CIvdFnkOWuKXr+wm4Nyk2h0yAFsr8ucJgEasO3g= +github.com/bytedance/sonic v1.13.1/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= -github.com/bytedance/sonic/loader v0.2.3 h1:yctD0Q3v2NOGfSWPLPvG2ggA2kV6TS6s4wioyEqssH0= -github.com/bytedance/sonic/loader v0.2.3/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= +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/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= @@ -51,6 +51,8 @@ github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJ github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo= github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= +github.com/colduction/nocopy v0.2.0 h1:9jMLCmIP/wnAWO0FfSXJ4h5HBRe6cBqIqacWw/5sRXY= +github.com/colduction/nocopy v0.2.0/go.mod h1:MO+QBkEnsZYE7QukMAcAq4b0rHpSxOTlVqD3fI34YJs= github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -106,6 +108,8 @@ github.com/hablullah/go-hijri v1.0.2 h1:drT/MZpSZJQXo7jftf5fthArShcaMtsal0Zf/dnm github.com/hablullah/go-hijri v1.0.2/go.mod h1:OS5qyYLDjORXzK4O1adFw9Q5WfhOcMdAKglDkcTxgWQ= github.com/hablullah/go-juliandays v1.0.0 h1:A8YM7wIj16SzlKT0SRJc9CD29iiaUzpBLzh5hr0/5p0= github.com/hablullah/go-juliandays v1.0.0/go.mod h1:0JOYq4oFOuDja+oospuc61YoX+uNEn7Z6uHYTbBzdGc= +github.com/hanwen/go-fuse/v2 v2.7.2 h1:SbJP1sUP+n1UF8NXBA14BuojmTez+mDgOk0bC057HQw= +github.com/hanwen/go-fuse/v2 v2.7.2/go.mod h1:ugNaD/iv5JYyS1Rcvi57Wz7/vrLQJo10mmketmoef48= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/jalaali/go-jalaali v0.0.0-20210801064154-80525e88d958 h1:qxLoi6CAcXVzjfvu+KXIXJOAsQB62LXjsfbOaErsVzE= github.com/jalaali/go-jalaali v0.0.0-20210801064154-80525e88d958/go.mod h1:Wqfu7mjUHj9WDzSSPI5KfBclTTEnLveRUFr/ujWnTgE= @@ -123,6 +127,8 @@ github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa02 github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo= github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= @@ -136,15 +142,15 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/minio/simdjson-go v0.4.5 h1:r4IQwjRGmWCQ2VeMc7fGiilu1z5du0gJ/I/FsKwgo5A= -github.com/minio/simdjson-go v0.4.5/go.mod h1:eoNz0DcLQRyEDeaPr4Ru6JpjlZPzbA0IodxVJk8lO8E= +github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78= +github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 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.50.3 h1:mRUJLOkCqnNTAwvjtSRogJyN3SUv1lze1UgnmqUBN0Q= -github.com/nbd-wtf/go-nostr v0.50.3/go.mod h1:XJyV09CfSZCtuf1ApdQFc+3RuEYzt4E/pbXn+doA8tQ= +github.com/nbd-wtf/go-nostr v0.50.5 h1:JOLrozw6nzWMD7CKhEGB5Ys7zXrTV82YjItBBnI5nw8= +github.com/nbd-wtf/go-nostr v0.50.5/go.mod h1:s7XMBrnFUTX+ylEekIAmdvkGxMjllLZeic93TmAi6hU= 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= diff --git a/helpers.go b/helpers.go index 106b8d7..63f43f6 100644 --- a/helpers.go +++ b/helpers.go @@ -11,14 +11,15 @@ import ( "net/url" "os" "slices" + "strconv" "strings" "time" "github.com/fatih/color" - "github.com/urfave/cli/v3" jsoniter "github.com/json-iterator/go" "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/sdk" + "github.com/urfave/cli/v3" ) var sys *sdk.System @@ -234,6 +235,11 @@ func leftPadKey(k string) string { return strings.Repeat("0", 64-len(k)) + k } +func hexToUint64(hexStr string) uint64 { + v, _ := strconv.ParseUint(hexStr[0:16], 16, 64) + return v +} + var colors = struct { reset func(...any) (int, error) italic func(...any) string diff --git a/main.go b/main.go index 96c4b47..018ed01 100644 --- a/main.go +++ b/main.go @@ -13,7 +13,10 @@ import ( "github.com/urfave/cli/v3" ) -var version string = "debug" +var ( + version string = "debug" + isVerbose bool = false +) var app = &cli.Command{ Name: "nak", @@ -41,6 +44,7 @@ var app = &cli.Command{ mcpServer, curl, dvm, + fsCmd, }, Version: version, Flags: []cli.Flag{ @@ -71,6 +75,7 @@ var app = &cli.Command{ v := c.Count("verbose") if v >= 1 { logverbose = log + isVerbose = true } return nil }, From 3d961d4becb4fa3f69f82a79c61a65045fa26ff3 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sat, 8 Mar 2025 12:52:05 -0300 Subject: [PATCH 223/401] fs: something that makes more sense. --- fs.go | 145 +------------------------------------------- go.mod | 4 +- go.sum | 2 - helpers.go | 6 -- nostrfs/eventdir.go | 68 +++++++++++++++++++++ nostrfs/helpers.go | 8 +++ nostrfs/npubdir.go | 46 ++++++++++++++ nostrfs/root.go | 80 ++++++++++++++++++++++++ 8 files changed, 208 insertions(+), 151 deletions(-) create mode 100644 nostrfs/eventdir.go create mode 100644 nostrfs/helpers.go create mode 100644 nostrfs/npubdir.go create mode 100644 nostrfs/root.go diff --git a/fs.go b/fs.go index b5845e4..494c2a3 100644 --- a/fs.go +++ b/fs.go @@ -5,17 +5,15 @@ import ( "fmt" "os" "os/signal" - "strings" - "sync/atomic" "syscall" "time" - "github.com/colduction/nocopy" "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/nip19" + "github.com/nbd-wtf/go-nostr/keyer" "github.com/urfave/cli/v3" ) @@ -43,7 +41,7 @@ var fsCmd = &cli.Command{ return fmt.Errorf("must be called with a directory path to serve as the mountpoint as an argument") } - root := &NostrRoot{ctx: ctx, rootPubKey: c.String("pubkey")} + root := nostrfs.NewNostrRoot(ctx, sys, keyer.NewReadOnlyUser(c.String("pubkey"))) // create the server log("- mounting at %s... ", color.HiCyanString(mountpoint)) @@ -83,140 +81,3 @@ var fsCmd = &cli.Command{ return <-chErr }, } - -type NostrRoot struct { - fs.Inode - rootPubKey string - ctx context.Context -} - -var _ = (fs.NodeOnAdder)((*NostrRoot)(nil)) - -func (r *NostrRoot) OnAdd(context.Context) { - if r.rootPubKey == "" { - return - } - - fl := sys.FetchFollowList(r.ctx, r.rootPubKey) - - for _, f := range fl.Items { - h := r.NewPersistentInode( - r.ctx, - &NpubDir{pointer: nostr.ProfilePointer{PublicKey: f.Pubkey, Relays: []string{f.Relay}}}, - fs.StableAttr{Mode: syscall.S_IFDIR}, - ) - npub, _ := nip19.EncodePublicKey(f.Pubkey) - r.AddChild(npub, h, true) - } -} - -func (r *NostrRoot) Lookup(ctx context.Context, name string, out *fuse.EntryOut) (*fs.Inode, syscall.Errno) { - // check if we already have this npub - child := r.GetChild(name) - if child != nil { - return child, fs.OK - } - - // if the name starts with "npub1" or "nprofile1", create a new npub directory - if strings.HasPrefix(name, "npub1") || strings.HasPrefix(name, "nprofile1") { - npubdir, err := NewNpubDir(name) - if err != nil { - return nil, syscall.ENOENT - } - - return r.NewPersistentInode( - ctx, - npubdir, - fs.StableAttr{Mode: syscall.S_IFDIR}, - ), 0 - } - - return nil, syscall.ENOENT -} - -type NpubDir struct { - fs.Inode - pointer nostr.ProfilePointer - ctx context.Context - fetched atomic.Bool -} - -func NewNpubDir(npub string) (*NpubDir, error) { - pointer, err := nip19.ToPointer(npub) - if err != nil { - return nil, err - } - - pp, ok := pointer.(nostr.ProfilePointer) - if !ok { - return nil, fmt.Errorf("directory must be npub or nprofile") - } - - return &NpubDir{pointer: pp}, nil -} - -var _ = (fs.NodeOpendirer)((*NpubDir)(nil)) - -func (n *NpubDir) Opendir(ctx context.Context) syscall.Errno { - if n.fetched.CompareAndSwap(true, true) { - return fs.OK - } - - for ie := range sys.Pool.FetchMany(ctx, sys.FetchOutboxRelays(ctx, n.pointer.PublicKey, 2), nostr.Filter{ - Kinds: []int{1}, - Authors: []string{n.pointer.PublicKey}, - }, nostr.WithLabel("nak-fs-feed")) { - h := n.NewPersistentInode( - ctx, - &EventFile{ctx: ctx, evt: *ie.Event}, - fs.StableAttr{ - Mode: syscall.S_IFREG, - Ino: hexToUint64(ie.Event.ID), - }, - ) - n.AddChild(ie.Event.ID, h, true) - } - - return fs.OK -} - -type EventFile struct { - fs.Inode - ctx context.Context - evt nostr.Event -} - -var ( - _ = (fs.NodeOpener)((*EventFile)(nil)) - _ = (fs.NodeGetattrer)((*EventFile)(nil)) -) - -func (c *EventFile) Getattr(ctx context.Context, fh fs.FileHandle, out *fuse.AttrOut) syscall.Errno { - out.Mode = 0444 - out.Size = uint64(len(c.evt.String())) - ts := uint64(c.evt.CreatedAt) - out.Atime = ts - out.Mtime = ts - out.Ctime = ts - - return fs.OK -} - -func (c *EventFile) Open(ctx context.Context, flags uint32) (fs.FileHandle, uint32, syscall.Errno) { - return nil, fuse.FOPEN_KEEP_CACHE, 0 -} - -func (c *EventFile) Read( - ctx context.Context, - fh fs.FileHandle, - dest []byte, - off int64, -) (fuse.ReadResult, syscall.Errno) { - buf := c.evt.String() - - end := int(off) + len(dest) - if end > len(buf) { - end = len(c.evt.Content) - } - return fuse.ReadResultData(nocopy.StringToByteSlice(c.evt.Content[off:end])), fs.OK -} diff --git a/go.mod b/go.mod index a2f633d..35ac8c8 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,6 @@ require ( 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/colduction/nocopy v0.2.0 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 github.com/fatih/color v1.16.0 github.com/fiatjaf/eventstore v0.15.0 @@ -37,6 +36,7 @@ require ( github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect github.com/cloudwego/base64x v0.1.5 // indirect github.com/coder/websocket v1.8.12 // indirect + github.com/colduction/nocopy v0.2.0 // indirect github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect github.com/dgraph-io/ristretto v1.0.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect @@ -77,3 +77,5 @@ require ( golang.org/x/sys v0.31.0 // indirect golang.org/x/text v0.21.0 // indirect ) + +replace github.com/nbd-wtf/go-nostr => ../go-nostr diff --git a/go.sum b/go.sum index 0fe7b5f..5c67fd5 100644 --- a/go.sum +++ b/go.sum @@ -149,8 +149,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.50.5 h1:JOLrozw6nzWMD7CKhEGB5Ys7zXrTV82YjItBBnI5nw8= -github.com/nbd-wtf/go-nostr v0.50.5/go.mod h1:s7XMBrnFUTX+ylEekIAmdvkGxMjllLZeic93TmAi6hU= 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= diff --git a/helpers.go b/helpers.go index 63f43f6..d0c8476 100644 --- a/helpers.go +++ b/helpers.go @@ -11,7 +11,6 @@ import ( "net/url" "os" "slices" - "strconv" "strings" "time" @@ -235,11 +234,6 @@ func leftPadKey(k string) string { return strings.Repeat("0", 64-len(k)) + k } -func hexToUint64(hexStr string) uint64 { - v, _ := strconv.ParseUint(hexStr[0:16], 16, 64) - return v -} - var colors = struct { reset func(...any) (int, error) italic func(...any) string diff --git a/nostrfs/eventdir.go b/nostrfs/eventdir.go new file mode 100644 index 0000000..c3ea83d --- /dev/null +++ b/nostrfs/eventdir.go @@ -0,0 +1,68 @@ +package nostrfs + +import ( + "context" + "fmt" + "syscall" + + "github.com/hanwen/go-fuse/v2/fs" + "github.com/hanwen/go-fuse/v2/fuse" + "github.com/mailru/easyjson" + "github.com/nbd-wtf/go-nostr" + sdk "github.com/nbd-wtf/go-nostr/sdk" +) + +type EventDir struct { + fs.Inode + ctx context.Context + evt *nostr.Event +} + +func FetchAndCreateEventDir( + ctx context.Context, + parent fs.InodeEmbedder, + sys *sdk.System, + pointer nostr.EventPointer, +) (*fs.Inode, error) { + event, _, err := sys.FetchSpecificEvent(ctx, pointer, sdk.FetchSpecificEventParameters{ + WithRelays: false, + }) + if err != nil { + return nil, fmt.Errorf("failed to fetch: %w", err) + } + + return CreateEventDir(ctx, parent, event), nil +} + +func CreateEventDir( + ctx context.Context, + parent fs.InodeEmbedder, + event *nostr.Event, +) *fs.Inode { + h := parent.EmbeddedInode().NewPersistentInode( + ctx, + &EventDir{ctx: ctx, evt: event}, + fs.StableAttr{Mode: syscall.S_IFDIR}, + ) + + eventj, _ := easyjson.Marshal(event) + h.AddChild("event.json", h.NewPersistentInode( + ctx, + &fs.MemRegularFile{ + Data: eventj, + Attr: fuse.Attr{Mode: 0444}, + }, + fs.StableAttr{}, + ), true) + + h.AddChild("content.txt", h.NewPersistentInode( + ctx, + &fs.MemRegularFile{ + Data: []byte(event.Content), + Attr: fuse.Attr{Mode: 0444}, + }, + fs.StableAttr{}, + ), true) + + return h +} diff --git a/nostrfs/helpers.go b/nostrfs/helpers.go new file mode 100644 index 0000000..1fd8672 --- /dev/null +++ b/nostrfs/helpers.go @@ -0,0 +1,8 @@ +package nostrfs + +import "strconv" + +func hexToUint64(hexStr string) uint64 { + v, _ := strconv.ParseUint(hexStr[0:16], 16, 64) + return v +} diff --git a/nostrfs/npubdir.go b/nostrfs/npubdir.go new file mode 100644 index 0000000..8eed441 --- /dev/null +++ b/nostrfs/npubdir.go @@ -0,0 +1,46 @@ +package nostrfs + +import ( + "context" + "sync/atomic" + "syscall" + + "github.com/hanwen/go-fuse/v2/fs" + "github.com/nbd-wtf/go-nostr" + sdk "github.com/nbd-wtf/go-nostr/sdk" +) + +type NpubDir struct { + sys *sdk.System + fs.Inode + pointer nostr.ProfilePointer + ctx context.Context + fetched atomic.Bool +} + +func CreateNpubDir(ctx context.Context, parent fs.InodeEmbedder, pointer nostr.ProfilePointer) *fs.Inode { + npubdir := &NpubDir{pointer: pointer} + return parent.EmbeddedInode().NewPersistentInode( + ctx, + npubdir, + fs.StableAttr{Mode: syscall.S_IFDIR}, + ) +} + +var _ = (fs.NodeOpendirer)((*NpubDir)(nil)) + +func (n *NpubDir) Opendir(ctx context.Context) syscall.Errno { + if n.fetched.CompareAndSwap(true, true) { + return fs.OK + } + + for ie := range n.sys.Pool.FetchMany(ctx, n.sys.FetchOutboxRelays(ctx, n.pointer.PublicKey, 2), nostr.Filter{ + Kinds: []int{1}, + Authors: []string{n.pointer.PublicKey}, + }, nostr.WithLabel("nak-fs-feed")) { + e := CreateEventDir(ctx, n, ie.Event) + n.AddChild(ie.Event.ID, e, true) + } + + return fs.OK +} diff --git a/nostrfs/root.go b/nostrfs/root.go new file mode 100644 index 0000000..201ec70 --- /dev/null +++ b/nostrfs/root.go @@ -0,0 +1,80 @@ +package nostrfs + +import ( + "context" + "syscall" + + "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/sdk" +) + +type NostrRoot struct { + sys *sdk.System + fs.Inode + + rootPubKey string + signer nostr.Signer + ctx context.Context +} + +var _ = (fs.NodeOnAdder)((*NostrRoot)(nil)) + +func NewNostrRoot(ctx context.Context, sys *sdk.System, user nostr.User) *NostrRoot { + pubkey, _ := user.GetPublicKey(ctx) + signer, _ := user.(nostr.Signer) + + return &NostrRoot{ + sys: sys, + ctx: ctx, + rootPubKey: pubkey, + signer: signer, + } +} + +func (r *NostrRoot) OnAdd(context.Context) { + if r.rootPubKey == "" { + return + } + + fl := r.sys.FetchFollowList(r.ctx, r.rootPubKey) + + for _, f := range fl.Items { + h := r.NewPersistentInode( + r.ctx, + &NpubDir{sys: r.sys, pointer: nostr.ProfilePointer{PublicKey: f.Pubkey, Relays: []string{f.Relay}}}, + fs.StableAttr{Mode: syscall.S_IFDIR}, + ) + npub, _ := nip19.EncodePublicKey(f.Pubkey) + r.AddChild(npub, h, true) + } +} + +func (r *NostrRoot) Lookup(ctx context.Context, name string, out *fuse.EntryOut) (*fs.Inode, syscall.Errno) { + // check if we already have this npub + child := r.GetChild(name) + if child != nil { + return child, fs.OK + } + + pointer, err := nip19.ToPointer(name) + if err != nil { + return nil, syscall.ENOENT + } + + switch p := pointer.(type) { + case nostr.ProfilePointer: + npubdir := CreateNpubDir(ctx, r, p) + return npubdir, fs.OK + case nostr.EventPointer: + eventdir, err := FetchAndCreateEventDir(ctx, r, r.sys, p) + if err != nil { + return nil, syscall.ENOENT + } + return eventdir, fs.OK + default: + return nil, syscall.ENOENT + } +} From 5fe354f6421e44d72c013325f33bbe9335fa8d42 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sun, 9 Mar 2025 00:13:30 -0300 Subject: [PATCH 224/401] fs: symlink from @me to ourselves. --- nostrfs/npubdir.go | 4 ++-- nostrfs/root.go | 31 +++++++++++++++++++++++-------- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/nostrfs/npubdir.go b/nostrfs/npubdir.go index 8eed441..962544e 100644 --- a/nostrfs/npubdir.go +++ b/nostrfs/npubdir.go @@ -18,8 +18,8 @@ type NpubDir struct { fetched atomic.Bool } -func CreateNpubDir(ctx context.Context, parent fs.InodeEmbedder, pointer nostr.ProfilePointer) *fs.Inode { - npubdir := &NpubDir{pointer: pointer} +func CreateNpubDir(ctx context.Context, sys *sdk.System, parent fs.InodeEmbedder, pointer nostr.ProfilePointer) *fs.Inode { + npubdir := &NpubDir{ctx: ctx, sys: sys, pointer: pointer} return parent.EmbeddedInode().NewPersistentInode( ctx, npubdir, diff --git a/nostrfs/root.go b/nostrfs/root.go index 201ec70..0a30436 100644 --- a/nostrfs/root.go +++ b/nostrfs/root.go @@ -39,17 +39,32 @@ func (r *NostrRoot) OnAdd(context.Context) { return } + // add our contacts fl := r.sys.FetchFollowList(r.ctx, r.rootPubKey) - for _, f := range fl.Items { - h := r.NewPersistentInode( - r.ctx, - &NpubDir{sys: r.sys, pointer: nostr.ProfilePointer{PublicKey: f.Pubkey, Relays: []string{f.Relay}}}, - fs.StableAttr{Mode: syscall.S_IFDIR}, - ) + pointer := nostr.ProfilePointer{PublicKey: f.Pubkey, Relays: []string{f.Relay}} npub, _ := nip19.EncodePublicKey(f.Pubkey) - r.AddChild(npub, h, true) + r.AddChild( + npub, + CreateNpubDir(r.ctx, r.sys, r, pointer), + true, + ) } + + // add ourselves + npub, _ := nip19.EncodePublicKey(r.rootPubKey) + if r.GetChild(npub) == nil { + pointer := nostr.ProfilePointer{PublicKey: r.rootPubKey} + r.AddChild( + npub, + CreateNpubDir(r.ctx, r.sys, r, pointer), + true, + ) + } + + // add a link to ourselves + me := r.NewPersistentInode(r.ctx, &fs.MemSymlink{Data: []byte(npub)}, fs.StableAttr{Mode: syscall.S_IFLNK}) + r.AddChild("@me", me, true) } func (r *NostrRoot) Lookup(ctx context.Context, name string, out *fuse.EntryOut) (*fs.Inode, syscall.Errno) { @@ -66,7 +81,7 @@ func (r *NostrRoot) Lookup(ctx context.Context, name string, out *fuse.EntryOut) switch p := pointer.(type) { case nostr.ProfilePointer: - npubdir := CreateNpubDir(ctx, r, p) + npubdir := CreateNpubDir(ctx, r.sys, r, p) return npubdir, fs.OK case nostr.EventPointer: eventdir, err := FetchAndCreateEventDir(ctx, r, r.sys, p) From 186948db9a9ae65e3ce1b4ba94a185ed0ad85368 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sun, 9 Mar 2025 00:17:56 -0300 Subject: [PATCH 225/401] fs: deterministic inode numbers. --- nostrfs/eventdir.go | 2 +- nostrfs/helpers.go | 2 +- nostrfs/npubdir.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/nostrfs/eventdir.go b/nostrfs/eventdir.go index c3ea83d..5c8b9e0 100644 --- a/nostrfs/eventdir.go +++ b/nostrfs/eventdir.go @@ -42,7 +42,7 @@ func CreateEventDir( h := parent.EmbeddedInode().NewPersistentInode( ctx, &EventDir{ctx: ctx, evt: event}, - fs.StableAttr{Mode: syscall.S_IFDIR}, + fs.StableAttr{Mode: syscall.S_IFDIR, Ino: hexToUint64(event.ID)}, ) eventj, _ := easyjson.Marshal(event) diff --git a/nostrfs/helpers.go b/nostrfs/helpers.go index 1fd8672..9cd82e5 100644 --- a/nostrfs/helpers.go +++ b/nostrfs/helpers.go @@ -3,6 +3,6 @@ package nostrfs import "strconv" func hexToUint64(hexStr string) uint64 { - v, _ := strconv.ParseUint(hexStr[0:16], 16, 64) + v, _ := strconv.ParseUint(hexStr[16:32], 16, 64) return v } diff --git a/nostrfs/npubdir.go b/nostrfs/npubdir.go index 962544e..b38174e 100644 --- a/nostrfs/npubdir.go +++ b/nostrfs/npubdir.go @@ -23,7 +23,7 @@ func CreateNpubDir(ctx context.Context, sys *sdk.System, parent fs.InodeEmbedder return parent.EmbeddedInode().NewPersistentInode( ctx, npubdir, - fs.StableAttr{Mode: syscall.S_IFDIR}, + fs.StableAttr{Mode: syscall.S_IFDIR, Ino: hexToUint64(pointer.PublicKey)}, ) } From a828ee37938a28ef6de74ab6ec4a8a2184e2d2b7 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Mon, 10 Mar 2025 14:38:19 -0300 Subject: [PATCH 226/401] fs: a much more complete directory hierarchy and everything mostly working in read-only mode. --- fs.go | 12 ++- go.mod | 5 +- go.sum | 4 +- nostrfs/asyncfile.go | 56 +++++++++++++ nostrfs/eventdir.go | 181 ++++++++++++++++++++++++++++++++++++++++++- nostrfs/npubdir.go | 156 ++++++++++++++++++++++++++++++++----- nostrfs/root.go | 37 ++++++--- nostrfs/viewdir.go | 72 +++++++++++++++++ 8 files changed, 479 insertions(+), 44 deletions(-) create mode 100644 nostrfs/asyncfile.go create mode 100644 nostrfs/viewdir.go diff --git a/fs.go b/fs.go index 494c2a3..ba63b27 100644 --- a/fs.go +++ b/fs.go @@ -41,15 +41,21 @@ var fsCmd = &cli.Command{ return fmt.Errorf("must be called with a directory path to serve as the mountpoint as an argument") } - root := nostrfs.NewNostrRoot(ctx, sys, keyer.NewReadOnlyUser(c.String("pubkey"))) + root := nostrfs.NewNostrRoot( + ctx, + sys, + keyer.NewReadOnlyUser(c.String("pubkey")), + mountpoint, + ) // create the server log("- mounting at %s... ", color.HiCyanString(mountpoint)) timeout := time.Second * 120 server, err := fs.Mount(mountpoint, root, &fs.Options{ MountOptions: fuse.MountOptions{ - Debug: isVerbose, - Name: "nak", + Debug: isVerbose, + Name: "nak", + FsName: "nak", }, AttrTimeout: &timeout, EntryTimeout: &timeout, diff --git a/go.mod b/go.mod index 35ac8c8..fbd39ee 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,7 @@ require ( 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.50.5 + github.com/nbd-wtf/go-nostr v0.51.0 github.com/urfave/cli/v3 v3.0.0-beta1 golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac ) @@ -36,7 +36,6 @@ require ( github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect github.com/cloudwego/base64x v0.1.5 // indirect github.com/coder/websocket v1.8.12 // indirect - github.com/colduction/nocopy v0.2.0 // indirect github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect github.com/dgraph-io/ristretto v1.0.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect @@ -77,5 +76,3 @@ require ( golang.org/x/sys v0.31.0 // indirect golang.org/x/text v0.21.0 // indirect ) - -replace github.com/nbd-wtf/go-nostr => ../go-nostr diff --git a/go.sum b/go.sum index 5c67fd5..0af85cf 100644 --- a/go.sum +++ b/go.sum @@ -51,8 +51,6 @@ github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJ github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo= github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= -github.com/colduction/nocopy v0.2.0 h1:9jMLCmIP/wnAWO0FfSXJ4h5HBRe6cBqIqacWw/5sRXY= -github.com/colduction/nocopy v0.2.0/go.mod h1:MO+QBkEnsZYE7QukMAcAq4b0rHpSxOTlVqD3fI34YJs= github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -149,6 +147,8 @@ 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.0 h1:Z6gir3lQmlbQGYkccEPbvHlfCydMWXD6bIqukR4DZqU= +github.com/nbd-wtf/go-nostr v0.51.0/go.mod h1:9PcGOZ+e1VOaLvcK0peT4dbip+/eS+eTWXR3HuexQrA= 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= diff --git a/nostrfs/asyncfile.go b/nostrfs/asyncfile.go new file mode 100644 index 0000000..c322f64 --- /dev/null +++ b/nostrfs/asyncfile.go @@ -0,0 +1,56 @@ +package nostrfs + +import ( + "context" + "sync/atomic" + "syscall" + + "github.com/hanwen/go-fuse/v2/fs" + "github.com/hanwen/go-fuse/v2/fuse" + "github.com/nbd-wtf/go-nostr" +) + +type AsyncFile struct { + fs.Inode + ctx context.Context + fetched atomic.Bool + data []byte + ts nostr.Timestamp + load func() ([]byte, nostr.Timestamp) +} + +var ( + _ = (fs.NodeOpener)((*AsyncFile)(nil)) + _ = (fs.NodeGetattrer)((*AsyncFile)(nil)) +) + +func (af *AsyncFile) Getattr(ctx context.Context, f fs.FileHandle, out *fuse.AttrOut) syscall.Errno { + if af.fetched.CompareAndSwap(false, true) { + af.data, af.ts = af.load() + } + + out.Size = uint64(len(af.data)) + out.Mtime = uint64(af.ts) + return fs.OK +} + +func (af *AsyncFile) Open(ctx context.Context, flags uint32) (fs.FileHandle, uint32, syscall.Errno) { + if af.fetched.CompareAndSwap(false, true) { + af.data, af.ts = af.load() + } + + return nil, fuse.FOPEN_KEEP_CACHE, 0 +} + +func (af *AsyncFile) Read( + ctx context.Context, + f fs.FileHandle, + dest []byte, + off int64, +) (fuse.ReadResult, syscall.Errno) { + end := int(off) + len(dest) + if end > len(af.data) { + end = len(af.data) + } + return fuse.ReadResultData(af.data[off:end]), 0 +} diff --git a/nostrfs/eventdir.go b/nostrfs/eventdir.go index 5c8b9e0..53c6e72 100644 --- a/nostrfs/eventdir.go +++ b/nostrfs/eventdir.go @@ -1,26 +1,45 @@ package nostrfs import ( + "bytes" "context" "fmt" + "io" + "net/http" + "path/filepath" "syscall" + "time" "github.com/hanwen/go-fuse/v2/fs" "github.com/hanwen/go-fuse/v2/fuse" "github.com/mailru/easyjson" "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/nip92" sdk "github.com/nbd-wtf/go-nostr/sdk" ) type EventDir struct { fs.Inode ctx context.Context + wd string evt *nostr.Event } +var _ = (fs.NodeGetattrer)((*EventDir)(nil)) + +func (e *EventDir) Getattr(ctx context.Context, f fs.FileHandle, out *fuse.AttrOut) syscall.Errno { + out.Mtime = uint64(e.evt.CreatedAt) + return fs.OK +} + func FetchAndCreateEventDir( ctx context.Context, parent fs.InodeEmbedder, + wd string, sys *sdk.System, pointer nostr.EventPointer, ) (*fs.Inode, error) { @@ -31,26 +50,55 @@ func FetchAndCreateEventDir( return nil, fmt.Errorf("failed to fetch: %w", err) } - return CreateEventDir(ctx, parent, event), nil + return CreateEventDir(ctx, parent, wd, event), nil } func CreateEventDir( ctx context.Context, parent fs.InodeEmbedder, + wd string, event *nostr.Event, ) *fs.Inode { h := parent.EmbeddedInode().NewPersistentInode( ctx, - &EventDir{ctx: ctx, evt: event}, + &EventDir{ctx: ctx, wd: wd, evt: event}, fs.StableAttr{Mode: syscall.S_IFDIR, Ino: hexToUint64(event.ID)}, ) + npub, _ := nip19.EncodePublicKey(event.PubKey) + h.AddChild("@author", h.NewPersistentInode( + ctx, + &fs.MemSymlink{ + Data: []byte(wd + "/" + npub), + }, + fs.StableAttr{Mode: syscall.S_IFLNK}, + ), true) + eventj, _ := easyjson.Marshal(event) h.AddChild("event.json", h.NewPersistentInode( ctx, &fs.MemRegularFile{ Data: eventj, - Attr: fuse.Attr{Mode: 0444}, + Attr: fuse.Attr{ + Mode: 0444, + Ctime: uint64(event.CreatedAt), + Mtime: uint64(event.CreatedAt), + Size: uint64(len(event.Content)), + }, + }, + fs.StableAttr{}, + ), true) + + h.AddChild("id", h.NewPersistentInode( + ctx, + &fs.MemRegularFile{ + Data: []byte(event.ID), + Attr: fuse.Attr{ + Mode: 0444, + Ctime: uint64(event.CreatedAt), + Mtime: uint64(event.CreatedAt), + Size: uint64(len(event.Content)), + }, }, fs.StableAttr{}, ), true) @@ -59,10 +107,135 @@ func CreateEventDir( ctx, &fs.MemRegularFile{ Data: []byte(event.Content), - Attr: fuse.Attr{Mode: 0444}, + Attr: fuse.Attr{ + Mode: 0444, + Ctime: uint64(event.CreatedAt), + Mtime: uint64(event.CreatedAt), + Size: uint64(len(event.Content)), + }, }, fs.StableAttr{}, ), true) + var refsdir *fs.Inode + i := 0 + for ref := range nip27.ParseReferences(*event) { + i++ + if refsdir == nil { + refsdir = h.NewPersistentInode(ctx, &fs.Inode{}, fs.StableAttr{Mode: syscall.S_IFDIR}) + h.AddChild("references", refsdir, true) + } + refsdir.AddChild(fmt.Sprintf("ref_%02d", i), refsdir.NewPersistentInode( + ctx, + &fs.MemSymlink{ + Data: []byte(wd + "/" + nip19.EncodePointer(ref.Pointer)), + }, + fs.StableAttr{Mode: syscall.S_IFLNK}, + ), true) + } + + var imagesdir *fs.Inode + images := nip92.ParseTags(event.Tags) + for _, imeta := range images { + if imeta.URL == "" { + continue + } + if imagesdir == nil { + in := &fs.Inode{} + imagesdir = h.NewPersistentInode(ctx, in, fs.StableAttr{Mode: syscall.S_IFDIR}) + h.AddChild("images", imagesdir, true) + } + imagesdir.AddChild(filepath.Base(imeta.URL), imagesdir.NewPersistentInode( + ctx, + &AsyncFile{ + ctx: ctx, + load: func() ([]byte, nostr.Timestamp) { + ctx, cancel := context.WithTimeout(ctx, time.Second*20) + defer cancel() + r, err := http.NewRequestWithContext(ctx, "GET", imeta.URL, nil) + if err != nil { + return nil, 0 + } + resp, err := http.DefaultClient.Do(r) + if err != nil { + return nil, 0 + } + defer resp.Body.Close() + if resp.StatusCode >= 300 { + return nil, 0 + } + w := &bytes.Buffer{} + io.Copy(w, resp.Body) + return w.Bytes(), 0 + }, + }, + fs.StableAttr{}, + ), true) + } + + if event.Kind == 1 { + if pointer := nip10.GetThreadRoot(event.Tags); pointer != nil { + nevent := nip19.EncodePointer(*pointer) + h.AddChild("@root", h.NewPersistentInode( + ctx, + &fs.MemSymlink{ + Data: []byte(wd + "/" + nevent), + }, + fs.StableAttr{Mode: syscall.S_IFLNK}, + ), true) + } + if pointer := nip10.GetImmediateParent(event.Tags); pointer != nil { + nevent := nip19.EncodePointer(*pointer) + h.AddChild("@parent", h.NewPersistentInode( + ctx, + &fs.MemSymlink{ + Data: []byte(wd + "/" + nevent), + }, + fs.StableAttr{Mode: syscall.S_IFLNK}, + ), true) + } + } else if event.Kind == 1111 { + if pointer := nip22.GetThreadRoot(event.Tags); pointer != nil { + if xp, ok := pointer.(nostr.ExternalPointer); ok { + h.AddChild("@root", h.NewPersistentInode( + ctx, + &fs.MemRegularFile{ + Data: []byte(``), + }, + fs.StableAttr{}, + ), true) + } else { + nevent := nip19.EncodePointer(pointer) + h.AddChild("@parent", h.NewPersistentInode( + ctx, + &fs.MemSymlink{ + Data: []byte(wd + "/" + nevent), + }, + fs.StableAttr{Mode: syscall.S_IFLNK}, + ), true) + } + } + if pointer := nip22.GetImmediateParent(event.Tags); pointer != nil { + if xp, ok := pointer.(nostr.ExternalPointer); ok { + h.AddChild("@parent", h.NewPersistentInode( + ctx, + &fs.MemRegularFile{ + Data: []byte(``), + }, + fs.StableAttr{}, + ), true) + } else { + nevent := nip19.EncodePointer(pointer) + h.AddChild("@parent", h.NewPersistentInode( + ctx, + &fs.MemSymlink{ + Data: []byte(wd + "/" + nevent), + }, + fs.StableAttr{Mode: syscall.S_IFLNK}, + ), true) + } + } + } + return h } diff --git a/nostrfs/npubdir.go b/nostrfs/npubdir.go index b38174e..f509344 100644 --- a/nostrfs/npubdir.go +++ b/nostrfs/npubdir.go @@ -2,10 +2,12 @@ package nostrfs import ( "context" + "encoding/json" "sync/atomic" "syscall" "github.com/hanwen/go-fuse/v2/fs" + "github.com/hanwen/go-fuse/v2/fuse" "github.com/nbd-wtf/go-nostr" sdk "github.com/nbd-wtf/go-nostr/sdk" ) @@ -18,29 +20,143 @@ type NpubDir struct { fetched atomic.Bool } -func CreateNpubDir(ctx context.Context, sys *sdk.System, parent fs.InodeEmbedder, pointer nostr.ProfilePointer) *fs.Inode { +func CreateNpubDir( + ctx context.Context, + sys *sdk.System, + parent fs.InodeEmbedder, + wd string, + pointer nostr.ProfilePointer, +) *fs.Inode { npubdir := &NpubDir{ctx: ctx, sys: sys, pointer: pointer} - return parent.EmbeddedInode().NewPersistentInode( + h := parent.EmbeddedInode().NewPersistentInode( ctx, npubdir, fs.StableAttr{Mode: syscall.S_IFDIR, Ino: hexToUint64(pointer.PublicKey)}, ) -} - -var _ = (fs.NodeOpendirer)((*NpubDir)(nil)) - -func (n *NpubDir) Opendir(ctx context.Context) syscall.Errno { - if n.fetched.CompareAndSwap(true, true) { - return fs.OK - } - - for ie := range n.sys.Pool.FetchMany(ctx, n.sys.FetchOutboxRelays(ctx, n.pointer.PublicKey, 2), nostr.Filter{ - Kinds: []int{1}, - Authors: []string{n.pointer.PublicKey}, - }, nostr.WithLabel("nak-fs-feed")) { - e := CreateEventDir(ctx, n, ie.Event) - n.AddChild(ie.Event.ID, e, true) - } - - return fs.OK + + relays := sys.FetchOutboxRelays(ctx, pointer.PublicKey, 2) + + h.AddChild("pubkey", h.NewPersistentInode( + ctx, + &fs.MemRegularFile{Data: []byte(pointer.PublicKey + "\n"), Attr: fuse.Attr{Mode: 0444}}, + fs.StableAttr{}, + ), true) + + h.AddChild( + "notes", + h.NewPersistentInode( + ctx, + &ViewDir{ + ctx: ctx, + sys: sys, + wd: wd, + filter: nostr.Filter{ + Kinds: []int{1}, + Authors: []string{pointer.PublicKey}, + }, + relays: relays, + }, + fs.StableAttr{Mode: syscall.S_IFDIR}, + ), + true, + ) + + h.AddChild( + "comments", + h.NewPersistentInode( + ctx, + &ViewDir{ + ctx: ctx, + sys: sys, + wd: wd, + filter: nostr.Filter{ + Kinds: []int{1111}, + Authors: []string{pointer.PublicKey}, + }, + relays: relays, + }, + fs.StableAttr{Mode: syscall.S_IFDIR}, + ), + true, + ) + + h.AddChild( + "pictures", + h.NewPersistentInode( + ctx, + &ViewDir{ + ctx: ctx, + sys: sys, + wd: wd, + filter: nostr.Filter{ + Kinds: []int{20}, + Authors: []string{pointer.PublicKey}, + }, + relays: relays, + }, + fs.StableAttr{Mode: syscall.S_IFDIR}, + ), + true, + ) + + h.AddChild( + "videos", + h.NewPersistentInode( + ctx, + &ViewDir{ + ctx: ctx, + sys: sys, + wd: wd, + filter: nostr.Filter{ + Kinds: []int{21, 22}, + Authors: []string{pointer.PublicKey}, + }, + relays: relays, + }, + fs.StableAttr{Mode: syscall.S_IFDIR}, + ), + true, + ) + + h.AddChild( + "highlights", + h.NewPersistentInode( + ctx, + &ViewDir{ + ctx: ctx, + sys: sys, + wd: wd, + filter: nostr.Filter{ + Kinds: []int{9802}, + Authors: []string{pointer.PublicKey}, + }, + relays: relays, + }, + fs.StableAttr{Mode: syscall.S_IFDIR}, + ), + true, + ) + + h.AddChild( + "metadata.json", + h.NewPersistentInode( + ctx, + &AsyncFile{ + ctx: ctx, + load: func() ([]byte, nostr.Timestamp) { + pm := sys.FetchProfileMetadata(ctx, pointer.PublicKey) + jsonb, _ := json.MarshalIndent(pm.Event, "", " ") + var ts nostr.Timestamp + if pm.Event != nil { + ts = pm.Event.CreatedAt + } + return jsonb, ts + }, + }, + fs.StableAttr{}, + ), + true, + ) + + return h } diff --git a/nostrfs/root.go b/nostrfs/root.go index 0a30436..32c6cc1 100644 --- a/nostrfs/root.go +++ b/nostrfs/root.go @@ -2,35 +2,41 @@ package nostrfs import ( "context" + "path/filepath" "syscall" + "time" "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 NostrRoot struct { - sys *sdk.System fs.Inode + ctx context.Context + wd string + sys *sdk.System rootPubKey string signer nostr.Signer - ctx context.Context } var _ = (fs.NodeOnAdder)((*NostrRoot)(nil)) -func NewNostrRoot(ctx context.Context, sys *sdk.System, user nostr.User) *NostrRoot { +func NewNostrRoot(ctx context.Context, sys *sdk.System, user nostr.User, mountpoint string) *NostrRoot { pubkey, _ := user.GetPublicKey(ctx) signer, _ := user.(nostr.Signer) + abs, _ := filepath.Abs(mountpoint) return &NostrRoot{ - sys: sys, ctx: ctx, + sys: sys, rootPubKey: pubkey, signer: signer, + wd: abs, } } @@ -46,7 +52,7 @@ func (r *NostrRoot) OnAdd(context.Context) { npub, _ := nip19.EncodePublicKey(f.Pubkey) r.AddChild( npub, - CreateNpubDir(r.ctx, r.sys, r, pointer), + CreateNpubDir(r.ctx, r.sys, r, r.wd, pointer), true, ) } @@ -57,23 +63,32 @@ func (r *NostrRoot) OnAdd(context.Context) { pointer := nostr.ProfilePointer{PublicKey: r.rootPubKey} r.AddChild( npub, - CreateNpubDir(r.ctx, r.sys, r, pointer), + CreateNpubDir(r.ctx, r.sys, r, r.wd, pointer), true, ) } // add a link to ourselves - me := r.NewPersistentInode(r.ctx, &fs.MemSymlink{Data: []byte(npub)}, fs.StableAttr{Mode: syscall.S_IFLNK}) - r.AddChild("@me", me, true) + r.AddChild("@me", r.NewPersistentInode( + r.ctx, + &fs.MemSymlink{Data: []byte(r.wd + "/" + npub)}, + fs.StableAttr{Mode: syscall.S_IFLNK}, + ), true) } func (r *NostrRoot) Lookup(ctx context.Context, name string, out *fuse.EntryOut) (*fs.Inode, syscall.Errno) { - // check if we already have this npub + out.SetEntryTimeout(time.Minute * 5) + child := r.GetChild(name) if child != nil { return child, fs.OK } + if pp, err := nip05.QueryIdentifier(ctx, name); err == nil { + npubdir := CreateNpubDir(ctx, r.sys, r, r.wd, *pp) + return npubdir, fs.OK + } + pointer, err := nip19.ToPointer(name) if err != nil { return nil, syscall.ENOENT @@ -81,10 +96,10 @@ func (r *NostrRoot) Lookup(ctx context.Context, name string, out *fuse.EntryOut) switch p := pointer.(type) { case nostr.ProfilePointer: - npubdir := CreateNpubDir(ctx, r.sys, r, p) + npubdir := CreateNpubDir(ctx, r.sys, r, r.wd, p) return npubdir, fs.OK case nostr.EventPointer: - eventdir, err := FetchAndCreateEventDir(ctx, r, r.sys, p) + eventdir, err := FetchAndCreateEventDir(ctx, r, r.wd, r.sys, p) if err != nil { return nil, syscall.ENOENT } diff --git a/nostrfs/viewdir.go b/nostrfs/viewdir.go new file mode 100644 index 0000000..04ce2eb --- /dev/null +++ b/nostrfs/viewdir.go @@ -0,0 +1,72 @@ +package nostrfs + +import ( + "context" + "sync/atomic" + "syscall" + + "github.com/hanwen/go-fuse/v2/fs" + "github.com/hanwen/go-fuse/v2/fuse" + "github.com/nbd-wtf/go-nostr" + sdk "github.com/nbd-wtf/go-nostr/sdk" +) + +type ViewDir struct { + fs.Inode + ctx context.Context + sys *sdk.System + wd string + fetched atomic.Bool + filter nostr.Filter + relays []string +} + +var ( + _ = (fs.NodeOpendirer)((*ViewDir)(nil)) + _ = (fs.NodeGetattrer)((*ViewDir)(nil)) +) + +func (n *ViewDir) Getattr(ctx context.Context, f fs.FileHandle, out *fuse.AttrOut) syscall.Errno { + now := nostr.Now() + if n.filter.Until != nil { + now = *n.filter.Until + } + aMonthAgo := now - 30*24*60*60 + out.Mtime = uint64(aMonthAgo) + return fs.OK +} + +func (n *ViewDir) Opendir(ctx context.Context) syscall.Errno { + if n.fetched.CompareAndSwap(true, true) { + return fs.OK + } + + now := nostr.Now() + if n.filter.Until != nil { + now = *n.filter.Until + } + aMonthAgo := now - 30*24*60*60 + n.filter.Since = &aMonthAgo + + for ie := range n.sys.Pool.FetchMany(ctx, n.relays, n.filter, nostr.WithLabel("nakfs")) { + e := CreateEventDir(ctx, n, n.wd, ie.Event) + n.AddChild(ie.Event.ID, e, true) + } + + filter := n.filter + filter.Until = &aMonthAgo + + n.AddChild("@previous", n.NewPersistentInode( + ctx, + &ViewDir{ + ctx: n.ctx, + sys: n.sys, + filter: filter, + wd: n.wd, + relays: n.relays, + }, + fs.StableAttr{Mode: syscall.S_IFDIR}, + ), true) + + return fs.OK +} From 30315682660aef2e3c329a1085425d57f9f94993 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Mon, 10 Mar 2025 16:02:47 -0300 Subject: [PATCH 227/401] fs: articles and wikis. --- nostrfs/entitydir.go | 214 +++++++++++++++++++++++++++++++++++++++++++ nostrfs/eventdir.go | 6 +- nostrfs/npubdir.go | 100 ++++++++++++++++---- nostrfs/viewdir.go | 72 ++++++++------- 4 files changed, 341 insertions(+), 51 deletions(-) create mode 100644 nostrfs/entitydir.go diff --git a/nostrfs/entitydir.go b/nostrfs/entitydir.go new file mode 100644 index 0000000..c45664c --- /dev/null +++ b/nostrfs/entitydir.go @@ -0,0 +1,214 @@ +package nostrfs + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "path/filepath" + "strconv" + "syscall" + "time" + + "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 { + fs.Inode + ctx context.Context + wd string + evt *nostr.Event +} + +var _ = (fs.NodeGetattrer)((*EntityDir)(nil)) + +func (e *EntityDir) Getattr(ctx context.Context, f fs.FileHandle, out *fuse.AttrOut) syscall.Errno { + publishedAt := uint64(e.evt.CreatedAt) + out.Ctime = publishedAt + + if tag := e.evt.Tags.Find("published_at"); tag != nil { + publishedAt, _ = strconv.ParseUint(tag[1], 10, 64) + } + out.Mtime = publishedAt + + return fs.OK +} + +func FetchAndCreateEntityDir( + ctx context.Context, + parent fs.InodeEmbedder, + wd string, + extension string, + sys *sdk.System, + pointer nostr.EntityPointer, +) (*fs.Inode, error) { + event, _, err := sys.FetchSpecificEvent(ctx, pointer, sdk.FetchSpecificEventParameters{ + WithRelays: false, + }) + if err != nil { + return nil, fmt.Errorf("failed to fetch: %w", err) + } + + return CreateEntityDir(ctx, parent, wd, extension, event), nil +} + +func CreateEntityDir( + ctx context.Context, + parent fs.InodeEmbedder, + wd string, + extension string, + event *nostr.Event, +) *fs.Inode { + h := parent.EmbeddedInode().NewPersistentInode( + ctx, + &EntityDir{ctx: ctx, wd: wd, evt: event}, + fs.StableAttr{Mode: syscall.S_IFDIR, Ino: hexToUint64(event.ID)}, + ) + + var publishedAt uint64 + if tag := event.Tags.Find("published_at"); tag != nil { + publishedAt, _ = strconv.ParseUint(tag[1], 10, 64) + } + + npub, _ := nip19.EncodePublicKey(event.PubKey) + h.AddChild("@author", h.NewPersistentInode( + ctx, + &fs.MemSymlink{ + Data: []byte(wd + "/" + npub), + }, + fs.StableAttr{Mode: syscall.S_IFLNK}, + ), true) + + eventj, _ := json.MarshalIndent(event, "", " ") + h.AddChild("event.json", h.NewPersistentInode( + ctx, + &fs.MemRegularFile{ + Data: eventj, + Attr: fuse.Attr{ + Mode: 0444, + Ctime: uint64(event.CreatedAt), + Mtime: uint64(publishedAt), + Size: uint64(len(event.Content)), + }, + }, + fs.StableAttr{}, + ), true) + + h.AddChild("identifier", h.NewPersistentInode( + ctx, + &fs.MemRegularFile{ + Data: []byte(event.Tags.GetD()), + Attr: fuse.Attr{ + Mode: 0444, + Ctime: uint64(event.CreatedAt), + Mtime: uint64(publishedAt), + Size: uint64(len(event.Tags.GetD())), + }, + }, + fs.StableAttr{}, + ), true) + + if tag := event.Tags.Find("title"); tag != nil { + h.AddChild("title", h.NewPersistentInode( + ctx, + &fs.MemRegularFile{ + Data: []byte(tag[1]), + Attr: fuse.Attr{ + Mode: 0444, + Ctime: uint64(event.CreatedAt), + Mtime: uint64(publishedAt), + Size: uint64(len(tag[1])), + }, + }, + fs.StableAttr{}, + ), true) + } + + h.AddChild("content"+extension, h.NewPersistentInode( + ctx, + &fs.MemRegularFile{ + Data: []byte(event.Content), + Attr: fuse.Attr{ + Mode: 0444, + Ctime: uint64(event.CreatedAt), + Mtime: uint64(publishedAt), + Size: uint64(len(event.Content)), + }, + }, + fs.StableAttr{}, + ), true) + + var refsdir *fs.Inode + i := 0 + for ref := range nip27.ParseReferences(*event) { + i++ + if refsdir == nil { + refsdir = h.NewPersistentInode(ctx, &fs.Inode{}, fs.StableAttr{Mode: syscall.S_IFDIR}) + h.AddChild("references", refsdir, true) + } + refsdir.AddChild(fmt.Sprintf("ref_%02d", i), refsdir.NewPersistentInode( + ctx, + &fs.MemSymlink{ + Data: []byte(wd + "/" + nip19.EncodePointer(ref.Pointer)), + }, + fs.StableAttr{Mode: syscall.S_IFLNK}, + ), true) + } + + var imagesdir *fs.Inode + addImage := func(url string) { + if imagesdir == nil { + in := &fs.Inode{} + imagesdir = h.NewPersistentInode(ctx, in, fs.StableAttr{Mode: syscall.S_IFDIR}) + h.AddChild("images", imagesdir, true) + } + imagesdir.AddChild(filepath.Base(url), imagesdir.NewPersistentInode( + ctx, + &AsyncFile{ + ctx: ctx, + load: func() ([]byte, nostr.Timestamp) { + ctx, cancel := context.WithTimeout(ctx, time.Second*20) + defer cancel() + r, err := http.NewRequestWithContext(ctx, "GET", url, nil) + if err != nil { + return nil, 0 + } + resp, err := http.DefaultClient.Do(r) + if err != nil { + return nil, 0 + } + defer resp.Body.Close() + if resp.StatusCode >= 300 { + return nil, 0 + } + w := &bytes.Buffer{} + io.Copy(w, resp.Body) + return w.Bytes(), 0 + }, + }, + fs.StableAttr{}, + ), true) + } + + images := nip92.ParseTags(event.Tags) + for _, imeta := range images { + if imeta.URL == "" { + continue + } + addImage(imeta.URL) + } + + if tag := event.Tags.Find("image"); tag != nil { + addImage(tag[1]) + } + + return h +} diff --git a/nostrfs/eventdir.go b/nostrfs/eventdir.go index 53c6e72..253e99c 100644 --- a/nostrfs/eventdir.go +++ b/nostrfs/eventdir.go @@ -3,6 +3,7 @@ package nostrfs import ( "bytes" "context" + "encoding/json" "fmt" "io" "net/http" @@ -12,7 +13,6 @@ import ( "github.com/hanwen/go-fuse/v2/fs" "github.com/hanwen/go-fuse/v2/fuse" - "github.com/mailru/easyjson" "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/nip10" "github.com/nbd-wtf/go-nostr/nip19" @@ -74,7 +74,7 @@ func CreateEventDir( fs.StableAttr{Mode: syscall.S_IFLNK}, ), true) - eventj, _ := easyjson.Marshal(event) + eventj, _ := json.MarshalIndent(event, "", " ") h.AddChild("event.json", h.NewPersistentInode( ctx, &fs.MemRegularFile{ @@ -97,7 +97,7 @@ func CreateEventDir( Mode: 0444, Ctime: uint64(event.CreatedAt), Mtime: uint64(event.CreatedAt), - Size: uint64(len(event.Content)), + Size: uint64(64), }, }, fs.StableAttr{}, diff --git a/nostrfs/npubdir.go b/nostrfs/npubdir.go index f509344..f2a0509 100644 --- a/nostrfs/npubdir.go +++ b/nostrfs/npubdir.go @@ -2,10 +2,10 @@ package nostrfs import ( "context" - "encoding/json" "sync/atomic" "syscall" + "github.com/bytedance/sonic" "github.com/hanwen/go-fuse/v2/fs" "github.com/hanwen/go-fuse/v2/fuse" "github.com/nbd-wtf/go-nostr" @@ -42,6 +42,27 @@ func CreateNpubDir( fs.StableAttr{}, ), true) + h.AddChild( + "metadata.json", + h.NewPersistentInode( + ctx, + &AsyncFile{ + ctx: ctx, + load: func() ([]byte, nostr.Timestamp) { + pm := sys.FetchProfileMetadata(ctx, pointer.PublicKey) + jsonb, _ := sonic.ConfigFastest.MarshalIndent(pm, "", " ") + var ts nostr.Timestamp + if pm.Event != nil { + ts = pm.Event.CreatedAt + } + return jsonb, ts + }, + }, + fs.StableAttr{}, + ), + true, + ) + h.AddChild( "notes", h.NewPersistentInode( @@ -54,7 +75,11 @@ func CreateNpubDir( Kinds: []int{1}, Authors: []string{pointer.PublicKey}, }, - relays: relays, + paginate: true, + relays: relays, + create: func(ctx context.Context, n *ViewDir, event *nostr.Event) (string, *fs.Inode) { + return event.ID, CreateEventDir(ctx, n, n.wd, event) + }, }, fs.StableAttr{Mode: syscall.S_IFDIR}, ), @@ -73,7 +98,11 @@ func CreateNpubDir( Kinds: []int{1111}, Authors: []string{pointer.PublicKey}, }, - relays: relays, + paginate: true, + relays: relays, + create: func(ctx context.Context, n *ViewDir, event *nostr.Event) (string, *fs.Inode) { + return event.ID, CreateEventDir(ctx, n, n.wd, event) + }, }, fs.StableAttr{Mode: syscall.S_IFDIR}, ), @@ -92,7 +121,11 @@ func CreateNpubDir( Kinds: []int{20}, Authors: []string{pointer.PublicKey}, }, - relays: relays, + paginate: true, + relays: relays, + create: func(ctx context.Context, n *ViewDir, event *nostr.Event) (string, *fs.Inode) { + return event.ID, CreateEventDir(ctx, n, n.wd, event) + }, }, fs.StableAttr{Mode: syscall.S_IFDIR}, ), @@ -111,7 +144,11 @@ func CreateNpubDir( Kinds: []int{21, 22}, Authors: []string{pointer.PublicKey}, }, - relays: relays, + paginate: true, + relays: relays, + create: func(ctx context.Context, n *ViewDir, event *nostr.Event) (string, *fs.Inode) { + return event.ID, CreateEventDir(ctx, n, n.wd, event) + }, }, fs.StableAttr{Mode: syscall.S_IFDIR}, ), @@ -130,7 +167,11 @@ func CreateNpubDir( Kinds: []int{9802}, Authors: []string{pointer.PublicKey}, }, - relays: relays, + paginate: true, + relays: relays, + create: func(ctx context.Context, n *ViewDir, event *nostr.Event) (string, *fs.Inode) { + return event.ID, CreateEventDir(ctx, n, n.wd, event) + }, }, fs.StableAttr{Mode: syscall.S_IFDIR}, ), @@ -138,22 +179,47 @@ func CreateNpubDir( ) h.AddChild( - "metadata.json", + "articles", h.NewPersistentInode( ctx, - &AsyncFile{ + &ViewDir{ ctx: ctx, - load: func() ([]byte, nostr.Timestamp) { - pm := sys.FetchProfileMetadata(ctx, pointer.PublicKey) - jsonb, _ := json.MarshalIndent(pm.Event, "", " ") - var ts nostr.Timestamp - if pm.Event != nil { - ts = pm.Event.CreatedAt - } - return jsonb, ts + sys: sys, + wd: wd, + filter: nostr.Filter{ + Kinds: []int{30023}, + Authors: []string{pointer.PublicKey}, + }, + paginate: false, + relays: relays, + create: func(ctx context.Context, n *ViewDir, event *nostr.Event) (string, *fs.Inode) { + return event.Tags.GetD(), CreateEntityDir(ctx, n, n.wd, ".md", event) }, }, - fs.StableAttr{}, + fs.StableAttr{Mode: syscall.S_IFDIR}, + ), + true, + ) + + h.AddChild( + "wiki", + h.NewPersistentInode( + ctx, + &ViewDir{ + ctx: ctx, + sys: sys, + wd: wd, + filter: nostr.Filter{ + Kinds: []int{30818}, + Authors: []string{pointer.PublicKey}, + }, + paginate: false, + relays: relays, + create: func(ctx context.Context, n *ViewDir, event *nostr.Event) (string, *fs.Inode) { + return event.Tags.GetD(), CreateEntityDir(ctx, n, n.wd, ".adoc", event) + }, + }, + fs.StableAttr{Mode: syscall.S_IFDIR}, ), true, ) diff --git a/nostrfs/viewdir.go b/nostrfs/viewdir.go index 04ce2eb..ddc6979 100644 --- a/nostrfs/viewdir.go +++ b/nostrfs/viewdir.go @@ -13,12 +13,14 @@ import ( type ViewDir struct { fs.Inode - ctx context.Context - sys *sdk.System - wd string - fetched atomic.Bool - filter nostr.Filter - relays []string + ctx context.Context + sys *sdk.System + wd string + fetched atomic.Bool + filter nostr.Filter + paginate bool + relays []string + create func(context.Context, *ViewDir, *nostr.Event) (string, *fs.Inode) } var ( @@ -33,6 +35,7 @@ func (n *ViewDir) Getattr(ctx context.Context, f fs.FileHandle, out *fuse.AttrOu } aMonthAgo := now - 30*24*60*60 out.Mtime = uint64(aMonthAgo) + return fs.OK } @@ -41,32 +44,39 @@ func (n *ViewDir) Opendir(ctx context.Context) syscall.Errno { return fs.OK } - now := nostr.Now() - if n.filter.Until != nil { - now = *n.filter.Until + if n.paginate { + now := nostr.Now() + if n.filter.Until != nil { + now = *n.filter.Until + } + aMonthAgo := now - 30*24*60*60 + n.filter.Since = &aMonthAgo + + for ie := range n.sys.Pool.FetchMany(ctx, n.relays, n.filter, nostr.WithLabel("nakfs")) { + basename, inode := n.create(ctx, n, ie.Event) + n.AddChild(basename, inode, true) + } + + filter := n.filter + filter.Until = &aMonthAgo + + n.AddChild("@previous", n.NewPersistentInode( + ctx, + &ViewDir{ + ctx: n.ctx, + sys: n.sys, + filter: filter, + wd: n.wd, + relays: n.relays, + }, + fs.StableAttr{Mode: syscall.S_IFDIR}, + ), true) + } else { + for ie := range n.sys.Pool.FetchMany(ctx, n.relays, n.filter, nostr.WithLabel("nakfs")) { + basename, inode := n.create(ctx, n, ie.Event) + n.AddChild(basename, inode, true) + } } - aMonthAgo := now - 30*24*60*60 - n.filter.Since = &aMonthAgo - - for ie := range n.sys.Pool.FetchMany(ctx, n.relays, n.filter, nostr.WithLabel("nakfs")) { - e := CreateEventDir(ctx, n, n.wd, ie.Event) - n.AddChild(ie.Event.ID, e, true) - } - - filter := n.filter - filter.Until = &aMonthAgo - - n.AddChild("@previous", n.NewPersistentInode( - ctx, - &ViewDir{ - ctx: n.ctx, - sys: n.sys, - filter: filter, - wd: n.wd, - relays: n.relays, - }, - fs.StableAttr{Mode: syscall.S_IFDIR}, - ), true) return fs.OK } From 4b4d9ec155ebc7ac02a4d3d30a8625d8914ec556 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Mon, 10 Mar 2025 17:06:59 -0300 Subject: [PATCH 228/401] fs: some fixes and profile pictures. --- go.mod | 5 +-- go.sum | 10 ++++-- nostrfs/npubdir.go | 89 +++++++++++++++++++++++++++++++++++----------- 3 files changed, 79 insertions(+), 25 deletions(-) diff --git a/go.mod b/go.mod index fbd39ee..fd27fcf 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ toolchain go1.23.4 require ( github.com/bep/debounce v1.2.1 github.com/btcsuite/btcd/btcec/v2 v2.3.4 + github.com/bytedance/sonic v1.13.1 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 github.com/fatih/color v1.16.0 @@ -14,10 +15,11 @@ require ( github.com/fiatjaf/khatru v0.16.0 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.0 + github.com/nbd-wtf/go-nostr v0.51.1 github.com/urfave/cli/v3 v3.0.0-beta1 golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac ) @@ -29,7 +31,6 @@ require ( github.com/btcsuite/btcd v0.24.2 // indirect github.com/btcsuite/btcd/btcutil v1.1.5 // indirect github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect - github.com/bytedance/sonic v1.13.1 // indirect github.com/bytedance/sonic/loader v0.2.4 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chzyer/logex v1.1.10 // indirect diff --git a/go.sum b/go.sum index 0af85cf..aa842ac 100644 --- a/go.sum +++ b/go.sum @@ -96,6 +96,8 @@ github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -127,6 +129,8 @@ github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQe github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/liamg/magic v0.0.1 h1:Ru22ElY+sCh6RvRTWjQzKKCxsEco8hE0co8n1qe7TBM= +github.com/liamg/magic v0.0.1/go.mod h1:yQkOmZZI52EA+SQ2xyHpVw8fNvTBruF873Y+Vt6S+fk= github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo= github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= @@ -147,8 +151,8 @@ 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.0 h1:Z6gir3lQmlbQGYkccEPbvHlfCydMWXD6bIqukR4DZqU= -github.com/nbd-wtf/go-nostr v0.51.0/go.mod h1:9PcGOZ+e1VOaLvcK0peT4dbip+/eS+eTWXR3HuexQrA= +github.com/nbd-wtf/go-nostr v0.51.1 h1:SKnvWRbcDAoNibMYWrNbG4V1VQzQUc9YWltUJsrSDbw= +github.com/nbd-wtf/go-nostr v0.51.1/go.mod h1:9PcGOZ+e1VOaLvcK0peT4dbip+/eS+eTWXR3HuexQrA= 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= @@ -258,4 +262,6 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= +gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= diff --git a/nostrfs/npubdir.go b/nostrfs/npubdir.go index f2a0509..8a20d06 100644 --- a/nostrfs/npubdir.go +++ b/nostrfs/npubdir.go @@ -1,13 +1,18 @@ package nostrfs import ( + "bytes" "context" + "io" + "net/http" "sync/atomic" "syscall" + "time" "github.com/bytedance/sonic" "github.com/hanwen/go-fuse/v2/fs" "github.com/hanwen/go-fuse/v2/fuse" + "github.com/liamg/magic" "github.com/nbd-wtf/go-nostr" sdk "github.com/nbd-wtf/go-nostr/sdk" ) @@ -42,26 +47,60 @@ func CreateNpubDir( fs.StableAttr{}, ), true) - h.AddChild( - "metadata.json", - h.NewPersistentInode( - ctx, - &AsyncFile{ - ctx: ctx, - load: func() ([]byte, nostr.Timestamp) { - pm := sys.FetchProfileMetadata(ctx, pointer.PublicKey) - jsonb, _ := sonic.ConfigFastest.MarshalIndent(pm, "", " ") - var ts nostr.Timestamp - if pm.Event != nil { - ts = pm.Event.CreatedAt - } - return jsonb, ts + go func() { + pm := sys.FetchProfileMetadata(ctx, pointer.PublicKey) + if pm.Event == nil { + return + } + + metadataj, _ := sonic.ConfigFastest.MarshalIndent(pm, "", " ") + h.AddChild( + "metadata.json", + h.NewPersistentInode( + ctx, + &fs.MemRegularFile{ + Data: metadataj, + Attr: fuse.Attr{ + Mtime: uint64(pm.Event.CreatedAt), + Mode: 0444, + }, }, - }, - fs.StableAttr{}, - ), - true, - ) + fs.StableAttr{}, + ), + true, + ) + + ctx, cancel := context.WithTimeout(ctx, time.Second*20) + defer cancel() + r, err := http.NewRequestWithContext(ctx, "GET", pm.Picture, nil) + if err == nil { + resp, err := http.DefaultClient.Do(r) + if err == nil { + defer resp.Body.Close() + if resp.StatusCode < 300 { + b := &bytes.Buffer{} + io.Copy(b, resp.Body) + + ext := "png" + if ft, err := magic.Lookup(b.Bytes()); err == nil { + ext = ft.Extension + } + + h.AddChild("picture."+ext, h.NewPersistentInode( + ctx, + &fs.MemRegularFile{ + Data: b.Bytes(), + Attr: fuse.Attr{ + Mtime: uint64(pm.Event.CreatedAt), + Mode: 0444, + }, + }, + fs.StableAttr{}, + ), true) + } + } + } + }() h.AddChild( "notes", @@ -193,7 +232,11 @@ func CreateNpubDir( paginate: false, relays: relays, create: func(ctx context.Context, n *ViewDir, event *nostr.Event) (string, *fs.Inode) { - return event.Tags.GetD(), CreateEntityDir(ctx, n, n.wd, ".md", event) + d := event.Tags.GetD() + if d == "" { + d = "_" + } + return d, CreateEntityDir(ctx, n, n.wd, ".md", event) }, }, fs.StableAttr{Mode: syscall.S_IFDIR}, @@ -216,7 +259,11 @@ func CreateNpubDir( paginate: false, relays: relays, create: func(ctx context.Context, n *ViewDir, event *nostr.Event) (string, *fs.Inode) { - return event.Tags.GetD(), CreateEntityDir(ctx, n, n.wd, ".adoc", event) + d := event.Tags.GetD() + if d == "" { + d = "_" + } + return d, CreateEntityDir(ctx, n, n.wd, ".adoc", event) }, }, fs.StableAttr{Mode: syscall.S_IFDIR}, From 1c058f2846d7bed170cf6581c0524d23cfeab76b Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Mon, 10 Mar 2025 17:13:19 -0300 Subject: [PATCH 229/401] fix github builds by removing some odd platform combinations. --- .github/workflows/release-cli.yml | 8 ++------ go.mod | 4 +--- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml index 87a941c..168f9d6 100644 --- a/.github/workflows/release-cli.yml +++ b/.github/workflows/release-cli.yml @@ -25,7 +25,7 @@ jobs: strategy: matrix: goos: [linux, freebsd, darwin, windows] - goarch: [arm, amd64, arm64, riscv64] + goarch: [amd64, arm64, riscv64] exclude: - goarch: arm64 goos: windows @@ -33,11 +33,7 @@ jobs: goos: windows - goarch: riscv64 goos: darwin - - goarch: arm - goos: windows - - goarch: arm - goos: darwin - - goarch: arm + - goarch: arm64 goos: freebsd steps: - uses: actions/checkout@v3 diff --git a/go.mod b/go.mod index fd27fcf..1373eff 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,6 @@ module github.com/fiatjaf/nak -go 1.23.3 - -toolchain go1.23.4 +go 1.24.1 require ( github.com/bep/debounce v1.2.1 From d899a92f156e746ae081e211f0f8ed1af40017f0 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Mon, 10 Mar 2025 17:36:44 -0300 Subject: [PATCH 230/401] trying to get this thing to build again on github. --- go.mod | 4 ++-- go.sum | 4 ++-- nostrfs/npubdir.go | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 1373eff..f95f2b2 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,6 @@ go 1.24.1 require ( github.com/bep/debounce v1.2.1 github.com/btcsuite/btcd/btcec/v2 v2.3.4 - github.com/bytedance/sonic v1.13.1 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 github.com/fatih/color v1.16.0 @@ -17,7 +16,7 @@ require ( 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.1 + github.com/nbd-wtf/go-nostr v0.51.2 github.com/urfave/cli/v3 v3.0.0-beta1 golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac ) @@ -29,6 +28,7 @@ require ( github.com/btcsuite/btcd v0.24.2 // indirect github.com/btcsuite/btcd/btcutil v1.1.5 // indirect github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect + github.com/bytedance/sonic v1.13.1 // indirect github.com/bytedance/sonic/loader v0.2.4 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chzyer/logex v1.1.10 // indirect diff --git a/go.sum b/go.sum index aa842ac..dc27658 100644 --- a/go.sum +++ b/go.sum @@ -151,8 +151,8 @@ 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.1 h1:SKnvWRbcDAoNibMYWrNbG4V1VQzQUc9YWltUJsrSDbw= -github.com/nbd-wtf/go-nostr v0.51.1/go.mod h1:9PcGOZ+e1VOaLvcK0peT4dbip+/eS+eTWXR3HuexQrA= +github.com/nbd-wtf/go-nostr v0.51.2 h1:wQysG8omkF4LO7kcU6yoeCBBxD92SwUNab4TMeSuZZM= +github.com/nbd-wtf/go-nostr v0.51.2/go.mod h1:9PcGOZ+e1VOaLvcK0peT4dbip+/eS+eTWXR3HuexQrA= 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= diff --git a/nostrfs/npubdir.go b/nostrfs/npubdir.go index 8a20d06..8c9f286 100644 --- a/nostrfs/npubdir.go +++ b/nostrfs/npubdir.go @@ -3,13 +3,13 @@ package nostrfs import ( "bytes" "context" + "encoding/json" "io" "net/http" "sync/atomic" "syscall" "time" - "github.com/bytedance/sonic" "github.com/hanwen/go-fuse/v2/fs" "github.com/hanwen/go-fuse/v2/fuse" "github.com/liamg/magic" @@ -53,7 +53,7 @@ func CreateNpubDir( return } - metadataj, _ := sonic.ConfigFastest.MarshalIndent(pm, "", " ") + metadataj, _ := json.MarshalIndent(pm, "", " ") h.AddChild( "metadata.json", h.NewPersistentInode( From fe1f50f79886dae845a0474e84853549c1455bbe Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 11 Mar 2025 12:37:27 -0300 Subject: [PATCH 231/401] fs: logging and proper (?) handling of context passing (basically now we ignore the context given to us by the fuse library because they're weird). --- fs.go | 2 +- nostrfs/entitydir.go | 7 ++++++- nostrfs/eventdir.go | 2 +- nostrfs/root.go | 12 ++++++------ nostrfs/viewdir.go | 14 +++++++------- 5 files changed, 21 insertions(+), 16 deletions(-) diff --git a/fs.go b/fs.go index ba63b27..6b09e04 100644 --- a/fs.go +++ b/fs.go @@ -42,7 +42,7 @@ var fsCmd = &cli.Command{ } root := nostrfs.NewNostrRoot( - ctx, + context.WithValue(ctx, "log", log), sys, keyer.NewReadOnlyUser(c.String("pubkey")), mountpoint, diff --git a/nostrfs/entitydir.go b/nostrfs/entitydir.go index c45664c..040b12b 100644 --- a/nostrfs/entitydir.go +++ b/nostrfs/entitydir.go @@ -30,7 +30,7 @@ type EntityDir struct { var _ = (fs.NodeGetattrer)((*EntityDir)(nil)) -func (e *EntityDir) Getattr(ctx context.Context, f fs.FileHandle, out *fuse.AttrOut) syscall.Errno { +func (e *EntityDir) Getattr(_ context.Context, f fs.FileHandle, out *fuse.AttrOut) syscall.Errno { publishedAt := uint64(e.evt.CreatedAt) out.Ctime = publishedAt @@ -67,6 +67,8 @@ func CreateEntityDir( extension string, event *nostr.Event, ) *fs.Inode { + log := ctx.Value("log").(func(msg string, args ...any)) + h := parent.EmbeddedInode().NewPersistentInode( ctx, &EntityDir{ctx: ctx, wd: wd, evt: event}, @@ -179,14 +181,17 @@ func CreateEntityDir( defer cancel() r, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { + log("failed to load image %s: %s\n", url, err) return nil, 0 } resp, err := http.DefaultClient.Do(r) if err != nil { + log("failed to load image %s: %s\n", url, err) return nil, 0 } defer resp.Body.Close() if resp.StatusCode >= 300 { + log("failed to load image %s: %s\n", url, err) return nil, 0 } w := &bytes.Buffer{} diff --git a/nostrfs/eventdir.go b/nostrfs/eventdir.go index 253e99c..e2edcce 100644 --- a/nostrfs/eventdir.go +++ b/nostrfs/eventdir.go @@ -31,7 +31,7 @@ type EventDir struct { var _ = (fs.NodeGetattrer)((*EventDir)(nil)) -func (e *EventDir) Getattr(ctx context.Context, f fs.FileHandle, out *fuse.AttrOut) syscall.Errno { +func (e *EventDir) Getattr(_ context.Context, f fs.FileHandle, out *fuse.AttrOut) syscall.Errno { out.Mtime = uint64(e.evt.CreatedAt) return fs.OK } diff --git a/nostrfs/root.go b/nostrfs/root.go index 32c6cc1..6bdf969 100644 --- a/nostrfs/root.go +++ b/nostrfs/root.go @@ -40,7 +40,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 == "" { return } @@ -76,7 +76,7 @@ func (r *NostrRoot) OnAdd(context.Context) { ), true) } -func (r *NostrRoot) Lookup(ctx context.Context, name string, out *fuse.EntryOut) (*fs.Inode, syscall.Errno) { +func (r *NostrRoot) Lookup(_ context.Context, name string, out *fuse.EntryOut) (*fs.Inode, syscall.Errno) { out.SetEntryTimeout(time.Minute * 5) child := r.GetChild(name) @@ -84,8 +84,8 @@ func (r *NostrRoot) Lookup(ctx context.Context, name string, out *fuse.EntryOut) return child, fs.OK } - if pp, err := nip05.QueryIdentifier(ctx, name); err == nil { - npubdir := CreateNpubDir(ctx, r.sys, r, r.wd, *pp) + if pp, err := nip05.QueryIdentifier(r.ctx, name); err == nil { + npubdir := CreateNpubDir(r.ctx, r.sys, r, r.wd, *pp) return npubdir, fs.OK } @@ -96,10 +96,10 @@ func (r *NostrRoot) Lookup(ctx context.Context, name string, out *fuse.EntryOut) switch p := pointer.(type) { case nostr.ProfilePointer: - npubdir := CreateNpubDir(ctx, r.sys, r, r.wd, p) + npubdir := CreateNpubDir(r.ctx, r.sys, r, r.wd, p) return npubdir, fs.OK case nostr.EventPointer: - eventdir, err := FetchAndCreateEventDir(ctx, r, r.wd, r.sys, p) + eventdir, err := FetchAndCreateEventDir(r.ctx, r, r.wd, r.sys, p) if err != nil { return nil, syscall.ENOENT } diff --git a/nostrfs/viewdir.go b/nostrfs/viewdir.go index ddc6979..8e9cf6e 100644 --- a/nostrfs/viewdir.go +++ b/nostrfs/viewdir.go @@ -28,7 +28,7 @@ var ( _ = (fs.NodeGetattrer)((*ViewDir)(nil)) ) -func (n *ViewDir) Getattr(ctx context.Context, f fs.FileHandle, out *fuse.AttrOut) syscall.Errno { +func (n *ViewDir) Getattr(_ context.Context, f fs.FileHandle, out *fuse.AttrOut) syscall.Errno { now := nostr.Now() if n.filter.Until != nil { now = *n.filter.Until @@ -39,7 +39,7 @@ func (n *ViewDir) Getattr(ctx context.Context, f fs.FileHandle, out *fuse.AttrOu return fs.OK } -func (n *ViewDir) Opendir(ctx context.Context) syscall.Errno { +func (n *ViewDir) Opendir(_ context.Context) syscall.Errno { if n.fetched.CompareAndSwap(true, true) { return fs.OK } @@ -52,8 +52,8 @@ func (n *ViewDir) Opendir(ctx context.Context) syscall.Errno { aMonthAgo := now - 30*24*60*60 n.filter.Since = &aMonthAgo - for ie := range n.sys.Pool.FetchMany(ctx, n.relays, n.filter, nostr.WithLabel("nakfs")) { - basename, inode := n.create(ctx, n, ie.Event) + for ie := range n.sys.Pool.FetchMany(n.ctx, n.relays, n.filter, nostr.WithLabel("nakfs")) { + basename, inode := n.create(n.ctx, n, ie.Event) n.AddChild(basename, inode, true) } @@ -61,7 +61,7 @@ func (n *ViewDir) Opendir(ctx context.Context) syscall.Errno { filter.Until = &aMonthAgo n.AddChild("@previous", n.NewPersistentInode( - ctx, + n.ctx, &ViewDir{ ctx: n.ctx, sys: n.sys, @@ -72,8 +72,8 @@ func (n *ViewDir) Opendir(ctx context.Context) syscall.Errno { fs.StableAttr{Mode: syscall.S_IFDIR}, ), true) } else { - for ie := range n.sys.Pool.FetchMany(ctx, n.relays, n.filter, nostr.WithLabel("nakfs")) { - basename, inode := n.create(ctx, n, ie.Event) + for ie := range n.sys.Pool.FetchMany(n.ctx, n.relays, n.filter, nostr.WithLabel("nakfs")) { + basename, inode := n.create(n.ctx, n, ie.Event) n.AddChild(basename, inode, true) } } From 602e03a9a1ff33dbe792c6b56103b9554cd869b7 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 11 Mar 2025 12:37:38 -0300 Subject: [PATCH 232/401] fs: do not paginate videos and highlights (should make this dynamic in the future). --- nostrfs/npubdir.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nostrfs/npubdir.go b/nostrfs/npubdir.go index 8c9f286..38ecfb8 100644 --- a/nostrfs/npubdir.go +++ b/nostrfs/npubdir.go @@ -183,7 +183,7 @@ func CreateNpubDir( Kinds: []int{21, 22}, Authors: []string{pointer.PublicKey}, }, - paginate: true, + paginate: false, relays: relays, create: func(ctx context.Context, n *ViewDir, event *nostr.Event) (string, *fs.Inode) { return event.ID, CreateEventDir(ctx, n, n.wd, event) @@ -206,7 +206,7 @@ func CreateNpubDir( Kinds: []int{9802}, Authors: []string{pointer.PublicKey}, }, - paginate: true, + paginate: false, relays: relays, create: func(ctx context.Context, n *ViewDir, event *nostr.Event) (string, *fs.Inode) { return event.ID, CreateEventDir(ctx, n, n.wd, event) From bfe1e6ca940f28d41fd5aba027ab04eb6249a8bc Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 11 Mar 2025 12:38:20 -0300 Subject: [PATCH 233/401] fs: rename pictures -> photos. --- nostrfs/npubdir.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nostrfs/npubdir.go b/nostrfs/npubdir.go index 38ecfb8..c6b025b 100644 --- a/nostrfs/npubdir.go +++ b/nostrfs/npubdir.go @@ -149,7 +149,7 @@ func CreateNpubDir( ) h.AddChild( - "pictures", + "photos", h.NewPersistentInode( ctx, &ViewDir{ From c87371208eabd7b092a6500ddd4bb95cd4b0e104 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 11 Mar 2025 13:18:33 -0300 Subject: [PATCH 234/401] fs: pass NostrRoot everywhere with a signer only if it can actually sign. --- fs.go | 13 ++++-- nostrfs/entitydir.go | 45 ++++++++++----------- nostrfs/eventdir.go | 61 +++++++++++++--------------- nostrfs/npubdir.go | 94 ++++++++++++++++++-------------------------- nostrfs/root.go | 22 +++++++---- nostrfs/viewdir.go | 21 ++++------ 6 files changed, 119 insertions(+), 137 deletions(-) diff --git a/fs.go b/fs.go index 6b09e04..796c821 100644 --- a/fs.go +++ b/fs.go @@ -22,7 +22,7 @@ var fsCmd = &cli.Command{ Usage: "mount a FUSE filesystem that exposes Nostr events as files.", Description: `(experimental)`, ArgsUsage: "", - Flags: []cli.Flag{ + Flags: append(defaultKeyFlags, &cli.StringFlag{ Name: "pubkey", Usage: "public key from where to to prepopulate directories", @@ -33,7 +33,7 @@ var fsCmd = &cli.Command{ return fmt.Errorf("invalid public key '%s'", pk) }, }, - }, + ), DisableSliceFlagSeparator: true, Action: func(ctx context.Context, c *cli.Command) error { mountpoint := c.Args().First() @@ -41,10 +41,17 @@ var fsCmd = &cli.Command{ return fmt.Errorf("must be called with a directory path to serve as the mountpoint as an argument") } + var kr nostr.User + if signer, _, err := gatherKeyerFromArguments(ctx, c); err == nil { + kr = signer + } else { + kr = keyer.NewReadOnlyUser(c.String("pubkey")) + } + root := nostrfs.NewNostrRoot( context.WithValue(ctx, "log", log), sys, - keyer.NewReadOnlyUser(c.String("pubkey")), + kr, mountpoint, ) diff --git a/nostrfs/entitydir.go b/nostrfs/entitydir.go index 040b12b..c9411bf 100644 --- a/nostrfs/entitydir.go +++ b/nostrfs/entitydir.go @@ -42,36 +42,31 @@ func (e *EntityDir) Getattr(_ context.Context, f fs.FileHandle, out *fuse.AttrOu return fs.OK } -func FetchAndCreateEntityDir( - ctx context.Context, +func (r *NostrRoot) FetchAndCreateEntityDir( parent fs.InodeEmbedder, - wd string, extension string, - sys *sdk.System, pointer nostr.EntityPointer, ) (*fs.Inode, error) { - event, _, err := sys.FetchSpecificEvent(ctx, pointer, sdk.FetchSpecificEventParameters{ + event, _, err := r.sys.FetchSpecificEvent(r.ctx, pointer, sdk.FetchSpecificEventParameters{ WithRelays: false, }) if err != nil { return nil, fmt.Errorf("failed to fetch: %w", err) } - return CreateEntityDir(ctx, parent, wd, extension, event), nil + return r.CreateEntityDir(parent, extension, event), nil } -func CreateEntityDir( - ctx context.Context, +func (r *NostrRoot) CreateEntityDir( parent fs.InodeEmbedder, - wd string, extension string, event *nostr.Event, ) *fs.Inode { - log := ctx.Value("log").(func(msg string, args ...any)) + log := r.ctx.Value("log").(func(msg string, args ...any)) h := parent.EmbeddedInode().NewPersistentInode( - ctx, - &EntityDir{ctx: ctx, wd: wd, evt: event}, + r.ctx, + &EntityDir{ctx: r.ctx, wd: r.wd, evt: event}, fs.StableAttr{Mode: syscall.S_IFDIR, Ino: hexToUint64(event.ID)}, ) @@ -82,16 +77,16 @@ func CreateEntityDir( npub, _ := nip19.EncodePublicKey(event.PubKey) h.AddChild("@author", h.NewPersistentInode( - ctx, + r.ctx, &fs.MemSymlink{ - Data: []byte(wd + "/" + npub), + Data: []byte(r.wd + "/" + npub), }, fs.StableAttr{Mode: syscall.S_IFLNK}, ), true) eventj, _ := json.MarshalIndent(event, "", " ") h.AddChild("event.json", h.NewPersistentInode( - ctx, + r.ctx, &fs.MemRegularFile{ Data: eventj, Attr: fuse.Attr{ @@ -105,7 +100,7 @@ func CreateEntityDir( ), true) h.AddChild("identifier", h.NewPersistentInode( - ctx, + r.ctx, &fs.MemRegularFile{ Data: []byte(event.Tags.GetD()), Attr: fuse.Attr{ @@ -120,7 +115,7 @@ func CreateEntityDir( if tag := event.Tags.Find("title"); tag != nil { h.AddChild("title", h.NewPersistentInode( - ctx, + r.ctx, &fs.MemRegularFile{ Data: []byte(tag[1]), Attr: fuse.Attr{ @@ -135,7 +130,7 @@ func CreateEntityDir( } h.AddChild("content"+extension, h.NewPersistentInode( - ctx, + r.ctx, &fs.MemRegularFile{ Data: []byte(event.Content), Attr: fuse.Attr{ @@ -153,13 +148,13 @@ func CreateEntityDir( for ref := range nip27.ParseReferences(*event) { i++ if refsdir == nil { - refsdir = h.NewPersistentInode(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) } refsdir.AddChild(fmt.Sprintf("ref_%02d", i), refsdir.NewPersistentInode( - ctx, + r.ctx, &fs.MemSymlink{ - Data: []byte(wd + "/" + nip19.EncodePointer(ref.Pointer)), + Data: []byte(r.wd + "/" + nip19.EncodePointer(ref.Pointer)), }, fs.StableAttr{Mode: syscall.S_IFLNK}, ), true) @@ -169,15 +164,15 @@ func CreateEntityDir( addImage := func(url string) { if imagesdir == nil { in := &fs.Inode{} - imagesdir = h.NewPersistentInode(ctx, in, fs.StableAttr{Mode: syscall.S_IFDIR}) + imagesdir = h.NewPersistentInode(r.ctx, in, fs.StableAttr{Mode: syscall.S_IFDIR}) h.AddChild("images", imagesdir, true) } imagesdir.AddChild(filepath.Base(url), imagesdir.NewPersistentInode( - ctx, + r.ctx, &AsyncFile{ - ctx: ctx, + ctx: r.ctx, load: func() ([]byte, nostr.Timestamp) { - ctx, cancel := context.WithTimeout(ctx, time.Second*20) + ctx, cancel := context.WithTimeout(r.ctx, time.Second*20) defer cancel() r, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { diff --git a/nostrfs/eventdir.go b/nostrfs/eventdir.go index e2edcce..ad7baee 100644 --- a/nostrfs/eventdir.go +++ b/nostrfs/eventdir.go @@ -36,47 +36,42 @@ func (e *EventDir) Getattr(_ context.Context, f fs.FileHandle, out *fuse.AttrOut return fs.OK } -func FetchAndCreateEventDir( - ctx context.Context, +func (r *NostrRoot) FetchAndCreateEventDir( parent fs.InodeEmbedder, - wd string, - sys *sdk.System, pointer nostr.EventPointer, ) (*fs.Inode, error) { - event, _, err := sys.FetchSpecificEvent(ctx, pointer, sdk.FetchSpecificEventParameters{ + event, _, err := r.sys.FetchSpecificEvent(r.ctx, pointer, sdk.FetchSpecificEventParameters{ WithRelays: false, }) if err != nil { return nil, fmt.Errorf("failed to fetch: %w", err) } - return CreateEventDir(ctx, parent, wd, event), nil + return r.CreateEventDir(parent, event), nil } -func CreateEventDir( - ctx context.Context, +func (r *NostrRoot) CreateEventDir( parent fs.InodeEmbedder, - wd string, event *nostr.Event, ) *fs.Inode { h := parent.EmbeddedInode().NewPersistentInode( - ctx, - &EventDir{ctx: ctx, wd: wd, evt: event}, + r.ctx, + &EventDir{ctx: r.ctx, wd: r.wd, evt: event}, fs.StableAttr{Mode: syscall.S_IFDIR, Ino: hexToUint64(event.ID)}, ) npub, _ := nip19.EncodePublicKey(event.PubKey) h.AddChild("@author", h.NewPersistentInode( - ctx, + r.ctx, &fs.MemSymlink{ - Data: []byte(wd + "/" + npub), + Data: []byte(r.wd + "/" + npub), }, fs.StableAttr{Mode: syscall.S_IFLNK}, ), true) eventj, _ := json.MarshalIndent(event, "", " ") h.AddChild("event.json", h.NewPersistentInode( - ctx, + r.ctx, &fs.MemRegularFile{ Data: eventj, Attr: fuse.Attr{ @@ -90,7 +85,7 @@ func CreateEventDir( ), true) h.AddChild("id", h.NewPersistentInode( - ctx, + r.ctx, &fs.MemRegularFile{ Data: []byte(event.ID), Attr: fuse.Attr{ @@ -104,7 +99,7 @@ func CreateEventDir( ), true) h.AddChild("content.txt", h.NewPersistentInode( - ctx, + r.ctx, &fs.MemRegularFile{ Data: []byte(event.Content), Attr: fuse.Attr{ @@ -122,13 +117,13 @@ func CreateEventDir( for ref := range nip27.ParseReferences(*event) { i++ if refsdir == nil { - refsdir = h.NewPersistentInode(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) } refsdir.AddChild(fmt.Sprintf("ref_%02d", i), refsdir.NewPersistentInode( - ctx, + r.ctx, &fs.MemSymlink{ - Data: []byte(wd + "/" + nip19.EncodePointer(ref.Pointer)), + Data: []byte(r.wd + "/" + nip19.EncodePointer(ref.Pointer)), }, fs.StableAttr{Mode: syscall.S_IFLNK}, ), true) @@ -142,15 +137,15 @@ func CreateEventDir( } if imagesdir == nil { in := &fs.Inode{} - imagesdir = h.NewPersistentInode(ctx, in, fs.StableAttr{Mode: syscall.S_IFDIR}) + imagesdir = h.NewPersistentInode(r.ctx, in, fs.StableAttr{Mode: syscall.S_IFDIR}) h.AddChild("images", imagesdir, true) } imagesdir.AddChild(filepath.Base(imeta.URL), imagesdir.NewPersistentInode( - ctx, + r.ctx, &AsyncFile{ - ctx: ctx, + ctx: r.ctx, load: func() ([]byte, nostr.Timestamp) { - ctx, cancel := context.WithTimeout(ctx, time.Second*20) + ctx, cancel := context.WithTimeout(r.ctx, time.Second*20) defer cancel() r, err := http.NewRequestWithContext(ctx, "GET", imeta.URL, nil) if err != nil { @@ -177,9 +172,9 @@ func CreateEventDir( if pointer := nip10.GetThreadRoot(event.Tags); pointer != nil { nevent := nip19.EncodePointer(*pointer) h.AddChild("@root", h.NewPersistentInode( - ctx, + r.ctx, &fs.MemSymlink{ - Data: []byte(wd + "/" + nevent), + Data: []byte(r.wd + "/" + nevent), }, fs.StableAttr{Mode: syscall.S_IFLNK}, ), true) @@ -187,9 +182,9 @@ func CreateEventDir( if pointer := nip10.GetImmediateParent(event.Tags); pointer != nil { nevent := nip19.EncodePointer(*pointer) h.AddChild("@parent", h.NewPersistentInode( - ctx, + r.ctx, &fs.MemSymlink{ - Data: []byte(wd + "/" + nevent), + Data: []byte(r.wd + "/" + nevent), }, fs.StableAttr{Mode: syscall.S_IFLNK}, ), true) @@ -198,7 +193,7 @@ func CreateEventDir( if pointer := nip22.GetThreadRoot(event.Tags); pointer != nil { if xp, ok := pointer.(nostr.ExternalPointer); ok { h.AddChild("@root", h.NewPersistentInode( - ctx, + r.ctx, &fs.MemRegularFile{ Data: []byte(``), }, @@ -207,9 +202,9 @@ func CreateEventDir( } else { nevent := nip19.EncodePointer(pointer) h.AddChild("@parent", h.NewPersistentInode( - ctx, + r.ctx, &fs.MemSymlink{ - Data: []byte(wd + "/" + nevent), + Data: []byte(r.wd + "/" + nevent), }, fs.StableAttr{Mode: syscall.S_IFLNK}, ), true) @@ -218,7 +213,7 @@ func CreateEventDir( if pointer := nip22.GetImmediateParent(event.Tags); pointer != nil { if xp, ok := pointer.(nostr.ExternalPointer); ok { h.AddChild("@parent", h.NewPersistentInode( - ctx, + r.ctx, &fs.MemRegularFile{ Data: []byte(``), }, @@ -227,9 +222,9 @@ func CreateEventDir( } else { nevent := nip19.EncodePointer(pointer) h.AddChild("@parent", h.NewPersistentInode( - ctx, + r.ctx, &fs.MemSymlink{ - Data: []byte(wd + "/" + nevent), + Data: []byte(r.wd + "/" + nevent), }, fs.StableAttr{Mode: syscall.S_IFLNK}, ), true) diff --git a/nostrfs/npubdir.go b/nostrfs/npubdir.go index c6b025b..20df761 100644 --- a/nostrfs/npubdir.go +++ b/nostrfs/npubdir.go @@ -14,41 +14,37 @@ import ( "github.com/hanwen/go-fuse/v2/fuse" "github.com/liamg/magic" "github.com/nbd-wtf/go-nostr" - sdk "github.com/nbd-wtf/go-nostr/sdk" ) type NpubDir struct { - sys *sdk.System fs.Inode + root *NostrRoot pointer nostr.ProfilePointer - ctx context.Context fetched atomic.Bool } -func CreateNpubDir( - ctx context.Context, - sys *sdk.System, +func (r *NostrRoot) CreateNpubDir( parent fs.InodeEmbedder, - wd string, pointer nostr.ProfilePointer, + signer nostr.Signer, ) *fs.Inode { - npubdir := &NpubDir{ctx: ctx, sys: sys, pointer: pointer} + npubdir := &NpubDir{root: r, pointer: pointer} h := parent.EmbeddedInode().NewPersistentInode( - ctx, + r.ctx, npubdir, fs.StableAttr{Mode: syscall.S_IFDIR, Ino: hexToUint64(pointer.PublicKey)}, ) - relays := sys.FetchOutboxRelays(ctx, pointer.PublicKey, 2) + relays := r.sys.FetchOutboxRelays(r.ctx, pointer.PublicKey, 2) h.AddChild("pubkey", h.NewPersistentInode( - ctx, + r.ctx, &fs.MemRegularFile{Data: []byte(pointer.PublicKey + "\n"), Attr: fuse.Attr{Mode: 0444}}, fs.StableAttr{}, ), true) go func() { - pm := sys.FetchProfileMetadata(ctx, pointer.PublicKey) + pm := r.sys.FetchProfileMetadata(r.ctx, pointer.PublicKey) if pm.Event == nil { return } @@ -57,7 +53,7 @@ func CreateNpubDir( h.AddChild( "metadata.json", h.NewPersistentInode( - ctx, + r.ctx, &fs.MemRegularFile{ Data: metadataj, Attr: fuse.Attr{ @@ -70,7 +66,7 @@ func CreateNpubDir( true, ) - ctx, cancel := context.WithTimeout(ctx, time.Second*20) + ctx, cancel := context.WithTimeout(r.ctx, time.Second*20) defer cancel() r, err := http.NewRequestWithContext(ctx, "GET", pm.Picture, nil) if err == nil { @@ -105,19 +101,17 @@ func CreateNpubDir( h.AddChild( "notes", h.NewPersistentInode( - ctx, + r.ctx, &ViewDir{ - ctx: ctx, - sys: sys, - wd: wd, + root: r, filter: nostr.Filter{ Kinds: []int{1}, Authors: []string{pointer.PublicKey}, }, paginate: true, relays: relays, - create: func(ctx context.Context, n *ViewDir, event *nostr.Event) (string, *fs.Inode) { - return event.ID, CreateEventDir(ctx, n, n.wd, event) + create: func(n *ViewDir, event *nostr.Event) (string, *fs.Inode) { + return event.ID, r.CreateEventDir(n, event) }, }, fs.StableAttr{Mode: syscall.S_IFDIR}, @@ -128,19 +122,17 @@ func CreateNpubDir( h.AddChild( "comments", h.NewPersistentInode( - ctx, + r.ctx, &ViewDir{ - ctx: ctx, - sys: sys, - wd: wd, + root: r, filter: nostr.Filter{ Kinds: []int{1111}, Authors: []string{pointer.PublicKey}, }, paginate: true, relays: relays, - create: func(ctx context.Context, n *ViewDir, event *nostr.Event) (string, *fs.Inode) { - return event.ID, CreateEventDir(ctx, n, n.wd, event) + create: func(n *ViewDir, event *nostr.Event) (string, *fs.Inode) { + return event.ID, r.CreateEventDir(n, event) }, }, fs.StableAttr{Mode: syscall.S_IFDIR}, @@ -151,19 +143,17 @@ func CreateNpubDir( h.AddChild( "photos", h.NewPersistentInode( - ctx, + r.ctx, &ViewDir{ - ctx: ctx, - sys: sys, - wd: wd, + root: r, filter: nostr.Filter{ Kinds: []int{20}, Authors: []string{pointer.PublicKey}, }, paginate: true, relays: relays, - create: func(ctx context.Context, n *ViewDir, event *nostr.Event) (string, *fs.Inode) { - return event.ID, CreateEventDir(ctx, n, n.wd, event) + create: func(n *ViewDir, event *nostr.Event) (string, *fs.Inode) { + return event.ID, r.CreateEventDir(n, event) }, }, fs.StableAttr{Mode: syscall.S_IFDIR}, @@ -174,19 +164,17 @@ func CreateNpubDir( h.AddChild( "videos", h.NewPersistentInode( - ctx, + r.ctx, &ViewDir{ - ctx: ctx, - sys: sys, - wd: wd, + root: r, filter: nostr.Filter{ Kinds: []int{21, 22}, Authors: []string{pointer.PublicKey}, }, paginate: false, relays: relays, - create: func(ctx context.Context, n *ViewDir, event *nostr.Event) (string, *fs.Inode) { - return event.ID, CreateEventDir(ctx, n, n.wd, event) + create: func(n *ViewDir, event *nostr.Event) (string, *fs.Inode) { + return event.ID, r.CreateEventDir(n, event) }, }, fs.StableAttr{Mode: syscall.S_IFDIR}, @@ -197,19 +185,17 @@ func CreateNpubDir( h.AddChild( "highlights", h.NewPersistentInode( - ctx, + r.ctx, &ViewDir{ - ctx: ctx, - sys: sys, - wd: wd, + root: r, filter: nostr.Filter{ Kinds: []int{9802}, Authors: []string{pointer.PublicKey}, }, paginate: false, relays: relays, - create: func(ctx context.Context, n *ViewDir, event *nostr.Event) (string, *fs.Inode) { - return event.ID, CreateEventDir(ctx, n, n.wd, event) + create: func(n *ViewDir, event *nostr.Event) (string, *fs.Inode) { + return event.ID, r.CreateEventDir(n, event) }, }, fs.StableAttr{Mode: syscall.S_IFDIR}, @@ -220,23 +206,21 @@ func CreateNpubDir( h.AddChild( "articles", h.NewPersistentInode( - ctx, + r.ctx, &ViewDir{ - ctx: ctx, - sys: sys, - wd: wd, + root: r, filter: nostr.Filter{ Kinds: []int{30023}, Authors: []string{pointer.PublicKey}, }, paginate: false, relays: relays, - create: func(ctx context.Context, n *ViewDir, event *nostr.Event) (string, *fs.Inode) { + create: func(n *ViewDir, event *nostr.Event) (string, *fs.Inode) { d := event.Tags.GetD() if d == "" { d = "_" } - return d, CreateEntityDir(ctx, n, n.wd, ".md", event) + return d, r.CreateEntityDir(n, ".md", event) }, }, fs.StableAttr{Mode: syscall.S_IFDIR}, @@ -247,23 +231,21 @@ func CreateNpubDir( h.AddChild( "wiki", h.NewPersistentInode( - ctx, + r.ctx, &ViewDir{ - ctx: ctx, - sys: sys, - wd: wd, + root: r, filter: nostr.Filter{ Kinds: []int{30818}, Authors: []string{pointer.PublicKey}, }, paginate: false, relays: relays, - create: func(ctx context.Context, n *ViewDir, event *nostr.Event) (string, *fs.Inode) { + create: func(n *ViewDir, event *nostr.Event) (string, *fs.Inode) { d := event.Tags.GetD() if d == "" { d = "_" } - return d, CreateEntityDir(ctx, n, n.wd, ".adoc", event) + return d, r.CreateEntityDir(n, ".adoc", event) }, }, fs.StableAttr{Mode: syscall.S_IFDIR}, diff --git a/nostrfs/root.go b/nostrfs/root.go index 6bdf969..c36ec1a 100644 --- a/nostrfs/root.go +++ b/nostrfs/root.go @@ -28,9 +28,13 @@ var _ = (fs.NodeOnAdder)((*NostrRoot)(nil)) func NewNostrRoot(ctx context.Context, sys *sdk.System, user nostr.User, mountpoint string) *NostrRoot { pubkey, _ := user.GetPublicKey(ctx) - signer, _ := user.(nostr.Signer) abs, _ := filepath.Abs(mountpoint) + var signer nostr.Signer + if user != nil { + signer, _ = user.(nostr.Signer) + } + return &NostrRoot{ ctx: ctx, sys: sys, @@ -52,7 +56,7 @@ func (r *NostrRoot) OnAdd(_ context.Context) { npub, _ := nip19.EncodePublicKey(f.Pubkey) r.AddChild( npub, - CreateNpubDir(r.ctx, r.sys, r, r.wd, pointer), + r.CreateNpubDir(r, pointer, nil), true, ) } @@ -61,9 +65,10 @@ func (r *NostrRoot) OnAdd(_ context.Context) { npub, _ := nip19.EncodePublicKey(r.rootPubKey) if r.GetChild(npub) == nil { pointer := nostr.ProfilePointer{PublicKey: r.rootPubKey} + r.AddChild( npub, - CreateNpubDir(r.ctx, r.sys, r, r.wd, pointer), + r.CreateNpubDir(r, pointer, r.signer), true, ) } @@ -85,8 +90,11 @@ func (r *NostrRoot) Lookup(_ context.Context, name string, out *fuse.EntryOut) ( } if pp, err := nip05.QueryIdentifier(r.ctx, name); err == nil { - npubdir := CreateNpubDir(r.ctx, r.sys, r, r.wd, *pp) - return npubdir, fs.OK + return r.NewPersistentInode( + r.ctx, + &fs.MemSymlink{Data: []byte(r.wd + "/" + nip19.EncodePointer(*pp))}, + fs.StableAttr{Mode: syscall.S_IFLNK}, + ), fs.OK } pointer, err := nip19.ToPointer(name) @@ -96,10 +104,10 @@ func (r *NostrRoot) Lookup(_ context.Context, name string, out *fuse.EntryOut) ( switch p := pointer.(type) { case nostr.ProfilePointer: - npubdir := CreateNpubDir(r.ctx, r.sys, r, r.wd, p) + npubdir := r.CreateNpubDir(r, p, nil) return npubdir, fs.OK case nostr.EventPointer: - eventdir, err := FetchAndCreateEventDir(r.ctx, r, r.wd, r.sys, p) + eventdir, err := r.FetchAndCreateEventDir(r, p) if err != nil { return nil, syscall.ENOENT } diff --git a/nostrfs/viewdir.go b/nostrfs/viewdir.go index 8e9cf6e..1a45e91 100644 --- a/nostrfs/viewdir.go +++ b/nostrfs/viewdir.go @@ -8,19 +8,16 @@ import ( "github.com/hanwen/go-fuse/v2/fs" "github.com/hanwen/go-fuse/v2/fuse" "github.com/nbd-wtf/go-nostr" - sdk "github.com/nbd-wtf/go-nostr/sdk" ) type ViewDir struct { fs.Inode - ctx context.Context - sys *sdk.System - wd string + root *NostrRoot fetched atomic.Bool filter nostr.Filter paginate bool relays []string - create func(context.Context, *ViewDir, *nostr.Event) (string, *fs.Inode) + create func(*ViewDir, *nostr.Event) (string, *fs.Inode) } var ( @@ -52,8 +49,8 @@ func (n *ViewDir) Opendir(_ context.Context) syscall.Errno { aMonthAgo := now - 30*24*60*60 n.filter.Since = &aMonthAgo - for ie := range n.sys.Pool.FetchMany(n.ctx, n.relays, n.filter, nostr.WithLabel("nakfs")) { - basename, inode := n.create(n.ctx, n, ie.Event) + for ie := range n.root.sys.Pool.FetchMany(n.root.ctx, n.relays, n.filter, nostr.WithLabel("nakfs")) { + basename, inode := n.create(n, ie.Event) n.AddChild(basename, inode, true) } @@ -61,19 +58,17 @@ func (n *ViewDir) Opendir(_ context.Context) syscall.Errno { filter.Until = &aMonthAgo n.AddChild("@previous", n.NewPersistentInode( - n.ctx, + n.root.ctx, &ViewDir{ - ctx: n.ctx, - sys: n.sys, + root: n.root, filter: filter, - wd: n.wd, relays: n.relays, }, fs.StableAttr{Mode: syscall.S_IFDIR}, ), true) } else { - for ie := range n.sys.Pool.FetchMany(n.ctx, n.relays, n.filter, nostr.WithLabel("nakfs")) { - basename, inode := n.create(n.ctx, n, ie.Event) + for ie := range n.root.sys.Pool.FetchMany(n.root.ctx, n.relays, n.filter, nostr.WithLabel("nakfs")) { + basename, inode := n.create(n, ie.Event) n.AddChild(basename, inode, true) } } From 931da4b0ae12b93fcd6d9834d7fd33d8a3753d89 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Wed, 12 Mar 2025 08:03:10 -0300 Subject: [PATCH 235/401] fs: editable articles and wiki. --- go.mod | 24 +-- go.sum | 42 +++-- nostrfs/deterministicfile.go | 50 ++++++ nostrfs/entitydir.go | 328 +++++++++++++++++++++++++---------- nostrfs/npubdir.go | 71 +++----- nostrfs/viewdir.go | 45 +++-- nostrfs/writeablefile.go | 88 ++++++++++ 7 files changed, 466 insertions(+), 182 deletions(-) create mode 100644 nostrfs/deterministicfile.go create mode 100644 nostrfs/writeablefile.go diff --git a/go.mod b/go.mod index f95f2b2..10cdd41 100644 --- a/go.mod +++ b/go.mod @@ -3,26 +3,26 @@ module github.com/fiatjaf/nak go 1.24.1 require ( + fiatjaf.com/lib v0.3.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.3.0 + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 github.com/fatih/color v1.16.0 - github.com/fiatjaf/eventstore v0.15.0 - github.com/fiatjaf/khatru v0.16.0 + github.com/fiatjaf/eventstore v0.16.2 + github.com/fiatjaf/khatru v0.17.3-0.20250312035319-596bca93c3ff 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.2 + github.com/nbd-wtf/go-nostr v0.51.3-0.20250312034958-cc23d81e8055 github.com/urfave/cli/v3 v3.0.0-beta1 - golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac + golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 ) require ( - fiatjaf.com/lib v0.2.0 // 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 @@ -57,7 +57,7 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/puzpuzpuz/xsync/v3 v3.5.0 // indirect + github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect github.com/rs/cors v1.11.1 // indirect github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 // indirect github.com/tetratelabs/wazero v1.8.0 // indirect @@ -66,12 +66,14 @@ require ( github.com/tidwall/pretty v1.2.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/valyala/fasthttp v1.58.0 // indirect + github.com/valyala/fasthttp v1.59.0 // indirect github.com/wasilibs/go-re2 v1.3.0 // indirect github.com/x448/float16 v0.8.4 // indirect golang.org/x/arch v0.15.0 // indirect - golang.org/x/crypto v0.32.0 // indirect - golang.org/x/net v0.34.0 // indirect + golang.org/x/crypto v0.36.0 // indirect + golang.org/x/net v0.37.0 // indirect golang.org/x/sys v0.31.0 // indirect - golang.org/x/text v0.21.0 // indirect + golang.org/x/text v0.23.0 // indirect ) + +replace github.com/nbd-wtf/go-nostr => ../go-nostr diff --git a/go.sum b/go.sum index dc27658..2885144 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -fiatjaf.com/lib v0.2.0 h1:TgIJESbbND6GjOgGHxF5jsO6EMjuAxIzZHPo5DXYexs= -fiatjaf.com/lib v0.2.0/go.mod h1:Ycqq3+mJ9jAWu7XjbQI1cVr+OFgnHn79dQR5oTII47g= +fiatjaf.com/lib v0.3.1 h1:/oFQwNtFRfV+ukmOCxfBEAuayoLwXp4wu2/fz5iHpwA= +fiatjaf.com/lib v0.3.1/go.mod h1:Ycqq3+mJ9jAWu7XjbQI1cVr+OFgnHn79dQR5oTII47g= 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/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= @@ -59,8 +59,8 @@ github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8= github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= github.com/dgraph-io/ristretto v1.0.0 h1:SYG07bONKMlFDUYu5pEu3DGAh8c2OFNzKm6G9J4Si84= github.com/dgraph-io/ristretto v1.0.0/go.mod h1:jTi2FiYEhQ1NsMmA7DeBykizjOuY88NhKBkepyu1jPc= @@ -77,10 +77,10 @@ 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.15.0 h1:5UXe0+vIb30/cYcOWipks8nR3g+X8W224TFy5yPzivk= -github.com/fiatjaf/eventstore v0.15.0/go.mod h1:KAsld5BhkmSck48aF11Txu8X+OGNmoabw4TlYVWqInc= -github.com/fiatjaf/khatru v0.16.0 h1:xgGwnnOqE3989wEWm7c/z6Y6g4X92BFe/Xp1UWQ3Zmc= -github.com/fiatjaf/khatru v0.16.0/go.mod h1:TLcMgPy3IAPh40VGYq6m+gxEMpDKHj+sumqcuvbSogc= +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.3-0.20250312035319-596bca93c3ff h1:b6LYwWlc8zAW6aoZpXYC3Gx/zkP4XW5amDx0VwyeREs= +github.com/fiatjaf/khatru v0.17.3-0.20250312035319-596bca93c3ff/go.mod h1:dAaXV6QZwuMVYlXQigp/0Uyl/m1nKOhtRssjQYsgMu0= 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= @@ -151,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.2 h1:wQysG8omkF4LO7kcU6yoeCBBxD92SwUNab4TMeSuZZM= -github.com/nbd-wtf/go-nostr v0.51.2/go.mod h1:9PcGOZ+e1VOaLvcK0peT4dbip+/eS+eTWXR3HuexQrA= 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= @@ -166,8 +164,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/puzpuzpuz/xsync/v3 v3.5.0 h1:i+cMcpEDY1BkNm7lPDkCtE4oElsYLn+EKF8kAu2vXT4= -github.com/puzpuzpuz/xsync/v3 v3.5.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= +github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg= +github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 h1:D0vL7YNisV2yqE55+q0lFuGse6U8lxlg7fYTctlT5Gc= @@ -199,8 +197,8 @@ github.com/urfave/cli/v3 v3.0.0-beta1 h1:6DTaaUarcM0wX7qj5Hcvs+5Dm3dyUTBbEwIWAjc github.com/urfave/cli/v3 v3.0.0-beta1/go.mod h1:FnIeEMYu+ko8zP1F9Ypr3xkZMIDqW3DR92yUtY39q1Y= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasthttp v1.58.0 h1:GGB2dWxSbEprU9j0iMJHgdKYJVDyjrOwF9RE59PbRuE= -github.com/valyala/fasthttp v1.58.0/go.mod h1:SYXvHHaFp7QZHGKSHmoMipInhrI5StHrhDTYVEjK/Kw= +github.com/valyala/fasthttp v1.59.0 h1:Qu0qYHfXvPk1mSLNqcFtEk6DpxgA26hy6bmydotDpRI= +github.com/valyala/fasthttp v1.59.0/go.mod h1:GTxNb9Bc6r2a9D0TWNSPwDz78UxnTGBViY3xZNEqyYU= github.com/wasilibs/go-re2 v1.3.0 h1:LFhBNzoStM3wMie6rN2slD1cuYH2CGiHpvNL3UtcsMw= github.com/wasilibs/go-re2 v1.3.0/go.mod h1:AafrCXVvGRJJOImMajgJ2M7rVmWyisVK7sFshbxnVrg= github.com/wasilibs/nottinygc v0.4.0 h1:h1TJMihMC4neN6Zq+WKpLxgd9xCFMw7O9ETLwY2exJQ= @@ -214,17 +212,17 @@ golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= -golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= -golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac h1:l5+whBCLH3iH2ZNHYLbAe58bo7yrN4mVcnkHDYz5vvs= -golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac/go.mod h1:hH+7mtFmImwwcMvScyxUhjuVHR3HGaDPMn9rMSUUbxo= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= +golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= -golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= +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/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= @@ -242,8 +240,8 @@ golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/nostrfs/deterministicfile.go b/nostrfs/deterministicfile.go new file mode 100644 index 0000000..95fe030 --- /dev/null +++ b/nostrfs/deterministicfile.go @@ -0,0 +1,50 @@ +package nostrfs + +import ( + "context" + "syscall" + "unsafe" + + "github.com/hanwen/go-fuse/v2/fs" + "github.com/hanwen/go-fuse/v2/fuse" +) + +type DeterministicFile struct { + fs.Inode + get func() (ctime, mtime uint64, data string) +} + +var ( + _ = (fs.NodeOpener)((*DeterministicFile)(nil)) + _ = (fs.NodeReader)((*DeterministicFile)(nil)) + _ = (fs.NodeGetattrer)((*DeterministicFile)(nil)) +) + +func (r *NostrRoot) NewDeterministicFile(get func() (ctime, mtime uint64, data string)) *DeterministicFile { + return &DeterministicFile{ + get: get, + } +} + +func (f *DeterministicFile) Open(ctx context.Context, flags uint32) (fh fs.FileHandle, fuseFlags uint32, errno syscall.Errno) { + return nil, fuse.FOPEN_KEEP_CACHE, fs.OK +} + +func (f *DeterministicFile) Getattr(ctx context.Context, fh fs.FileHandle, out *fuse.AttrOut) syscall.Errno { + var content string + out.Mode = 0444 + out.Ctime, out.Mtime, content = f.get() + out.Size = uint64(len(content)) + return fs.OK +} + +func (f *DeterministicFile) Read(ctx context.Context, fh fs.FileHandle, dest []byte, off int64) (fuse.ReadResult, syscall.Errno) { + _, _, content := f.get() + data := unsafe.Slice(unsafe.StringData(content), len(content)) + + end := int(off) + len(dest) + if end > len(data) { + end = len(data) + } + return fuse.ReadResultData(data[off:end]), fs.OK +} diff --git a/nostrfs/entitydir.go b/nostrfs/entitydir.go index c9411bf..9a242aa 100644 --- a/nostrfs/entitydir.go +++ b/nostrfs/entitydir.go @@ -9,9 +9,13 @@ import ( "net/http" "path/filepath" "strconv" + "strings" "syscall" "time" + "unsafe" + "fiatjaf.com/lib/debouncer" + "github.com/fatih/color" "github.com/hanwen/go-fuse/v2/fs" "github.com/hanwen/go-fuse/v2/fuse" "github.com/nbd-wtf/go-nostr" @@ -23,18 +27,29 @@ import ( type EntityDir struct { fs.Inode - ctx context.Context - wd string - evt *nostr.Event + root *NostrRoot + + publisher *debouncer.Debouncer + extension string + event *nostr.Event + updating struct { + title string + content string + } } -var _ = (fs.NodeGetattrer)((*EntityDir)(nil)) +var ( + _ = (fs.NodeOnAdder)((*EntityDir)(nil)) + _ = (fs.NodeGetattrer)((*EntityDir)(nil)) + _ = (fs.NodeCreater)((*EntityDir)(nil)) + _ = (fs.NodeUnlinker)((*EntityDir)(nil)) +) func (e *EntityDir) Getattr(_ context.Context, f fs.FileHandle, out *fuse.AttrOut) syscall.Errno { - publishedAt := uint64(e.evt.CreatedAt) + publishedAt := uint64(e.event.CreatedAt) out.Ctime = publishedAt - if tag := e.evt.Tags.Find("published_at"); tag != nil { + if tag := e.event.Tags.Find("published_at"); tag != nil { publishedAt, _ = strconv.ParseUint(tag[1], 10, 64) } out.Mtime = publishedAt @@ -42,119 +57,147 @@ func (e *EntityDir) Getattr(_ context.Context, f fs.FileHandle, out *fuse.AttrOu return fs.OK } -func (r *NostrRoot) FetchAndCreateEntityDir( - parent fs.InodeEmbedder, - extension string, - pointer nostr.EntityPointer, -) (*fs.Inode, error) { - event, _, err := r.sys.FetchSpecificEvent(r.ctx, pointer, sdk.FetchSpecificEventParameters{ - WithRelays: false, - }) - if err != nil { - return nil, fmt.Errorf("failed to fetch: %w", err) +func (e *EntityDir) Create( + _ context.Context, + name string, + flags uint32, + mode uint32, + out *fuse.EntryOut, +) (node *fs.Inode, fh fs.FileHandle, fuseFlags uint32, errno syscall.Errno) { + if name == "publish" { + // this causes the publish process to be triggered faster + e.publisher.Flush() + return nil, nil, 0, syscall.ENOTDIR } - return r.CreateEntityDir(parent, extension, event), nil + return nil, nil, 0, syscall.ENOTSUP } -func (r *NostrRoot) CreateEntityDir( - parent fs.InodeEmbedder, - extension string, - event *nostr.Event, -) *fs.Inode { - log := r.ctx.Value("log").(func(msg string, args ...any)) +func (e *EntityDir) Unlink(ctx context.Context, name string) syscall.Errno { + switch name { + case "content" + e.extension: + e.updating.content = e.event.Content + return syscall.ENOTDIR + case "title": + e.updating.title = "" + if titleTag := e.event.Tags.Find("title"); titleTag != nil { + e.updating.title = titleTag[1] + } + return syscall.ENOTDIR + default: + return syscall.EINTR + } +} - h := parent.EmbeddedInode().NewPersistentInode( - r.ctx, - &EntityDir{ctx: r.ctx, wd: r.wd, evt: event}, - fs.StableAttr{Mode: syscall.S_IFDIR, Ino: hexToUint64(event.ID)}, - ) - - var publishedAt uint64 - if tag := event.Tags.Find("published_at"); tag != nil { +func (e *EntityDir) OnAdd(_ context.Context) { + log := e.root.ctx.Value("log").(func(msg string, args ...any)) + publishedAt := uint64(e.event.CreatedAt) + if tag := e.event.Tags.Find("published_at"); tag != nil { publishedAt, _ = strconv.ParseUint(tag[1], 10, 64) } - npub, _ := nip19.EncodePublicKey(event.PubKey) - h.AddChild("@author", h.NewPersistentInode( - r.ctx, + npub, _ := nip19.EncodePublicKey(e.event.PubKey) + e.AddChild("@author", e.NewPersistentInode( + e.root.ctx, &fs.MemSymlink{ - Data: []byte(r.wd + "/" + npub), + Data: []byte(e.root.wd + "/" + npub), }, fs.StableAttr{Mode: syscall.S_IFLNK}, ), true) - eventj, _ := json.MarshalIndent(event, "", " ") - h.AddChild("event.json", h.NewPersistentInode( - r.ctx, - &fs.MemRegularFile{ - Data: eventj, - Attr: fuse.Attr{ - Mode: 0444, - Ctime: uint64(event.CreatedAt), - Mtime: uint64(publishedAt), - Size: uint64(len(event.Content)), + e.AddChild("event.json", e.NewPersistentInode( + e.root.ctx, + &DeterministicFile{ + get: func() (ctime uint64, mtime uint64, data string) { + eventj, _ := json.MarshalIndent(e.event, "", " ") + return uint64(e.event.CreatedAt), + uint64(e.event.CreatedAt), + unsafe.String(unsafe.SliceData(eventj), len(eventj)) }, }, fs.StableAttr{}, ), true) - h.AddChild("identifier", h.NewPersistentInode( - r.ctx, + e.AddChild("identifier", e.NewPersistentInode( + e.root.ctx, &fs.MemRegularFile{ - Data: []byte(event.Tags.GetD()), + Data: []byte(e.event.Tags.GetD()), Attr: fuse.Attr{ Mode: 0444, - Ctime: uint64(event.CreatedAt), - Mtime: uint64(publishedAt), - Size: uint64(len(event.Tags.GetD())), + Ctime: uint64(e.event.CreatedAt), + Mtime: uint64(e.event.CreatedAt), + Size: uint64(len(e.event.Tags.GetD())), }, }, fs.StableAttr{}, ), true) - if tag := event.Tags.Find("title"); tag != nil { - h.AddChild("title", h.NewPersistentInode( - r.ctx, - &fs.MemRegularFile{ - Data: []byte(tag[1]), - Attr: fuse.Attr{ - Mode: 0444, - Ctime: uint64(event.CreatedAt), - Mtime: uint64(publishedAt), - Size: uint64(len(tag[1])), + if e.root.signer == nil { + // read-only + e.AddChild("title", e.NewPersistentInode( + e.root.ctx, + &DeterministicFile{ + get: func() (ctime uint64, mtime uint64, data string) { + var title string + if tag := e.event.Tags.Find("title"); tag != nil { + title = tag[1] + } else { + title = e.event.Tags.GetD() + } + return uint64(e.event.CreatedAt), publishedAt, title }, }, fs.StableAttr{}, ), true) - } - - h.AddChild("content"+extension, h.NewPersistentInode( - r.ctx, - &fs.MemRegularFile{ - Data: []byte(event.Content), - Attr: fuse.Attr{ - Mode: 0444, - Ctime: uint64(event.CreatedAt), - Mtime: uint64(publishedAt), - Size: uint64(len(event.Content)), + e.AddChild("content."+e.extension, e.NewPersistentInode( + e.root.ctx, + &DeterministicFile{ + get: func() (ctime uint64, mtime uint64, data string) { + return uint64(e.event.CreatedAt), publishedAt, e.event.Content + }, }, - }, - fs.StableAttr{}, - ), true) + fs.StableAttr{}, + ), true) + } else { + // writeable + if tag := e.event.Tags.Find("title"); tag != nil { + e.updating.title = tag[1] + } + e.updating.content = e.event.Content + + e.AddChild("title", e.NewPersistentInode( + e.root.ctx, + e.root.NewWriteableFile(e.updating.title, uint64(e.event.CreatedAt), publishedAt, func(s string) { + log("title updated") + e.updating.title = strings.TrimSpace(s) + e.handleWrite() + }), + fs.StableAttr{}, + ), true) + + e.AddChild("content."+e.extension, e.NewPersistentInode( + e.root.ctx, + e.root.NewWriteableFile(e.updating.content, uint64(e.event.CreatedAt), publishedAt, func(s string) { + log("content updated") + e.updating.content = strings.TrimSpace(s) + e.handleWrite() + }), + fs.StableAttr{}, + ), true) + } var refsdir *fs.Inode i := 0 - for ref := range nip27.ParseReferences(*event) { + for ref := range nip27.ParseReferences(*e.event) { i++ if refsdir == nil { - refsdir = h.NewPersistentInode(r.ctx, &fs.Inode{}, fs.StableAttr{Mode: syscall.S_IFDIR}) - h.AddChild("references", refsdir, true) + refsdir = e.NewPersistentInode(e.root.ctx, &fs.Inode{}, fs.StableAttr{Mode: syscall.S_IFDIR}) + e.root.AddChild("references", refsdir, true) } refsdir.AddChild(fmt.Sprintf("ref_%02d", i), refsdir.NewPersistentInode( - r.ctx, + e.root.ctx, &fs.MemSymlink{ - Data: []byte(r.wd + "/" + nip19.EncodePointer(ref.Pointer)), + Data: []byte(e.root.wd + "/" + nip19.EncodePointer(ref.Pointer)), }, fs.StableAttr{Mode: syscall.S_IFLNK}, ), true) @@ -164,15 +207,15 @@ func (r *NostrRoot) CreateEntityDir( addImage := func(url string) { if imagesdir == nil { in := &fs.Inode{} - imagesdir = h.NewPersistentInode(r.ctx, in, fs.StableAttr{Mode: syscall.S_IFDIR}) - h.AddChild("images", imagesdir, true) + imagesdir = e.NewPersistentInode(e.root.ctx, in, fs.StableAttr{Mode: syscall.S_IFDIR}) + e.AddChild("images", imagesdir, true) } imagesdir.AddChild(filepath.Base(url), imagesdir.NewPersistentInode( - r.ctx, + e.root.ctx, &AsyncFile{ - ctx: r.ctx, + ctx: e.root.ctx, load: func() ([]byte, nostr.Timestamp) { - ctx, cancel := context.WithTimeout(r.ctx, time.Second*20) + ctx, cancel := context.WithTimeout(e.root.ctx, time.Second*20) defer cancel() r, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { @@ -198,7 +241,7 @@ func (r *NostrRoot) CreateEntityDir( ), true) } - images := nip92.ParseTags(event.Tags) + images := nip92.ParseTags(e.event.Tags) for _, imeta := range images { if imeta.URL == "" { continue @@ -206,9 +249,116 @@ func (r *NostrRoot) CreateEntityDir( addImage(imeta.URL) } - if tag := event.Tags.Find("image"); tag != nil { + if tag := e.event.Tags.Find("image"); tag != nil { addImage(tag[1]) } - - return h +} + +func (e *EntityDir) handleWrite() { + log := e.root.ctx.Value("log").(func(msg string, args ...any)) + + if e.publisher.IsRunning() { + log(", timer reset") + } + log(", will publish the updated event in 30 seconds...\n") + if !e.publisher.IsRunning() { + log("- `touch publish` to publish immediately\n") + log("- `rm title content." + e.extension + "` to erase and cancel the edits\n") + } + + e.publisher.Call(func() { + if currentTitle := e.event.Tags.Find("title"); (currentTitle != nil && currentTitle[1] == e.updating.title) || (currentTitle == nil && e.updating.title == "") && e.updating.content == e.event.Content { + log("back into the previous state, not publishing.\n") + return + } + + evt := nostr.Event{ + Kind: e.event.Kind, + Content: e.updating.content, + Tags: make(nostr.Tags, len(e.event.Tags)), + CreatedAt: nostr.Now(), + } + copy(evt.Tags, e.event.Tags) + if e.updating.title != "" { + if titleTag := evt.Tags.Find("title"); titleTag != nil { + titleTag[1] = e.updating.title + } else { + evt.Tags = append(evt.Tags, nostr.Tag{"title", e.updating.title}) + } + } + if publishedAtTag := evt.Tags.Find("published_at"); publishedAtTag == nil { + evt.Tags = append(evt.Tags, nostr.Tag{ + "published_at", + strconv.FormatInt(int64(e.event.CreatedAt), 10), + }) + } + for ref := range nip27.ParseReferences(evt) { + tag := ref.Pointer.AsTag() + if existing := evt.Tags.FindWithValue(tag[0], tag[1]); existing == nil { + evt.Tags = append(evt.Tags, tag) + } + } + if err := e.root.signer.SignEvent(e.root.ctx, &evt); err != nil { + log("failed to sign: '%s'.\n", err) + return + } + + relays := e.root.sys.FetchWriteRelays(e.root.ctx, evt.PubKey, 8) + log("publishing to %d relays... ", len(relays)) + success := false + first := true + for res := range e.root.sys.Pool.PublishMany(e.root.ctx, relays, evt) { + cleanUrl, ok := strings.CutPrefix(res.RelayURL, "wss://") + if !ok { + cleanUrl = res.RelayURL + } + + if !first { + log(", ") + } + first = false + + if res.Error != nil { + log("%s: %s", color.RedString(cleanUrl), res.Error) + } else { + success = true + log("%s: ok", color.GreenString(cleanUrl)) + } + } + log("\n") + + if success { + e.event = &evt + log("event updated locally.\n") + } else { + log("failed.\n") + } + }) +} + +func (r *NostrRoot) FetchAndCreateEntityDir( + parent fs.InodeEmbedder, + extension string, + pointer nostr.EntityPointer, +) (*fs.Inode, error) { + event, _, err := r.sys.FetchSpecificEvent(r.ctx, pointer, sdk.FetchSpecificEventParameters{ + WithRelays: false, + }) + if err != nil { + return nil, fmt.Errorf("failed to fetch: %w", err) + } + + return r.CreateEntityDir(parent, extension, event), nil +} + +func (r *NostrRoot) CreateEntityDir( + parent fs.InodeEmbedder, + extension string, + event *nostr.Event, +) *fs.Inode { + return parent.EmbeddedInode().NewPersistentInode( + r.ctx, + &EntityDir{root: r, event: event, publisher: debouncer.New(time.Second * 30), extension: extension}, + fs.StableAttr{Mode: syscall.S_IFDIR, Ino: hexToUint64(event.ID)}, + ) } diff --git a/nostrfs/npubdir.go b/nostrfs/npubdir.go index 20df761..633137e 100644 --- a/nostrfs/npubdir.go +++ b/nostrfs/npubdir.go @@ -108,11 +108,10 @@ func (r *NostrRoot) CreateNpubDir( Kinds: []int{1}, Authors: []string{pointer.PublicKey}, }, - paginate: true, - relays: relays, - create: func(n *ViewDir, event *nostr.Event) (string, *fs.Inode) { - return event.ID, r.CreateEventDir(n, event) - }, + paginate: true, + relays: relays, + replaceable: false, + extension: "txt", }, fs.StableAttr{Mode: syscall.S_IFDIR}, ), @@ -129,11 +128,10 @@ func (r *NostrRoot) CreateNpubDir( Kinds: []int{1111}, Authors: []string{pointer.PublicKey}, }, - paginate: true, - relays: relays, - create: func(n *ViewDir, event *nostr.Event) (string, *fs.Inode) { - return event.ID, r.CreateEventDir(n, event) - }, + paginate: true, + relays: relays, + replaceable: false, + extension: "txt", }, fs.StableAttr{Mode: syscall.S_IFDIR}, ), @@ -150,11 +148,10 @@ func (r *NostrRoot) CreateNpubDir( Kinds: []int{20}, Authors: []string{pointer.PublicKey}, }, - paginate: true, - relays: relays, - create: func(n *ViewDir, event *nostr.Event) (string, *fs.Inode) { - return event.ID, r.CreateEventDir(n, event) - }, + paginate: true, + relays: relays, + replaceable: false, + extension: "txt", }, fs.StableAttr{Mode: syscall.S_IFDIR}, ), @@ -171,11 +168,10 @@ func (r *NostrRoot) CreateNpubDir( Kinds: []int{21, 22}, Authors: []string{pointer.PublicKey}, }, - paginate: false, - relays: relays, - create: func(n *ViewDir, event *nostr.Event) (string, *fs.Inode) { - return event.ID, r.CreateEventDir(n, event) - }, + paginate: false, + relays: relays, + replaceable: false, + extension: "txt", }, fs.StableAttr{Mode: syscall.S_IFDIR}, ), @@ -192,11 +188,10 @@ func (r *NostrRoot) CreateNpubDir( Kinds: []int{9802}, Authors: []string{pointer.PublicKey}, }, - paginate: false, - relays: relays, - create: func(n *ViewDir, event *nostr.Event) (string, *fs.Inode) { - return event.ID, r.CreateEventDir(n, event) - }, + paginate: false, + relays: relays, + replaceable: false, + extension: "txt", }, fs.StableAttr{Mode: syscall.S_IFDIR}, ), @@ -213,15 +208,10 @@ func (r *NostrRoot) CreateNpubDir( Kinds: []int{30023}, Authors: []string{pointer.PublicKey}, }, - paginate: false, - relays: relays, - create: func(n *ViewDir, event *nostr.Event) (string, *fs.Inode) { - d := event.Tags.GetD() - if d == "" { - d = "_" - } - return d, r.CreateEntityDir(n, ".md", event) - }, + paginate: false, + relays: relays, + replaceable: true, + extension: "md", }, fs.StableAttr{Mode: syscall.S_IFDIR}, ), @@ -238,15 +228,10 @@ func (r *NostrRoot) CreateNpubDir( Kinds: []int{30818}, Authors: []string{pointer.PublicKey}, }, - paginate: false, - relays: relays, - create: func(n *ViewDir, event *nostr.Event) (string, *fs.Inode) { - d := event.Tags.GetD() - if d == "" { - d = "_" - } - return d, r.CreateEntityDir(n, ".adoc", event) - }, + paginate: false, + relays: relays, + replaceable: true, + extension: "adoc", }, fs.StableAttr{Mode: syscall.S_IFDIR}, ), diff --git a/nostrfs/viewdir.go b/nostrfs/viewdir.go index 1a45e91..b4b94a1 100644 --- a/nostrfs/viewdir.go +++ b/nostrfs/viewdir.go @@ -12,12 +12,13 @@ import ( type ViewDir struct { fs.Inode - root *NostrRoot - fetched atomic.Bool - filter nostr.Filter - paginate bool - relays []string - create func(*ViewDir, *nostr.Event) (string, *fs.Inode) + root *NostrRoot + fetched atomic.Bool + filter nostr.Filter + paginate bool + relays []string + replaceable bool + extension string } var ( @@ -49,27 +50,37 @@ func (n *ViewDir) Opendir(_ context.Context) syscall.Errno { aMonthAgo := now - 30*24*60*60 n.filter.Since = &aMonthAgo - for ie := range n.root.sys.Pool.FetchMany(n.root.ctx, n.relays, n.filter, nostr.WithLabel("nakfs")) { - basename, inode := n.create(n, ie.Event) - n.AddChild(basename, inode, true) - } - filter := n.filter filter.Until = &aMonthAgo n.AddChild("@previous", n.NewPersistentInode( n.root.ctx, &ViewDir{ - root: n.root, - filter: filter, - relays: n.relays, + root: n.root, + filter: filter, + relays: n.relays, + extension: n.extension, + replaceable: n.replaceable, }, fs.StableAttr{Mode: syscall.S_IFDIR}, ), true) + } + + if n.replaceable { + for rkey, evt := range n.root.sys.Pool.FetchManyReplaceable(n.root.ctx, n.relays, n.filter, + nostr.WithLabel("nakfs"), + ).Range { + name := rkey.D + if name == "" { + name = "_" + } + n.AddChild(name, n.root.CreateEntityDir(n, n.extension, evt), true) + } } else { - for ie := range n.root.sys.Pool.FetchMany(n.root.ctx, n.relays, n.filter, nostr.WithLabel("nakfs")) { - basename, inode := n.create(n, ie.Event) - n.AddChild(basename, inode, true) + for ie := range n.root.sys.Pool.FetchMany(n.root.ctx, n.relays, n.filter, + nostr.WithLabel("nakfs"), + ) { + n.AddChild(ie.Event.ID, n.root.CreateEventDir(n, ie.Event), true) } } diff --git a/nostrfs/writeablefile.go b/nostrfs/writeablefile.go new file mode 100644 index 0000000..2c897fb --- /dev/null +++ b/nostrfs/writeablefile.go @@ -0,0 +1,88 @@ +package nostrfs + +import ( + "context" + "sync" + "syscall" + + "github.com/hanwen/go-fuse/v2/fs" + "github.com/hanwen/go-fuse/v2/fuse" +) + +type WriteableFile struct { + fs.Inode + root *NostrRoot + mu sync.Mutex + data []byte + attr fuse.Attr + onWrite func(string) +} + +var ( + _ = (fs.NodeOpener)((*WriteableFile)(nil)) + _ = (fs.NodeReader)((*WriteableFile)(nil)) + _ = (fs.NodeWriter)((*WriteableFile)(nil)) + _ = (fs.NodeGetattrer)((*WriteableFile)(nil)) + _ = (fs.NodeSetattrer)((*WriteableFile)(nil)) + _ = (fs.NodeFlusher)((*WriteableFile)(nil)) +) + +func (r *NostrRoot) NewWriteableFile(data string, ctime, mtime uint64, onWrite func(string)) *WriteableFile { + return &WriteableFile{ + root: r, + data: []byte(data), + attr: fuse.Attr{ + Mode: 0666, + Ctime: ctime, + Mtime: mtime, + Size: uint64(len(data)), + }, + onWrite: onWrite, + } +} + +func (f *WriteableFile) Open(ctx context.Context, flags uint32) (fh fs.FileHandle, fuseFlags uint32, errno syscall.Errno) { + return nil, fuse.FOPEN_KEEP_CACHE, fs.OK +} + +func (f *WriteableFile) Write(ctx context.Context, fh fs.FileHandle, data []byte, off int64) (uint32, syscall.Errno) { + f.mu.Lock() + defer f.mu.Unlock() + end := int64(len(data)) + off + if int64(len(f.data)) < end { + n := make([]byte, end) + copy(n, f.data) + f.data = n + } + copy(f.data[off:off+int64(len(data))], data) + + f.onWrite(string(f.data)) + + return uint32(len(data)), fs.OK +} + +func (f *WriteableFile) Getattr(ctx context.Context, fh fs.FileHandle, out *fuse.AttrOut) syscall.Errno { + f.mu.Lock() + defer f.mu.Unlock() + out.Attr = f.attr + out.Attr.Size = uint64(len(f.data)) + return fs.OK +} + +func (f *WriteableFile) Setattr(ctx context.Context, fh fs.FileHandle, in *fuse.SetAttrIn, out *fuse.AttrOut) syscall.Errno { + return fs.OK +} + +func (f *WriteableFile) Flush(ctx context.Context, fh fs.FileHandle) syscall.Errno { + return fs.OK +} + +func (f *WriteableFile) Read(ctx context.Context, fh fs.FileHandle, dest []byte, off int64) (fuse.ReadResult, syscall.Errno) { + f.mu.Lock() + defer f.mu.Unlock() + end := int(off) + len(dest) + if end > len(f.data) { + end = len(f.data) + } + return fuse.ReadResultData(f.data[off:end]), fs.OK +} From 4b8c067e000b2a8f45b8322c95fa44c4d7f746fc Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Thu, 13 Mar 2025 01:13:34 -0300 Subject: [PATCH 236/401] fs: creating articles (and presumably wikis); fixes and improvements to editing articles. --- fs.go | 25 +++- nostrfs/entitydir.go | 146 +++++++++++++------- nostrfs/helpers.go | 11 ++ nostrfs/npubdir.go | 291 +++++++++++++++++++++------------------ nostrfs/root.go | 68 +++++---- nostrfs/viewdir.go | 34 ++++- nostrfs/writeablefile.go | 18 +-- 7 files changed, 357 insertions(+), 236 deletions(-) diff --git a/fs.go b/fs.go index 796c821..1d8d048 100644 --- a/fs.go +++ b/fs.go @@ -33,6 +33,11 @@ var fsCmd = &cli.Command{ return fmt.Errorf("invalid public key '%s'", pk) }, }, + &cli.DurationFlag{ + Name: "auto-publish", + Usage: "delay after which edited articles will be auto-published.", + Value: time.Hour * 24 * 365 * 2, + }, ), DisableSliceFlagSeparator: true, Action: func(ctx context.Context, c *cli.Command) error { @@ -49,10 +54,19 @@ var fsCmd = &cli.Command{ } root := nostrfs.NewNostrRoot( - context.WithValue(ctx, "log", log), + context.WithValue( + context.WithValue( + ctx, + "log", log, + ), + "logverbose", logverbose, + ), sys, kr, mountpoint, + nostrfs.Options{ + AutoPublishTimeout: c.Duration("auto-publish"), + }, ) // create the server @@ -60,9 +74,10 @@ var fsCmd = &cli.Command{ timeout := time.Second * 120 server, err := fs.Mount(mountpoint, root, &fs.Options{ MountOptions: fuse.MountOptions{ - Debug: isVerbose, - Name: "nak", - FsName: "nak", + Debug: isVerbose, + Name: "nak", + FsName: "nak", + RememberInodes: true, }, AttrTimeout: &timeout, EntryTimeout: &timeout, @@ -71,7 +86,7 @@ var fsCmd = &cli.Command{ if err != nil { return fmt.Errorf("mount failed: %w", err) } - log("ok\n") + log("ok.\n") // setup signal handling for clean unmount ch := make(chan os.Signal, 1) diff --git a/nostrfs/entitydir.go b/nostrfs/entitydir.go index 9a242aa..bfdd275 100644 --- a/nostrfs/entitydir.go +++ b/nostrfs/entitydir.go @@ -30,30 +30,29 @@ type EntityDir struct { root *NostrRoot publisher *debouncer.Debouncer - extension string event *nostr.Event updating struct { - title string - content string + title string + content string + publishedAt uint64 } } var ( _ = (fs.NodeOnAdder)((*EntityDir)(nil)) _ = (fs.NodeGetattrer)((*EntityDir)(nil)) + _ = (fs.NodeSetattrer)((*EntityDir)(nil)) _ = (fs.NodeCreater)((*EntityDir)(nil)) _ = (fs.NodeUnlinker)((*EntityDir)(nil)) ) func (e *EntityDir) Getattr(_ context.Context, f fs.FileHandle, out *fuse.AttrOut) syscall.Errno { - publishedAt := uint64(e.event.CreatedAt) - out.Ctime = publishedAt - - if tag := e.event.Tags.Find("published_at"); tag != nil { - publishedAt, _ = strconv.ParseUint(tag[1], 10, 64) + out.Ctime = uint64(e.event.CreatedAt) + if e.updating.publishedAt != 0 { + out.Mtime = e.updating.publishedAt + } else { + out.Mtime = e.PublishedAt() } - out.Mtime = publishedAt - return fs.OK } @@ -64,8 +63,10 @@ func (e *EntityDir) Create( mode uint32, out *fuse.EntryOut, ) (node *fs.Inode, fh fs.FileHandle, fuseFlags uint32, errno syscall.Errno) { - if name == "publish" { + if name == "publish" && e.publisher.IsRunning() { // this causes the publish process to be triggered faster + log := e.root.ctx.Value("log").(func(msg string, args ...any)) + log("publishing now!\n") e.publisher.Flush() return nil, nil, 0, syscall.ENOTDIR } @@ -75,26 +76,24 @@ func (e *EntityDir) Create( func (e *EntityDir) Unlink(ctx context.Context, name string) syscall.Errno { switch name { - case "content" + e.extension: + case "content" + kindToExtension(e.event.Kind): e.updating.content = e.event.Content return syscall.ENOTDIR case "title": - e.updating.title = "" - if titleTag := e.event.Tags.Find("title"); titleTag != nil { - e.updating.title = titleTag[1] - } + e.updating.title = e.Title() return syscall.ENOTDIR default: return syscall.EINTR } } +func (e *EntityDir) Setattr(_ context.Context, _ fs.FileHandle, in *fuse.SetAttrIn, _ *fuse.AttrOut) syscall.Errno { + e.updating.publishedAt = in.Mtime + return fs.OK +} + func (e *EntityDir) OnAdd(_ context.Context) { log := e.root.ctx.Value("log").(func(msg string, args ...any)) - publishedAt := uint64(e.event.CreatedAt) - if tag := e.event.Tags.Find("published_at"); tag != nil { - publishedAt, _ = strconv.ParseUint(tag[1], 10, 64) - } npub, _ := nip19.EncodePublicKey(e.event.PubKey) e.AddChild("@author", e.NewPersistentInode( @@ -132,42 +131,35 @@ func (e *EntityDir) OnAdd(_ context.Context) { fs.StableAttr{}, ), true) - if e.root.signer == nil { + if e.root.signer == nil || e.root.rootPubKey != e.event.PubKey { // read-only e.AddChild("title", e.NewPersistentInode( e.root.ctx, &DeterministicFile{ get: func() (ctime uint64, mtime uint64, data string) { - var title string - if tag := e.event.Tags.Find("title"); tag != nil { - title = tag[1] - } else { - title = e.event.Tags.GetD() - } - return uint64(e.event.CreatedAt), publishedAt, title + return uint64(e.event.CreatedAt), e.PublishedAt(), e.Title() }, }, fs.StableAttr{}, ), true) - e.AddChild("content."+e.extension, e.NewPersistentInode( + e.AddChild("content."+kindToExtension(e.event.Kind), e.NewPersistentInode( e.root.ctx, &DeterministicFile{ get: func() (ctime uint64, mtime uint64, data string) { - return uint64(e.event.CreatedAt), publishedAt, e.event.Content + return uint64(e.event.CreatedAt), e.PublishedAt(), e.event.Content }, }, fs.StableAttr{}, ), true) } else { // writeable - if tag := e.event.Tags.Find("title"); tag != nil { - e.updating.title = tag[1] - } + e.updating.title = e.Title() + e.updating.publishedAt = e.PublishedAt() e.updating.content = e.event.Content e.AddChild("title", e.NewPersistentInode( e.root.ctx, - e.root.NewWriteableFile(e.updating.title, uint64(e.event.CreatedAt), publishedAt, func(s string) { + e.root.NewWriteableFile(e.updating.title, uint64(e.event.CreatedAt), e.updating.publishedAt, func(s string) { log("title updated") e.updating.title = strings.TrimSpace(s) e.handleWrite() @@ -175,9 +167,9 @@ func (e *EntityDir) OnAdd(_ context.Context) { fs.StableAttr{}, ), true) - e.AddChild("content."+e.extension, e.NewPersistentInode( + e.AddChild("content."+kindToExtension(e.event.Kind), e.NewPersistentInode( e.root.ctx, - e.root.NewWriteableFile(e.updating.content, uint64(e.event.CreatedAt), publishedAt, func(s string) { + e.root.NewWriteableFile(e.updating.content, uint64(e.event.CreatedAt), e.updating.publishedAt, func(s string) { log("content updated") e.updating.content = strings.TrimSpace(s) e.handleWrite() @@ -254,21 +246,51 @@ func (e *EntityDir) OnAdd(_ context.Context) { } } +func (e *EntityDir) IsNew() bool { + return e.event.CreatedAt == 0 +} + +func (e *EntityDir) PublishedAt() uint64 { + if tag := e.event.Tags.Find("published_at"); tag != nil { + publishedAt, _ := strconv.ParseUint(tag[1], 10, 64) + return publishedAt + } + return uint64(e.event.CreatedAt) +} + +func (e *EntityDir) Title() string { + if tag := e.event.Tags.Find("title"); tag != nil { + return tag[1] + } + return "" +} + func (e *EntityDir) handleWrite() { log := e.root.ctx.Value("log").(func(msg string, args ...any)) + logverbose := e.root.ctx.Value("logverbose").(func(msg string, args ...any)) - if e.publisher.IsRunning() { - log(", timer reset") + if e.root.opts.AutoPublishTimeout.Hours() < 24*365 { + if e.publisher.IsRunning() { + log(", timer reset") + } + log(", will publish the ") + if e.IsNew() { + log("new") + } else { + log("updated") + } + log(" event in %d seconds...\n", e.root.opts.AutoPublishTimeout.Seconds()) + } else { + log(".\n") } - log(", will publish the updated event in 30 seconds...\n") if !e.publisher.IsRunning() { log("- `touch publish` to publish immediately\n") - log("- `rm title content." + e.extension + "` to erase and cancel the edits\n") + log("- `rm title content." + kindToExtension(e.event.Kind) + "` to erase and cancel the edits\n") } e.publisher.Call(func() { - if currentTitle := e.event.Tags.Find("title"); (currentTitle != nil && currentTitle[1] == e.updating.title) || (currentTitle == nil && e.updating.title == "") && e.updating.content == e.event.Content { - log("back into the previous state, not publishing.\n") + if e.Title() == e.updating.title && e.event.Content == e.updating.content { + log("not modified, publish canceled.\n") return } @@ -278,7 +300,7 @@ func (e *EntityDir) handleWrite() { Tags: make(nostr.Tags, len(e.event.Tags)), CreatedAt: nostr.Now(), } - copy(evt.Tags, e.event.Tags) + copy(evt.Tags, e.event.Tags) // copy tags because that's the rule if e.updating.title != "" { if titleTag := evt.Tags.Find("title"); titleTag != nil { titleTag[1] = e.updating.title @@ -286,24 +308,42 @@ func (e *EntityDir) handleWrite() { evt.Tags = append(evt.Tags, nostr.Tag{"title", e.updating.title}) } } - if publishedAtTag := evt.Tags.Find("published_at"); publishedAtTag == nil { - evt.Tags = append(evt.Tags, nostr.Tag{ - "published_at", - strconv.FormatInt(int64(e.event.CreatedAt), 10), - }) + + // "published_at" tag + publishedAtStr := strconv.FormatUint(e.updating.publishedAt, 10) + if publishedAtStr != "0" { + if publishedAtTag := evt.Tags.Find("published_at"); publishedAtTag != nil { + publishedAtTag[1] = publishedAtStr + } else { + evt.Tags = append(evt.Tags, nostr.Tag{"published_at", publishedAtStr}) + } } + + // add "p" tags from people mentioned and "q" tags from events mentioned for ref := range nip27.ParseReferences(evt) { tag := ref.Pointer.AsTag() - if existing := evt.Tags.FindWithValue(tag[0], tag[1]); existing == nil { + key := tag[0] + val := tag[1] + if key == "e" || key == "a" { + key = "q" + } + if existing := evt.Tags.FindWithValue(key, val); existing == nil { evt.Tags = append(evt.Tags, tag) } } + + // sign and publish if err := e.root.signer.SignEvent(e.root.ctx, &evt); err != nil { log("failed to sign: '%s'.\n", err) return } + logverbose("%s\n", evt) relays := e.root.sys.FetchWriteRelays(e.root.ctx, evt.PubKey, 8) + if len(relays) == 0 { + relays = e.root.sys.FetchOutboxRelays(e.root.ctx, evt.PubKey, 6) + } + log("publishing to %d relays... ", len(relays)) success := false first := true @@ -330,6 +370,7 @@ func (e *EntityDir) handleWrite() { if success { e.event = &evt log("event updated locally.\n") + e.updating.publishedAt = uint64(evt.CreatedAt) // set this so subsequent edits get the correct value } else { log("failed.\n") } @@ -348,17 +389,16 @@ func (r *NostrRoot) FetchAndCreateEntityDir( return nil, fmt.Errorf("failed to fetch: %w", err) } - return r.CreateEntityDir(parent, extension, event), nil + return r.CreateEntityDir(parent, event), nil } func (r *NostrRoot) CreateEntityDir( parent fs.InodeEmbedder, - extension string, event *nostr.Event, ) *fs.Inode { return parent.EmbeddedInode().NewPersistentInode( r.ctx, - &EntityDir{root: r, event: event, publisher: debouncer.New(time.Second * 30), extension: extension}, - fs.StableAttr{Mode: syscall.S_IFDIR, Ino: hexToUint64(event.ID)}, + &EntityDir{root: r, event: event, publisher: debouncer.New(r.opts.AutoPublishTimeout)}, + fs.StableAttr{Mode: syscall.S_IFDIR}, ) } diff --git a/nostrfs/helpers.go b/nostrfs/helpers.go index 9cd82e5..e067e6b 100644 --- a/nostrfs/helpers.go +++ b/nostrfs/helpers.go @@ -2,6 +2,17 @@ package nostrfs import "strconv" +func kindToExtension(kind int) string { + switch kind { + case 30023: + return "md" + case 30818: + return "adoc" + default: + return "txt" + } +} + func hexToUint64(hexStr string) uint64 { v, _ := strconv.ParseUint(hexStr[16:32], 16, 64) return v diff --git a/nostrfs/npubdir.go b/nostrfs/npubdir.go index 633137e..b721002 100644 --- a/nostrfs/npubdir.go +++ b/nostrfs/npubdir.go @@ -10,10 +10,12 @@ import ( "syscall" "time" + "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 { @@ -23,28 +25,36 @@ type NpubDir struct { fetched atomic.Bool } +var _ = (fs.NodeOnAdder)((*NpubDir)(nil)) + func (r *NostrRoot) CreateNpubDir( parent fs.InodeEmbedder, pointer nostr.ProfilePointer, signer nostr.Signer, ) *fs.Inode { npubdir := &NpubDir{root: r, pointer: pointer} - h := parent.EmbeddedInode().NewPersistentInode( + return parent.EmbeddedInode().NewPersistentInode( r.ctx, npubdir, fs.StableAttr{Mode: syscall.S_IFDIR, Ino: hexToUint64(pointer.PublicKey)}, ) +} - relays := r.sys.FetchOutboxRelays(r.ctx, pointer.PublicKey, 2) +func (h *NpubDir) OnAdd(_ context.Context) { + log := h.root.ctx.Value("log").(func(msg string, args ...any)) + + relays := h.root.sys.FetchOutboxRelays(h.root.ctx, h.pointer.PublicKey, 2) + log("- adding folder for %s with relays %s\n", + color.HiYellowString(nip19.EncodePointer(h.pointer)), color.HiGreenString("%v", relays)) h.AddChild("pubkey", h.NewPersistentInode( - r.ctx, - &fs.MemRegularFile{Data: []byte(pointer.PublicKey + "\n"), Attr: fuse.Attr{Mode: 0444}}, + h.root.ctx, + &fs.MemRegularFile{Data: []byte(h.pointer.PublicKey + "\n"), Attr: fuse.Attr{Mode: 0444}}, fs.StableAttr{}, ), true) go func() { - pm := r.sys.FetchProfileMetadata(r.ctx, pointer.PublicKey) + pm := h.root.sys.FetchProfileMetadata(h.root.ctx, h.pointer.PublicKey) if pm.Event == nil { return } @@ -53,7 +63,7 @@ func (r *NostrRoot) CreateNpubDir( h.AddChild( "metadata.json", h.NewPersistentInode( - r.ctx, + h.root.ctx, &fs.MemRegularFile{ Data: metadataj, Attr: fuse.Attr{ @@ -66,11 +76,11 @@ func (r *NostrRoot) CreateNpubDir( true, ) - ctx, cancel := context.WithTimeout(r.ctx, time.Second*20) + ctx, cancel := context.WithTimeout(h.root.ctx, time.Second*20) defer cancel() - r, err := http.NewRequestWithContext(ctx, "GET", pm.Picture, nil) + req, err := http.NewRequestWithContext(ctx, "GET", pm.Picture, nil) if err == nil { - resp, err := http.DefaultClient.Do(r) + resp, err := http.DefaultClient.Do(req) if err == nil { defer resp.Body.Close() if resp.StatusCode < 300 { @@ -98,145 +108,152 @@ func (r *NostrRoot) CreateNpubDir( } }() - h.AddChild( - "notes", - h.NewPersistentInode( - r.ctx, - &ViewDir{ - root: r, - filter: nostr.Filter{ - Kinds: []int{1}, - Authors: []string{pointer.PublicKey}, + if h.GetChild("notes") == nil { + h.AddChild( + "notes", + h.NewPersistentInode( + h.root.ctx, + &ViewDir{ + root: h.root, + filter: nostr.Filter{ + Kinds: []int{1}, + Authors: []string{h.pointer.PublicKey}, + }, + paginate: true, + relays: relays, + replaceable: false, }, - paginate: true, - relays: relays, - replaceable: false, - extension: "txt", - }, - fs.StableAttr{Mode: syscall.S_IFDIR}, - ), - true, - ) + fs.StableAttr{Mode: syscall.S_IFDIR}, + ), + true, + ) + } - h.AddChild( - "comments", - h.NewPersistentInode( - r.ctx, - &ViewDir{ - root: r, - filter: nostr.Filter{ - Kinds: []int{1111}, - Authors: []string{pointer.PublicKey}, + if h.GetChild("comments") == nil { + h.AddChild( + "comments", + h.NewPersistentInode( + h.root.ctx, + &ViewDir{ + root: h.root, + filter: nostr.Filter{ + Kinds: []int{1111}, + Authors: []string{h.pointer.PublicKey}, + }, + paginate: true, + relays: relays, + replaceable: false, }, - paginate: true, - relays: relays, - replaceable: false, - extension: "txt", - }, - fs.StableAttr{Mode: syscall.S_IFDIR}, - ), - true, - ) + fs.StableAttr{Mode: syscall.S_IFDIR}, + ), + true, + ) + } - h.AddChild( - "photos", - h.NewPersistentInode( - r.ctx, - &ViewDir{ - root: r, - filter: nostr.Filter{ - Kinds: []int{20}, - Authors: []string{pointer.PublicKey}, + if h.GetChild("photos") == nil { + h.AddChild( + "photos", + h.NewPersistentInode( + h.root.ctx, + &ViewDir{ + root: h.root, + filter: nostr.Filter{ + Kinds: []int{20}, + Authors: []string{h.pointer.PublicKey}, + }, + paginate: true, + relays: relays, + replaceable: false, }, - paginate: true, - relays: relays, - replaceable: false, - extension: "txt", - }, - fs.StableAttr{Mode: syscall.S_IFDIR}, - ), - true, - ) + fs.StableAttr{Mode: syscall.S_IFDIR}, + ), + true, + ) + } - h.AddChild( - "videos", - h.NewPersistentInode( - r.ctx, - &ViewDir{ - root: r, - filter: nostr.Filter{ - Kinds: []int{21, 22}, - Authors: []string{pointer.PublicKey}, + if h.GetChild("videos") == nil { + h.AddChild( + "videos", + h.NewPersistentInode( + h.root.ctx, + &ViewDir{ + root: h.root, + filter: nostr.Filter{ + Kinds: []int{21, 22}, + Authors: []string{h.pointer.PublicKey}, + }, + paginate: false, + relays: relays, + replaceable: false, }, - paginate: false, - relays: relays, - replaceable: false, - extension: "txt", - }, - fs.StableAttr{Mode: syscall.S_IFDIR}, - ), - true, - ) + fs.StableAttr{Mode: syscall.S_IFDIR}, + ), + true, + ) + } - h.AddChild( - "highlights", - h.NewPersistentInode( - r.ctx, - &ViewDir{ - root: r, - filter: nostr.Filter{ - Kinds: []int{9802}, - Authors: []string{pointer.PublicKey}, + if h.GetChild("highlights") == nil { + h.AddChild( + "highlights", + h.NewPersistentInode( + h.root.ctx, + &ViewDir{ + root: h.root, + filter: nostr.Filter{ + Kinds: []int{9802}, + Authors: []string{h.pointer.PublicKey}, + }, + paginate: false, + relays: relays, + replaceable: false, }, - paginate: false, - relays: relays, - replaceable: false, - extension: "txt", - }, - fs.StableAttr{Mode: syscall.S_IFDIR}, - ), - true, - ) + fs.StableAttr{Mode: syscall.S_IFDIR}, + ), + true, + ) + } - h.AddChild( - "articles", - h.NewPersistentInode( - r.ctx, - &ViewDir{ - root: r, - filter: nostr.Filter{ - Kinds: []int{30023}, - Authors: []string{pointer.PublicKey}, + if h.GetChild("articles") == nil { + h.AddChild( + "articles", + h.NewPersistentInode( + h.root.ctx, + &ViewDir{ + root: h.root, + filter: nostr.Filter{ + Kinds: []int{30023}, + Authors: []string{h.pointer.PublicKey}, + }, + paginate: false, + relays: relays, + replaceable: true, + createable: true, }, - paginate: false, - relays: relays, - replaceable: true, - extension: "md", - }, - fs.StableAttr{Mode: syscall.S_IFDIR}, - ), - true, - ) + fs.StableAttr{Mode: syscall.S_IFDIR}, + ), + true, + ) + } - h.AddChild( - "wiki", - h.NewPersistentInode( - r.ctx, - &ViewDir{ - root: r, - filter: nostr.Filter{ - Kinds: []int{30818}, - Authors: []string{pointer.PublicKey}, + if h.GetChild("wiki") == nil { + h.AddChild( + "wiki", + h.NewPersistentInode( + h.root.ctx, + &ViewDir{ + root: h.root, + filter: nostr.Filter{ + Kinds: []int{30818}, + Authors: []string{h.pointer.PublicKey}, + }, + paginate: false, + relays: relays, + replaceable: true, + createable: true, }, - paginate: false, - relays: relays, - replaceable: true, - extension: "adoc", - }, - fs.StableAttr{Mode: syscall.S_IFDIR}, - ), - true, - ) - - return h + fs.StableAttr{Mode: syscall.S_IFDIR}, + ), + true, + ) + } } diff --git a/nostrfs/root.go b/nostrfs/root.go index c36ec1a..510969c 100644 --- a/nostrfs/root.go +++ b/nostrfs/root.go @@ -14,6 +14,10 @@ import ( "github.com/nbd-wtf/go-nostr/sdk" ) +type Options struct { + AutoPublishTimeout time.Duration // a negative number means do not publish +} + type NostrRoot struct { fs.Inode @@ -22,11 +26,13 @@ type NostrRoot struct { sys *sdk.System rootPubKey string signer nostr.Signer + + opts Options } var _ = (fs.NodeOnAdder)((*NostrRoot)(nil)) -func NewNostrRoot(ctx context.Context, sys *sdk.System, user nostr.User, mountpoint string) *NostrRoot { +func NewNostrRoot(ctx context.Context, sys *sdk.System, user nostr.User, mountpoint string, o Options) *NostrRoot { pubkey, _ := user.GetPublicKey(ctx) abs, _ := filepath.Abs(mountpoint) @@ -41,6 +47,8 @@ func NewNostrRoot(ctx context.Context, sys *sdk.System, user nostr.User, mountpo rootPubKey: pubkey, signer: signer, wd: abs, + + opts: o, } } @@ -49,36 +57,40 @@ func (r *NostrRoot) OnAdd(_ context.Context) { return } - // add our contacts - 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, - r.CreateNpubDir(r, pointer, nil), - true, - ) - } + go func() { + time.Sleep(time.Millisecond * 100) - // add ourselves - npub, _ := nip19.EncodePublicKey(r.rootPubKey) - if r.GetChild(npub) == nil { - pointer := nostr.ProfilePointer{PublicKey: r.rootPubKey} + // add our contacts + 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, + r.CreateNpubDir(r, pointer, nil), + true, + ) + } - r.AddChild( - npub, - r.CreateNpubDir(r, pointer, r.signer), - true, - ) - } + // add ourselves + npub, _ := nip19.EncodePublicKey(r.rootPubKey) + if r.GetChild(npub) == nil { + pointer := nostr.ProfilePointer{PublicKey: r.rootPubKey} - // add a link to ourselves - r.AddChild("@me", r.NewPersistentInode( - r.ctx, - &fs.MemSymlink{Data: []byte(r.wd + "/" + npub)}, - fs.StableAttr{Mode: syscall.S_IFLNK}, - ), true) + r.AddChild( + npub, + r.CreateNpubDir(r, pointer, r.signer), + true, + ) + } + + // add a link to ourselves + r.AddChild("@me", r.NewPersistentInode( + r.ctx, + &fs.MemSymlink{Data: []byte(r.wd + "/" + npub)}, + fs.StableAttr{Mode: syscall.S_IFLNK}, + ), true) + }() } func (r *NostrRoot) Lookup(_ context.Context, name string, out *fuse.EntryOut) (*fs.Inode, syscall.Errno) { diff --git a/nostrfs/viewdir.go b/nostrfs/viewdir.go index b4b94a1..838ab05 100644 --- a/nostrfs/viewdir.go +++ b/nostrfs/viewdir.go @@ -18,12 +18,13 @@ type ViewDir struct { paginate bool relays []string replaceable bool - extension string + createable bool } var ( _ = (fs.NodeOpendirer)((*ViewDir)(nil)) _ = (fs.NodeGetattrer)((*ViewDir)(nil)) + _ = (fs.NodeMkdirer)((*ViewDir)(nil)) ) func (n *ViewDir) Getattr(_ context.Context, f fs.FileHandle, out *fuse.AttrOut) syscall.Errno { @@ -37,7 +38,7 @@ func (n *ViewDir) Getattr(_ context.Context, f fs.FileHandle, out *fuse.AttrOut) return fs.OK } -func (n *ViewDir) Opendir(_ context.Context) syscall.Errno { +func (n *ViewDir) Opendir(ctx context.Context) syscall.Errno { if n.fetched.CompareAndSwap(true, true) { return fs.OK } @@ -59,7 +60,6 @@ func (n *ViewDir) Opendir(_ context.Context) syscall.Errno { root: n.root, filter: filter, relays: n.relays, - extension: n.extension, replaceable: n.replaceable, }, fs.StableAttr{Mode: syscall.S_IFDIR}, @@ -74,15 +74,39 @@ func (n *ViewDir) Opendir(_ context.Context) syscall.Errno { if name == "" { name = "_" } - n.AddChild(name, n.root.CreateEntityDir(n, n.extension, evt), true) + if n.GetChild(name) == nil { + 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"), ) { - n.AddChild(ie.Event.ID, n.root.CreateEventDir(n, ie.Event), true) + if n.GetChild(ie.Event.ID) == nil { + n.AddChild(ie.Event.ID, n.root.CreateEventDir(n, ie.Event), true) + } } } return fs.OK } + +func (n *ViewDir) Mkdir(ctx context.Context, name string, mode uint32, out *fuse.EntryOut) (*fs.Inode, syscall.Errno) { + if !n.createable || n.root.signer == nil || n.root.rootPubKey != n.filter.Authors[0] { + return nil, syscall.ENOTSUP + } + + if n.replaceable { + // create a template event that can later be modified and published as new + return n.root.CreateEntityDir(n, &nostr.Event{ + PubKey: n.root.rootPubKey, + CreatedAt: 0, + Kind: n.filter.Kinds[0], + Tags: nostr.Tags{ + nostr.Tag{"d", name}, + }, + }), fs.OK + } + + return nil, syscall.ENOTSUP +} diff --git a/nostrfs/writeablefile.go b/nostrfs/writeablefile.go index 2c897fb..c37291a 100644 --- a/nostrfs/writeablefile.go +++ b/nostrfs/writeablefile.go @@ -48,16 +48,18 @@ func (f *WriteableFile) Open(ctx context.Context, flags uint32) (fh fs.FileHandl func (f *WriteableFile) Write(ctx context.Context, fh fs.FileHandle, data []byte, off int64) (uint32, syscall.Errno) { f.mu.Lock() defer f.mu.Unlock() - end := int64(len(data)) + off - if int64(len(f.data)) < end { - n := make([]byte, end) - copy(n, f.data) - f.data = n + + offset := int(off) + end := offset + len(data) + if len(f.data) < end { + newData := make([]byte, offset+len(data)) + copy(newData, f.data) + f.data = newData } - copy(f.data[off:off+int64(len(data))], data) + copy(f.data[offset:], data) + f.data = f.data[0:end] f.onWrite(string(f.data)) - return uint32(len(data)), fs.OK } @@ -69,7 +71,7 @@ func (f *WriteableFile) Getattr(ctx context.Context, fh fs.FileHandle, out *fuse return fs.OK } -func (f *WriteableFile) Setattr(ctx context.Context, fh fs.FileHandle, in *fuse.SetAttrIn, out *fuse.AttrOut) syscall.Errno { +func (f *WriteableFile) Setattr(_ context.Context, _ fs.FileHandle, _ *fuse.SetAttrIn, _ *fuse.AttrOut) syscall.Errno { return fs.OK } From 4b15cdf6256503b5a253e66f13948808c74b678f Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sat, 15 Mar 2025 00:33:14 -0300 Subject: [PATCH 237/401] fs: publishing new notes by writing to ./notes/new --- fs.go | 24 +++++- go.mod | 4 +- go.sum | 2 + nostrfs/entitydir.go | 12 +-- nostrfs/npubdir.go | 1 + nostrfs/root.go | 3 +- nostrfs/viewdir.go | 178 +++++++++++++++++++++++++++++++++++++++ nostrfs/writeablefile.go | 5 +- 8 files changed, 214 insertions(+), 15 deletions(-) diff --git a/fs.go b/fs.go index 1d8d048..6750a93 100644 --- a/fs.go +++ b/fs.go @@ -34,9 +34,15 @@ var fsCmd = &cli.Command{ }, }, &cli.DurationFlag{ - Name: "auto-publish", - Usage: "delay after which edited articles will be auto-published.", - Value: time.Hour * 24 * 365 * 2, + Name: "auto-publish-notes", + Usage: "delay after which new notes will be auto-published, set to -1 to not publish.", + Value: time.Second * 30, + }, + &cli.DurationFlag{ + Name: "auto-publish-articles", + Usage: "delay after which edited articles will be auto-published.", + Value: time.Hour * 24 * 365 * 2, + DefaultText: "basically infinite", }, ), DisableSliceFlagSeparator: true, @@ -53,6 +59,15 @@ var fsCmd = &cli.Command{ kr = keyer.NewReadOnlyUser(c.String("pubkey")) } + apnt := c.Duration("auto-publish-notes") + if apnt < 0 { + apnt = time.Hour * 24 * 365 * 3 + } + apat := c.Duration("auto-publish-articles") + if apat < 0 { + apat = time.Hour * 24 * 365 * 3 + } + root := nostrfs.NewNostrRoot( context.WithValue( context.WithValue( @@ -65,7 +80,8 @@ var fsCmd = &cli.Command{ kr, mountpoint, nostrfs.Options{ - AutoPublishTimeout: c.Duration("auto-publish"), + AutoPublishNotesTimeout: apnt, + AutoPublishArticlesTimeout: apat, }, ) diff --git a/go.mod b/go.mod index 10cdd41..7a5cb32 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,7 @@ require ( 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.3-0.20250312034958-cc23d81e8055 + github.com/nbd-wtf/go-nostr v0.51.5 github.com/urfave/cli/v3 v3.0.0-beta1 golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 ) @@ -75,5 +75,3 @@ require ( golang.org/x/sys v0.31.0 // indirect golang.org/x/text v0.23.0 // indirect ) - -replace github.com/nbd-wtf/go-nostr => ../go-nostr diff --git a/go.sum b/go.sum index 2885144..c39a094 100644 --- a/go.sum +++ b/go.sum @@ -151,6 +151,8 @@ 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.5 h1:kztpm/JuavVefyuEjG0QaCgDtzHIW9K/Hzq+y9Ph2DY= +github.com/nbd-wtf/go-nostr v0.51.5/go.mod h1:raIUNOilCdhiVIqgwe+9enCtdXu1iuPjbLh1hO7wTqI= 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= diff --git a/nostrfs/entitydir.go b/nostrfs/entitydir.go index bfdd275..852a53f 100644 --- a/nostrfs/entitydir.go +++ b/nostrfs/entitydir.go @@ -269,17 +269,17 @@ func (e *EntityDir) handleWrite() { log := e.root.ctx.Value("log").(func(msg string, args ...any)) logverbose := e.root.ctx.Value("logverbose").(func(msg string, args ...any)) - if e.root.opts.AutoPublishTimeout.Hours() < 24*365 { + if e.root.opts.AutoPublishArticlesTimeout.Hours() < 24*365 { if e.publisher.IsRunning() { log(", timer reset") } - log(", will publish the ") + log(", publishing the ") if e.IsNew() { log("new") } else { log("updated") } - log(" event in %d seconds...\n", e.root.opts.AutoPublishTimeout.Seconds()) + log(" event in %d seconds...\n", int(e.root.opts.AutoPublishArticlesTimeout.Seconds())) } else { log(".\n") } @@ -339,9 +339,9 @@ func (e *EntityDir) handleWrite() { } logverbose("%s\n", evt) - relays := e.root.sys.FetchWriteRelays(e.root.ctx, evt.PubKey, 8) + relays := e.root.sys.FetchWriteRelays(e.root.ctx, e.root.rootPubKey, 8) if len(relays) == 0 { - relays = e.root.sys.FetchOutboxRelays(e.root.ctx, evt.PubKey, 6) + relays = e.root.sys.FetchOutboxRelays(e.root.ctx, e.root.rootPubKey, 6) } log("publishing to %d relays... ", len(relays)) @@ -398,7 +398,7 @@ func (r *NostrRoot) CreateEntityDir( ) *fs.Inode { return parent.EmbeddedInode().NewPersistentInode( r.ctx, - &EntityDir{root: r, event: event, publisher: debouncer.New(r.opts.AutoPublishTimeout)}, + &EntityDir{root: r, event: event, publisher: debouncer.New(r.opts.AutoPublishArticlesTimeout)}, fs.StableAttr{Mode: syscall.S_IFDIR}, ) } diff --git a/nostrfs/npubdir.go b/nostrfs/npubdir.go index b721002..d34a226 100644 --- a/nostrfs/npubdir.go +++ b/nostrfs/npubdir.go @@ -122,6 +122,7 @@ func (h *NpubDir) OnAdd(_ context.Context) { paginate: true, relays: relays, replaceable: false, + createable: true, }, fs.StableAttr{Mode: syscall.S_IFDIR}, ), diff --git a/nostrfs/root.go b/nostrfs/root.go index 510969c..d821bc8 100644 --- a/nostrfs/root.go +++ b/nostrfs/root.go @@ -15,7 +15,8 @@ import ( ) type Options struct { - AutoPublishTimeout time.Duration // a negative number means do not publish + AutoPublishNotesTimeout time.Duration + AutoPublishArticlesTimeout time.Duration } type NostrRoot struct { diff --git a/nostrfs/viewdir.go b/nostrfs/viewdir.go index 838ab05..b861290 100644 --- a/nostrfs/viewdir.go +++ b/nostrfs/viewdir.go @@ -2,12 +2,17 @@ package nostrfs import ( "context" + "slices" + "strings" "sync/atomic" "syscall" + "fiatjaf.com/lib/debouncer" + "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 { @@ -19,14 +24,187 @@ type ViewDir struct { relays []string replaceable bool createable bool + publisher *debouncer.Debouncer + publishing struct { + note string + } } var ( _ = (fs.NodeOpendirer)((*ViewDir)(nil)) _ = (fs.NodeGetattrer)((*ViewDir)(nil)) _ = (fs.NodeMkdirer)((*ViewDir)(nil)) + _ = (fs.NodeSetattrer)((*ViewDir)(nil)) + _ = (fs.NodeCreater)((*ViewDir)(nil)) + _ = (fs.NodeUnlinker)((*ViewDir)(nil)) ) +func (f *ViewDir) Setattr(_ context.Context, _ fs.FileHandle, _ *fuse.SetAttrIn, _ *fuse.AttrOut) syscall.Errno { + return fs.OK +} + +func (n *ViewDir) Create( + _ context.Context, + name string, + flags uint32, + mode uint32, + out *fuse.EntryOut, +) (node *fs.Inode, fh fs.FileHandle, fuseFlags uint32, errno syscall.Errno) { + if !n.createable || n.root.rootPubKey != n.filter.Authors[0] { + return nil, nil, 0, syscall.EPERM + } + if n.publisher == nil { + n.publisher = debouncer.New(n.root.opts.AutoPublishNotesTimeout) + } + if n.filter.Kinds[0] != 1 { + return nil, nil, 0, syscall.ENOTSUP + } + + switch name { + case "new": + log := n.root.ctx.Value("log").(func(msg string, args ...any)) + + if n.publisher.IsRunning() { + log("pending note updated, timer reset.") + } else { + log("new note detected") + if n.root.opts.AutoPublishNotesTimeout.Hours() < 24*365 { + log(", publishing it in %d seconds...\n", int(n.root.opts.AutoPublishNotesTimeout.Seconds())) + } else { + log(".\n") + } + log("- `touch publish` to publish immediately\n") + log("- `rm new` to erase and cancel the publication.\n") + } + + n.publisher.Call(n.publishNote) + + first := true + + return n.NewPersistentInode( + n.root.ctx, + n.root.NewWriteableFile(n.publishing.note, uint64(nostr.Now()), uint64(nostr.Now()), func(s string) { + if !first { + log("pending note updated, timer reset.\n") + } + first = false + n.publishing.note = strings.TrimSpace(s) + n.publisher.Call(n.publishNote) + }), + fs.StableAttr{}, + ), nil, 0, fs.OK + case "publish": + if n.publisher.IsRunning() { + // this causes the publish process to be triggered faster + log := n.root.ctx.Value("log").(func(msg string, args ...any)) + log("publishing now!\n") + n.publisher.Flush() + return nil, nil, 0, syscall.ENOTDIR + } + } + + return nil, nil, 0, syscall.ENOTSUP +} + +func (n *ViewDir) Unlink(ctx context.Context, name string) syscall.Errno { + if !n.createable || n.root.rootPubKey != n.filter.Authors[0] { + return syscall.EPERM + } + if n.publisher == nil { + n.publisher = debouncer.New(n.root.opts.AutoPublishNotesTimeout) + } + if n.filter.Kinds[0] != 1 { + return syscall.ENOTSUP + } + + switch name { + case "new": + log := n.root.ctx.Value("log").(func(msg string, args ...any)) + log("publishing canceled.\n") + n.publisher.Stop() + n.publishing.note = "" + return fs.OK + } + + return syscall.ENOTSUP +} + +func (n *ViewDir) publishNote() { + log := n.root.ctx.Value("log").(func(msg string, args ...any)) + + log("publishing note...\n") + evt := nostr.Event{ + Kind: 1, + CreatedAt: nostr.Now(), + Content: n.publishing.note, + Tags: make(nostr.Tags, 0, 2), + } + + // our write relays + relays := n.root.sys.FetchWriteRelays(n.root.ctx, n.root.rootPubKey, 8) + 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) { + tag := ref.Pointer.AsTag() + key := tag[0] + val := tag[1] + if key == "e" || key == "a" { + key = "q" + } + if existing := evt.Tags.FindWithValue(key, val); existing == nil { + evt.Tags = append(evt.Tags, tag) + + // add their "read" relays + if key == "p" { + for _, r := range n.root.sys.FetchInboxRelays(n.root.ctx, val, 4) { + if !slices.Contains(relays, r) { + relays = append(relays, r) + } + } + } + } + } + + // sign and publish + if err := n.root.signer.SignEvent(n.root.ctx, &evt); err != nil { + log("failed to sign: %s\n", err) + return + } + log(evt.String() + "\n") + + log("publishing to %d relays... ", len(relays)) + success := false + first := true + for res := range n.root.sys.Pool.PublishMany(n.root.ctx, relays, evt) { + cleanUrl, ok := strings.CutPrefix(res.RelayURL, "wss://") + if !ok { + cleanUrl = res.RelayURL + } + + if !first { + log(", ") + } + first = false + + if res.Error != nil { + log("%s: %s", color.RedString(cleanUrl), res.Error) + } else { + success = true + log("%s: ok", color.GreenString(cleanUrl)) + } + } + log("\n") + + 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)) + } +} + func (n *ViewDir) Getattr(_ context.Context, f fs.FileHandle, out *fuse.AttrOut) syscall.Errno { now := nostr.Now() if n.filter.Until != nil { diff --git a/nostrfs/writeablefile.go b/nostrfs/writeablefile.go index c37291a..b6ca0a9 100644 --- a/nostrfs/writeablefile.go +++ b/nostrfs/writeablefile.go @@ -71,7 +71,10 @@ func (f *WriteableFile) Getattr(ctx context.Context, fh fs.FileHandle, out *fuse return fs.OK } -func (f *WriteableFile) Setattr(_ context.Context, _ fs.FileHandle, _ *fuse.SetAttrIn, _ *fuse.AttrOut) syscall.Errno { +func (f *WriteableFile) Setattr(_ context.Context, _ fs.FileHandle, in *fuse.SetAttrIn, _ *fuse.AttrOut) syscall.Errno { + f.attr.Mtime = in.Mtime + f.attr.Atime = in.Atime + f.attr.Ctime = in.Ctime return fs.OK } From db5dafb58aac5d77a80d42f3b9aca327972bab0f Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Wed, 19 Mar 2025 15:05:27 -0300 Subject: [PATCH 238/401] fix arm64 builds by removing base64x dependencies. --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 7a5cb32..3ad5114 100644 --- a/go.mod +++ b/go.mod @@ -10,14 +10,14 @@ require ( 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.3-0.20250312035319-596bca93c3ff + 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.5 + github.com/nbd-wtf/go-nostr v0.51.6 github.com/urfave/cli/v3 v3.0.0-beta1 golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 ) diff --git a/go.sum b/go.sum index c39a094..bbf7d1d 100644 --- a/go.sum +++ b/go.sum @@ -79,8 +79,8 @@ 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.3-0.20250312035319-596bca93c3ff h1:b6LYwWlc8zAW6aoZpXYC3Gx/zkP4XW5amDx0VwyeREs= -github.com/fiatjaf/khatru v0.17.3-0.20250312035319-596bca93c3ff/go.mod h1:dAaXV6QZwuMVYlXQigp/0Uyl/m1nKOhtRssjQYsgMu0= +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= @@ -151,8 +151,8 @@ 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.5 h1:kztpm/JuavVefyuEjG0QaCgDtzHIW9K/Hzq+y9Ph2DY= -github.com/nbd-wtf/go-nostr v0.51.5/go.mod h1:raIUNOilCdhiVIqgwe+9enCtdXu1iuPjbLh1hO7wTqI= +github.com/nbd-wtf/go-nostr v0.51.6 h1:H51l39mp4dJztvtxjTNfNqNIxQyMoJMLSKt+1aGq3UU= +github.com/nbd-wtf/go-nostr v0.51.6/go.mod h1:so45r53GkZq+8vkdIW2jBlKEjdHyxEMrl/1g9tEoSFQ= 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= From b1a03800e611c1b52794fcb6768d5b04bd9494b2 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Wed, 19 Mar 2025 15:10:17 -0300 Subject: [PATCH 239/401] add fake `fs` command that doesn't work when compiling for windows but at least compiles. --- fs.go | 2 ++ fs_windows.go | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 fs_windows.go diff --git a/fs.go b/fs.go index 6750a93..4d4b886 100644 --- a/fs.go +++ b/fs.go @@ -1,3 +1,5 @@ +//go:build !windows + package main import ( diff --git a/fs_windows.go b/fs_windows.go new file mode 100644 index 0000000..60f98d6 --- /dev/null +++ b/fs_windows.go @@ -0,0 +1,20 @@ +//go:build windows + +package main + +import ( + "context" + "fmt" + + "github.com/urfave/cli/v3" +) + +var fsCmd = &cli.Command{ + Name: "fs", + Usage: "mount a FUSE filesystem that exposes Nostr events as files.", + Description: `doesn't work on Windows.`, + DisableSliceFlagSeparator: true, + Action: func(ctx context.Context, c *cli.Command) error { + return fmt.Errorf("this doesn't work on Windows.") + }, +} From 7b6f387aad4424001891beded6488592af0b8cf9 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sat, 29 Mar 2025 17:12:31 -0300 Subject: [PATCH 240/401] tags GetFirst() => Find() --- helpers.go | 4 ++-- req.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/helpers.go b/helpers.go index d0c8476..70b8789 100644 --- a/helpers.go +++ b/helpers.go @@ -176,8 +176,8 @@ relayLoop: // beginhack // here starts the biggest and ugliest hack of this codebase if err := relay.Auth(ctx, func(authEvent *nostr.Event) error { - challengeTag := authEvent.Tags.GetFirst([]string{"challenge", ""}) - if (*challengeTag)[1] == "" { + challengeTag := authEvent.Tags.Find("challenge") + if challengeTag[1] == "" { return fmt.Errorf("auth not received yet *****") } return signer(ctx, nostr.RelayEvent{Event: authEvent, Relay: relay}) diff --git a/req.go b/req.go index 3d3e799..854f1d7 100644 --- a/req.go +++ b/req.go @@ -84,7 +84,7 @@ example: defer func() { if err != nil { log("auth to %s failed: %s\n", - (*authEvent.Tags.GetFirst([]string{"relay", ""}))[1], + authEvent.Tags.Find("relay")[1], err, ) } From 33f4272dd0f0bffa3878ef603ab31f3d7c773ecb Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Wed, 2 Apr 2025 22:37:59 -0300 Subject: [PATCH 241/401] update go-nostr to maybe fix nip-60 wallets? --- go.mod | 9 +++++---- go.sum | 16 ++++++++++++---- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/go.mod b/go.mod index 3ad5114..81cc8a0 100644 --- a/go.mod +++ b/go.mod @@ -17,24 +17,25 @@ require ( 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.6 + 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 ) 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 github.com/btcsuite/btcd/btcutil v1.1.5 // indirect github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect - github.com/bytedance/sonic v1.13.1 // indirect + github.com/bytedance/sonic v1.13.2 // indirect github.com/bytedance/sonic/loader v0.2.4 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chzyer/logex v1.1.10 // indirect github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect github.com/cloudwego/base64x v0.1.5 // indirect - github.com/coder/websocket v1.8.12 // indirect + github.com/coder/websocket v1.8.13 // indirect github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect github.com/dgraph-io/ristretto v1.0.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect @@ -43,7 +44,6 @@ require ( github.com/fasthttp/websocket v1.5.12 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/graph-gophers/dataloader/v7 v7.1.0 // indirect github.com/hablullah/go-hijri v1.0.2 // indirect github.com/hablullah/go-juliandays v1.0.0 // indirect github.com/jalaali/go-jalaali v0.0.0-20210801064154-80525e88d958 // indirect @@ -72,6 +72,7 @@ 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 ) diff --git a/go.sum b/go.sum index bbf7d1d..3c2ec46 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ 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/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= @@ -35,9 +37,13 @@ github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtE github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= github.com/bytedance/sonic v1.13.1 h1:Jyd5CIvdFnkOWuKXr+wm4Nyk2h0yAFsr8ucJgEasO3g= github.com/bytedance/sonic v1.13.1/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= +github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ= +github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= 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= @@ -51,6 +57,8 @@ github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJ github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo= github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= +github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE= +github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -102,8 +110,6 @@ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/graph-gophers/dataloader/v7 v7.1.0 h1:Wn8HGF/q7MNXcvfaBnLEPEFJttVHR8zuEqP1obys/oc= -github.com/graph-gophers/dataloader/v7 v7.1.0/go.mod h1:1bKE0Dm6OUcTB/OAuYVOZctgIz7Q3d0XrYtlIzTgg6Q= github.com/hablullah/go-hijri v1.0.2 h1:drT/MZpSZJQXo7jftf5fthArShcaMtsal0Zf/dnmp6k= github.com/hablullah/go-hijri v1.0.2/go.mod h1:OS5qyYLDjORXzK4O1adFw9Q5WfhOcMdAKglDkcTxgWQ= github.com/hablullah/go-juliandays v1.0.0 h1:A8YM7wIj16SzlKT0SRJc9CD29iiaUzpBLzh5hr0/5p0= @@ -151,8 +157,8 @@ 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.6 h1:H51l39mp4dJztvtxjTNfNqNIxQyMoJMLSKt+1aGq3UU= -github.com/nbd-wtf/go-nostr v0.51.6/go.mod h1:so45r53GkZq+8vkdIW2jBlKEjdHyxEMrl/1g9tEoSFQ= +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= @@ -226,6 +232,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= From 50119e21e6a1f1a69ccb4c8e75835336cdc1496f Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Wed, 2 Apr 2025 22:38:12 -0300 Subject: [PATCH 242/401] fix nip73.ExternalPointer reference --- nostrfs/eventdir.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/nostrfs/eventdir.go b/nostrfs/eventdir.go index ad7baee..ac99196 100644 --- a/nostrfs/eventdir.go +++ b/nostrfs/eventdir.go @@ -18,6 +18,7 @@ import ( "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" ) @@ -191,7 +192,7 @@ func (r *NostrRoot) CreateEventDir( } } else if event.Kind == 1111 { if pointer := nip22.GetThreadRoot(event.Tags); pointer != nil { - if xp, ok := pointer.(nostr.ExternalPointer); ok { + if xp, ok := pointer.(nip73.ExternalPointer); ok { h.AddChild("@root", h.NewPersistentInode( r.ctx, &fs.MemRegularFile{ @@ -211,7 +212,7 @@ func (r *NostrRoot) CreateEventDir( } } if pointer := nip22.GetImmediateParent(event.Tags); pointer != nil { - if xp, ok := pointer.(nostr.ExternalPointer); ok { + if xp, ok := pointer.(nip73.ExternalPointer); ok { h.AddChild("@parent", h.NewPersistentInode( r.ctx, &fs.MemRegularFile{ From 9547711e8df33d3f4a4c1998870cb9e2312c1a85 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Thu, 3 Apr 2025 11:42:33 -0300 Subject: [PATCH 243/401] nice dynamic UI when connecting to relays, and go much faster concurrently. --- .gitignore | 1 + bunker.go | 4 +- count.go | 7 +-- dvm.go | 2 +- event.go | 14 +++-- go.mod | 1 + go.sum | 2 + helpers.go | 182 +++++++++++++++++++++++++++++++++++++++++------------ req.go | 65 +++++++++++-------- 9 files changed, 199 insertions(+), 79 deletions(-) diff --git a/.gitignore b/.gitignore index 9f82d74..5f39b84 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ nak mnt +nak.exe diff --git a/bunker.go b/bunker.go index a1eedba..0787e18 100644 --- a/bunker.go +++ b/bunker.go @@ -11,10 +11,10 @@ import ( "time" "github.com/fatih/color" - "github.com/urfave/cli/v3" "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" ) var bunker = &cli.Command{ @@ -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, relayUrls, false) + relays := connectToAllRelays(ctx, relayUrls, nil) if len(relays) == 0 { log("failed to connect to any of the given relays.\n") os.Exit(3) diff --git a/count.go b/count.go index 4599c0b..26157b0 100644 --- a/count.go +++ b/count.go @@ -6,10 +6,10 @@ import ( "os" "strings" - "github.com/urfave/cli/v3" "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/nip45" "github.com/nbd-wtf/go-nostr/nip45/hyperloglog" + "github.com/urfave/cli/v3" ) var count = &cli.Command{ @@ -70,10 +70,7 @@ var count = &cli.Command{ biggerUrlSize := 0 relayUrls := c.Args().Slice() if len(relayUrls) > 0 { - relays := connectToAllRelays(ctx, - relayUrls, - false, - ) + relays := connectToAllRelays(ctx, relayUrls, nil) if len(relays) == 0 { log("failed to connect to any of the given relays.\n") os.Exit(3) diff --git a/dvm.go b/dvm.go index f4142fc..88ed9cc 100644 --- a/dvm.go +++ b/dvm.go @@ -60,7 +60,7 @@ var dvm = &cli.Command{ Flags: flags, Action: func(ctx context.Context, c *cli.Command) error { relayUrls := c.StringSlice("relay") - relays := connectToAllRelays(ctx, relayUrls, false) + relays := connectToAllRelays(ctx, relayUrls, nil) if len(relays) == 0 { log("failed to connect to any of the given relays.\n") os.Exit(3) diff --git a/event.go b/event.go index dbc2a7c..5bae947 100644 --- a/event.go +++ b/event.go @@ -134,7 +134,7 @@ example: // try to connect to the relays here var relays []*nostr.Relay if relayUrls := c.Args().Slice(); len(relayUrls) > 0 { - relays = connectToAllRelays(ctx, relayUrls, false) + relays = connectToAllRelays(ctx, relayUrls, nil) if len(relays) == 0 { log("failed to connect to any of the given relays.\n") os.Exit(3) @@ -209,13 +209,19 @@ example: } for _, etag := range c.StringSlice("e") { - tags = tags.AppendUnique([]string{"e", etag}) + if tags.FindWithValue("e", etag) == nil { + tags = append(tags, nostr.Tag{"e", etag}) + } } for _, ptag := range c.StringSlice("p") { - tags = tags.AppendUnique([]string{"p", ptag}) + if tags.FindWithValue("p", ptag) == nil { + tags = append(tags, nostr.Tag{"p", ptag}) + } } for _, dtag := range c.StringSlice("d") { - tags = tags.AppendUnique([]string{"d", dtag}) + if tags.FindWithValue("d", dtag) == nil { + tags = append(tags, nostr.Tag{"d", dtag}) + } } if len(tags) > 0 { for _, tag := range tags { diff --git a/go.mod b/go.mod index 81cc8a0..4b7b3a6 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,7 @@ require ( 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 ( diff --git a/go.sum b/go.sum index 3c2ec46..9a3a22d 100644 --- a/go.sum +++ b/go.sum @@ -247,6 +247,8 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= +golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= diff --git a/helpers.go b/helpers.go index 70b8789..1e4a0db 100644 --- a/helpers.go +++ b/helpers.go @@ -10,8 +10,10 @@ import ( "net/textproto" "net/url" "os" + "runtime" "slices" "strings" + "sync" "time" "github.com/fatih/color" @@ -19,6 +21,7 @@ import ( "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/sdk" "github.com/urfave/cli/v3" + "golang.org/x/term" ) var sys *sdk.System @@ -149,7 +152,7 @@ func normalizeAndValidateRelayURLs(wsurls []string) error { func connectToAllRelays( ctx context.Context, relayUrls []string, - forcePreAuth bool, + preAuthSigner func(ctx context.Context, log func(s string, args ...any), authEvent nostr.RelayEvent) (err error), // if this exists we will force preauth opts ...nostr.PoolOption, ) []*nostr.Relay { sys.Pool = nostr.NewSimplePool(context.Background(), @@ -163,52 +166,149 @@ func connectToAllRelays( ) relays := make([]*nostr.Relay, 0, len(relayUrls)) -relayLoop: - for _, url := range relayUrls { - log("connecting to %s... ", url) - if relay, err := sys.Pool.EnsureRelay(url); err == nil { - if forcePreAuth { - log("waiting for auth challenge... ") - signer := opts[0].(nostr.WithAuthHandler) - time.Sleep(time.Millisecond * 200) - challengeWaitLoop: - for { - // beginhack - // here starts the biggest and ugliest hack of this codebase - if err := relay.Auth(ctx, func(authEvent *nostr.Event) error { - challengeTag := authEvent.Tags.Find("challenge") - if challengeTag[1] == "" { - return fmt.Errorf("auth not received yet *****") - } - return signer(ctx, nostr.RelayEvent{Event: authEvent, Relay: relay}) - }); err == nil { - // auth succeeded - break challengeWaitLoop - } else { - // auth failed - if strings.HasSuffix(err.Error(), "auth not received yet *****") { - // it failed because we didn't receive the challenge yet, so keep waiting - time.Sleep(time.Second) - continue challengeWaitLoop - } else { - // it failed for some other reason, so skip this relay - log(err.Error() + "\n") - continue relayLoop - } - } - // endhack - } - } - relays = append(relays, relay) - log("ok.\n") - } else { - log(err.Error() + "\n") + if supportsDynamicMultilineMagic() { + // overcomplicated multiline rendering magic + lines := make([][][]byte, len(relayUrls)) + flush := func() { + for _, line := range lines { + for _, part := range line { + os.Stderr.Write(part) + } + os.Stderr.Write([]byte{'\n'}) + } + } + render := func() { + clearLines(len(lines)) + flush() + } + flush() + + wg := sync.WaitGroup{} + wg.Add(len(relayUrls)) + for i, url := range relayUrls { + lines[i] = make([][]byte, 1, 2) + logthis := func(s string, args ...any) { + lines[i] = append(lines[i], []byte(fmt.Sprintf(s, args...))) + render() + } + colorizepreamble := func(c func(string, ...any) string) { + lines[i][0] = []byte(fmt.Sprintf("%s... ", c(url))) + } + colorizepreamble(color.CyanString) + + go func() { + relay := connectToSingleRelay(ctx, url, preAuthSigner, colorizepreamble, logthis) + if relay != nil { + relays = append(relays, relay) + } + wg.Done() + }() + } + wg.Wait() + } else { + // simple flow + for _, url := range relayUrls { + log("connecting to %s... ", url) + relay := connectToSingleRelay(ctx, url, preAuthSigner, nil, log) + if relay != nil { + relays = append(relays, relay) + } + log("\n") } } + return relays } +func connectToSingleRelay( + ctx context.Context, + url string, + preAuthSigner func(ctx context.Context, log func(s string, args ...any), authEvent nostr.RelayEvent) (err error), + colorizepreamble func(c func(string, ...any) string), + logthis func(s string, args ...any), +) *nostr.Relay { + if relay, err := sys.Pool.EnsureRelay(url); err == nil { + if preAuthSigner != nil { + if colorizepreamble != nil { + colorizepreamble(color.YellowString) + } + logthis("waiting for auth challenge... ") + time.Sleep(time.Millisecond * 200) + + for range 5 { + if err := relay.Auth(ctx, func(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, logthis, nostr.RelayEvent{Event: authEvent, Relay: relay}) + }); err == nil { + // auth succeeded + goto preauthSuccess + } else { + // auth failed + if strings.HasSuffix(err.Error(), "auth not received yet *****") { + // it failed because we didn't receive the challenge yet, so keep waiting + time.Sleep(time.Second) + continue + } else { + // it failed for some other reason, so skip this relay + if colorizepreamble != nil { + colorizepreamble(color.RedString) + } + logthis(err.Error()) + return nil + } + } + } + if colorizepreamble != nil { + colorizepreamble(color.RedString) + } + logthis("failed to get an AUTH challenge in enough time.") + return nil + } + + preauthSuccess: + if colorizepreamble != nil { + colorizepreamble(color.GreenString) + } + logthis("ok.") + return relay + } else { + if colorizepreamble != nil { + colorizepreamble(color.RedString) + } + logthis(err.Error()) + return nil + } +} + +func clearLines(lineCount int) { + for i := 0; i < lineCount; i++ { + os.Stderr.Write([]byte("\033[0A\033[2K\r")) + } +} + +func supportsDynamicMultilineMagic() bool { + if runtime.GOOS == "windows" { + return false + } + if !term.IsTerminal(0) { + return false + } + + width, _, err := term.GetSize(0) + if err != nil { + return false + } + if width < 110 { + return false + } + + return true +} + func lineProcessingError(ctx context.Context, msg string, args ...any) context.Context { log(msg+"\n", args...) return context.WithValue(ctx, LINE_PROCESSING_ERROR, true) diff --git a/req.go b/req.go index 854f1d7..559088e 100644 --- a/req.go +++ b/req.go @@ -8,6 +8,7 @@ import ( "github.com/mailru/easyjson" "github.com/nbd-wtf/go-nostr" + "github.com/nbd-wtf/go-nostr/nip19" "github.com/nbd-wtf/go-nostr/nip77" "github.com/urfave/cli/v3" ) @@ -76,35 +77,46 @@ example: Action: func(ctx context.Context, c *cli.Command) error { relayUrls := c.Args().Slice() if len(relayUrls) > 0 { + // this is used both for the normal AUTH (after "auth-required:" is received) or forced pre-auth + authSigner := func(ctx context.Context, log func(s string, args ...any), authEvent nostr.RelayEvent) (err error) { + defer func() { + if err != nil { + log("auth to %s failed: %s", + authEvent.Tags.Find("relay")[1], + err, + ) + } + }() + + if !c.Bool("auth") && !c.Bool("force-pre-auth") { + return fmt.Errorf("auth not authorized") + } + kr, _, err := gatherKeyerFromArguments(ctx, c) + if err != nil { + return err + } + + pk, _ := kr.GetPublicKey(ctx) + npub, _ := nip19.EncodePublicKey(pk) + log("performing auth as %s…%s... ", npub[0:7], npub[58:]) + + return kr.SignEvent(ctx, authEvent.Event) + } + + // connect to all relays we expect to use in this call in parallel + forcePreAuthSigner := authSigner + if !c.Bool("force-pre-auth") { + forcePreAuthSigner = nil + } relays := connectToAllRelays(ctx, relayUrls, - c.Bool("force-pre-auth"), - nostr.WithAuthHandler( - func(ctx context.Context, authEvent nostr.RelayEvent) (err error) { - defer func() { - if err != nil { - log("auth to %s failed: %s\n", - authEvent.Tags.Find("relay")[1], - err, - ) - } - }() - - if !c.Bool("auth") && !c.Bool("force-pre-auth") { - return fmt.Errorf("auth not authorized") - } - kr, _, err := gatherKeyerFromArguments(ctx, c) - if err != nil { - return err - } - - pk, _ := kr.GetPublicKey(ctx) - log("performing auth as %s... ", pk) - - return kr.SignEvent(ctx, authEvent.Event) - }, - ), + forcePreAuthSigner, + nostr.WithAuthHandler(func(ctx context.Context, authEvent nostr.RelayEvent) error { + return authSigner(ctx, func(s string, args ...any) { log(s+"\n", args...) }, authEvent) + }), ) + + // stop here already if all connections failed if len(relays) == 0 { log("failed to connect to any of the given relays.\n") os.Exit(3) @@ -121,6 +133,7 @@ example: }() } + // go line by line from stdin or run once with input from flags for stdinFilter := range getJsonsOrBlank() { filter := nostr.Filter{} if stdinFilter != "" { From 7ae2e686cbeea559f92b0a170e2824f2c033fe22 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Thu, 3 Apr 2025 11:57:18 -0300 Subject: [PATCH 244/401] more colors. --- helpers.go | 2 +- req.go | 15 +++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/helpers.go b/helpers.go index 1e4a0db..184136b 100644 --- a/helpers.go +++ b/helpers.go @@ -209,7 +209,7 @@ func connectToAllRelays( } else { // simple flow for _, url := range relayUrls { - log("connecting to %s... ", url) + log("connecting to %s... ", color.CyanString(url)) relay := connectToSingleRelay(ctx, url, preAuthSigner, nil, log) if relay != nil { relays = append(relays, relay) diff --git a/req.go b/req.go index 559088e..a2b8d92 100644 --- a/req.go +++ b/req.go @@ -6,6 +6,7 @@ import ( "os" "strings" + "github.com/fatih/color" "github.com/mailru/easyjson" "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/nip19" @@ -81,7 +82,7 @@ example: authSigner := func(ctx context.Context, log func(s string, args ...any), authEvent nostr.RelayEvent) (err error) { defer func() { if err != nil { - log("auth to %s failed: %s", + log("%s failed: %s", authEvent.Tags.Find("relay")[1], err, ) @@ -89,7 +90,7 @@ example: }() if !c.Bool("auth") && !c.Bool("force-pre-auth") { - return fmt.Errorf("auth not authorized") + return fmt.Errorf("auth required, but --auth flag not given") } kr, _, err := gatherKeyerFromArguments(ctx, c) if err != nil { @@ -98,7 +99,7 @@ example: pk, _ := kr.GetPublicKey(ctx) npub, _ := nip19.EncodePublicKey(pk) - log("performing auth as %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) } @@ -112,7 +113,13 @@ example: relayUrls, forcePreAuthSigner, nostr.WithAuthHandler(func(ctx context.Context, authEvent nostr.RelayEvent) error { - return authSigner(ctx, func(s string, args ...any) { log(s+"\n", args...) }, authEvent) + return authSigner(ctx, func(s string, args ...any) { + if strings.HasPrefix(s, "authenticating as") { + url := authEvent.Tags.Find("relay")[1] + s = "authenticating to " + color.CyanString(url) + " as" + s[len("authenticating as"):] + } + log(s+"\n", args...) + }, authEvent) }), ) From 703c1869582d7cdffce2608fa765108eb0b5c3d8 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Thu, 3 Apr 2025 14:50:25 -0300 Subject: [PATCH 245/401] much more colors everywhere and everything is prettier. --- bunker.go | 2 +- count.go | 2 +- dvm.go | 12 ++-- event.go | 129 ++++++++++++++++++++++++++++++++++--------- go.mod | 2 + go.sum | 6 -- helpers.go | 94 ++++++++++++++++++++++++++----- nostrfs/entitydir.go | 6 +- nostrfs/viewdir.go | 6 +- req.go | 36 ++---------- wallet.go | 30 +++------- 11 files changed, 207 insertions(+), 118 deletions(-) diff --git a/bunker.go b/bunker.go index 0787e18..12e8e32 100644 --- a/bunker.go +++ b/bunker.go @@ -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, relayUrls, nil) + relays := connectToAllRelays(ctx, c, relayUrls, nil) if len(relays) == 0 { log("failed to connect to any of the given relays.\n") os.Exit(3) diff --git a/count.go b/count.go index 26157b0..c01b65e 100644 --- a/count.go +++ b/count.go @@ -70,7 +70,7 @@ var count = &cli.Command{ biggerUrlSize := 0 relayUrls := c.Args().Slice() if len(relayUrls) > 0 { - relays := connectToAllRelays(ctx, relayUrls, nil) + relays := connectToAllRelays(ctx, c, relayUrls, nil) if len(relays) == 0 { log("failed to connect to any of the given relays.\n") os.Exit(3) diff --git a/dvm.go b/dvm.go index 88ed9cc..5e05e39 100644 --- a/dvm.go +++ b/dvm.go @@ -7,7 +7,6 @@ import ( "strconv" "strings" - "github.com/fatih/color" "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/nip90" "github.com/urfave/cli/v3" @@ -60,7 +59,7 @@ var dvm = &cli.Command{ Flags: flags, Action: func(ctx context.Context, c *cli.Command) error { relayUrls := c.StringSlice("relay") - relays := connectToAllRelays(ctx, relayUrls, nil) + relays := connectToAllRelays(ctx, c, relayUrls, nil) if len(relays) == 0 { log("failed to connect to any of the given relays.\n") os.Exit(3) @@ -103,10 +102,7 @@ var dvm = &cli.Command{ log("- publishing job request... ") first := true for res := range sys.Pool.PublishMany(ctx, relayUrls, evt) { - cleanUrl, ok := strings.CutPrefix(res.RelayURL, "wss://") - if !ok { - cleanUrl = res.RelayURL - } + cleanUrl, _ := strings.CutPrefix(res.RelayURL, "wss://") if !first { log(", ") @@ -114,9 +110,9 @@ var dvm = &cli.Command{ first = false if res.Error != nil { - log("%s: %s", color.RedString(cleanUrl), res.Error) + log("%s: %s", colors.errorf(cleanUrl), res.Error) } else { - log("%s: ok", color.GreenString(cleanUrl)) + log("%s: ok", colors.successf(cleanUrl)) } } diff --git a/event.go b/event.go index 5bae947..f4b86bb 100644 --- a/event.go +++ b/event.go @@ -8,6 +8,7 @@ import ( "strings" "time" + "github.com/fatih/color" "github.com/mailru/easyjson" "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/nip13" @@ -133,8 +134,17 @@ example: Action: func(ctx context.Context, c *cli.Command) error { // try to connect to the relays here var relays []*nostr.Relay + + // these are defaults, they will be replaced if we use the magic dynamic thing + logthis := func(relayUrl string, s string, args ...any) { log(s, args...) } + colorizethis := func(relayUrl string, colorize func(string, ...any) string) {} + if relayUrls := c.Args().Slice(); len(relayUrls) > 0 { - relays = connectToAllRelays(ctx, relayUrls, nil) + relays = connectToAllRelays(ctx, c, relayUrls, nil, + nostr.WithAuthHandler(func(ctx context.Context, authEvent nostr.RelayEvent) 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") os.Exit(3) @@ -301,37 +311,104 @@ example: successRelays := make([]string, 0, len(relays)) if len(relays) > 0 { os.Stdout.Sync() - for _, relay := range relays { - publish: - log("publishing to %s... ", relay.URL) + + if supportsDynamicMultilineMagic() { + // overcomplicated multiline rendering magic ctx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() - err := relay.Publish(ctx, evt) - if err == nil { - // published fine - log("success.\n") - successRelays = append(successRelays, relay.URL) - continue // continue to next relay - } - - // error publishing - 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) - log("performing auth as %s... ", pk) - if err := relay.Auth(ctx, func(authEvent *nostr.Event) error { - return kr.SignEvent(ctx, authEvent) - }); err == nil { - // try to publish again, but this time don't try to auth again - doAuth = false - goto publish - } else { - log("auth error: %s. ", err) + urls := make([]string, len(relays)) + lines := make([][][]byte, len(urls)) + flush := func() { + for _, line := range lines { + for _, part := range line { + os.Stderr.Write(part) + } + os.Stderr.Write([]byte{'\n'}) } } - log("failed: %s\n", err) + render := func() { + clearLines(len(lines)) + flush() + } + flush() + + logthis = func(relayUrl, s string, args ...any) { + idx := slices.Index(urls, relayUrl) + lines[idx] = append(lines[idx], []byte(fmt.Sprintf(s, args...))) + render() + } + colorizethis = func(relayUrl string, colorize func(string, ...any) string) { + cleanUrl, _ := strings.CutPrefix(relayUrl, "wss://") + idx := slices.Index(urls, relayUrl) + lines[idx][0] = []byte(fmt.Sprintf("publishing to %s... ", colorize(cleanUrl))) + render() + } + + for i, relay := range relays { + urls[i] = relay.URL + lines[i] = make([][]byte, 1, 3) + colorizethis(relay.URL, color.CyanString) + } + render() + + for res := range sys.Pool.PublishMany(ctx, urls, evt) { + if res.Error == nil { + colorizethis(res.RelayURL, colors.successf) + logthis(res.RelayURL, "success.") + successRelays = append(successRelays, res.RelayURL) + } else { + colorizethis(res.RelayURL, colors.errorf) + + // in this case it's likely that the lowest-level error is the one that will be more helpful + low := unwrapAll(res.Error) + + // hack for some messages such as from relay.westernbtc.com + msg := strings.ReplaceAll(low.Error(), evt.PubKey, "author") + + // do not allow the message to overflow the term window + msg = clampMessage(msg, 20+len(res.RelayURL)) + + logthis(res.RelayURL, msg) + } + } + } else { + // normal dumb flow + for _, relay := range relays { + publish: + cleanUrl, _ := strings.CutPrefix(relay.URL, "wss://") + log("publishing to %s... ", color.CyanString(cleanUrl)) + ctx, cancel := context.WithTimeout(ctx, 10*time.Second) + defer cancel() + + err := relay.Publish(ctx, evt) + if err == nil { + // published fine + log("success.\n") + successRelays = append(successRelays, relay.URL) + continue // continue to next relay + } + + // error publishing + 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) + 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 { + // try to publish again, but this time don't try to auth again + doAuth = false + goto publish + } else { + log("auth error: %s. ", err) + } + } + log("failed: %s\n", err) + } } + if len(successRelays) > 0 && c.Bool("nevent") { nevent, _ := nip19.EncodeEvent(evt.ID, successRelays, evt.PubKey) log(nevent + "\n") diff --git a/go.mod b/go.mod index 4b7b3a6..add0a56 100644 --- a/go.mod +++ b/go.mod @@ -77,3 +77,5 @@ require ( golang.org/x/sys v0.31.0 // indirect golang.org/x/text v0.23.0 // indirect ) + +replace github.com/nbd-wtf/go-nostr => ../go-nostr diff --git a/go.sum b/go.sum index 9a3a22d..af918e9 100644 --- a/go.sum +++ b/go.sum @@ -35,8 +35,6 @@ github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= -github.com/bytedance/sonic v1.13.1 h1:Jyd5CIvdFnkOWuKXr+wm4Nyk2h0yAFsr8ucJgEasO3g= -github.com/bytedance/sonic v1.13.1/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ= github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= @@ -55,8 +53,6 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= -github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo= -github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE= github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -157,8 +153,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= diff --git a/helpers.go b/helpers.go index 184136b..247dd83 100644 --- a/helpers.go +++ b/helpers.go @@ -3,6 +3,7 @@ package main import ( "bufio" "context" + "errors" "fmt" "iter" "math/rand" @@ -19,6 +20,7 @@ import ( "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" @@ -151,8 +153,9 @@ func normalizeAndValidateRelayURLs(wsurls []string) error { func connectToAllRelays( ctx context.Context, + c *cli.Command, relayUrls []string, - preAuthSigner func(ctx context.Context, 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.RelayEvent) (err error), // if this exists we will force preauth opts ...nostr.PoolOption, ) []*nostr.Relay { sys.Pool = nostr.NewSimplePool(context.Background(), @@ -198,7 +201,7 @@ func connectToAllRelays( colorizepreamble(color.CyanString) go func() { - relay := connectToSingleRelay(ctx, url, preAuthSigner, colorizepreamble, logthis) + relay := connectToSingleRelay(ctx, c, url, preAuthSigner, colorizepreamble, logthis) if relay != nil { relays = append(relays, relay) } @@ -210,7 +213,7 @@ func connectToAllRelays( // simple flow for _, url := range relayUrls { log("connecting to %s... ", color.CyanString(url)) - relay := connectToSingleRelay(ctx, url, preAuthSigner, nil, log) + relay := connectToSingleRelay(ctx, c, url, preAuthSigner, nil, log) if relay != nil { relays = append(relays, relay) } @@ -223,8 +226,9 @@ func connectToAllRelays( func connectToSingleRelay( ctx context.Context, + c *cli.Command, url string, - preAuthSigner func(ctx context.Context, 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.RelayEvent) (err error), colorizepreamble func(c func(string, ...any) string), logthis func(s string, args ...any), ) *nostr.Relay { @@ -242,7 +246,7 @@ func connectToSingleRelay( if challengeTag[1] == "" { return fmt.Errorf("auth not received yet *****") // what a giant hack } - return preAuthSigner(ctx, logthis, nostr.RelayEvent{Event: authEvent, Relay: relay}) + return preAuthSigner(ctx, c, logthis, nostr.RelayEvent{Event: authEvent, Relay: relay}) }); err == nil { // auth succeeded goto preauthSuccess @@ -255,7 +259,7 @@ func connectToSingleRelay( } else { // it failed for some other reason, so skip this relay if colorizepreamble != nil { - colorizepreamble(color.RedString) + colorizepreamble(colors.errorf) } logthis(err.Error()) return nil @@ -263,7 +267,7 @@ func connectToSingleRelay( } } if colorizepreamble != nil { - colorizepreamble(color.RedString) + colorizepreamble(colors.errorf) } logthis("failed to get an AUTH challenge in enough time.") return nil @@ -271,15 +275,18 @@ func connectToSingleRelay( preauthSuccess: if colorizepreamble != nil { - colorizepreamble(color.GreenString) + colorizepreamble(colors.successf) } logthis("ok.") return relay } else { if colorizepreamble != nil { - colorizepreamble(color.RedString) + colorizepreamble(colors.errorf) } - logthis(err.Error()) + + // if we're here that means we've failed to connect, this may be a huge message + // but we're likely to only be interested in the lowest level error (although we can leave space) + logthis(clampError(err, len(url)+12)) return nil } } @@ -309,6 +316,29 @@ func supportsDynamicMultilineMagic() bool { return true } +func authSigner(ctx context.Context, c *cli.Command, log func(s string, args ...any), authEvent nostr.RelayEvent) (err error) { + defer func() { + if err != nil { + cleanUrl, _ := strings.CutPrefix(authEvent.Relay.URL, "wss://") + log("%s auth failed: %s", colors.errorf(cleanUrl), err) + } + }() + + if !c.Bool("auth") && !c.Bool("force-pre-auth") { + return fmt.Errorf("auth required, but --auth flag not given") + } + kr, _, err := gatherKeyerFromArguments(ctx, c) + if err != nil { + return err + } + + pk, _ := kr.GetPublicKey(ctx) + npub, _ := nip19.EncodePublicKey(pk) + log("authenticating as %s... ", color.YellowString("%s…%s", npub[0:7], npub[58:])) + + return kr.SignEvent(ctx, authEvent.Event) +} + func lineProcessingError(ctx context.Context, msg string, args ...any) context.Context { log(msg+"\n", args...) return context.WithValue(ctx, LINE_PROCESSING_ERROR, true) @@ -334,16 +364,50 @@ 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) { + low = n + } + return low +} + +func clampMessage(msg string, prefixAlreadyPrinted int) string { + termSize, _, _ := term.GetSize(0) + if len(msg) > termSize-prefixAlreadyPrinted { + msg = msg[0:termSize-prefixAlreadyPrinted-1] + "…" + } + return msg +} + +func clampError(err error, prefixAlreadyPrinted int) string { + termSize, _, _ := term.GetSize(0) + msg := err.Error() + if len(msg) > termSize-prefixAlreadyPrinted { + err = unwrapAll(err) + msg = clampMessage(err.Error(), prefixAlreadyPrinted) + } + return msg +} + var colors = struct { - reset func(...any) (int, error) - italic func(...any) string - italicf func(string, ...any) string - bold func(...any) string - boldf func(string, ...any) string + reset func(...any) (int, error) + italic func(...any) string + italicf func(string, ...any) string + bold func(...any) string + boldf func(string, ...any) string + error func(...any) string + errorf func(string, ...any) string + success func(...any) string + successf func(string, ...any) string }{ color.New(color.Reset).Print, color.New(color.Italic).Sprint, color.New(color.Italic).Sprintf, color.New(color.Bold).Sprint, color.New(color.Bold).Sprintf, + color.New(color.Bold, color.FgHiRed).Sprint, + color.New(color.Bold, color.FgHiRed).Sprintf, + color.New(color.Bold, color.FgHiGreen).Sprint, + color.New(color.Bold, color.FgHiGreen).Sprintf, } diff --git a/nostrfs/entitydir.go b/nostrfs/entitydir.go index 852a53f..ed8bd76 100644 --- a/nostrfs/entitydir.go +++ b/nostrfs/entitydir.go @@ -348,11 +348,7 @@ func (e *EntityDir) handleWrite() { success := false first := true for res := range e.root.sys.Pool.PublishMany(e.root.ctx, relays, evt) { - cleanUrl, ok := strings.CutPrefix(res.RelayURL, "wss://") - if !ok { - cleanUrl = res.RelayURL - } - + cleanUrl, _ := strings.CutPrefix(res.RelayURL, "wss://") if !first { log(", ") } diff --git a/nostrfs/viewdir.go b/nostrfs/viewdir.go index b861290..1dbbd90 100644 --- a/nostrfs/viewdir.go +++ b/nostrfs/viewdir.go @@ -179,11 +179,7 @@ func (n *ViewDir) publishNote() { success := false first := true for res := range n.root.sys.Pool.PublishMany(n.root.ctx, relays, evt) { - cleanUrl, ok := strings.CutPrefix(res.RelayURL, "wss://") - if !ok { - cleanUrl = res.RelayURL - } - + cleanUrl, _ := strings.CutPrefix(res.RelayURL, "wss://") if !first { log(", ") } diff --git a/req.go b/req.go index a2b8d92..3221fdd 100644 --- a/req.go +++ b/req.go @@ -9,7 +9,6 @@ import ( "github.com/fatih/color" "github.com/mailru/easyjson" "github.com/nbd-wtf/go-nostr" - "github.com/nbd-wtf/go-nostr/nip19" "github.com/nbd-wtf/go-nostr/nip77" "github.com/urfave/cli/v3" ) @@ -79,44 +78,21 @@ example: relayUrls := c.Args().Slice() if len(relayUrls) > 0 { // this is used both for the normal AUTH (after "auth-required:" is received) or forced pre-auth - authSigner := func(ctx context.Context, log func(s string, args ...any), authEvent nostr.RelayEvent) (err error) { - defer func() { - if err != nil { - log("%s failed: %s", - authEvent.Tags.Find("relay")[1], - err, - ) - } - }() - - if !c.Bool("auth") && !c.Bool("force-pre-auth") { - return fmt.Errorf("auth required, but --auth flag not given") - } - kr, _, err := gatherKeyerFromArguments(ctx, c) - if err != nil { - return err - } - - pk, _ := kr.GetPublicKey(ctx) - npub, _ := nip19.EncodePublicKey(pk) - log("authenticating as %s... ", color.YellowString("%s…%s", npub[0:7], npub[58:])) - - return kr.SignEvent(ctx, authEvent.Event) - } - // connect to all relays we expect to use in this call in parallel forcePreAuthSigner := authSigner if !c.Bool("force-pre-auth") { forcePreAuthSigner = nil } - relays := connectToAllRelays(ctx, + relays := connectToAllRelays( + ctx, + c, relayUrls, forcePreAuthSigner, nostr.WithAuthHandler(func(ctx context.Context, authEvent nostr.RelayEvent) error { - return authSigner(ctx, func(s string, args ...any) { + return authSigner(ctx, c, func(s string, args ...any) { if strings.HasPrefix(s, "authenticating as") { - url := authEvent.Tags.Find("relay")[1] - s = "authenticating to " + color.CyanString(url) + " as" + s[len("authenticating as"):] + cleanUrl, _ := strings.CutPrefix(authEvent.Relay.URL, "wss://") + s = "authenticating to " + color.CyanString(cleanUrl) + " as" + s[len("authenticating as"):] } log(s+"\n", args...) }, authEvent) diff --git a/wallet.go b/wallet.go index aebde63..088aaa5 100644 --- a/wallet.go +++ b/wallet.go @@ -6,7 +6,6 @@ import ( "strconv" "strings" - "github.com/fatih/color" "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/nip60" "github.com/nbd-wtf/go-nostr/nip61" @@ -60,20 +59,16 @@ func prepareWallet(ctx context.Context, c *cli.Command) (*nip60.Wallet, func(), log("- saving kind:%d event (%s)... ", event.Kind, desc) first := true for res := range sys.Pool.PublishMany(ctx, relays, event) { - cleanUrl, ok := strings.CutPrefix(res.RelayURL, "wss://") - if !ok { - cleanUrl = res.RelayURL - } - + cleanUrl, _ := strings.CutPrefix(res.RelayURL, "wss://") if !first { log(", ") } first = false if res.Error != nil { - log("%s: %s", color.RedString(cleanUrl), res.Error) + log("%s: %s", colors.errorf(cleanUrl), res.Error) } else { - log("%s: ok", color.GreenString(cleanUrl)) + log("%s: ok", colors.successf(cleanUrl)) } } log("\n") @@ -376,19 +371,15 @@ var wallet = &cli.Command{ log("- publishing nutzap... ") first := true for res := range results { - cleanUrl, ok := strings.CutPrefix(res.RelayURL, "wss://") - if !ok { - cleanUrl = res.RelayURL - } - + cleanUrl, _ := strings.CutPrefix(res.RelayURL, "wss://") if !first { log(", ") } first = false if res.Error != nil { - log("%s: %s", color.RedString(cleanUrl), res.Error) + log("%s: %s", colors.errorf(cleanUrl), res.Error) } else { - log("%s: ok", color.GreenString(cleanUrl)) + log("%s: ok", colors.successf(cleanUrl)) } } @@ -463,10 +454,7 @@ var wallet = &cli.Command{ log("- saving kind:10019 event... ") first := true for res := range sys.Pool.PublishMany(ctx, relays, evt) { - cleanUrl, ok := strings.CutPrefix(res.RelayURL, "wss://") - if !ok { - cleanUrl = res.RelayURL - } + cleanUrl, _ := strings.CutPrefix(res.RelayURL, "wss://") if !first { log(", ") @@ -474,9 +462,9 @@ var wallet = &cli.Command{ first = false if res.Error != nil { - log("%s: %s", color.RedString(cleanUrl), res.Error) + log("%s: %s", colors.errorf(cleanUrl), res.Error) } else { - log("%s: ok", color.GreenString(cleanUrl)) + log("%s: ok", colors.successf(cleanUrl)) } } From 6f48c29d0f8830efd1a9c58ac84a0ee14e7188f8 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Thu, 3 Apr 2025 21:31:12 -0300 Subject: [PATCH 246/401] fix go-nostr dependency. --- go.mod | 4 ---- go.sum | 8 ++------ 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/go.mod b/go.mod index add0a56..95c9d4d 100644 --- a/go.mod +++ b/go.mod @@ -24,7 +24,6 @@ require ( ) 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 @@ -73,9 +72,6 @@ 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 github.com/nbd-wtf/go-nostr => ../go-nostr diff --git a/go.sum b/go.sum index af918e9..7468d1a 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,5 @@ 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/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= @@ -40,8 +38,6 @@ 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= @@ -153,6 +149,8 @@ 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= @@ -226,8 +224,6 @@ 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= From 55fd63178765138184c487b7b52167e2ea69407b Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Thu, 3 Apr 2025 22:08:11 -0300 Subject: [PATCH 247/401] fix term.GetSize() when piping. --- helpers.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/helpers.go b/helpers.go index 247dd83..73f1fd7 100644 --- a/helpers.go +++ b/helpers.go @@ -305,7 +305,7 @@ func supportsDynamicMultilineMagic() bool { return false } - width, _, err := term.GetSize(0) + width, _, err := term.GetSize(int(os.Stderr.Fd())) if err != nil { return false } @@ -373,7 +373,7 @@ func unwrapAll(err error) error { } func clampMessage(msg string, prefixAlreadyPrinted int) string { - termSize, _, _ := term.GetSize(0) + termSize, _, _ := term.GetSize(int(os.Stderr.Fd())) if len(msg) > termSize-prefixAlreadyPrinted { msg = msg[0:termSize-prefixAlreadyPrinted-1] + "…" } From 15aefe3df44fd165bbb7e4130a16f7041531a1a6 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Thu, 3 Apr 2025 22:17:30 -0300 Subject: [PATCH 248/401] more examples on readme. --- README.md | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/README.md b/README.md index 0ddabc7..d0c4df5 100644 --- a/README.md +++ b/README.md @@ -229,6 +229,39 @@ type the password to decrypt your secret key: ******** ~> aria2c $(nak fetch nevent1qqsdsg6x7uujekac4ga7k7qa9q9sx8gqj7xzjf5w9us0dm0ghvf4ugspp4mhxue69uhkummn9ekx7mq6dw9y4 | jq -r '"magnet:?xt=urn:btih:\(tag_value("x"))&dn=\(tag_value("title"))&tr=http%3A%2F%2Ftracker.loadpeers.org%3A8080%2FxvRKfvAlnfuf5EfxTT5T0KIVPtbqAHnX%2Fannounce&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A6969%2Fannounce&tr=udp%3A%2F%2Fopen.stealth.si%3A80%2Fannounce&tr=udp%3A%2F%2Ftracker.torrent.eu.org%3A451%2Fannounce&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337&tr=\(tags("tracker") | map(.[1] | @uri) | join("&tr="))"') ``` +### mount Nostr as a FUSE filesystem and publish a note +```shell +~> nak fs --sec 01 ~/nostr +- mounting at /home/user/nostr... ok. +~> cd ~/nostr/npub1xxxxxx/notes/ +~> echo "satellites are bad!" > new +pending note updated, timer reset. +- `touch publish` to publish immediately +- `rm new` to erase and cancel the publication. +~> touch publish +publishing now! +{"id":"f1cbfa6...","pubkey":"...","content":"satellites are bad!","sig":"..."} +publishing to 3 relays... offchain.pub: ok, nostr.wine: ok, pyramid.fiatjaf.com: ok +event published as f1cbfa6... and updated locally. +``` + +### list NIP-60 wallet tokens and send some +```shell +~> nak wallet tokens +91a10b6fc8bbe7ef2ad9ad0142871d80468b697716d9d2820902db304ff1165e 500 cashu.space +cac7f89f0611021984d92a7daca219e4cd1c9798950e50e952bba7cde1ac1337 1000 legend.lnbits.com +~> nak wallet send 100 +cashuA1psxqyry8... +~> nak wallet pay lnbc1... +``` + +### upload and download files with blossom +```shell +~> nak blossom --server blossom.azzamo.net --sec 01 upload image.png +{"sha256":"38c51756f3e9fedf039488a1f6e513286f6743194e7a7f25effdc84a0ee4c2cf","url":"https://blossom.azzamo.net/38c51756f3e9fedf039488a1f6e513286f6743194e7a7f25effdc84a0ee4c2cf.png"} +~> nak blossom --server aegis.utxo.one download acc8ea43d4e6b706f68b249144364f446854b7f63ba1927371831c05dcf0256c -o downloaded.png +``` + ## contributing to this repository Use NIP-34 to send your patches to `naddr1qqpkucttqy28wumn8ghj7un9d3shjtnwdaehgu3wvfnsz9nhwden5te0wfjkccte9ehx7um5wghxyctwvsq3gamnwvaz7tmjv4kxz7fwv3sk6atn9e5k7q3q80cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsxpqqqpmej2wctpn`. From 35da063c307272f9eace30d73f1b45011185184e Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Mon, 7 Apr 2025 23:13:32 -0300 Subject: [PATCH 249/401] precheck for validity of relay URLs and prevent unwanted crash otherwise. --- helpers.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/helpers.go b/helpers.go index 73f1fd7..53a4c1b 100644 --- a/helpers.go +++ b/helpers.go @@ -158,6 +158,14 @@ func connectToAllRelays( 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, ) []*nostr.Relay { + // first pass to check if these are valid relay URLs + for _, url := range relayUrls { + if !nostr.IsValidRelayURL(nostr.NormalizeURL(url)) { + log("invalid relay URL: %s\n", url) + os.Exit(4) + } + } + sys.Pool = nostr.NewSimplePool(context.Background(), append(opts, nostr.WithEventMiddleware(sys.TrackEventHints), @@ -374,7 +382,8 @@ func unwrapAll(err error) error { func clampMessage(msg string, prefixAlreadyPrinted int) string { termSize, _, _ := term.GetSize(int(os.Stderr.Fd())) - if len(msg) > termSize-prefixAlreadyPrinted { + + if len(msg) > termSize-prefixAlreadyPrinted && prefixAlreadyPrinted+1 < termSize { msg = msg[0:termSize-prefixAlreadyPrinted-1] + "…" } return msg From e45b54ea620e9c276eacfcc672d88f3f47da8627 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Thu, 10 Apr 2025 16:59:56 -0300 Subject: [PATCH 250/401] fix nak mcp. --- mcp.go | 115 ++++++++++++++++++++++++++------------------------------- 1 file changed, 53 insertions(+), 62 deletions(-) diff --git a/mcp.go b/mcp.go index f664d9b..d8c45fa 100644 --- a/mcp.go +++ b/mcp.go @@ -28,25 +28,13 @@ var mcpServer = &cli.Command{ s.AddTool(mcp.NewTool("publish_note", mcp.WithDescription("Publish a short note event to Nostr with the given text content"), - mcp.WithString("relay", - mcp.Description("Relay to publish the note to"), - ), - mcp.WithString("content", - mcp.Required(), - mcp.Description("Arbitrary string to be published"), - ), - mcp.WithString("mention", - mcp.Required(), - mcp.Description("Nostr user's public key to be mentioned"), - ), - ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - content, _ := request.Params.Arguments["content"].(string) - mention, _ := request.Params.Arguments["mention"].(string) - relayI, ok := request.Params.Arguments["relay"] - var relay string - if ok { - relay, _ = relayI.(string) - } + mcp.WithString("content", mcp.Description("Arbitrary string to be published"), mcp.Required()), + mcp.WithString("relay", mcp.Description("Relay to publish the note to")), + mcp.WithString("mention", mcp.Description("Nostr user's public key to be mentioned")), + ), func(ctx context.Context, r mcp.CallToolRequest) (*mcp.CallToolResult, error) { + content := required[string](r, "content") + 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 @@ -81,7 +69,9 @@ var mcpServer = &cli.Command{ } // extra relay specified - relays = append(relays, relay) + if relay != "" { + relays = append(relays, relay) + } result := strings.Builder{} result.WriteString( @@ -111,12 +101,9 @@ var mcpServer = &cli.Command{ s.AddTool(mcp.NewTool("resolve_nostr_uri", mcp.WithDescription("Resolve URIs prefixed with nostr:, including nostr:nevent1..., nostr:npub1..., nostr:nprofile1... and nostr:naddr1..."), - mcp.WithString("uri", - mcp.Required(), - mcp.Description("URI to be resolved"), - ), - ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - uri, _ := request.Params.Arguments["uri"].(string) + mcp.WithString("uri", mcp.Description("URI to be resolved"), mcp.Required()), + ), func(ctx context.Context, r mcp.CallToolRequest) (*mcp.CallToolResult, error) { + uri := required[string](r, "uri") if strings.HasPrefix(uri, "nostr:") { uri = uri[6:] } @@ -159,12 +146,9 @@ var mcpServer = &cli.Command{ s.AddTool(mcp.NewTool("search_profile", mcp.WithDescription("Search for the public key of a Nostr user given their name"), - mcp.WithString("name", - mcp.Required(), - mcp.Description("Name to be searched"), - ), - ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - name, _ := request.Params.Arguments["name"].(string) + 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}}) if re == nil { return mcp.NewToolResultError("couldn't find anyone with that name"), nil @@ -175,42 +159,24 @@ var mcpServer = &cli.Command{ 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.Required(), - mcp.Description("Public key of Nostr user we want to know the relay from where to read"), - ), - ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - pubkey, _ := request.Params.Arguments["pubkey"].(string) + 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") res := sys.FetchOutboxRelays(ctx, pubkey, 1) return mcp.NewToolResultText(res[0]), nil }) s.AddTool(mcp.NewTool("read_events_from_relay", mcp.WithDescription("Makes a REQ query to one relay using the specified parameters, this can be used to fetch notes from a profile"), - mcp.WithNumber("kind", - mcp.Required(), - mcp.Description("event kind number to include in the 'kinds' field"), - ), - mcp.WithString("pubkey", - mcp.Description("pubkey to include in the 'authors' field"), - ), - mcp.WithNumber("limit", - mcp.Required(), - mcp.Description("maximum number of events to query"), - ), - mcp.WithString("relay", - mcp.Required(), - mcp.Description("relay URL to send the query to"), - ), - ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - relay, _ := request.Params.Arguments["relay"].(string) - limit, _ := request.Params.Arguments["limit"].(int) - kind, _ := request.Params.Arguments["kind"].(int) - pubkeyI, ok := request.Params.Arguments["pubkey"] - var pubkey string - if ok { - pubkey, _ = pubkeyI.(string) - } + 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")), + ), 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 @@ -242,3 +208,28 @@ var mcpServer = &cli.Command{ return server.ServeStdio(s) }, } + +func required[T comparable](r mcp.CallToolRequest, p string) T { + var zero T + if _, ok := r.Params.Arguments[p]; !ok { + return zero + } + if _, ok := r.Params.Arguments[p].(T); !ok { + return zero + } + if r.Params.Arguments[p].(T) == zero { + return zero + } + return r.Params.Arguments[p].(T) +} + +func optional[T any](r mcp.CallToolRequest, p string) (T, bool) { + var zero T + if _, ok := r.Params.Arguments[p]; !ok { + return zero, false + } + if _, ok := r.Params.Arguments[p].(T); !ok { + return zero, false + } + return r.Params.Arguments[p].(T), true +} From 1b43dbda02641542548810f63b367f1c91edfb36 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sat, 19 Apr 2025 18:07:01 -0300 Subject: [PATCH 251/401] remove dvm command. --- dvm.go | 133 -------------------------------------------------------- main.go | 1 - 2 files changed, 134 deletions(-) delete mode 100644 dvm.go diff --git a/dvm.go b/dvm.go deleted file mode 100644 index 5e05e39..0000000 --- a/dvm.go +++ /dev/null @@ -1,133 +0,0 @@ -package main - -import ( - "context" - "fmt" - "os" - "strconv" - "strings" - - "github.com/nbd-wtf/go-nostr" - "github.com/nbd-wtf/go-nostr/nip90" - "github.com/urfave/cli/v3" -) - -var dvm = &cli.Command{ - Name: "dvm", - Usage: "deal with nip90 data-vending-machine things (experimental)", - DisableSliceFlagSeparator: true, - Flags: append(defaultKeyFlags, - &cli.StringSliceFlag{ - Name: "relay", - Aliases: []string{"r"}, - }, - ), - Commands: append([]*cli.Command{ - { - Name: "list", - Usage: "find DVMs that have announced themselves for a specific kind", - DisableSliceFlagSeparator: true, - Action: func(ctx context.Context, c *cli.Command) error { - return fmt.Errorf("we don't know how to do this yet") - }, - }, - }, (func() []*cli.Command { - commands := make([]*cli.Command, len(nip90.Jobs)) - for i, job := range nip90.Jobs { - flags := make([]cli.Flag, 0, 2+len(job.Params)) - - if job.InputType != "" { - flags = append(flags, &cli.StringSliceFlag{ - Name: "input", - Aliases: []string{"i"}, - Category: "INPUT", - }) - } - - for _, param := range job.Params { - flags = append(flags, &cli.StringSliceFlag{ - Name: param, - Category: "PARAMETER", - }) - } - - commands[i] = &cli.Command{ - Name: strconv.Itoa(job.InputKind), - Usage: job.Name, - Description: job.Description, - DisableSliceFlagSeparator: true, - Flags: flags, - Action: func(ctx context.Context, c *cli.Command) error { - relayUrls := c.StringSlice("relay") - relays := connectToAllRelays(ctx, c, relayUrls, nil) - if len(relays) == 0 { - log("failed to connect to any of the given relays.\n") - os.Exit(3) - } - defer func() { - for _, relay := range relays { - relay.Close() - } - }() - - evt := nostr.Event{ - Kind: job.InputKind, - Tags: make(nostr.Tags, 0, 2+len(job.Params)), - CreatedAt: nostr.Now(), - } - - for _, input := range c.StringSlice("input") { - evt.Tags = append(evt.Tags, nostr.Tag{"i", input, job.InputType}) - } - for _, paramN := range job.Params { - for _, paramV := range c.StringSlice(paramN) { - tag := nostr.Tag{"param", paramN, "", ""}[0:2] - for _, v := range strings.Split(paramV, ";") { - tag = append(tag, v) - } - evt.Tags = append(evt.Tags, tag) - } - } - - kr, _, err := gatherKeyerFromArguments(ctx, c) - if err != nil { - return err - } - if err := kr.SignEvent(ctx, &evt); err != nil { - return err - } - - logverbose("%s", evt) - - log("- publishing job request... ") - first := true - for res := range sys.Pool.PublishMany(ctx, relayUrls, evt) { - cleanUrl, _ := strings.CutPrefix(res.RelayURL, "wss://") - - if !first { - log(", ") - } - first = false - - if res.Error != nil { - log("%s: %s", colors.errorf(cleanUrl), res.Error) - } else { - log("%s: ok", colors.successf(cleanUrl)) - } - } - - log("\n- waiting for response...\n") - for ie := range sys.Pool.SubscribeMany(ctx, relayUrls, nostr.Filter{ - Kinds: []int{7000, job.OutputKind}, - Tags: nostr.TagMap{"e": []string{evt.ID}}, - }) { - stdout(ie.Event) - } - - return nil - }, - } - } - return commands - })()...), -} diff --git a/main.go b/main.go index 018ed01..44278cf 100644 --- a/main.go +++ b/main.go @@ -43,7 +43,6 @@ var app = &cli.Command{ wallet, mcpServer, curl, - dvm, fsCmd, }, Version: version, From d733a31898a94de0433e1f7ec1e967702587a033 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sun, 20 Apr 2025 18:11:21 -0300 Subject: [PATCH 252/401] convert to using nostrlib. --- blossom.go | 11 ++- bunker.go | 40 ++++++----- count.go | 21 +++--- curl.go | 2 +- decode.go | 8 +-- encode.go | 69 ++++++------------- encrypt_decrypt.go | 17 ++--- event.go | 29 ++++---- fetch.go | 22 +++--- flags.go | 157 ++++++++++++++++++++++++++++++++++++++++--- fs.go | 14 ++-- go.mod | 8 ++- go.sum | 14 ++-- helpers.go | 44 ++++++------ helpers_key.go | 63 ++++++++--------- key.go | 33 ++++----- main.go | 20 +++--- mcp.go | 72 ++++++++++++-------- musig2.go | 36 ++++------ nostrfs/asyncfile.go | 2 +- nostrfs/entitydir.go | 28 +++++--- nostrfs/eventdir.go | 30 +++++---- nostrfs/helpers.go | 11 ++- nostrfs/npubdir.go | 37 +++++----- nostrfs/root.go | 17 +++-- nostrfs/viewdir.go | 41 ++++++----- outbox.go | 10 +-- relay.go | 6 +- req.go | 45 +++++++------ serve.go | 32 +++++---- verify.go | 6 +- wallet.go | 41 ++++++----- 32 files changed, 568 insertions(+), 418 deletions(-) diff --git a/blossom.go b/blossom.go index f4b039f..eb4b168 100644 --- a/blossom.go +++ b/blossom.go @@ -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) diff --git a/bunker.go b/bunker.go index 12e8e32..2f412dc 100644 --- a/bunker.go +++ b/bunker.go @@ -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") }, }, }, diff --git a/count.go b/count.go index c01b65e..905d0f0 100644 --- a/count.go +++ b/count.go @@ -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 { diff --git a/curl.go b/curl.go index 973746d..d3d6e83 100644 --- a/curl.go +++ b/curl.go @@ -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" ) diff --git a/decode.go b/decode.go index 4ad2b13..5bda33b 100644 --- a/decode.go +++ b/decode.go @@ -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 diff --git a/encode.go b/encode.go index c8dd419..c5f9db9 100644 --- a/encode.go +++ b/encode.go @@ -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) diff --git a/encrypt_decrypt.go b/encrypt_decrypt.go index 930948a..457546f 100644 --- a/encrypt_decrypt.go +++ b/encrypt_decrypt.go @@ -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() diff --git a/event.go b/event.go index f4b86bb..905e313 100644 --- a/event.go +++ b/event.go @@ -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 { - return authSigner(ctx, c, func(s string, args ...any) {}, authEvent) - }), + 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") } } diff --git a/fetch.go b/fetch.go index a2d141f..0bcef74 100644 --- a/fetch.go +++ b/fetch.go @@ -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) } } diff --git a/flags.go b/flags.go index e9ddbcc..c3c18f4 100644 --- a/flags.go +++ b/flags.go @@ -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: ×tamp} -} - // 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) +} diff --git a/fs.go b/fs.go index 4d4b886..f55ebbe 100644 --- a/fs.go +++ b/fs.go @@ -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: "", 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") diff --git a/go.mod b/go.mod index 95c9d4d..bbf639b 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 7468d1a..c319162 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/helpers.go b/helpers.go index 53a4c1b..ba4be7d 100644 --- a/helpers.go +++ b/helpers.go @@ -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) { diff --git a/helpers_key.go b/helpers_key.go index d560001..972c11c 100644 --- a/helpers_key.go +++ b/helpers_key.go @@ -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) { diff --git a/key.go b/key.go index 4ccd5cb..2d5c115 100644 --- a/key.go +++ b/key.go @@ -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) }() diff --git a/main.go b/main.go index 44278cf..70d3954 100644 --- a/main.go +++ b/main.go @@ -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 }, diff --git a/mcp.go b/mcp.go index d8c45fa..96e847a 100644 --- a/mcp.go +++ b/mcp.go @@ -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}) + 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.Tags = append(evt.Tags, nostr.Tag{"p", pk.Hex()}) // their inbox relays - relays = sys.FetchInboxRelays(ctx, mention, 3) + relays = sys.FetchInboxRelays(ctx, pk, 3) } - evt.Sign(sk) + 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("'") diff --git a/musig2.go b/musig2.go index 1d87f32..eacb96e 100644 --- a/musig2.go +++ b/musig2.go @@ -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]) diff --git a/nostrfs/asyncfile.go b/nostrfs/asyncfile.go index c322f64..14d5b16 100644 --- a/nostrfs/asyncfile.go +++ b/nostrfs/asyncfile.go @@ -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 { diff --git a/nostrfs/entitydir.go b/nostrfs/entitydir.go index ed8bd76..2b8a9c8 100644 --- a/nostrfs/entitydir.go +++ b/nostrfs/entitydir.go @@ -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) } diff --git a/nostrfs/eventdir.go b/nostrfs/eventdir.go index ac99196..ced7a71 100644 --- a/nostrfs/eventdir.go +++ b/nostrfs/eventdir.go @@ -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) diff --git a/nostrfs/helpers.go b/nostrfs/helpers.go index e067e6b..79562b2 100644 --- a/nostrfs/helpers.go +++ b/nostrfs/helpers.go @@ -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 -} diff --git a/nostrfs/npubdir.go b/nostrfs/npubdir.go index d34a226..afce05c 100644 --- a/nostrfs/npubdir.go +++ b/nostrfs/npubdir.go @@ -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, diff --git a/nostrfs/root.go b/nostrfs/root.go index d821bc8..6c9fc2f 100644 --- a/nostrfs/root.go +++ b/nostrfs/root.go @@ -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} diff --git a/nostrfs/viewdir.go b/nostrfs/viewdir.go index 1dbbd90..4f992fb 100644 --- a/nostrfs/viewdir.go +++ b/nostrfs/viewdir.go @@ -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) } } } diff --git a/outbox.go b/outbox.go index 284f148..3c3ae3e 100644 --- a/outbox.go +++ b/outbox.go @@ -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) } diff --git a/relay.go b/relay.go index b05f580..450ca33 100644 --- a/relay.go +++ b/relay.go @@ -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" ) diff --git a/req.go b/req.go index 3221fdd..c55c1fa 100644 --- a/req.go +++ b/req.go @@ -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 { - return authSigner(ctx, c, func(s string, args ...any) { - if strings.HasPrefix(s, "authenticating as") { - cleanUrl, _ := strings.CutPrefix(authEvent.Relay.URL, "wss://") - s = "authenticating to " + color.CyanString(cleanUrl) + " as" + s[len("authenticating as"):] - } - log(s+"\n", args...) - }, authEvent) - }), - ) + 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( + 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 diff --git a/serve.go b/serve.go index 28489dc..ee6ed51 100644 --- a/serve.go +++ b/serve.go @@ -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() diff --git a/verify.go b/verify.go index bccd27e..c74eab8 100644 --- a/verify.go +++ b/verify.go @@ -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 } } diff --git a/wallet.go b/wallet.go index 088aaa5..615998c 100644 --- a/wallet.go +++ b/wallet.go @@ -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,12 +340,12 @@ var wallet = &cli.Command{ return err } eventId = evt.ID - target = evt.PubKey - } - - pm, err := sys.FetchProfileFromInput(ctx, target) - if err != nil { - return err + 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) } From 01be954ae67ca4e33ab576c4a69e1d89841b8683 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Mon, 21 Apr 2025 15:33:49 -0300 Subject: [PATCH 253/401] use badger for outbox hints. --- go.mod | 10 +++++++++ go.sum | 66 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ helpers.go | 5 ----- main.go | 66 +++++++++++++++--------------------------------------- outbox.go | 47 +++++++++++++++++++++++++++++++++----- 5 files changed, 135 insertions(+), 59 deletions(-) diff --git a/go.mod b/go.mod index bbf639b..5dc9581 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( 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/stretchr/testify v1.10.0 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 @@ -35,13 +36,18 @@ require ( github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect github.com/cloudwego/base64x v0.1.5 // indirect github.com/coder/websocket v1.8.13 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect + github.com/dgraph-io/badger/v4 v4.5.0 // indirect github.com/dgraph-io/ristretto v1.0.0 // indirect + github.com/dgraph-io/ristretto/v2 v2.1.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/elliotchance/pie/v2 v2.7.0 // indirect github.com/elnosh/gonuts v0.3.1-0.20250123162555-7c0381a585e3 // indirect github.com/fasthttp/websocket v1.5.12 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect + github.com/google/flatbuffers v24.12.23+incompatible // indirect github.com/google/uuid v1.6.0 // indirect github.com/hablullah/go-hijri v1.0.2 // indirect github.com/hablullah/go-juliandays v1.0.0 // indirect @@ -56,6 +62,7 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect github.com/rs/cors v1.11.1 // indirect github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 // indirect @@ -68,12 +75,15 @@ require ( github.com/valyala/fasthttp v1.59.0 // indirect github.com/wasilibs/go-re2 v1.3.0 // indirect github.com/x448/float16 v0.8.4 // indirect + go.opencensus.io v0.24.0 // indirect 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 + google.golang.org/protobuf v1.36.2 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) replace fiatjaf.com/nostr => ../nostrlib diff --git a/go.sum b/go.sum index c319162..a33ff50 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= fiatjaf.com/lib v0.3.1 h1:/oFQwNtFRfV+ukmOCxfBEAuayoLwXp4wu2/fz5iHpwA= fiatjaf.com/lib v0.3.1/go.mod h1:Ycqq3+mJ9jAWu7XjbQI1cVr+OFgnHn79dQR5oTII47g= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 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= @@ -42,6 +44,7 @@ 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/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 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= @@ -52,9 +55,11 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5O github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE= github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -68,8 +73,12 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeC github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= +github.com/dgraph-io/badger/v4 v4.5.0 h1:TeJE3I1pIWLBjYhIYCA1+uxrjWEoJXImFBMEBVSm16g= +github.com/dgraph-io/badger/v4 v4.5.0/go.mod h1:ysgYmIeG8dS/E8kwxT7xHyc7MkmwNYLRoYnFbr7387A= github.com/dgraph-io/ristretto v1.0.0 h1:SYG07bONKMlFDUYu5pEu3DGAh8c2OFNzKm6G9J4Si84= github.com/dgraph-io/ristretto v1.0.0/go.mod h1:jTi2FiYEhQ1NsMmA7DeBykizjOuY88NhKBkepyu1jPc= +github.com/dgraph-io/ristretto/v2 v2.1.0 h1:59LjpOJLNDULHh8MC4UaegN52lC4JnO2dITsie/Pa8I= +github.com/dgraph-io/ristretto/v2 v2.1.0/go.mod h1:uejeqfYXpUomfse0+lO+13ATz4TypQYLJZzBSAemuB4= github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y= github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= @@ -79,6 +88,10 @@ github.com/elliotchance/pie/v2 v2.7.0 h1:FqoIKg4uj0G/CrLGuMS9ejnFKa92lxE1dEgBD3p github.com/elliotchance/pie/v2 v2.7.0/go.mod h1:18t0dgGFH006g4eVdDtWfgFZPQEgl10IoEO8YWEq3Og= github.com/elnosh/gonuts v0.3.1-0.20250123162555-7c0381a585e3 h1:k7evIqJ2BtFn191DgY/b03N2bMYA/iQwzr4f/uHYn20= github.com/elnosh/gonuts v0.3.1-0.20250123162555-7c0381a585e3/go.mod h1:vgZomh4YQk7R3w4ltZc0sHwCmndfHkuX6V4sga/8oNs= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fasthttp/websocket v1.5.12 h1:e4RGPpWW2HTbL3zV0Y/t7g0ub294LkiuXXUuTOUInlE= 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= @@ -87,20 +100,34 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo 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/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/flatbuffers v24.12.23+incompatible h1:ubBKR94NR4pXUCY/MUsRVzd9umNW7ht7EG9hHfS9FX8= +github.com/google/flatbuffers v24.12.23+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= @@ -164,6 +191,7 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg= github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= @@ -207,6 +235,8 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= golang.org/x/arch v0.15.0 h1:QtOrQd0bTUnhNVNndMpLHNWrDmYzZ2KDqSrEymqInZw= golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -214,18 +244,31 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 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/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/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-20180830151530-49385e6e1522/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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -235,6 +278,7 @@ golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= @@ -247,14 +291,34 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.36.2 h1:R8FeyR1/eLmkutZOM5CWghmo5itiG9z0ktFlTVLuTmU= +google.golang.org/protobuf v1.36.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= @@ -266,4 +330,6 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= diff --git a/helpers.go b/helpers.go index ba4be7d..c9f542c 100644 --- a/helpers.go +++ b/helpers.go @@ -29,11 +29,6 @@ import ( var sys *sdk.System -var ( - hintsFilePath string - hintsFileExists bool -) - var json = jsoniter.ConfigFastest const ( diff --git a/main.go b/main.go index 70d3954..84e6acf 100644 --- a/main.go +++ b/main.go @@ -2,14 +2,14 @@ package main import ( "context" + "fmt" "net/http" "net/textproto" "os" - "path/filepath" "fiatjaf.com/nostr" "fiatjaf.com/nostr/sdk" - "fiatjaf.com/nostr/sdk/hints/memoryh" + "github.com/fatih/color" "github.com/urfave/cli/v3" ) @@ -81,36 +81,12 @@ var app = &cli.Command{ }, }, Before: func(ctx context.Context, c *cli.Command) (context.Context, error) { - configPath := c.String("config-path") - if configPath == "" { - if home, err := os.UserHomeDir(); err == nil { - configPath = filepath.Join(home, ".config/nak") - } - } - if configPath != "" { - hintsFilePath = filepath.Join(configPath, "outbox/hints.db") - } - if hintsFilePath != "" { - if _, err := os.Stat(hintsFilePath); !os.IsNotExist(err) { - hintsFileExists = true - } - } - - if hintsFilePath != "" { - if data, err := os.ReadFile(hintsFilePath); err == nil { - hintsdb := memoryh.NewHintDB() - if err := json.Unmarshal(data, &hintsdb); err == nil { - sys = sdk.NewSystem( - sdk.WithHintsDB(hintsdb), - ) - goto systemOperational - } - } - } - sys = sdk.NewSystem() - systemOperational: + if err := initializeOutboxHintsDB(c, sys); err != nil { + return ctx, fmt.Errorf("failed to initialized outbox hints: %w", err) + } + sys.Pool = nostr.NewPool(nostr.PoolOptions{ AuthorKindQueryMiddleware: sys.TrackQueryAttempts, EventMiddleware: sys.TrackEventHints, @@ -121,32 +97,24 @@ var app = &cli.Command{ return ctx, nil }, - After: func(ctx context.Context, c *cli.Command) error { - // save hints database on exit - if hintsFileExists { - data, err := json.Marshal(sys.Hints) - if err != nil { - return err - } - return os.WriteFile(hintsFilePath, data, 0644) - } +} - return nil - }, +func init() { + cli.VersionFlag = &cli.BoolFlag{ + Name: "version", + Usage: "prints the version", + } } func main() { defer colors.reset() - cli.VersionFlag = &cli.BoolFlag{ - Name: "version", - Usage: "prints the version", - } - // a megahack to enable this curl command proxy if len(os.Args) > 2 && os.Args[1] == "curl" { if err := realCurl(); err != nil { - stdout(err) + if err != nil { + log(color.YellowString(err.Error()) + "\n") + } colors.reset() os.Exit(1) } @@ -154,7 +122,9 @@ func main() { } if err := app.Run(context.Background(), os.Args); err != nil { - stdout(err) + if err != nil { + log(color.YellowString(err.Error()) + "\n") + } colors.reset() os.Exit(1) } diff --git a/outbox.go b/outbox.go index 3c3ae3e..877cebb 100644 --- a/outbox.go +++ b/outbox.go @@ -7,9 +7,44 @@ import ( "path/filepath" "fiatjaf.com/nostr" + "fiatjaf.com/nostr/sdk" + "fiatjaf.com/nostr/sdk/hints/badgerh" + "github.com/fatih/color" "github.com/urfave/cli/v3" ) +var ( + hintsFilePath string + hintsFileExists bool +) + +func initializeOutboxHintsDB(c *cli.Command, sys *sdk.System) error { + configPath := c.String("config-path") + if configPath == "" { + if home, err := os.UserHomeDir(); err == nil { + configPath = filepath.Join(home, ".config/nak") + } + } + if configPath != "" { + hintsFilePath = filepath.Join(configPath, "outbox/hints.bg") + } + if hintsFilePath != "" { + if _, err := os.Stat(hintsFilePath); !os.IsNotExist(err) { + hintsFileExists = true + } else if err != nil { + return err + } + } + if hintsFileExists && hintsFilePath != "" { + hintsdb, err := badgerh.NewBadgerHints(hintsFilePath) + if err == nil { + sys.Hints = hintsdb + } + } + + return nil +} + var outbox = &cli.Command{ Name: "outbox", Usage: "manage outbox relay hints database", @@ -27,10 +62,10 @@ var outbox = &cli.Command{ return fmt.Errorf("couldn't find a place to store the hints, pass --config-path to fix.") } - if err := os.MkdirAll(filepath.Dir(hintsFilePath), 0777); err == nil { - if err := os.WriteFile(hintsFilePath, []byte("{}"), 0644); err != nil { - return fmt.Errorf("failed to create hints database: %w", err) - } + os.MkdirAll(hintsFilePath, 0755) + _, err := badgerh.NewBadgerHints(hintsFilePath) + if err != nil { + return fmt.Errorf("failed to create badger hints db at '%s': %w", hintsFilePath, err) } log("initialized hints database at %s\n", hintsFilePath) @@ -44,8 +79,8 @@ var outbox = &cli.Command{ DisableSliceFlagSeparator: true, Action: func(ctx context.Context, c *cli.Command) error { if !hintsFileExists { - log("running with temporary fragile data.\n") - log("call `nak outbox init` to setup persistence.\n") + log(color.YellowString("running with temporary fragile data.\n")) + log(color.YellowString("call `nak outbox init` to setup persistence.\n")) } if c.Args().Len() != 1 { From 5a8c7df811d61a52739b0c8ff51a79dc77585e16 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Mon, 21 Apr 2025 15:37:13 -0300 Subject: [PATCH 254/401] fix and simplify `nak decode`. --- decode.go | 113 ++++++++++++++++++++---------------------------------- 1 file changed, 41 insertions(+), 72 deletions(-) diff --git a/decode.go b/decode.go index 5bda33b..06c5f1a 100644 --- a/decode.go +++ b/decode.go @@ -3,11 +3,12 @@ package main import ( "context" "encoding/hex" + stdjson "encoding/json" "strings" "fiatjaf.com/nostr" + "fiatjaf.com/nostr/nip05" "fiatjaf.com/nostr/nip19" - "fiatjaf.com/nostr/sdk" "github.com/urfave/cli/v3" ) @@ -39,88 +40,56 @@ var decode = &cli.Command{ input = input[6:] } - var decodeResult DecodeResult - if b, err := hex.DecodeString(input); err == nil { - if len(b) == 64 { - decodeResult.HexResult.PossibleTypes = []string{"sig"} - decodeResult.HexResult.Signature = hex.EncodeToString(b) - } else if len(b) == 32 { - decodeResult.HexResult.PossibleTypes = []string{"pubkey", "private_key", "event_id"} - decodeResult.HexResult.ID = hex.EncodeToString(b) - decodeResult.HexResult.PrivateKey = hex.EncodeToString(b) - decodeResult.HexResult.PublicKey = hex.EncodeToString(b) - } else { - ctx = lineProcessingError(ctx, "hex string with invalid number of bytes: %d", len(b)) + _, data, err := nip19.Decode(input) + if err == nil { + switch v := data.(type) { + case nostr.SecretKey: + stdout(v.Hex()) + continue + case nostr.PubKey: + stdout(v.Hex()) + continue + case [32]byte: + stdout(hex.EncodeToString(v[:])) + continue + case nostr.EventPointer: + if c.Bool("id") { + stdout(v.ID.Hex()) + continue + } + out, _ := stdjson.MarshalIndent(v, "", " ") + stdout(string(out)) + continue + case nostr.ProfilePointer: + if c.Bool("pubkey") { + stdout(v.PublicKey.Hex()) + continue + } + out, _ := stdjson.MarshalIndent(v, "", " ") + stdout(string(out)) + continue + case nostr.EntityPointer: + out, _ := stdjson.MarshalIndent(v, "", " ") + stdout(string(out)) continue } - } else if evp := sdk.InputToEventPointer(input); evp != nil { - decodeResult = DecodeResult{EventPointer: evp} - if c.Bool("id") { - stdout(evp.ID) - continue - } - } else if pp := sdk.InputToProfile(ctx, input); pp != nil { - decodeResult = DecodeResult{ProfilePointer: pp} + } + + pp, _ := nip05.QueryIdentifier(ctx, input) + if pp != nil { if c.Bool("pubkey") { - stdout(pp.PublicKey) + stdout(pp.PublicKey.Hex()) continue } - } else if prefix, value, err := nip19.Decode(input); err == nil && prefix == "naddr" { - if ep, ok := value.(nostr.EntityPointer); ok { - decodeResult = DecodeResult{EntityPointer: &ep} - } else { - ctx = lineProcessingError(ctx, "couldn't decode naddr: %s", err) - } - } else if prefix, value, err := nip19.Decode(input); err == nil && prefix == "nsec" { - decodeResult.PrivateKey.PrivateKey = value.(string) - decodeResult.PrivateKey.PublicKey = nostr.GetPublicKey(value.(nostr.SecretKey)) - } else { - ctx = lineProcessingError(ctx, "couldn't decode input '%s': %s", input, err) + out, _ := stdjson.MarshalIndent(pp, "", " ") + stdout(string(out)) continue } - if c.Bool("pubkey") || c.Bool("id") { - return nil - } - - stdout(decodeResult.JSON()) - + ctx = lineProcessingError(ctx, "couldn't decode input '%s'", input) } exitIfLineProcessingError(ctx) return nil }, } - -type DecodeResult struct { - *nostr.EventPointer - *nostr.ProfilePointer - *nostr.EntityPointer - HexResult struct { - PossibleTypes []string `json:"possible_types"` - PublicKey string `json:"pubkey,omitempty"` - ID string `json:"event_id,omitempty"` - PrivateKey string `json:"private_key,omitempty"` - Signature string `json:"sig,omitempty"` - } - PrivateKey struct { - nostr.ProfilePointer - PrivateKey string `json:"private_key"` - } -} - -func (d DecodeResult) JSON() string { - var j []byte - if d.EventPointer != nil { - j, _ = json.MarshalIndent(d.EventPointer, "", " ") - } else if d.ProfilePointer != nil { - j, _ = json.MarshalIndent(d.ProfilePointer, "", " ") - } else if d.EntityPointer != nil { - j, _ = json.MarshalIndent(d.EntityPointer, "", " ") - } else if len(d.HexResult.PossibleTypes) > 0 { - j, _ = json.MarshalIndent(d.HexResult, "", " ") - } else if d.PrivateKey.PrivateKey != "" { - j, _ = json.MarshalIndent(d.PrivateKey, "", " ") - } - return string(j) -} From 5d44600f1758b4c574efd2fbb8242b370f3ca911 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Mon, 21 Apr 2025 18:09:27 -0300 Subject: [PATCH 255/401] test and fixes. --- cli_test.go | 232 ++++++++++++++++++++++++++++++++++++++++++++++++ example_test.go | 138 ---------------------------- helpers_key.go | 4 +- justfile | 5 ++ key.go | 12 +-- 5 files changed, 246 insertions(+), 145 deletions(-) create mode 100644 cli_test.go delete mode 100644 example_test.go create mode 100644 justfile diff --git a/cli_test.go b/cli_test.go new file mode 100644 index 0000000..f9cf6c4 --- /dev/null +++ b/cli_test.go @@ -0,0 +1,232 @@ +package main + +import ( + "encoding/hex" + stdjson "encoding/json" + "fmt" + "strings" + "testing" + + "fiatjaf.com/nostr" + "github.com/stretchr/testify/require" +) + +// these tests are tricky because commands and flags are declared as globals and values set in one call may persist +// to the next. for example, if in the first test we set --limit 2 then doesn't specify --limit in the second then +// it will still return true for cmd.IsSet("limit") and then we will set .LimitZero = true + +func call(t *testing.T, cmd string) string { + var output strings.Builder + stdout = func(a ...any) (int, error) { + output.WriteString(fmt.Sprint(a...)) + output.WriteString("\n") + return 0, nil + } + err := app.Run(t.Context(), strings.Split(cmd, " ")) + require.NoError(t, err) + + return strings.TrimSpace(output.String()) +} + +func TestEventBasic(t *testing.T) { + output := call(t, "nak event --ts 1699485669") + + var evt nostr.Event + err := stdjson.Unmarshal([]byte(output), &evt) + require.NoError(t, err) + + require.Equal(t, nostr.Kind(1), evt.Kind) + require.Equal(t, nostr.Timestamp(1699485669), evt.CreatedAt) + require.Equal(t, "hello from the nostr army knife", evt.Content) + require.Equal(t, "36d88cf5fcc449f2390a424907023eda7a74278120eebab8d02797cd92e7e29c", evt.ID.Hex()) + require.Equal(t, "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", evt.PubKey.Hex()) + require.Equal(t, "68e71a192e8abcf8582a222434ac823ecc50607450ebe8cc4c145eb047794cc382dc3f888ce879d2f404f5ba6085a47601360a0fa2dd4b50d317bd0c6197c2c2", hex.EncodeToString(evt.Sig[:])) +} + +func TestEventComplex(t *testing.T) { + output := call(t, "nak event --ts 1699485669 -k 11 -c skjdbaskd --sec 17 -t t=spam -e 36d88cf5fcc449f2390a424907023eda7a74278120eebab8d02797cd92e7e29c -t r=https://abc.def?name=foobar;nothing") + + var evt nostr.Event + err := stdjson.Unmarshal([]byte(output), &evt) + require.NoError(t, err) + + require.Equal(t, nostr.Kind(11), evt.Kind) + require.Equal(t, nostr.Timestamp(1699485669), evt.CreatedAt) + require.Equal(t, "skjdbaskd", evt.Content) + require.Equal(t, "19aba166dcf354bf5ef64f4afe69ada1eb851495001ee05e07d393ee8c8ea179", evt.ID.Hex()) + require.Equal(t, "2fa2104d6b38d11b0230010559879124e42ab8dfeff5ff29dc9cdadd4ecacc3f", evt.PubKey.Hex()) + require.Equal(t, "cf452def4a68341c897c3fc96fa34dc6895a5b8cc266d4c041bcdf758ec992ec5adb8b0179e98552aaaf9450526a26d7e62e413b15b1c57e0cfc8db6b29215d7", hex.EncodeToString(evt.Sig[:])) + + require.Len(t, evt.Tags, 3) + require.Equal(t, nostr.Tag{"t", "spam"}, evt.Tags[0]) + require.Equal(t, nostr.Tag{"r", "https://abc.def?name=foobar", "nothing"}, evt.Tags[1]) + require.Equal(t, nostr.Tag{"e", "36d88cf5fcc449f2390a424907023eda7a74278120eebab8d02797cd92e7e29c"}, evt.Tags[2]) +} + +func TestEncode(t *testing.T) { + require.Equal(t, + "npub156n8a7wuhwk9tgrzjh8gwzc8q2dlekedec5djk0js9d3d7qhnq3qjpdq28", + call(t, "nak encode npub a6a67ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179822"), + ) + require.Equal(t, + `nprofile1qqs2dfn7l8wthtz45p3ftn58pvrs9xlumvkuu2xet8egzkcklqtesgspz9mhxue69uhk27rpd4cxcefwvdhk6fl5jug +nprofile1qqs22kfpwwt4mmvlsd4f2uh23vg60ctvadnyvntx659jw93l0upe6tqpz9mhxue69uhk27rpd4cxcefwvdhk64h265a`, + call(t, "nak encode nprofile -r wss://example.com a6a67ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179822 a5592173975ded9f836a9572ea8b11a7e16ceb66464d66d50b27163f7f039d2c"), + ) +} + +func TestDecodeNaddr(t *testing.T) { + output := call(t, "nak decode naddr1qqyrgcmyxe3kvefhqyxhwumn8ghj7mn0wvhxcmmvqgs9kqvr4dkruv3t7n2pc6e6a7v9v2s5fprmwjv4gde8c4fe5y29v0srqsqqql9ngrt6tu") + + var result map[string]interface{} + err := stdjson.Unmarshal([]byte(output), &result) + require.NoError(t, err) + + require.Equal(t, "5b0183ab6c3e322bf4d41c6b3aef98562a144847b7499543727c5539a114563e", result["pubkey"]) + require.Equal(t, float64(31923), result["kind"]) + require.Equal(t, "4cd6cfe7", result["identifier"]) + require.Equal(t, []interface{}{"wss://nos.lol"}, result["relays"]) +} + +func TestDecodePubkey(t *testing.T) { + output := call(t, "nak decode -p npub10xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqpkge6d npub1ccz8l9zpa47k6vz9gphftsrumpw80rjt3nhnefat4symjhrsnmjs38mnyd") + + expected := "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798\nc6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5" + require.Equal(t, expected, output) +} + +func TestDecodeMultipleNpubs(t *testing.T) { + output := call(t, "nak decode npub1l2vyh47mk2p0qlsku7hg0vn29faehy9hy34ygaclpn66ukqp3afqutajft npub10000003zmk89narqpczy4ff6rnuht2wu05na7kpnh3mak7z2tqzsv8vwqk") + require.Len(t, strings.Split(output, "\n"), 2) +} + +func TestDecodeEventId(t *testing.T) { + output := call(t, "nak decode -e nevent1qyd8wumn8ghj7urewfsk66ty9enxjct5dfskvtnrdakj7qgmwaehxw309aex2mrp0yh8wetnw3jhymnzw33jucm0d5hszxthwden5te0wfjkccte9eekummjwsh8xmmrd9skctcpzamhxue69uhkzarvv9ejumn0wd68ytnvv9hxgtcqyqllp5v5j0nxr74fptqxkhvfv0h3uj870qpk3ln8a58agyxl3fka296ewr8 nevent1qqswh48lurxs8u0pll9qj2rzctvjncwhstpzlstq59rdtzlty79awns5hl5uf") + + expected := "3ff0d19493e661faa90ac06b5d8963ef1e48fe780368fe67ed0fd410df8a6dd5\nebd4ffe0cd03f1e1ffca092862c2d929e1d782c22fc160a146d58beb278bd74e" + require.Equal(t, expected, output) +} + +func TestReq(t *testing.T) { + output := call(t, "nak req -k 1 -l 18 -a 2fa2104d6b38d11b0230010559879124e42ab8dfeff5ff29dc9cdadd4ecacc3f -e aec4de6d051a7c2b6ca2d087903d42051a31e07fb742f1240970084822de10a6") + + var result []interface{} + err := stdjson.Unmarshal([]byte(output), &result) + require.NoError(t, err) + + require.Equal(t, "REQ", result[0]) + require.Equal(t, "nak", result[1]) + + filter := result[2].(map[string]interface{}) + require.Equal(t, []interface{}{float64(1)}, filter["kinds"]) + require.Equal(t, []interface{}{"2fa2104d6b38d11b0230010559879124e42ab8dfeff5ff29dc9cdadd4ecacc3f"}, filter["authors"]) + require.Equal(t, float64(18), filter["limit"]) + require.Equal(t, []interface{}{"aec4de6d051a7c2b6ca2d087903d42051a31e07fb742f1240970084822de10a6"}, filter["#e"]) +} + +func TestMultipleFetch(t *testing.T) { + output := call(t, "nak fetch naddr1qqyrgcmyxe3kvefhqyxhwumn8ghj7mn0wvhxcmmvqgs9kqvr4dkruv3t7n2pc6e6a7v9v2s5fprmwjv4gde8c4fe5y29v0srqsqqql9ngrt6tu nevent1qyd8wumn8ghj7urewfsk66ty9enxjct5dfskvtnrdakj7qgmwaehxw309aex2mrp0yh8wetnw3jhymnzw33jucm0d5hszxthwden5te0wfjkccte9eekummjwsh8xmmrd9skctcpzamhxue69uhkzarvv9ejumn0wd68ytnvv9hxgtcqyqllp5v5j0nxr74fptqxkhvfv0h3uj870qpk3ln8a58agyxl3fka296ewr8") + + var events []nostr.Event + for _, line := range strings.Split(output, "\n") { + var evt nostr.Event + err := stdjson.Unmarshal([]byte(line), &evt) + require.NoError(t, err) + events = append(events, evt) + } + + require.Len(t, events, 2) + + // First event validation + require.Equal(t, nostr.Kind(31923), events[0].Kind) + require.Equal(t, "9ae5014573fc75ced00b343868d2cd9343ebcbbae50591c6fa8ae1cd99568f05", events[0].ID.Hex()) + require.Equal(t, "5b0183ab6c3e322bf4d41c6b3aef98562a144847b7499543727c5539a114563e", events[0].PubKey.Hex()) + require.Equal(t, nostr.Timestamp(1707764605), events[0].CreatedAt) + + // Second event validation + require.Equal(t, nostr.Kind(1), events[1].Kind) + require.Equal(t, "3ff0d19493e661faa90ac06b5d8963ef1e48fe780368fe67ed0fd410df8a6dd5", events[1].ID.Hex()) + require.Equal(t, "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d", events[1].PubKey.Hex()) + require.Equal(t, nostr.Timestamp(1710759386), events[1].CreatedAt) +} + +func TestKeyPublic(t *testing.T) { + output := call(t, "nak key public 3ff0d19493e661faa90ac06b5d8963ef1e48fe780368fe67ed0fd410df8a6dd5 3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d") + + expected := "70f7120d065870513a6bddb61c8d400ad1e43449b1900ffdb5551e4c421375c8\n718d756f60cf5179ef35b39dc6db3ff58f04c0734f81f6d4410f0b047ddf9029" + require.Equal(t, expected, output) +} + +func TestKeyDecrypt(t *testing.T) { + output := call(t, "nak key decrypt ncryptsec1qgg2gx2a7hxpsse2zulrv7m8qwccvl3mh8e9k8vtz3wpyrwuuclaq73gz7ddt5kpa93qyfhfjakguuf8uhw90jn6mszh7kqeh9mxzlyw8hy75fluzx4h75frwmu2yngsq7hx7w32d0vdyxyns5g6rqft banana") + require.Equal(t, "718d756f60cf5179ef35b39dc6db3ff58f04c0734f81f6d4410f0b047ddf9029", output) +} + +func TestReqIdFromRelay(t *testing.T) { + output := call(t, "nak req -i 20a6606ed548fe7107533cf3416ce1aa5e957c315c2a40249e12bd9873dca7da --limit 1 nos.lol") + + var evt nostr.Event + err := stdjson.Unmarshal([]byte(output), &evt) + require.NoError(t, err) + + require.Equal(t, nostr.Kind(1), evt.Kind) + require.Equal(t, "20a6606ed548fe7107533cf3416ce1aa5e957c315c2a40249e12bd9873dca7da", evt.ID.Hex()) + require.Equal(t, "dd664d5e4016433a8cd69f005ae1480804351789b59de5af06276de65633d319", evt.PubKey.Hex()) + require.Equal(t, nostr.Timestamp(1720972243), evt.CreatedAt) + require.Equal(t, "Yeah, so bizarre, but I guess most people are meant to be serfs.", evt.Content) +} + +func TestReqWithFlagsAfter1(t *testing.T) { + output := call(t, "nak req nos.lol -i 20a6606ed548fe7107533cf3416ce1aa5e957c315c2a40249e12bd9873dca7da --limit 1") + + var evt nostr.Event + err := stdjson.Unmarshal([]byte(output), &evt) + require.NoError(t, err) + + require.Equal(t, nostr.Kind(1), evt.Kind) + require.Equal(t, "20a6606ed548fe7107533cf3416ce1aa5e957c315c2a40249e12bd9873dca7da", evt.ID.Hex()) + require.Equal(t, "dd664d5e4016433a8cd69f005ae1480804351789b59de5af06276de65633d319", evt.PubKey.Hex()) + require.Equal(t, nostr.Timestamp(1720972243), evt.CreatedAt) + require.Equal(t, "Yeah, so bizarre, but I guess most people are meant to be serfs.", evt.Content) +} + +func TestReqWithFlagsAfter2(t *testing.T) { + output := call(t, "nak req -e 893d4c10f1c230240812c6bdf9ad877eed1e29e87029d153820c24680bb183b1 nostr.mom --author 2a7dcf382bcc96a393ada5c975f500393b3f7be6e466bff220aa161ad6b15eb6 --limit 1 -k 7") + + var evt nostr.Event + err := stdjson.Unmarshal([]byte(output), &evt) + require.NoError(t, err) + + require.Equal(t, nostr.Kind(7), evt.Kind) + require.Equal(t, "9b4868b068ea34ae51092807586c4541b3569d9efc23862aea48ef13de275857", evt.ID.Hex()) + require.Equal(t, "2a7dcf382bcc96a393ada5c975f500393b3f7be6e466bff220aa161ad6b15eb6", evt.PubKey.Hex()) + require.Equal(t, nostr.Timestamp(1720987327), evt.CreatedAt) + require.Equal(t, "❤️", evt.Content) +} + +func TestReqWithFlagsAfter3(t *testing.T) { + output := call(t, "nak req --limit 1 pyramid.fiatjaf.com -a 3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24 -qp 3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24 -e 9f3c1121c96edf17d84b9194f74d66d012b28c4e25b3ef190582c76b8546a188") + + var evt nostr.Event + err := stdjson.Unmarshal([]byte(output), &evt) + require.NoError(t, err) + + require.Equal(t, nostr.Kind(1), evt.Kind) + require.Equal(t, "101572c80ebdc963dab8440f6307387a3023b6d90f7e495d6c5ee1ef77045a67", evt.ID.Hex()) + require.Equal(t, "3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24", evt.PubKey.Hex()) + require.Equal(t, nostr.Timestamp(1720987305), evt.CreatedAt) + require.Equal(t, "Nope. I grew up playing in the woods. Never once saw a bear in the woods. If I did, I'd probably shiy my pants, then scream at it like I was a crazy person with my arms above my head to make me seem huge.", evt.Content) +} + +func TestNaturalTimestamps(t *testing.T) { + output := call(t, "nak event -t plu=pla -e 3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24 --ts '2018-May-19T03:37:19' -c nn") + + var evt nostr.Event + err := stdjson.Unmarshal([]byte(output), &evt) + require.NoError(t, err) + + require.Equal(t, nostr.Kind(1), evt.Kind) + require.Equal(t, "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", evt.PubKey.Hex()) + require.Equal(t, nostr.Timestamp(1526711839), evt.CreatedAt) + require.Equal(t, "nn", evt.Content) +} diff --git a/example_test.go b/example_test.go deleted file mode 100644 index e77c4eb..0000000 --- a/example_test.go +++ /dev/null @@ -1,138 +0,0 @@ -package main - -import ( - "context" -) - -// these tests are tricky because commands and flags are declared as globals and values set in one call may persist -// to the next. for example, if in the first test we set --limit 2 then doesn't specify --limit in the second then -// it will still return true for cmd.IsSet("limit") and then we will set .LimitZero = true - -var ctx = context.Background() - -func ExampleEventBasic() { - app.Run(ctx, []string{"nak", "event", "--ts", "1699485669"}) - // Output: - // {"kind":1,"id":"36d88cf5fcc449f2390a424907023eda7a74278120eebab8d02797cd92e7e29c","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1699485669,"tags":[],"content":"hello from the nostr army knife","sig":"68e71a192e8abcf8582a222434ac823ecc50607450ebe8cc4c145eb047794cc382dc3f888ce879d2f404f5ba6085a47601360a0fa2dd4b50d317bd0c6197c2c2"} -} - -// (for some reason there can only be one test dealing with stdin in the suite otherwise it halts) -// func ExampleEventParsingFromStdin() { -// prevStdin := os.Stdin -// defer func() { os.Stdin = prevStdin }() -// r, w, _ := os.Pipe() -// os.Stdin = r -// w.WriteString("{\"content\":\"hello world\"}\n{\"content\":\"hello sun\"}\n") -// app.Run(ctx, []string{"nak", "event", "-t", "t=spam", "--ts", "1699485669"}) -// // Output: -// // {"id":"bda134f9077c11973afe6aa5a1cc6f5bcea01c40d318b8f91dcb8e50507cfa52","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1699485669,"kind":1,"tags":[["t","spam"]],"content":"hello world","sig":"7552454bb8e7944230142634e3e34ac7468bad9b21ed6909da572c611018dff1d14d0792e98b5806f6330edc51e09efa6d0b66a9694dc34606c70f4e580e7493"} -// // {"id":"879c36ec73acca288825b53585389581d3836e7f0fe4d46e5eba237ca56d6af5","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1699485669,"kind":1,"tags":[["t","spam"]],"content":"hello sun","sig":"6c7e6b13ebdf931d26acfdd00bec2ec1140ddaf8d1ed61453543a14e729a460fe36c40c488ccb194a0e1ab9511cb6c36741485f501bdb93c39ca4c51bc59cbd4"} -// } - -func ExampleEventComplex() { - app.Run(ctx, []string{"nak", "event", "--ts", "1699485669", "-k", "11", "-c", "skjdbaskd", "--sec", "17", "-t", "t=spam", "-e", "36d88cf5fcc449f2390a424907023eda7a74278120eebab8d02797cd92e7e29c", "-t", "r=https://abc.def?name=foobar;nothing"}) - // Output: - // {"kind":11,"id":"19aba166dcf354bf5ef64f4afe69ada1eb851495001ee05e07d393ee8c8ea179","pubkey":"2fa2104d6b38d11b0230010559879124e42ab8dfeff5ff29dc9cdadd4ecacc3f","created_at":1699485669,"tags":[["t","spam"],["r","https://abc.def?name=foobar","nothing"],["e","36d88cf5fcc449f2390a424907023eda7a74278120eebab8d02797cd92e7e29c"]],"content":"skjdbaskd","sig":"cf452def4a68341c897c3fc96fa34dc6895a5b8cc266d4c041bcdf758ec992ec5adb8b0179e98552aaaf9450526a26d7e62e413b15b1c57e0cfc8db6b29215d7"} -} - -func ExampleEncode() { - app.Run(ctx, []string{"nak", "encode", "npub", "a6a67ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179822"}) - app.Run(ctx, []string{"nak", "encode", "nprofile", "-r", "wss://example.com", "a6a67ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179822"}) - app.Run(ctx, []string{"nak", "encode", "nprofile", "-r", "wss://example.com", "a6a67ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179822", "a5592173975ded9f836a9572ea8b11a7e16ceb66464d66d50b27163f7f039d2c"}) - // npub156n8a7wuhwk9tgrzjh8gwzc8q2dlekedec5djk0js9d3d7qhnq3qjpdq28 - // nprofile1qqs2dfn7l8wthtz45p3ftn58pvrs9xlumvkuu2xet8egzkcklqtesgspz9mhxue69uhk27rpd4cxcefwvdhk6fl5jug - // nprofile1qqs2dfn7l8wthtz45p3ftn58pvrs9xlumvkuu2xet8egzkcklqtesgspz9mhxue69uhk27rpd4cxcefwvdhk6fl5jug - // nprofile1qqs22kfpwwt4mmvlsd4f2uh23vg60ctvadnyvntx659jw93l0upe6tqpz9mhxue69uhk27rpd4cxcefwvdhk64h265a -} - -func ExampleDecode() { - app.Run(ctx, []string{"nak", "decode", "naddr1qqyrgcmyxe3kvefhqyxhwumn8ghj7mn0wvhxcmmvqgs9kqvr4dkruv3t7n2pc6e6a7v9v2s5fprmwjv4gde8c4fe5y29v0srqsqqql9ngrt6tu", "nevent1qyd8wumn8ghj7urewfsk66ty9enxjct5dfskvtnrdakj7qgmwaehxw309aex2mrp0yh8wetnw3jhymnzw33jucm0d5hszxthwden5te0wfjkccte9eekummjwsh8xmmrd9skctcpzamhxue69uhkzarvv9ejumn0wd68ytnvv9hxgtcqyqllp5v5j0nxr74fptqxkhvfv0h3uj870qpk3ln8a58agyxl3fka296ewr8"}) - // Output: - // { - // "pubkey": "5b0183ab6c3e322bf4d41c6b3aef98562a144847b7499543727c5539a114563e", - // "kind": 31923, - // "identifier": "4cd6cfe7", - // "relays": [ - // "wss://nos.lol" - // ] - // } - // { - // "id": "3ff0d19493e661faa90ac06b5d8963ef1e48fe780368fe67ed0fd410df8a6dd5", - // "relays": [ - // "wss://pyramid.fiatjaf.com/", - // "wss://relay.westernbtc.com/", - // "wss://relay.snort.social/", - // "wss://atlas.nostr.land/" - // ] - // } -} - -func ExampleDecodePubkey() { - app.Run(ctx, []string{"nak", "decode", "-p", "npub10xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqpkge6d", "npub1ccz8l9zpa47k6vz9gphftsrumpw80rjt3nhnefat4symjhrsnmjs38mnyd"}) - // Output: - // 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 - // c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5 -} - -func ExampleDecodeEventId() { - app.Run(ctx, []string{"nak", "decode", "-e", "nevent1qyd8wumn8ghj7urewfsk66ty9enxjct5dfskvtnrdakj7qgmwaehxw309aex2mrp0yh8wetnw3jhymnzw33jucm0d5hszxthwden5te0wfjkccte9eekummjwsh8xmmrd9skctcpzamhxue69uhkzarvv9ejumn0wd68ytnvv9hxgtcqyqllp5v5j0nxr74fptqxkhvfv0h3uj870qpk3ln8a58agyxl3fka296ewr8", "nevent1qqswh48lurxs8u0pll9qj2rzctvjncwhstpzlstq59rdtzlty79awns5hl5uf"}) - // Output: - // 3ff0d19493e661faa90ac06b5d8963ef1e48fe780368fe67ed0fd410df8a6dd5 - // ebd4ffe0cd03f1e1ffca092862c2d929e1d782c22fc160a146d58beb278bd74e -} - -func ExampleReq() { - app.Run(ctx, []string{"nak", "req", "-k", "1", "-l", "18", "-a", "2fa2104d6b38d11b0230010559879124e42ab8dfeff5ff29dc9cdadd4ecacc3f", "-e", "aec4de6d051a7c2b6ca2d087903d42051a31e07fb742f1240970084822de10a6"}) - // Output: - // ["REQ","nak",{"kinds":[1],"authors":["2fa2104d6b38d11b0230010559879124e42ab8dfeff5ff29dc9cdadd4ecacc3f"],"limit":18,"#e":["aec4de6d051a7c2b6ca2d087903d42051a31e07fb742f1240970084822de10a6"]}] -} - -func ExampleMultipleFetch() { - app.Run(ctx, []string{"nak", "fetch", "naddr1qqyrgcmyxe3kvefhqyxhwumn8ghj7mn0wvhxcmmvqgs9kqvr4dkruv3t7n2pc6e6a7v9v2s5fprmwjv4gde8c4fe5y29v0srqsqqql9ngrt6tu", "nevent1qyd8wumn8ghj7urewfsk66ty9enxjct5dfskvtnrdakj7qgmwaehxw309aex2mrp0yh8wetnw3jhymnzw33jucm0d5hszxthwden5te0wfjkccte9eekummjwsh8xmmrd9skctcpzamhxue69uhkzarvv9ejumn0wd68ytnvv9hxgtcqyqllp5v5j0nxr74fptqxkhvfv0h3uj870qpk3ln8a58agyxl3fka296ewr8"}) - // Output: - // {"kind":31923,"id":"9ae5014573fc75ced00b343868d2cd9343ebcbbae50591c6fa8ae1cd99568f05","pubkey":"5b0183ab6c3e322bf4d41c6b3aef98562a144847b7499543727c5539a114563e","created_at":1707764605,"tags":[["d","4cd6cfe7"],["name","Nostr PHX Presents Culture Shock"],["description","Nostr PHX presents Culture Shock the first Value 4 Value Cultural Event in Downtown Phoenix. We will showcase the power of Nostr + Bitcoin / Lightning with a full day of education, food, drinks, conversation, vendors and best of all, a live convert which will stream globally for the world to zap. "],["start","1708185600"],["end","1708228800"],["start_tzid","America/Phoenix"],["p","5b0183ab6c3e322bf4d41c6b3aef98562a144847b7499543727c5539a114563e","","host"],["location","Hello Merch, 850 W Lincoln St, Phoenix, AZ 85007, USA","Hello Merch","850 W Lincoln St, Phoenix, AZ 85007, USA"],["address","Hello Merch, 850 W Lincoln St, Phoenix, AZ 85007, USA","Hello Merch","850 W Lincoln St, Phoenix, AZ 85007, USA"],["g","9tbq1rzn"],["image","https://flockstr.s3.amazonaws.com/event/15vSaiscDhVH1KBXhA0i8"],["about","Nostr PHX presents Culture Shock : the first Value 4 Value Cultural Event in Downtown Phoenix. We will showcase the power of Nostr + Bitcoin / Lightning with a full day of education, conversation, food and goods which will be capped off with a live concert streamed globally for the world to boost \u0026 zap. \n\nWe strive to source local vendors, local artists, local partnerships. Please reach out to us if you are interested in participating in this historic event. "],["calendar","31924:5b0183ab6c3e322bf4d41c6b3aef98562a144847b7499543727c5539a114563e:1f238c94"]],"content":"Nostr PHX presents Culture Shock : the first Value 4 Value Cultural Event in Downtown Phoenix. We will showcase the power of Nostr + Bitcoin / Lightning with a full day of education, conversation, food and goods which will be capped off with a live concert streamed globally for the world to boost \u0026 zap. \n\nWe strive to source local vendors, local artists, local partnerships. Please reach out to us if you are interested in participating in this historic event. ","sig":"f676629d1414d96b464644de6babde0c96bd21ef9b41ba69ad886a1d13a942b855b715b22ccf38bc07fead18d3bdeee82d9e3825cf6f003fb5ff1766d95c70a0"} - // {"kind":1,"id":"3ff0d19493e661faa90ac06b5d8963ef1e48fe780368fe67ed0fd410df8a6dd5","pubkey":"3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d","created_at":1710759386,"tags":[],"content":"Nostr was coopted by our the corporate overlords. It is now featured in https://www.iana.org/assignments/well-known-uris/well-known-uris.xhtml.","sig":"faaec167cca4de50b562b7702e8854e2023f0ccd5f36d1b95b6eac20d352206342d6987e9516d283068c768e94dbe8858e2990c3e05405e707fb6fb771ef92f9"} -} - -func ExampleKeyPublic() { - app.Run(ctx, []string{"nak", "key", "public", "3ff0d19493e661faa90ac06b5d8963ef1e48fe780368fe67ed0fd410df8a6dd5", "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"}) - // Output: - // 70f7120d065870513a6bddb61c8d400ad1e43449b1900ffdb5551e4c421375c8 - // 718d756f60cf5179ef35b39dc6db3ff58f04c0734f81f6d4410f0b047ddf9029 -} - -func ExampleKeyDecrypt() { - app.Run(ctx, []string{"nak", "key", "decrypt", "ncryptsec1qgg2gx2a7hxpsse2zulrv7m8qwccvl3mh8e9k8vtz3wpyrwuuclaq73gz7ddt5kpa93qyfhfjakguuf8uhw90jn6mszh7kqeh9mxzlyw8hy75fluzx4h75frwmu2yngsq7hx7w32d0vdyxyns5g6rqft", "banana"}) - // Output: - // 718d756f60cf5179ef35b39dc6db3ff58f04c0734f81f6d4410f0b047ddf9029 -} - -func ExampleReqIdFromRelay() { - app.Run(ctx, []string{"nak", "req", "-i", "20a6606ed548fe7107533cf3416ce1aa5e957c315c2a40249e12bd9873dca7da", "--limit", "1", "nos.lol"}) - // Output: - // {"kind":1,"id":"20a6606ed548fe7107533cf3416ce1aa5e957c315c2a40249e12bd9873dca7da","pubkey":"dd664d5e4016433a8cd69f005ae1480804351789b59de5af06276de65633d319","created_at":1720972243,"tags":[["e","bdb2210fe6d9c4b141f08b5d9d1147cd8e1dc1d82f552a889ab171894249d21d","","root"],["e","c2e45f09e7d62ed12afe2b8b1bcf6be823b560a53ef06905365a78979a1b9ee3","","reply"],["p","036533caa872376946d4e4fdea4c1a0441eda38ca2d9d9417bb36006cbaabf58","","mention"]],"content":"Yeah, so bizarre, but I guess most people are meant to be serfs.","sig":"9ea7488415c250d0ac8fcb2219f211cb369dddf2a75c0f63d2db773c6dc1ef9dd9679b8941c0e7551744ea386afebad2024be8ce3ac418d4f47c95e7491af38e"} -} - -func ExampleReqWithFlagsAfter1() { - app.Run(ctx, []string{"nak", "req", "nos.lol", "-i", "20a6606ed548fe7107533cf3416ce1aa5e957c315c2a40249e12bd9873dca7da", "--limit", "1"}) - // Output: - // {"kind":1,"id":"20a6606ed548fe7107533cf3416ce1aa5e957c315c2a40249e12bd9873dca7da","pubkey":"dd664d5e4016433a8cd69f005ae1480804351789b59de5af06276de65633d319","created_at":1720972243,"tags":[["e","bdb2210fe6d9c4b141f08b5d9d1147cd8e1dc1d82f552a889ab171894249d21d","","root"],["e","c2e45f09e7d62ed12afe2b8b1bcf6be823b560a53ef06905365a78979a1b9ee3","","reply"],["p","036533caa872376946d4e4fdea4c1a0441eda38ca2d9d9417bb36006cbaabf58","","mention"]],"content":"Yeah, so bizarre, but I guess most people are meant to be serfs.","sig":"9ea7488415c250d0ac8fcb2219f211cb369dddf2a75c0f63d2db773c6dc1ef9dd9679b8941c0e7551744ea386afebad2024be8ce3ac418d4f47c95e7491af38e"} -} - -func ExampleReqWithFlagsAfter2() { - app.Run(ctx, []string{"nak", "req", "-e", "893d4c10f1c230240812c6bdf9ad877eed1e29e87029d153820c24680bb183b1", "nostr.mom", "--author", "2a7dcf382bcc96a393ada5c975f500393b3f7be6e466bff220aa161ad6b15eb6", "--limit", "1", "-k", "7"}) - // Output: - // {"kind":7,"id":"9b4868b068ea34ae51092807586c4541b3569d9efc23862aea48ef13de275857","pubkey":"2a7dcf382bcc96a393ada5c975f500393b3f7be6e466bff220aa161ad6b15eb6","created_at":1720987327,"tags":[["e","893d4c10f1c230240812c6bdf9ad877eed1e29e87029d153820c24680bb183b1"],["p","1e978baae414eee990dba992871549ad4a099b9d6f7e71c8059b254ea024dddc"],["k","1"]],"content":"❤️","sig":"7eddd112c642ecdb031330dadc021790642b3c10ecc64158ba3ae63edd798b26afb9b5a3bba72835ce171719a724de1472f65c9b3339b6bead0ce2846f93dfc9"} -} - -func ExampleReqWithFlagsAfter3() { - app.Run(ctx, []string{"nak", "req", "--limit", "1", "pyramid.fiatjaf.com", "-a", "3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24", "-qp", "3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24", "-e", "9f3c1121c96edf17d84b9194f74d66d012b28c4e25b3ef190582c76b8546a188"}) - // Output: - // {"kind":1,"id":"101572c80ebdc963dab8440f6307387a3023b6d90f7e495d6c5ee1ef77045a67","pubkey":"3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24","created_at":1720987305,"tags":[["e","ceacdc29fa7a0b51640b30d2424e188215460617db5ba5bb52d3fbf0094eebb3","","root"],["e","9f3c1121c96edf17d84b9194f74d66d012b28c4e25b3ef190582c76b8546a188","","reply"],["p","3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24"],["p","6b96c3eb36c6cd457d906bbaafe7b36cacfb8bcc4ab235be6eab3b71c6669251"]],"content":"Nope. I grew up playing in the woods. Never once saw a bear in the woods. If I did, I'd probably shiy my pants, then scream at it like I was a crazy person with my arms above my head to make me seem huge.","sig":"b098820b4a5635865cada9f9a5813be2bc6dd7180e16e590cf30e07916d8ed6ed98ab38b64f3bfba12d88d37335f229f7ef8c084bc48132e936c664a54d3e650"} -} - -func ExampleNaturalTimestamps() { - app.Run(ctx, []string{"nak", "event", "-t", "plu=pla", "-e", "3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24", "--ts", "May 19 2018 03:37:19", "-c", "nn"}) - // Output: - // {"kind":0,"id":"b10da0095f96aa2accd99fa3d93bf29a76f51d2594cf5a0a52f8e961aecd0b67","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1526711839,"tags":[["plu","pla"],["e","3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24"]],"content":"nn","sig":"988442c97064a041ba5e2bfbd64e84d3f819b2169e865511d9d53e74667949ff165325942acaa2ca233c8b529adedf12cf44088cf04081b56d098c5f4d52dd8f"} -} diff --git a/helpers_key.go b/helpers_key.go index 972c11c..a2b731f 100644 --- a/helpers_key.go +++ b/helpers_key.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "os" + "runtime/debug" "strings" "fiatjaf.com/nostr" @@ -110,7 +111,8 @@ func gatherSecretKeyOrBunkerFromArguments(ctx context.Context, c *cli.Command) ( sk, err := nostr.SecretKeyFromHex(sec) if err != nil { - return nostr.SecretKey{}, nil, fmt.Errorf("invalid secret key") + debug.PrintStack() + return nostr.SecretKey{}, nil, fmt.Errorf("invalid secret key: %w", err) } return sk, nil, nil diff --git a/justfile b/justfile new file mode 100644 index 0000000..628193f --- /dev/null +++ b/justfile @@ -0,0 +1,5 @@ +test: + #!/usr/bin/env fish + for test in (go test -list .) + go test -run=$test -v + end diff --git a/key.go b/key.go index 2d5c115..dd64bb1 100644 --- a/key.go +++ b/key.go @@ -122,30 +122,30 @@ var decryptKey = &cli.Command{ if password == "" { return fmt.Errorf("no password given") } - sec, err := nip49.Decrypt(ncryptsec, password) + sk, err := nip49.Decrypt(ncryptsec, password) if err != nil { return fmt.Errorf("failed to decrypt: %s", err) } - stdout(sec) + stdout(sk.Hex()) return nil case 1: if arg := c.Args().Get(0); strings.HasPrefix(arg, "ncryptsec1") { ncryptsec = arg - if res, err := promptDecrypt(ncryptsec); err != nil { + if sk, err := promptDecrypt(ncryptsec); err != nil { return err } else { - stdout(res) + stdout(sk.Hex()) return nil } } else { password = c.Args().Get(0) for ncryptsec := range getStdinLinesOrArgumentsFromSlice([]string{ncryptsec}) { - sec, err := nip49.Decrypt(ncryptsec, password) + sk, err := nip49.Decrypt(ncryptsec, password) if err != nil { ctx = lineProcessingError(ctx, "failed to decrypt: %s", err) continue } - stdout(sec) + stdout(sk.Hex()) } return nil } From 4d12550d74ffb55b7ff65ec43990129a07c7bcfb Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 22 Apr 2025 08:38:00 -0300 Subject: [PATCH 256/401] bunker: cosmetic fixes. --- bunker.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bunker.go b/bunker.go index 2f412dc..cafce16 100644 --- a/bunker.go +++ b/bunker.go @@ -84,7 +84,7 @@ var bunker = &cli.Command{ // this function will be called every now and then printBunkerInfo := func() { qs.Set("secret", newSecret) - bunkerURI := fmt.Sprintf("bunker://%s?%s", pubkey, qs.Encode()) + bunkerURI := fmt.Sprintf("bunker://%s?%s", pubkey.Hex(), qs.Encode()) authorizedKeysStr := "" if len(authorizedKeys) != 0 { @@ -129,7 +129,7 @@ var bunker = &cli.Command{ log("listening at %v:\n pubkey: %s \n npub: %s%s%s\n to restart: %s\n bunker: %s\n\n", colors.bold(relayURLs), - colors.bold(pubkey), + colors.bold(pubkey.Hex()), colors.bold(npub), authorizedKeysStr, authorizedSecretsStr, @@ -187,7 +187,7 @@ var bunker = &cli.Command{ } jreq, _ := json.MarshalIndent(req, "", " ") - log("- got request from '%s': %s\n", color.New(color.Bold, color.FgBlue).Sprint(ie.Event.PubKey), string(jreq)) + log("- got request from '%s': %s\n", color.New(color.Bold, color.FgBlue).Sprint(ie.Event.PubKey.Hex()), string(jreq)) jresp, _ := json.MarshalIndent(resp, "", " ") log("~ responding with %s\n", string(jresp)) From 8fba611ad027dff09bf0fb174d93a2cb6b97399b Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 22 Apr 2025 15:32:48 -0300 Subject: [PATCH 257/401] mcp: make search return multiple users and also their name and description. --- mcp.go | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/mcp.go b/mcp.go index 96e847a..ddade00 100644 --- a/mcp.go +++ b/mcp.go @@ -160,14 +160,33 @@ var mcpServer = &cli.Command{ s.AddTool(mcp.NewTool("search_profile", mcp.WithDescription("Search for the public key of a Nostr user given their name"), mcp.WithString("name", mcp.Description("Name to be searched"), mcp.Required()), + mcp.WithNumber("limit", mcp.Description("How many results to return")), ), 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: []nostr.Kind{0}}, nostr.SubscriptionOptions{}) - if re == nil { - return mcp.NewToolResultError("couldn't find anyone with that name"), nil + limit, _ := optional[float64](r, "limit") + + filter := nostr.Filter{Search: name, Kinds: []nostr.Kind{0}} + if limit > 0 { + filter.Limit = int(limit) } - return mcp.NewToolResultText(re.PubKey.Hex()), nil + res := strings.Builder{} + res.WriteString("Search results: ") + l := 0 + for result := range sys.Pool.FetchMany(ctx, []string{"relay.nostr.band", "nostr.wine"}, filter, nostr.SubscriptionOptions{}) { + l++ + pm, _ := sdk.ParseMetadata(result.Event) + res.WriteString(fmt.Sprintf("\n\nResult %d\nUser name: \"%s\"\nPublic key: \"%s\"\nDescription: \"%s\"\n", + l, pm.ShortName, pm.PubKey.Hex(), pm.About)) + + if l >= int(limit) { + break + } + } + if l == 0 { + return mcp.NewToolResultError("Couldn't find anyone with that name."), nil + } + return mcp.NewToolResultText(res.String()), nil }) s.AddTool(mcp.NewTool("get_outbox_relay_for_pubkey", From 024111a8be57f3010ae3da57e7f42087938cca13 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Thu, 24 Apr 2025 13:22:44 -0300 Subject: [PATCH 258/401] fix bunker client key variable. --- helpers_key.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/helpers_key.go b/helpers_key.go index a2b731f..386b8d3 100644 --- a/helpers_key.go +++ b/helpers_key.go @@ -56,8 +56,6 @@ func gatherKeyerFromArguments(ctx context.Context, c *cli.Command) (nostr.Keyer, } 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 @@ -66,7 +64,11 @@ func gatherSecretKeyOrBunkerFromArguments(ctx context.Context, c *cli.Command) ( var clientKey nostr.SecretKey if clientKeyHex != "" { - clientKey, err = nostr.SecretKeyFromHex(sec) + var err error + clientKey, err = nostr.SecretKeyFromHex(clientKeyHex) + if err != nil { + return nostr.SecretKey{}, nil, fmt.Errorf("bunker client key '%s' is invalid: %w", clientKeyHex, err) + } } else { clientKey = nostr.Generate() } @@ -91,6 +93,7 @@ func gatherSecretKeyOrBunkerFromArguments(ctx context.Context, c *cli.Command) ( if isPiped() { return nostr.SecretKey{}, nil, fmt.Errorf("can't prompt for a secret key when processing data from a pipe, try again without --prompt-sec") } + var err error sec, err = askPassword("type your secret key as ncryptsec, nsec or hex: ", nil) if err != nil { return nostr.SecretKey{}, nil, fmt.Errorf("failed to get secret key: %w", err) From 148f6e8bcb76f98bcbd5e58667894ff432aada85 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Fri, 25 Apr 2025 12:45:38 -0300 Subject: [PATCH 259/401] remove cruft and comments from flags.go --- flags.go | 83 ++++++++------------------------------------------------ 1 file changed, 11 insertions(+), 72 deletions(-) diff --git a/flags.go b/flags.go index c3c18f4..7e4e32d 100644 --- a/flags.go +++ b/flags.go @@ -11,13 +11,8 @@ import ( "github.com/urfave/cli/v3" ) -// -// -// - type NaturalTimeFlag = cli.FlagBase[nostr.Timestamp, struct{}, naturalTimeValue] -// wrap to satisfy flag interface. type naturalTimeValue struct { timestamp *nostr.Timestamp hasBeenSet bool @@ -25,8 +20,6 @@ type naturalTimeValue struct { var _ cli.ValueCreator[nostr.Timestamp, struct{}] = naturalTimeValue{} -// Below functions are to satisfy the ValueCreator interface - func (t naturalTimeValue) Create(val nostr.Timestamp, p *nostr.Timestamp, c struct{}) cli.Value { *p = val return &naturalTimeValue{ @@ -36,16 +29,12 @@ func (t naturalTimeValue) Create(val nostr.Timestamp, p *nostr.Timestamp, c stru func (t naturalTimeValue) ToString(b nostr.Timestamp) string { ts := b.Time() - if ts.IsZero() { return "" } return fmt.Sprintf("%v", ts) } -// Below functions are to satisfy the flag.Value interface - -// Parses the string value to timestamp func (t *naturalTimeValue) Set(value string) error { var ts time.Time if n, err := strconv.ParseInt(value, 10, 64); err == nil { @@ -74,20 +63,9 @@ func (t *naturalTimeValue) Set(value string) error { return nil } -// String returns a readable representation of this value (for usage defaults) -func (t *naturalTimeValue) String() string { - return fmt.Sprintf("%#v", t.timestamp) -} - -// Value returns the timestamp value stored in the flag -func (t *naturalTimeValue) Value() *nostr.Timestamp { - return t.timestamp -} - -// Get returns the flag structure -func (t *naturalTimeValue) Get() any { - return *t.timestamp -} +func (t *naturalTimeValue) String() string { return fmt.Sprintf("%#v", t.timestamp) } +func (t *naturalTimeValue) Value() *nostr.Timestamp { return t.timestamp } +func (t *naturalTimeValue) Get() any { return *t.timestamp } func getNaturalDate(cmd *cli.Command, name string) nostr.Timestamp { return cmd.Value(name).(nostr.Timestamp) @@ -101,7 +79,6 @@ type ( PubKeyFlag = cli.FlagBase[nostr.PubKey, struct{}, pubkeyValue] ) -// wrap to satisfy flag interface. type pubkeyValue struct { pubkey nostr.PubKey hasBeenSet bool @@ -109,8 +86,6 @@ type pubkeyValue struct { 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{ @@ -118,13 +93,8 @@ func (t pubkeyValue) Create(val nostr.PubKey, p *nostr.PubKey, c struct{}) cli.V } } -func (t pubkeyValue) ToString(b nostr.PubKey) string { - return t.pubkey.String() -} +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 @@ -132,20 +102,9 @@ func (t *pubkeyValue) Set(value string) error { 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 (t *pubkeyValue) String() string { return fmt.Sprintf("%#v", t.pubkey) } +func (t *pubkeyValue) Value() nostr.PubKey { return t.pubkey } +func (t *pubkeyValue) Get() any { return t.pubkey } func getPubKey(cmd *cli.Command, name string) nostr.PubKey { return cmd.Value(name).(nostr.PubKey) @@ -172,7 +131,6 @@ type ( IDFlag = cli.FlagBase[nostr.ID, struct{}, idValue] ) -// wrap to satisfy flag interface. type idValue struct { id nostr.ID hasBeenSet bool @@ -180,22 +138,14 @@ type idValue struct { 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() } -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 @@ -203,20 +153,9 @@ func (t *idValue) Set(value string) error { 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 (t *idValue) String() string { return fmt.Sprintf("%#v", t.id) } +func (t *idValue) Value() nostr.ID { return t.id } +func (t *idValue) Get() any { return t.id } func getID(cmd *cli.Command, name string) nostr.ID { return cmd.Value(name).(nostr.ID) From e91a454fc0a5b1792b14897bb53a89100db886ff Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Fri, 25 Apr 2025 13:30:32 -0300 Subject: [PATCH 260/401] nak encode that takes json from stdin. --- encode.go | 76 ++++++++++++++++++++++++++++++++++++++++++------------ helpers.go | 20 ++++++++++++++ 2 files changed, 80 insertions(+), 16 deletions(-) diff --git a/encode.go b/encode.go index c5f9db9..59c5a58 100644 --- a/encode.go +++ b/encode.go @@ -18,14 +18,68 @@ var encode = &cli.Command{ nak encode nprofile --relay nak encode nevent nak encode nevent --author --relay --relay - nak encode nsec `, - Before: func(ctx context.Context, c *cli.Command) (context.Context, error) { - if c.Args().Len() < 1 { - return ctx, fmt.Errorf("expected more than 1 argument.") - } - return ctx, nil + nak encode nsec + echo '{"pubkey":"7b225d32d3edb978dba1adfd9440105646babbabbda181ea383f74ba53c3be19","relays":["wss://nada.zero"]}' | nak encode + echo '{ + "id":"7b225d32d3edb978dba1adfd9440105646babbabbda181ea383f74ba53c3be19" + "relays":["wss://nada.zero"], + "author":"ebb6ff85430705651b311ed51328767078fd790b14f02d22efba68d5513376bc" + } | nak encode`, + Flags: []cli.Flag{ + &cli.StringSliceFlag{ + Name: "relay", + Aliases: []string{"r"}, + Usage: "attach relay hints to naddr code", + }, }, DisableSliceFlagSeparator: true, + Action: func(ctx context.Context, c *cli.Command) error { + if c.Args().Len() != 0 { + return nil + } + + relays := c.StringSlice("relay") + if err := normalizeAndValidateRelayURLs(relays); err != nil { + return err + } + + hasStdin := false + for jsonStr := range getJsonsOrBlank() { + if jsonStr == "{}" { + hasStdin = false + continue + } else { + hasStdin = true + } + + var eventPtr nostr.EventPointer + if err := json.Unmarshal([]byte(jsonStr), &eventPtr); err == nil && eventPtr.ID != nostr.ZeroID { + stdout(nip19.EncodeNevent(eventPtr.ID, appendUnique(relays, eventPtr.Relays...), eventPtr.Author)) + continue + } + + var profilePtr nostr.ProfilePointer + if err := json.Unmarshal([]byte(jsonStr), &profilePtr); err == nil && profilePtr.PublicKey != nostr.ZeroPK { + stdout(nip19.EncodeNprofile(profilePtr.PublicKey, appendUnique(relays, profilePtr.Relays...))) + continue + } + + var entityPtr nostr.EntityPointer + if err := json.Unmarshal([]byte(jsonStr), &entityPtr); err == nil && entityPtr.PublicKey != nostr.ZeroPK { + stdout(nip19.EncodeNaddr(entityPtr.PublicKey, entityPtr.Kind, entityPtr.Identifier, appendUnique(relays, entityPtr.Relays...))) + continue + } + + ctx = lineProcessingError(ctx, "couldn't decode JSON '%s'", jsonStr) + } + + if !hasStdin { + return nil + } + + exitIfLineProcessingError(ctx) + return nil + }, Commands: []*cli.Command{ { Name: "npub", @@ -100,11 +154,6 @@ var encode = &cli.Command{ Name: "nevent", Usage: "generate event codes with optionally attached relay information", Flags: []cli.Flag{ - &cli.StringSliceFlag{ - Name: "relay", - Aliases: []string{"r"}, - Usage: "attach relay hints to nevent code", - }, &PubKeyFlag{ Name: "author", Aliases: []string{"a"}, @@ -155,11 +204,6 @@ var encode = &cli.Command{ Usage: "kind of referred replaceable event", Required: true, }, - &cli.StringSliceFlag{ - Name: "relay", - Aliases: []string{"r"}, - Usage: "attach relay hints to naddr code", - }, }, DisableSliceFlagSeparator: true, Action: func(ctx context.Context, c *cli.Command) error { diff --git a/helpers.go b/helpers.go index c9f542c..a154d81 100644 --- a/helpers.go +++ b/helpers.go @@ -49,6 +49,7 @@ func isPiped() bool { func getJsonsOrBlank() iter.Seq[string] { var curr strings.Builder + var finalJsonErr error return func(yield func(string) bool) { hasStdin := writeStdinLinesOrNothing(func(stdinLine string) bool { // we're look for an event, but it may be in multiple lines, so if json parsing fails @@ -58,8 +59,10 @@ func getJsonsOrBlank() iter.Seq[string] { var dummy any if err := json.Unmarshal([]byte(stdinEvent), &dummy); err != nil { + finalJsonErr = err return true } + finalJsonErr = nil if !yield(stdinEvent) { return false @@ -72,6 +75,10 @@ func getJsonsOrBlank() iter.Seq[string] { if !hasStdin { yield("{}") } + + if finalJsonErr != nil { + log(color.YellowString("stdin json parse error: %s", finalJsonErr)) + } } } @@ -388,6 +395,19 @@ func clampError(err error, prefixAlreadyPrinted int) string { return msg } +func appendUnique[A comparable](list []A, newEls ...A) []A { +ex: + for _, newEl := range newEls { + for _, el := range list { + if el == newEl { + continue ex + } + } + list = append(list, newEl) + } + return list +} + var colors = struct { reset func(...any) (int, error) italic func(...any) string From 3005c6256682eaec51765b6f29f6931e8e52da6f Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sat, 3 May 2025 07:22:08 -0300 Subject: [PATCH 261/401] blossom method name update. --- blossom.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blossom.go b/blossom.go index eb4b168..1ba9aa9 100644 --- a/blossom.go +++ b/blossom.go @@ -75,7 +75,7 @@ var blossomCmd = &cli.Command{ hasError := false for _, fpath := range c.Args().Slice() { - bd, err := client.UploadFile(ctx, fpath) + bd, err := client.UploadFilePath(ctx, fpath) if err != nil { fmt.Fprintf(os.Stderr, "%s\n", err) hasError = true From f98bd7483f9f12b9d9267d78db80b4aa59b34574 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 4 Apr 2025 11:34:23 -0500 Subject: [PATCH 262/401] allow --prompt-sec to be used with pipes --- go.mod | 1 + go.sum | 2 ++ helpers_key.go | 69 +++++++++++++++++++++++++++++++++++--------------- 3 files changed, 51 insertions(+), 21 deletions(-) diff --git a/go.mod b/go.mod index 5dc9581..1d53d7e 100644 --- a/go.mod +++ b/go.mod @@ -59,6 +59,7 @@ require ( github.com/magefile/mage v1.14.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-tty v0.0.7 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pkg/errors v0.9.1 // indirect diff --git a/go.sum b/go.sum index a33ff50..4206862 100644 --- a/go.sum +++ b/go.sum @@ -171,6 +171,8 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-tty v0.0.7 h1:KJ486B6qI8+wBO7kQxYgmmEFDaFEE96JMBQ7h400N8Q= +github.com/mattn/go-tty v0.0.7/go.mod h1:f2i5ZOvXBU/tCABmLmOfzLz9azMo5wdAaElRNnJKr+k= github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78= github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= diff --git a/helpers_key.go b/helpers_key.go index 386b8d3..ccccc8b 100644 --- a/helpers_key.go +++ b/helpers_key.go @@ -14,6 +14,7 @@ import ( "fiatjaf.com/nostr/nip49" "github.com/chzyer/readline" "github.com/fatih/color" + "github.com/mattn/go-tty" "github.com/urfave/cli/v3" ) @@ -90,9 +91,6 @@ func gatherSecretKeyOrBunkerFromArguments(ctx context.Context, c *cli.Command) ( } if c.Bool("prompt-sec") { - if isPiped() { - return nostr.SecretKey{}, nil, fmt.Errorf("can't prompt for a secret key when processing data from a pipe, try again without --prompt-sec") - } var err error sec, err = askPassword("type your secret key as ncryptsec, nsec or hex: ", nil) if err != nil { @@ -141,29 +139,58 @@ func promptDecrypt(ncryptsec string) (nostr.SecretKey, error) { } func askPassword(msg string, shouldAskAgain func(answer string) bool) (string, error) { - config := &readline.Config{ - Stdout: color.Error, - Prompt: color.YellowString(msg), - InterruptPrompt: "^C", - DisableAutoSaveHistory: true, - EnableMask: true, - MaskRune: '*', - } + if isPiped() { + // use TTY method when stdin is piped + tty, err := tty.Open() + if err != nil { + return "", fmt.Errorf("can't prompt for a secret key when processing data from a pipe on this system (failed to open /dev/tty: %w), try again without --prompt-sec or provide the key via --sec or NOSTR_SECRET_KEY environment variable", err) + } + defer tty.Close() + for { + // print the prompt to stderr so it's visible to the user + fmt.Fprintf(os.Stderr, color.YellowString(msg)) - rl, err := readline.NewEx(config) - if err != nil { - return "", err - } + // read password from TTY with masking + password, err := tty.ReadPassword() + if err != nil { + return "", err + } - for { - answer, err := rl.Readline() + // print newline after password input + fmt.Fprintln(os.Stderr) + + answer := strings.TrimSpace(string(password)) + if shouldAskAgain != nil && shouldAskAgain(answer) { + continue + } + return answer, nil + } + } else { + // use normal readline method when stdin is not piped + config := &readline.Config{ + Stdout: os.Stderr, + Prompt: color.YellowString(msg), + InterruptPrompt: "^C", + DisableAutoSaveHistory: true, + EnableMask: true, + MaskRune: '*', + } + + rl, err := readline.NewEx(config) if err != nil { return "", err } - answer = strings.TrimSpace(answer) - if shouldAskAgain != nil && shouldAskAgain(answer) { - continue + + for { + answer, err := rl.Readline() + if err != nil { + return "", err + } + answer = strings.TrimSpace(answer) + if shouldAskAgain != nil && shouldAskAgain(answer) { + continue + } + return answer, err } - return answer, err } } From 02f22a8c2faa5b4c685aabf25df78a805c5fce9f Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sat, 3 May 2025 21:44:59 -0300 Subject: [PATCH 263/401] nak event --confirm --- event.go | 16 ++++++++++++++++ helpers.go | 45 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/event.go b/event.go index 905e313..be14ce7 100644 --- a/event.go +++ b/event.go @@ -129,6 +129,11 @@ example: Value: nostr.Now(), Category: CATEGORY_EVENT_FIELDS, }, + &cli.BoolFlag{ + Name: "confirm", + Usage: "ask before publishing the event", + Category: CATEGORY_EXTRAS, + }, ), ArgsUsage: "[relay...]", Action: func(ctx context.Context, c *cli.Command) error { @@ -314,6 +319,17 @@ example: if len(relays) > 0 { os.Stdout.Sync() + if c.Bool("confirm") { + relaysStr := make([]string, len(relays)) + for i, r := range relays { + relaysStr[i] = strings.ToLower(strings.Split(r.URL, "://")[1]) + } + time.Sleep(time.Millisecond * 10) + if !askConfirmation("publish to [ " + strings.Join(relaysStr, " ") + " ]? ") { + return nil + } + } + if supportsDynamicMultilineMagic() { // overcomplicated multiline rendering magic ctx, cancel := context.WithTimeout(ctx, 10*time.Second) diff --git a/helpers.go b/helpers.go index a154d81..903c879 100644 --- a/helpers.go +++ b/helpers.go @@ -21,8 +21,10 @@ import ( "fiatjaf.com/nostr/nip19" "fiatjaf.com/nostr/nip42" "fiatjaf.com/nostr/sdk" + "github.com/chzyer/readline" "github.com/fatih/color" jsoniter "github.com/json-iterator/go" + "github.com/mattn/go-tty" "github.com/urfave/cli/v3" "golang.org/x/term" ) @@ -408,6 +410,49 @@ ex: return list } +func askConfirmation(msg string) bool { + if isPiped() { + tty, err := tty.Open() + if err != nil { + return false + } + defer tty.Close() + + fmt.Fprintf(os.Stderr, color.YellowString(msg)) + answer, err := tty.ReadString() + if err != nil { + return false + } + + // print newline after password input + fmt.Fprintln(os.Stderr) + + answer = strings.TrimSpace(string(answer)) + return answer == "y" || answer == "yes" + } else { + config := &readline.Config{ + Stdout: color.Error, + Prompt: color.YellowString(msg), + InterruptPrompt: "^C", + DisableAutoSaveHistory: true, + EnableMask: false, + MaskRune: '*', + } + + rl, err := readline.NewEx(config) + if err != nil { + return false + } + + answer, err := rl.Readline() + if err != nil { + return false + } + answer = strings.ToLower(strings.TrimSpace(answer)) + return answer == "y" || answer == "yes" + } +} + var colors = struct { reset func(...any) (int, error) italic func(...any) string From 9055f98f6637dca9583f4d98e77b4c4d5d900ab4 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sat, 3 May 2025 21:45:28 -0300 Subject: [PATCH 264/401] use color.Output and color.Error instead of os.Stdout and os.Stderr in some places. --- blossom.go | 2 +- helpers.go | 2 +- main.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/blossom.go b/blossom.go index 1ba9aa9..a5ae8aa 100644 --- a/blossom.go +++ b/blossom.go @@ -130,7 +130,7 @@ var blossomCmd = &cli.Command{ hasError = true continue } - os.Stdout.Write(data) + stdout(data) } } diff --git a/helpers.go b/helpers.go index 903c879..1884556 100644 --- a/helpers.go +++ b/helpers.go @@ -40,7 +40,7 @@ const ( var ( log = func(msg string, args ...any) { fmt.Fprintf(color.Error, msg, args...) } logverbose = func(msg string, args ...any) {} // by default do nothing - stdout = fmt.Println + stdout = func(args ...any) { fmt.Fprintln(color.Output, args...) } ) func isPiped() bool { diff --git a/main.go b/main.go index 84e6acf..f0ddfb4 100644 --- a/main.go +++ b/main.go @@ -60,7 +60,7 @@ var app = &cli.Command{ if q >= 1 { log = func(msg string, args ...any) {} if q >= 2 { - stdout = func(_ ...any) (int, error) { return 0, nil } + stdout = func(_ ...any) {} } } return nil From f9033f778d091c8e0eeec8047d306e28e1de3235 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Mon, 5 May 2025 16:57:05 -0300 Subject: [PATCH 265/401] adapt wallet to upstream changes. --- wallet.go | 47 ++++++++++++++++++++++++----------------------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/wallet.go b/wallet.go index 615998c..cc73424 100644 --- a/wallet.go +++ b/wallet.go @@ -25,7 +25,7 @@ func prepareWallet(ctx context.Context, c *cli.Command) (*nip60.Wallet, func(), } relays := sys.FetchOutboxRelays(ctx, pk, 3) - w := nip60.LoadWallet(ctx, kr, sys.Pool, relays) + w := nip60.LoadWallet(ctx, kr, sys.Pool, relays, nip60.WalletOptions{}) if w == nil { return nil, nil, fmt.Errorf("error loading walle") } @@ -200,12 +200,9 @@ var wallet = &cli.Command{ return err } - opts := make([]nip60.ReceiveOption, 0, 1) - for _, url := range c.StringSlice("mint") { - opts = append(opts, nip60.WithMintDestination(url)) - } - - if err := w.Receive(ctx, proofs, mint, opts...); err != nil { + if err := w.Receive(ctx, proofs, mint, nip60.ReceiveOptions{ + IntoMint: c.StringSlice("mint"), + }); err != nil { return err } @@ -239,12 +236,13 @@ var wallet = &cli.Command{ return err } - opts := make([]nip60.SendOption, 0, 1) + var sourceMint string if mint := c.String("mint"); mint != "" { - mint = "http" + nostr.NormalizeURL(mint)[2:] - opts = append(opts, nip60.WithMint(mint)) + sourceMint = "http" + nostr.NormalizeURL(mint)[2:] } - proofs, mint, err := w.Send(ctx, amount, opts...) + proofs, mint, err := w.Send(ctx, amount, nip60.SendOptions{ + SpecificSourceMint: sourceMint, + }) if err != nil { return err } @@ -277,13 +275,14 @@ var wallet = &cli.Command{ return err } - opts := make([]nip60.SendOption, 0, 1) + var sourceMint string if mint := c.String("mint"); mint != "" { - mint = "http" + nostr.NormalizeURL(mint)[2:] - opts = append(opts, nip60.WithMint(mint)) + sourceMint = "http" + nostr.NormalizeURL(mint)[2:] } - preimage, err := w.PayBolt11(ctx, args[0], opts...) + preimage, err := w.PayBolt11(ctx, args[0], nip60.PayOptions{ + FromMint: sourceMint, + }) if err != nil { return err } @@ -350,10 +349,9 @@ var wallet = &cli.Command{ log("sending %d sat to '%s' (%s)", amount, pm.ShortName(), pm.Npub()) - opts := make([]nip60.SendOption, 0, 1) + var sourceMint string if mint := c.String("mint"); mint != "" { - mint = "http" + nostr.NormalizeURL(mint)[2:] - opts = append(opts, nip60.WithMint(mint)) + sourceMint = "http" + nostr.NormalizeURL(mint)[2:] } kr, _, _ := gatherKeyerFromArguments(ctx, c) @@ -362,12 +360,15 @@ var wallet = &cli.Command{ kr, w, sys.Pool, - pm.PubKey, - sys.FetchInboxRelays, - sys.FetchOutboxRelays(ctx, pm.PubKey, 3), - eventId, uint64(amount), - c.String("message"), + pm.PubKey, + sys.FetchWriteRelays(ctx, pm.PubKey), + nip61.NutzapOptions{ + Message: c.String("message"), + SendToRelays: sys.FetchInboxRelays(ctx, pm.PubKey, 3), + EventID: eventId, + SpecificSourceMint: sourceMint, + }, ) if err != nil { return err From 83195d9a0025f1990a5c739bf82756061e670420 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 6 May 2025 00:05:50 -0300 Subject: [PATCH 266/401] fs: use sdk/PrepareNoteEvent() when publishing. --- nostrfs/viewdir.go | 36 +++--------------------------------- 1 file changed, 3 insertions(+), 33 deletions(-) diff --git a/nostrfs/viewdir.go b/nostrfs/viewdir.go index 4f992fb..cc094e2 100644 --- a/nostrfs/viewdir.go +++ b/nostrfs/viewdir.go @@ -2,15 +2,12 @@ package nostrfs import ( "context" - "slices" "strings" "sync/atomic" "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" @@ -147,36 +144,9 @@ func (n *ViewDir) publishNote() { 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.Parse(evt.Content) { - if _, isExternal := ref.Pointer.(nip73.ExternalPointer); isExternal { - continue - } - - tag := ref.Pointer.AsTag() - key := tag[0] - val := tag[1] - if key == "e" || key == "a" { - key = "q" - } - if existing := evt.Tags.FindWithValue(key, val); existing == nil { - evt.Tags = append(evt.Tags, tag) - - // add their "read" relays - if key == "p" { - 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) - } - } - } - } - } + // massage and extract tags from raw text + targetRelays := n.root.sys.PrepareNoteEvent(n.root.ctx, &evt) + relays = nostr.AppendUnique(relays, targetRelays...) // sign and publish if err := n.root.signer.SignEvent(n.root.ctx, &evt); err != nil { From 67e291e80dace3dd8c10252b4a3c8b4363afc894 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 6 May 2025 00:52:49 -0300 Subject: [PATCH 267/401] nak publish --- count.go | 6 -- event.go | 252 ++++++++++++++++++++++++++--------------------------- fetch.go | 7 -- go.mod | 4 +- go.sum | 2 + main.go | 1 + publish.go | 181 ++++++++++++++++++++++++++++++++++++++ req.go | 6 -- 8 files changed, 308 insertions(+), 151 deletions(-) create mode 100644 publish.go diff --git a/count.go b/count.go index 905d0f0..bad4b8e 100644 --- a/count.go +++ b/count.go @@ -82,12 +82,6 @@ var count = &cli.Command{ biggerUrlSize = len(relay.URL) } } - - defer func() { - for _, relay := range relays { - relay.Close() - } - }() } filter := nostr.Filter{} diff --git a/event.go b/event.go index be14ce7..d385e76 100644 --- a/event.go +++ b/event.go @@ -140,10 +140,6 @@ example: // try to connect to the relays here var relays []*nostr.Relay - // these are defaults, they will be replaced if we use the magic dynamic thing - logthis := func(relayUrl string, s string, args ...any) { log(s, args...) } - colorizethis := func(relayUrl string, colorize func(string, ...any) string) {} - if relayUrls := c.Args().Slice(); len(relayUrls) > 0 { relays = connectToAllRelays(ctx, c, relayUrls, nil, nostr.PoolOptions{ @@ -157,19 +153,11 @@ example: os.Exit(3) } } - defer func() { - for _, relay := range relays { - relay.Close() - } - }() - kr, sec, err := gatherKeyerFromArguments(ctx, c) if err != nil { return err } - doAuth := c.Bool("auth") - // then process input and generate events: // will reuse this @@ -314,123 +302,7 @@ example: } stdout(result) - // publish to relays - successRelays := make([]string, 0, len(relays)) - if len(relays) > 0 { - os.Stdout.Sync() - - if c.Bool("confirm") { - relaysStr := make([]string, len(relays)) - for i, r := range relays { - relaysStr[i] = strings.ToLower(strings.Split(r.URL, "://")[1]) - } - time.Sleep(time.Millisecond * 10) - if !askConfirmation("publish to [ " + strings.Join(relaysStr, " ") + " ]? ") { - return nil - } - } - - if supportsDynamicMultilineMagic() { - // overcomplicated multiline rendering magic - ctx, cancel := context.WithTimeout(ctx, 10*time.Second) - defer cancel() - - urls := make([]string, len(relays)) - lines := make([][][]byte, len(urls)) - flush := func() { - for _, line := range lines { - for _, part := range line { - os.Stderr.Write(part) - } - os.Stderr.Write([]byte{'\n'}) - } - } - render := func() { - clearLines(len(lines)) - flush() - } - flush() - - logthis = func(relayUrl, s string, args ...any) { - idx := slices.Index(urls, relayUrl) - lines[idx] = append(lines[idx], []byte(fmt.Sprintf(s, args...))) - render() - } - colorizethis = func(relayUrl string, colorize func(string, ...any) string) { - cleanUrl, _ := strings.CutPrefix(relayUrl, "wss://") - idx := slices.Index(urls, relayUrl) - lines[idx][0] = []byte(fmt.Sprintf("publishing to %s... ", colorize(cleanUrl))) - render() - } - - for i, relay := range relays { - urls[i] = relay.URL - lines[i] = make([][]byte, 1, 3) - colorizethis(relay.URL, color.CyanString) - } - render() - - for res := range sys.Pool.PublishMany(ctx, urls, evt) { - if res.Error == nil { - colorizethis(res.RelayURL, colors.successf) - logthis(res.RelayURL, "success.") - successRelays = append(successRelays, res.RelayURL) - } else { - colorizethis(res.RelayURL, colors.errorf) - - // in this case it's likely that the lowest-level error is the one that will be more helpful - low := unwrapAll(res.Error) - - // hack for some messages such as from relay.westernbtc.com - 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)) - - logthis(res.RelayURL, msg) - } - } - } else { - // normal dumb flow - for _, relay := range relays { - publish: - cleanUrl, _ := strings.CutPrefix(relay.URL, "wss://") - log("publishing to %s... ", color.CyanString(cleanUrl)) - ctx, cancel := context.WithTimeout(ctx, 10*time.Second) - defer cancel() - - err := relay.Publish(ctx, evt) - if err == nil { - // published fine - log("success.\n") - successRelays = append(successRelays, relay.URL) - continue // continue to next relay - } - - // error publishing - 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.EncodeNpub(pk) - log("authenticating as %s... ", color.YellowString("%s…%s", npub[0:7], npub[58:])) - 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 - } else { - log("auth error: %s. ", err) - } - } - log("failed: %s\n", err) - } - } - - if len(successRelays) > 0 && c.Bool("nevent") { - log(nip19.EncodeNevent(evt.ID, successRelays, evt.PubKey) + "\n") - } - } - - return nil + return publishFlow(ctx, c, kr, evt, relays) } for stdinEvent := range getJsonsOrBlank() { @@ -443,3 +315,125 @@ example: return nil }, } + +func publishFlow(ctx context.Context, c *cli.Command, kr nostr.Signer, evt nostr.Event, relays []*nostr.Relay) error { + doAuth := c.Bool("auth") + + // publish to relays + successRelays := make([]string, 0, len(relays)) + if len(relays) > 0 { + os.Stdout.Sync() + + if c.Bool("confirm") { + relaysStr := make([]string, len(relays)) + for i, r := range relays { + relaysStr[i] = strings.ToLower(strings.Split(r.URL, "://")[1]) + } + time.Sleep(time.Millisecond * 10) + if !askConfirmation("publish to [ " + strings.Join(relaysStr, " ") + " ]? ") { + return nil + } + } + + if supportsDynamicMultilineMagic() { + // overcomplicated multiline rendering magic + ctx, cancel := context.WithTimeout(ctx, 10*time.Second) + defer cancel() + + urls := make([]string, len(relays)) + lines := make([][][]byte, len(urls)) + flush := func() { + for _, line := range lines { + for _, part := range line { + os.Stderr.Write(part) + } + os.Stderr.Write([]byte{'\n'}) + } + } + render := func() { + clearLines(len(lines)) + flush() + } + flush() + + logthis := func(relayUrl, s string, args ...any) { + idx := slices.Index(urls, relayUrl) + lines[idx] = append(lines[idx], []byte(fmt.Sprintf(s, args...))) + render() + } + colorizethis := func(relayUrl string, colorize func(string, ...any) string) { + cleanUrl, _ := strings.CutPrefix(relayUrl, "wss://") + idx := slices.Index(urls, relayUrl) + lines[idx][0] = []byte(fmt.Sprintf("publishing to %s... ", colorize(cleanUrl))) + render() + } + + for i, relay := range relays { + urls[i] = relay.URL + lines[i] = make([][]byte, 1, 3) + colorizethis(relay.URL, color.CyanString) + } + render() + + for res := range sys.Pool.PublishMany(ctx, urls, evt) { + if res.Error == nil { + colorizethis(res.RelayURL, colors.successf) + logthis(res.RelayURL, "success.") + successRelays = append(successRelays, res.RelayURL) + } else { + colorizethis(res.RelayURL, colors.errorf) + + // in this case it's likely that the lowest-level error is the one that will be more helpful + low := unwrapAll(res.Error) + + // hack for some messages such as from relay.westernbtc.com + 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)) + + logthis(res.RelayURL, msg) + } + } + } else { + // normal dumb flow + for _, relay := range relays { + publish: + cleanUrl, _ := strings.CutPrefix(relay.URL, "wss://") + log("publishing to %s... ", color.CyanString(cleanUrl)) + ctx, cancel := context.WithTimeout(ctx, 10*time.Second) + defer cancel() + + err := relay.Publish(ctx, evt) + if err == nil { + // published fine + log("success.\n") + successRelays = append(successRelays, relay.URL) + continue // continue to next relay + } + + // error publishing + 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.EncodeNpub(pk) + log("authenticating as %s... ", color.YellowString("%s…%s", npub[0:7], npub[58:])) + 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 + } else { + log("auth error: %s. ", err) + } + } + log("failed: %s\n", err) + } + } + + if len(successRelays) > 0 && c.Bool("nevent") { + log(nip19.EncodeNevent(evt.ID, successRelays, evt.PubKey) + "\n") + } + } + + return nil +} diff --git a/fetch.go b/fetch.go index 0bcef74..7a1c370 100644 --- a/fetch.go +++ b/fetch.go @@ -27,13 +27,6 @@ var fetch = &cli.Command{ ), ArgsUsage: "[nip05_or_nip19_code]", Action: func(ctx context.Context, c *cli.Command) error { - defer func() { - sys.Pool.Relays.Range(func(_ string, relay *nostr.Relay) bool { - relay.Close() - return true - }) - }() - for code := range getStdinLinesOrArguments(c.Args()) { filter := nostr.Filter{} var authorHint nostr.PubKey diff --git a/go.mod b/go.mod index 1d53d7e..140d5d3 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,6 @@ 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 @@ -23,6 +22,7 @@ require ( ) require ( + fiatjaf.com/nostr v0.0.0-20250506031545-0d99789a54e2 // indirect 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 @@ -86,5 +86,3 @@ require ( google.golang.org/protobuf v1.36.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) - -replace fiatjaf.com/nostr => ../nostrlib diff --git a/go.sum b/go.sum index 4206862..3c5965f 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,8 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= fiatjaf.com/lib v0.3.1 h1:/oFQwNtFRfV+ukmOCxfBEAuayoLwXp4wu2/fz5iHpwA= fiatjaf.com/lib v0.3.1/go.mod h1:Ycqq3+mJ9jAWu7XjbQI1cVr+OFgnHn79dQR5oTII47g= +fiatjaf.com/nostr v0.0.0-20250506031545-0d99789a54e2 h1:WDjFQ8hPUAvTDKderZ0NC6vaRBBxODPchKER4wuQdG8= +fiatjaf.com/nostr v0.0.0-20250506031545-0d99789a54e2/go.mod h1:VPs38Fc8J1XAErV750CXAmMUqIq3XEX9VZVj/LuQzzM= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/FastFilter/xorfilter v0.2.1 h1:lbdeLG9BdpquK64ZsleBS8B4xO/QW1IM0gMzF7KaBKc= github.com/FastFilter/xorfilter v0.2.1/go.mod h1:aumvdkhscz6YBZF9ZA/6O4fIoNod4YR50kIVGGZ7l9I= diff --git a/main.go b/main.go index f0ddfb4..136c73c 100644 --- a/main.go +++ b/main.go @@ -44,6 +44,7 @@ var app = &cli.Command{ mcpServer, curl, fsCmd, + publish, }, Version: version, Flags: []cli.Flag{ diff --git a/publish.go b/publish.go new file mode 100644 index 0000000..e297030 --- /dev/null +++ b/publish.go @@ -0,0 +1,181 @@ +package main + +import ( + "context" + "fmt" + "io" + "os" + "strings" + + "fiatjaf.com/nostr" + "fiatjaf.com/nostr/nip19" + "fiatjaf.com/nostr/sdk" + "github.com/urfave/cli/v3" +) + +var publish = &cli.Command{ + Name: "publish", + Usage: "publishes a note with content from stdin", + Description: `reads content from stdin and publishes it as a note, optionally as a reply to another note. + +example: + echo "hello world" | nak publish + echo "I agree!" | nak publish --reply nevent1... + echo "tagged post" | nak publish -t t=mytag -t e=someeventid`, + DisableSliceFlagSeparator: true, + Flags: append(defaultKeyFlags, + &cli.StringFlag{ + Name: "reply", + Usage: "event id, naddr1 or nevent1 code to reply to", + }, + &cli.StringSliceFlag{ + Name: "tag", + Aliases: []string{"t"}, + Usage: "sets a tag field on the event, takes a value like -t e= or -t sometag=\"value one;value two;value three\"", + }, + &NaturalTimeFlag{ + Name: "created-at", + Aliases: []string{"time", "ts"}, + Usage: "unix timestamp value for the created_at field", + DefaultText: "now", + Value: nostr.Now(), + }, + &cli.BoolFlag{ + Name: "auth", + Usage: "always perform nip42 \"AUTH\" when facing an \"auth-required: \" rejection and try again", + Category: CATEGORY_EXTRAS, + }, + &cli.BoolFlag{ + Name: "nevent", + Usage: "print the nevent code (to stderr) after the event is published", + Category: CATEGORY_EXTRAS, + }, + &cli.BoolFlag{ + Name: "confirm", + Usage: "ask before publishing the event", + Category: CATEGORY_EXTRAS, + }, + ), + Action: func(ctx context.Context, c *cli.Command) error { + content, err := io.ReadAll(os.Stdin) + if err != nil { + return fmt.Errorf("failed to read from stdin: %w", err) + } + + evt := nostr.Event{ + Kind: 1, + Content: strings.TrimSpace(string(content)), + Tags: make(nostr.Tags, 0, 4), + CreatedAt: nostr.Now(), + } + + // handle timestamp flag + if c.IsSet("created-at") { + evt.CreatedAt = getNaturalDate(c, "created-at") + } + + // handle reply flag + var replyRelays []string + if replyTo := c.String("reply"); replyTo != "" { + var replyEvent *nostr.Event + + // try to decode as nevent or naddr first + if strings.HasPrefix(replyTo, "nevent1") || strings.HasPrefix(replyTo, "naddr1") { + _, value, err := nip19.Decode(replyTo) + if err != nil { + return fmt.Errorf("invalid reply target: %w", err) + } + + switch pointer := value.(type) { + case nostr.EventPointer: + replyEvent, _, err = sys.FetchSpecificEvent(ctx, pointer, sdk.FetchSpecificEventParameters{}) + case nostr.EntityPointer: + replyEvent, _, err = sys.FetchSpecificEvent(ctx, pointer, sdk.FetchSpecificEventParameters{}) + } + if err != nil { + return fmt.Errorf("failed to fetch reply target event: %w", err) + } + } else { + // try as raw event ID + id, err := nostr.IDFromHex(replyTo) + if err != nil { + return fmt.Errorf("invalid event id: %w", err) + } + replyEvent, _, err = sys.FetchSpecificEvent(ctx, nostr.EventPointer{ID: id}, sdk.FetchSpecificEventParameters{}) + if err != nil { + return fmt.Errorf("failed to fetch reply target event: %w", err) + } + } + + if replyEvent.Kind != 1 { + evt.Kind = 1111 + } + + // add reply tags + evt.Tags = append(evt.Tags, + nostr.Tag{"e", replyEvent.ID.Hex(), "", "reply"}, + nostr.Tag{"p", replyEvent.PubKey.Hex()}, + ) + + replyRelays = sys.FetchInboxRelays(ctx, replyEvent.PubKey, 3) + } + + // handle other tags -- copied from event.go + tagFlags := c.StringSlice("tag") + for _, tagFlag := range tagFlags { + // tags are in the format key=value + tagName, tagValue, found := strings.Cut(tagFlag, "=") + tag := []string{tagName} + if found { + // tags may also contain extra elements separated with a ";" + tagValues := strings.Split(tagValue, ";") + tag = append(tag, tagValues...) + } + evt.Tags = append(evt.Tags, tag) + } + + // process the content + targetRelays := sys.PrepareNoteEvent(ctx, &evt) + + // connect to all the relays (like event.go) + kr, _, err := gatherKeyerFromArguments(ctx, c) + if err != nil { + return err + } + pk, err := kr.GetPublicKey(ctx) + if err != nil { + return fmt.Errorf("failed to get our public key: %w", err) + } + + relayUrls := sys.FetchWriteRelays(ctx, pk) + relayUrls = nostr.AppendUnique(relayUrls, targetRelays...) + relayUrls = nostr.AppendUnique(relayUrls, replyRelays...) + relayUrls = nostr.AppendUnique(relayUrls, c.Args().Slice()...) + relays := connectToAllRelays(ctx, c, relayUrls, nil, + 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 { + if len(relayUrls) == 0 { + return fmt.Errorf("no relays to publish this note to.") + } else { + return fmt.Errorf("failed to connect to any of [ %v ].", relayUrls) + } + } + + // sign the event + if err := kr.SignEvent(ctx, &evt); err != nil { + return fmt.Errorf("error signing event: %w", err) + } + + // print + stdout(evt.String()) + + // publish (like event.go) + return publishFlow(ctx, c, kr, evt, relays) + }, +} diff --git a/req.go b/req.go index c55c1fa..c1fec94 100644 --- a/req.go +++ b/req.go @@ -113,12 +113,6 @@ example: for i, relay := range relays { relayUrls[i] = relay.URL } - - defer func() { - for _, relay := range relays { - relay.Close() - } - }() } // go line by line from stdin or run once with input from flags From c3822225b4a423d94b92ad16e0f6811a3c395611 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 6 May 2025 11:50:29 -0300 Subject: [PATCH 268/401] small tweaks to readme examples. --- README.md | 4 ++-- go.mod | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d0c4df5..34e4250 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,7 @@ demo videos with [2](https://njump.me/nevent1qqs8pmmae89agph80928l6gjm0wymechqaz ### generate a private key ```shell -~> nak key generate 18:59 +~> nak key generate 7b94e287b1fafa694ded1619b27de7effd3646104a158e187ff4edc56bc6148d ``` @@ -130,7 +130,7 @@ type the password to decrypt your secret key: ********** ### sign an event using a remote NIP-46 bunker ```shell -~> nak event --connect 'bunker://a9e0f110f636f3191644110c19a33448daf09d7cda9708a769e91b7e91340208?relay=wss%3A%2F%2Frelay.damus.io&relay=wss%3A%2F%2Frelay.nsecbunker.com&relay=wss%3A%2F%2Fnos.lol&secret=TWfGbjQCLxUf' -c 'hello from bunker' +~> nak event --sec 'bunker://a9e0f110f636f3191644110c19a33448daf09d7cda9708a769e91b7e91340208?relay=wss%3A%2F%2Frelay.damus.io&relay=wss%3A%2F%2Frelay.nsecbunker.com&relay=wss%3A%2F%2Fnos.lol&secret=TWfGbjQCLxUf' -c 'hello from bunker' ``` ### sign an event using a NIP-49 encrypted key diff --git a/go.mod b/go.mod index 140d5d3..1d53d7e 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ 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 @@ -22,7 +23,6 @@ require ( ) require ( - fiatjaf.com/nostr v0.0.0-20250506031545-0d99789a54e2 // indirect 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 @@ -86,3 +86,5 @@ require ( google.golang.org/protobuf v1.36.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) + +replace fiatjaf.com/nostr => ../nostrlib From f799c65779f06bbae8f0223a26d4705200fd0777 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Wed, 7 May 2025 07:59:43 -0300 Subject: [PATCH 269/401] add nak publish to README. --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index 34e4250..f75241a 100644 --- a/README.md +++ b/README.md @@ -262,6 +262,15 @@ cashuA1psxqyry8... ~> nak blossom --server aegis.utxo.one download acc8ea43d4e6b706f68b249144364f446854b7f63ba1927371831c05dcf0256c -o downloaded.png ``` +### publish a fully formed event with correct tags, URIs and to the correct read and write relays +```shell +echo "#surely you're joking, mr npub1l2vyh47mk2p0qlsku7hg0vn29faehy9hy34ygaclpn66ukqp3afqutajft olas.app is broken again" | nak publish + +# it will add the hashtag, turn the npub1 code into a nostr:npub1 URI, turn the olas.app string into https://olas.app, add the "p" tag (and "q" tags too if you were mentioning an nevent1 code or naddr1 code) and finally publish it to your "write" relays and to any mentioned person (or author of mentioned events)'s "read" relays. +# there is also a --reply flag that you can pass an nevent, naddr or hex id to and it will do the right thing (including setting the correct kind to either 1 or 1111). +# and there is a --confirm flag that gives you a chance to confirm before actually publishing the result to relays. +``` + ## contributing to this repository Use NIP-34 to send your patches to `naddr1qqpkucttqy28wumn8ghj7un9d3shjtnwdaehgu3wvfnsz9nhwden5te0wfjkccte9ehx7um5wghxyctwvsq3gamnwvaz7tmjv4kxz7fwv3sk6atn9e5k7q3q80cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsxpqqqpmej2wctpn`. From aadcc73906152f0000f5048a49b099d1b9fb5b61 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Thu, 8 May 2025 09:59:03 -0300 Subject: [PATCH 270/401] adapt to since and until not being pointers. --- bunker.go | 3 +-- count.go | 15 +++++++-------- go.mod | 2 +- go.sum | 2 -- helpers_key.go | 4 ++-- nostrfs/viewdir.go | 12 ++++++------ req.go | 7 ++----- wallet.go | 2 +- 8 files changed, 20 insertions(+), 27 deletions(-) diff --git a/bunker.go b/bunker.go index cafce16..ff5169c 100644 --- a/bunker.go +++ b/bunker.go @@ -140,11 +140,10 @@ var bunker = &cli.Command{ printBunkerInfo() // subscribe to relays - now := nostr.Now() events := sys.Pool.SubscribeMany(ctx, relayURLs, nostr.Filter{ Kinds: []nostr.Kind{nostr.KindNostrConnect}, Tags: nostr.TagMap{"p": []string{pubkey.Hex()}}, - Since: &now, + Since: nostr.Now(), LimitZero: true, }, nostr.SubscriptionOptions{ Label: "nak-bunker", diff --git a/count.go b/count.go index bad4b8e..f22ecb8 100644 --- a/count.go +++ b/count.go @@ -46,13 +46,13 @@ var count = &cli.Command{ Usage: "shortcut for --tag p=", Category: CATEGORY_FILTER_ATTRIBUTES, }, - &cli.IntFlag{ + &NaturalTimeFlag{ Name: "since", Aliases: []string{"s"}, Usage: "only accept events newer than this (unix timestamp)", Category: CATEGORY_FILTER_ATTRIBUTES, }, - &cli.IntFlag{ + &NaturalTimeFlag{ Name: "until", Aliases: []string{"u"}, Usage: "only accept events older than this (unix timestamp)", @@ -122,14 +122,13 @@ var count = &cli.Command{ } } - if since := c.Int("since"); since != 0 { - ts := nostr.Timestamp(since) - filter.Since = &ts + if c.IsSet("since") { + filter.Since = getNaturalDate(c, "since") } - if until := c.Int("until"); until != 0 { - ts := nostr.Timestamp(until) - filter.Until = &ts + if c.IsSet("until") { + filter.Until = getNaturalDate(c, "until") } + if limit := c.Int("limit"); limit != 0 { filter.Limit = int(limit) } diff --git a/go.mod b/go.mod index 1d53d7e..ef35812 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( 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/mattn/go-tty v0.0.7 github.com/stretchr/testify v1.10.0 github.com/urfave/cli/v3 v3.0.0-beta1 golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 @@ -59,7 +60,6 @@ require ( github.com/magefile/mage v1.14.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-tty v0.0.7 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pkg/errors v0.9.1 // indirect diff --git a/go.sum b/go.sum index 3c5965f..4206862 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,6 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= fiatjaf.com/lib v0.3.1 h1:/oFQwNtFRfV+ukmOCxfBEAuayoLwXp4wu2/fz5iHpwA= fiatjaf.com/lib v0.3.1/go.mod h1:Ycqq3+mJ9jAWu7XjbQI1cVr+OFgnHn79dQR5oTII47g= -fiatjaf.com/nostr v0.0.0-20250506031545-0d99789a54e2 h1:WDjFQ8hPUAvTDKderZ0NC6vaRBBxODPchKER4wuQdG8= -fiatjaf.com/nostr v0.0.0-20250506031545-0d99789a54e2/go.mod h1:VPs38Fc8J1XAErV750CXAmMUqIq3XEX9VZVj/LuQzzM= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/FastFilter/xorfilter v0.2.1 h1:lbdeLG9BdpquK64ZsleBS8B4xO/QW1IM0gMzF7KaBKc= github.com/FastFilter/xorfilter v0.2.1/go.mod h1:aumvdkhscz6YBZF9ZA/6O4fIoNod4YR50kIVGGZ7l9I= diff --git a/helpers_key.go b/helpers_key.go index ccccc8b..ad7a33f 100644 --- a/helpers_key.go +++ b/helpers_key.go @@ -50,10 +50,10 @@ func gatherKeyerFromArguments(ctx context.Context, c *cli.Command) (nostr.Keyer, if bunker != nil { kr = keyer.NewBunkerSignerFromBunkerClient(bunker) } else { - kr, err = keyer.NewPlainKeySigner(key) + kr = keyer.NewPlainKeySigner(key) } - return kr, key, err + return kr, key, nil } func gatherSecretKeyOrBunkerFromArguments(ctx context.Context, c *cli.Command) (nostr.SecretKey, *nip46.BunkerClient, error) { diff --git a/nostrfs/viewdir.go b/nostrfs/viewdir.go index cc094e2..de3afba 100644 --- a/nostrfs/viewdir.go +++ b/nostrfs/viewdir.go @@ -183,8 +183,8 @@ func (n *ViewDir) publishNote() { func (n *ViewDir) Getattr(_ context.Context, f fs.FileHandle, out *fuse.AttrOut) syscall.Errno { now := nostr.Now() - if n.filter.Until != nil { - now = *n.filter.Until + if n.filter.Until != 0 { + now = n.filter.Until } aMonthAgo := now - 30*24*60*60 out.Mtime = uint64(aMonthAgo) @@ -199,14 +199,14 @@ func (n *ViewDir) Opendir(ctx context.Context) syscall.Errno { if n.paginate { now := nostr.Now() - if n.filter.Until != nil { - now = *n.filter.Until + if n.filter.Until != 0 { + now = n.filter.Until } aMonthAgo := now - 30*24*60*60 - n.filter.Since = &aMonthAgo + n.filter.Since = aMonthAgo filter := n.filter - filter.Until = &aMonthAgo + filter.Until = aMonthAgo n.AddChild("@previous", n.NewPersistentInode( n.root.ctx, diff --git a/req.go b/req.go index c1fec94..9b73a9c 100644 --- a/req.go +++ b/req.go @@ -286,13 +286,10 @@ func applyFlagsToFilter(c *cli.Command, filter *nostr.Filter) error { } if c.IsSet("since") { - nts := getNaturalDate(c, "since") - filter.Since = &nts + filter.Since = getNaturalDate(c, "since") } - if c.IsSet("until") { - nts := getNaturalDate(c, "until") - filter.Until = &nts + filter.Until = getNaturalDate(c, "until") } if limit := c.Uint("limit"); limit != 0 { diff --git a/wallet.go b/wallet.go index cc73424..414d03e 100644 --- a/wallet.go +++ b/wallet.go @@ -240,7 +240,7 @@ var wallet = &cli.Command{ if mint := c.String("mint"); mint != "" { sourceMint = "http" + nostr.NormalizeURL(mint)[2:] } - proofs, mint, err := w.Send(ctx, amount, nip60.SendOptions{ + proofs, mint, err := w.SendInternal(ctx, amount, nip60.SendOptions{ SpecificSourceMint: sourceMint, }) if err != nil { From 5bcf2da79406af43cf2cd4bbddf5a7f02baef3bb Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sun, 11 May 2025 12:09:56 -0300 Subject: [PATCH 271/401] adapt serve to variable max eventstores. --- serve.go | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/serve.go b/serve.go index ee6ed51..9127d10 100644 --- a/serve.go +++ b/serve.go @@ -4,7 +4,6 @@ import ( "bufio" "context" "fmt" - "math" "os" "time" @@ -38,7 +37,7 @@ var serve = &cli.Command{ }, }, Action: func(ctx context.Context, c *cli.Command) error { - db := &slicestore.SliceStore{MaxLimit: math.MaxInt} + db := &slicestore.SliceStore{} var scanner *bufio.Scanner if path := c.String("events"); path != "" { @@ -71,7 +70,7 @@ var serve = &cli.Command{ rl.Info.Software = "https://github.com/fiatjaf/nak" rl.Info.Version = version - rl.UseEventstore(db) + rl.UseEventstore(db, 1_000_000) started := make(chan bool) exited := make(chan error) @@ -108,9 +107,9 @@ var serve = &cli.Command{ d := debounce.New(time.Second * 2) printStatus = func() { d(func() { - totalEvents := 0 - for range db.QueryEvents(nostr.Filter{}) { - totalEvents++ + totalEvents, err := db.CountEvents(nostr.Filter{}) + if err != nil { + log("failed to count: %s\n", err) } subs := rl.GetListeningFilters() From fc255b5a9ab6c8b4c5324ff8c973f7a1949f8f78 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sun, 11 May 2025 12:15:50 -0300 Subject: [PATCH 272/401] optimized clamped error message for status code failures. --- helpers.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/helpers.go b/helpers.go index 1884556..a4f8a86 100644 --- a/helpers.go +++ b/helpers.go @@ -381,9 +381,15 @@ func unwrapAll(err error) error { func clampMessage(msg string, prefixAlreadyPrinted int) string { termSize, _, _ := term.GetSize(int(os.Stderr.Fd())) + prf := "expected handshake response status code 101 but got " + if len(msg) > len(prf) && msg[0:len(prf)] == prf { + msg = "status " + msg[len(prf):] + } + if len(msg) > termSize-prefixAlreadyPrinted && prefixAlreadyPrinted+1 < termSize { msg = msg[0:termSize-prefixAlreadyPrinted-1] + "…" } + return msg } From 150625ee74dd58a7053d554e4434e1885e85f7c8 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Mon, 12 May 2025 09:20:27 -0300 Subject: [PATCH 273/401] remove debug.PrintStack() --- helpers_key.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/helpers_key.go b/helpers_key.go index ad7a33f..16fbb51 100644 --- a/helpers_key.go +++ b/helpers_key.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "os" - "runtime/debug" "strings" "fiatjaf.com/nostr" @@ -112,7 +111,6 @@ func gatherSecretKeyOrBunkerFromArguments(ctx context.Context, c *cli.Command) ( sk, err := nostr.SecretKeyFromHex(sec) if err != nil { - debug.PrintStack() return nostr.SecretKey{}, nil, fmt.Errorf("invalid secret key: %w", err) } From 4eb5e929d4412485b71b5f24559b1ae086b0691c Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Wed, 14 May 2025 23:36:45 -0300 Subject: [PATCH 274/401] blossom: upload from stdin. --- blossom.go | 42 +++++++++++++++++++++++++++++++++--------- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/blossom.go b/blossom.go index a5ae8aa..3c8ed63 100644 --- a/blossom.go +++ b/blossom.go @@ -1,8 +1,10 @@ package main import ( + "bytes" "context" "fmt" + "io" "os" "fiatjaf.com/nostr" @@ -73,22 +75,44 @@ var blossomCmd = &cli.Command{ return err } - hasError := false - for _, fpath := range c.Args().Slice() { - bd, err := client.UploadFilePath(ctx, fpath) + if isPiped() { + // get file from stdin + if c.Args().Len() > 0 { + return fmt.Errorf("do not pass arguments when piping from stdin") + } + + data, err := io.ReadAll(os.Stdin) if err != nil { - fmt.Fprintf(os.Stderr, "%s\n", err) - hasError = true - continue + return fmt.Errorf("failed to read stdin: %w", err) + } + + bd, err := client.UploadBlob(ctx, bytes.NewReader(data), "") + if err != nil { + return err } j, _ := json.Marshal(bd) stdout(string(j)) + } else { + // get filenames from arguments + hasError := false + for _, fpath := range c.Args().Slice() { + bd, err := client.UploadFilePath(ctx, fpath) + if err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err) + hasError = true + continue + } + + j, _ := json.Marshal(bd) + stdout(string(j)) + } + + if hasError { + os.Exit(3) + } } - if hasError { - os.Exit(3) - } return nil }, }, From 4387595437b6aa4ced7ac0d2fa47e4f0f46e01c1 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sat, 17 May 2025 21:42:45 -0300 Subject: [PATCH 275/401] serve: display number of connections. --- serve.go | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/serve.go b/serve.go index 9127d10..4180d81 100644 --- a/serve.go +++ b/serve.go @@ -5,6 +5,7 @@ import ( "context" "fmt" "os" + "sync/atomic" "time" "fiatjaf.com/nostr" @@ -104,6 +105,15 @@ var serve = &cli.Command{ return false, "" } + totalConnections := atomic.Int32{} + rl.OnConnect = func(ctx context.Context) { + totalConnections.Add(1) + go func() { + <-ctx.Done() + totalConnections.Add(-1) + }() + } + d := debounce.New(time.Second * 2) printStatus = func() { d(func() { @@ -113,7 +123,12 @@ var serve = &cli.Command{ } subs := rl.GetListeningFilters() - log(" %s events stored: %s, subscriptions opened: %s\n", color.HiMagentaString("•"), color.HiMagentaString("%d", totalEvents), color.HiMagentaString("%d", len(subs))) + log(" %s events: %s, connections: %s, subscriptions: %s\n", + color.HiMagentaString("•"), + color.HiMagentaString("%d", totalEvents), + color.HiMagentaString("%d", totalConnections.Load()), + color.HiMagentaString("%d", len(subs)), + ) }) } From aa89093d57bedcee1104f87c463e86d7ebc26bc8 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 20 May 2025 23:31:54 -0300 Subject: [PATCH 276/401] accept bunker URIs in $NOSTR_SECRET_KEY, simplify. fixes https://github.com/fiatjaf/nak/issues/66 --- helpers_key.go | 15 ++++----------- mcp.go | 10 +--------- 2 files changed, 5 insertions(+), 20 deletions(-) diff --git a/helpers_key.go b/helpers_key.go index 16fbb51..b06fb8c 100644 --- a/helpers_key.go +++ b/helpers_key.go @@ -20,10 +20,12 @@ import ( var defaultKeyFlags = []cli.Flag{ &cli.StringFlag{ Name: "sec", - Usage: "secret key to sign the event, as nsec, ncryptsec or hex, or a bunker URL, it is more secure to use the environment variable NOSTR_SECRET_KEY than this flag", + Usage: "secret key to sign the event, as nsec, ncryptsec or hex, or a bunker URL", DefaultText: "the key '1'", - Aliases: []string{"connect"}, Category: CATEGORY_SIGNER, + Sources: cli.EnvVars("NOSTR_SECRET_KEY"), + Value: nostr.KeyOne.Hex(), + HideDefault: true, }, &cli.BoolFlag{ Name: "prompt-sec", @@ -80,15 +82,6 @@ func gatherSecretKeyOrBunkerFromArguments(ctx context.Context, c *cli.Command) ( return nostr.SecretKey{}, bunker, err } - // take private from flags, environment variable or default to 1 - if sec == "" { - if key, ok := os.LookupEnv("NOSTR_SECRET_KEY"); ok { - sec = key - } else { - sec = "0000000000000000000000000000000000000000000000000000000000000001" - } - } - if c.Bool("prompt-sec") { var err error sec, err = askPassword("type your secret key as ncryptsec, nsec or hex: ", nil) diff --git a/mcp.go b/mcp.go index ddade00..5ab4250 100644 --- a/mcp.go +++ b/mcp.go @@ -3,7 +3,6 @@ package main import ( "context" "fmt" - "os" "strings" "fiatjaf.com/nostr" @@ -28,13 +27,10 @@ var mcpServer = &cli.Command{ version, ) - keyer, sk, err := gatherKeyerFromArguments(ctx, c) + keyer, _, 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"), @@ -46,10 +42,6 @@ var mcpServer = &cli.Command{ mention, _ := optional[string](r, "mention") relay, _ := optional[string](r, "relay") - sk := os.Getenv("NOSTR_SECRET_KEY") - if sk == "" { - sk = "0000000000000000000000000000000000000000000000000000000000000001" - } var relays []string evt := nostr.Event{ From 1304a6517978652edc8b215066048e2af035cae7 Mon Sep 17 00:00:00 2001 From: franzaps Date: Tue, 20 May 2025 23:36:06 -0300 Subject: [PATCH 277/401] Update zapstore.yaml --- zapstore.yaml | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/zapstore.yaml b/zapstore.yaml index 9d70234..18ac1ab 100644 --- a/zapstore.yaml +++ b/zapstore.yaml @@ -1,14 +1,7 @@ -nak: - cli: - name: nak - summary: a command line tool for doing all things nostr - repository: https://github.com/fiatjaf/nak - artifacts: - nak-v%v-darwin-arm64: - platforms: [darwin-arm64] - nak-v%v-darwin-amd64: - platforms: [darwin-x86_64] - nak-v%v-linux-arm64: - platforms: [linux-aarch64] - nak-v%v-linux-amd64: - platforms: [linux-x86_64] \ No newline at end of file +repository: https://github.com/fiatjaf/nak +assets: + - nak-v\d+\.\d+\.\d+-darwin-arm64 + - nak-v\d+\.\d+\.\d+-linux-amd64 + - nak-v\d+\.\d+\.\d+-linux-arm64 +remote_metadata: + - github From f450e735b6b1910a7bb59e25815db0f55fc287b1 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 20 May 2025 23:45:54 -0300 Subject: [PATCH 278/401] sticky version. --- go.mod | 12 +++++------- go.sum | 18 ++++++++++-------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/go.mod b/go.mod index ef35812..197cfc1 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.24.1 require ( fiatjaf.com/lib v0.3.1 - fiatjaf.com/nostr v0.0.1 + fiatjaf.com/nostr v0.0.0-20250521022139-d3fb25441ab3 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 @@ -19,7 +19,7 @@ require ( github.com/mattn/go-tty v0.0.7 github.com/stretchr/testify v1.10.0 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-20250506013437-ce4c2cf36ca6 golang.org/x/term v0.30.0 ) @@ -77,14 +77,12 @@ require ( github.com/wasilibs/go-re2 v1.3.0 // indirect github.com/x448/float16 v0.8.4 // indirect go.opencensus.io v0.24.0 // indirect - golang.org/x/arch v0.15.0 // indirect + golang.org/x/arch v0.17.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/sync v0.14.0 // indirect + golang.org/x/sys v0.33.0 // indirect golang.org/x/text v0.23.0 // indirect google.golang.org/protobuf v1.36.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) - -replace fiatjaf.com/nostr => ../nostrlib diff --git a/go.sum b/go.sum index 4206862..b444805 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,8 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= fiatjaf.com/lib v0.3.1 h1:/oFQwNtFRfV+ukmOCxfBEAuayoLwXp4wu2/fz5iHpwA= fiatjaf.com/lib v0.3.1/go.mod h1:Ycqq3+mJ9jAWu7XjbQI1cVr+OFgnHn79dQR5oTII47g= +fiatjaf.com/nostr v0.0.0-20250521022139-d3fb25441ab3 h1:JRtme8g4UQ5KYlxI31wBa8YMWmAxvxdwtNn+PiI/XCs= +fiatjaf.com/nostr v0.0.0-20250521022139-d3fb25441ab3/go.mod h1:VPs38Fc8J1XAErV750CXAmMUqIq3XEX9VZVj/LuQzzM= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/FastFilter/xorfilter v0.2.1 h1:lbdeLG9BdpquK64ZsleBS8B4xO/QW1IM0gMzF7KaBKc= github.com/FastFilter/xorfilter v0.2.1/go.mod h1:aumvdkhscz6YBZF9ZA/6O4fIoNod4YR50kIVGGZ7l9I= @@ -239,16 +241,16 @@ github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZ github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -golang.org/x/arch v0.15.0 h1:QtOrQd0bTUnhNVNndMpLHNWrDmYzZ2KDqSrEymqInZw= -golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE= +golang.org/x/arch v0.17.0 h1:4O3dfLzd+lQewptAHqjewQZQDyEdejz3VwgeYwkZneU= +golang.org/x/arch v0.17.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= -golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= +golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI= +golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= @@ -268,8 +270,8 @@ golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAG golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/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/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= +golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/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= @@ -283,8 +285,8 @@ golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= From 61a68f3dca63658046144936aacd5a4f8b360ce4 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Wed, 21 May 2025 14:12:07 -0300 Subject: [PATCH 279/401] fix faulty outbox database check logic when it doesn't exist. fixes https://github.com/fiatjaf/nak/issues/66 --- main.go | 2 +- outbox.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/main.go b/main.go index 136c73c..d9c2ff8 100644 --- a/main.go +++ b/main.go @@ -85,7 +85,7 @@ var app = &cli.Command{ sys = sdk.NewSystem() if err := initializeOutboxHintsDB(c, sys); err != nil { - return ctx, fmt.Errorf("failed to initialized outbox hints: %w", err) + return ctx, fmt.Errorf("failed to initialize outbox hints: %w", err) } sys.Pool = nostr.NewPool(nostr.PoolOptions{ diff --git a/outbox.go b/outbox.go index 877cebb..37db434 100644 --- a/outbox.go +++ b/outbox.go @@ -29,9 +29,9 @@ func initializeOutboxHintsDB(c *cli.Command, sys *sdk.System) error { hintsFilePath = filepath.Join(configPath, "outbox/hints.bg") } if hintsFilePath != "" { - if _, err := os.Stat(hintsFilePath); !os.IsNotExist(err) { + if _, err := os.Stat(hintsFilePath); err == nil { hintsFileExists = true - } else if err != nil { + } else if !os.IsNotExist(err) { return err } } From 6e5441aa18703d2fb48f3f94fa7ad64e33bbe880 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Thu, 22 May 2025 09:23:24 -0300 Subject: [PATCH 280/401] fix type assertions. closes https://github.com/fiatjaf/nak/issues/67 --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 197cfc1..35e6d24 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.24.1 require ( fiatjaf.com/lib v0.3.1 - fiatjaf.com/nostr v0.0.0-20250521022139-d3fb25441ab3 + fiatjaf.com/nostr v0.0.0-20250522115245-f38ce069a93d 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 diff --git a/go.sum b/go.sum index b444805..5755f0b 100644 --- a/go.sum +++ b/go.sum @@ -3,6 +3,8 @@ fiatjaf.com/lib v0.3.1 h1:/oFQwNtFRfV+ukmOCxfBEAuayoLwXp4wu2/fz5iHpwA= fiatjaf.com/lib v0.3.1/go.mod h1:Ycqq3+mJ9jAWu7XjbQI1cVr+OFgnHn79dQR5oTII47g= fiatjaf.com/nostr v0.0.0-20250521022139-d3fb25441ab3 h1:JRtme8g4UQ5KYlxI31wBa8YMWmAxvxdwtNn+PiI/XCs= fiatjaf.com/nostr v0.0.0-20250521022139-d3fb25441ab3/go.mod h1:VPs38Fc8J1XAErV750CXAmMUqIq3XEX9VZVj/LuQzzM= +fiatjaf.com/nostr v0.0.0-20250522115245-f38ce069a93d h1:sl/BOXW5eK7v+cchMMEZvnzQW+n/jWiHGQn+CRt5m5Q= +fiatjaf.com/nostr v0.0.0-20250522115245-f38ce069a93d/go.mod h1:VPs38Fc8J1XAErV750CXAmMUqIq3XEX9VZVj/LuQzzM= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/FastFilter/xorfilter v0.2.1 h1:lbdeLG9BdpquK64ZsleBS8B4xO/QW1IM0gMzF7KaBKc= github.com/FastFilter/xorfilter v0.2.1/go.mod h1:aumvdkhscz6YBZF9ZA/6O4fIoNod4YR50kIVGGZ7l9I= From f27ac6c0e3d0a72233ffd531a63d441dda390ee2 Mon Sep 17 00:00:00 2001 From: Chris McCormick Date: Fri, 23 May 2025 11:24:30 +0800 Subject: [PATCH 281/401] Basic smoke tests. --- .github/workflows/smoke-test-release.yml | 93 ++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 .github/workflows/smoke-test-release.yml diff --git a/.github/workflows/smoke-test-release.yml b/.github/workflows/smoke-test-release.yml new file mode 100644 index 0000000..bc40a65 --- /dev/null +++ b/.github/workflows/smoke-test-release.yml @@ -0,0 +1,93 @@ +name: Smoke test the binary + +on: + push: + branches: + - master + +jobs: + smoke-test-linux-amd64: + runs-on: ubuntu-latest + steps: + - name: Download and smoke test latest binary + run: | + set -eo pipefail # Exit on error, and on pipe failures + + echo "Downloading nak binary from releases" + RELEASE_URL="https://api.github.com/repos/fiatjaf/nak/releases/latest" + wget $(wget -q -O - ${RELEASE_URL} | jq -r '.assets[] | select(.name | contains("linux-amd64")) | .browser_download_url') -O nak -nv + chmod +x nak + + echo "Running basic tests..." + ./nak --version + + # Generate and manipulate keys + echo "Testing key operations..." + SECRET_KEY=$(./nak key generate) + PUBLIC_KEY=$(echo $SECRET_KEY | ./nak key public) + echo "Generated key pair: $PUBLIC_KEY" + + # Create events + echo "Testing event creation..." + ./nak event -c "hello world" + ./nak event --ts "2 days ago" -c "event with timestamp" + ./nak event -k 1 -t "t=test" -c "event with tag" + + # Test NIP-19 encoding/decoding + echo "Testing NIP-19 encoding/decoding..." + NSEC=$(echo $SECRET_KEY | ./nak encode nsec) + echo "Encoded nsec: $NSEC" + ./nak encode npub 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 + NOTE_ID="5ae731bbc7711f78513da14927c48cc7143a91e6cad0565fdc4d73b8967a7d59" + NOTE1=$(./nak encode note $NOTE_ID) + echo "Encoded note1: $NOTE1" + ./nak decode $NOTE1 + ./nak decode npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6 + + # Test event verification + echo "Testing event verification..." + # Create an event and verify it + VERIFY_EVENT=$(./nak event -c "verify me") + echo $VERIFY_EVENT | ./nak verify + + # Test PoW + echo "Testing PoW..." + ./nak event -c "testing pow" --pow 8 + + # Test NIP-49 key encryption/decryption + echo "Testing NIP-49 key encryption/decryption..." + ENCRYPTED_KEY=$(./nak key encrypt $SECRET_KEY "testpassword") + echo "Encrypted key: ${ENCRYPTED_KEY:0:20}..." + DECRYPTED_KEY=$(./nak key decrypt $ENCRYPTED_KEY "testpassword") + if [ "$DECRYPTED_KEY" != "$SECRET_KEY" ]; then + echo "NIP-49 encryption/decryption test failed!" + exit 1 + fi + + # Test multi-value tags + echo "Testing multi-value tags..." + ./nak event --ts "yesterday" -t "e=f59911b561c37c90b01e9e5c2557307380835c83399756f4d62d8167227e420a;wss://relay.example.com;root" -c "Testing multi-value tags" + + # Test relay operations (with a public relay) + echo "Testing relay operations..." + # Publish a simple event to a public relay + EVENT_JSON=$(./nak event --sec $SECRET_KEY -c "Test from nak smoke test" nos.lol) + EVENT_ID=$(echo $EVENT_JSON | jq -r .id) + echo "Published event ID: $EVENT_ID" + + # Wait a moment for propagation + sleep 2 + + # Fetch the event we just published + ./nak req -i $EVENT_ID nos.lol + + # Test serving (just start and immediately kill) + echo "Testing serve command..." + timeout 2s ./nak serve || true + + # Test filesystem mount (just start and immediately kill) + echo "Testing fs mount command..." + mkdir -p /tmp/nostr-mount + timeout 2s ./nak fs --sec $SECRET_KEY /tmp/nostr-mount || true + + echo "All tests passed" From b5bd2aecf6f28275fda38ce2688ec7ed5d3fd952 Mon Sep 17 00:00:00 2001 From: Chris McCormick Date: Fri, 23 May 2025 11:40:27 +0800 Subject: [PATCH 282/401] Run smoke test on release workflow success. --- .github/workflows/smoke-test-release.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/smoke-test-release.yml b/.github/workflows/smoke-test-release.yml index bc40a65..6b6ebba 100644 --- a/.github/workflows/smoke-test-release.yml +++ b/.github/workflows/smoke-test-release.yml @@ -1,13 +1,17 @@ name: Smoke test the binary on: - push: + workflow_run: + workflows: ["build cli for all platforms"] + types: + - completed branches: - master jobs: smoke-test-linux-amd64: runs-on: ubuntu-latest + if: ${{ github.event.workflow_run.conclusion == 'success' }} steps: - name: Download and smoke test latest binary run: | From 0073c9bdf197d667d99c0fb8c92aa5eb10ab51ec Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Fri, 23 May 2025 07:52:19 -0300 Subject: [PATCH 283/401] compile tests again. --- cli_test.go | 3 +-- helpers.go | 2 +- helpers_key.go | 2 +- mcp.go | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/cli_test.go b/cli_test.go index f9cf6c4..d44891e 100644 --- a/cli_test.go +++ b/cli_test.go @@ -17,10 +17,9 @@ import ( func call(t *testing.T, cmd string) string { var output strings.Builder - stdout = func(a ...any) (int, error) { + stdout = func(a ...any) { output.WriteString(fmt.Sprint(a...)) output.WriteString("\n") - return 0, nil } err := app.Run(t.Context(), strings.Split(cmd, " ")) require.NoError(t, err) diff --git a/helpers.go b/helpers.go index a4f8a86..8110b23 100644 --- a/helpers.go +++ b/helpers.go @@ -424,7 +424,7 @@ func askConfirmation(msg string) bool { } defer tty.Close() - fmt.Fprintf(os.Stderr, color.YellowString(msg)) + log(color.YellowString(msg)) answer, err := tty.ReadString() if err != nil { return false diff --git a/helpers_key.go b/helpers_key.go index b06fb8c..c4fff11 100644 --- a/helpers_key.go +++ b/helpers_key.go @@ -139,7 +139,7 @@ func askPassword(msg string, shouldAskAgain func(answer string) bool) (string, e defer tty.Close() for { // print the prompt to stderr so it's visible to the user - fmt.Fprintf(os.Stderr, color.YellowString(msg)) + log(color.YellowString(msg)) // read password from TTY with masking password, err := tty.ReadPassword() diff --git a/mcp.go b/mcp.go index 5ab4250..bbf1062 100644 --- a/mcp.go +++ b/mcp.go @@ -169,7 +169,7 @@ var mcpServer = &cli.Command{ l++ pm, _ := sdk.ParseMetadata(result.Event) res.WriteString(fmt.Sprintf("\n\nResult %d\nUser name: \"%s\"\nPublic key: \"%s\"\nDescription: \"%s\"\n", - l, pm.ShortName, pm.PubKey.Hex(), pm.About)) + l, pm.ShortName(), pm.PubKey.Hex(), pm.About)) if l >= int(limit) { break From 239dd2d42acf00a957c3424237c979a132d4abf2 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sun, 25 May 2025 23:34:05 -0300 Subject: [PATCH 284/401] add example of recording and publishing a voice note. --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index f75241a..83c6d29 100644 --- a/README.md +++ b/README.md @@ -271,6 +271,11 @@ echo "#surely you're joking, mr npub1l2vyh47mk2p0qlsku7hg0vn29faehy9hy34ygaclpn6 # and there is a --confirm flag that gives you a chance to confirm before actually publishing the result to relays. ``` +### record and publish an audio note of 10s (yakbak etc) signed from a bunker +```shell +ffmpeg -f alsa -i default -f webm -t 00:00:03 pipe:1 | nak blossom --server blossom.primal.net upload | jq -rc '{content: .url}' | nak event -k 1222 --sec 'bunker://urlgoeshere' pyramid.fiatjaf.com nostr.wine +``` + ## contributing to this repository Use NIP-34 to send your patches to `naddr1qqpkucttqy28wumn8ghj7un9d3shjtnwdaehgu3wvfnsz9nhwden5te0wfjkccte9ehx7um5wghxyctwvsq3gamnwvaz7tmjv4kxz7fwv3sk6atn9e5k7q3q80cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsxpqqqpmej2wctpn`. From a6509909d0d83405a5be57fbc1831f6bb96b316e Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sun, 8 Jun 2025 10:50:03 -0300 Subject: [PATCH 285/401] nostrfs: update pointer thing from nostrlib. --- nostrfs/eventdir.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nostrfs/eventdir.go b/nostrfs/eventdir.go index ced7a71..9cf875b 100644 --- a/nostrfs/eventdir.go +++ b/nostrfs/eventdir.go @@ -175,7 +175,7 @@ func (r *NostrRoot) CreateEventDir( if event.Kind == 1 { if pointer := nip10.GetThreadRoot(event.Tags); pointer != nil { - nevent := nip19.EncodePointer(*pointer) + nevent := nip19.EncodePointer(pointer) h.AddChild("@root", h.NewPersistentInode( r.ctx, &fs.MemSymlink{ @@ -185,7 +185,7 @@ func (r *NostrRoot) CreateEventDir( ), true) } if pointer := nip10.GetImmediateParent(event.Tags); pointer != nil { - nevent := nip19.EncodePointer(*pointer) + nevent := nip19.EncodePointer(pointer) h.AddChild("@parent", h.NewPersistentInode( r.ctx, &fs.MemSymlink{ From fa63dbfea339037b7ab01efd83f92c514bdc4695 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Fri, 20 Jun 2025 11:06:09 -0300 Subject: [PATCH 286/401] release v0.14.3 --- go.mod | 12 ++++++------ go.sum | 11 +++++++++++ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 35e6d24..ca570f4 100644 --- a/go.mod +++ b/go.mod @@ -4,9 +4,9 @@ go 1.24.1 require ( fiatjaf.com/lib v0.3.1 - fiatjaf.com/nostr v0.0.0-20250522115245-f38ce069a93d + fiatjaf.com/nostr v0.0.0-20250610194330-027d016d9706 github.com/bep/debounce v1.2.1 - github.com/btcsuite/btcd/btcec/v2 v2.3.4 + github.com/btcsuite/btcd/btcec/v2 v2.3.5 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 @@ -19,7 +19,7 @@ require ( github.com/mattn/go-tty v0.0.7 github.com/stretchr/testify v1.10.0 github.com/urfave/cli/v3 v3.0.0-beta1 - golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 + golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b golang.org/x/term v0.30.0 ) @@ -30,7 +30,7 @@ require ( github.com/btcsuite/btcd v0.24.2 // indirect github.com/btcsuite/btcd/btcutil v1.1.5 // indirect github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect - github.com/bytedance/sonic v1.13.2 // indirect + github.com/bytedance/sonic v1.13.3 // indirect github.com/bytedance/sonic/loader v0.2.4 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chzyer/logex v1.1.10 // indirect @@ -77,10 +77,10 @@ require ( github.com/wasilibs/go-re2 v1.3.0 // indirect github.com/x448/float16 v0.8.4 // indirect go.opencensus.io v0.24.0 // indirect - golang.org/x/arch v0.17.0 // indirect + golang.org/x/arch v0.18.0 // indirect golang.org/x/crypto v0.36.0 // indirect golang.org/x/net v0.37.0 // indirect - golang.org/x/sync v0.14.0 // indirect + golang.org/x/sync v0.15.0 // indirect golang.org/x/sys v0.33.0 // indirect golang.org/x/text v0.23.0 // indirect google.golang.org/protobuf v1.36.2 // indirect diff --git a/go.sum b/go.sum index 5755f0b..7307bf6 100644 --- a/go.sum +++ b/go.sum @@ -5,6 +5,8 @@ fiatjaf.com/nostr v0.0.0-20250521022139-d3fb25441ab3 h1:JRtme8g4UQ5KYlxI31wBa8YM fiatjaf.com/nostr v0.0.0-20250521022139-d3fb25441ab3/go.mod h1:VPs38Fc8J1XAErV750CXAmMUqIq3XEX9VZVj/LuQzzM= fiatjaf.com/nostr v0.0.0-20250522115245-f38ce069a93d h1:sl/BOXW5eK7v+cchMMEZvnzQW+n/jWiHGQn+CRt5m5Q= fiatjaf.com/nostr v0.0.0-20250522115245-f38ce069a93d/go.mod h1:VPs38Fc8J1XAErV750CXAmMUqIq3XEX9VZVj/LuQzzM= +fiatjaf.com/nostr v0.0.0-20250610194330-027d016d9706 h1:G0xS5h9dsbODWh+f8rYvDkY328h79MsNs2dGPGqm8nY= +fiatjaf.com/nostr v0.0.0-20250610194330-027d016d9706/go.mod h1:VPs38Fc8J1XAErV750CXAmMUqIq3XEX9VZVj/LuQzzM= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/FastFilter/xorfilter v0.2.1 h1:lbdeLG9BdpquK64ZsleBS8B4xO/QW1IM0gMzF7KaBKc= github.com/FastFilter/xorfilter v0.2.1/go.mod h1:aumvdkhscz6YBZF9ZA/6O4fIoNod4YR50kIVGGZ7l9I= @@ -26,6 +28,8 @@ github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v5 github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= github.com/btcsuite/btcd/btcec/v2 v2.3.4 h1:3EJjcN70HCu/mwqlUsGK8GcNVyLVxFDlWurTXGPFfiQ= github.com/btcsuite/btcd/btcec/v2 v2.3.4/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= +github.com/btcsuite/btcd/btcec/v2 v2.3.5 h1:dpAlnAwmT1yIBm3exhT1/8iUSD98RDJM5vqJVQDQLiU= +github.com/btcsuite/btcd/btcec/v2 v2.3.5/go.mod h1:m22FrOAiuxl/tht9wIqAoGHcbnCCaPWyauO8y2LGGtQ= github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A= github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE= github.com/btcsuite/btcd/btcutil v1.1.5 h1:+wER79R5670vs/ZusMTF1yTcRYE5GUsFbdjdisflzM8= @@ -45,6 +49,8 @@ github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtE github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ= github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= +github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0= +github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= 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= @@ -245,6 +251,8 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= golang.org/x/arch v0.17.0 h1:4O3dfLzd+lQewptAHqjewQZQDyEdejz3VwgeYwkZneU= golang.org/x/arch v0.17.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= +golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc= +golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -253,6 +261,8 @@ golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZv golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI= golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= @@ -274,6 +284,7 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/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= From 35ea2582d814ee2d4855fd27a2789c26f1ea2186 Mon Sep 17 00:00:00 2001 From: Rui Chen Date: Fri, 20 Jun 2025 15:18:17 -0400 Subject: [PATCH 287/401] fix: update go.sum to fix build Signed-off-by: Rui Chen --- go.sum | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/go.sum b/go.sum index 7307bf6..97f21c9 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,6 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= fiatjaf.com/lib v0.3.1 h1:/oFQwNtFRfV+ukmOCxfBEAuayoLwXp4wu2/fz5iHpwA= fiatjaf.com/lib v0.3.1/go.mod h1:Ycqq3+mJ9jAWu7XjbQI1cVr+OFgnHn79dQR5oTII47g= -fiatjaf.com/nostr v0.0.0-20250521022139-d3fb25441ab3 h1:JRtme8g4UQ5KYlxI31wBa8YMWmAxvxdwtNn+PiI/XCs= -fiatjaf.com/nostr v0.0.0-20250521022139-d3fb25441ab3/go.mod h1:VPs38Fc8J1XAErV750CXAmMUqIq3XEX9VZVj/LuQzzM= -fiatjaf.com/nostr v0.0.0-20250522115245-f38ce069a93d h1:sl/BOXW5eK7v+cchMMEZvnzQW+n/jWiHGQn+CRt5m5Q= -fiatjaf.com/nostr v0.0.0-20250522115245-f38ce069a93d/go.mod h1:VPs38Fc8J1XAErV750CXAmMUqIq3XEX9VZVj/LuQzzM= fiatjaf.com/nostr v0.0.0-20250610194330-027d016d9706 h1:G0xS5h9dsbODWh+f8rYvDkY328h79MsNs2dGPGqm8nY= fiatjaf.com/nostr v0.0.0-20250610194330-027d016d9706/go.mod h1:VPs38Fc8J1XAErV750CXAmMUqIq3XEX9VZVj/LuQzzM= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= @@ -26,8 +22,6 @@ github.com/btcsuite/btcd v0.24.2 h1:aLmxPguqxza+4ag8R1I2nnJjSu2iFn/kqtHTIImswcY= github.com/btcsuite/btcd v0.24.2/go.mod h1:5C8ChTkl5ejr3WHj8tkQSCmydiMEPB0ZhQhehpq7Dgg= github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA= github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= -github.com/btcsuite/btcd/btcec/v2 v2.3.4 h1:3EJjcN70HCu/mwqlUsGK8GcNVyLVxFDlWurTXGPFfiQ= -github.com/btcsuite/btcd/btcec/v2 v2.3.4/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= github.com/btcsuite/btcd/btcec/v2 v2.3.5 h1:dpAlnAwmT1yIBm3exhT1/8iUSD98RDJM5vqJVQDQLiU= github.com/btcsuite/btcd/btcec/v2 v2.3.5/go.mod h1:m22FrOAiuxl/tht9wIqAoGHcbnCCaPWyauO8y2LGGtQ= github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A= @@ -47,8 +41,6 @@ github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= -github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ= -github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0= github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= @@ -249,8 +241,6 @@ github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZ github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -golang.org/x/arch v0.17.0 h1:4O3dfLzd+lQewptAHqjewQZQDyEdejz3VwgeYwkZneU= -golang.org/x/arch v0.17.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc= golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -259,8 +249,6 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI= -golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ= golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -282,8 +270,7 @@ golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAG golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= -golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= From bd5569955cd36f58cd8c85c5606d343b39c5e22f Mon Sep 17 00:00:00 2001 From: Anthony Accioly <1591739+aaccioly@users.noreply.github.com> Date: Fri, 20 Jun 2025 23:11:12 +0100 Subject: [PATCH 288/401] fix(helpers): add timeout and verbose logging for bunker connection - Add a 10-second timeout to the bunker connection process using context - Include detailed verbose logging for debugging. --- helpers_key.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/helpers_key.go b/helpers_key.go index c4fff11..782724a 100644 --- a/helpers_key.go +++ b/helpers_key.go @@ -2,9 +2,11 @@ package main import ( "context" + "errors" "fmt" "os" "strings" + "time" "fiatjaf.com/nostr" "fiatjaf.com/nostr/keyer" @@ -75,10 +77,21 @@ func gatherSecretKeyOrBunkerFromArguments(ctx context.Context, c *cli.Command) ( clientKey = nostr.Generate() } + logverbose("[nip46]: connecting to bunker %s with client key %s", bunkerURL, clientKey.Hex()) + + ctx, cancel := context.WithTimeout(ctx, 10*time.Second) + defer cancel() bunker, err := nip46.ConnectBunker(ctx, clientKey, bunkerURL, nil, func(s string) { log(color.CyanString("[nip46]: open the following URL: %s"), s) }) + if err != nil { + if errors.Is(ctx.Err(), context.DeadlineExceeded) { + err = fmt.Errorf("timeout waiting for bunker to respond: %w", err) + } + return nostr.SecretKey{}, nil, fmt.Errorf("failed to connect to bunker %s: %w", bunkerURL, err) + } + return nostr.SecretKey{}, bunker, err } From fba83ea39e2d4d6e854df864502b2b59513480ff Mon Sep 17 00:00:00 2001 From: Anthony Accioly <1591739+aaccioly@users.noreply.github.com> Date: Sat, 21 Jun 2025 00:46:39 +0100 Subject: [PATCH 289/401] docs(readme): clarify NIP-46 signing with remote bunker - Add example linking to Amber for NIP-46 bunker usage. - Include note on setting `NOSTR_CLIENT_KEY` --- README.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 83c6d29..b86ba59 100644 --- a/README.md +++ b/README.md @@ -128,11 +128,20 @@ type the password to decrypt your secret key: ********** 985d66d2644dfa7676e26046914470d66ebc7fa783a3f57f139fde32d0d631d7 ``` -### sign an event using a remote NIP-46 bunker +### sign an event using a remote NIP-46 bunker (e.g. [Amber](https://github.com/greenart7c3/Amber)) ```shell +~> export NOSTR_CLIENT_KEY="$(nak key generate)" ~> nak event --sec 'bunker://a9e0f110f636f3191644110c19a33448daf09d7cda9708a769e91b7e91340208?relay=wss%3A%2F%2Frelay.damus.io&relay=wss%3A%2F%2Frelay.nsecbunker.com&relay=wss%3A%2F%2Fnos.lol&secret=TWfGbjQCLxUf' -c 'hello from bunker' ``` +> [!IMPORTANT] +> If you don't set `NOSTR_CLIENT_KEY`, nak will generate a new client key on every run. +This may cause signing to fail or trigger repeated authorization requests, depending on the remote signer setup. +To avoid this, consider setting a fixed `NOSTR_CLIENT_KEY` in your shell configuration file, e.g., for `bash`: +> ```shell +> echo 'export NOSTR_CLIENT_KEY="$(nak key generate)"' >> ~/.bashrc +> ``` + ### sign an event using a NIP-49 encrypted key ```shell ~> nak event --sec ncryptsec1qggx54cg270zy9y8krwmfz29jyypsuxken2fkk99gr52qhje968n6mwkrfstqaqhq9eq94pnzl4nff437l4lp4ur2cs4f9um8738s35l2esx2tas48thtfhrk5kq94pf9j2tpk54yuermra0xu6hl5ls -c 'hello from encrypted key' From 89ec8b982261acc248a773613c1f4657a7349e6d Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Fri, 20 Jun 2025 21:05:29 -0300 Subject: [PATCH 290/401] simplify README about $NOSTR_CLIENT_KEY. --- README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index b86ba59..9a83326 100644 --- a/README.md +++ b/README.md @@ -128,16 +128,14 @@ type the password to decrypt your secret key: ********** 985d66d2644dfa7676e26046914470d66ebc7fa783a3f57f139fde32d0d631d7 ``` -### sign an event using a remote NIP-46 bunker (e.g. [Amber](https://github.com/greenart7c3/Amber)) +### sign an event using [Amber](https://github.com/greenart7c3/Amber) (or other bunker provider) ```shell ~> export NOSTR_CLIENT_KEY="$(nak key generate)" ~> nak event --sec 'bunker://a9e0f110f636f3191644110c19a33448daf09d7cda9708a769e91b7e91340208?relay=wss%3A%2F%2Frelay.damus.io&relay=wss%3A%2F%2Frelay.nsecbunker.com&relay=wss%3A%2F%2Fnos.lol&secret=TWfGbjQCLxUf' -c 'hello from bunker' ``` > [!IMPORTANT] -> If you don't set `NOSTR_CLIENT_KEY`, nak will generate a new client key on every run. -This may cause signing to fail or trigger repeated authorization requests, depending on the remote signer setup. -To avoid this, consider setting a fixed `NOSTR_CLIENT_KEY` in your shell configuration file, e.g., for `bash`: +> Remember to set a `NOSTR_CLIENT_KEY` permanently on your shell, otherwise you'll only be able to use the bunker once. For `bash`: > ```shell > echo 'export NOSTR_CLIENT_KEY="$(nak key generate)"' >> ~/.bashrc > ``` From 1e237b4c4265bef75c0af69bd5ba852f199c738c Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Mon, 23 Jun 2025 17:57:44 -0300 Subject: [PATCH 291/401] do not fill .Content when "content" is received empty from stdin. fixes https://github.com/fiatjaf/nak/issues/71 --- event.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/event.go b/event.go index d385e76..0a205fc 100644 --- a/event.go +++ b/event.go @@ -168,6 +168,7 @@ example: evt.Content = "" kindWasSupplied := strings.Contains(stdinEvent, `"kind"`) + contentWasSupplied := strings.Contains(stdinEvent, `"content"`) mustRehashAndResign := false if err := easyjson.Unmarshal([]byte(stdinEvent), &evt); err != nil { @@ -194,7 +195,7 @@ example: evt.Content = content } mustRehashAndResign = true - } else if evt.Content == "" && evt.Kind == 1 { + } else if !contentWasSupplied && evt.Content == "" && evt.Kind == 1 { evt.Content = "hello from the nostr army knife" mustRehashAndResign = true } From 79cbc57dde14115eed45b1f761a9d47b1575444a Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Fri, 27 Jun 2025 13:48:07 -0300 Subject: [PATCH 292/401] fix main command error handler printing wrongly formatted stuff. --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index d9c2ff8..5f5e04b 100644 --- a/main.go +++ b/main.go @@ -124,7 +124,7 @@ func main() { if err := app.Run(context.Background(), os.Args); err != nil { if err != nil { - log(color.YellowString(err.Error()) + "\n") + log("%s\n", color.YellowString(err.Error())) } colors.reset() os.Exit(1) From 1e9be3ed84614952230f6b77f50ab7d55c5009fb Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Fri, 27 Jun 2025 13:48:36 -0300 Subject: [PATCH 293/401] nak filter --- filter.go | 96 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ main.go | 1 + 2 files changed, 97 insertions(+) create mode 100644 filter.go diff --git a/filter.go b/filter.go new file mode 100644 index 0000000..b8edc16 --- /dev/null +++ b/filter.go @@ -0,0 +1,96 @@ +package main + +import ( + "context" + "fmt" + + "fiatjaf.com/nostr" + "github.com/mailru/easyjson" + "github.com/urfave/cli/v3" +) + +var filter = &cli.Command{ + Name: "filter", + Usage: "applies an event filter to an event to see if it matches.", + Description: ` +example: + echo '{"kind": 1, "content": "hello"}' | nak filter -k 1 + nak filter '{"kind": 1, "content": "hello"}' -k 1 + nak filter '{"kind": 1, "content": "hello"}' '{"kinds": [1]}' -k 0 +`, + DisableSliceFlagSeparator: true, + Flags: reqFilterFlags, + ArgsUsage: "[event_json] [base_filter_json]", + Action: func(ctx context.Context, c *cli.Command) error { + args := c.Args().Slice() + + var baseFilter nostr.Filter + var baseEvent nostr.Event + + if len(args) == 2 { + // two arguments: first is event, second is base filter + if err := easyjson.Unmarshal([]byte(args[0]), &baseEvent); err != nil { + return fmt.Errorf("invalid base event: %w", err) + } + if err := easyjson.Unmarshal([]byte(args[1]), &baseFilter); err != nil { + return fmt.Errorf("invalid base filter: %w", err) + } + } else if len(args) == 1 { + if isPiped() { + // one argument + stdin: argument is base filter + if err := easyjson.Unmarshal([]byte(args[0]), &baseFilter); err != nil { + return fmt.Errorf("invalid base filter: %w", err) + } + } else { + // one argument, no stdin: argument is event + if err := easyjson.Unmarshal([]byte(args[0]), &baseEvent); err != nil { + return fmt.Errorf("invalid base event: %w", err) + } + } + } + + // apply flags to filter + if err := applyFlagsToFilter(c, &baseFilter); err != nil { + return err + } + + // if there is no stdin we'll still get an empty object here + for evtj := range getJsonsOrBlank() { + var evt nostr.Event + if err := easyjson.Unmarshal([]byte(evtj), &evt); err != nil { + ctx = lineProcessingError(ctx, "invalid event: %s", err) + continue + } + + // merge that with the base event + if evt.ID == nostr.ZeroID { + evt.ID = baseEvent.ID + } + if evt.PubKey == nostr.ZeroPK { + evt.PubKey = baseEvent.PubKey + } + if evt.Sig == [64]byte{} { + evt.Sig = baseEvent.Sig + } + if evt.Content == "" { + evt.Content = baseEvent.Content + } + if len(evt.Tags) == 0 { + evt.Tags = baseEvent.Tags + } + if evt.CreatedAt == 0 { + evt.CreatedAt = baseEvent.CreatedAt + } + + if baseFilter.Matches(evt) { + stdout(evt) + } else { + fmt.Println(baseFilter.LimitZero) + logverbose("event %s didn't match %s", evt, baseFilter) + } + } + + exitIfLineProcessingError(ctx) + return nil + }, +} diff --git a/main.go b/main.go index 5f5e04b..9545804 100644 --- a/main.go +++ b/main.go @@ -27,6 +27,7 @@ var app = &cli.Command{ Commands: []*cli.Command{ event, req, + filter, fetch, count, decode, From 550c89d8d70b557cb46a20a0be291769cb9f4426 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Fri, 27 Jun 2025 13:50:28 -0300 Subject: [PATCH 294/401] slightly improve some error messages. --- event.go | 5 +++++ helpers_key.go | 6 +++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/event.go b/event.go index 0a205fc..f8b3705 100644 --- a/event.go +++ b/event.go @@ -2,6 +2,7 @@ package main import ( "context" + "errors" "fmt" "os" "slices" @@ -9,6 +10,7 @@ import ( "time" "fiatjaf.com/nostr" + "fiatjaf.com/nostr/keyer" "fiatjaf.com/nostr/nip13" "fiatjaf.com/nostr/nip19" "github.com/fatih/color" @@ -288,6 +290,9 @@ example: return nil } } else if err := kr.SignEvent(ctx, &evt); err != nil { + if _, isBunker := kr.(keyer.BunkerSigner); isBunker && errors.Is(ctx.Err(), context.DeadlineExceeded) { + err = fmt.Errorf("timeout waiting for bunker to respond") + } return fmt.Errorf("error signing with provided key: %w", err) } } diff --git a/helpers_key.go b/helpers_key.go index 782724a..2c21fa5 100644 --- a/helpers_key.go +++ b/helpers_key.go @@ -77,19 +77,19 @@ func gatherSecretKeyOrBunkerFromArguments(ctx context.Context, c *cli.Command) ( clientKey = nostr.Generate() } - logverbose("[nip46]: connecting to bunker %s with client key %s", bunkerURL, clientKey.Hex()) + logverbose("[nip46]: connecting to %s with client key %s", bunkerURL, clientKey.Hex()) ctx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() + bunker, err := nip46.ConnectBunker(ctx, clientKey, bunkerURL, nil, func(s string) { log(color.CyanString("[nip46]: open the following URL: %s"), s) }) - if err != nil { if errors.Is(ctx.Err(), context.DeadlineExceeded) { err = fmt.Errorf("timeout waiting for bunker to respond: %w", err) } - return nostr.SecretKey{}, nil, fmt.Errorf("failed to connect to bunker %s: %w", bunkerURL, err) + return nostr.SecretKey{}, nil, fmt.Errorf("failed to connect to %s: %w", bunkerURL, err) } return nostr.SecretKey{}, bunker, err From 55c9d4ee456e81557f0b93ec269d80a367a0f7ac Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Fri, 27 Jun 2025 16:28:21 -0300 Subject: [PATCH 295/401] remove the bunker context timeout because it causes the entire bunker to disconnect. --- helpers_key.go | 8 -------- 1 file changed, 8 deletions(-) diff --git a/helpers_key.go b/helpers_key.go index 2c21fa5..7f3285b 100644 --- a/helpers_key.go +++ b/helpers_key.go @@ -2,11 +2,9 @@ package main import ( "context" - "errors" "fmt" "os" "strings" - "time" "fiatjaf.com/nostr" "fiatjaf.com/nostr/keyer" @@ -79,16 +77,10 @@ func gatherSecretKeyOrBunkerFromArguments(ctx context.Context, c *cli.Command) ( logverbose("[nip46]: connecting to %s with client key %s", bunkerURL, clientKey.Hex()) - ctx, cancel := context.WithTimeout(ctx, 10*time.Second) - defer cancel() - bunker, err := nip46.ConnectBunker(ctx, clientKey, bunkerURL, nil, func(s string) { log(color.CyanString("[nip46]: open the following URL: %s"), s) }) if err != nil { - if errors.Is(ctx.Err(), context.DeadlineExceeded) { - err = fmt.Errorf("timeout waiting for bunker to respond: %w", err) - } return nostr.SecretKey{}, nil, fmt.Errorf("failed to connect to %s: %w", bunkerURL, err) } From 6e4a546212d2b50f3affa41034ce96df906b0263 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Fri, 27 Jun 2025 16:34:59 -0300 Subject: [PATCH 296/401] release with fixes. --- go.mod | 4 ++-- go.sum | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index ca570f4..4d9a7a7 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.24.1 require ( fiatjaf.com/lib v0.3.1 - fiatjaf.com/nostr v0.0.0-20250610194330-027d016d9706 + fiatjaf.com/nostr v0.0.0-20250627165101-028a1637fbd0 github.com/bep/debounce v1.2.1 github.com/btcsuite/btcd/btcec/v2 v2.3.5 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e @@ -55,7 +55,7 @@ require ( github.com/jalaali/go-jalaali v0.0.0-20210801064154-80525e88d958 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/klauspost/compress v1.18.0 // indirect - github.com/klauspost/cpuid/v2 v2.2.10 // indirect + github.com/klauspost/cpuid/v2 v2.2.11 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/magefile/mage v1.14.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect diff --git a/go.sum b/go.sum index 97f21c9..d8aaade 100644 --- a/go.sum +++ b/go.sum @@ -3,6 +3,8 @@ fiatjaf.com/lib v0.3.1 h1:/oFQwNtFRfV+ukmOCxfBEAuayoLwXp4wu2/fz5iHpwA= fiatjaf.com/lib v0.3.1/go.mod h1:Ycqq3+mJ9jAWu7XjbQI1cVr+OFgnHn79dQR5oTII47g= fiatjaf.com/nostr v0.0.0-20250610194330-027d016d9706 h1:G0xS5h9dsbODWh+f8rYvDkY328h79MsNs2dGPGqm8nY= fiatjaf.com/nostr v0.0.0-20250610194330-027d016d9706/go.mod h1:VPs38Fc8J1XAErV750CXAmMUqIq3XEX9VZVj/LuQzzM= +fiatjaf.com/nostr v0.0.0-20250627165101-028a1637fbd0 h1:Se07jECWueD3fZyaHO08oIzFOPwT6A6wNPQ8QWccX5c= +fiatjaf.com/nostr v0.0.0-20250627165101-028a1637fbd0/go.mod h1:VPs38Fc8J1XAErV750CXAmMUqIq3XEX9VZVj/LuQzzM= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/FastFilter/xorfilter v0.2.1 h1:lbdeLG9BdpquK64ZsleBS8B4xO/QW1IM0gMzF7KaBKc= github.com/FastFilter/xorfilter v0.2.1/go.mod h1:aumvdkhscz6YBZF9ZA/6O4fIoNod4YR50kIVGGZ7l9I= @@ -155,6 +157,8 @@ github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYW github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/klauspost/cpuid/v2 v2.2.11 h1:0OwqZRYI2rFrjS4kvkDnqJkKHdHaRnCm68/DY4OxRzU= +github.com/klauspost/cpuid/v2 v2.2.11/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= From 0aef173e8be4dec1a597457a8b358beb7fbc2e21 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 1 Jul 2025 11:40:34 -0300 Subject: [PATCH 297/401] nak bunker --persist/--profile --- bunker.go | 289 ++++++++++++++++++++++++++++++++++++++++++------- helpers_key.go | 6 +- main.go | 10 +- outbox.go | 5 - 4 files changed, 265 insertions(+), 45 deletions(-) diff --git a/bunker.go b/bunker.go index ff5169c..af02e1f 100644 --- a/bunker.go +++ b/bunker.go @@ -1,10 +1,13 @@ package main import ( + "bytes" "context" + "encoding/hex" "fmt" "net/url" "os" + "path/filepath" "slices" "strings" "sync" @@ -17,6 +20,8 @@ import ( "github.com/urfave/cli/v3" ) +const PERSISTENCE = "PERSISTENCE" + var bunker = &cli.Command{ Name: "bunker", Usage: "starts a nip46 signer daemon with the given --sec key", @@ -24,6 +29,18 @@ var bunker = &cli.Command{ Description: ``, DisableSliceFlagSeparator: true, Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "persist", + Usage: "whether to read and store authorized keys from and to a config file", + Category: PERSISTENCE, + }, + &cli.StringFlag{ + Name: "profile", + Value: "default", + Usage: "config file name to use for --persist mode (implies that if provided) -- based on --config-path, i.e. ~/.config/nak/", + OnlyOnce: true, + Category: PERSISTENCE, + }, &cli.StringFlag{ Name: "sec", Usage: "secret key to sign the event, as hex or nsec", @@ -43,34 +60,147 @@ var bunker = &cli.Command{ Aliases: []string{"k"}, Usage: "pubkeys for which we will always respond", }, + &cli.StringSliceFlag{ + Name: "relay", + Usage: "relays to connect to (can also be provided as naked arguments)", + Hidden: true, + }, }, Action: func(ctx context.Context, c *cli.Command) error { + // read config from file + config := struct { + AuthorizedKeys []nostr.PubKey `json:"authorized-keys"` + Secret plainOrEncryptedKey `json:"sec"` + Relays []string `json:"relays"` + }{ + AuthorizedKeys: make([]nostr.PubKey, 0, 3), + } + baseRelaysUrls := appendUnique(c.Args().Slice(), c.StringSlice("relay")...) + for i, url := range baseRelaysUrls { + baseRelaysUrls[i] = nostr.NormalizeURL(url) + } + baseAuthorizedKeys := getPubKeySlice(c, "authorized-keys") + + var baseSecret plainOrEncryptedKey + { + sec := c.String("sec") + if c.Bool("prompt-sec") { + var err error + sec, err = askPassword("type your secret key as ncryptsec, nsec or hex: ", nil) + if err != nil { + return fmt.Errorf("failed to get secret key: %w", err) + } + } + if strings.HasPrefix(sec, "ncryptsec1") { + baseSecret.Encrypted = &sec + } else { + if sec == "" { + sec = os.Getenv("NOSTR_SECRET_KEY") + if sec == "" { + sec = defaultKey + } + } + if prefix, ski, err := nip19.Decode(sec); err == nil && prefix == "nsec" { + sk := ski.(nostr.SecretKey) + baseSecret.Plain = &sk + } else if sk, err := nostr.SecretKeyFromHex(sec); err != nil { + return fmt.Errorf("invalid secret key: %w", err) + } else { + baseSecret.Plain = &sk + } + } + } + + // default case: persist() is nil + var persist func() + + if c.Bool("persist") || c.IsSet("profile") { + path := filepath.Join(c.String("config-path"), "bunker") + if err := os.MkdirAll(path, 0755); err != nil { + return err + } + path = filepath.Join(path, c.String("profile")) + + persist = func() { + if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { + log(color.RedString("failed to persist: %w\n"), err) + os.Exit(4) + } + data, err := json.MarshalIndent(config, "", " ") + if err != nil { + log(color.RedString("failed to persist: %w\n"), err) + os.Exit(4) + } + if err := os.WriteFile(path, data, 0600); err != nil { + log(color.RedString("failed to persist: %w\n"), err) + os.Exit(4) + } + } + + log(color.YellowString("reading config from %s\n"), path) + b, err := os.ReadFile(path) + if err == nil { + if err := json.Unmarshal(b, &config); err != nil { + return err + } + } else if !os.IsNotExist(err) { + return err + } + + for i, url := range config.Relays { + config.Relays[i] = nostr.NormalizeURL(url) + } + config.Relays = appendUnique(config.Relays, baseRelaysUrls...) + config.AuthorizedKeys = appendUnique(config.AuthorizedKeys, baseAuthorizedKeys...) + + if config.Secret.Plain == nil && config.Secret.Encrypted == nil { + config.Secret = baseSecret + } else if !baseSecret.equals(config.Secret) { + return fmt.Errorf("--sec provided conflicts with stored, you should create a new --profile or omit the --sec flag") + } + } else { + config.Secret = baseSecret + config.Relays = baseRelaysUrls + config.AuthorizedKeys = baseAuthorizedKeys + } + + if len(config.Relays) == 0 { + return fmt.Errorf("no relays given") + } + + // decrypt key here if necessary + var sec nostr.SecretKey + if config.Secret.Plain != nil { + sec = *config.Secret.Plain + } else { + plain, err := promptDecrypt(*config.Secret.Encrypted) + if err != nil { + return fmt.Errorf("failed to decrypt: %w", err) + } + sec = plain + } + + if persist != nil { + persist() + } + // try to connect to the relays here qs := url.Values{} - relayURLs := make([]string, 0, c.Args().Len()) - if relayUrls := c.Args().Slice(); len(relayUrls) > 0 { - 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) - } - for _, relay := range relays { - relayURLs = append(relayURLs, relay.URL) - qs.Add("relay", relay.URL) - } + relayURLs := make([]string, 0, len(config.Relays)) + relays := connectToAllRelays(ctx, c, config.Relays, nil, nostr.PoolOptions{}) + if len(relays) == 0 { + log("failed to connect to any of the given relays.\n") + os.Exit(3) + } + for _, relay := range relays { + relayURLs = append(relayURLs, relay.URL) + qs.Add("relay", relay.URL) } if len(relayURLs) == 0 { return fmt.Errorf("not connected to any relays: please specify at least one") } - // gather the secret key - sec, _, err := gatherSecretKeyOrBunkerFromArguments(ctx, c) - if err != nil { - return err - } - // other arguments - 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 @@ -87,9 +217,9 @@ var bunker = &cli.Command{ bunkerURI := fmt.Sprintf("bunker://%s?%s", pubkey.Hex(), qs.Encode()) authorizedKeysStr := "" - if len(authorizedKeys) != 0 { + if len(config.AuthorizedKeys) != 0 { authorizedKeysStr = "\n authorized keys:" - for _, pubkey := range authorizedKeys { + for _, pubkey := range config.AuthorizedKeys { authorizedKeysStr += "\n - " + colors.italic(pubkey.Hex()) } } @@ -100,7 +230,7 @@ var bunker = &cli.Command{ } preauthorizedFlags := "" - for _, k := range authorizedKeys { + for _, k := range config.AuthorizedKeys { preauthorizedFlags += " -k " + k.Hex() } for _, s := range authorizedSecrets { @@ -121,21 +251,34 @@ var bunker = &cli.Command{ } } - restartCommand := fmt.Sprintf("nak bunker %s%s %s", - secretKeyFlag, - preauthorizedFlags, - strings.Join(relayURLsPossiblyWithoutSchema, " "), - ) + // only print the restart command if not persisting: + if persist == nil { + restartCommand := fmt.Sprintf("nak bunker %s%s %s", + secretKeyFlag, + preauthorizedFlags, + strings.Join(relayURLsPossiblyWithoutSchema, " "), + ) - log("listening at %v:\n pubkey: %s \n npub: %s%s%s\n to restart: %s\n bunker: %s\n\n", - colors.bold(relayURLs), - colors.bold(pubkey.Hex()), - colors.bold(npub), - authorizedKeysStr, - authorizedSecretsStr, - color.CyanString(restartCommand), - colors.bold(bunkerURI), - ) + log("listening at %v:\n pubkey: %s \n npub: %s%s%s\n to restart: %s\n bunker: %s\n\n", + colors.bold(relayURLs), + colors.bold(pubkey.Hex()), + colors.bold(npub), + authorizedKeysStr, + authorizedSecretsStr, + color.CyanString(restartCommand), + colors.bold(bunkerURI), + ) + } else { + // otherwise just print the data + log("listening at %v:\n pubkey: %s \n npub: %s%s%s\n bunker: %s\n\n", + colors.bold(relayURLs), + colors.bold(pubkey.Hex()), + colors.bold(npub), + authorizedKeysStr, + authorizedSecretsStr, + colors.bold(bunkerURI), + ) + } } printBunkerInfo() @@ -162,7 +305,7 @@ var bunker = &cli.Command{ signer.AuthorizeRequest = func(harmless bool, from nostr.PubKey, secret string) bool { if secret == newSecret { // store this key - authorizedKeys = append(authorizedKeys, from) + config.AuthorizedKeys = appendUnique(config.AuthorizedKeys, from) // discard this and generate a new secret newSecret = randString(12) // print bunker info again after this @@ -170,9 +313,13 @@ var bunker = &cli.Command{ time.Sleep(3 * time.Second) printBunkerInfo() }() + + if persist != nil { + persist() + } } - return slices.Contains(authorizedKeys, from) || slices.Contains(authorizedSecrets, secret) + return slices.Contains(config.AuthorizedKeys, from) || slices.Contains(authorizedSecrets, secret) } for ie := range events { @@ -248,3 +395,71 @@ var bunker = &cli.Command{ }, }, } + +type plainOrEncryptedKey struct { + Plain *nostr.SecretKey + Encrypted *string +} + +func (pe plainOrEncryptedKey) MarshalJSON() ([]byte, error) { + if pe.Plain != nil { + res := make([]byte, 66) + hex.Encode(res[1:], (*pe.Plain)[:]) + res[0] = '"' + res[65] = '"' + return res, nil + } else if pe.Encrypted != nil { + return json.Marshal(*pe.Encrypted) + } + + return nil, fmt.Errorf("no key to marshal") +} + +func (pe *plainOrEncryptedKey) UnmarshalJSON(buf []byte) error { + if len(buf) == 66 { + sk, err := nostr.SecretKeyFromHex(string(buf[1 : 1+64])) + if err != nil { + return err + } + pe.Plain = &sk + return nil + } else if bytes.HasPrefix(buf, []byte("\"nsec")) { + _, v, err := nip19.Decode(string(buf[1 : len(buf)-1])) + if err != nil { + return err + } + sk := v.(nostr.SecretKey) + pe.Plain = &sk + return nil + } else if bytes.HasPrefix(buf, []byte("\"ncryptsec1")) { + ncryptsec := string(buf[1 : len(buf)-1]) + pe.Encrypted = &ncryptsec + return nil + } + + return fmt.Errorf("unrecognized key format '%s'", string(buf)) +} + +func (a plainOrEncryptedKey) equals(b plainOrEncryptedKey) bool { + if a.Plain == nil && b.Plain != nil { + return false + } + if a.Plain != nil && b.Plain == nil { + return false + } + if a.Plain != nil && b.Plain != nil && *a.Plain != *b.Plain { + return false + } + + if a.Encrypted == nil && b.Encrypted != nil { + return false + } + if a.Encrypted != nil && b.Encrypted == nil { + return false + } + if a.Encrypted != nil && b.Encrypted != nil && *a.Encrypted != *b.Encrypted { + return false + } + + return true +} diff --git a/helpers_key.go b/helpers_key.go index 7f3285b..c578f77 100644 --- a/helpers_key.go +++ b/helpers_key.go @@ -17,14 +17,16 @@ import ( "github.com/urfave/cli/v3" ) +var defaultKey = nostr.KeyOne.Hex() + var defaultKeyFlags = []cli.Flag{ &cli.StringFlag{ Name: "sec", Usage: "secret key to sign the event, as nsec, ncryptsec or hex, or a bunker URL", - DefaultText: "the key '1'", + DefaultText: "the key '01'", Category: CATEGORY_SIGNER, Sources: cli.EnvVars("NOSTR_SECRET_KEY"), - Value: nostr.KeyOne.Hex(), + Value: defaultKey, HideDefault: true, }, &cli.BoolFlag{ diff --git a/main.go b/main.go index 9545804..8e683ef 100644 --- a/main.go +++ b/main.go @@ -6,6 +6,7 @@ import ( "net/http" "net/textproto" "os" + "path/filepath" "fiatjaf.com/nostr" "fiatjaf.com/nostr/sdk" @@ -52,6 +53,13 @@ var app = &cli.Command{ &cli.StringFlag{ Name: "config-path", Hidden: true, + Value: (func() string { + if home, err := os.UserHomeDir(); err == nil { + return filepath.Join(home, ".config/nak") + } else { + return filepath.Join("/dev/null") + } + })(), }, &cli.BoolFlag{ Name: "quiet", @@ -125,7 +133,7 @@ func main() { if err := app.Run(context.Background(), os.Args); err != nil { if err != nil { - log("%s\n", color.YellowString(err.Error())) + log("%s\n", color.RedString(err.Error())) } colors.reset() os.Exit(1) diff --git a/outbox.go b/outbox.go index 37db434..bbec2f7 100644 --- a/outbox.go +++ b/outbox.go @@ -20,11 +20,6 @@ var ( func initializeOutboxHintsDB(c *cli.Command, sys *sdk.System) error { configPath := c.String("config-path") - if configPath == "" { - if home, err := os.UserHomeDir(); err == nil { - configPath = filepath.Join(home, ".config/nak") - } - } if configPath != "" { hintsFilePath = filepath.Join(configPath, "outbox/hints.bg") } From 9c5f68a955356dda4b603107cda5314567f0d37e Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 1 Jul 2025 12:36:54 -0300 Subject: [PATCH 298/401] bunker: fix handling of provided and stored secret keys. --- bunker.go | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/bunker.go b/bunker.go index af02e1f..b0942fb 100644 --- a/bunker.go +++ b/bunker.go @@ -93,13 +93,7 @@ var bunker = &cli.Command{ } if strings.HasPrefix(sec, "ncryptsec1") { baseSecret.Encrypted = &sec - } else { - if sec == "" { - sec = os.Getenv("NOSTR_SECRET_KEY") - if sec == "" { - sec = defaultKey - } - } + } else if sec != "" { if prefix, ski, err := nip19.Decode(sec); err == nil && prefix == "nsec" { sk := ski.(nostr.SecretKey) baseSecret.Plain = &sk @@ -154,9 +148,16 @@ var bunker = &cli.Command{ config.AuthorizedKeys = appendUnique(config.AuthorizedKeys, baseAuthorizedKeys...) if config.Secret.Plain == nil && config.Secret.Encrypted == nil { + // we don't have any secret key stored, so just use whatever was given via flags config.Secret = baseSecret - } else if !baseSecret.equals(config.Secret) { - return fmt.Errorf("--sec provided conflicts with stored, you should create a new --profile or omit the --sec flag") + } else if baseSecret.Plain == nil && baseSecret.Encrypted == nil { + // we didn't provide any keys, so we just use the stored + } else { + // we have a secret key stored + // if we also provided a key we check if they match and fail otherwise + if !baseSecret.equals(config.Secret) { + return fmt.Errorf("--sec provided conflicts with stored, you should create a new --profile or omit the --sec flag") + } } } else { config.Secret = baseSecret @@ -164,6 +165,19 @@ var bunker = &cli.Command{ config.AuthorizedKeys = baseAuthorizedKeys } + // if we got here without any keys set (no flags, first time using a profile), use the default + { + sec := os.Getenv("NOSTR_SECRET_KEY") + if sec == "" { + sec = defaultKey + } + sk, err := nostr.SecretKeyFromHex(sec) + if err != nil { + return fmt.Errorf("default key is wrong: %w", err) + } + config.Secret.Plain = &sk + } + if len(config.Relays) == 0 { return fmt.Errorf("no relays given") } From ecfe3a298e1a04a39e58d99e29c2735189314b20 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 1 Jul 2025 12:42:57 -0300 Subject: [PATCH 299/401] add persisted bunker and filter examples to readme. --- README.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/README.md b/README.md index 9a83326..f98d44d 100644 --- a/README.md +++ b/README.md @@ -174,6 +174,23 @@ listening at [wss://relay.damus.io wss://nos.lol wss://relay.nsecbunker.com]: bunker: bunker://f59911b561c37c90b01e9e5c2557307380835c83399756f4d62d8167227e420a?relay=wss%3A%2F%2Frelay.damus.io&relay=wss%3A%2F%2Fnos.lol&relay=wss%3A%2F%2Frelay.nsecbunker.com&secret=XuuiMbcLwuwL ``` +### start a bunker that persists its list of authorized keys to disc +```shell +~> nak bunker --persist --sec ncryptsec1... relay.nsec.app nos.lol +``` + +then later just + +```shell +~> nak bunker --persist +``` + +or give it a named profile: + +```shell +~> nak bunker --profile myself ... +``` + ### generate a NIP-70 protected event with a date set to two weeks ago and some multi-value tags ```shell ~> nak event --ts 'two weeks ago' -t '-' -t 'e=f59911b561c37c90b01e9e5c2557307380835c83399756f4d62d8167227e420a;wss://relay.whatever.com;root;a9e0f110f636f3191644110c19a33448daf09d7cda9708a769e91b7e91340208' -t 'p=a9e0f110f636f3191644110c19a33448daf09d7cda9708a769e91b7e91340208;wss://p-relay.com' -c 'I know the future' @@ -283,6 +300,11 @@ echo "#surely you're joking, mr npub1l2vyh47mk2p0qlsku7hg0vn29faehy9hy34ygaclpn6 ffmpeg -f alsa -i default -f webm -t 00:00:03 pipe:1 | nak blossom --server blossom.primal.net upload | jq -rc '{content: .url}' | nak event -k 1222 --sec 'bunker://urlgoeshere' pyramid.fiatjaf.com nostr.wine ``` +### from a file with events get only those that have kind 1111 and were created by a given pubkey +```shell +~> cat all.jsonl | nak filter -k 1111 > filtered.jsonl +``` + ## contributing to this repository Use NIP-34 to send your patches to `naddr1qqpkucttqy28wumn8ghj7un9d3shjtnwdaehgu3wvfnsz9nhwden5te0wfjkccte9ehx7um5wghxyctwvsq3gamnwvaz7tmjv4kxz7fwv3sk6atn9e5k7q3q80cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsxpqqqpmej2wctpn`. From fd198555435d4c9a1d26a752164d1c886a90064f Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 1 Jul 2025 12:43:04 -0300 Subject: [PATCH 300/401] remove a dangling print statement. --- filter.go | 1 - 1 file changed, 1 deletion(-) diff --git a/filter.go b/filter.go index b8edc16..75e2c41 100644 --- a/filter.go +++ b/filter.go @@ -85,7 +85,6 @@ example: if baseFilter.Matches(evt) { stdout(evt) } else { - fmt.Println(baseFilter.LimitZero) logverbose("event %s didn't match %s", evt, baseFilter) } } From cc526acb10d2f24642f5d58c8b77d43cfc95a67d Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 1 Jul 2025 15:52:44 -0300 Subject: [PATCH 301/401] bunker: fix overwriting all keys always with default. --- bunker.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bunker.go b/bunker.go index b0942fb..37614ab 100644 --- a/bunker.go +++ b/bunker.go @@ -166,7 +166,7 @@ var bunker = &cli.Command{ } // if we got here without any keys set (no flags, first time using a profile), use the default - { + if config.Secret.Plain == nil && config.Secret.Encrypted == nil { sec := os.Getenv("NOSTR_SECRET_KEY") if sec == "" { sec = defaultKey From fea23aecc3d28628e7fb77435c5c199fa510fc56 Mon Sep 17 00:00:00 2001 From: mplorentz Date: Wed, 2 Jul 2025 23:28:36 -0300 Subject: [PATCH 302/401] Add Dockerfile I added this so that I could run a nak bunker on my server alongside my other containers. Thought it might be useful for others. --- .dockerignore | 36 ++++++++++++++++++++++++++++++++++++ Dockerfile | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 8 ++++++++ 3 files changed, 93 insertions(+) create mode 100644 .dockerignore create mode 100644 Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..166c79f --- /dev/null +++ b/.dockerignore @@ -0,0 +1,36 @@ +# git files +.git +.gitignore + +# documentation +README.md +LICENSE + +# development files +justfile +*.md + +# test files +*_test.go +cli_test.go + +# build artifacts +nak +*.exe +mnt + +# ide and editor files +.vscode +.idea +*.swp +*.swo +*~ + +# os generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6836670 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,49 @@ +# build stage +FROM golang:1.24-alpine AS builder + +# install git and ca-certificates (needed for fetching dependencies) +RUN apk add --no-cache git ca-certificates + +# set working directory +WORKDIR /app + +# copy go mod files first for better caching +COPY go.mod go.sum ./ + +# download dependencies +RUN go mod download + +# copy source code +COPY . . + +# build the application +# use cgo_enabled=0 to create a static binary +# use -ldflags to strip debug info and reduce binary size +RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o nak . + +# runtime stage +FROM alpine:latest + +# install ca-certificates for https requests (needed for relay connections) +RUN apk --no-cache add ca-certificates + +# create a non-root user +RUN adduser -D -s /bin/sh nakuser + +# set working directory +WORKDIR /home/nakuser + +# copy the binary from builder stage +COPY --from=builder /app/nak /usr/local/bin/nak + +# make sure the binary is executable +RUN chmod +x /usr/local/bin/nak + +# switch to non-root user +USER nakuser + +# set the entrypoint +ENTRYPOINT ["nak"] + +# default command (show help) +CMD ["--help"] diff --git a/README.md b/README.md index f98d44d..e849812 100644 --- a/README.md +++ b/README.md @@ -304,6 +304,14 @@ ffmpeg -f alsa -i default -f webm -t 00:00:03 pipe:1 | nak blossom --server blos ```shell ~> cat all.jsonl | nak filter -k 1111 > filtered.jsonl ``` +### run nak in Docker + +If you want to run nak inside a container (i.e. to run nak as a server, or to avoid installing the Go toolchain) you can run it with Docker: + +```shell +docker build -t nak . +docker run nak event +``` ## contributing to this repository From d32654447a0357d02c32c86e6ea42a4d8d0f1161 Mon Sep 17 00:00:00 2001 From: Anthony Accioly <1591739+aaccioly@users.noreply.github.com> Date: Thu, 3 Jul 2025 23:14:00 +0100 Subject: [PATCH 303/401] feat(bunker): add QR code generation for bunker URI - Add `--qrcode` flag to display a QR code for the bunker URI. - Update `README.md` with usage instructions for the new flag. - Include `qrterminal` dependency for QR code generation. --- README.md | 6 ++++++ bunker.go | 12 ++++++++++++ go.mod | 6 ++++-- go.sum | 18 ++++++++---------- 4 files changed, 30 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index e849812..84a9748 100644 --- a/README.md +++ b/README.md @@ -174,6 +174,12 @@ listening at [wss://relay.damus.io wss://nos.lol wss://relay.nsecbunker.com]: bunker: bunker://f59911b561c37c90b01e9e5c2557307380835c83399756f4d62d8167227e420a?relay=wss%3A%2F%2Frelay.damus.io&relay=wss%3A%2F%2Fnos.lol&relay=wss%3A%2F%2Frelay.nsecbunker.com&secret=XuuiMbcLwuwL ``` +You can also display a QR code for the bunker URI by adding the `--qrcode` flag: + +```shell +~> nak bunker --qrcode --sec ncryptsec1... relay.damus.io +``` + ### start a bunker that persists its list of authorized keys to disc ```shell ~> nak bunker --persist --sec ncryptsec1... relay.nsec.app nos.lol diff --git a/bunker.go b/bunker.go index 37614ab..f676a60 100644 --- a/bunker.go +++ b/bunker.go @@ -17,6 +17,7 @@ import ( "fiatjaf.com/nostr/nip19" "fiatjaf.com/nostr/nip46" "github.com/fatih/color" + "github.com/mdp/qrterminal/v3" "github.com/urfave/cli/v3" ) @@ -65,6 +66,10 @@ var bunker = &cli.Command{ Usage: "relays to connect to (can also be provided as naked arguments)", Hidden: true, }, + &cli.BoolFlag{ + Name: "qrcode", + Usage: "display a QR code for the bunker URI", + }, }, Action: func(ctx context.Context, c *cli.Command) error { // read config from file @@ -293,6 +298,13 @@ var bunker = &cli.Command{ colors.bold(bunkerURI), ) } + + // Print QR code if requested + if c.Bool("qrcode") { + log("QR Code for bunker URI:\n") + qrterminal.Generate(bunkerURI, qrterminal.L, os.Stdout) + log("\n\n") + } } printBunkerInfo() diff --git a/go.mod b/go.mod index 4d9a7a7..324484d 100644 --- a/go.mod +++ b/go.mod @@ -17,10 +17,11 @@ require ( github.com/mark3labs/mcp-go v0.8.3 github.com/markusmobius/go-dateparser v1.2.3 github.com/mattn/go-tty v0.0.7 + github.com/mdp/qrterminal/v3 v3.2.1 github.com/stretchr/testify v1.10.0 github.com/urfave/cli/v3 v3.0.0-beta1 golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b - golang.org/x/term v0.30.0 + golang.org/x/term v0.32.0 ) require ( @@ -58,7 +59,7 @@ require ( github.com/klauspost/cpuid/v2 v2.2.11 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/magefile/mage v1.14.0 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect @@ -85,4 +86,5 @@ require ( golang.org/x/text v0.23.0 // indirect google.golang.org/protobuf v1.36.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + rsc.io/qr v0.2.0 // indirect ) diff --git a/go.sum b/go.sum index d8aaade..ead2a3c 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,6 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= fiatjaf.com/lib v0.3.1 h1:/oFQwNtFRfV+ukmOCxfBEAuayoLwXp4wu2/fz5iHpwA= fiatjaf.com/lib v0.3.1/go.mod h1:Ycqq3+mJ9jAWu7XjbQI1cVr+OFgnHn79dQR5oTII47g= -fiatjaf.com/nostr v0.0.0-20250610194330-027d016d9706 h1:G0xS5h9dsbODWh+f8rYvDkY328h79MsNs2dGPGqm8nY= -fiatjaf.com/nostr v0.0.0-20250610194330-027d016d9706/go.mod h1:VPs38Fc8J1XAErV750CXAmMUqIq3XEX9VZVj/LuQzzM= fiatjaf.com/nostr v0.0.0-20250627165101-028a1637fbd0 h1:Se07jECWueD3fZyaHO08oIzFOPwT6A6wNPQ8QWccX5c= fiatjaf.com/nostr v0.0.0-20250627165101-028a1637fbd0/go.mod h1:VPs38Fc8J1XAErV750CXAmMUqIq3XEX9VZVj/LuQzzM= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= @@ -155,8 +153,6 @@ github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6 github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= -github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/klauspost/cpuid/v2 v2.2.11 h1:0OwqZRYI2rFrjS4kvkDnqJkKHdHaRnCm68/DY4OxRzU= github.com/klauspost/cpuid/v2 v2.2.11/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= @@ -172,13 +168,14 @@ github.com/mark3labs/mcp-go v0.8.3 h1:IzlyN8BaP4YwUMUDqxOGJhGdZXEDQiAPX43dNPgnzr github.com/mark3labs/mcp-go v0.8.3/go.mod h1:cjMlBU0cv/cj9kjlgmRhoJ5JREdS7YX83xeIG9Ko/jE= github.com/markusmobius/go-dateparser v1.2.3 h1:TvrsIvr5uk+3v6poDjaicnAFJ5IgtFHgLiuMY2Eb7Nw= github.com/markusmobius/go-dateparser v1.2.3/go.mod h1:cMwQRrBUQlK1UI5TIFHEcvpsMbkWrQLXuaPNMFzuYLk= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-tty v0.0.7 h1:KJ486B6qI8+wBO7kQxYgmmEFDaFEE96JMBQ7h400N8Q= github.com/mattn/go-tty v0.0.7/go.mod h1:f2i5ZOvXBU/tCABmLmOfzLz9azMo5wdAaElRNnJKr+k= +github.com/mdp/qrterminal/v3 v3.2.1 h1:6+yQjiiOsSuXT5n9/m60E54vdgFsw0zhADHhHLrFet4= +github.com/mdp/qrterminal/v3 v3.2.1/go.mod h1:jOTmXvnBsMy5xqLniO0R++Jmjs2sTm9dFSuQ5kpz/SU= github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78= github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -287,12 +284,11 @@ golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= -golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= +golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -341,3 +337,5 @@ gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY= +rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs= From 2d2e6577787542e13918acfbdc405df835918008 Mon Sep 17 00:00:00 2001 From: Anthony Accioly <1591739+aaccioly@users.noreply.github.com> Date: Fri, 4 Jul 2025 00:55:02 +0100 Subject: [PATCH 304/401] docs(readme): caution note on plaintext credential storage - Add a warning about credentials being stored in plain text when using `--persist`. --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 84a9748..e8ce64f 100644 --- a/README.md +++ b/README.md @@ -185,6 +185,9 @@ You can also display a QR code for the bunker URI by adding the `--qrcode` flag: ~> nak bunker --persist --sec ncryptsec1... relay.nsec.app nos.lol ``` +> [!CAUTION] +> When you start a bunker with `--persist`, it will store credentials in plain text at `~/.config/nak/bunker`. + then later just ```shell From e0febbf190eb73d4724c972e8a9ff980dbe80b51 Mon Sep 17 00:00:00 2001 From: Anthony Accioly <1591739+aaccioly@users.noreply.github.com> Date: Fri, 4 Jul 2025 10:13:51 +0100 Subject: [PATCH 305/401] docs(readme): remove outdated contributing section - Delete the outdated contributing section referencing NIP-34. --- README.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/README.md b/README.md index e8ce64f..2760e0c 100644 --- a/README.md +++ b/README.md @@ -320,8 +320,4 @@ If you want to run nak inside a container (i.e. to run nak as a server, or to av ```shell docker build -t nak . docker run nak event -``` - -## contributing to this repository - -Use NIP-34 to send your patches to `naddr1qqpkucttqy28wumn8ghj7un9d3shjtnwdaehgu3wvfnsz9nhwden5te0wfjkccte9ehx7um5wghxyctwvsq3gamnwvaz7tmjv4kxz7fwv3sk6atn9e5k7q3q80cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsxpqqqpmej2wctpn`. +``` \ No newline at end of file From b1114766e5f671cc89c6c96f2b1736ee2a655d67 Mon Sep 17 00:00:00 2001 From: Anthony Accioly <1591739+aaccioly@users.noreply.github.com> Date: Fri, 4 Jul 2025 10:32:49 +0100 Subject: [PATCH 306/401] docs(readme): update caution note with encryption guidance - Revise the caution note to include instructions for encrypting private keys using NIP-49. --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2760e0c..35dc71b 100644 --- a/README.md +++ b/README.md @@ -186,8 +186,11 @@ You can also display a QR code for the bunker URI by adding the `--qrcode` flag: ``` > [!CAUTION] -> When you start a bunker with `--persist`, it will store credentials in plain text at `~/.config/nak/bunker`. +> When you start a bunker with `--persist` or `--profile`, it will store `--sec` credentials and authorized keys in +> `~/.config/nak/bunker`. If you don't want your private key to be stored in plain text, you can +> [encrypt it with NIP-49](#encrypt-key-with-nip-49) it beforehand. +```shell then later just ```shell From fb377f4775b7150509dcf6fe131c1f1c82a55174 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sat, 5 Jul 2025 11:14:29 -0300 Subject: [PATCH 307/401] reword some things. --- README.md | 10 +++++----- bunker.go | 2 +- cli_test.go | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 35dc71b..4a89f37 100644 --- a/README.md +++ b/README.md @@ -174,20 +174,20 @@ listening at [wss://relay.damus.io wss://nos.lol wss://relay.nsecbunker.com]: bunker: bunker://f59911b561c37c90b01e9e5c2557307380835c83399756f4d62d8167227e420a?relay=wss%3A%2F%2Frelay.damus.io&relay=wss%3A%2F%2Fnos.lol&relay=wss%3A%2F%2Frelay.nsecbunker.com&secret=XuuiMbcLwuwL ``` -You can also display a QR code for the bunker URI by adding the `--qrcode` flag: +you can also display a QR code for the bunker URI by adding the `--qrcode` flag: ```shell ~> nak bunker --qrcode --sec ncryptsec1... relay.damus.io ``` -### start a bunker that persists its list of authorized keys to disc +### start a bunker that persists its metadata to disc ```shell ~> nak bunker --persist --sec ncryptsec1... relay.nsec.app nos.lol ``` > [!CAUTION] -> When you start a bunker with `--persist` or `--profile`, it will store `--sec` credentials and authorized keys in -> `~/.config/nak/bunker`. If you don't want your private key to be stored in plain text, you can +> when you start a bunker with `--persist` or `--profile`, it will store `--sec` credentials and authorized keys in +> `~/.config/nak/bunker`. if you don't want your private key to be stored in plain text, you can > [encrypt it with NIP-49](#encrypt-key-with-nip-49) it beforehand. ```shell @@ -323,4 +323,4 @@ If you want to run nak inside a container (i.e. to run nak as a server, or to av ```shell docker build -t nak . docker run nak event -``` \ No newline at end of file +``` diff --git a/bunker.go b/bunker.go index f676a60..850e2f9 100644 --- a/bunker.go +++ b/bunker.go @@ -299,7 +299,7 @@ var bunker = &cli.Command{ ) } - // Print QR code if requested + // print QR code if requested if c.Bool("qrcode") { log("QR Code for bunker URI:\n") qrterminal.Generate(bunkerURI, qrterminal.L, os.Stdout) diff --git a/cli_test.go b/cli_test.go index d44891e..1cb2fb9 100644 --- a/cli_test.go +++ b/cli_test.go @@ -136,13 +136,13 @@ func TestMultipleFetch(t *testing.T) { require.Len(t, events, 2) - // First event validation + // first event validation require.Equal(t, nostr.Kind(31923), events[0].Kind) require.Equal(t, "9ae5014573fc75ced00b343868d2cd9343ebcbbae50591c6fa8ae1cd99568f05", events[0].ID.Hex()) require.Equal(t, "5b0183ab6c3e322bf4d41c6b3aef98562a144847b7499543727c5539a114563e", events[0].PubKey.Hex()) require.Equal(t, nostr.Timestamp(1707764605), events[0].CreatedAt) - // Second event validation + // second event validation require.Equal(t, nostr.Kind(1), events[1].Kind) require.Equal(t, "3ff0d19493e661faa90ac06b5d8963ef1e48fe780368fe67ed0fd410df8a6dd5", events[1].ID.Hex()) require.Equal(t, "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d", events[1].PubKey.Hex()) From ff02e6890b211f85d7b1c2e4edbbd2297e447232 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Fri, 11 Jul 2025 13:02:06 -0300 Subject: [PATCH 308/401] adapt to nostr lib websocket refactor commit (which includes the filters thing). --- go.mod | 8 ++------ go.sum | 19 ------------------- req.go | 2 +- 3 files changed, 3 insertions(+), 26 deletions(-) diff --git a/go.mod b/go.mod index 324484d..2b2198b 100644 --- a/go.mod +++ b/go.mod @@ -31,12 +31,9 @@ require ( github.com/btcsuite/btcd v0.24.2 // indirect github.com/btcsuite/btcd/btcutil v1.1.5 // indirect github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect - github.com/bytedance/sonic v1.13.3 // indirect - github.com/bytedance/sonic/loader v0.2.4 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chzyer/logex v1.1.10 // indirect github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect - github.com/cloudwego/base64x v0.1.5 // indirect github.com/coder/websocket v1.8.13 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect @@ -56,7 +53,6 @@ require ( github.com/jalaali/go-jalaali v0.0.0-20210801064154-80525e88d958 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/klauspost/compress v1.18.0 // indirect - github.com/klauspost/cpuid/v2 v2.2.11 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/magefile/mage v1.14.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect @@ -72,13 +68,11 @@ require ( github.com/tidwall/gjson v1.18.0 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect - github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.59.0 // indirect github.com/wasilibs/go-re2 v1.3.0 // indirect github.com/x448/float16 v0.8.4 // indirect go.opencensus.io v0.24.0 // indirect - golang.org/x/arch v0.18.0 // indirect golang.org/x/crypto v0.36.0 // indirect golang.org/x/net v0.37.0 // indirect golang.org/x/sync v0.15.0 // indirect @@ -88,3 +82,5 @@ require ( gopkg.in/yaml.v3 v3.0.1 // indirect rsc.io/qr v0.2.0 // indirect ) + +replace fiatjaf.com/nostr => ../nostrlib diff --git a/go.sum b/go.sum index ead2a3c..e39e6b2 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,6 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= fiatjaf.com/lib v0.3.1 h1:/oFQwNtFRfV+ukmOCxfBEAuayoLwXp4wu2/fz5iHpwA= fiatjaf.com/lib v0.3.1/go.mod h1:Ycqq3+mJ9jAWu7XjbQI1cVr+OFgnHn79dQR5oTII47g= -fiatjaf.com/nostr v0.0.0-20250627165101-028a1637fbd0 h1:Se07jECWueD3fZyaHO08oIzFOPwT6A6wNPQ8QWccX5c= -fiatjaf.com/nostr v0.0.0-20250627165101-028a1637fbd0/go.mod h1:VPs38Fc8J1XAErV750CXAmMUqIq3XEX9VZVj/LuQzzM= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/FastFilter/xorfilter v0.2.1 h1:lbdeLG9BdpquK64ZsleBS8B4xO/QW1IM0gMzF7KaBKc= github.com/FastFilter/xorfilter v0.2.1/go.mod h1:aumvdkhscz6YBZF9ZA/6O4fIoNod4YR50kIVGGZ7l9I= @@ -41,11 +39,6 @@ github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= -github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0= -github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= -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/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= @@ -58,9 +51,6 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= -github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= -github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE= github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= @@ -152,10 +142,6 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= -github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.11 h1:0OwqZRYI2rFrjS4kvkDnqJkKHdHaRnCm68/DY4OxRzU= -github.com/klauspost/cpuid/v2 v2.2.11/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= -github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/liamg/magic v0.0.1 h1:Ru22ElY+sCh6RvRTWjQzKKCxsEco8hE0co8n1qe7TBM= @@ -224,8 +210,6 @@ github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JT github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= -github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= -github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/urfave/cli/v3 v3.0.0-beta1 h1:6DTaaUarcM0wX7qj5Hcvs+5Dm3dyUTBbEwIWAjcw9Zg= github.com/urfave/cli/v3 v3.0.0-beta1/go.mod h1:FnIeEMYu+ko8zP1F9Ypr3xkZMIDqW3DR92yUtY39q1Y= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= @@ -242,8 +226,6 @@ github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZ github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc= -golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -336,6 +318,5 @@ gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY= rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs= diff --git a/req.go b/req.go index 9b73a9c..57eaaed 100644 --- a/req.go +++ b/req.go @@ -164,7 +164,7 @@ example: if c.Bool("bare") { result = filter.String() } else { - j, _ := json.Marshal(nostr.ReqEnvelope{SubscriptionID: "nak", Filter: filter}) + j, _ := json.Marshal(nostr.ReqEnvelope{SubscriptionID: "nak", Filters: []nostr.Filter{filter}}) result = string(j) } From 7c589489246f5d566830ece360365b92904b62b0 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Thu, 17 Jul 2025 16:14:33 -0300 Subject: [PATCH 309/401] verify: better handling of stdout and verbose logging output. fixes: https://github.com/fiatjaf/nak/issues/74 --- verify.go | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/verify.go b/verify.go index c74eab8..9f2c673 100644 --- a/verify.go +++ b/verify.go @@ -9,31 +9,41 @@ import ( var verify = &cli.Command{ Name: "verify", - Usage: "checks the hash and signature of an event given through stdin", + Usage: "checks the hash and signature of an event given through stdin or as the first argument", Description: `example: echo '{"id":"a889df6a387419ff204305f4c2d296ee328c3cd4f8b62f205648a541b4554dfb","pubkey":"c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5","created_at":1698623783,"kind":1,"tags":[],"content":"hello from the nostr army knife","sig":"84876e1ee3e726da84e5d195eb79358b2b3eaa4d9bd38456fde3e8a2af3f1cd4cda23f23fda454869975b3688797d4c66e12f4c51c1b43c6d2997c5e61865661"}' | nak verify it outputs nothing if the verification is successful.`, DisableSliceFlagSeparator: true, Action: func(ctx context.Context, c *cli.Command) error { - for stdinEvent := range getStdinLinesOrArguments(c.Args()) { + for stdinEvent := range getJsonsOrBlank() { evt := nostr.Event{} - if stdinEvent != "" { - if err := json.Unmarshal([]byte(stdinEvent), &evt); err != nil { - ctx = lineProcessingError(ctx, "invalid event: %s", err) + if stdinEvent == "" { + stdinEvent = c.Args().First() + if stdinEvent == "" { continue } } + if err := json.Unmarshal([]byte(stdinEvent), &evt); err != nil { + ctx = lineProcessingError(ctx, "invalid event: %s", err) + logverbose("<>: invalid event.\n", evt.ID.Hex()) + continue + } + if evt.GetID() != evt.ID { ctx = lineProcessingError(ctx, "invalid .id, expected %s, got %s", evt.GetID(), evt.ID) + logverbose("%s: invalid id.\n", evt.ID.Hex()) continue } if !evt.VerifySignature() { ctx = lineProcessingError(ctx, "invalid signature") + logverbose("%s: invalid signature.\n", evt.ID.Hex()) continue } + + logverbose("%s: valid.\n", evt.ID.Hex()) } exitIfLineProcessingError(ctx) From 87bf5ef446b8992031d4ccaf1fd362ccad4f94b5 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Thu, 17 Jul 2025 20:00:03 -0300 Subject: [PATCH 310/401] fix nak blossom list stupid segfault. --- blossom.go | 2 +- go.mod | 8 +++----- go.sum | 10 ++++++---- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/blossom.go b/blossom.go index 3c8ed63..b5e6e75 100644 --- a/blossom.go +++ b/blossom.go @@ -41,7 +41,7 @@ var blossomCmd = &cli.Command{ 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)) + client = blossom.NewClient(c.String("server"), keyer.NewReadOnlySigner(pk)) } } else { var err error diff --git a/go.mod b/go.mod index 2b2198b..6ec4f0a 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.24.1 require ( fiatjaf.com/lib v0.3.1 - fiatjaf.com/nostr v0.0.0-20250627165101-028a1637fbd0 + fiatjaf.com/nostr v0.0.0-20250715161459-840e2846ed15 github.com/bep/debounce v1.2.1 github.com/btcsuite/btcd/btcec/v2 v2.3.5 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e @@ -20,7 +20,7 @@ require ( github.com/mdp/qrterminal/v3 v3.2.1 github.com/stretchr/testify v1.10.0 github.com/urfave/cli/v3 v3.0.0-beta1 - golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b + golang.org/x/exp v0.0.0-20250717185816-542afb5b7346 golang.org/x/term v0.32.0 ) @@ -75,12 +75,10 @@ require ( go.opencensus.io v0.24.0 // indirect golang.org/x/crypto v0.36.0 // indirect golang.org/x/net v0.37.0 // indirect - golang.org/x/sync v0.15.0 // indirect + golang.org/x/sync v0.16.0 // indirect golang.org/x/sys v0.33.0 // indirect golang.org/x/text v0.23.0 // indirect google.golang.org/protobuf v1.36.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect rsc.io/qr v0.2.0 // indirect ) - -replace fiatjaf.com/nostr => ../nostrlib diff --git a/go.sum b/go.sum index e39e6b2..b7984ef 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,8 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= fiatjaf.com/lib v0.3.1 h1:/oFQwNtFRfV+ukmOCxfBEAuayoLwXp4wu2/fz5iHpwA= fiatjaf.com/lib v0.3.1/go.mod h1:Ycqq3+mJ9jAWu7XjbQI1cVr+OFgnHn79dQR5oTII47g= +fiatjaf.com/nostr v0.0.0-20250715161459-840e2846ed15 h1:XQq9DyW9j14wRKCU0cNyBUDCjJO6HAm+rK9abLLJKes= +fiatjaf.com/nostr v0.0.0-20250715161459-840e2846ed15/go.mod h1:lJ9x/Ehcq/7x2mf6iMlC4AOjPUh3WbfLMY+3PyaPRNs= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/FastFilter/xorfilter v0.2.1 h1:lbdeLG9BdpquK64ZsleBS8B4xO/QW1IM0gMzF7KaBKc= github.com/FastFilter/xorfilter v0.2.1/go.mod h1:aumvdkhscz6YBZF9ZA/6O4fIoNod4YR50kIVGGZ7l9I= @@ -232,8 +234,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= -golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= +golang.org/x/exp v0.0.0-20250717185816-542afb5b7346 h1:vuCObX8mQzik1tfEcYxWZBuVsmQtD1IjxCyPKM18Bh4= +golang.org/x/exp v0.0.0-20250717185816-542afb5b7346/go.mod h1:A+z0yzpGtvnG90cToK5n2tu8UJVP2XUATh+r+sfOOOc= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= @@ -253,8 +255,8 @@ golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAG golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= -golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/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= From a698c59b0bdb1d45df7edc44682cd269dc6473a0 Mon Sep 17 00:00:00 2001 From: George Date: Wed, 23 Jul 2025 13:59:49 +0000 Subject: [PATCH 311/401] fix build on OpenBSD --- fs.go | 2 +- fs_windows.go => fs_other.go | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) rename fs_windows.go => fs_other.go (66%) diff --git a/fs.go b/fs.go index f55ebbe..029afdf 100644 --- a/fs.go +++ b/fs.go @@ -1,4 +1,4 @@ -//go:build !windows +//go:build !windows && !openbsd package main diff --git a/fs_windows.go b/fs_other.go similarity index 66% rename from fs_windows.go rename to fs_other.go index 60f98d6..fba75fc 100644 --- a/fs_windows.go +++ b/fs_other.go @@ -1,4 +1,4 @@ -//go:build windows +//go:build windows || openbsd package main @@ -12,9 +12,9 @@ import ( var fsCmd = &cli.Command{ Name: "fs", Usage: "mount a FUSE filesystem that exposes Nostr events as files.", - Description: `doesn't work on Windows.`, + Description: `doesn't work on Windows and OpenBSD.`, DisableSliceFlagSeparator: true, Action: func(ctx context.Context, c *cli.Command) error { - return fmt.Errorf("this doesn't work on Windows.") + return fmt.Errorf("this doesn't work on Windows and OpenBSD.") }, } From 1a221a133c069f5d88fb58bca2b1b91d225e76e5 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Fri, 25 Jul 2025 17:03:13 -0300 Subject: [PATCH 312/401] cleanup and fix readme. --- README.md | 31 ++++++++----------------------- 1 file changed, 8 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 4a89f37..7d64ea8 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ install with `go install github.com/fiatjaf/nak@latest` or [download a binary](https://github.com/fiatjaf/nak/releases). +or get the source with `git clone https://github.com/fiatjaf/nak` then install with `go install` or run with docker using `docker build -t nak . && docker run nak event`. + ## what can you do with it? take a look at the help text that comes in it to learn all possibilities, but here are some: @@ -128,17 +130,13 @@ type the password to decrypt your secret key: ********** 985d66d2644dfa7676e26046914470d66ebc7fa783a3f57f139fde32d0d631d7 ``` -### sign an event using [Amber](https://github.com/greenart7c3/Amber) (or other bunker provider) +### sign an event using a bunker provider (amber, promenade etc) ```shell ~> export NOSTR_CLIENT_KEY="$(nak key generate)" ~> nak event --sec 'bunker://a9e0f110f636f3191644110c19a33448daf09d7cda9708a769e91b7e91340208?relay=wss%3A%2F%2Frelay.damus.io&relay=wss%3A%2F%2Frelay.nsecbunker.com&relay=wss%3A%2F%2Fnos.lol&secret=TWfGbjQCLxUf' -c 'hello from bunker' ``` -> [!IMPORTANT] -> Remember to set a `NOSTR_CLIENT_KEY` permanently on your shell, otherwise you'll only be able to use the bunker once. For `bash`: -> ```shell -> echo 'export NOSTR_CLIENT_KEY="$(nak key generate)"' >> ~/.bashrc -> ``` +(in most cases it's better to set `NOSTR_CLIENT_KEY` permanently on your shell, as that identity will be recorded by the bunker provider.) ### sign an event using a NIP-49 encrypted key ```shell @@ -180,16 +178,11 @@ you can also display a QR code for the bunker URI by adding the `--qrcode` flag: ~> nak bunker --qrcode --sec ncryptsec1... relay.damus.io ``` -### start a bunker that persists its metadata to disc +### start a bunker that persists its metadata (secret key, relays, authorized client pubkeys) to disc ```shell ~> nak bunker --persist --sec ncryptsec1... relay.nsec.app nos.lol ``` -> [!CAUTION] -> when you start a bunker with `--persist` or `--profile`, it will store `--sec` credentials and authorized keys in -> `~/.config/nak/bunker`. if you don't want your private key to be stored in plain text, you can -> [encrypt it with NIP-49](#encrypt-key-with-nip-49) it beforehand. - ```shell then later just @@ -250,7 +243,7 @@ type the password to decrypt your secret key: ******** ~> nak req -i 412f2d3e73acc312942c055ac2a695dc60bf58ff97e06689a8a79e97796c4cdb relay.westernbtc.com | jq -r .content > ~/.jq ``` -### watch a NIP-53 livestream (zap.stream etc) +### watch a NIP-53 livestream (zap.stream, amethyst, shosho etc) ```shell ~> # this requires the jq utils from the step above ~> mpv $(nak fetch naddr1qqjxvvm9xscnsdtx95cxvcfk956rsvtx943rje3k95mx2dp389jnwwrp8ymxgqg4waehxw309aex2mrp0yhxgctdw4eju6t09upzpn6956apxcad0mfp8grcuugdysg44eepex68h50t73zcathmfs49qvzqqqrkvu7ed38k | jq -r 'tag_value("streaming")') @@ -307,20 +300,12 @@ echo "#surely you're joking, mr npub1l2vyh47mk2p0qlsku7hg0vn29faehy9hy34ygaclpn6 # and there is a --confirm flag that gives you a chance to confirm before actually publishing the result to relays. ``` -### record and publish an audio note of 10s (yakbak etc) signed from a bunker +### record and publish an audio note (yakbak, nostur etc) signed from a bunker ```shell ffmpeg -f alsa -i default -f webm -t 00:00:03 pipe:1 | nak blossom --server blossom.primal.net upload | jq -rc '{content: .url}' | nak event -k 1222 --sec 'bunker://urlgoeshere' pyramid.fiatjaf.com nostr.wine ``` ### from a file with events get only those that have kind 1111 and were created by a given pubkey ```shell -~> cat all.jsonl | nak filter -k 1111 > filtered.jsonl -``` -### run nak in Docker - -If you want to run nak inside a container (i.e. to run nak as a server, or to avoid installing the Go toolchain) you can run it with Docker: - -```shell -docker build -t nak . -docker run nak event +~> cat all.jsonl | nak filter -k 1111 -a 117673e191b10fe1aedf1736ee74de4cffd4c132ca701960b70a5abad5870faa > filtered.jsonl ``` From 23e27da077544a7cb2fb17cd90c81fdedba87430 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Wed, 6 Aug 2025 15:08:36 -0300 Subject: [PATCH 313/401] use isatty for detecting stuff for the fancy output (it doesn't work). --- go.mod | 6 ++++-- go.sum | 6 ++---- helpers.go | 5 +++++ 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 6ec4f0a..876b863 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( 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/mattn/go-isatty v0.0.20 github.com/mattn/go-tty v0.0.7 github.com/mdp/qrterminal/v3 v3.2.1 github.com/stretchr/testify v1.10.0 @@ -56,7 +57,6 @@ require ( github.com/kylelemons/godebug v1.1.0 // indirect github.com/magefile/mage v1.14.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pkg/errors v0.9.1 // indirect @@ -76,9 +76,11 @@ require ( golang.org/x/crypto v0.36.0 // indirect golang.org/x/net v0.37.0 // indirect golang.org/x/sync v0.16.0 // indirect - golang.org/x/sys v0.33.0 // indirect + golang.org/x/sys v0.34.0 // indirect golang.org/x/text v0.23.0 // indirect google.golang.org/protobuf v1.36.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect rsc.io/qr v0.2.0 // indirect ) + +replace fiatjaf.com/nostr => ../nostrlib diff --git a/go.sum b/go.sum index b7984ef..f85beba 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,6 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= fiatjaf.com/lib v0.3.1 h1:/oFQwNtFRfV+ukmOCxfBEAuayoLwXp4wu2/fz5iHpwA= fiatjaf.com/lib v0.3.1/go.mod h1:Ycqq3+mJ9jAWu7XjbQI1cVr+OFgnHn79dQR5oTII47g= -fiatjaf.com/nostr v0.0.0-20250715161459-840e2846ed15 h1:XQq9DyW9j14wRKCU0cNyBUDCjJO6HAm+rK9abLLJKes= -fiatjaf.com/nostr v0.0.0-20250715161459-840e2846ed15/go.mod h1:lJ9x/Ehcq/7x2mf6iMlC4AOjPUh3WbfLMY+3PyaPRNs= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/FastFilter/xorfilter v0.2.1 h1:lbdeLG9BdpquK64ZsleBS8B4xO/QW1IM0gMzF7KaBKc= github.com/FastFilter/xorfilter v0.2.1/go.mod h1:aumvdkhscz6YBZF9ZA/6O4fIoNod4YR50kIVGGZ7l9I= @@ -269,8 +267,8 @@ golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= +golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/helpers.go b/helpers.go index 8110b23..de5da1b 100644 --- a/helpers.go +++ b/helpers.go @@ -24,6 +24,7 @@ import ( "github.com/chzyer/readline" "github.com/fatih/color" jsoniter "github.com/json-iterator/go" + "github.com/mattn/go-isatty" "github.com/mattn/go-tty" "github.com/urfave/cli/v3" "golang.org/x/term" @@ -315,6 +316,10 @@ func supportsDynamicMultilineMagic() bool { return false } + if !isatty.IsTerminal(os.Stdout.Fd()) { + return false + } + width, _, err := term.GetSize(int(os.Stderr.Fd())) if err != nil { return false From d3975679e4323e2436bc76d2c34cfecf72946d62 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Thu, 14 Aug 2025 13:27:29 -0300 Subject: [PATCH 314/401] add labels to subscriptions for easier debugging. --- count.go | 4 +++- fetch.go | 4 +++- mcp.go | 8 ++++++-- req.go | 4 +++- 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/count.go b/count.go index f22ecb8..662baf7 100644 --- a/count.go +++ b/count.go @@ -141,7 +141,9 @@ var count = &cli.Command{ } for _, relayUrl := range relayUrls { relay, _ := sys.Pool.EnsureRelay(relayUrl) - count, hllRegisters, err := relay.Count(ctx, filter, nostr.SubscriptionOptions{}) + count, hllRegisters, err := relay.Count(ctx, filter, nostr.SubscriptionOptions{ + Label: "nak-count", + }) fmt.Fprintf(os.Stderr, "%s%s: ", strings.Repeat(" ", biggerUrlSize-len(relayUrl)), relayUrl) if err != nil { diff --git a/fetch.go b/fetch.go index 7a1c370..ae84eec 100644 --- a/fetch.go +++ b/fetch.go @@ -106,7 +106,9 @@ var fetch = &cli.Command{ continue } - for ie := range sys.Pool.FetchMany(ctx, relays, filter, nostr.SubscriptionOptions{}) { + for ie := range sys.Pool.FetchMany(ctx, relays, filter, nostr.SubscriptionOptions{ + Label: "nak-fetch", + }) { stdout(ie.Event) } } diff --git a/mcp.go b/mcp.go index bbf1062..e327ef9 100644 --- a/mcp.go +++ b/mcp.go @@ -165,7 +165,9 @@ var mcpServer = &cli.Command{ res := strings.Builder{} res.WriteString("Search results: ") l := 0 - for result := range sys.Pool.FetchMany(ctx, []string{"relay.nostr.band", "nostr.wine"}, filter, nostr.SubscriptionOptions{}) { + for result := range sys.Pool.FetchMany(ctx, []string{"relay.nostr.band", "nostr.wine"}, filter, nostr.SubscriptionOptions{ + Label: "nak-mcp-search", + }) { l++ pm, _ := sdk.ParseMetadata(result.Event) res.WriteString(fmt.Sprintf("\n\nResult %d\nUser name: \"%s\"\nPublic key: \"%s\"\nDescription: \"%s\"\n", @@ -219,7 +221,9 @@ var mcpServer = &cli.Command{ } } - events := sys.Pool.FetchMany(ctx, []string{relay}, filter, nostr.SubscriptionOptions{}) + events := sys.Pool.FetchMany(ctx, []string{relay}, filter, nostr.SubscriptionOptions{ + Label: "nak-mcp-profile-events", + }) result := strings.Builder{} for ie := range events { diff --git a/req.go b/req.go index 57eaaed..2fe7301 100644 --- a/req.go +++ b/req.go @@ -154,7 +154,9 @@ example: fn = sys.Pool.SubscribeMany } - for ie := range fn(ctx, relayUrls, filter, nostr.SubscriptionOptions{}) { + for ie := range fn(ctx, relayUrls, filter, nostr.SubscriptionOptions{ + Label: "nak-req", + }) { stdout(ie.Event) } } From b316646821008ec6fbbed727d39d2a4040eae47e Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Mon, 18 Aug 2025 21:01:52 -0300 Subject: [PATCH 315/401] new release with updated dependencies. --- go.mod | 8 +++----- go.sum | 10 ++++++++-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index 876b863..f01fbe2 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.24.1 require ( fiatjaf.com/lib v0.3.1 - fiatjaf.com/nostr v0.0.0-20250715161459-840e2846ed15 + fiatjaf.com/nostr v0.0.0-20250818235102-c8d5aa703fab github.com/bep/debounce v1.2.1 github.com/btcsuite/btcd/btcec/v2 v2.3.5 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e @@ -21,7 +21,7 @@ require ( github.com/mdp/qrterminal/v3 v3.2.1 github.com/stretchr/testify v1.10.0 github.com/urfave/cli/v3 v3.0.0-beta1 - golang.org/x/exp v0.0.0-20250717185816-542afb5b7346 + golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 golang.org/x/term v0.32.0 ) @@ -43,7 +43,7 @@ require ( github.com/dgraph-io/ristretto/v2 v2.1.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/elliotchance/pie/v2 v2.7.0 // indirect - github.com/elnosh/gonuts v0.3.1-0.20250123162555-7c0381a585e3 // indirect + github.com/elnosh/gonuts v0.4.2 // indirect github.com/fasthttp/websocket v1.5.12 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect @@ -82,5 +82,3 @@ require ( gopkg.in/yaml.v3 v3.0.1 // indirect rsc.io/qr v0.2.0 // indirect ) - -replace fiatjaf.com/nostr => ../nostrlib diff --git a/go.sum b/go.sum index f85beba..03adb20 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,8 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= fiatjaf.com/lib v0.3.1 h1:/oFQwNtFRfV+ukmOCxfBEAuayoLwXp4wu2/fz5iHpwA= fiatjaf.com/lib v0.3.1/go.mod h1:Ycqq3+mJ9jAWu7XjbQI1cVr+OFgnHn79dQR5oTII47g= +fiatjaf.com/nostr v0.0.0-20250818235102-c8d5aa703fab h1:zMp+G9Et5Z7ku/WUflZpmQzDIAB/Ah00Ms3cMtX9Pw4= +fiatjaf.com/nostr v0.0.0-20250818235102-c8d5aa703fab/go.mod h1:j7AfnEAevFuLcpH4Y1RYM27sYJfshL3An6ZSAQNlUlY= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/FastFilter/xorfilter v0.2.1 h1:lbdeLG9BdpquK64ZsleBS8B4xO/QW1IM0gMzF7KaBKc= github.com/FastFilter/xorfilter v0.2.1/go.mod h1:aumvdkhscz6YBZF9ZA/6O4fIoNod4YR50kIVGGZ7l9I= @@ -78,8 +80,8 @@ github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+m github.com/dvyukov/go-fuzz v0.0.0-20200318091601-be3528f3a813/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw= github.com/elliotchance/pie/v2 v2.7.0 h1:FqoIKg4uj0G/CrLGuMS9ejnFKa92lxE1dEgBD3pShXg= github.com/elliotchance/pie/v2 v2.7.0/go.mod h1:18t0dgGFH006g4eVdDtWfgFZPQEgl10IoEO8YWEq3Og= -github.com/elnosh/gonuts v0.3.1-0.20250123162555-7c0381a585e3 h1:k7evIqJ2BtFn191DgY/b03N2bMYA/iQwzr4f/uHYn20= -github.com/elnosh/gonuts v0.3.1-0.20250123162555-7c0381a585e3/go.mod h1:vgZomh4YQk7R3w4ltZc0sHwCmndfHkuX6V4sga/8oNs= +github.com/elnosh/gonuts v0.4.2 h1:/WubPAWGxTE+okJ0WPvmtEzTzpi04RGxiTHAF1FYU+M= +github.com/elnosh/gonuts v0.4.2/go.mod h1:vgZomh4YQk7R3w4ltZc0sHwCmndfHkuX6V4sga/8oNs= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -210,6 +212,8 @@ github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JT github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8= +github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U= github.com/urfave/cli/v3 v3.0.0-beta1 h1:6DTaaUarcM0wX7qj5Hcvs+5Dm3dyUTBbEwIWAjcw9Zg= github.com/urfave/cli/v3 v3.0.0-beta1/go.mod h1:FnIeEMYu+ko8zP1F9Ypr3xkZMIDqW3DR92yUtY39q1Y= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= @@ -234,6 +238,8 @@ golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZv golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20250717185816-542afb5b7346 h1:vuCObX8mQzik1tfEcYxWZBuVsmQtD1IjxCyPKM18Bh4= golang.org/x/exp v0.0.0-20250717185816-542afb5b7346/go.mod h1:A+z0yzpGtvnG90cToK5n2tu8UJVP2XUATh+r+sfOOOc= +golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 h1:SbTAbRFnd5kjQXbczszQ0hdk3ctwYf3qBNH9jIsGclE= +golang.org/x/exp v0.0.0-20250813145105-42675adae3e6/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= From 6f0e7773249f3b487563804c6aca799ba2499b35 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Fri, 29 Aug 2025 09:51:03 -0300 Subject: [PATCH 316/401] wallet tokens drop --- wallet.go | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/wallet.go b/wallet.go index 414d03e..102fb99 100644 --- a/wallet.go +++ b/wallet.go @@ -3,6 +3,7 @@ package main import ( "context" "fmt" + "slices" "strconv" "strings" @@ -172,6 +173,35 @@ var wallet = &cli.Command{ closew() return nil }, + Commands: []*cli.Command{ + { + Name: "drop", + Usage: "deletes a token from the wallet", + DisableSliceFlagSeparator: true, + ArgsUsage: "...", + Action: func(ctx context.Context, c *cli.Command) error { + ids := c.Args().Slice() + if len(ids) == 0 { + return fmt.Errorf("no token ids specified") + } + + w, closew, err := prepareWallet(ctx, c) + if err != nil { + return err + } + + for _, token := range w.Tokens { + if slices.Contains(ids, token.ID()) { + w.DropToken(ctx, token.ID()) + log("dropped %s %d %s\n", token.ID(), token.Proofs.Amount(), strings.Split(token.Mint, "://")[1]) + } + } + + closew() + return nil + }, + }, + }, }, { Name: "receive", From 88031c888b999e455e9afb3f87b07ce391189a15 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Fri, 29 Aug 2025 16:24:58 -0300 Subject: [PATCH 317/401] wallet --stream --- go.mod | 4 ++-- go.sum | 4 ++++ wallet.go | 30 +++++++++++++++++++++++++++++- 3 files changed, 35 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index f01fbe2..5e7fcdb 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.24.1 require ( fiatjaf.com/lib v0.3.1 - fiatjaf.com/nostr v0.0.0-20250818235102-c8d5aa703fab + fiatjaf.com/nostr v0.0.0-20250829192328-aa321f6e7f10 github.com/bep/debounce v1.2.1 github.com/btcsuite/btcd/btcec/v2 v2.3.5 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e @@ -21,7 +21,7 @@ require ( github.com/mdp/qrterminal/v3 v3.2.1 github.com/stretchr/testify v1.10.0 github.com/urfave/cli/v3 v3.0.0-beta1 - golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 + golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b golang.org/x/term v0.32.0 ) diff --git a/go.sum b/go.sum index 03adb20..7698af4 100644 --- a/go.sum +++ b/go.sum @@ -3,6 +3,8 @@ fiatjaf.com/lib v0.3.1 h1:/oFQwNtFRfV+ukmOCxfBEAuayoLwXp4wu2/fz5iHpwA= fiatjaf.com/lib v0.3.1/go.mod h1:Ycqq3+mJ9jAWu7XjbQI1cVr+OFgnHn79dQR5oTII47g= fiatjaf.com/nostr v0.0.0-20250818235102-c8d5aa703fab h1:zMp+G9Et5Z7ku/WUflZpmQzDIAB/Ah00Ms3cMtX9Pw4= fiatjaf.com/nostr v0.0.0-20250818235102-c8d5aa703fab/go.mod h1:j7AfnEAevFuLcpH4Y1RYM27sYJfshL3An6ZSAQNlUlY= +fiatjaf.com/nostr v0.0.0-20250829192328-aa321f6e7f10 h1:T1rQuoQgC6GwxJTLTDjli8ZauCtOa+BjybU3pxCN84w= +fiatjaf.com/nostr v0.0.0-20250829192328-aa321f6e7f10/go.mod h1:92kdTV0aFobQ14aq5IMpkYy3lGI8RUabMwN3Cfv/qmg= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/FastFilter/xorfilter v0.2.1 h1:lbdeLG9BdpquK64ZsleBS8B4xO/QW1IM0gMzF7KaBKc= github.com/FastFilter/xorfilter v0.2.1/go.mod h1:aumvdkhscz6YBZF9ZA/6O4fIoNod4YR50kIVGGZ7l9I= @@ -240,6 +242,8 @@ golang.org/x/exp v0.0.0-20250717185816-542afb5b7346 h1:vuCObX8mQzik1tfEcYxWZBuVs golang.org/x/exp v0.0.0-20250717185816-542afb5b7346/go.mod h1:A+z0yzpGtvnG90cToK5n2tu8UJVP2XUATh+r+sfOOOc= golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 h1:SbTAbRFnd5kjQXbczszQ0hdk3ctwYf3qBNH9jIsGclE= golang.org/x/exp v0.0.0-20250813145105-42675adae3e6/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4= +golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b h1:DXr+pvt3nC887026GRP39Ej11UATqWDmWuS99x26cD0= +golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= diff --git a/wallet.go b/wallet.go index 102fb99..3f39815 100644 --- a/wallet.go +++ b/wallet.go @@ -34,6 +34,24 @@ func prepareWallet(ctx context.Context, c *cli.Command) (*nip60.Wallet, func(), w.Processed = func(evt nostr.Event, err error) { if err == nil { logverbose("processed event %s\n", evt) + + if c.Bool("stream") { + // after EOSE log updates and the new balance + select { + case <-w.Stable: + switch evt.Kind { + case 5: + log("- token deleted\n") + case 7375: + log("- token added\n") + default: + return + } + + log(" balance: %d\n", w.Balance()) + default: + } + } } else { log("error processing event %s: %s\n", evt, err) } @@ -87,15 +105,25 @@ var wallet = &cli.Command{ Usage: "displays the current wallet balance", Description: "all wallet data is stored on Nostr relays, signed and encrypted with the given key, and reloaded again from relays on every call.\n\nthe same data can be accessed by other compatible nip60 clients.", DisableSliceFlagSeparator: true, - Flags: defaultKeyFlags, + Flags: append(defaultKeyFlags, + &cli.BoolFlag{ + Name: "stream", + Usage: "keep listening for wallet-related events and logging them", + }, + ), Action: func(ctx context.Context, c *cli.Command) error { w, closew, err := prepareWallet(ctx, c) if err != nil { return err } + log("balance: ") stdout(w.Balance()) + if c.Bool("stream") { + <-ctx.Done() // this will hang forever + } + closew() return nil }, From bf1690a041e0637dca33921beeabf25d3b888479 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Wed, 3 Sep 2025 21:37:03 -0300 Subject: [PATCH 318/401] get rid of badger, replace with bolt, following nostrlib. --- go.mod | 15 ++++---- go.sum | 100 +++++++----------------------------------------------- outbox.go | 10 +++--- 3 files changed, 23 insertions(+), 102 deletions(-) diff --git a/go.mod b/go.mod index 5e7fcdb..2cc9575 100644 --- a/go.mod +++ b/go.mod @@ -38,16 +38,12 @@ require ( github.com/coder/websocket v1.8.13 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect - github.com/dgraph-io/badger/v4 v4.5.0 // indirect github.com/dgraph-io/ristretto v1.0.0 // indirect - github.com/dgraph-io/ristretto/v2 v2.1.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/elliotchance/pie/v2 v2.7.0 // indirect github.com/elnosh/gonuts v0.4.2 // indirect github.com/fasthttp/websocket v1.5.12 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect - github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect - github.com/google/flatbuffers v24.12.23+incompatible // indirect github.com/google/uuid v1.6.0 // indirect github.com/hablullah/go-hijri v1.0.2 // indirect github.com/hablullah/go-juliandays v1.0.0 // indirect @@ -72,13 +68,14 @@ require ( github.com/valyala/fasthttp v1.59.0 // indirect github.com/wasilibs/go-re2 v1.3.0 // indirect github.com/x448/float16 v0.8.4 // indirect - go.opencensus.io v0.24.0 // indirect - golang.org/x/crypto v0.36.0 // indirect - golang.org/x/net v0.37.0 // indirect + go.etcd.io/bbolt v1.4.2 // indirect + golang.org/x/crypto v0.39.0 // indirect + golang.org/x/net v0.41.0 // indirect golang.org/x/sync v0.16.0 // indirect golang.org/x/sys v0.34.0 // indirect - golang.org/x/text v0.23.0 // indirect - google.golang.org/protobuf v1.36.2 // indirect + golang.org/x/text v0.26.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect rsc.io/qr v0.2.0 // indirect ) + +replace fiatjaf.com/nostr => ../nostrlib diff --git a/go.sum b/go.sum index 7698af4..45585c8 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,5 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= fiatjaf.com/lib v0.3.1 h1:/oFQwNtFRfV+ukmOCxfBEAuayoLwXp4wu2/fz5iHpwA= fiatjaf.com/lib v0.3.1/go.mod h1:Ycqq3+mJ9jAWu7XjbQI1cVr+OFgnHn79dQR5oTII47g= -fiatjaf.com/nostr v0.0.0-20250818235102-c8d5aa703fab h1:zMp+G9Et5Z7ku/WUflZpmQzDIAB/Ah00Ms3cMtX9Pw4= -fiatjaf.com/nostr v0.0.0-20250818235102-c8d5aa703fab/go.mod h1:j7AfnEAevFuLcpH4Y1RYM27sYJfshL3An6ZSAQNlUlY= -fiatjaf.com/nostr v0.0.0-20250829192328-aa321f6e7f10 h1:T1rQuoQgC6GwxJTLTDjli8ZauCtOa+BjybU3pxCN84w= -fiatjaf.com/nostr v0.0.0-20250829192328-aa321f6e7f10/go.mod h1:92kdTV0aFobQ14aq5IMpkYy3lGI8RUabMwN3Cfv/qmg= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 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= @@ -43,7 +37,6 @@ github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 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= @@ -54,8 +47,6 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5O github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE= github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -69,14 +60,10 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeC github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= -github.com/dgraph-io/badger/v4 v4.5.0 h1:TeJE3I1pIWLBjYhIYCA1+uxrjWEoJXImFBMEBVSm16g= -github.com/dgraph-io/badger/v4 v4.5.0/go.mod h1:ysgYmIeG8dS/E8kwxT7xHyc7MkmwNYLRoYnFbr7387A= github.com/dgraph-io/ristretto v1.0.0 h1:SYG07bONKMlFDUYu5pEu3DGAh8c2OFNzKm6G9J4Si84= github.com/dgraph-io/ristretto v1.0.0/go.mod h1:jTi2FiYEhQ1NsMmA7DeBykizjOuY88NhKBkepyu1jPc= -github.com/dgraph-io/ristretto/v2 v2.1.0 h1:59LjpOJLNDULHh8MC4UaegN52lC4JnO2dITsie/Pa8I= -github.com/dgraph-io/ristretto/v2 v2.1.0/go.mod h1:uejeqfYXpUomfse0+lO+13ATz4TypQYLJZzBSAemuB4= -github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y= -github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38= +github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dvyukov/go-fuzz v0.0.0-20200318091601-be3528f3a813/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw= @@ -84,10 +71,6 @@ github.com/elliotchance/pie/v2 v2.7.0 h1:FqoIKg4uj0G/CrLGuMS9ejnFKa92lxE1dEgBD3p github.com/elliotchance/pie/v2 v2.7.0/go.mod h1:18t0dgGFH006g4eVdDtWfgFZPQEgl10IoEO8YWEq3Og= github.com/elnosh/gonuts v0.4.2 h1:/WubPAWGxTE+okJ0WPvmtEzTzpi04RGxiTHAF1FYU+M= github.com/elnosh/gonuts v0.4.2/go.mod h1:vgZomh4YQk7R3w4ltZc0sHwCmndfHkuX6V4sga/8oNs= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fasthttp/websocket v1.5.12 h1:e4RGPpWW2HTbL3zV0Y/t7g0ub294LkiuXXUuTOUInlE= 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= @@ -96,34 +79,20 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo 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/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= -github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/flatbuffers v24.12.23+incompatible h1:ubBKR94NR4pXUCY/MUsRVzd9umNW7ht7EG9hHfS9FX8= -github.com/google/flatbuffers v24.12.23+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= @@ -186,7 +155,6 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg= github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= @@ -194,14 +162,9 @@ github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 h1:D0vL7YNisV2yqE55+q0lFuGse6U8lxlg7fYTctlT5Gc= github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38/go.mod h1:sM7Mt7uEoCeFSCBM+qBrqvEo+/9vdmj19wzp3yzUhmg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= @@ -230,42 +193,25 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= -go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= -go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.etcd.io/bbolt v1.4.2 h1:IrUHp260R8c+zYx/Tm8QZr04CX+qWS5PGfPdevhdm1I= +go.etcd.io/bbolt v1.4.2/go.mod h1:Is8rSHO/b4f3XigBC0lL0+4FwAQv3HXEEIgFMuKHceM= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= -golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20250717185816-542afb5b7346 h1:vuCObX8mQzik1tfEcYxWZBuVsmQtD1IjxCyPKM18Bh4= -golang.org/x/exp v0.0.0-20250717185816-542afb5b7346/go.mod h1:A+z0yzpGtvnG90cToK5n2tu8UJVP2XUATh+r+sfOOOc= -golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 h1:SbTAbRFnd5kjQXbczszQ0hdk3ctwYf3qBNH9jIsGclE= -golang.org/x/exp v0.0.0-20250813145105-42675adae3e6/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4= +golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= +golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b h1:DXr+pvt3nC887026GRP39Ej11UATqWDmWuS99x26cD0= golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -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/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= +golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -275,7 +221,6 @@ golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= @@ -284,36 +229,17 @@ golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.36.2 h1:R8FeyR1/eLmkutZOM5CWghmo5itiG9z0ktFlTVLuTmU= -google.golang.org/protobuf v1.36.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= @@ -326,7 +252,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY= rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs= diff --git a/outbox.go b/outbox.go index bbec2f7..fc916f0 100644 --- a/outbox.go +++ b/outbox.go @@ -8,7 +8,7 @@ import ( "fiatjaf.com/nostr" "fiatjaf.com/nostr/sdk" - "fiatjaf.com/nostr/sdk/hints/badgerh" + "fiatjaf.com/nostr/sdk/hints/bbolth" "github.com/fatih/color" "github.com/urfave/cli/v3" ) @@ -21,7 +21,7 @@ var ( func initializeOutboxHintsDB(c *cli.Command, sys *sdk.System) error { configPath := c.String("config-path") if configPath != "" { - hintsFilePath = filepath.Join(configPath, "outbox/hints.bg") + hintsFilePath = filepath.Join(configPath, "outbox/hints.db") } if hintsFilePath != "" { if _, err := os.Stat(hintsFilePath); err == nil { @@ -31,7 +31,7 @@ func initializeOutboxHintsDB(c *cli.Command, sys *sdk.System) error { } } if hintsFileExists && hintsFilePath != "" { - hintsdb, err := badgerh.NewBadgerHints(hintsFilePath) + hintsdb, err := bbolth.NewBoltHints(hintsFilePath) if err == nil { sys.Hints = hintsdb } @@ -58,9 +58,9 @@ var outbox = &cli.Command{ } os.MkdirAll(hintsFilePath, 0755) - _, err := badgerh.NewBadgerHints(hintsFilePath) + _, err := bbolth.NewBoltHints(hintsFilePath) if err != nil { - return fmt.Errorf("failed to create badger hints db at '%s': %w", hintsFilePath, err) + return fmt.Errorf("failed to create bolt hints db at '%s': %w", hintsFilePath, err) } log("initialized hints database at %s\n", hintsFilePath) From 3b4d6046cf12cd5b83a9a6ee7c0a7eb25a6d15c1 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Thu, 4 Sep 2025 13:04:13 -0300 Subject: [PATCH 319/401] nak admin: for nip86 management (the previous command was broken). --- README.md | 2 +- admin.go | 186 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 5 +- go.sum | 8 +-- main.go | 1 + relay.go | 177 +-------------------------------------------------- 6 files changed, 197 insertions(+), 182 deletions(-) create mode 100644 admin.go diff --git a/README.md b/README.md index 7d64ea8..7cade1f 100644 --- a/README.md +++ b/README.md @@ -147,7 +147,7 @@ type the password to decrypt your secret key: ********** ### talk to a relay's NIP-86 management API ```shell -nak relay allowpubkey --sec ncryptsec1qggx54cg270zy9y8krwmfz29jyypsuxken2fkk99gr52qhje968n6mwkrfstqaqhq9eq94pnzl4nff437l4lp4ur2cs4f9um8738s35l2esx2tas48thtfhrk5kq94pf9j2tpk54yuermra0xu6hl5ls --pubkey a9e0f110f636f3191644110c19a33448daf09d7cda9708a769e91b7e91340208 pyramid.fiatjaf.com +nak admin allowpubkey --sec ncryptsec1qggx54cg270zy9y8krwmfz29jyypsuxken2fkk99gr52qhje968n6mwkrfstqaqhq9eq94pnzl4nff437l4lp4ur2cs4f9um8738s35l2esx2tas48thtfhrk5kq94pf9j2tpk54yuermra0xu6hl5ls --pubkey a9e0f110f636f3191644110c19a33448daf09d7cda9708a769e91b7e91340208 pyramid.fiatjaf.com type the password to decrypt your secret key: ********** calling 'allowpubkey' on https://pyramid.fiatjaf.com... { diff --git a/admin.go b/admin.go new file mode 100644 index 0000000..f25fc71 --- /dev/null +++ b/admin.go @@ -0,0 +1,186 @@ +package main + +import ( + "bytes" + "context" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "fmt" + "io" + "net/http" + + "fiatjaf.com/nostr" + "fiatjaf.com/nostr/nip86" + "github.com/urfave/cli/v3" +) + +var admin = &cli.Command{ + Name: "admin", + Usage: "manage relays using the relay management API", + Description: `examples: + nak admin allowpubkey myrelay.com --pubkey 1234... --reason "good user" + nak admin banpubkey myrelay.com --pubkey 1234... --reason "spam" + nak admin listallowedpubkeys myrelay.com + nak admin changerelayname myrelay.com --name "My Relay"`, + ArgsUsage: "", + DisableSliceFlagSeparator: true, + Flags: defaultKeyFlags, + Commands: (func() []*cli.Command { + methods := []struct { + method string + args []string + }{ + {"allowpubkey", []string{"pubkey", "reason"}}, + {"banpubkey", []string{"pubkey", "reason"}}, + {"listallowedpubkeys", nil}, + {"listbannedpubkeys", nil}, + {"listeventsneedingmoderation", nil}, + {"allowevent", []string{"id", "reason"}}, + {"banevent", []string{"id", "reason"}}, + {"listbannedevents", nil}, + {"changerelayname", []string{"name"}}, + {"changerelaydescription", []string{"description"}}, + {"changerelayicon", []string{"icon"}}, + {"allowkind", []string{"kind"}}, + {"disallowkind", []string{"kind"}}, + {"listallowedkinds", nil}, + {"blockip", []string{"ip", "reason"}}, + {"unblockip", []string{"ip", "reason"}}, + {"listblockedips", nil}, + } + + commands := make([]*cli.Command, 0, len(methods)) + for _, def := range methods { + def := def + + flags := make([]cli.Flag, len(def.args), len(def.args)+4) + for i, argName := range def.args { + flags[i] = declareFlag(argName) + } + + cmd := &cli.Command{ + Name: def.method, + Usage: fmt.Sprintf(`the "%s" relay management RPC call`, def.method), + Description: fmt.Sprintf( + `the "%s" management RPC call, see https://nips.nostr.com/86 for more information`, def.method), + Flags: flags, + DisableSliceFlagSeparator: true, + Action: func(ctx context.Context, c *cli.Command) error { + params := make([]any, len(def.args)) + for i, argName := range def.args { + params[i] = getArgument(c, argName) + } + req := nip86.Request{Method: def.method, Params: params} + reqj, _ := json.Marshal(req) + + relayUrls := c.Args().Slice() + if len(relayUrls) == 0 { + stdout(string(reqj)) + return nil + } + + kr, _, err := gatherKeyerFromArguments(ctx, c) + if err != nil { + return err + } + + for _, relayUrl := range relayUrls { + httpUrl := "http" + nostr.NormalizeURL(relayUrl)[2:] + log("calling '%s' on %s... ", def.method, httpUrl) + body := bytes.NewBuffer(nil) + body.Write(reqj) + req, err := http.NewRequestWithContext(ctx, "POST", httpUrl, body) + if err != nil { + return fmt.Errorf("failed to create request: %w", err) + } + + // Authorization + payloadHash := sha256.Sum256(reqj) + tokenEvent := nostr.Event{ + Kind: 27235, + CreatedAt: nostr.Now(), + Tags: nostr.Tags{ + {"u", httpUrl}, + {"method", "POST"}, + {"payload", hex.EncodeToString(payloadHash[:])}, + }, + } + if err := kr.SignEvent(ctx, &tokenEvent); err != nil { + return fmt.Errorf("failed to sign token event: %w", err) + } + evtj, _ := json.Marshal(tokenEvent) + req.Header.Set("Authorization", "Nostr "+base64.StdEncoding.EncodeToString(evtj)) + + // Content-Type + req.Header.Set("Content-Type", "application/nostr+json+rpc") + + // make request to relay + resp, err := http.DefaultClient.Do(req) + if err != nil { + log("failed: %s\n", err) + continue + } + b, err := io.ReadAll(resp.Body) + if err != nil { + log("failed to read response: %s\n", err) + continue + } + if resp.StatusCode >= 300 { + log("failed with status %d\n", resp.StatusCode) + bodyPrintable := string(b) + if len(bodyPrintable) > 300 { + bodyPrintable = bodyPrintable[0:297] + "..." + } + log(bodyPrintable) + continue + } + var response nip86.Response + if err := json.Unmarshal(b, &response); err != nil { + log("bad json response: %s\n", err) + bodyPrintable := string(b) + if len(bodyPrintable) > 300 { + bodyPrintable = bodyPrintable[0:297] + "..." + } + log(bodyPrintable) + continue + } + resp.Body.Close() + + // print the result + log("\n") + pretty, _ := json.MarshalIndent(response, "", " ") + stdout(string(pretty)) + } + + return nil + }, + } + + commands = append(commands, cmd) + } + + return commands + })(), +} + +func declareFlag(argName string) cli.Flag { + usage := "parameter for this management RPC call, see https://nips.nostr.com/86 for more information." + switch argName { + case "kind": + return &cli.IntFlag{Name: argName, Required: true, Usage: usage} + case "reason": + return &cli.StringFlag{Name: argName, Usage: usage} + default: + return &cli.StringFlag{Name: argName, Required: true, Usage: usage} + } +} + +func getArgument(c *cli.Command, argName string) any { + switch argName { + case "kind": + return c.Int(argName) + default: + return c.String(argName) + } +} diff --git a/go.mod b/go.mod index 2cc9575..7cb2f34 100644 --- a/go.mod +++ b/go.mod @@ -38,7 +38,7 @@ require ( github.com/coder/websocket v1.8.13 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect - github.com/dgraph-io/ristretto v1.0.0 // indirect + github.com/dgraph-io/ristretto/v2 v2.3.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/elliotchance/pie/v2 v2.7.0 // indirect github.com/elnosh/gonuts v0.4.2 // indirect @@ -55,7 +55,6 @@ require ( github.com/mattn/go-colorable v0.1.14 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect github.com/rs/cors v1.11.1 // indirect @@ -72,7 +71,7 @@ require ( golang.org/x/crypto v0.39.0 // indirect golang.org/x/net v0.41.0 // indirect golang.org/x/sync v0.16.0 // indirect - golang.org/x/sys v0.34.0 // indirect + golang.org/x/sys v0.35.0 // indirect golang.org/x/text v0.26.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect rsc.io/qr v0.2.0 // indirect diff --git a/go.sum b/go.sum index 45585c8..4f09f0b 100644 --- a/go.sum +++ b/go.sum @@ -60,8 +60,8 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeC github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= -github.com/dgraph-io/ristretto v1.0.0 h1:SYG07bONKMlFDUYu5pEu3DGAh8c2OFNzKm6G9J4Si84= -github.com/dgraph-io/ristretto v1.0.0/go.mod h1:jTi2FiYEhQ1NsMmA7DeBykizjOuY88NhKBkepyu1jPc= +github.com/dgraph-io/ristretto/v2 v2.3.0 h1:qTQ38m7oIyd4GAed/QkUZyPFNMnvVWyazGXRwvOt5zk= +github.com/dgraph-io/ristretto/v2 v2.3.0/go.mod h1:gpoRV3VzrEY1a9dWAYV6T1U7YzfgttXdd/ZzL1s9OZM= github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38= github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= @@ -222,8 +222,8 @@ golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= -golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/main.go b/main.go index 8e683ef..b1689b5 100644 --- a/main.go +++ b/main.go @@ -36,6 +36,7 @@ var app = &cli.Command{ key, verify, relay, + admin, bunker, serve, blossomCmd, diff --git a/relay.go b/relay.go index 450ca33..5b0ff9c 100644 --- a/relay.go +++ b/relay.go @@ -1,30 +1,19 @@ package main import ( - "bytes" "context" - "crypto/sha256" - "encoding/base64" - "encoding/hex" "fmt" - "io" - "net/http" - "fiatjaf.com/nostr" "fiatjaf.com/nostr/nip11" - "fiatjaf.com/nostr/nip86" "github.com/urfave/cli/v3" ) var relay = &cli.Command{ Name: "relay", - Usage: "gets the relay information document for the given relay, as JSON -- or allows usage of the relay management API.", - Description: `examples: - fetching relay information: + Usage: "gets the relay information document for the given relay, as JSON", + Description: ` nak relay nostr.wine - - managing a relay - nak relay nostr.wine banevent --sec 1234 --id 037eb3751073770ff17483b1b1ff125866cd5147668271975ef0a8a8e7ee184a --reason "I don't like it"`, +`, ArgsUsage: "", DisableSliceFlagSeparator: true, Action: func(ctx context.Context, c *cli.Command) error { @@ -44,164 +33,4 @@ var relay = &cli.Command{ } return nil }, - Commands: (func() []*cli.Command { - methods := []struct { - method string - args []string - }{ - {"allowpubkey", []string{"pubkey", "reason"}}, - {"banpubkey", []string{"pubkey", "reason"}}, - {"listallowedpubkeys", nil}, - {"allowpubkey", []string{"pubkey", "reason"}}, - {"listallowedpubkeys", nil}, - {"listeventsneedingmoderation", nil}, - {"allowevent", []string{"id", "reason"}}, - {"banevent", []string{"id", "reason"}}, - {"listbannedevents", nil}, - {"changerelayname", []string{"name"}}, - {"changerelaydescription", []string{"description"}}, - {"changerelayicon", []string{"icon"}}, - {"allowkind", []string{"kind"}}, - {"disallowkind", []string{"kind"}}, - {"listallowedkinds", nil}, - {"blockip", []string{"ip", "reason"}}, - {"unblockip", []string{"ip", "reason"}}, - {"listblockedips", nil}, - } - - commands := make([]*cli.Command, 0, len(methods)) - for _, def := range methods { - def := def - - flags := make([]cli.Flag, len(def.args), len(def.args)+4) - for i, argName := range def.args { - flags[i] = declareFlag(argName) - } - - flags = append(flags, defaultKeyFlags...) - - cmd := &cli.Command{ - Name: def.method, - Usage: fmt.Sprintf(`the "%s" relay management RPC call`, def.method), - Description: fmt.Sprintf( - `the "%s" management RPC call, see https://nips.nostr.com/86 for more information`, def.method), - Flags: flags, - DisableSliceFlagSeparator: true, - Action: func(ctx context.Context, c *cli.Command) error { - params := make([]any, len(def.args)) - for i, argName := range def.args { - params[i] = getArgument(c, argName) - } - req := nip86.Request{Method: def.method, Params: params} - reqj, _ := json.Marshal(req) - - relayUrls := c.Args().Slice() - if len(relayUrls) == 0 { - stdout(string(reqj)) - return nil - } - - kr, _, err := gatherKeyerFromArguments(ctx, c) - if err != nil { - return err - } - - for _, relayUrl := range relayUrls { - httpUrl := "http" + nostr.NormalizeURL(relayUrl)[2:] - log("calling '%s' on %s... ", def.method, httpUrl) - body := bytes.NewBuffer(nil) - body.Write(reqj) - req, err := http.NewRequestWithContext(ctx, "POST", httpUrl, body) - if err != nil { - return fmt.Errorf("failed to create request: %w", err) - } - - // Authorization - payloadHash := sha256.Sum256(reqj) - tokenEvent := nostr.Event{ - Kind: 27235, - CreatedAt: nostr.Now(), - Tags: nostr.Tags{ - {"u", httpUrl}, - {"method", "POST"}, - {"payload", hex.EncodeToString(payloadHash[:])}, - }, - } - if err := kr.SignEvent(ctx, &tokenEvent); err != nil { - return fmt.Errorf("failed to sign token event: %w", err) - } - evtj, _ := json.Marshal(tokenEvent) - req.Header.Set("Authorization", "Nostr "+base64.StdEncoding.EncodeToString(evtj)) - - // Content-Type - req.Header.Set("Content-Type", "application/nostr+json+rpc") - - // make request to relay - resp, err := http.DefaultClient.Do(req) - if err != nil { - log("failed: %s\n", err) - continue - } - b, err := io.ReadAll(resp.Body) - if err != nil { - log("failed to read response: %s\n", err) - continue - } - if resp.StatusCode >= 300 { - log("failed with status %d\n", resp.StatusCode) - bodyPrintable := string(b) - if len(bodyPrintable) > 300 { - bodyPrintable = bodyPrintable[0:297] + "..." - } - log(bodyPrintable) - continue - } - var response nip86.Response - if err := json.Unmarshal(b, &response); err != nil { - log("bad json response: %s\n", err) - bodyPrintable := string(b) - if len(bodyPrintable) > 300 { - bodyPrintable = bodyPrintable[0:297] + "..." - } - log(bodyPrintable) - continue - } - resp.Body.Close() - - // print the result - log("\n") - pretty, _ := json.MarshalIndent(response, "", " ") - stdout(string(pretty)) - } - - return nil - }, - } - - commands = append(commands, cmd) - } - - return commands - })(), -} - -func declareFlag(argName string) cli.Flag { - usage := "parameter for this management RPC call, see https://nips.nostr.com/86 for more information." - switch argName { - case "kind": - return &cli.IntFlag{Name: argName, Required: true, Usage: usage} - case "reason": - return &cli.StringFlag{Name: argName, Usage: usage} - default: - return &cli.StringFlag{Name: argName, Required: true, Usage: usage} - } -} - -func getArgument(c *cli.Command, argName string) any { - switch argName { - case "kind": - return c.Int(argName) - default: - return c.String(argName) - } } From cdd64e340fb1851ed0271bd37a997168253a7fbf Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Fri, 5 Sep 2025 17:12:21 -0300 Subject: [PATCH 320/401] nak req --outbox --- req.go | 73 ++++++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 64 insertions(+), 9 deletions(-) diff --git a/req.go b/req.go index 2fe7301..0cd6629 100644 --- a/req.go +++ b/req.go @@ -44,6 +44,17 @@ example: Usage: "keep the subscription open, printing all events as they are returned", DefaultText: "false, will close on EOSE", }, + &cli.BoolFlag{ + Name: "outbox", + Usage: "use outbox relays from specified public keys", + DefaultText: "false, will only use manually-specified relays", + }, + &cli.UintFlag{ + Name: "outbox-relays-number", + Aliases: []string{"n"}, + Usage: "number of outbox relays to use for each pubkey", + Value: 3, + }, &cli.BoolFlag{ Name: "paginate", Usage: "make multiple REQs to the relay decreasing the value of 'until' until 'limit' or 'since' conditions are met", @@ -76,6 +87,14 @@ example: ), ArgsUsage: "[relay...]", Action: func(ctx context.Context, c *cli.Command) error { + if c.Bool("paginate") && c.Bool("stream") { + return fmt.Errorf("incompatible flags --paginate and --stream") + } + + if c.Bool("paginate") && c.Bool("outbox") { + return fmt.Errorf("incompatible flags --paginate and --outbox") + } + relayUrls := c.Args().Slice() if len(relayUrls) > 0 { // this is used both for the normal AUTH (after "auth-required:" is received) or forced pre-auth @@ -129,7 +148,7 @@ example: return err } - if len(relayUrls) > 0 { + if len(relayUrls) > 0 || c.Bool("outbox") { if c.Bool("ids-only") { seen := make(map[nostr.ID]struct{}, max(500, filter.Limit)) for _, url := range relayUrls { @@ -147,16 +166,52 @@ example: } } } else { - fn := sys.Pool.FetchMany - if c.Bool("paginate") { - fn = sys.Pool.PaginatorWithInterval(c.Duration("paginate-interval")) - } else if c.Bool("stream") { - fn = sys.Pool.SubscribeMany + var results chan nostr.RelayEvent + opts := nostr.SubscriptionOptions{ + Label: "nak-req", } - for ie := range fn(ctx, relayUrls, filter, nostr.SubscriptionOptions{ - Label: "nak-req", - }) { + if c.Bool("paginate") { + paginator := sys.Pool.PaginatorWithInterval(c.Duration("paginate-interval")) + results = paginator(ctx, relayUrls, filter, opts) + } else if c.Bool("outbox") { + defs := make([]nostr.DirectedFilter, 0, len(filter.Authors)*2) + + // hardcoded relays, if any + for _, relayUrl := range relayUrls { + defs = append(defs, nostr.DirectedFilter{ + Filter: filter, + Relay: relayUrl, + }) + } + + // relays for each pubkey + for _, pubkey := range filter.Authors { + n := int(c.Uint("outbox-relays-number")) + this := filter.Clone() + this.Authors = []nostr.PubKey{pubkey} + for _, url := range sys.FetchOutboxRelays(ctx, pubkey, n) { + defs = append(defs, nostr.DirectedFilter{ + Filter: this, + Relay: url, + }) + } + } + + if c.Bool("stream") { + results = sys.Pool.BatchedSubscribeMany(ctx, defs, opts) + } else { + results = sys.Pool.BatchedQueryMany(ctx, defs, opts) + } + } else { + if c.Bool("stream") { + results = sys.Pool.SubscribeMany(ctx, relayUrls, filter, opts) + } else { + results = sys.Pool.FetchMany(ctx, relayUrls, filter, opts) + } + } + + for ie := range results { stdout(ie.Event) } } From 13452e6916e3476f116cacced2930c83025e858b Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sat, 6 Sep 2025 07:39:25 -0300 Subject: [PATCH 321/401] fix nostrlib dependency. --- go.mod | 8 +++----- go.sum | 9 ++++++--- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/go.mod b/go.mod index 7cb2f34..5725256 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.24.1 require ( fiatjaf.com/lib v0.3.1 - fiatjaf.com/nostr v0.0.0-20250829192328-aa321f6e7f10 + fiatjaf.com/nostr v0.0.0-20250905141851-8750197ea7a8 github.com/bep/debounce v1.2.1 github.com/btcsuite/btcd/btcec/v2 v2.3.5 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e @@ -35,7 +35,7 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chzyer/logex v1.1.10 // indirect github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect - github.com/coder/websocket v1.8.13 // indirect + github.com/coder/websocket v1.8.14 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect github.com/dgraph-io/ristretto/v2 v2.3.0 // indirect @@ -61,7 +61,7 @@ require ( github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 // indirect github.com/tetratelabs/wazero v1.8.0 // indirect github.com/tidwall/gjson v1.18.0 // indirect - github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/match v1.2.0 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.59.0 // indirect @@ -76,5 +76,3 @@ require ( gopkg.in/yaml.v3 v3.0.1 // indirect rsc.io/qr v0.2.0 // indirect ) - -replace fiatjaf.com/nostr => ../nostrlib diff --git a/go.sum b/go.sum index 4f09f0b..d50d0b2 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ fiatjaf.com/lib v0.3.1 h1:/oFQwNtFRfV+ukmOCxfBEAuayoLwXp4wu2/fz5iHpwA= fiatjaf.com/lib v0.3.1/go.mod h1:Ycqq3+mJ9jAWu7XjbQI1cVr+OFgnHn79dQR5oTII47g= +fiatjaf.com/nostr v0.0.0-20250905141851-8750197ea7a8 h1:qRilSzVyy8vA9gxbsNxm4D8ZsV2WmGdyNq6B2Tkclo8= +fiatjaf.com/nostr v0.0.0-20250905141851-8750197ea7a8/go.mod h1:RHPNZ7jtRi7dZf590g1urHYhWXnbetvd1oByNbAlgKE= 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= @@ -47,8 +49,8 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5O github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE= -github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= +github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g= +github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -172,8 +174,9 @@ github.com/tetratelabs/wazero v1.8.0 h1:iEKu0d4c2Pd+QSRieYbnQC9yiFlMS9D+Jr0LsRmc github.com/tetratelabs/wazero v1.8.0/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= -github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM= +github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= From 925170246054f3eb02d5fa148e88cd027bc0e0b3 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sat, 6 Sep 2025 22:20:50 -0300 Subject: [PATCH 322/401] query batching on nak req --outbox. --- go.mod | 2 +- req.go | 56 +++++++++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 48 insertions(+), 10 deletions(-) diff --git a/go.mod b/go.mod index 5725256..2131920 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,7 @@ require ( github.com/stretchr/testify v1.10.0 github.com/urfave/cli/v3 v3.0.0-beta1 golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b + golang.org/x/sync v0.16.0 golang.org/x/term v0.32.0 ) @@ -70,7 +71,6 @@ require ( go.etcd.io/bbolt v1.4.2 // indirect golang.org/x/crypto v0.39.0 // indirect golang.org/x/net v0.41.0 // indirect - golang.org/x/sync v0.16.0 // indirect golang.org/x/sys v0.35.0 // indirect golang.org/x/text v0.26.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/req.go b/req.go index 0cd6629..bd410a4 100644 --- a/req.go +++ b/req.go @@ -4,7 +4,9 @@ import ( "context" "fmt" "os" + "slices" "strings" + "sync" "fiatjaf.com/nostr" "fiatjaf.com/nostr/nip42" @@ -12,6 +14,7 @@ import ( "github.com/fatih/color" "github.com/mailru/easyjson" "github.com/urfave/cli/v3" + "golang.org/x/sync/errgroup" ) const ( @@ -186,17 +189,52 @@ example: } // relays for each pubkey + errg := errgroup.Group{} + errg.SetLimit(16) + mu := sync.Mutex{} for _, pubkey := range filter.Authors { - n := int(c.Uint("outbox-relays-number")) - this := filter.Clone() - this.Authors = []nostr.PubKey{pubkey} - for _, url := range sys.FetchOutboxRelays(ctx, pubkey, n) { - defs = append(defs, nostr.DirectedFilter{ - Filter: this, - Relay: url, - }) - } + errg.Go(func() error { + n := int(c.Uint("outbox-relays-number")) + for _, url := range sys.FetchOutboxRelays(ctx, pubkey, n) { + if slices.Contains(relayUrls, url) { + // already hardcoded, ignore + continue + } + if !nostr.IsValidRelayURL(url) { + continue + } + + matchUrl := func(def nostr.DirectedFilter) bool { return def.Relay == url } + idx := slices.IndexFunc(defs, matchUrl) + if idx == -1 { + // new relay, add it + mu.Lock() + // check again after locking to prevent races + idx = slices.IndexFunc(defs, matchUrl) + if idx == -1 { + // then add it + filter := filter.Clone() + filter.Authors = []nostr.PubKey{pubkey} + defs = append(defs, nostr.DirectedFilter{ + Filter: filter, + Relay: url, + }) + mu.Unlock() + continue // done with this relay url + } + + // otherwise we'll just use the idx + mu.Unlock() + } + + // existing relay, add this pubkey + defs[idx].Authors = append(defs[idx].Authors, pubkey) + } + + return nil + }) } + errg.Wait() if c.Bool("stream") { results = sys.Pool.BatchedSubscribeMany(ctx, defs, opts) From ecb7f8f195d5ccc9915f825bb5efd3cc835f5ff1 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sun, 7 Sep 2025 18:56:51 -0300 Subject: [PATCH 323/401] event: renew relay connection before publishing if necessary. --- event.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/event.go b/event.go index f8b3705..7516363 100644 --- a/event.go +++ b/event.go @@ -403,13 +403,20 @@ func publishFlow(ctx context.Context, c *cli.Command, kr nostr.Signer, evt nostr } } else { // normal dumb flow - for _, relay := range relays { + for i, relay := range relays { publish: cleanUrl, _ := strings.CutPrefix(relay.URL, "wss://") log("publishing to %s... ", color.CyanString(cleanUrl)) ctx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() + if !relay.IsConnected() { + if new_, err := sys.Pool.EnsureRelay(relay.URL); err == nil { + relays[i] = new_ + relay = new_ + } + } + err := relay.Publish(ctx, evt) if err == nil { // published fine From 2758285d51caa98ab9c11adbb0678dad6e3fbeb3 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Mon, 8 Sep 2025 11:11:07 -0300 Subject: [PATCH 324/401] update nostrlib. --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 2131920..6d51ed2 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.24.1 require ( fiatjaf.com/lib v0.3.1 - fiatjaf.com/nostr v0.0.0-20250905141851-8750197ea7a8 + fiatjaf.com/nostr v0.0.0-20250907220143-b67e3092b02e github.com/bep/debounce v1.2.1 github.com/btcsuite/btcd/btcec/v2 v2.3.5 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e diff --git a/go.sum b/go.sum index d50d0b2..2b1f672 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ fiatjaf.com/lib v0.3.1 h1:/oFQwNtFRfV+ukmOCxfBEAuayoLwXp4wu2/fz5iHpwA= fiatjaf.com/lib v0.3.1/go.mod h1:Ycqq3+mJ9jAWu7XjbQI1cVr+OFgnHn79dQR5oTII47g= -fiatjaf.com/nostr v0.0.0-20250905141851-8750197ea7a8 h1:qRilSzVyy8vA9gxbsNxm4D8ZsV2WmGdyNq6B2Tkclo8= -fiatjaf.com/nostr v0.0.0-20250905141851-8750197ea7a8/go.mod h1:RHPNZ7jtRi7dZf590g1urHYhWXnbetvd1oByNbAlgKE= +fiatjaf.com/nostr v0.0.0-20250907220143-b67e3092b02e h1:SnM2u7nAlK5rUFJsQYOdTUOAbz0B9Z9ihq95akFHdOE= +fiatjaf.com/nostr v0.0.0-20250907220143-b67e3092b02e/go.mod h1:RHPNZ7jtRi7dZf590g1urHYhWXnbetvd1oByNbAlgKE= 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= From 210c0aa2820536f3bb947faed93a334c107961c9 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 4 Nov 2025 09:17:57 -0300 Subject: [PATCH 325/401] update nostrlib again, mostly for the blossom client timeout issue. --- go.mod | 10 +++++----- go.sum | 20 ++++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/go.mod b/go.mod index 6d51ed2..bbf0ae4 100644 --- a/go.mod +++ b/go.mod @@ -4,16 +4,16 @@ go 1.24.1 require ( fiatjaf.com/lib v0.3.1 - fiatjaf.com/nostr v0.0.0-20250907220143-b67e3092b02e + fiatjaf.com/nostr v0.0.0-20251104112613-38a6ca92b954 github.com/bep/debounce v1.2.1 - github.com/btcsuite/btcd/btcec/v2 v2.3.5 + github.com/btcsuite/btcd/btcec/v2 v2.3.6 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/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/mailru/easyjson v0.9.1 github.com/mark3labs/mcp-go v0.8.3 github.com/markusmobius/go-dateparser v1.2.3 github.com/mattn/go-isatty v0.0.20 @@ -21,8 +21,8 @@ require ( github.com/mdp/qrterminal/v3 v3.2.1 github.com/stretchr/testify v1.10.0 github.com/urfave/cli/v3 v3.0.0-beta1 - golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b - golang.org/x/sync v0.16.0 + golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 + golang.org/x/sync v0.17.0 golang.org/x/term v0.32.0 ) diff --git a/go.sum b/go.sum index 2b1f672..556b0db 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ fiatjaf.com/lib v0.3.1 h1:/oFQwNtFRfV+ukmOCxfBEAuayoLwXp4wu2/fz5iHpwA= fiatjaf.com/lib v0.3.1/go.mod h1:Ycqq3+mJ9jAWu7XjbQI1cVr+OFgnHn79dQR5oTII47g= -fiatjaf.com/nostr v0.0.0-20250907220143-b67e3092b02e h1:SnM2u7nAlK5rUFJsQYOdTUOAbz0B9Z9ihq95akFHdOE= -fiatjaf.com/nostr v0.0.0-20250907220143-b67e3092b02e/go.mod h1:RHPNZ7jtRi7dZf590g1urHYhWXnbetvd1oByNbAlgKE= +fiatjaf.com/nostr v0.0.0-20251104112613-38a6ca92b954 h1:CMD8D3TgEjGhuIBNMnvZ0EXOW0JR9O3w8AI6Yuzt8Ec= +fiatjaf.com/nostr v0.0.0-20251104112613-38a6ca92b954/go.mod h1:Nq86Jjsd0OmsOEImUg0iCcLuqM5B67Nj2eu/2dP74Ss= 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= @@ -20,8 +20,8 @@ github.com/btcsuite/btcd v0.24.2 h1:aLmxPguqxza+4ag8R1I2nnJjSu2iFn/kqtHTIImswcY= github.com/btcsuite/btcd v0.24.2/go.mod h1:5C8ChTkl5ejr3WHj8tkQSCmydiMEPB0ZhQhehpq7Dgg= github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA= github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= -github.com/btcsuite/btcd/btcec/v2 v2.3.5 h1:dpAlnAwmT1yIBm3exhT1/8iUSD98RDJM5vqJVQDQLiU= -github.com/btcsuite/btcd/btcec/v2 v2.3.5/go.mod h1:m22FrOAiuxl/tht9wIqAoGHcbnCCaPWyauO8y2LGGtQ= +github.com/btcsuite/btcd/btcec/v2 v2.3.6 h1:IzlsEr9olcSRKB/n7c4351F3xHKxS2lma+1UFGCYd4E= +github.com/btcsuite/btcd/btcec/v2 v2.3.6/go.mod h1:m22FrOAiuxl/tht9wIqAoGHcbnCCaPWyauO8y2LGGtQ= github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A= github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE= github.com/btcsuite/btcd/btcutil v1.1.5 h1:+wER79R5670vs/ZusMTF1yTcRYE5GUsFbdjdisflzM8= @@ -123,8 +123,8 @@ github.com/liamg/magic v0.0.1 h1:Ru22ElY+sCh6RvRTWjQzKKCxsEco8hE0co8n1qe7TBM= github.com/liamg/magic v0.0.1/go.mod h1:yQkOmZZI52EA+SQ2xyHpVw8fNvTBruF873Y+Vt6S+fk= github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo= github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= -github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= -github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= +github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mark3labs/mcp-go v0.8.3 h1:IzlyN8BaP4YwUMUDqxOGJhGdZXEDQiAPX43dNPgnzrg= github.com/mark3labs/mcp-go v0.8.3/go.mod h1:cjMlBU0cv/cj9kjlgmRhoJ5JREdS7YX83xeIG9Ko/jE= github.com/markusmobius/go-dateparser v1.2.3 h1:TvrsIvr5uk+3v6poDjaicnAFJ5IgtFHgLiuMY2Eb7Nw= @@ -203,8 +203,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= -golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b h1:DXr+pvt3nC887026GRP39Ej11UATqWDmWuS99x26cD0= -golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4= +golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= +golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -213,8 +213,8 @@ golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= -golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 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= From bef3739a6786bba7fcc257a43ad5b30bd8c608f5 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 11 Nov 2025 15:58:53 -0300 Subject: [PATCH 326/401] accept npub/nprofile/nevent instead of just hex in flags. --- blossom.go | 7 +++---- count.go | 2 +- encode.go | 2 +- flags.go | 8 ++++---- helpers.go | 44 ++++++++++++++++++++++++++++++++++++++++++++ outbox.go | 3 +-- req.go | 4 ++-- 7 files changed, 56 insertions(+), 14 deletions(-) diff --git a/blossom.go b/blossom.go index b5e6e75..45a5eaa 100644 --- a/blossom.go +++ b/blossom.go @@ -7,7 +7,6 @@ import ( "io" "os" - "fiatjaf.com/nostr" "fiatjaf.com/nostr/keyer" "fiatjaf.com/nostr/nipb0/blossom" "github.com/urfave/cli/v3" @@ -38,11 +37,11 @@ var blossomCmd = &cli.Command{ var client *blossom.Client pubkey := c.Args().First() if pubkey != "" { - if pk, err := nostr.PubKeyFromHex(pubkey); err != nil { + pk, err := parsePubKey(pubkey) + if err != nil { return fmt.Errorf("invalid public key '%s': %w", pubkey, err) - } else { - client = blossom.NewClient(c.String("server"), keyer.NewReadOnlySigner(pk)) } + client = blossom.NewClient(c.String("server"), keyer.NewReadOnlySigner(pk)) } else { var err error client, err = getBlossomClient(ctx, c) diff --git a/count.go b/count.go index 662baf7..48bcaa4 100644 --- a/count.go +++ b/count.go @@ -21,7 +21,7 @@ var count = &cli.Command{ &PubKeySliceFlag{ Name: "author", Aliases: []string{"a"}, - Usage: "only accept events from these authors (pubkey as hex)", + Usage: "only accept events from these authors", Category: CATEGORY_FILTER_ATTRIBUTES, }, &cli.IntSliceFlag{ diff --git a/encode.go b/encode.go index 59c5a58..d15bf90 100644 --- a/encode.go +++ b/encode.go @@ -163,7 +163,7 @@ var encode = &cli.Command{ DisableSliceFlagSeparator: true, Action: func(ctx context.Context, c *cli.Command) error { for target := range getStdinLinesOrArguments(c.Args()) { - id, err := nostr.IDFromHex(target) + id, err := parseEventID(target) if err != nil { ctx = lineProcessingError(ctx, "invalid event id: %s", target) continue diff --git a/flags.go b/flags.go index 7e4e32d..671e2d2 100644 --- a/flags.go +++ b/flags.go @@ -96,8 +96,8 @@ func (t pubkeyValue) Create(val nostr.PubKey, p *nostr.PubKey, c struct{}) cli.V func (t pubkeyValue) ToString(b nostr.PubKey) string { return t.pubkey.String() } func (t *pubkeyValue) Set(value string) error { - pk, err := nostr.PubKeyFromHex(value) - t.pubkey = pk + pubkey, err := parsePubKey(value) + t.pubkey = pubkey t.hasBeenSet = true return err } @@ -147,8 +147,8 @@ func (t idValue) Create(val nostr.ID, p *nostr.ID, c struct{}) cli.Value { func (t idValue) ToString(b nostr.ID) string { return t.id.String() } func (t *idValue) Set(value string) error { - pk, err := nostr.IDFromHex(value) - t.id = pk + id, err := parseEventID(value) + t.id = id t.hasBeenSet = true return err } diff --git a/helpers.go b/helpers.go index de5da1b..02d30ed 100644 --- a/helpers.go +++ b/helpers.go @@ -464,6 +464,50 @@ func askConfirmation(msg string) bool { } } +func parsePubKey(value string) (nostr.PubKey, error) { + pk, err := nostr.PubKeyFromHex(value) + if err == nil { + return pk, nil + } + + if prefix, decoded, err := nip19.Decode(value); err == nil { + switch prefix { + case "npub": + if pk, ok := decoded.(nostr.PubKey); ok { + return pk, nil + } + case "nprofile": + if profile, ok := decoded.(nostr.ProfilePointer); ok { + return profile.PublicKey, nil + } + } + } + + return nostr.PubKey{}, fmt.Errorf("invalid pubkey (\"%s\"): expected hex, npub, or nprofile", value) +} + +func parseEventID(value string) (nostr.ID, error) { + id, err := nostr.IDFromHex(value) + if err == nil { + return id, nil + } + + if prefix, decoded, err := nip19.Decode(value); err == nil { + switch prefix { + case "note": + if id, ok := decoded.(nostr.ID); ok { + return id, nil + } + case "nevent": + if event, ok := decoded.(nostr.EventPointer); ok { + return event.ID, nil + } + } + } + + return nostr.ID{}, fmt.Errorf("invalid event id (\"%s\"): expected hex, note, or nevent", value) +} + var colors = struct { reset func(...any) (int, error) italic func(...any) string diff --git a/outbox.go b/outbox.go index fc916f0..82fc775 100644 --- a/outbox.go +++ b/outbox.go @@ -6,7 +6,6 @@ import ( "os" "path/filepath" - "fiatjaf.com/nostr" "fiatjaf.com/nostr/sdk" "fiatjaf.com/nostr/sdk/hints/bbolth" "github.com/fatih/color" @@ -82,7 +81,7 @@ var outbox = &cli.Command{ return fmt.Errorf("expected exactly one argument (pubkey)") } - pk, err := nostr.PubKeyFromHex(c.Args().First()) + pk, err := parsePubKey(c.Args().First()) if err != nil { return fmt.Errorf("invalid public key '%s': %w", c.Args().First(), err) } diff --git a/req.go b/req.go index bd410a4..554f0db 100644 --- a/req.go +++ b/req.go @@ -276,13 +276,13 @@ var reqFilterFlags = []cli.Flag{ &PubKeySliceFlag{ Name: "author", Aliases: []string{"a"}, - Usage: "only accept events from these authors (pubkey as hex)", + Usage: "only accept events from these authors", Category: CATEGORY_FILTER_ATTRIBUTES, }, &IDSliceFlag{ Name: "id", Aliases: []string{"i"}, - Usage: "only accept events with these ids (hex)", + Usage: "only accept events with these ids", Category: CATEGORY_FILTER_ATTRIBUTES, }, &cli.IntSliceFlag{ From e0ca768695d300887584c6d6e8f3d7284d5cc01b Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 11 Nov 2025 16:32:14 -0300 Subject: [PATCH 327/401] also parse npub/nevent/naddr when used as tag values, turn them into their corresponding hex or address format. --- count.go | 6 +++--- event.go | 18 ++++++++++++------ helpers.go | 9 +++++++++ req.go | 8 ++++---- 4 files changed, 28 insertions(+), 13 deletions(-) diff --git a/count.go b/count.go index 48bcaa4..e0d2080 100644 --- a/count.go +++ b/count.go @@ -101,16 +101,16 @@ var count = &cli.Command{ for _, tagFlag := range c.StringSlice("tag") { spl := strings.SplitN(tagFlag, "=", 2) if len(spl) == 2 { - tags = append(tags, spl) + tags = append(tags, []string{spl[0], decodeTagValue(spl[1])}) } else { return fmt.Errorf("invalid --tag '%s'", tagFlag) } } for _, etag := range c.StringSlice("e") { - tags = append(tags, []string{"e", etag}) + tags = append(tags, []string{"e", decodeTagValue(etag)}) } for _, ptag := range c.StringSlice("p") { - tags = append(tags, []string{"p", ptag}) + tags = append(tags, []string{"p", decodeTagValue(ptag)}) } if len(tags) > 0 { filter.Tags = make(nostr.TagMap) diff --git a/event.go b/event.go index 7516363..fb4de9f 100644 --- a/event.go +++ b/event.go @@ -211,24 +211,30 @@ example: if found { // tags may also contain extra elements separated with a ";" tagValues := strings.Split(tagValue, ";") + if len(tagValues) >= 1 { + tagValues[0] = decodeTagValue(tagValues[0]) + } tag = append(tag, tagValues...) } tags = append(tags, tag) } for _, etag := range c.StringSlice("e") { - if tags.FindWithValue("e", etag) == nil { - tags = append(tags, nostr.Tag{"e", etag}) + decodedEtag := decodeTagValue(etag) + if tags.FindWithValue("e", decodedEtag) == nil { + tags = append(tags, nostr.Tag{"e", decodedEtag}) } } for _, ptag := range c.StringSlice("p") { - if tags.FindWithValue("p", ptag) == nil { - tags = append(tags, nostr.Tag{"p", ptag}) + decodedPtag := decodeTagValue(ptag) + if tags.FindWithValue("p", decodedPtag) == nil { + tags = append(tags, nostr.Tag{"p", decodedPtag}) } } for _, dtag := range c.StringSlice("d") { - if tags.FindWithValue("d", dtag) == nil { - tags = append(tags, nostr.Tag{"d", dtag}) + decodedDtag := decodeTagValue(dtag) + if tags.FindWithValue("d", decodedDtag) == nil { + tags = append(tags, nostr.Tag{"d", decodedDtag}) } } if len(tags) > 0 { diff --git a/helpers.go b/helpers.go index 02d30ed..68e3c72 100644 --- a/helpers.go +++ b/helpers.go @@ -508,6 +508,15 @@ func parseEventID(value string) (nostr.ID, error) { return nostr.ID{}, fmt.Errorf("invalid event id (\"%s\"): expected hex, note, or nevent", value) } +func decodeTagValue(value string) string { + if strings.HasPrefix(value, "npub1") || strings.HasPrefix(value, "nevent1") || strings.HasPrefix(value, "note1") || strings.HasPrefix(value, "nprofile1") || strings.HasPrefix(value, "naddr1") { + if ptr, err := nip19.ToPointer(value); err == nil { + return ptr.AsTagReference() + } + } + return value +} + var colors = struct { reset func(...any) (int, error) italic func(...any) string diff --git a/req.go b/req.go index 554f0db..2725054 100644 --- a/req.go +++ b/req.go @@ -354,19 +354,19 @@ func applyFlagsToFilter(c *cli.Command, filter *nostr.Filter) error { for _, tagFlag := range c.StringSlice("tag") { spl := strings.SplitN(tagFlag, "=", 2) if len(spl) == 2 { - tags = append(tags, spl) + tags = append(tags, []string{spl[0], decodeTagValue(spl[1])}) } else { return fmt.Errorf("invalid --tag '%s'", tagFlag) } } for _, etag := range c.StringSlice("e") { - tags = append(tags, []string{"e", etag}) + tags = append(tags, []string{"e", decodeTagValue(etag)}) } for _, ptag := range c.StringSlice("p") { - tags = append(tags, []string{"p", ptag}) + tags = append(tags, []string{"p", decodeTagValue(ptag)}) } for _, dtag := range c.StringSlice("d") { - tags = append(tags, []string{"d", dtag}) + tags = append(tags, []string{"d", decodeTagValue(dtag)}) } if len(tags) > 0 && filter.Tags == nil { From 85a04aa7ceb368814e9e0e713773178379641826 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Wed, 12 Nov 2025 00:01:45 -0300 Subject: [PATCH 328/401] req --only-missing for negentropy downloading. --- go.mod | 2 +- go.sum | 4 +-- req.go | 98 ++++++++++++++++++++++++++++++++++++++++++++++++++-------- 3 files changed, 88 insertions(+), 16 deletions(-) diff --git a/go.mod b/go.mod index bbf0ae4..2452abf 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.24.1 require ( fiatjaf.com/lib v0.3.1 - fiatjaf.com/nostr v0.0.0-20251104112613-38a6ca92b954 + fiatjaf.com/nostr v0.0.0-20251112024900-1c43f0d66643 github.com/bep/debounce v1.2.1 github.com/btcsuite/btcd/btcec/v2 v2.3.6 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e diff --git a/go.sum b/go.sum index 556b0db..0b64989 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ fiatjaf.com/lib v0.3.1 h1:/oFQwNtFRfV+ukmOCxfBEAuayoLwXp4wu2/fz5iHpwA= fiatjaf.com/lib v0.3.1/go.mod h1:Ycqq3+mJ9jAWu7XjbQI1cVr+OFgnHn79dQR5oTII47g= -fiatjaf.com/nostr v0.0.0-20251104112613-38a6ca92b954 h1:CMD8D3TgEjGhuIBNMnvZ0EXOW0JR9O3w8AI6Yuzt8Ec= -fiatjaf.com/nostr v0.0.0-20251104112613-38a6ca92b954/go.mod h1:Nq86Jjsd0OmsOEImUg0iCcLuqM5B67Nj2eu/2dP74Ss= +fiatjaf.com/nostr v0.0.0-20251112024900-1c43f0d66643 h1:GcoAN1FQV+rayCIklvj+mIB/ZR3Oni98C3bS/M+vzts= +fiatjaf.com/nostr v0.0.0-20251112024900-1c43f0d66643/go.mod h1:Nq86Jjsd0OmsOEImUg0iCcLuqM5B67Nj2eu/2dP74Ss= 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= diff --git a/req.go b/req.go index 2725054..7b28049 100644 --- a/req.go +++ b/req.go @@ -1,14 +1,19 @@ package main import ( + "bufio" "context" "fmt" + "math" "os" "slices" "strings" "sync" "fiatjaf.com/nostr" + "fiatjaf.com/nostr/eventstore" + "fiatjaf.com/nostr/eventstore/slicestore" + "fiatjaf.com/nostr/eventstore/wrappers" "fiatjaf.com/nostr/nip42" "fiatjaf.com/nostr/nip77" "github.com/fatih/color" @@ -38,6 +43,11 @@ example: DisableSliceFlagSeparator: true, Flags: append(defaultKeyFlags, append(reqFilterFlags, + &cli.StringFlag{ + Name: "only-missing", + Usage: "use nip77 negentropy to only fetch events that aren't present in the given jsonl file", + TakesFile: true, + }, &cli.BoolFlag{ Name: "ids-only", Usage: "use nip77 to fetch just a list of ids", @@ -53,7 +63,7 @@ example: DefaultText: "false, will only use manually-specified relays", }, &cli.UintFlag{ - Name: "outbox-relays-number", + Name: "outbox-relays-per-pubkey", Aliases: []string{"n"}, Usage: "number of outbox relays to use for each pubkey", Value: 3, @@ -90,6 +100,13 @@ example: ), ArgsUsage: "[relay...]", Action: func(ctx context.Context, c *cli.Command) error { + negentropy := c.Bool("ids-only") || c.IsSet("only-missing") + if negentropy { + if c.Bool("paginate") || c.Bool("stream") || c.Bool("outbox") { + return fmt.Errorf("negentropy is incompatible with --stream, --outbox or --paginate") + } + } + if c.Bool("paginate") && c.Bool("stream") { return fmt.Errorf("incompatible flags --paginate and --stream") } @@ -99,7 +116,7 @@ example: } relayUrls := c.Args().Slice() - if len(relayUrls) > 0 { + if len(relayUrls) > 0 && !negentropy { // this is used both for the normal AUTH (after "auth-required:" is received) or forced pre-auth // connect to all relays we expect to use in this call in parallel forcePreAuthSigner := authSigner @@ -152,20 +169,60 @@ example: } if len(relayUrls) > 0 || c.Bool("outbox") { - if c.Bool("ids-only") { - seen := make(map[nostr.ID]struct{}, max(500, filter.Limit)) - for _, url := range relayUrls { - ch, err := nip77.FetchIDsOnly(ctx, url, filter) + if negentropy { + store := &slicestore.SliceStore{} + store.Init() + + if syncFile := c.String("only-missing"); syncFile != "" { + file, err := os.Open(syncFile) if err != nil { - log("negentropy call to %s failed: %s", url, err) - continue + return fmt.Errorf("failed to open sync file: %w", err) } - for id := range ch { - if _, ok := seen[id]; ok { + defer file.Close() + scanner := bufio.NewScanner(file) + scanner.Buffer(make([]byte, 16*1024*1024), 256*1024*1024) + for scanner.Scan() { + var evt nostr.Event + if err := easyjson.Unmarshal([]byte(scanner.Text()), &evt); err != nil { continue } - seen[id] = struct{}{} - stdout(id) + if err := store.SaveEvent(evt); err != nil || err == eventstore.ErrDupEvent { + continue + } + } + if err := scanner.Err(); err != nil { + return fmt.Errorf("failed to read sync file: %w", err) + } + } + + target := PrintingQuerierPublisher{ + QuerierPublisher: wrappers.StorePublisher{Store: store, MaxLimit: math.MaxInt}, + } + + var source nostr.Querier = nil + if c.IsSet("only-missing") { + source = target + } + + handle := nip77.SyncEventsFromIDs + + if c.Bool("ids-only") { + seen := make(map[nostr.ID]struct{}, max(500, filter.Limit)) + handle = func(ctx context.Context, dir nip77.Direction) { + for id := range dir.Items { + if _, ok := seen[id]; ok { + continue + } + seen[id] = struct{}{} + stdout(id.Hex()) + } + } + } + + for _, url := range relayUrls { + err := nip77.NegentropySync(ctx, url, filter, source, target, handle) + if err != nil { + log("negentropy sync from %s failed: %s", url, err) } } } else { @@ -194,7 +251,7 @@ example: mu := sync.Mutex{} for _, pubkey := range filter.Authors { errg.Go(func() error { - n := int(c.Uint("outbox-relays-number")) + n := int(c.Uint("outbox-relays-per-pubkey")) for _, url := range sys.FetchOutboxRelays(ctx, pubkey, n) { if slices.Contains(relayUrls, url) { // already hardcoded, ignore @@ -395,3 +452,18 @@ func applyFlagsToFilter(c *cli.Command, filter *nostr.Filter) error { return nil } + +type PrintingQuerierPublisher struct { + nostr.QuerierPublisher +} + +func (p PrintingQuerierPublisher) Publish(ctx context.Context, evt nostr.Event) error { + if err := p.QuerierPublisher.Publish(ctx, evt); err == nil { + stdout(evt) + return nil + } else if err == eventstore.ErrDupEvent { + return nil + } else { + return err + } +} From ea4ad84aa0f88984ed463b082bc68ced221f20f3 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Fri, 14 Nov 2025 11:21:25 -0300 Subject: [PATCH 329/401] "nak git" command with "init", "announce" and "push". --- git.go | 736 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ main.go | 1 + 2 files changed, 737 insertions(+) create mode 100644 git.go diff --git a/git.go b/git.go new file mode 100644 index 0000000..e6fe306 --- /dev/null +++ b/git.go @@ -0,0 +1,736 @@ +package main + +import ( + "context" + "fmt" + "os" + "os/exec" + "path/filepath" + "slices" + "strings" + + "fiatjaf.com/nostr" + "fiatjaf.com/nostr/nip19" + "fiatjaf.com/nostr/nip34" + "github.com/chzyer/readline" + "github.com/fatih/color" + "github.com/urfave/cli/v3" +) + +type Nip34Config struct { + Identifier string `json:"identifier"` + Name string `json:"name"` + Description string `json:"description"` + Web []string `json:"web"` + Owner string `json:"owner"` + GraspServers []string `json:"grasp-servers"` + EarliestUniqueCommit string `json:"earliest-unique-commit"` + Maintainers []string `json:"maintainers"` +} + +var git = &cli.Command{ + Name: "git", + Usage: "git-related operations", + Commands: []*cli.Command{ + gitInit, + gitPush, + gitPull, + gitFetch, + gitAnnounce, + }, +} + +var gitInit = &cli.Command{ + Name: "init", + Usage: "initialize a NIP-34 repository configuration", + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "interactive", + Aliases: []string{"i"}, + Usage: "prompt for repository details interactively", + }, + &cli.BoolFlag{ + Name: "force", + Aliases: []string{"f"}, + Usage: "overwrite existing nip34.json file", + }, + &cli.StringFlag{ + Name: "identifier", + Usage: "unique identifier for the repository", + }, + &cli.StringFlag{ + Name: "name", + Usage: "repository name", + }, + &cli.StringFlag{ + Name: "description", + Usage: "repository description", + }, + &cli.StringSliceFlag{ + Name: "web", + Usage: "web URLs for the repository (can be used multiple times)", + }, + &cli.StringFlag{ + Name: "owner", + Usage: "owner public key", + }, + &cli.StringSliceFlag{ + Name: "grasp-servers", + Usage: "grasp servers (can be used multiple times)", + }, + &cli.StringSliceFlag{ + Name: "relays", + Usage: "relay URLs to publish to (can be used multiple times)", + }, + &cli.StringSliceFlag{ + Name: "maintainers", + Usage: "maintainer public keys as npub or hex (can be used multiple times)", + }, + &cli.StringFlag{ + Name: "earliest-unique-commit", + Usage: "earliest unique commit of the repository", + }, + }, + Action: func(ctx context.Context, c *cli.Command) error { + // check if current directory is a git repository + cmd := exec.Command("git", "rev-parse", "--git-dir") + if err := cmd.Run(); err != nil { + return fmt.Errorf("current directory is not a git repository") + } + + // check if nip34.json already exists + configPath := "nip34.json" + var existingConfig Nip34Config + if data, err := os.ReadFile(configPath); err == nil { + // file exists, read it + if !c.Bool("force") && !c.Bool("interactive") { + return fmt.Errorf("nip34.json already exists, use --force to overwrite or --interactive to update") + } + if err := json.Unmarshal(data, &existingConfig); err != nil { + return fmt.Errorf("failed to parse existing nip34.json: %s", err) + } + } + + // get repository base directory name for defaults + cwd, err := os.Getwd() + if err != nil { + return fmt.Errorf("failed to get current directory: %w", err) + } + baseName := filepath.Base(cwd) + + // get earliest unique commit + var earliestCommit string + if output, err := exec.Command("git", "rev-list", "--max-parents=0", "HEAD").Output(); err == nil { + earliest := strings.Split(strings.TrimSpace(string(output)), "\n") + if len(earliest) > 0 { + earliestCommit = earliest[0] + } + } + + // extract clone URLs from nostr:// git remotes + var defaultCloneURLs []string + if output, err := exec.Command("git", "remote", "-v").Output(); err == nil { + remotes := strings.Split(strings.TrimSpace(string(output)), "\n") + for _, remote := range remotes { + if strings.Contains(remote, "nostr://") { + parts := strings.Fields(remote) + if len(parts) >= 2 { + nostrURL := parts[1] + // parse nostr://npub.../relay_hostname/identifier + if strings.HasPrefix(nostrURL, "nostr://") { + urlParts := strings.TrimPrefix(nostrURL, "nostr://") + components := strings.Split(urlParts, "/") + if len(components) == 3 { + npub := components[0] + relayHostname := components[1] + identifier := components[2] + // convert to https://relay_hostname/npub.../identifier.git + cloneURL := fmt.Sprintf("http%s/%s/%s.git", nostr.NormalizeURL(relayHostname)[2:], npub, identifier) + defaultCloneURLs = appendUnique(defaultCloneURLs, cloneURL) + } + } + } + } + } + } + + // helper to get value from flags, existing config, or default + getValue := func(existingVal, flagVal, defaultVal string) string { + if flagVal != "" { + return flagVal + } + if existingVal != "" { + return existingVal + } + return defaultVal + } + + getSliceValue := func(existingVals, flagVals, defaultVals []string) []string { + if len(flagVals) > 0 { + return flagVals + } + if len(existingVals) > 0 { + return existingVals + } + return defaultVals + } + + config := Nip34Config{ + Identifier: getValue(existingConfig.Identifier, c.String("identifier"), baseName), + Name: getValue(existingConfig.Name, c.String("name"), baseName), + Description: getValue(existingConfig.Description, c.String("description"), ""), + Web: getSliceValue(existingConfig.Web, c.StringSlice("web"), []string{}), + Owner: getValue(existingConfig.Owner, c.String("owner"), ""), + GraspServers: getSliceValue(existingConfig.GraspServers, c.StringSlice("grasp-servers"), []string{"gitnostr.com", "relay.ngit.dev"}), + EarliestUniqueCommit: getValue(existingConfig.EarliestUniqueCommit, c.String("earliest-unique-commit"), earliestCommit), + Maintainers: getSliceValue(existingConfig.Maintainers, c.StringSlice("maintainers"), []string{}), + } + + if c.Bool("interactive") { + if err := promptForConfig(&config); err != nil { + return err + } + } + + // write config file + data, err := json.MarshalIndent(config, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal config: %w", err) + } + + if err := os.WriteFile(configPath, data, 0644); err != nil { + return fmt.Errorf("failed to write nip34.json: %w", err) + } + + log("created %s\n", color.GreenString(configPath)) + + // parse owner to npub + pk, err := parsePubKey(config.Owner) + if err != nil { + return fmt.Errorf("invalid owner public key: %w", err) + } + ownerNpub := nip19.EncodeNpub(pk) + + // check existing git remotes + cmd = exec.Command("git", "remote", "-v") + output, err := cmd.Output() + if err != nil { + return fmt.Errorf("failed to get git remotes: %w", err) + } + + remotes := strings.Split(strings.TrimSpace(string(output)), "\n") + var nostrRemote string + for _, remote := range remotes { + if strings.Contains(remote, "nostr://") { + parts := strings.Fields(remote) + if len(parts) >= 2 { + nostrRemote = parts[1] + break + } + } + } + + if nostrRemote == "" { + remoteURL := fmt.Sprintf("nostr://%s/%s/%s", ownerNpub, config.GraspServers[0], config.Identifier) + cmd = exec.Command("git", "remote", "add", "origin", remoteURL) + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed to add git remote: %w", err) + } + log("added git remote: %s\n", remoteURL) + } else { + // validate existing remote + if !strings.HasPrefix(nostrRemote, "nostr://") { + return fmt.Errorf("invalid nostr remote URL: %s", nostrRemote) + } + urlParts := strings.TrimPrefix(nostrRemote, "nostr://") + parts := strings.Split(urlParts, "/") + if len(parts) != 3 { + return fmt.Errorf("invalid nostr URL format, expected nostr:////, got: %s", nostrRemote) + } + repoNpub := parts[0] + relayHostname := parts[1] + identifier := parts[2] + if repoNpub != ownerNpub { + return fmt.Errorf("git remote npub '%s' does not match owner '%s'", repoNpub, ownerNpub) + } + if !slices.Contains(config.GraspServers, relayHostname) { + return fmt.Errorf("git remote relay '%s' not in grasp servers %v", relayHostname, config.GraspServers) + } + if identifier != config.Identifier { + return fmt.Errorf("git remote identifier '%s' does not match config '%s'", identifier, config.Identifier) + } + } + + // gitignore it + excludePath := ".git/info/exclude" + excludeContent, err := os.ReadFile(excludePath) + if err != nil { + // file doesn't exist, create it + excludeContent = []byte("") + } + + // check if nip34.json is already in exclude + if !strings.Contains(string(excludeContent), "nip34.json") { + newContent := string(excludeContent) + if len(newContent) > 0 && !strings.HasSuffix(newContent, "\n") { + newContent += "\n" + } + newContent += "nip34.json\n" + if err := os.WriteFile(excludePath, []byte(newContent), 0644); err != nil { + log(color.YellowString("failed to add nip34.json to .git/info/exclude: %v\n", err)) + } else { + log("added nip34.json to %s\n", color.GreenString(".git/info/exclude")) + } + } + + log("edit %s if needed, then run %s to publish.\n", + color.CyanString("nip34.json"), + color.CyanString("nak git announce")) + + return nil + }, +} + +func repositoriesEqual(a, b nip34.Repository) bool { + if a.ID != b.ID || a.Name != b.Name || a.Description != b.Description { + return false + } + if a.EarliestUniqueCommitID != b.EarliestUniqueCommitID { + return false + } + if len(a.Web) != len(b.Web) || len(a.Clone) != len(b.Clone) || + len(a.Relays) != len(b.Relays) || len(a.Maintainers) != len(b.Maintainers) { + return false + } + for i := range a.Web { + if a.Web[i] != b.Web[i] { + return false + } + } + for i := range a.Clone { + if a.Clone[i] != b.Clone[i] { + return false + } + } + for i := range a.Relays { + if a.Relays[i] != b.Relays[i] { + return false + } + } + for i := range a.Maintainers { + if a.Maintainers[i] != b.Maintainers[i] { + return false + } + } + return true +} + +func promptForConfig(config *Nip34Config) error { + rlConfig := &readline.Config{ + Stdout: os.Stderr, + InterruptPrompt: "^C", + DisableAutoSaveHistory: true, + } + + rl, err := readline.NewEx(rlConfig) + if err != nil { + return err + } + defer rl.Close() + + promptString := func(currentVal *string, prompt string) error { + rl.SetPrompt(color.YellowString("%s [%s]: ", prompt, *currentVal)) + answer, err := rl.Readline() + if err != nil { + return err + } + answer = strings.TrimSpace(answer) + if answer != "" { + *currentVal = answer + } + return nil + } + + promptSlice := func(currentVal *[]string, prompt string) error { + defaultStr := strings.Join(*currentVal, ", ") + rl.SetPrompt(color.YellowString("%s (comma-separated) [%s]: ", prompt, defaultStr)) + answer, err := rl.Readline() + if err != nil { + return err + } + answer = strings.TrimSpace(answer) + if answer != "" { + parts := strings.Split(answer, ",") + result := make([]string, 0, len(parts)) + for _, p := range parts { + if trimmed := strings.TrimSpace(p); trimmed != "" { + result = append(result, trimmed) + } + } + *currentVal = result + } + return nil + } + + log("\nenter repository details (press Enter to keep default):\n\n") + + if err := promptString(&config.Identifier, "identifier"); err != nil { + return err + } + if err := promptString(&config.Name, "name"); err != nil { + return err + } + if err := promptString(&config.Description, "description"); err != nil { + return err + } + if err := promptString(&config.Owner, "owner (npub or hex)"); err != nil { + return err + } + if err := promptSlice(&config.GraspServers, "grasp servers"); err != nil { + return err + } + if err := promptSlice(&config.Web, "web URLs"); err != nil { + return err + } + if err := promptSlice(&config.Maintainers, "other maintainers"); err != nil { + return err + } + + log("\n") + return nil +} + +func gitSanityCheck(localConfig Nip34Config, nostrRemote string) (nostr.PubKey, error) { + urlParts := strings.TrimPrefix(nostrRemote, "nostr://") + parts := strings.Split(urlParts, "/") + if len(parts) != 3 { + return nostr.ZeroPK, fmt.Errorf("invalid nostr URL format, expected nostr:////, got: %s", nostrRemote) + } + + remoteNpub := parts[0] + remoteHostname := parts[1] + remoteIdentifier := parts[2] + + ownerPk, err := parsePubKey(localConfig.Owner) + if err != nil { + return nostr.ZeroPK, fmt.Errorf("invalid owner public key: %w", err) + } + if nip19.EncodeNpub(ownerPk) != remoteNpub { + return nostr.ZeroPK, fmt.Errorf("owner in nip34.json does not match git remote npub") + } + if remoteIdentifier != localConfig.Identifier { + return nostr.ZeroPK, fmt.Errorf("git remote identifier '%s' differs from nip34.json identifier '%s'", remoteIdentifier, localConfig.Identifier) + } + if !slices.Contains(localConfig.GraspServers, remoteHostname) { + return nostr.ZeroPK, fmt.Errorf("git remote relay '%s' not in grasp servers %v", remoteHostname, localConfig.GraspServers) + } + return ownerPk, nil +} + +var gitPush = &cli.Command{ + Name: "push", + Usage: "push git changes", + Flags: defaultKeyFlags, + Action: func(ctx context.Context, c *cli.Command) error { + // setup signer + kr, _, err := gatherKeyerFromArguments(ctx, c) + if err != nil { + return fmt.Errorf("failed to gather keyer: %w", err) + } + + // log publishing as npub + currentPk, _ := kr.GetPublicKey(ctx) + currentNpub := nip19.EncodeNpub(currentPk) + log("publishing as %s\n", color.CyanString(currentNpub)) + + // read nip34.json configuration + configPath := "nip34.json" + var localConfig Nip34Config + data, err := os.ReadFile(configPath) + if err != nil { + return fmt.Errorf("failed to read nip34.json: %w (run 'nak git init' first)", err) + } + if err := json.Unmarshal(data, &localConfig); err != nil { + return fmt.Errorf("failed to parse nip34.json: %w", err) + } + + // get git remotes + cmd := exec.Command("git", "remote", "-v") + output, err := cmd.Output() + if err != nil { + return fmt.Errorf("failed to get git remotes: %w", err) + } + + remotes := strings.Split(strings.TrimSpace(string(output)), "\n") + var nostrRemote string + for _, remote := range remotes { + if strings.Contains(remote, "nostr://") { + parts := strings.Fields(remote) + if len(parts) >= 2 { + nostrRemote = parts[1] + break + } + } + } + + if nostrRemote == "" { + return fmt.Errorf("no nostr:// remote found") + } + + // parse the URL: nostr://// + if !strings.HasPrefix(nostrRemote, "nostr://") { + return fmt.Errorf("invalid nostr remote URL: %s", nostrRemote) + } + + ownerPk, err := gitSanityCheck(localConfig, nostrRemote) + if err != nil { + return err + } + + // fetch repository announcement (30617) and state (30618) events + var repo nip34.Repository + var state nip34.RepositoryState + relays := append(sys.FetchOutboxRelays(ctx, ownerPk, 3), localConfig.GraspServers...) + results := sys.Pool.FetchMany(ctx, relays, nostr.Filter{ + Kinds: []nostr.Kind{30617, 30618}, + Tags: nostr.TagMap{ + "d": []string{localConfig.Identifier}, + }, + Limit: 2, + }, nostr.SubscriptionOptions{ + Label: "nak-git-push", + }) + for ie := range results { + if ie.Event.Kind == 30617 { + repo = nip34.ParseRepository(ie.Event) + } else if ie.Event.Kind == 30618 { + state = nip34.ParseRepositoryState(ie.Event) + } + } + + if repo.Event.ID == nostr.ZeroID { + return fmt.Errorf("no existing repository announcement found") + } + + // check if signer matches owner or is in maintainers + if currentPk != ownerPk && !slices.Contains(repo.Maintainers, currentPk) { + return fmt.Errorf("current user is not allowed to push") + } + + if state.Event.ID != nostr.ZeroID { + log("found state event: %s\n", state.Event.ID) + } + + // get current branch and commit + res, err := exec.Command("git", "rev-parse", "--abbrev-ref", "HEAD").Output() + if err != nil { + return fmt.Errorf("failed to get current branch: %w", err) + } + currentBranch := strings.TrimSpace(string(res)) + + res, err = exec.Command("git", "rev-parse", "HEAD").Output() + if err != nil { + return fmt.Errorf("failed to get current commit: %w", err) + } + currentCommit := strings.TrimSpace(string(res)) + + log("current branch: %s, commit: %s\n", currentBranch, currentCommit) + + // create a new state if we didn't find any + if state.Event.ID == nostr.ZeroID { + state = nip34.RepositoryState{ + ID: repo.ID, + Branches: make(map[string]string), + Tags: make(map[string]string), + } + } + + // update the branch + state.Branches[currentBranch] = currentCommit + log("> setting branch %s to commit %s\n", currentBranch, currentCommit) + + // set the HEAD to the current branch if none is set + if state.HEAD == "" { + state.HEAD = currentBranch + log("> setting HEAD to branch %s\n", currentBranch) + } + + // create and sign the new state event + newStateEvent := state.ToEvent() + err = kr.SignEvent(ctx, &newStateEvent) + if err != nil { + return fmt.Errorf("error signing state event: %w", err) + } + + log("> publishing updated repository state %s\n", newStateEvent.ID) + for res := range sys.Pool.PublishMany(ctx, relays, newStateEvent) { + if res.Error != nil { + log("(!) error publishing event to relay %s: %v\n", res.RelayURL, res.Error) + } else { + log("> published to relay %s\n", res.RelayURL) + } + } + + // push to git clone URLs + for _, cloneURL := range repo.Clone { + log("> pushing to: %s\n", cloneURL) + cmd := exec.Command("git", "push", cloneURL, fmt.Sprintf("refs/heads/%s:refs/heads/%s", currentBranch, currentBranch)) + output, err := cmd.CombinedOutput() + if err != nil { + log("(!) failed to push to %s: %v\n%s\n", cloneURL, err, string(output)) + } else { + log("> successfully pushed to %s\n", cloneURL) + } + } + + return nil + }, +} + +var gitPull = &cli.Command{ + Name: "pull", + Usage: "pull git changes", + Action: func(ctx context.Context, c *cli.Command) error { + return fmt.Errorf("git pull not implemented yet") + }, +} + +var gitFetch = &cli.Command{ + Name: "fetch", + Usage: "fetch git data", + Action: func(ctx context.Context, c *cli.Command) error { + return fmt.Errorf("git fetch not implemented yet") + }, +} + +var gitAnnounce = &cli.Command{ + Name: "announce", + Usage: "announce repository to Nostr", + Flags: defaultKeyFlags, + Action: func(ctx context.Context, c *cli.Command) error { + // check if current directory is a git repository + cmd := exec.Command("git", "rev-parse", "--git-dir") + if err := cmd.Run(); err != nil { + return fmt.Errorf("current directory is not a git repository") + } + + // read nip34.json configuration + configPath := "nip34.json" + var localConfig Nip34Config + data, err := os.ReadFile(configPath) + if err != nil { + return fmt.Errorf("failed to read nip34.json: %w (run 'nak git init' first)", err) + } + if err := json.Unmarshal(data, &localConfig); err != nil { + return fmt.Errorf("failed to parse nip34.json: %w", err) + } + + // get git remotes + cmd = exec.Command("git", "remote", "-v") + output, err := cmd.Output() + if err != nil { + return fmt.Errorf("failed to get git remotes: %w", err) + } + + remotes := strings.Split(strings.TrimSpace(string(output)), "\n") + var nostrRemote string + for _, remote := range remotes { + if strings.Contains(remote, "nostr://") { + parts := strings.Fields(remote) + if len(parts) >= 2 { + nostrRemote = parts[1] + break + } + } + } + + if nostrRemote == "" { + return fmt.Errorf("no nostr:// remote found") + } + + ownerPk, err := gitSanityCheck(localConfig, nostrRemote) + if err != nil { + return err + } + + // setup signer + kr, _, err := gatherKeyerFromArguments(ctx, c) + if err != nil { + return fmt.Errorf("failed to gather keyer: %w", err) + } + currentPk, _ := kr.GetPublicKey(ctx) + + // current signer must match owner otherwise we can't announce + if currentPk != ownerPk { + return fmt.Errorf("current user is not the owner of this repository, can't announce") + } + + // convert local config to nip34.Repository + localRepo := nip34.Repository{ + ID: localConfig.Identifier, + Name: localConfig.Name, + Description: localConfig.Description, + Web: localConfig.Web, + EarliestUniqueCommitID: localConfig.EarliestUniqueCommit, + Maintainers: []nostr.PubKey{}, + } + for _, server := range localConfig.GraspServers { + graspRelayURL := nostr.NormalizeURL(server) + url := fmt.Sprintf("http%s/%s/%s.git", graspRelayURL[2:], nip19.EncodeNpub(ownerPk), localConfig.Identifier) + localRepo.Clone = append(localRepo.Clone, url) + localRepo.Relays = append(localRepo.Relays, graspRelayURL) + } + for _, maintainer := range localConfig.Maintainers { + if pk, err := parsePubKey(maintainer); err == nil { + localRepo.Maintainers = append(localRepo.Maintainers, pk) + } else { + log(color.YellowString("invalid maintainer pubkey '%s': %v\n", maintainer, err)) + } + } + + // fetch repository announcement (30617) events + var repo nip34.Repository + relays := append(sys.FetchOutboxRelays(ctx, ownerPk, 3), localConfig.GraspServers...) + results := sys.Pool.FetchMany(ctx, relays, nostr.Filter{ + Kinds: []nostr.Kind{30617}, + Tags: nostr.TagMap{ + "d": []string{localConfig.Identifier}, + }, + Limit: 1, + }, nostr.SubscriptionOptions{ + Label: "nak-git-announce", + }) + for ie := range results { + repo = nip34.ParseRepository(ie.Event) + } + + // publish repository announcement if needed + var needsAnnouncement bool + if repo.Event.ID == nostr.ZeroID { + log("no existing repository announcement found, will create one\n") + needsAnnouncement = true + } else if !repositoriesEqual(repo, localRepo) { + log("local repository config differs from published announcement, will update\n") + needsAnnouncement = true + } + if needsAnnouncement { + announcementEvent := localRepo.ToEvent() + if err := kr.SignEvent(ctx, &announcementEvent); err != nil { + return fmt.Errorf("failed to sign announcement event: %w", err) + } + + log("> publishing repository announcement %s\n", announcementEvent.ID) + for res := range sys.Pool.PublishMany(ctx, relays, announcementEvent) { + if res.Error != nil { + log("(!) error publishing announcement to relay %s: %v\n", res.RelayURL, res.Error) + } else { + log("> published announcement to relay %s\n", res.RelayURL) + } + } + } else { + log("repository announcement is up to date\n") + } + + return nil + }, +} diff --git a/main.go b/main.go index b1689b5..79af04e 100644 --- a/main.go +++ b/main.go @@ -48,6 +48,7 @@ var app = &cli.Command{ curl, fsCmd, publish, + git, }, Version: version, Flags: []cli.Flag{ From bbe1661096962ed90bd63b40a362dc45d6223b7e Mon Sep 17 00:00:00 2001 From: Lez Date: Tue, 18 Nov 2025 08:49:23 +0100 Subject: [PATCH 330/401] Don't emit hello event if no events were received from stdin When running `nak req ... relay.one | nak event relay.two`, if the first req doesn't return any events, the second nak should not publish a "hello from nostr army knife" note to the second relay as it is clearly not the intention. `nak event relay.two` behavior is unchanged, it will publish the hello. --- helpers.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helpers.go b/helpers.go index 68e3c72..c59aa2d 100644 --- a/helpers.go +++ b/helpers.go @@ -75,7 +75,7 @@ func getJsonsOrBlank() iter.Seq[string] { return true }) - if !hasStdin { + if !hasStdin && !isPiped() { yield("{}") } From 5d7240b11231c9422c31d34fe17be9cfc147235e Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Mon, 17 Nov 2025 20:18:51 -0300 Subject: [PATCH 331/401] git betterments with remote and branch determination, force-push and fast-forward check. --- git.go | 257 ++++++++++++++++++++++++++++++++++----------------------- go.mod | 1 + go.sum | 6 +- 3 files changed, 159 insertions(+), 105 deletions(-) diff --git a/git.go b/git.go index e6fe306..a6501a3 100644 --- a/git.go +++ b/git.go @@ -212,25 +212,8 @@ var gitInit = &cli.Command{ ownerNpub := nip19.EncodeNpub(pk) // check existing git remotes - cmd = exec.Command("git", "remote", "-v") - output, err := cmd.Output() + nostrRemote, _, _, err := getGitNostrRemote(c) if err != nil { - return fmt.Errorf("failed to get git remotes: %w", err) - } - - remotes := strings.Split(strings.TrimSpace(string(output)), "\n") - var nostrRemote string - for _, remote := range remotes { - if strings.Contains(remote, "nostr://") { - parts := strings.Fields(remote) - if len(parts) >= 2 { - nostrRemote = parts[1] - break - } - } - } - - if nostrRemote == "" { remoteURL := fmt.Sprintf("nostr://%s/%s/%s", ownerNpub, config.GraspServers[0], config.Identifier) cmd = exec.Command("git", "remote", "add", "origin", remoteURL) if err := cmd.Run(); err != nil { @@ -400,37 +383,14 @@ func promptForConfig(config *Nip34Config) error { return nil } -func gitSanityCheck(localConfig Nip34Config, nostrRemote string) (nostr.PubKey, error) { - urlParts := strings.TrimPrefix(nostrRemote, "nostr://") - parts := strings.Split(urlParts, "/") - if len(parts) != 3 { - return nostr.ZeroPK, fmt.Errorf("invalid nostr URL format, expected nostr:////, got: %s", nostrRemote) - } - - remoteNpub := parts[0] - remoteHostname := parts[1] - remoteIdentifier := parts[2] - - ownerPk, err := parsePubKey(localConfig.Owner) - if err != nil { - return nostr.ZeroPK, fmt.Errorf("invalid owner public key: %w", err) - } - if nip19.EncodeNpub(ownerPk) != remoteNpub { - return nostr.ZeroPK, fmt.Errorf("owner in nip34.json does not match git remote npub") - } - if remoteIdentifier != localConfig.Identifier { - return nostr.ZeroPK, fmt.Errorf("git remote identifier '%s' differs from nip34.json identifier '%s'", remoteIdentifier, localConfig.Identifier) - } - if !slices.Contains(localConfig.GraspServers, remoteHostname) { - return nostr.ZeroPK, fmt.Errorf("git remote relay '%s' not in grasp servers %v", remoteHostname, localConfig.GraspServers) - } - return ownerPk, nil -} - var gitPush = &cli.Command{ Name: "push", Usage: "push git changes", - Flags: defaultKeyFlags, + Flags: append(defaultKeyFlags, &cli.BoolFlag{ + Name: "force", + Aliases: []string{"f"}, + Usage: "force push to git remotes", + }), Action: func(ctx context.Context, c *cli.Command) error { // setup signer kr, _, err := gatherKeyerFromArguments(ctx, c) @@ -455,26 +415,9 @@ var gitPush = &cli.Command{ } // get git remotes - cmd := exec.Command("git", "remote", "-v") - output, err := cmd.Output() + nostrRemote, localBranch, remoteBranch, err := getGitNostrRemote(c) if err != nil { - return fmt.Errorf("failed to get git remotes: %w", err) - } - - remotes := strings.Split(strings.TrimSpace(string(output)), "\n") - var nostrRemote string - for _, remote := range remotes { - if strings.Contains(remote, "nostr://") { - parts := strings.Fields(remote) - if len(parts) >= 2 { - nostrRemote = parts[1] - break - } - } - } - - if nostrRemote == "" { - return fmt.Errorf("no nostr:// remote found") + return err } // parse the URL: nostr://// @@ -521,20 +464,14 @@ var gitPush = &cli.Command{ log("found state event: %s\n", state.Event.ID) } - // get current branch and commit - res, err := exec.Command("git", "rev-parse", "--abbrev-ref", "HEAD").Output() + // get commit for the local branch + res, err := exec.Command("git", "rev-parse", localBranch).Output() if err != nil { - return fmt.Errorf("failed to get current branch: %w", err) - } - currentBranch := strings.TrimSpace(string(res)) - - res, err = exec.Command("git", "rev-parse", "HEAD").Output() - if err != nil { - return fmt.Errorf("failed to get current commit: %w", err) + return fmt.Errorf("failed to get commit for branch %s: %w", localBranch, err) } currentCommit := strings.TrimSpace(string(res)) - log("current branch: %s, commit: %s\n", currentBranch, currentCommit) + log("pushing branch %s to remote branch %s, commit: %s\n", localBranch, remoteBranch, currentCommit) // create a new state if we didn't find any if state.Event.ID == nostr.ZeroID { @@ -546,13 +483,22 @@ var gitPush = &cli.Command{ } // update the branch - state.Branches[currentBranch] = currentCommit - log("> setting branch %s to commit %s\n", currentBranch, currentCommit) + if !c.Bool("force") { + if prevCommit, exists := state.Branches[remoteBranch]; exists { + // check if prevCommit is an ancestor of currentCommit (fast-forward check) + cmd := exec.Command("git", "merge-base", "--is-ancestor", prevCommit, currentCommit) + if err := cmd.Run(); err != nil { + return fmt.Errorf("non-fast-forward push not allowed, use --force to override") + } + } + } + state.Branches[remoteBranch] = currentCommit + log("> setting branch %s to commit %s\n", remoteBranch, currentCommit) - // set the HEAD to the current branch if none is set + // set the HEAD to the local branch if none is set if state.HEAD == "" { - state.HEAD = currentBranch - log("> setting HEAD to branch %s\n", currentBranch) + state.HEAD = remoteBranch + log("> setting HEAD to branch %s\n", remoteBranch) } // create and sign the new state event @@ -573,13 +519,21 @@ var gitPush = &cli.Command{ // push to git clone URLs for _, cloneURL := range repo.Clone { - log("> pushing to: %s\n", cloneURL) - cmd := exec.Command("git", "push", cloneURL, fmt.Sprintf("refs/heads/%s:refs/heads/%s", currentBranch, currentBranch)) + log("> pushing to: %s\n", color.CyanString(cloneURL)) + args := []string{"push"} + if c.Bool("force") { + args = append(args, "--force") + } + args = append(args, + cloneURL, + fmt.Sprintf("refs/heads/%s:refs/heads/%s", localBranch, remoteBranch), + ) + cmd := exec.Command("git", args...) output, err := cmd.CombinedOutput() if err != nil { - log("(!) failed to push to %s: %v\n%s\n", cloneURL, err, string(output)) + log("> failed to push to %s: %v\n%s\n", color.RedString(cloneURL), err, string(output)) } else { - log("> successfully pushed to %s\n", cloneURL) + log("> successfully pushed to %s\n", color.GreenString(cloneURL)) } } @@ -626,26 +580,9 @@ var gitAnnounce = &cli.Command{ } // get git remotes - cmd = exec.Command("git", "remote", "-v") - output, err := cmd.Output() + nostrRemote, _, _, err := getGitNostrRemote(c) if err != nil { - return fmt.Errorf("failed to get git remotes: %w", err) - } - - remotes := strings.Split(strings.TrimSpace(string(output)), "\n") - var nostrRemote string - for _, remote := range remotes { - if strings.Contains(remote, "nostr://") { - parts := strings.Fields(remote) - if len(parts) >= 2 { - nostrRemote = parts[1] - break - } - } - } - - if nostrRemote == "" { - return fmt.Errorf("no nostr:// remote found") + return err } ownerPk, err := gitSanityCheck(localConfig, nostrRemote) @@ -734,3 +671,117 @@ var gitAnnounce = &cli.Command{ return nil }, } + +func getGitNostrRemote(c *cli.Command) ( + remoteURL string, + localBranch string, + remoteBranch string, + err error, +) { + // remote + var remoteName string + var cmd *exec.Cmd + args := c.Args() + if args.Len() > 0 { + remoteName = args.Get(0) + } else { + // get current branch + cmd = exec.Command("git", "rev-parse", "--abbrev-ref", "HEAD") + output, err := cmd.Output() + if err != nil { + return "", "", "", fmt.Errorf("failed to get current branch: %w", err) + } + branch := strings.TrimSpace(string(output)) + // get remote for branch + cmd = exec.Command("git", "config", "--get", fmt.Sprintf("branch.%s.remote", branch)) + output, err = cmd.Output() + if err != nil { + remoteName = "origin" + } else { + remoteName = strings.TrimSpace(string(output)) + } + } + // get the URL + cmd = exec.Command("git", "remote", "get-url", remoteName) + output, err := cmd.Output() + if err != nil { + return "", "", "", fmt.Errorf("remote '%s' does not exist", remoteName) + } + remoteURL = strings.TrimSpace(string(output)) + if !strings.Contains(remoteURL, "nostr://") { + return "", "", "", fmt.Errorf("remote '%s' is not a nostr remote: %s", remoteName, remoteURL) + } + + // branch (local and remote) + if args.Len() > 1 { + branchSpec := args.Get(1) + if strings.Contains(branchSpec, ":") { + parts := strings.Split(branchSpec, ":") + if len(parts) == 2 { + localBranch = parts[0] + remoteBranch = parts[1] + } else { + return "", "", "", fmt.Errorf("invalid branch spec: %s", branchSpec) + } + } else { + localBranch = branchSpec + } + } else { + // get current branch + cmd = exec.Command("git", "rev-parse", "--abbrev-ref", "HEAD") + output, err := cmd.Output() + if err != nil { + return "", "", "", fmt.Errorf("failed to get current branch: %w", err) + } + localBranch = strings.TrimSpace(string(output)) + } + + // get the upstream branch from git config + cmd = exec.Command("git", "config", "--get", fmt.Sprintf("branch.%s.merge", localBranch)) + output, err = cmd.Output() + if err == nil { + // parse refs/heads/ to get just the branch name + mergeRef := strings.TrimSpace(string(output)) + if strings.HasPrefix(mergeRef, "refs/heads/") { + remoteBranch = strings.TrimPrefix(mergeRef, "refs/heads/") + } else { + // fallback if it's not in expected format + remoteBranch = localBranch + } + } else { + // no upstream configured, assume same branch name + remoteBranch = localBranch + } + + return remoteURL, localBranch, remoteBranch, nil +} + +func gitSanityCheck( + localConfig Nip34Config, + nostrRemote string, +) (nostr.PubKey, error) { + urlParts := strings.TrimPrefix(nostrRemote, "nostr://") + parts := strings.Split(urlParts, "/") + if len(parts) != 3 { + return nostr.ZeroPK, fmt.Errorf("invalid nostr URL format, expected nostr:////, got: %s", nostrRemote) + } + + remoteNpub := parts[0] + remoteHostname := parts[1] + remoteIdentifier := parts[2] + + ownerPk, err := parsePubKey(localConfig.Owner) + if err != nil { + return nostr.ZeroPK, fmt.Errorf("invalid owner public key: %w", err) + } + if nip19.EncodeNpub(ownerPk) != remoteNpub { + return nostr.ZeroPK, fmt.Errorf("owner in nip34.json does not match git remote npub") + } + if remoteIdentifier != localConfig.Identifier { + return nostr.ZeroPK, fmt.Errorf("git remote identifier '%s' differs from nip34.json identifier '%s'", remoteIdentifier, localConfig.Identifier) + } + if !slices.Contains(localConfig.GraspServers, remoteHostname) { + return nostr.ZeroPK, fmt.Errorf("git remote relay '%s' not in grasp servers %v", remoteHostname, localConfig.GraspServers) + } + return ownerPk, nil +} diff --git a/go.mod b/go.mod index 2452abf..c077c35 100644 --- a/go.mod +++ b/go.mod @@ -30,6 +30,7 @@ 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/bluekeyes/go-gitdiff v0.7.1 // indirect github.com/btcsuite/btcd v0.24.2 // indirect github.com/btcsuite/btcd/btcutil v1.1.5 // indirect github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect diff --git a/go.sum b/go.sum index 0b64989..c42b1cc 100644 --- a/go.sum +++ b/go.sum @@ -13,6 +13,8 @@ github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7X github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= +github.com/bluekeyes/go-gitdiff v0.7.1 h1:graP4ElLRshr8ecu0UtqfNTCHrtSyZd3DABQm/DWesQ= +github.com/bluekeyes/go-gitdiff v0.7.1/go.mod h1:QpfYYO1E0fTVHVZAZKiRjtSGY9823iCdvGXBcEzHGbM= github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M= github.com/btcsuite/btcd v0.23.5-0.20231215221805-96c9fd8078fd/go.mod h1:nm3Bko6zh6bWP60UxwoT5LzdGJsQJaPo6HjduXq9p6A= @@ -92,8 +94,8 @@ github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= -github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= From bec821d3c0ddd47202c7de4c2e09944ae479266b Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 18 Nov 2025 11:57:15 -0300 Subject: [PATCH 332/401] build with latest nostrlib. we had to do this git thing just so we could publish nostrlib to grasp servers and make it downloadable as a dependency, now finally. --- go.mod | 6 +++--- go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index c077c35..fd9f677 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.24.1 require ( fiatjaf.com/lib v0.3.1 - fiatjaf.com/nostr v0.0.0-20251112024900-1c43f0d66643 + fiatjaf.com/nostr v0.0.0-20251117111008-078e9b4cc257 github.com/bep/debounce v1.2.1 github.com/btcsuite/btcd/btcec/v2 v2.3.6 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e @@ -21,8 +21,8 @@ require ( github.com/mdp/qrterminal/v3 v3.2.1 github.com/stretchr/testify v1.10.0 github.com/urfave/cli/v3 v3.0.0-beta1 - golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 - golang.org/x/sync v0.17.0 + golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 + golang.org/x/sync v0.18.0 golang.org/x/term v0.32.0 ) diff --git a/go.sum b/go.sum index c42b1cc..83882ae 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ fiatjaf.com/lib v0.3.1 h1:/oFQwNtFRfV+ukmOCxfBEAuayoLwXp4wu2/fz5iHpwA= fiatjaf.com/lib v0.3.1/go.mod h1:Ycqq3+mJ9jAWu7XjbQI1cVr+OFgnHn79dQR5oTII47g= -fiatjaf.com/nostr v0.0.0-20251112024900-1c43f0d66643 h1:GcoAN1FQV+rayCIklvj+mIB/ZR3Oni98C3bS/M+vzts= -fiatjaf.com/nostr v0.0.0-20251112024900-1c43f0d66643/go.mod h1:Nq86Jjsd0OmsOEImUg0iCcLuqM5B67Nj2eu/2dP74Ss= +fiatjaf.com/nostr v0.0.0-20251117111008-078e9b4cc257 h1:u8ah+Cc+5bXZ3qnBf8MRtzyRk6VAdhA0EYTY3+hVejs= +fiatjaf.com/nostr v0.0.0-20251117111008-078e9b4cc257/go.mod h1:pCbBk3hfs5x0+ND8k25mq9e50LEmQpAYMdTUe1M1Rt0= 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= @@ -205,8 +205,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= -golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= -golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= +golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 h1:zfMcR1Cs4KNuomFFgGefv5N0czO2XZpUbxGUy8i8ug0= +golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0= golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -215,8 +215,8 @@ golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= -golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 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= From ae3cb7c10864ef9ac6dacbbd8ed8a06e8c9f3d16 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 18 Nov 2025 16:05:04 -0300 Subject: [PATCH 333/401] serve: blossom and grasp support. --- serve.go | 105 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 100 insertions(+), 5 deletions(-) diff --git a/serve.go b/serve.go index 4180d81..235366c 100644 --- a/serve.go +++ b/serve.go @@ -2,17 +2,24 @@ package main import ( "bufio" + "bytes" "context" "fmt" + "io" + "net/url" "os" + "path/filepath" "sync/atomic" "time" "fiatjaf.com/nostr" "fiatjaf.com/nostr/eventstore/slicestore" "fiatjaf.com/nostr/khatru" + "fiatjaf.com/nostr/khatru/blossom" + "fiatjaf.com/nostr/khatru/grasp" "github.com/bep/debounce" "github.com/fatih/color" + "github.com/puzpuzpuz/xsync/v3" "github.com/urfave/cli/v3" ) @@ -36,10 +43,21 @@ var serve = &cli.Command{ Usage: "file containing the initial batch of events that will be served by the relay as newline-separated JSON (jsonl)", DefaultText: "the relay will start empty", }, + &cli.BoolFlag{ + Name: "grasp", + Usage: "enable grasp server", + }, + &cli.BoolFlag{ + Name: "blossom", + Usage: "enable blossom server", + }, }, Action: func(ctx context.Context, c *cli.Command) error { db := &slicestore.SliceStore{} + var blobStore *xsync.MapOf[string, []byte] + var repoDir string + var scanner *bufio.Scanner if path := c.String("events"); path != "" { f, err := os.Open(path) @@ -71,7 +89,7 @@ var serve = &cli.Command{ rl.Info.Software = "https://github.com/fiatjaf/nak" rl.Info.Version = version - rl.UseEventstore(db, 1_000_000) + rl.UseEventstore(db, 500) started := make(chan bool) exited := make(chan error) @@ -79,13 +97,59 @@ var serve = &cli.Command{ hostname := c.String("hostname") port := int(c.Uint("port")) + var printStatus func() + + if c.Bool("blossom") { + bs := blossom.New(rl, fmt.Sprintf("http://%s:%d", hostname, port)) + bs.Store = blossom.NewMemoryBlobIndex() + + blobStore = xsync.NewMapOf[string, []byte]() + bs.StoreBlob = func(ctx context.Context, sha256 string, ext string, body []byte) error { + blobStore.Store(sha256+ext, body) + log(" got %s %s\n", color.GreenString("blob stored"), sha256+ext) + printStatus() + return nil + } + bs.LoadBlob = func(ctx context.Context, sha256 string, ext string) (io.ReadSeeker, *url.URL, error) { + if body, ok := blobStore.Load(sha256 + ext); ok { + log(" got %s %s\n", color.BlueString("blob downloaded"), sha256+ext) + printStatus() + return bytes.NewReader(body), nil, nil + } + return nil, nil, nil + } + bs.DeleteBlob = func(ctx context.Context, sha256 string, ext string) error { + blobStore.Delete(sha256 + ext) + log(" got %s %s\n", color.RedString("blob deleted"), sha256+ext) + printStatus() + return nil + } + } + + if c.Bool("grasp") { + var err error + repoDir, err = os.MkdirTemp("", "nak-serve-grasp-repos-") + if err != nil { + return fmt.Errorf("failed to create grasp repos directory: %w", err) + } + g := grasp.New(rl, repoDir) + g.OnRead = func(ctx context.Context, pubkey nostr.PubKey, repo string) (reject bool, reason string) { + log(" got %s %s %s\n", color.CyanString("git read"), pubkey.Hex(), repo) + printStatus() + return false, "" + } + g.OnWrite = func(ctx context.Context, pubkey nostr.PubKey, repo string) (reject bool, reason string) { + log(" got %s %s %s\n", color.YellowString("git write"), pubkey.Hex(), repo) + printStatus() + return false, "" + } + } + go func() { err := rl.Start(hostname, port, started) exited <- err }() - var printStatus func() - // relay logging rl.OnRequest = func(ctx context.Context, filter nostr.Filter) (reject bool, msg string) { log(" got %s %v\n", color.HiYellowString("request"), colors.italic(filter)) @@ -123,9 +187,36 @@ var serve = &cli.Command{ } subs := rl.GetListeningFilters() - log(" %s events: %s, connections: %s, subscriptions: %s\n", + blossomMsg := "" + if c.Bool("blossom") { + blobsStored := blobStore.Size() + blossomMsg = fmt.Sprintf("blobs: %s, ", + color.HiMagentaString("%d", blobsStored), + ) + } + + graspMsg := "" + if c.Bool("grasp") { + gitAnnounced := 0 + gitStored := 0 + for evt := range db.QueryEvents(nostr.Filter{Kinds: []nostr.Kind{nostr.Kind(30617)}}, 500) { + gitAnnounced++ + identifier := evt.Tags.GetD() + if info, err := os.Stat(filepath.Join(repoDir, identifier)); err == nil && info.IsDir() { + gitStored++ + } + } + graspMsg = fmt.Sprintf("git announced: %s, git stored: %s, ", + color.HiMagentaString("%d", gitAnnounced), + color.HiMagentaString("%d", gitStored), + ) + } + + log(" %s events: %s, %s%sconnections: %s, subscriptions: %s\n", color.HiMagentaString("•"), color.HiMagentaString("%d", totalEvents), + blossomMsg, + graspMsg, color.HiMagentaString("%d", totalConnections.Load()), color.HiMagentaString("%d", len(subs)), ) @@ -133,7 +224,11 @@ var serve = &cli.Command{ } <-started - log("%s relay running at %s\n", color.HiRedString(">"), colors.boldf("ws://%s:%d", hostname, port)) + log("%s relay running at %s", color.HiRedString(">"), colors.boldf("ws://%s:%d", hostname, port)) + if c.Bool("grasp") { + log(" (grasp repos at %s)", repoDir) + } + log("\n") return <-exited }, From 51876f89c434490c5e0f08f7f77b436bf83a77a9 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 18 Nov 2025 23:06:12 -0300 Subject: [PATCH 334/401] git: nicer logs and fix announce to update only and all outdated relays. --- git.go | 63 +++++++++++++++++++++++++++++++++------------------------- go.mod | 3 ++- go.sum | 2 ++ 3 files changed, 40 insertions(+), 28 deletions(-) diff --git a/git.go b/git.go index a6501a3..4c8729e 100644 --- a/git.go +++ b/git.go @@ -461,7 +461,7 @@ var gitPush = &cli.Command{ } if state.Event.ID != nostr.ZeroID { - log("found state event: %s\n", state.Event.ID) + logverbose("found state event: %s\n", state.Event.ID) } // get commit for the local branch @@ -471,7 +471,7 @@ var gitPush = &cli.Command{ } currentCommit := strings.TrimSpace(string(res)) - log("pushing branch %s to remote branch %s, commit: %s\n", localBranch, remoteBranch, currentCommit) + logverbose("pushing branch %s to remote branch %s, commit: %s\n", localBranch, remoteBranch, currentCommit) // create a new state if we didn't find any if state.Event.ID == nostr.ZeroID { @@ -493,12 +493,12 @@ var gitPush = &cli.Command{ } } state.Branches[remoteBranch] = currentCommit - log("> setting branch %s to commit %s\n", remoteBranch, currentCommit) + log("- setting branch %s to commit %s\n", color.CyanString(remoteBranch), color.CyanString(currentCommit)) // set the HEAD to the local branch if none is set if state.HEAD == "" { state.HEAD = remoteBranch - log("> setting HEAD to branch %s\n", remoteBranch) + log("- setting HEAD to branch %s\n", color.CyanString(remoteBranch)) } // create and sign the new state event @@ -508,12 +508,12 @@ var gitPush = &cli.Command{ return fmt.Errorf("error signing state event: %w", err) } - log("> publishing updated repository state %s\n", newStateEvent.ID) + log("- publishing updated repository state to " + color.CyanString("%v", relays) + "\n") for res := range sys.Pool.PublishMany(ctx, relays, newStateEvent) { if res.Error != nil { - log("(!) error publishing event to relay %s: %v\n", res.RelayURL, res.Error) + log("! error publishing event to %s: %v\n", color.YellowString(res.RelayURL), res.Error) } else { - log("> published to relay %s\n", res.RelayURL) + log("> published to %s\n", color.GreenString(res.RelayURL)) } } @@ -625,9 +625,15 @@ var gitAnnounce = &cli.Command{ } } - // fetch repository announcement (30617) events - var repo nip34.Repository + // these are the relays where we'll publish the announcement to relays := append(sys.FetchOutboxRelays(ctx, ownerPk, 3), localConfig.GraspServers...) + for i := range relays { + relays[i] = nostr.NormalizeURL(relays[i]) + } + + // fetch repository announcement (30617) events + oks := make([]bool, len(relays)) + var repo nip34.Repository results := sys.Pool.FetchMany(ctx, relays, nostr.Filter{ Kinds: []nostr.Kind{30617}, Tags: nostr.TagMap{ @@ -635,37 +641,40 @@ var gitAnnounce = &cli.Command{ }, Limit: 1, }, nostr.SubscriptionOptions{ - Label: "nak-git-announce", + Label: "nak-git-announce", + CheckDuplicate: func(id nostr.ID, relay string) bool { return false }, // get the same event from multiple relays }) for ie := range results { repo = nip34.ParseRepository(ie.Event) + + // check if this is ok or the announcement in this relay needs to be updated + if repositoriesEqual(repo, localRepo) { + relayIdx := slices.Index(relays, ie.Relay.URL) + oks[relayIdx] = true + } } // publish repository announcement if needed - var needsAnnouncement bool - if repo.Event.ID == nostr.ZeroID { - log("no existing repository announcement found, will create one\n") - needsAnnouncement = true - } else if !repositoriesEqual(repo, localRepo) { - log("local repository config differs from published announcement, will update\n") - needsAnnouncement = true - } - if needsAnnouncement { + if slices.Contains(oks, false) { announcementEvent := localRepo.ToEvent() if err := kr.SignEvent(ctx, &announcementEvent); err != nil { return fmt.Errorf("failed to sign announcement event: %w", err) } - log("> publishing repository announcement %s\n", announcementEvent.ID) - for res := range sys.Pool.PublishMany(ctx, relays, announcementEvent) { - if res.Error != nil { - log("(!) error publishing announcement to relay %s: %v\n", res.RelayURL, res.Error) - } else { - log("> published announcement to relay %s\n", res.RelayURL) + targets := make([]string, 0, len(oks)) + for i, ok := range oks { + if !ok { + targets = append(targets, relays[i]) + } + } + log("- publishing repository announcement to " + color.CyanString("%v", targets) + "\n") + for res := range sys.Pool.PublishMany(ctx, targets, announcementEvent) { + if res.Error != nil { + log("! error publishing announcement to relay %s: %v\n", color.YellowString(res.RelayURL), res.Error) + } else { + log("> published announcement to relay %s\n", color.GreenString(res.RelayURL)) } } - } else { - log("repository announcement is up to date\n") } return nil diff --git a/go.mod b/go.mod index fd9f677..ada5a43 100644 --- a/go.mod +++ b/go.mod @@ -19,6 +19,7 @@ require ( github.com/mattn/go-isatty v0.0.20 github.com/mattn/go-tty v0.0.7 github.com/mdp/qrterminal/v3 v3.2.1 + github.com/puzpuzpuz/xsync/v3 v3.5.1 github.com/stretchr/testify v1.10.0 github.com/urfave/cli/v3 v3.0.0-beta1 golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 @@ -46,6 +47,7 @@ require ( github.com/elnosh/gonuts v0.4.2 // indirect github.com/fasthttp/websocket v1.5.12 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/go-git/go-git/v5 v5.16.3 // indirect github.com/google/uuid v1.6.0 // indirect github.com/hablullah/go-hijri v1.0.2 // indirect github.com/hablullah/go-juliandays v1.0.0 // indirect @@ -58,7 +60,6 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect github.com/rs/cors v1.11.1 // indirect github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 // indirect github.com/tetratelabs/wazero v1.8.0 // indirect diff --git a/go.sum b/go.sum index 83882ae..d5c5862 100644 --- a/go.sum +++ b/go.sum @@ -83,6 +83,8 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo 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/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/go-git/go-git/v5 v5.16.3 h1:Z8BtvxZ09bYm/yYNgPKCzgWtaRqDTgIKRgIRHBfU6Z8= +github.com/go-git/go-git/v5 v5.16.3/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= From 26f9b33d5316ca651133b6acc358c84f56de1d8a Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Thu, 20 Nov 2025 23:39:24 -0300 Subject: [PATCH 335/401] git clone --- git.go | 257 +++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 252 insertions(+), 5 deletions(-) diff --git a/git.go b/git.go index 4c8729e..ad044df 100644 --- a/git.go +++ b/git.go @@ -3,6 +3,7 @@ package main import ( "context" "fmt" + "net/url" "os" "os/exec" "path/filepath" @@ -33,6 +34,7 @@ var git = &cli.Command{ Usage: "git-related operations", Commands: []*cli.Command{ gitInit, + gitClone, gitPush, gitPull, gitFetch, @@ -236,7 +238,7 @@ var gitInit = &cli.Command{ if repoNpub != ownerNpub { return fmt.Errorf("git remote npub '%s' does not match owner '%s'", repoNpub, ownerNpub) } - if !slices.Contains(config.GraspServers, relayHostname) { + if !slices.Contains(config.GraspServers, nostr.NormalizeURL(relayHostname)) { return fmt.Errorf("git remote relay '%s' not in grasp servers %v", relayHostname, config.GraspServers) } if identifier != config.Identifier { @@ -383,6 +385,188 @@ func promptForConfig(config *Nip34Config) error { return nil } +var gitClone = &cli.Command{ + Name: "clone", + Usage: "clone a NIP-34 repository from a nostr:// URI", + ArgsUsage: "nostr://// [directory]", + Action: func(ctx context.Context, c *cli.Command) error { + args := c.Args() + if args.Len() == 0 { + return fmt.Errorf("missing repository URI (expected nostr:////)") + } + + repoURI := args.Get(0) + if !strings.HasPrefix(repoURI, "nostr://") { + return fmt.Errorf("invalid nostr URI: %s", repoURI) + } + + uriParts := strings.Split(strings.TrimPrefix(repoURI, "nostr://"), "/") + if len(uriParts) != 3 { + return fmt.Errorf("invalid nostr URI format, expected nostr:////, got: %s", repoURI) + } + + ownerNpub := uriParts[0] + relayHost := uriParts[1] + identifier := uriParts[2] + + prefix, decoded, err := nip19.Decode(ownerNpub) + if err != nil || prefix != "npub" { + return fmt.Errorf("invalid owner npub in URI: %w", err) + } + + ownerPk := decoded.(nostr.PubKey) + primaryRelay := nostr.NormalizeURL(relayHost) + + // fetch repository announcement (30617) + relays := appendUnique([]string{primaryRelay}, sys.FetchOutboxRelays(ctx, ownerPk, 3)...) + var repo nip34.Repository + for ie := range sys.Pool.FetchMany(ctx, relays, nostr.Filter{ + Kinds: []nostr.Kind{30617}, + Authors: []nostr.PubKey{ownerPk}, + Tags: nostr.TagMap{ + "d": []string{identifier}, + }, + Limit: 2, + }, nostr.SubscriptionOptions{Label: "nak-git-clone-meta"}) { + if ie.Event.CreatedAt > repo.CreatedAt { + repo = nip34.ParseRepository(ie.Event) + } + } + if repo.Event.ID == nostr.ZeroID { + return fmt.Errorf("no repository announcement (kind 30617) found for %s", identifier) + } + + // fetch repository state (30618) + var state nip34.RepositoryState + var stateFound bool + var stateErr error + for ie := range sys.Pool.FetchMany(ctx, repo.Relays, nostr.Filter{ + Kinds: []nostr.Kind{30618}, + Authors: []nostr.PubKey{ownerPk}, + Tags: nostr.TagMap{ + "d": []string{identifier}, + }, + Limit: 2, + }, nostr.SubscriptionOptions{Label: "nak-git-clone-meta"}) { + if ie.Event.CreatedAt > state.CreatedAt { + state = nip34.ParseRepositoryState(ie.Event) + stateFound = true + + if state.HEAD == "" { + stateErr = fmt.Errorf("state is missing HEAD") + continue + } + if _, ok := state.Branches[state.HEAD]; !ok { + stateErr = fmt.Errorf("state is missing commit for HEAD branch '%s'", state.HEAD) + continue + } + + stateErr = nil + } + } + if !stateFound { + return fmt.Errorf("no repository state (kind 30618) found") + } + if stateErr != nil { + return stateErr + } + + // determine target directory + targetDir := "" + if args.Len() >= 2 { + targetDir = args.Get(1) + } else { + targetDir = repo.ID + } + if targetDir == "" { + targetDir = identifier + } + + // if targetDir exists and is non-empty, bail + if fi, err := os.Stat(targetDir); err == nil && fi.IsDir() { + entries, err := os.ReadDir(targetDir) + if err == nil && len(entries) > 0 { + return fmt.Errorf("target directory '%s' already exists and is not empty", targetDir) + } + } + + // decide which clone URL to use + if len(repo.Clone) == 0 { + return fmt.Errorf("no clone urls found for repository") + } + + cloned := false + for _, url := range repo.Clone { + log("- cloning %s... ", color.CyanString(url)) + if err := tryCloneAndCheckState(ctx, url, targetDir, &state); err != nil { + log(color.YellowString("failed: %v\n", err)) + continue + } + log("%s\n", color.GreenString("ok")) + cloned = true + break + } + + if !cloned { + return fmt.Errorf("failed to clone") + } + + // write nip34.json inside cloned directory + // normalize relay URLs for consistency + normalizedRelays := make([]string, 0, len(repo.Relays)) + for _, r := range repo.Relays { + normalizedRelays = append(normalizedRelays, nostr.NormalizeURL(r)) + } + + cfg := Nip34Config{ + Identifier: repo.ID, + Name: repo.Name, + Description: repo.Description, + Web: repo.Web, + Owner: nip19.EncodeNpub(repo.Event.PubKey), + GraspServers: normalizedRelays, + EarliestUniqueCommit: repo.EarliestUniqueCommitID, + Maintainers: make([]string, 0, len(repo.Maintainers)), + } + for _, m := range repo.Maintainers { + cfg.Maintainers = append(cfg.Maintainers, nip19.EncodeNpub(m)) + } + + data, err := json.MarshalIndent(cfg, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal nip34.json: %w", err) + } + + configPath := filepath.Join(targetDir, "nip34.json") + if err := os.WriteFile(configPath, data, 0644); err != nil { + return fmt.Errorf("failed to write %s: %w", configPath, err) + } + + // add nip34.json to .git/info/exclude in cloned repo + gitDir := filepath.Join(targetDir, ".git") + if st, err := os.Stat(gitDir); err == nil && st.IsDir() { + excludePath := filepath.Join(gitDir, "info", "exclude") + excludeContent, err := os.ReadFile(excludePath) + if err != nil { + excludeContent = []byte("") + } + if !strings.Contains(string(excludeContent), "nip34.json") { + newContent := string(excludeContent) + if len(newContent) > 0 && !strings.HasSuffix(newContent, "\n") { + newContent += "\n" + } + newContent += "nip34.json\n" + if err := os.WriteFile(excludePath, []byte(newContent), 0644); err != nil { + log(color.YellowString("failed to add nip34.json to %s: %v\n", excludePath, err)) + } + } + } + + log("cloned into %s\n", color.GreenString(targetDir)) + return nil + }, +} + var gitPush = &cli.Command{ Name: "push", Usage: "push git changes", @@ -445,9 +629,13 @@ var gitPush = &cli.Command{ }) for ie := range results { if ie.Event.Kind == 30617 { - repo = nip34.ParseRepository(ie.Event) + if ie.Event.CreatedAt > repo.CreatedAt { + repo = nip34.ParseRepository(ie.Event) + } } else if ie.Event.Kind == 30618 { - state = nip34.ParseRepositoryState(ie.Event) + if ie.Event.CreatedAt > state.CreatedAt { + state = nip34.ParseRepositoryState(ie.Event) + } } } @@ -457,7 +645,7 @@ var gitPush = &cli.Command{ // check if signer matches owner or is in maintainers if currentPk != ownerPk && !slices.Contains(repo.Maintainers, currentPk) { - return fmt.Errorf("current user is not allowed to push") + return fmt.Errorf("current user '%s' is not allowed to push", nip19.EncodeNpub(currentPk)) } if state.Event.ID != nostr.ZeroID { @@ -789,8 +977,67 @@ func gitSanityCheck( if remoteIdentifier != localConfig.Identifier { return nostr.ZeroPK, fmt.Errorf("git remote identifier '%s' differs from nip34.json identifier '%s'", remoteIdentifier, localConfig.Identifier) } - if !slices.Contains(localConfig.GraspServers, remoteHostname) { + if !slices.Contains(localConfig.GraspServers, nostr.NormalizeURL(remoteHostname)) { return nostr.ZeroPK, fmt.Errorf("git remote relay '%s' not in grasp servers %v", remoteHostname, localConfig.GraspServers) } return ownerPk, nil } + +func tryCloneAndCheckState(ctx context.Context, cloneURL, targetDir string, state *nip34.RepositoryState) (err error) { + // if we get here we know we were the ones who created the target directory, so we're safe to remove it + defer func() { + if err != nil { + if err := os.RemoveAll(targetDir); err != nil { + log("failed to remove '%s' when handling error from clone: %s", targetDir, err) + } + } + }() + + cmd := exec.CommandContext(ctx, "git", "clone", cloneURL, targetDir) + output, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("git clone failed: %v: %s", err, strings.TrimSpace(string(output))) + } + + // if we don't have any state information, we can't verify anything + if state == nil || state.Event.ID == nostr.ZeroID { + return nil + } + + // check that the HEAD branch matches the state HEAD + cmd = exec.Command("git", "-C", targetDir, "rev-parse", "--abbrev-ref", "HEAD") + headOut, err := cmd.Output() + if err != nil { + return fmt.Errorf("failed to read HEAD") + } + currentBranch := strings.TrimSpace(string(headOut)) + if currentBranch != state.HEAD { + return fmt.Errorf("received HEAD '%s' isn't the expected '%s'", currentBranch, state.HEAD) + } + + // verify the HEAD branch only as it's the only one we have + expectedCommit := state.Branches[state.HEAD] // we've tested before if state has this + cmd = exec.Command("git", "-C", targetDir, "rev-parse", state.HEAD) + actualOut, err := cmd.Output() + if err != nil { + return fmt.Errorf("failed to check commit for '%s': %s", state.HEAD, err) + } + actualCommit := strings.TrimSpace(string(actualOut)) + if actualCommit != expectedCommit { + return fmt.Errorf("branch %s is at %s, expected %s", state.HEAD, actualCommit, expectedCommit) + } + + // set nostr remote + parsed, _ := url.Parse(cloneURL) + repoURI := fmt.Sprintf("nostr://%s/%s/%s", + nip19.EncodeNpub(state.PubKey), + parsed.Host, + state.ID, + ) + cmd = exec.Command("git", "-C", targetDir, "remote", "set-url", "origin", repoURI) + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed to add git remote: %v\n", err) + } + + return nil +} From afa31a58fc9870b4318f09a4bed98834a125e01b Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Fri, 21 Nov 2025 11:24:01 -0300 Subject: [PATCH 336/401] serve: --negentropy --- serve.go | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/serve.go b/serve.go index 235366c..3a8147c 100644 --- a/serve.go +++ b/serve.go @@ -43,6 +43,10 @@ var serve = &cli.Command{ Usage: "file containing the initial batch of events that will be served by the relay as newline-separated JSON (jsonl)", DefaultText: "the relay will start empty", }, + &cli.BoolFlag{ + Name: "negentropy", + Usage: "enable negentropy syncing", + }, &cli.BoolFlag{ Name: "grasp", Usage: "enable grasp server", @@ -91,6 +95,10 @@ var serve = &cli.Command{ rl.UseEventstore(db, 500) + if c.Bool("negentropy") { + rl.Negentropy = true + } + started := make(chan bool) exited := make(chan error) @@ -152,7 +160,12 @@ var serve = &cli.Command{ // relay logging rl.OnRequest = func(ctx context.Context, filter nostr.Filter) (reject bool, msg string) { - log(" got %s %v\n", color.HiYellowString("request"), colors.italic(filter)) + negentropy := "" + if khatru.IsNegentropySession(ctx) { + negentropy = color.HiBlueString("negentropy ") + } + + log(" got %s%s %v\n", negentropy, color.HiYellowString("request"), colors.italic(filter)) printStatus() return false, "" } From a4f53021f081efd3b3fabb20da5fa7aedf3bb6e3 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Fri, 21 Nov 2025 11:24:14 -0300 Subject: [PATCH 337/401] add examples for newer use cases. --- README.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/README.md b/README.md index 7cade1f..8589c64 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,11 @@ nevent1qqs94ee3h0rhz8mc2y76zjf8cjxvw9p6j8nv45zktlwy6uacjea86kgpzfmhxue69uhkummnw } ``` +### fetch all events except those that are present in a given line-delimited json file (negentropy sync) +```shell +~> nak req --only-missing ./events.jsonl -k 30617 pyramid.fiatjaf.com +``` + ### fetch an event using relay and author hints automatically from a nevent1 code, pretty-print it ```shell nak fetch nevent1qqs2e3k48vtrkzjm8vvyzcmsmkf58unrxtq2k4h5yspay6vhcqm4wqcpz9mhxue69uhkummnw3ezuamfdejj7q3ql2vyh47mk2p0qlsku7hg0vn29faehy9hy34ygaclpn66ukqp3afqxpqqqqqqz7ttjyq | jq @@ -223,6 +228,21 @@ or give it a named profile: • events stored: 4, subscriptions opened: 1 ``` +### enable negentropy (nip77) support in your development relay +```shell +~> nak serve --negentropy +``` + +### run a grasp server (with a relay) +```shell +~> nak serve --grasp +``` + +### run a blossom server (with a relay) +```shell +~> nak serve --blossom +``` + ### make an event with a PoW target ```shell ~> nak event -c 'hello getwired.app and labour.fiatjaf.com' --pow 24 @@ -309,3 +329,8 @@ ffmpeg -f alsa -i default -f webm -t 00:00:03 pipe:1 | nak blossom --server blos ```shell ~> cat all.jsonl | nak filter -k 1111 -a 117673e191b10fe1aedf1736ee74de4cffd4c132ca701960b70a5abad5870faa > filtered.jsonl ``` + +### use negentropy (nip77) to only fetch the ids for a given query +```shell +~> nak req --ids-only -k 1111 -a npub1vyrx2prp0mne8pczrcvv38ahn5wahsl8hlceeu3f3aqyvmu8zh5s7kfy55 relay.damus.io +``` From 77afab780b3550c2b140dda58374093166652335 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Fri, 21 Nov 2025 11:24:37 -0300 Subject: [PATCH 338/401] git: fetch and pull (wip). --- git.go | 291 ++++++++++++++++++++++++++++++++++----------------------- 1 file changed, 175 insertions(+), 116 deletions(-) diff --git a/git.go b/git.go index ad044df..0c7f9be 100644 --- a/git.go +++ b/git.go @@ -395,81 +395,7 @@ var gitClone = &cli.Command{ return fmt.Errorf("missing repository URI (expected nostr:////)") } - repoURI := args.Get(0) - if !strings.HasPrefix(repoURI, "nostr://") { - return fmt.Errorf("invalid nostr URI: %s", repoURI) - } - - uriParts := strings.Split(strings.TrimPrefix(repoURI, "nostr://"), "/") - if len(uriParts) != 3 { - return fmt.Errorf("invalid nostr URI format, expected nostr:////, got: %s", repoURI) - } - - ownerNpub := uriParts[0] - relayHost := uriParts[1] - identifier := uriParts[2] - - prefix, decoded, err := nip19.Decode(ownerNpub) - if err != nil || prefix != "npub" { - return fmt.Errorf("invalid owner npub in URI: %w", err) - } - - ownerPk := decoded.(nostr.PubKey) - primaryRelay := nostr.NormalizeURL(relayHost) - - // fetch repository announcement (30617) - relays := appendUnique([]string{primaryRelay}, sys.FetchOutboxRelays(ctx, ownerPk, 3)...) - var repo nip34.Repository - for ie := range sys.Pool.FetchMany(ctx, relays, nostr.Filter{ - Kinds: []nostr.Kind{30617}, - Authors: []nostr.PubKey{ownerPk}, - Tags: nostr.TagMap{ - "d": []string{identifier}, - }, - Limit: 2, - }, nostr.SubscriptionOptions{Label: "nak-git-clone-meta"}) { - if ie.Event.CreatedAt > repo.CreatedAt { - repo = nip34.ParseRepository(ie.Event) - } - } - if repo.Event.ID == nostr.ZeroID { - return fmt.Errorf("no repository announcement (kind 30617) found for %s", identifier) - } - - // fetch repository state (30618) - var state nip34.RepositoryState - var stateFound bool - var stateErr error - for ie := range sys.Pool.FetchMany(ctx, repo.Relays, nostr.Filter{ - Kinds: []nostr.Kind{30618}, - Authors: []nostr.PubKey{ownerPk}, - Tags: nostr.TagMap{ - "d": []string{identifier}, - }, - Limit: 2, - }, nostr.SubscriptionOptions{Label: "nak-git-clone-meta"}) { - if ie.Event.CreatedAt > state.CreatedAt { - state = nip34.ParseRepositoryState(ie.Event) - stateFound = true - - if state.HEAD == "" { - stateErr = fmt.Errorf("state is missing HEAD") - continue - } - if _, ok := state.Branches[state.HEAD]; !ok { - stateErr = fmt.Errorf("state is missing commit for HEAD branch '%s'", state.HEAD) - continue - } - - stateErr = nil - } - } - if !stateFound { - return fmt.Errorf("no repository state (kind 30618) found") - } - if stateErr != nil { - return stateErr - } + repo, state, err := fetchRepositoryAndState(ctx, args.Get(0)) // determine target directory targetDir := "" @@ -479,7 +405,7 @@ var gitClone = &cli.Command{ targetDir = repo.ID } if targetDir == "" { - targetDir = identifier + targetDir = repo.ID } // if targetDir exists and is non-empty, bail @@ -604,47 +530,13 @@ var gitPush = &cli.Command{ return err } - // parse the URL: nostr://// - if !strings.HasPrefix(nostrRemote, "nostr://") { - return fmt.Errorf("invalid nostr remote URL: %s", nostrRemote) - } - - ownerPk, err := gitSanityCheck(localConfig, nostrRemote) + repo, state, err := fetchRepositoryAndState(ctx, nostrRemote) if err != nil { return err } - // fetch repository announcement (30617) and state (30618) events - var repo nip34.Repository - var state nip34.RepositoryState - relays := append(sys.FetchOutboxRelays(ctx, ownerPk, 3), localConfig.GraspServers...) - results := sys.Pool.FetchMany(ctx, relays, nostr.Filter{ - Kinds: []nostr.Kind{30617, 30618}, - Tags: nostr.TagMap{ - "d": []string{localConfig.Identifier}, - }, - Limit: 2, - }, nostr.SubscriptionOptions{ - Label: "nak-git-push", - }) - for ie := range results { - if ie.Event.Kind == 30617 { - if ie.Event.CreatedAt > repo.CreatedAt { - repo = nip34.ParseRepository(ie.Event) - } - } else if ie.Event.Kind == 30618 { - if ie.Event.CreatedAt > state.CreatedAt { - state = nip34.ParseRepositoryState(ie.Event) - } - } - } - - if repo.Event.ID == nostr.ZeroID { - return fmt.Errorf("no existing repository announcement found") - } - // check if signer matches owner or is in maintainers - if currentPk != ownerPk && !slices.Contains(repo.Maintainers, currentPk) { + if currentPk != repo.PubKey && !slices.Contains(repo.Maintainers, currentPk) { return fmt.Errorf("current user '%s' is not allowed to push", nip19.EncodeNpub(currentPk)) } @@ -696,8 +588,8 @@ var gitPush = &cli.Command{ return fmt.Errorf("error signing state event: %w", err) } - log("- publishing updated repository state to " + color.CyanString("%v", relays) + "\n") - for res := range sys.Pool.PublishMany(ctx, relays, newStateEvent) { + log("- publishing updated repository state to " + color.CyanString("%v", repo.Relays) + "\n") + for res := range sys.Pool.PublishMany(ctx, repo.Relays, newStateEvent) { if res.Error != nil { log("! error publishing event to %s: %v\n", color.YellowString(res.RelayURL), res.Error) } else { @@ -733,7 +625,18 @@ var gitPull = &cli.Command{ Name: "pull", Usage: "pull git changes", Action: func(ctx context.Context, c *cli.Command) error { - return fmt.Errorf("git pull not implemented yet") + state, localBranch, remoteBranch, err := gitFetchInternal(ctx, c) + if err != nil { + return err + } + + cmd := exec.Command("git", "checkout", fmt.Sprintf("origin/%s", state.HEAD)) + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed to checkout %s: %w", state.HEAD, err) + } + log("- checked out to %s\n", color.CyanString(state.HEAD)) + + return nil }, } @@ -741,7 +644,8 @@ var gitFetch = &cli.Command{ Name: "fetch", Usage: "fetch git data", Action: func(ctx context.Context, c *cli.Command) error { - return fmt.Errorf("git fetch not implemented yet") + _, err := gitFetchInternal(ctx, c) + return err }, } @@ -1041,3 +945,158 @@ func tryCloneAndCheckState(ctx context.Context, cloneURL, targetDir string, stat return nil } + +func gitFetchInternal(ctx context.Context, c *cli.Command) ( + state nip34.RepositoryState, + localBranch string, + remoteBranch string, + err error, +) { + // read nip34.json + configPath := "nip34.json" + var localConfig Nip34Config + data, err := os.ReadFile(configPath) + if err != nil { + return state, localBranch, remoteBranch, fmt.Errorf("failed to read nip34.json: %w (run 'nak git init' first)", err) + } + if err := json.Unmarshal(data, &localConfig); err != nil { + return state, localBranch, remoteBranch, fmt.Errorf("failed to parse nip34.json: %w", err) + } + + // get nostr remote + nostrRemote, localBranch, remoteBranch, err := getGitNostrRemote(c) + if err != nil { + return state, localBranch, remoteBranch, err + } + + if _, err = gitSanityCheck(localConfig, nostrRemote); err != nil { + return state, localBranch, remoteBranch, err + } + + // fetch repo and state + repo, state, err := fetchRepositoryAndState(ctx, nostrRemote) + if err != nil { + return state, localBranch, remoteBranch, err + } + + // try fetch from each clone url + fetched := false + for _, cloneURL := range repo.Clone { + log("- fetching from %s... ", color.CyanString(cloneURL)) + cmd := exec.Command("git", "fetch", cloneURL, "--update-head-ok") + if err := cmd.Run(); err != nil { + log(color.YellowString("failed: %v\n", err)) + continue + } + + // check downloaded remote branches + mismatch := false + for branch, commit := range state.Branches { + cmd = exec.Command("git", "rev-parse", fmt.Sprintf("refs/remotes/origin/%s", branch)) + out, err := cmd.Output() + if err != nil { + log(color.YellowString("branch %s not found\n", branch)) + mismatch = true + break + } + if strings.TrimSpace(string(out)) != commit { + log(color.YellowString("branch %s commit mismatch: got %s, expected %s\n", branch, strings.TrimSpace(string(out)), commit)) + mismatch = true + break + } + } + if !mismatch { + log("%s\n", color.GreenString("ok")) + fetched = true + break + } else { + log(color.YellowString("mismatch\n")) + } + } + + if !fetched { + return state, localBranch, remoteBranch, fmt.Errorf("failed to fetch from any clone url") + } + + return state, localBranch, remoteBranch, nil +} + +func fetchRepositoryAndState( + ctx context.Context, + repoURI string, +) (repo nip34.Repository, state nip34.RepositoryState, err error) { + if !strings.HasPrefix(repoURI, "nostr://") { + return repo, state, fmt.Errorf("invalid nostr URI: %s", repoURI) + } + + uriParts := strings.Split(strings.TrimPrefix(repoURI, "nostr://"), "/") + if len(uriParts) != 3 { + return repo, state, fmt.Errorf("invalid nostr URI format, expected nostr:////, got: %s", repoURI) + } + + ownerNpub := uriParts[0] + relayHost := uriParts[1] + identifier := uriParts[2] + + prefix, decoded, err := nip19.Decode(ownerNpub) + if err != nil || prefix != "npub" { + return repo, state, fmt.Errorf("invalid owner npub in URI: %w", err) + } + + ownerPk := decoded.(nostr.PubKey) + primaryRelay := nostr.NormalizeURL(relayHost) + + // fetch repository announcement (30617) + relays := appendUnique([]string{primaryRelay}, sys.FetchOutboxRelays(ctx, ownerPk, 3)...) + for ie := range sys.Pool.FetchMany(ctx, relays, nostr.Filter{ + Kinds: []nostr.Kind{30617}, + Authors: []nostr.PubKey{ownerPk}, + Tags: nostr.TagMap{ + "d": []string{identifier}, + }, + Limit: 2, + }, nostr.SubscriptionOptions{Label: "nak-git-clone-meta"}) { + if ie.Event.CreatedAt > repo.CreatedAt { + repo = nip34.ParseRepository(ie.Event) + } + } + if repo.Event.ID == nostr.ZeroID { + return repo, state, fmt.Errorf("no repository announcement (kind 30617) found for %s", identifier) + } + + // fetch repository state (30618) + var stateFound bool + var stateErr error + for ie := range sys.Pool.FetchMany(ctx, repo.Relays, nostr.Filter{ + Kinds: []nostr.Kind{30618}, + Authors: []nostr.PubKey{ownerPk}, + Tags: nostr.TagMap{ + "d": []string{identifier}, + }, + Limit: 2, + }, nostr.SubscriptionOptions{Label: "nak-git-clone-meta"}) { + if ie.Event.CreatedAt > state.CreatedAt { + state = nip34.ParseRepositoryState(ie.Event) + stateFound = true + + if state.HEAD == "" { + stateErr = fmt.Errorf("state is missing HEAD") + continue + } + if _, ok := state.Branches[state.HEAD]; !ok { + stateErr = fmt.Errorf("state is missing commit for HEAD branch '%s'", state.HEAD) + continue + } + + stateErr = nil + } + } + if !stateFound { + return repo, state, fmt.Errorf("no repository state (kind 30618) found") + } + if stateErr != nil { + return repo, state, stateErr + } + + return repo, state, nil +} From 79c1a706838f672c58a79e85ce1c30bd8b6545a6 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Fri, 21 Nov 2025 22:19:51 -0300 Subject: [PATCH 339/401] git: cleanup. --- git.go | 402 ++++++++++++++++++++++++++++----------------------------- 1 file changed, 199 insertions(+), 203 deletions(-) diff --git a/git.go b/git.go index 0c7f9be..c84ad81 100644 --- a/git.go +++ b/git.go @@ -29,6 +29,13 @@ type Nip34Config struct { Maintainers []string `json:"maintainers"` } +type nostrRemote struct { + formatted string + owner nostr.PubKey + identifier string + relayHost string +} + var git = &cli.Command{ Name: "git", Usage: "git-related operations", @@ -101,9 +108,8 @@ var gitInit = &cli.Command{ } // check if nip34.json already exists - configPath := "nip34.json" var existingConfig Nip34Config - if data, err := os.ReadFile(configPath); err == nil { + if data, err := os.ReadFile("nip34.json"); err == nil { // file exists, read it if !c.Bool("force") && !c.Bool("interactive") { return fmt.Errorf("nip34.json already exists, use --force to overwrite or --interactive to update") @@ -139,17 +145,11 @@ var gitInit = &cli.Command{ if len(parts) >= 2 { nostrURL := parts[1] // parse nostr://npub.../relay_hostname/identifier - if strings.HasPrefix(nostrURL, "nostr://") { - urlParts := strings.TrimPrefix(nostrURL, "nostr://") - components := strings.Split(urlParts, "/") - if len(components) == 3 { - npub := components[0] - relayHostname := components[1] - identifier := components[2] - // convert to https://relay_hostname/npub.../identifier.git - cloneURL := fmt.Sprintf("http%s/%s/%s.git", nostr.NormalizeURL(relayHostname)[2:], npub, identifier) - defaultCloneURLs = appendUnique(defaultCloneURLs, cloneURL) - } + if remote, err := parseRemote(nostrURL); err == nil { + // convert to https://relay_hostname/npub.../identifier.git + cloneURL := fmt.Sprintf("http%s/%s/%s.git", + nostr.NormalizeURL(remote.relayHost)[2:], nip19.EncodeNpub(remote.owner), remote.identifier) + defaultCloneURLs = appendUnique(defaultCloneURLs, cloneURL) } } } @@ -195,28 +195,23 @@ var gitInit = &cli.Command{ } // write config file - data, err := json.MarshalIndent(config, "", " ") - if err != nil { - return fmt.Errorf("failed to marshal config: %w", err) + if err := writeNip34ConfigFile("", config); err != nil { + return err } - if err := os.WriteFile(configPath, data, 0644); err != nil { - return fmt.Errorf("failed to write nip34.json: %w", err) - } - - log("created %s\n", color.GreenString(configPath)) + log("created %s\n", color.GreenString("nip34.json")) // parse owner to npub - pk, err := parsePubKey(config.Owner) + owner, err := parsePubKey(config.Owner) if err != nil { return fmt.Errorf("invalid owner public key: %w", err) } - ownerNpub := nip19.EncodeNpub(pk) // check existing git remotes - nostrRemote, _, _, err := getGitNostrRemote(c) + remote, _, _, err := getGitNostrRemote(c) if err != nil { - remoteURL := fmt.Sprintf("nostr://%s/%s/%s", ownerNpub, config.GraspServers[0], config.Identifier) + remoteURL := fmt.Sprintf("nostr://%s/%s/%s", + nip19.EncodeNpub(owner), config.GraspServers[0], config.Identifier) cmd = exec.Command("git", "remote", "add", "origin", remoteURL) if err := cmd.Run(); err != nil { return fmt.Errorf("failed to add git remote: %w", err) @@ -224,49 +219,22 @@ var gitInit = &cli.Command{ log("added git remote: %s\n", remoteURL) } else { // validate existing remote - if !strings.HasPrefix(nostrRemote, "nostr://") { - return fmt.Errorf("invalid nostr remote URL: %s", nostrRemote) + if remote.owner != owner { + return fmt.Errorf("git remote npub '%s' does not match owner '%s'", + nip19.EncodeNpub(remote.owner), nip19.EncodeNpub(owner)) } - urlParts := strings.TrimPrefix(nostrRemote, "nostr://") - parts := strings.Split(urlParts, "/") - if len(parts) != 3 { - return fmt.Errorf("invalid nostr URL format, expected nostr:////, got: %s", nostrRemote) + if !slices.Contains(config.GraspServers, nostr.NormalizeURL(remote.relayHost)) { + return fmt.Errorf("git remote relay '%s' not in grasp servers %v", + remote.relayHost, config.GraspServers) } - repoNpub := parts[0] - relayHostname := parts[1] - identifier := parts[2] - if repoNpub != ownerNpub { - return fmt.Errorf("git remote npub '%s' does not match owner '%s'", repoNpub, ownerNpub) - } - if !slices.Contains(config.GraspServers, nostr.NormalizeURL(relayHostname)) { - return fmt.Errorf("git remote relay '%s' not in grasp servers %v", relayHostname, config.GraspServers) - } - if identifier != config.Identifier { - return fmt.Errorf("git remote identifier '%s' does not match config '%s'", identifier, config.Identifier) + if remote.identifier != config.Identifier { + return fmt.Errorf("git remote identifier '%s' does not match config '%s'", + remote.identifier, config.Identifier) } } // gitignore it - excludePath := ".git/info/exclude" - excludeContent, err := os.ReadFile(excludePath) - if err != nil { - // file doesn't exist, create it - excludeContent = []byte("") - } - - // check if nip34.json is already in exclude - if !strings.Contains(string(excludeContent), "nip34.json") { - newContent := string(excludeContent) - if len(newContent) > 0 && !strings.HasSuffix(newContent, "\n") { - newContent += "\n" - } - newContent += "nip34.json\n" - if err := os.WriteFile(excludePath, []byte(newContent), 0644); err != nil { - log(color.YellowString("failed to add nip34.json to .git/info/exclude: %v\n", err)) - } else { - log("added nip34.json to %s\n", color.GreenString(".git/info/exclude")) - } - } + excludeNip34ConfigFile() log("edit %s if needed, then run %s to publish.\n", color.CyanString("nip34.json"), @@ -395,7 +363,15 @@ var gitClone = &cli.Command{ return fmt.Errorf("missing repository URI (expected nostr:////)") } - repo, state, err := fetchRepositoryAndState(ctx, args.Get(0)) + remote, err := parseRemote(args.Get(0)) + if err != nil { + return fmt.Errorf("failed to parse remote url '%s': %s", args.Get(0), err) + } + + repo, state, err := fetchRepositoryAndState(ctx, remote) + if err != nil { + return err + } // determine target directory targetDir := "" @@ -444,7 +420,7 @@ var gitClone = &cli.Command{ normalizedRelays = append(normalizedRelays, nostr.NormalizeURL(r)) } - cfg := Nip34Config{ + localConfig := Nip34Config{ Identifier: repo.ID, Name: repo.Name, Description: repo.Description, @@ -455,38 +431,16 @@ var gitClone = &cli.Command{ Maintainers: make([]string, 0, len(repo.Maintainers)), } for _, m := range repo.Maintainers { - cfg.Maintainers = append(cfg.Maintainers, nip19.EncodeNpub(m)) + localConfig.Maintainers = append(localConfig.Maintainers, nip19.EncodeNpub(m)) } - data, err := json.MarshalIndent(cfg, "", " ") - if err != nil { - return fmt.Errorf("failed to marshal nip34.json: %w", err) - } - - configPath := filepath.Join(targetDir, "nip34.json") - if err := os.WriteFile(configPath, data, 0644); err != nil { - return fmt.Errorf("failed to write %s: %w", configPath, err) + // write nip34.json + if err := writeNip34ConfigFile("", localConfig); err != nil { + return err } // add nip34.json to .git/info/exclude in cloned repo - gitDir := filepath.Join(targetDir, ".git") - if st, err := os.Stat(gitDir); err == nil && st.IsDir() { - excludePath := filepath.Join(gitDir, "info", "exclude") - excludeContent, err := os.ReadFile(excludePath) - if err != nil { - excludeContent = []byte("") - } - if !strings.Contains(string(excludeContent), "nip34.json") { - newContent := string(excludeContent) - if len(newContent) > 0 && !strings.HasSuffix(newContent, "\n") { - newContent += "\n" - } - newContent += "nip34.json\n" - if err := os.WriteFile(excludePath, []byte(newContent), 0644); err != nil { - log(color.YellowString("failed to add nip34.json to %s: %v\n", excludePath, err)) - } - } - } + excludeNip34ConfigFile() log("cloned into %s\n", color.GreenString(targetDir)) return nil @@ -514,14 +468,9 @@ var gitPush = &cli.Command{ log("publishing as %s\n", color.CyanString(currentNpub)) // read nip34.json configuration - configPath := "nip34.json" - var localConfig Nip34Config - data, err := os.ReadFile(configPath) + localConfig, err := readNip34ConfigFile("") if err != nil { - return fmt.Errorf("failed to read nip34.json: %w (run 'nak git init' first)", err) - } - if err := json.Unmarshal(data, &localConfig); err != nil { - return fmt.Errorf("failed to parse nip34.json: %w", err) + return err } // get git remotes @@ -535,6 +484,10 @@ var gitPush = &cli.Command{ return err } + if err := gitSanityCheck(localConfig, nostrRemote); err != nil { + return err + } + // check if signer matches owner or is in maintainers if currentPk != repo.PubKey && !slices.Contains(repo.Maintainers, currentPk) { return fmt.Errorf("current user '%s' is not allowed to push", nip19.EncodeNpub(currentPk)) @@ -625,16 +578,16 @@ var gitPull = &cli.Command{ Name: "pull", Usage: "pull git changes", Action: func(ctx context.Context, c *cli.Command) error { - state, localBranch, remoteBranch, err := gitFetchInternal(ctx, c) + _, _, remoteBranch, err := gitFetchInternal(ctx, c) if err != nil { return err } - cmd := exec.Command("git", "checkout", fmt.Sprintf("origin/%s", state.HEAD)) + cmd := exec.Command("git", "checkout", fmt.Sprintf("origin/%s", remoteBranch)) if err := cmd.Run(); err != nil { - return fmt.Errorf("failed to checkout %s: %w", state.HEAD, err) + return fmt.Errorf("failed to checkout %s: %w", remoteBranch, err) } - log("- checked out to %s\n", color.CyanString(state.HEAD)) + log("- checked out to %s\n", color.CyanString(remoteBranch)) return nil }, @@ -644,7 +597,7 @@ var gitFetch = &cli.Command{ Name: "fetch", Usage: "fetch git data", Action: func(ctx context.Context, c *cli.Command) error { - _, err := gitFetchInternal(ctx, c) + _, _, _, err := gitFetchInternal(ctx, c) return err }, } @@ -661,14 +614,9 @@ var gitAnnounce = &cli.Command{ } // read nip34.json configuration - configPath := "nip34.json" - var localConfig Nip34Config - data, err := os.ReadFile(configPath) + localConfig, err := readNip34ConfigFile("") if err != nil { - return fmt.Errorf("failed to read nip34.json: %w (run 'nak git init' first)", err) - } - if err := json.Unmarshal(data, &localConfig); err != nil { - return fmt.Errorf("failed to parse nip34.json: %w", err) + return err } // get git remotes @@ -677,10 +625,10 @@ var gitAnnounce = &cli.Command{ return err } - ownerPk, err := gitSanityCheck(localConfig, nostrRemote) - if err != nil { + if err := gitSanityCheck(localConfig, nostrRemote); err != nil { return err } + owner, _ := parsePubKey(localConfig.Owner) // setup signer kr, _, err := gatherKeyerFromArguments(ctx, c) @@ -690,7 +638,7 @@ var gitAnnounce = &cli.Command{ currentPk, _ := kr.GetPublicKey(ctx) // current signer must match owner otherwise we can't announce - if currentPk != ownerPk { + if currentPk != owner { return fmt.Errorf("current user is not the owner of this repository, can't announce") } @@ -705,7 +653,8 @@ var gitAnnounce = &cli.Command{ } for _, server := range localConfig.GraspServers { graspRelayURL := nostr.NormalizeURL(server) - url := fmt.Sprintf("http%s/%s/%s.git", graspRelayURL[2:], nip19.EncodeNpub(ownerPk), localConfig.Identifier) + url := fmt.Sprintf("http%s/%s/%s.git", + graspRelayURL[2:], nip19.EncodeNpub(owner), localConfig.Identifier) localRepo.Clone = append(localRepo.Clone, url) localRepo.Relays = append(localRepo.Relays, graspRelayURL) } @@ -718,7 +667,7 @@ var gitAnnounce = &cli.Command{ } // these are the relays where we'll publish the announcement to - relays := append(sys.FetchOutboxRelays(ctx, ownerPk, 3), localConfig.GraspServers...) + relays := append(sys.FetchOutboxRelays(ctx, owner, 3), localConfig.GraspServers...) for i := range relays { relays[i] = nostr.NormalizeURL(relays[i]) } @@ -774,9 +723,9 @@ var gitAnnounce = &cli.Command{ } func getGitNostrRemote(c *cli.Command) ( - remoteURL string, - localBranch string, - remoteBranch string, + remote nostrRemote, + sourceBranch string, + targetBranch string, err error, ) { // remote @@ -790,7 +739,7 @@ func getGitNostrRemote(c *cli.Command) ( cmd = exec.Command("git", "rev-parse", "--abbrev-ref", "HEAD") output, err := cmd.Output() if err != nil { - return "", "", "", fmt.Errorf("failed to get current branch: %w", err) + return remote, "", "", fmt.Errorf("failed to get current branch: %w", err) } branch := strings.TrimSpace(string(output)) // get remote for branch @@ -806,85 +755,101 @@ func getGitNostrRemote(c *cli.Command) ( cmd = exec.Command("git", "remote", "get-url", remoteName) output, err := cmd.Output() if err != nil { - return "", "", "", fmt.Errorf("remote '%s' does not exist", remoteName) - } - remoteURL = strings.TrimSpace(string(output)) - if !strings.Contains(remoteURL, "nostr://") { - return "", "", "", fmt.Errorf("remote '%s' is not a nostr remote: %s", remoteName, remoteURL) + return remote, "", "", fmt.Errorf("remote '%s' does not exist", remoteName) } - // branch (local and remote) + remoteURL := strings.TrimSpace(string(output)) + if !strings.Contains(remoteURL, "nostr://") { + return remote, "", "", fmt.Errorf("remote '%s' is not a nostr remote: %s", remoteName, remoteURL) + } + + // branch (src and dst) if args.Len() > 1 { branchSpec := args.Get(1) if strings.Contains(branchSpec, ":") { parts := strings.Split(branchSpec, ":") if len(parts) == 2 { - localBranch = parts[0] - remoteBranch = parts[1] + sourceBranch = parts[0] + targetBranch = parts[1] } else { - return "", "", "", fmt.Errorf("invalid branch spec: %s", branchSpec) + return remote, "", "", fmt.Errorf("invalid branch spec: %s", branchSpec) } } else { - localBranch = branchSpec + sourceBranch = branchSpec } } else { // get current branch cmd = exec.Command("git", "rev-parse", "--abbrev-ref", "HEAD") output, err := cmd.Output() if err != nil { - return "", "", "", fmt.Errorf("failed to get current branch: %w", err) + return remote, "", "", fmt.Errorf("failed to get current branch: %w", err) } - localBranch = strings.TrimSpace(string(output)) + sourceBranch = strings.TrimSpace(string(output)) } - // get the upstream branch from git config - cmd = exec.Command("git", "config", "--get", fmt.Sprintf("branch.%s.merge", localBranch)) + // get the target branch from git config + cmd = exec.Command("git", "config", "--get", fmt.Sprintf("branch.%s.merge", sourceBranch)) output, err = cmd.Output() if err == nil { // parse refs/heads/ to get just the branch name mergeRef := strings.TrimSpace(string(output)) if strings.HasPrefix(mergeRef, "refs/heads/") { - remoteBranch = strings.TrimPrefix(mergeRef, "refs/heads/") + targetBranch = strings.TrimPrefix(mergeRef, "refs/heads/") } else { // fallback if it's not in expected format - remoteBranch = localBranch + targetBranch = sourceBranch } } else { // no upstream configured, assume same branch name - remoteBranch = localBranch + targetBranch = sourceBranch } - return remoteURL, localBranch, remoteBranch, nil + // parse remote + remote, err = parseRemote(remoteURL) + + return remote, sourceBranch, targetBranch, err +} + +func parseRemote(remoteURL string) (remote nostrRemote, err error) { + parts := strings.Split(remoteURL, "/") + if len(parts) != 5 { + return remote, fmt.Errorf( + "invalid nostr URL format, expected nostr:////, got: %s", remoteURL, + ) + } + + prefix, data, err := nip19.Decode(parts[2]) + if err != nil || prefix != "npub" { + return remote, fmt.Errorf("invalid owner public key: %w", err) + } + remote.formatted = remoteURL + remote.owner = data.(nostr.PubKey) + remote.relayHost = parts[3] + remote.identifier = parts[4] + + return remote, nil } func gitSanityCheck( localConfig Nip34Config, - nostrRemote string, -) (nostr.PubKey, error) { - urlParts := strings.TrimPrefix(nostrRemote, "nostr://") - parts := strings.Split(urlParts, "/") - if len(parts) != 3 { - return nostr.ZeroPK, fmt.Errorf("invalid nostr URL format, expected nostr:////, got: %s", nostrRemote) - } - - remoteNpub := parts[0] - remoteHostname := parts[1] - remoteIdentifier := parts[2] - - ownerPk, err := parsePubKey(localConfig.Owner) + remote nostrRemote, +) error { + owner, err := parsePubKey(localConfig.Owner) if err != nil { - return nostr.ZeroPK, fmt.Errorf("invalid owner public key: %w", err) + return fmt.Errorf("invalid owner pubkey in nip34.json: %w", err) } - if nip19.EncodeNpub(ownerPk) != remoteNpub { - return nostr.ZeroPK, fmt.Errorf("owner in nip34.json does not match git remote npub") + + if owner != remote.owner { + return fmt.Errorf("owner in nip34.json does not match git remote npub") } - if remoteIdentifier != localConfig.Identifier { - return nostr.ZeroPK, fmt.Errorf("git remote identifier '%s' differs from nip34.json identifier '%s'", remoteIdentifier, localConfig.Identifier) + if remote.identifier != localConfig.Identifier { + return fmt.Errorf("git remote identifier '%s' differs from nip34.json identifier '%s'", + remote.identifier, localConfig.Identifier) } - if !slices.Contains(localConfig.GraspServers, nostr.NormalizeURL(remoteHostname)) { - return nostr.ZeroPK, fmt.Errorf("git remote relay '%s' not in grasp servers %v", remoteHostname, localConfig.GraspServers) + if !slices.Contains(localConfig.GraspServers, nostr.NormalizeURL(remote.relayHost)) { + return fmt.Errorf("git remote relay '%s' not in grasp servers %v", remote.relayHost, localConfig.GraspServers) } - return ownerPk, nil + return nil } func tryCloneAndCheckState(ctx context.Context, cloneURL, targetDir string, state *nip34.RepositoryState) (err error) { @@ -946,30 +911,23 @@ func tryCloneAndCheckState(ctx context.Context, cloneURL, targetDir string, stat return nil } -func gitFetchInternal(ctx context.Context, c *cli.Command) ( - state nip34.RepositoryState, - localBranch string, - remoteBranch string, - err error, -) { - // read nip34.json - configPath := "nip34.json" - var localConfig Nip34Config - data, err := os.ReadFile(configPath) +func gitFetchInternal( + ctx context.Context, + c *cli.Command, +) (state nip34.RepositoryState, localBranch string, remoteBranch string, err error) { + localConfig, err := readNip34ConfigFile("") if err != nil { - return state, localBranch, remoteBranch, fmt.Errorf("failed to read nip34.json: %w (run 'nak git init' first)", err) - } - if err := json.Unmarshal(data, &localConfig); err != nil { - return state, localBranch, remoteBranch, fmt.Errorf("failed to parse nip34.json: %w", err) + return state, "", "", err } - // get nostr remote - nostrRemote, localBranch, remoteBranch, err := getGitNostrRemote(c) + nostrRemote, sourceBranch, targetBranch, err := getGitNostrRemote(c) + localBranch = targetBranch + remoteBranch = sourceBranch if err != nil { return state, localBranch, remoteBranch, err } - if _, err = gitSanityCheck(localConfig, nostrRemote); err != nil { + if err := gitSanityCheck(localConfig, nostrRemote); err != nil { return state, localBranch, remoteBranch, err } @@ -983,7 +941,15 @@ func gitFetchInternal(ctx context.Context, c *cli.Command) ( fetched := false for _, cloneURL := range repo.Clone { log("- fetching from %s... ", color.CyanString(cloneURL)) - cmd := exec.Command("git", "fetch", cloneURL, "--update-head-ok") + // construct git fetch command with appropriate refspec + args := []string{"fetch", cloneURL} + if remoteBranch != "" { + // fetch specific branch when refspec is provided + refspec := fmt.Sprintf("%s:%s", remoteBranch, remoteBranch) + args = append(args, refspec) + } + args = append(args, "--update-head-ok") + cmd := exec.Command("git", args...) if err := cmd.Run(); err != nil { log(color.YellowString("failed: %v\n", err)) continue @@ -1023,36 +989,17 @@ func gitFetchInternal(ctx context.Context, c *cli.Command) ( func fetchRepositoryAndState( ctx context.Context, - repoURI string, + remote nostrRemote, ) (repo nip34.Repository, state nip34.RepositoryState, err error) { - if !strings.HasPrefix(repoURI, "nostr://") { - return repo, state, fmt.Errorf("invalid nostr URI: %s", repoURI) - } - - uriParts := strings.Split(strings.TrimPrefix(repoURI, "nostr://"), "/") - if len(uriParts) != 3 { - return repo, state, fmt.Errorf("invalid nostr URI format, expected nostr:////, got: %s", repoURI) - } - - ownerNpub := uriParts[0] - relayHost := uriParts[1] - identifier := uriParts[2] - - prefix, decoded, err := nip19.Decode(ownerNpub) - if err != nil || prefix != "npub" { - return repo, state, fmt.Errorf("invalid owner npub in URI: %w", err) - } - - ownerPk := decoded.(nostr.PubKey) - primaryRelay := nostr.NormalizeURL(relayHost) + primaryRelay := nostr.NormalizeURL(remote.relayHost) // fetch repository announcement (30617) - relays := appendUnique([]string{primaryRelay}, sys.FetchOutboxRelays(ctx, ownerPk, 3)...) + relays := appendUnique([]string{primaryRelay}, sys.FetchOutboxRelays(ctx, remote.owner, 3)...) for ie := range sys.Pool.FetchMany(ctx, relays, nostr.Filter{ Kinds: []nostr.Kind{30617}, - Authors: []nostr.PubKey{ownerPk}, + Authors: []nostr.PubKey{remote.owner}, Tags: nostr.TagMap{ - "d": []string{identifier}, + "d": []string{remote.identifier}, }, Limit: 2, }, nostr.SubscriptionOptions{Label: "nak-git-clone-meta"}) { @@ -1061,7 +1008,7 @@ func fetchRepositoryAndState( } } if repo.Event.ID == nostr.ZeroID { - return repo, state, fmt.Errorf("no repository announcement (kind 30617) found for %s", identifier) + return repo, state, fmt.Errorf("no repository announcement (kind 30617) found for %s", remote.identifier) } // fetch repository state (30618) @@ -1069,9 +1016,9 @@ func fetchRepositoryAndState( var stateErr error for ie := range sys.Pool.FetchMany(ctx, repo.Relays, nostr.Filter{ Kinds: []nostr.Kind{30618}, - Authors: []nostr.PubKey{ownerPk}, + Authors: []nostr.PubKey{remote.owner}, Tags: nostr.TagMap{ - "d": []string{identifier}, + "d": []string{remote.identifier}, }, Limit: 2, }, nostr.SubscriptionOptions{Label: "nak-git-clone-meta"}) { @@ -1100,3 +1047,52 @@ func fetchRepositoryAndState( return repo, state, nil } + +func readNip34ConfigFile(baseDir string) (Nip34Config, error) { + var localConfig Nip34Config + data, err := os.ReadFile(filepath.Join(baseDir, "nip34.json")) + if err != nil { + return localConfig, fmt.Errorf("failed to read nip34.json: %w (run 'nak git init' first)", err) + } + if err := json.Unmarshal(data, &localConfig); err != nil { + return localConfig, fmt.Errorf("failed to parse nip34.json: %w", err) + } + return localConfig, nil +} + +func excludeNip34ConfigFile() { + excludePath := ".git/info/exclude" + excludeContent, err := os.ReadFile(excludePath) + if err != nil { + // file doesn't exist, create it + excludeContent = []byte("") + } + + // check if nip34.json is already in exclude + if !strings.Contains(string(excludeContent), "nip34.json") { + newContent := string(excludeContent) + if len(newContent) > 0 && !strings.HasSuffix(newContent, "\n") { + newContent += "\n" + } + newContent += "nip34.json\n" + if err := os.WriteFile(excludePath, []byte(newContent), 0644); err != nil { + log(color.YellowString("failed to add nip34.json to .git/info/exclude: %v\n", err)) + } else { + log("added nip34.json to %s\n", color.GreenString(".git/info/exclude")) + } + } +} + +func writeNip34ConfigFile(baseDir string, cfg Nip34Config) error { + data, err := json.MarshalIndent(cfg, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal nip34.json: %w", err) + } + + configPath := filepath.Join(baseDir, "nip34.json") + if err := os.WriteFile(configPath, data, 0644); err != nil { + return fmt.Errorf("failed to write %s: %w", configPath, err) + } + + return nil +} From 68e49fa6e52326f7b7909b3c63f03f8547fe8523 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sun, 23 Nov 2025 14:50:19 -0300 Subject: [PATCH 340/401] git: fix the local/remote madness finally I think. --- git.go | 77 ++++++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 50 insertions(+), 27 deletions(-) diff --git a/git.go b/git.go index c84ad81..2d88137 100644 --- a/git.go +++ b/git.go @@ -208,7 +208,7 @@ var gitInit = &cli.Command{ } // check existing git remotes - remote, _, _, err := getGitNostrRemote(c) + remote, _, _, err := getGitNostrRemote(c, false) if err != nil { remoteURL := fmt.Sprintf("nostr://%s/%s/%s", nip19.EncodeNpub(owner), config.GraspServers[0], config.Identifier) @@ -474,7 +474,7 @@ var gitPush = &cli.Command{ } // get git remotes - nostrRemote, localBranch, remoteBranch, err := getGitNostrRemote(c) + nostrRemote, localBranch, remoteBranch, err := getGitNostrRemote(c, true) if err != nil { return err } @@ -620,7 +620,7 @@ var gitAnnounce = &cli.Command{ } // get git remotes - nostrRemote, _, _, err := getGitNostrRemote(c) + nostrRemote, _, _, err := getGitNostrRemote(c, false) if err != nil { return err } @@ -722,10 +722,10 @@ var gitAnnounce = &cli.Command{ }, } -func getGitNostrRemote(c *cli.Command) ( +func getGitNostrRemote(c *cli.Command, isPush bool) ( remote nostrRemote, - sourceBranch string, - targetBranch string, + localBranch string, + remoteBranch string, err error, ) { // remote @@ -742,6 +742,7 @@ func getGitNostrRemote(c *cli.Command) ( return remote, "", "", fmt.Errorf("failed to get current branch: %w", err) } branch := strings.TrimSpace(string(output)) + // get remote for branch cmd = exec.Command("git", "config", "--get", fmt.Sprintf("branch.%s.remote", branch)) output, err = cmd.Output() @@ -765,17 +766,25 @@ func getGitNostrRemote(c *cli.Command) ( // branch (src and dst) if args.Len() > 1 { + var src, dst string branchSpec := args.Get(1) if strings.Contains(branchSpec, ":") { parts := strings.Split(branchSpec, ":") if len(parts) == 2 { - sourceBranch = parts[0] - targetBranch = parts[1] + src = parts[0] + dst = parts[1] } else { return remote, "", "", fmt.Errorf("invalid branch spec: %s", branchSpec) } } else { - sourceBranch = branchSpec + src = branchSpec + } + if isPush { + localBranch = src + remoteBranch = dst + } else { + localBranch = dst + remoteBranch = src } } else { // get current branch @@ -784,30 +793,39 @@ func getGitNostrRemote(c *cli.Command) ( if err != nil { return remote, "", "", fmt.Errorf("failed to get current branch: %w", err) } - sourceBranch = strings.TrimSpace(string(output)) + localBranch = strings.TrimSpace(string(output)) } // get the target branch from git config - cmd = exec.Command("git", "config", "--get", fmt.Sprintf("branch.%s.merge", sourceBranch)) - output, err = cmd.Output() - if err == nil { - // parse refs/heads/ to get just the branch name - mergeRef := strings.TrimSpace(string(output)) - if strings.HasPrefix(mergeRef, "refs/heads/") { - targetBranch = strings.TrimPrefix(mergeRef, "refs/heads/") - } else { - // fallback if it's not in expected format - targetBranch = sourceBranch + if isPush { + cmd = exec.Command("git", "config", "--get", fmt.Sprintf("branch.%s.merge", localBranch)) + output, err = cmd.Output() + if err == nil { + // parse refs/heads/ to get just the branch name + mergeRef := strings.TrimSpace(string(output)) + if strings.HasPrefix(mergeRef, "refs/heads/") { + remoteBranch = strings.TrimPrefix(mergeRef, "refs/heads/") + } else { + // fallback if it's not in expected format + remoteBranch = localBranch + } + } + + if remoteBranch == "" { + // no upstream configured, assume same branch name + remoteBranch = localBranch } } else { - // no upstream configured, assume same branch name - targetBranch = sourceBranch + if localBranch == "" { + // no local branch configured, assume same branch name + localBranch = remoteBranch + } } // parse remote remote, err = parseRemote(remoteURL) - return remote, sourceBranch, targetBranch, err + return remote, localBranch, remoteBranch, err } func parseRemote(remoteURL string) (remote nostrRemote, err error) { @@ -920,9 +938,7 @@ func gitFetchInternal( return state, "", "", err } - nostrRemote, sourceBranch, targetBranch, err := getGitNostrRemote(c) - localBranch = targetBranch - remoteBranch = sourceBranch + nostrRemote, localBranch, remoteBranch, err := getGitNostrRemote(c, false) if err != nil { return state, localBranch, remoteBranch, err } @@ -945,7 +961,7 @@ func gitFetchInternal( args := []string{"fetch", cloneURL} if remoteBranch != "" { // fetch specific branch when refspec is provided - refspec := fmt.Sprintf("%s:%s", remoteBranch, remoteBranch) + refspec := fmt.Sprintf("%s:%s", remoteBranch, localBranch) args = append(args, refspec) } args = append(args, "--update-head-ok") @@ -1050,6 +1066,9 @@ func fetchRepositoryAndState( func readNip34ConfigFile(baseDir string) (Nip34Config, error) { var localConfig Nip34Config + + // TODO: the baseDir should inspect parents until we reach the directory that has the ".git" + data, err := os.ReadFile(filepath.Join(baseDir, "nip34.json")) if err != nil { return localConfig, fmt.Errorf("failed to read nip34.json: %w (run 'nak git init' first)", err) @@ -1061,6 +1080,8 @@ func readNip34ConfigFile(baseDir string) (Nip34Config, error) { } func excludeNip34ConfigFile() { + // TODO: inspect parents until we reach the directory that has the ".git" + excludePath := ".git/info/exclude" excludeContent, err := os.ReadFile(excludePath) if err != nil { @@ -1084,6 +1105,8 @@ func excludeNip34ConfigFile() { } func writeNip34ConfigFile(baseDir string, cfg Nip34Config) error { + // TODO: baseDir should inspect parents until we reach the directory that has the ".git" + data, err := json.MarshalIndent(cfg, "", " ") if err != nil { return fmt.Errorf("failed to marshal nip34.json: %w", err) From ddc009a391cf38f0dfe0ace3ebfcf6cb240ce775 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sun, 23 Nov 2025 17:09:02 -0300 Subject: [PATCH 341/401] git: rework it to be more git-native and expose the internals more in a cool way. --- git.go | 1060 +++++++++++++++++++++++++++++++--------------------- go.mod | 4 + go.sum | 4 + helpers.go | 12 + 4 files changed, 658 insertions(+), 422 deletions(-) diff --git a/git.go b/git.go index 2d88137..3b62dec 100644 --- a/git.go +++ b/git.go @@ -3,12 +3,12 @@ package main import ( "context" "fmt" - "net/url" "os" "os/exec" "path/filepath" "slices" "strings" + "time" "fiatjaf.com/nostr" "fiatjaf.com/nostr/nip19" @@ -39,19 +39,25 @@ type nostrRemote struct { var git = &cli.Command{ Name: "git", Usage: "git-related operations", + Description: `this implements versions of common git commands, like 'clone', 'fetch', 'pull' and 'push', but differently from the normal git commands these never take a remote name, the remote is assumed to what is defined by nip34 events and specified in the (automatically hidden) nip34.json file. + +aside from those, there is also: + - 'nak git init' for setting up nip34 repository metadata; and + - 'nak git sync' for getting the latest metadata update from nostr relays (called automatically by other commands) +`, Commands: []*cli.Command{ gitInit, + gitSync, gitClone, gitPush, gitPull, gitFetch, - gitAnnounce, }, } var gitInit = &cli.Command{ Name: "init", - Usage: "initialize a NIP-34 repository configuration", + Usage: "initialize a nip34 repository configuration", Flags: []cli.Flag{ &cli.BoolFlag{ Name: "interactive", @@ -104,19 +110,23 @@ var gitInit = &cli.Command{ // check if current directory is a git repository cmd := exec.Command("git", "rev-parse", "--git-dir") if err := cmd.Run(); err != nil { - return fmt.Errorf("current directory is not a git repository") + // initialize a git repository + log("initializing git repository...\n") + initCmd := exec.Command("git", "init") + initCmd.Stderr = os.Stderr + initCmd.Stdout = os.Stdout + if err := initCmd.Run(); err != nil { + return fmt.Errorf("failed to initialize git repository: %w", err) + } } // check if nip34.json already exists - var existingConfig Nip34Config - if data, err := os.ReadFile("nip34.json"); err == nil { - // file exists, read it + existingConfig, err := readNip34ConfigFile("") + if err == nil { + // file exists if !c.Bool("force") && !c.Bool("interactive") { return fmt.Errorf("nip34.json already exists, use --force to overwrite or --interactive to update") } - if err := json.Unmarshal(data, &existingConfig); err != nil { - return fmt.Errorf("failed to parse existing nip34.json: %s", err) - } } // get repository base directory name for defaults @@ -136,6 +146,7 @@ var gitInit = &cli.Command{ } // extract clone URLs from nostr:// git remotes + // (this is just for migrating from ngit) var defaultCloneURLs []string if output, err := exec.Command("git", "remote", "-v").Output(); err == nil { remotes := strings.Split(strings.TrimSpace(string(output)), "\n") @@ -145,10 +156,11 @@ var gitInit = &cli.Command{ if len(parts) >= 2 { nostrURL := parts[1] // parse nostr://npub.../relay_hostname/identifier - if remote, err := parseRemote(nostrURL); err == nil { + if owner, identifier, relays, err := parseRepositoryAddress(ctx, nostrURL); err == nil && len(relays) > 0 { + relayURL := relays[0] // convert to https://relay_hostname/npub.../identifier.git cloneURL := fmt.Sprintf("http%s/%s/%s.git", - nostr.NormalizeURL(remote.relayHost)[2:], nip19.EncodeNpub(remote.owner), remote.identifier) + relayURL[2:], nip19.EncodeNpub(owner), identifier) defaultCloneURLs = appendUnique(defaultCloneURLs, cloneURL) } } @@ -207,34 +219,31 @@ var gitInit = &cli.Command{ return fmt.Errorf("invalid owner public key: %w", err) } - // check existing git remotes - remote, _, _, err := getGitNostrRemote(c, false) - if err != nil { - remoteURL := fmt.Sprintf("nostr://%s/%s/%s", - nip19.EncodeNpub(owner), config.GraspServers[0], config.Identifier) - cmd = exec.Command("git", "remote", "add", "origin", remoteURL) - if err := cmd.Run(); err != nil { - return fmt.Errorf("failed to add git remote: %w", err) - } - log("added git remote: %s\n", remoteURL) - } else { - // validate existing remote - if remote.owner != owner { - return fmt.Errorf("git remote npub '%s' does not match owner '%s'", - nip19.EncodeNpub(remote.owner), nip19.EncodeNpub(owner)) - } - if !slices.Contains(config.GraspServers, nostr.NormalizeURL(remote.relayHost)) { - return fmt.Errorf("git remote relay '%s' not in grasp servers %v", - remote.relayHost, config.GraspServers) - } - if remote.identifier != config.Identifier { - return fmt.Errorf("git remote identifier '%s' does not match config '%s'", - remote.identifier, config.Identifier) + // convert local config to nip34.Repository for setting up remotes + localRepo := nip34.Repository{ + ID: config.Identifier, + Name: config.Name, + Description: config.Description, + Web: config.Web, + EarliestUniqueCommitID: config.EarliestUniqueCommit, + Maintainers: []nostr.PubKey{}, + Event: nostr.Event{PubKey: owner}, + } + for _, server := range config.GraspServers { + graspRelayURL := nostr.NormalizeURL(server) + localRepo.Relays = append(localRepo.Relays, graspRelayURL) + } + for _, maintainer := range config.Maintainers { + if pk, err := parsePubKey(maintainer); err == nil { + localRepo.Maintainers = append(localRepo.Maintainers, pk) } } + // setup git remotes + gitSetupRemotes(ctx, "", localRepo) + // gitignore it - excludeNip34ConfigFile() + excludeNip34ConfigFile("") log("edit %s if needed, then run %s to publish.\n", color.CyanString("nip34.json"), @@ -244,40 +253,6 @@ var gitInit = &cli.Command{ }, } -func repositoriesEqual(a, b nip34.Repository) bool { - if a.ID != b.ID || a.Name != b.Name || a.Description != b.Description { - return false - } - if a.EarliestUniqueCommitID != b.EarliestUniqueCommitID { - return false - } - if len(a.Web) != len(b.Web) || len(a.Clone) != len(b.Clone) || - len(a.Relays) != len(b.Relays) || len(a.Maintainers) != len(b.Maintainers) { - return false - } - for i := range a.Web { - if a.Web[i] != b.Web[i] { - return false - } - } - for i := range a.Clone { - if a.Clone[i] != b.Clone[i] { - return false - } - } - for i := range a.Relays { - if a.Relays[i] != b.Relays[i] { - return false - } - } - for i := range a.Maintainers { - if a.Maintainers[i] != b.Maintainers[i] { - return false - } - } - return true -} - func promptForConfig(config *Nip34Config) error { rlConfig := &readline.Config{ Stdout: os.Stderr, @@ -354,21 +329,23 @@ func promptForConfig(config *Nip34Config) error { } var gitClone = &cli.Command{ - Name: "clone", - Usage: "clone a NIP-34 repository from a nostr:// URI", - ArgsUsage: "nostr://// [directory]", + Name: "clone", + Usage: "clone a NIP-34 repository from a nostr:// URI", + Description: `the parameter maybe in the form "/", ngit-style like "nostr:////" or an "naddr1..." code.`, + ArgsUsage: " [directory]", Action: func(ctx context.Context, c *cli.Command) error { args := c.Args() if args.Len() == 0 { - return fmt.Errorf("missing repository URI (expected nostr:////)") + return fmt.Errorf("missing repository address") } - remote, err := parseRemote(args.Get(0)) + owner, identifier, relayHints, err := parseRepositoryAddress(ctx, args.Get(0)) if err != nil { return fmt.Errorf("failed to parse remote url '%s': %s", args.Get(0), err) } - repo, state, err := fetchRepositoryAndState(ctx, remote) + // fetch repository metadata and state + repo, state, err := fetchRepositoryAndState(ctx, owner, identifier, relayHints) if err != nil { return err } @@ -392,55 +369,73 @@ var gitClone = &cli.Command{ } } - // decide which clone URL to use - if len(repo.Clone) == 0 { - return fmt.Errorf("no clone urls found for repository") + // create directory + if err := os.MkdirAll(targetDir, 0755); err != nil { + return fmt.Errorf("failed to create directory '%s': %w", targetDir, err) } - cloned := false - for _, url := range repo.Clone { - log("- cloning %s... ", color.CyanString(url)) - if err := tryCloneAndCheckState(ctx, url, targetDir, &state); err != nil { - log(color.YellowString("failed: %v\n", err)) - continue - } - log("%s\n", color.GreenString("ok")) - cloned = true - break - } - - if !cloned { - return fmt.Errorf("failed to clone") + // initialize git inside the directory + initCmd := exec.Command("git", "init") + initCmd.Dir = targetDir + if err := initCmd.Run(); err != nil { + return fmt.Errorf("failed to initialize git repository: %w", err) } // write nip34.json inside cloned directory - // normalize relay URLs for consistency - normalizedRelays := make([]string, 0, len(repo.Relays)) - for _, r := range repo.Relays { - normalizedRelays = append(normalizedRelays, nostr.NormalizeURL(r)) - } - localConfig := Nip34Config{ Identifier: repo.ID, Name: repo.Name, Description: repo.Description, Web: repo.Web, Owner: nip19.EncodeNpub(repo.Event.PubKey), - GraspServers: normalizedRelays, + GraspServers: make([]string, 0, len(repo.Relays)), EarliestUniqueCommit: repo.EarliestUniqueCommitID, Maintainers: make([]string, 0, len(repo.Maintainers)), } + for _, r := range repo.Relays { + localConfig.GraspServers = append(localConfig.GraspServers, nostr.NormalizeURL(r)) + } for _, m := range repo.Maintainers { localConfig.Maintainers = append(localConfig.Maintainers, nip19.EncodeNpub(m)) } // write nip34.json - if err := writeNip34ConfigFile("", localConfig); err != nil { + if err := writeNip34ConfigFile(targetDir, localConfig); err != nil { return err } // add nip34.json to .git/info/exclude in cloned repo - excludeNip34ConfigFile() + excludeNip34ConfigFile(targetDir) + + // setup git remotes + gitSetupRemotes(ctx, targetDir, repo) + + // fetch from each grasp remote + fetchFromRemotes(ctx, targetDir, repo) + + // if we have a state with a HEAD, try to reset to it + if state.Event.ID != nostr.ZeroID && state.HEAD != "" { + if headCommit, ok := state.Branches[state.HEAD]; ok { + // check if we have that commit + checkCmd := exec.Command("git", "cat-file", "-e", headCommit) + checkCmd.Dir = targetDir + if err := checkCmd.Run(); err == nil { + // commit exists, reset to it + log("resetting to commit %s...\n", color.CyanString(headCommit)) + resetCmd := exec.Command("git", "reset", "--hard", headCommit) + resetCmd.Dir = targetDir + resetCmd.Stderr = os.Stderr + if err := resetCmd.Run(); err != nil { + log("! failed to reset: %v\n", color.YellowString("%v", err)) + } + } + } + } + + // update refs from state + if state.Event.ID != nostr.ZeroID { + gitUpdateRefs(ctx, targetDir, state) + } log("cloned into %s\n", color.GreenString(targetDir)) return nil @@ -467,29 +462,20 @@ var gitPush = &cli.Command{ currentNpub := nip19.EncodeNpub(currentPk) log("publishing as %s\n", color.CyanString(currentNpub)) - // read nip34.json configuration - localConfig, err := readNip34ConfigFile("") + // sync to ensure everything is up to date + repo, state, err := syncRepository(ctx, kr) if err != nil { - return err + return fmt.Errorf("failed to sync: %w", err) } - // get git remotes - nostrRemote, localBranch, remoteBranch, err := getGitNostrRemote(c, true) + // figure out which branches to push + localBranch, remoteBranch, err := figureOutBranches(c, c.Args().First(), true) if err != nil { return err } - repo, state, err := fetchRepositoryAndState(ctx, nostrRemote) - if err != nil { - return err - } - - if err := gitSanityCheck(localConfig, nostrRemote); err != nil { - return err - } - // check if signer matches owner or is in maintainers - if currentPk != repo.PubKey && !slices.Contains(repo.Maintainers, currentPk) { + if currentPk != repo.Event.PubKey && !slices.Contains(repo.Maintainers, currentPk) { return fmt.Errorf("current user '%s' is not allowed to push", nip19.EncodeNpub(currentPk)) } @@ -550,26 +536,32 @@ var gitPush = &cli.Command{ } } - // push to git clone URLs - for _, cloneURL := range repo.Clone { - log("> pushing to: %s\n", color.CyanString(cloneURL)) - args := []string{"push"} + // push to each grasp remote + pushSuccesses := 0 + for _, relay := range repo.Relays { + relayURL := nostr.NormalizeURL(relay) + remoteName := "nip34/grasp/" + strings.TrimPrefix(relayURL, "wss://") + remoteName = strings.TrimPrefix(remoteName, "ws://") + + log("pushing to %s...\n", color.CyanString(remoteName)) + pushArgs := []string{"push", remoteName, fmt.Sprintf("%s:refs/heads/%s", localBranch, remoteBranch)} if c.Bool("force") { - args = append(args, "--force") + pushArgs = append(pushArgs, "--force") } - args = append(args, - cloneURL, - fmt.Sprintf("refs/heads/%s:refs/heads/%s", localBranch, remoteBranch), - ) - cmd := exec.Command("git", args...) - output, err := cmd.CombinedOutput() - if err != nil { - log("> failed to push to %s: %v\n%s\n", color.RedString(cloneURL), err, string(output)) + pushCmd := exec.Command("git", pushArgs...) + pushCmd.Stderr = os.Stderr + if err := pushCmd.Run(); err != nil { + log("! failed to push to %s: %v\n", color.YellowString(remoteName), err) } else { - log("> successfully pushed to %s\n", color.GreenString(cloneURL)) + log("> pushed to %s\n", color.GreenString(remoteName)) + pushSuccesses++ } } + if pushSuccesses == 0 { + return fmt.Errorf("failed to push to any remote") + } + return nil }, } @@ -577,18 +569,61 @@ var gitPush = &cli.Command{ var gitPull = &cli.Command{ Name: "pull", Usage: "pull git changes", + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "rebase", + Usage: "rebase instead of merge", + }, + }, Action: func(ctx context.Context, c *cli.Command) error { - _, _, remoteBranch, err := gitFetchInternal(ctx, c) + // sync to fetch latest state and metadata + _, state, err := syncRepository(ctx, nil) + if err != nil { + return fmt.Errorf("failed to sync: %w", err) + } + + // figure out which branches to pull + localBranch, remoteBranch, err := figureOutBranches(c, c.Args().First(), false) if err != nil { return err } - cmd := exec.Command("git", "checkout", fmt.Sprintf("origin/%s", remoteBranch)) - if err := cmd.Run(); err != nil { - return fmt.Errorf("failed to checkout %s: %w", remoteBranch, err) + // get the commit from state for the remote branch + if state.Event.ID == nostr.ZeroID { + return fmt.Errorf("no repository state found") } - log("- checked out to %s\n", color.CyanString(remoteBranch)) + targetCommit, ok := state.Branches[remoteBranch] + if !ok { + return fmt.Errorf("branch '%s' not found in repository state", remoteBranch) + } + + // check if the commit exists locally + checkCmd := exec.Command("git", "cat-file", "-e", targetCommit) + if err := checkCmd.Run(); err != nil { + return fmt.Errorf("commit %s not found locally, try 'nak git fetch' first", targetCommit) + } + + // merge or rebase + if c.Bool("rebase") { + log("rebasing %s onto %s...\n", color.CyanString(localBranch), color.CyanString(targetCommit)) + rebaseCmd := exec.Command("git", "rebase", targetCommit) + rebaseCmd.Stderr = os.Stderr + rebaseCmd.Stdout = os.Stdout + if err := rebaseCmd.Run(); err != nil { + return fmt.Errorf("rebase failed: %w", err) + } + } else { + log("merging %s into %s...\n", color.CyanString(targetCommit), color.CyanString(localBranch)) + mergeCmd := exec.Command("git", "merge", targetCommit) + mergeCmd.Stderr = os.Stderr + mergeCmd.Stdout = os.Stdout + if err := mergeCmd.Run(); err != nil { + return fmt.Errorf("merge failed: %w", err) + } + } + + log("pull complete\n") return nil }, } @@ -597,7 +632,7 @@ var gitFetch = &cli.Command{ Name: "fetch", Usage: "fetch git data", Action: func(ctx context.Context, c *cli.Command) error { - _, _, _, err := gitFetchInternal(ctx, c) + _, _, err := syncRepository(ctx, nil) return err }, } @@ -619,17 +654,12 @@ var gitAnnounce = &cli.Command{ return err } - // get git remotes - nostrRemote, _, _, err := getGitNostrRemote(c, false) + // parse owner + owner, err := parsePubKey(localConfig.Owner) if err != nil { - return err + return fmt.Errorf("invalid owner public key: %w", err) } - if err := gitSanityCheck(localConfig, nostrRemote); err != nil { - return err - } - owner, _ := parsePubKey(localConfig.Owner) - // setup signer kr, _, err := gatherKeyerFromArguments(ctx, c) if err != nil { @@ -689,7 +719,7 @@ var gitAnnounce = &cli.Command{ repo = nip34.ParseRepository(ie.Event) // check if this is ok or the announcement in this relay needs to be updated - if repositoriesEqual(repo, localRepo) { + if repo.Equals(localRepo) { relayIdx := slices.Index(relays, ie.Relay.URL) oks[relayIdx] = true } @@ -722,300 +752,276 @@ var gitAnnounce = &cli.Command{ }, } -func getGitNostrRemote(c *cli.Command, isPush bool) ( - remote nostrRemote, - localBranch string, - remoteBranch string, - err error, -) { - // remote - var remoteName string - var cmd *exec.Cmd - args := c.Args() - if args.Len() > 0 { - remoteName = args.Get(0) - } else { - // get current branch - cmd = exec.Command("git", "rev-parse", "--abbrev-ref", "HEAD") - output, err := cmd.Output() - if err != nil { - return remote, "", "", fmt.Errorf("failed to get current branch: %w", err) - } - branch := strings.TrimSpace(string(output)) - - // get remote for branch - cmd = exec.Command("git", "config", "--get", fmt.Sprintf("branch.%s.remote", branch)) - output, err = cmd.Output() - if err != nil { - remoteName = "origin" - } else { - remoteName = strings.TrimSpace(string(output)) - } - } - // get the URL - cmd = exec.Command("git", "remote", "get-url", remoteName) - output, err := cmd.Output() - if err != nil { - return remote, "", "", fmt.Errorf("remote '%s' does not exist", remoteName) - } - - remoteURL := strings.TrimSpace(string(output)) - if !strings.Contains(remoteURL, "nostr://") { - return remote, "", "", fmt.Errorf("remote '%s' is not a nostr remote: %s", remoteName, remoteURL) - } - - // branch (src and dst) - if args.Len() > 1 { - var src, dst string - branchSpec := args.Get(1) - if strings.Contains(branchSpec, ":") { - parts := strings.Split(branchSpec, ":") - if len(parts) == 2 { - src = parts[0] - dst = parts[1] - } else { - return remote, "", "", fmt.Errorf("invalid branch spec: %s", branchSpec) - } - } else { - src = branchSpec - } - if isPush { - localBranch = src - remoteBranch = dst - } else { - localBranch = dst - remoteBranch = src - } - } else { - // get current branch - cmd = exec.Command("git", "rev-parse", "--abbrev-ref", "HEAD") - output, err := cmd.Output() - if err != nil { - return remote, "", "", fmt.Errorf("failed to get current branch: %w", err) - } - localBranch = strings.TrimSpace(string(output)) - } - - // get the target branch from git config - if isPush { - cmd = exec.Command("git", "config", "--get", fmt.Sprintf("branch.%s.merge", localBranch)) - output, err = cmd.Output() - if err == nil { - // parse refs/heads/ to get just the branch name - mergeRef := strings.TrimSpace(string(output)) - if strings.HasPrefix(mergeRef, "refs/heads/") { - remoteBranch = strings.TrimPrefix(mergeRef, "refs/heads/") - } else { - // fallback if it's not in expected format - remoteBranch = localBranch - } - } - - if remoteBranch == "" { - // no upstream configured, assume same branch name - remoteBranch = localBranch - } - } else { - if localBranch == "" { - // no local branch configured, assume same branch name - localBranch = remoteBranch - } - } - - // parse remote - remote, err = parseRemote(remoteURL) - - return remote, localBranch, remoteBranch, err +var gitSync = &cli.Command{ + Name: "sync", + Usage: "sync repository with relays", + Flags: defaultKeyFlags, + Action: func(ctx context.Context, c *cli.Command) error { + kr, _, _ := gatherKeyerFromArguments(ctx, c) + _, _, err := syncRepository(ctx, kr) + return err + }, } -func parseRemote(remoteURL string) (remote nostrRemote, err error) { - parts := strings.Split(remoteURL, "/") - if len(parts) != 5 { - return remote, fmt.Errorf( - "invalid nostr URL format, expected nostr:////, got: %s", remoteURL, - ) - } - - prefix, data, err := nip19.Decode(parts[2]) - if err != nil || prefix != "npub" { - return remote, fmt.Errorf("invalid owner public key: %w", err) - } - remote.formatted = remoteURL - remote.owner = data.(nostr.PubKey) - remote.relayHost = parts[3] - remote.identifier = parts[4] - - return remote, nil -} - -func gitSanityCheck( - localConfig Nip34Config, - remote nostrRemote, -) error { - owner, err := parsePubKey(localConfig.Owner) - if err != nil { - return fmt.Errorf("invalid owner pubkey in nip34.json: %w", err) - } - - if owner != remote.owner { - return fmt.Errorf("owner in nip34.json does not match git remote npub") - } - if remote.identifier != localConfig.Identifier { - return fmt.Errorf("git remote identifier '%s' differs from nip34.json identifier '%s'", - remote.identifier, localConfig.Identifier) - } - if !slices.Contains(localConfig.GraspServers, nostr.NormalizeURL(remote.relayHost)) { - return fmt.Errorf("git remote relay '%s' not in grasp servers %v", remote.relayHost, localConfig.GraspServers) - } - return nil -} - -func tryCloneAndCheckState(ctx context.Context, cloneURL, targetDir string, state *nip34.RepositoryState) (err error) { - // if we get here we know we were the ones who created the target directory, so we're safe to remove it - defer func() { - if err != nil { - if err := os.RemoveAll(targetDir); err != nil { - log("failed to remove '%s' when handling error from clone: %s", targetDir, err) - } - } - }() - - cmd := exec.CommandContext(ctx, "git", "clone", cloneURL, targetDir) - output, err := cmd.CombinedOutput() - if err != nil { - return fmt.Errorf("git clone failed: %v: %s", err, strings.TrimSpace(string(output))) - } - - // if we don't have any state information, we can't verify anything - if state == nil || state.Event.ID == nostr.ZeroID { - return nil - } - - // check that the HEAD branch matches the state HEAD - cmd = exec.Command("git", "-C", targetDir, "rev-parse", "--abbrev-ref", "HEAD") - headOut, err := cmd.Output() - if err != nil { - return fmt.Errorf("failed to read HEAD") - } - currentBranch := strings.TrimSpace(string(headOut)) - if currentBranch != state.HEAD { - return fmt.Errorf("received HEAD '%s' isn't the expected '%s'", currentBranch, state.HEAD) - } - - // verify the HEAD branch only as it's the only one we have - expectedCommit := state.Branches[state.HEAD] // we've tested before if state has this - cmd = exec.Command("git", "-C", targetDir, "rev-parse", state.HEAD) - actualOut, err := cmd.Output() - if err != nil { - return fmt.Errorf("failed to check commit for '%s': %s", state.HEAD, err) - } - actualCommit := strings.TrimSpace(string(actualOut)) - if actualCommit != expectedCommit { - return fmt.Errorf("branch %s is at %s, expected %s", state.HEAD, actualCommit, expectedCommit) - } - - // set nostr remote - parsed, _ := url.Parse(cloneURL) - repoURI := fmt.Sprintf("nostr://%s/%s/%s", - nip19.EncodeNpub(state.PubKey), - parsed.Host, - state.ID, - ) - cmd = exec.Command("git", "-C", targetDir, "remote", "set-url", "origin", repoURI) - if err := cmd.Run(); err != nil { - return fmt.Errorf("failed to add git remote: %v\n", err) - } - - return nil -} - -func gitFetchInternal( - ctx context.Context, - c *cli.Command, -) (state nip34.RepositoryState, localBranch string, remoteBranch string, err error) { +func syncRepository(ctx context.Context, signer nostr.Signer) (nip34.Repository, nip34.RepositoryState, error) { + // read current nip34.json localConfig, err := readNip34ConfigFile("") if err != nil { - return state, "", "", err + return nip34.Repository{}, nip34.RepositoryState{}, err } - nostrRemote, localBranch, remoteBranch, err := getGitNostrRemote(c, false) + // parse owner + owner, err := parsePubKey(localConfig.Owner) if err != nil { - return state, localBranch, remoteBranch, err + return nip34.Repository{}, nip34.RepositoryState{}, fmt.Errorf("invalid owner public key: %w", err) } - if err := gitSanityCheck(localConfig, nostrRemote); err != nil { - return state, localBranch, remoteBranch, err - } - - // fetch repo and state - repo, state, err := fetchRepositoryAndState(ctx, nostrRemote) + // fetch repository announcement and state from relays + repo, state, err := fetchRepositoryAndState(ctx, owner, localConfig.Identifier, localConfig.GraspServers) if err != nil { - return state, localBranch, remoteBranch, err - } - - // try fetch from each clone url - fetched := false - for _, cloneURL := range repo.Clone { - log("- fetching from %s... ", color.CyanString(cloneURL)) - // construct git fetch command with appropriate refspec - args := []string{"fetch", cloneURL} - if remoteBranch != "" { - // fetch specific branch when refspec is provided - refspec := fmt.Sprintf("%s:%s", remoteBranch, localBranch) - args = append(args, refspec) + logverbose("failed to fetch repository metadata: %v\n", err) + // create a local repository object from config + repo = nip34.Repository{ + ID: localConfig.Identifier, + Name: localConfig.Name, + Description: localConfig.Description, + Web: localConfig.Web, + EarliestUniqueCommitID: localConfig.EarliestUniqueCommit, + Event: nostr.Event{PubKey: owner}, + Maintainers: []nostr.PubKey{}, } - args = append(args, "--update-head-ok") - cmd := exec.Command("git", args...) - if err := cmd.Run(); err != nil { - log(color.YellowString("failed: %v\n", err)) - continue + for _, server := range localConfig.GraspServers { + graspRelayURL := nostr.NormalizeURL(server) + repo.Relays = append(repo.Relays, graspRelayURL) } - - // check downloaded remote branches - mismatch := false - for branch, commit := range state.Branches { - cmd = exec.Command("git", "rev-parse", fmt.Sprintf("refs/remotes/origin/%s", branch)) - out, err := cmd.Output() - if err != nil { - log(color.YellowString("branch %s not found\n", branch)) - mismatch = true - break - } - if strings.TrimSpace(string(out)) != commit { - log(color.YellowString("branch %s commit mismatch: got %s, expected %s\n", branch, strings.TrimSpace(string(out)), commit)) - mismatch = true - break + for _, maintainer := range localConfig.Maintainers { + if pk, err := parsePubKey(maintainer); err == nil { + repo.Maintainers = append(repo.Maintainers, pk) } } - if !mismatch { - log("%s\n", color.GreenString("ok")) - fetched = true - break - } else { - log(color.YellowString("mismatch\n")) + } else { + // check if local config differs from remote announcement + // construct local repo from config for comparison + localRepo := nip34.Repository{ + ID: localConfig.Identifier, + Name: localConfig.Name, + Description: localConfig.Description, + Web: localConfig.Web, + EarliestUniqueCommitID: localConfig.EarliestUniqueCommit, + Maintainers: []nostr.PubKey{}, + Event: nostr.Event{PubKey: owner}, + } + for _, server := range localConfig.GraspServers { + graspRelayURL := nostr.NormalizeURL(server) + localRepo.Relays = append(localRepo.Relays, graspRelayURL) + } + for _, maintainer := range localConfig.Maintainers { + if pk, err := parsePubKey(maintainer); err == nil { + localRepo.Maintainers = append(localRepo.Maintainers, pk) + } + } + + if !repo.Equals(localRepo) { + // check if we need to update local config or publish new announcement + // check modification times + configPath := filepath.Join(findGitRoot(""), "nip34.json") + if fi, err := os.Stat(configPath); err == nil { + configModTime := fi.ModTime() + announcementTime := time.Unix(int64(repo.Event.CreatedAt), 0) + + if configModTime.After(announcementTime) { + // local config is newer, publish new announcement if signer is available + if signer != nil { + log("local configuration is newer, publishing updated announcement...\n") + // prepare clone URLs + for _, server := range localConfig.GraspServers { + graspRelayURL := nostr.NormalizeURL(server) + url := fmt.Sprintf("http%s/%s/%s.git", + graspRelayURL[2:], nip19.EncodeNpub(owner), localConfig.Identifier) + localRepo.Clone = append(localRepo.Clone, url) + localRepo.Relays = append(localRepo.Relays, graspRelayURL) + } + + announcementEvent := localRepo.ToEvent() + if err := signer.SignEvent(ctx, &announcementEvent); err != nil { + return repo, state, fmt.Errorf("failed to sign announcement: %w", err) + } + + relays := append(sys.FetchOutboxRelays(ctx, owner, 3), localConfig.GraspServers...) + for res := range sys.Pool.PublishMany(ctx, relays, announcementEvent) { + if res.Error != nil { + log("! error publishing to %s: %v\n", color.YellowString(res.RelayURL), res.Error) + } else { + log("> published to %s\n", color.GreenString(res.RelayURL)) + } + } + repo = nip34.ParseRepository(announcementEvent) + } else { + log("local configuration is newer than remote, but no signer provided to publish update\n") + } + } else { + // remote is newer, update local config + log("remote announcement is newer, updating local configuration...\n") + localConfig.Name = repo.Name + localConfig.Description = repo.Description + localConfig.Web = repo.Web + localConfig.EarliestUniqueCommit = repo.EarliestUniqueCommitID + localConfig.Maintainers = make([]string, 0, len(repo.Maintainers)) + for _, m := range repo.Maintainers { + localConfig.Maintainers = append(localConfig.Maintainers, nip19.EncodeNpub(m)) + } + if err := writeNip34ConfigFile("", localConfig); err != nil { + log("! failed to update local config: %v\n", err) + } + } + } } } - if !fetched { - return state, localBranch, remoteBranch, fmt.Errorf("failed to fetch from any clone url") + // setup remotes + gitSetupRemotes(ctx, "", repo) + + // fetch from each grasp remote + fetchFromRemotes(ctx, "", repo) + + // update refs from state + if state.Event.ID != nostr.ZeroID { + gitUpdateRefs(ctx, "", state) } - return state, localBranch, remoteBranch, nil + return repo, state, nil +} + +func fetchFromRemotes(ctx context.Context, targetDir string, repo nip34.Repository) { + // fetch from each grasp remote + for _, grasp := range repo.Relays { + remoteName := "nip34/grasp/" + strings.Split(grasp, "/")[2] + + logverbose("fetching from %s...\n", remoteName) + fetchCmd := exec.Command("git", "fetch", remoteName) + if targetDir != "" { + fetchCmd.Dir = targetDir + } + fetchCmd.Stderr = os.Stderr + if err := fetchCmd.Run(); err != nil { + logverbose("failed to fetch from %s: %v\n", remoteName, err) + } + } +} + +func gitSetupRemotes(ctx context.Context, dir string, repo nip34.Repository) { + // get list of all remotes + listCmd := exec.Command("git", "remote") + if dir != "" { + listCmd.Dir = dir + } + output, err := listCmd.Output() + if err != nil { + logverbose("failed to list remotes: %v\n", err) + return + } + + // delete all nip34/grasp/ remotes + remotes := strings.Split(strings.TrimSpace(string(output)), "\n") + for _, remote := range remotes { + remote = strings.TrimSpace(remote) + if strings.HasPrefix(remote, "nip34/grasp/") { + delCmd := exec.Command("git", "remote", "remove", remote) + if dir != "" { + delCmd.Dir = dir + } + if err := delCmd.Run(); err != nil { + logverbose("failed to remove remote %s: %v\n", remote, err) + } + } + } + + // create new remotes for each grasp server + for _, relay := range repo.Relays { + relayURL := nostr.NormalizeURL(relay) + remoteName := "nip34/grasp/" + strings.TrimPrefix(relayURL, "wss://") + remoteName = strings.TrimPrefix(remoteName, "ws://") + + // construct the git URL + gitURL := fmt.Sprintf("http%s/%s/%s.git", + relayURL[2:], nip19.EncodeNpub(repo.Event.PubKey), repo.ID) + + addCmd := exec.Command("git", "remote", "add", remoteName, gitURL) + if dir != "" { + addCmd.Dir = dir + } + if err := addCmd.Run(); err != nil { + logverbose("failed to add remote %s: %v\n", remoteName, err) + } + } +} + +func gitUpdateRefs(ctx context.Context, dir string, state nip34.RepositoryState) { + // delete all existing nip34/state refs + showRefCmd := exec.Command("git", "show-ref") + if dir != "" { + showRefCmd.Dir = dir + } + output, err := showRefCmd.Output() + if err == nil { + lines := strings.Split(string(output), "\n") + for _, line := range lines { + parts := strings.Fields(line) + if len(parts) >= 2 && strings.Contains(parts[1], "refs/remotes/nip34/state/") { + delCmd := exec.Command("git", "update-ref", "-d", parts[1]) + if dir != "" { + delCmd.Dir = dir + } + delCmd.Run() + } + } + } + + // create refs for each branch in state + for branchName, commit := range state.Branches { + // skip non-refs branches + if !strings.HasPrefix(branchName, "refs/") { + branchName = "refs/heads/" + branchName + } + + refName := "refs/remotes/nip34/state/" + strings.TrimPrefix(branchName, "refs/heads/") + updateCmd := exec.Command("git", "update-ref", refName, commit) + if dir != "" { + updateCmd.Dir = dir + } + if err := updateCmd.Run(); err != nil { + logverbose("failed to update ref %s: %v\n", refName, err) + } + } + + // create ref for HEAD + if state.HEAD != "" { + if headCommit, ok := state.Branches[state.HEAD]; ok { + headRefName := "refs/remotes/nip34/state/HEAD" + updateCmd := exec.Command("git", "update-ref", headRefName, headCommit) + if dir != "" { + updateCmd.Dir = dir + } + if err := updateCmd.Run(); err != nil { + logverbose("failed to update HEAD ref: %v\n", err) + } + } + } } func fetchRepositoryAndState( ctx context.Context, - remote nostrRemote, + pubkey nostr.PubKey, + identifier string, + relayHints []string, ) (repo nip34.Repository, state nip34.RepositoryState, err error) { - primaryRelay := nostr.NormalizeURL(remote.relayHost) - // fetch repository announcement (30617) - relays := appendUnique([]string{primaryRelay}, sys.FetchOutboxRelays(ctx, remote.owner, 3)...) + relays := appendUnique(relayHints, sys.FetchOutboxRelays(ctx, pubkey, 3)...) for ie := range sys.Pool.FetchMany(ctx, relays, nostr.Filter{ Kinds: []nostr.Kind{30617}, - Authors: []nostr.PubKey{remote.owner}, + Authors: []nostr.PubKey{pubkey}, Tags: nostr.TagMap{ - "d": []string{remote.identifier}, + "d": []string{identifier}, }, Limit: 2, }, nostr.SubscriptionOptions{Label: "nak-git-clone-meta"}) { @@ -1024,7 +1030,7 @@ func fetchRepositoryAndState( } } if repo.Event.ID == nostr.ZeroID { - return repo, state, fmt.Errorf("no repository announcement (kind 30617) found for %s", remote.identifier) + return repo, state, fmt.Errorf("no repository announcement (kind 30617) found for %s", identifier) } // fetch repository state (30618) @@ -1032,9 +1038,9 @@ func fetchRepositoryAndState( var stateErr error for ie := range sys.Pool.FetchMany(ctx, repo.Relays, nostr.Filter{ Kinds: []nostr.Kind{30618}, - Authors: []nostr.PubKey{remote.owner}, + Authors: []nostr.PubKey{pubkey}, Tags: nostr.TagMap{ - "d": []string{remote.identifier}, + "d": []string{identifier}, }, Limit: 2, }, nostr.SubscriptionOptions{Label: "nak-git-clone-meta"}) { @@ -1064,25 +1070,77 @@ func fetchRepositoryAndState( return repo, state, nil } +func findGitRoot(startDir string) string { + if startDir == "" { + var err error + startDir, err = os.Getwd() + if err != nil { + return "" + } + } + + // make absolute + if !filepath.IsAbs(startDir) { + if abs, err := filepath.Abs(startDir); err == nil { + startDir = abs + } + } + + currentDir := startDir + for { + gitDir := filepath.Join(currentDir, ".git") + if fi, err := os.Stat(gitDir); err == nil { + if fi.IsDir() { + return currentDir + } + // .git might be a file (for submodules/worktrees) + return currentDir + } + + // move to parent directory + parentDir := filepath.Dir(currentDir) + if parentDir == currentDir { + // reached root without finding .git + return "" + } + currentDir = parentDir + } +} + func readNip34ConfigFile(baseDir string) (Nip34Config, error) { var localConfig Nip34Config - // TODO: the baseDir should inspect parents until we reach the directory that has the ".git" + // find git root + gitRoot := findGitRoot(baseDir) + if gitRoot == "" { + return localConfig, fmt.Errorf("not in a git repository") + } - data, err := os.ReadFile(filepath.Join(baseDir, "nip34.json")) + data, err := os.ReadFile(filepath.Join(gitRoot, "nip34.json")) if err != nil { return localConfig, fmt.Errorf("failed to read nip34.json: %w (run 'nak git init' first)", err) } if err := json.Unmarshal(data, &localConfig); err != nil { return localConfig, fmt.Errorf("failed to parse nip34.json: %w", err) } + + // normalize grasp relay URLs + for i := range localConfig.GraspServers { + localConfig.GraspServers[i] = nostr.NormalizeURL(localConfig.GraspServers[i]) + } + return localConfig, nil } -func excludeNip34ConfigFile() { - // TODO: inspect parents until we reach the directory that has the ".git" +func excludeNip34ConfigFile(baseDir string) { + // find git root + gitRoot := findGitRoot(baseDir) + if gitRoot == "" { + log(color.YellowString("not in a git repository, skipping exclude\n")) + return + } - excludePath := ".git/info/exclude" + excludePath := filepath.Join(gitRoot, ".git", "info", "exclude") excludeContent, err := os.ReadFile(excludePath) if err != nil { // file doesn't exist, create it @@ -1105,17 +1163,175 @@ func excludeNip34ConfigFile() { } func writeNip34ConfigFile(baseDir string, cfg Nip34Config) error { - // TODO: baseDir should inspect parents until we reach the directory that has the ".git" + // find git root (or use baseDir if it doesn't have .git yet, for initial setup) + gitRoot := findGitRoot(baseDir) + if gitRoot == "" { + // not in a git repo yet, use the provided baseDir + if baseDir == "" { + var err error + baseDir, err = os.Getwd() + if err != nil { + return fmt.Errorf("failed to get current directory: %w", err) + } + } + gitRoot = baseDir + } data, err := json.MarshalIndent(cfg, "", " ") if err != nil { return fmt.Errorf("failed to marshal nip34.json: %w", err) } - configPath := filepath.Join(baseDir, "nip34.json") + configPath := filepath.Join(gitRoot, "nip34.json") if err := os.WriteFile(configPath, data, 0644); err != nil { return fmt.Errorf("failed to write %s: %w", configPath, err) } return nil } + +func parseRepositoryAddress( + ctx context.Context, + address string, +) (owner nostr.PubKey, identifier string, relayHints []string, err error) { + // format 1: naddr1... (NIP-19 address pointer) + if strings.HasPrefix(address, "naddr1") { + prefix, data, err := nip19.Decode(address) + if err != nil { + return nostr.PubKey{}, "", nil, fmt.Errorf("invalid naddr: %w", err) + } + if prefix != "naddr" { + return nostr.PubKey{}, "", nil, fmt.Errorf("expected naddr, got %s", prefix) + } + ptr := data.(nostr.EntityPointer) + return ptr.PublicKey, ptr.Identifier, ptr.Relays, nil + } + + // format 2: nostr://// (ngit-style) + if strings.HasPrefix(address, "nostr://") { + parts := strings.Split(address, "/") + if len(parts) != 5 { + return nostr.PubKey{}, "", nil, fmt.Errorf( + "invalid nostr URL format, expected nostr:////, got: %s", address, + ) + } + + prefix, data, err := nip19.Decode(parts[2]) + if err != nil { + return nostr.PubKey{}, "", nil, fmt.Errorf("invalid owner public key: %w", err) + } + if prefix != "npub" { + return nostr.PubKey{}, "", nil, fmt.Errorf("expected npub in URL") + } + owner = data.(nostr.PubKey) + relayHost := parts[3] + identifier = parts[4] + + // construct relay hint from hostname + if strings.HasPrefix(relayHost, "wss:") || strings.HasPrefix(relayHost, "ws:") { + relayHints = []string{relayHost} + } else { + relayHints = []string{"wss://" + relayHost} + } + + return owner, identifier, relayHints, nil + } + + // format 3: / + parts := strings.SplitN(address, "/", 2) + if len(parts) != 2 { + return nostr.PubKey{}, "", nil, fmt.Errorf( + "invalid repository address format, expected /, got: %s", address, + ) + } + + ownerPart := parts[0] + identifier = parts[1] + + // try to parse as pubkey (npub, nprofile, or hex) + owner, err = parsePubKey(ownerPart) + if err != nil { + return nostr.PubKey{}, "", nil, fmt.Errorf("invalid owner identifier '%s': %w", ownerPart, err) + } + + // if it was an nprofile, extract relays + if strings.HasPrefix(ownerPart, "nprofile") { + if _, data, err := nip19.Decode(ownerPart); err == nil { + if profile, ok := data.(nostr.ProfilePointer); ok { + relayHints = profile.Relays + } + } + } + + return owner, identifier, relayHints, nil +} + +func figureOutBranches(c *cli.Command, refspec string, isPush bool) ( + localBranch string, + remoteBranch string, + err error, +) { + var src, dst string + + // parse refspec if provided + if refspec != "" && strings.Contains(refspec, ":") { + parts := strings.Split(refspec, ":") + if len(parts) == 2 { + src = parts[0] + dst = parts[1] + } else { + return "", "", fmt.Errorf("invalid branch spec: %s", refspec) + } + } else if refspec != "" { + src = refspec + } + + // assign src/dst to local/remote based on push vs pull + if isPush { + if src != "" { + localBranch = src + } + if dst != "" { + remoteBranch = dst + } + } else { + if src != "" { + remoteBranch = src + } + if dst != "" { + localBranch = dst + } + } + + // get current branch if not specified + if localBranch == "" { + cmd := exec.Command("git", "rev-parse", "--abbrev-ref", "HEAD") + output, err := cmd.Output() + if err != nil { + return "", "", fmt.Errorf("failed to get current branch: %w", err) + } + localBranch = strings.TrimSpace(string(output)) + } + + // get the remote branch from git config if not specified + if remoteBranch == "" { + cmd := exec.Command("git", "config", "--get", fmt.Sprintf("branch.%s.merge", localBranch)) + output, err := cmd.Output() + if err == nil { + // parse refs/heads/ to get just the branch name + mergeRef := strings.TrimSpace(string(output)) + if strings.HasPrefix(mergeRef, "refs/heads/") { + remoteBranch = strings.TrimPrefix(mergeRef, "refs/heads/") + } else { + remoteBranch = mergeRef + } + } + + if remoteBranch == "" { + // no upstream configured, assume same branch name + remoteBranch = localBranch + } + } + + return localBranch, remoteBranch, nil +} diff --git a/go.mod b/go.mod index ada5a43..5133ab0 100644 --- a/go.mod +++ b/go.mod @@ -62,6 +62,8 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rs/cors v1.11.1 // indirect github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 // indirect + github.com/templexxx/cpu v0.0.1 // indirect + github.com/templexxx/xhex v0.0.0-20200614015412-aed53437177b // indirect github.com/tetratelabs/wazero v1.8.0 // indirect github.com/tidwall/gjson v1.18.0 // indirect github.com/tidwall/match v1.2.0 // indirect @@ -78,3 +80,5 @@ require ( gopkg.in/yaml.v3 v3.0.1 // indirect rsc.io/qr v0.2.0 // indirect ) + +replace fiatjaf.com/nostr => ../nostrlib diff --git a/go.sum b/go.sum index d5c5862..38305ae 100644 --- a/go.sum +++ b/go.sum @@ -174,6 +174,10 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +github.com/templexxx/cpu v0.0.1 h1:hY4WdLOgKdc8y13EYklu9OUTXik80BkxHoWvTO6MQQY= +github.com/templexxx/cpu v0.0.1/go.mod h1:w7Tb+7qgcAlIyX4NhLuDKt78AHA5SzPmq0Wj6HiEnnk= +github.com/templexxx/xhex v0.0.0-20200614015412-aed53437177b h1:XeDLE6c9mzHpdv3Wb1+pWBaWv/BlHK0ZYIu/KaL6eHg= +github.com/templexxx/xhex v0.0.0-20200614015412-aed53437177b/go.mod h1:7rwmCH0wC2fQvNEvPZ3sKXukhyCTyiaZ5VTZMQYpZKQ= github.com/tetratelabs/wazero v1.8.0 h1:iEKu0d4c2Pd+QSRieYbnQC9yiFlMS9D+Jr0LsRmcF4g= github.com/tetratelabs/wazero v1.8.0/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= diff --git a/helpers.go b/helpers.go index c59aa2d..cda3e96 100644 --- a/helpers.go +++ b/helpers.go @@ -18,6 +18,7 @@ import ( "time" "fiatjaf.com/nostr" + "fiatjaf.com/nostr/nip05" "fiatjaf.com/nostr/nip19" "fiatjaf.com/nostr/nip42" "fiatjaf.com/nostr/sdk" @@ -465,6 +466,17 @@ func askConfirmation(msg string) bool { } func parsePubKey(value string) (nostr.PubKey, error) { + // try nip05 first + if nip05.IsValidIdentifier(value) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) + pp, err := nip05.QueryIdentifier(ctx, value) + cancel() + if err == nil { + return pp.PublicKey, nil + } + // if nip05 fails, fall through to try as pubkey + } + pk, err := nostr.PubKeyFromHex(value) if err == nil { return pk, nil From 26fc7c338a76c14feeaaca6114912e9d44697080 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sun, 23 Nov 2025 21:32:33 -0300 Subject: [PATCH 342/401] git: nip34.json into repository object helpers. --- git.go | 148 ++++++++++++++++++++++++------------------------- helpers_key.go | 2 +- 2 files changed, 75 insertions(+), 75 deletions(-) diff --git a/git.go b/git.go index 3b62dec..363154d 100644 --- a/git.go +++ b/git.go @@ -29,11 +29,55 @@ type Nip34Config struct { Maintainers []string `json:"maintainers"` } -type nostrRemote struct { - formatted string - owner nostr.PubKey - identifier string - relayHost string +func (localConfig Nip34Config) Validate() error { + _, err := parsePubKey(localConfig.Owner) + if err != nil { + return fmt.Errorf("owner pubkey '%s' is not valid: %w", localConfig.Owner, err) + } + + for _, maintainer := range localConfig.Maintainers { + _, err := parsePubKey(maintainer) + if err != nil { + return fmt.Errorf("maintainer pubkey '%s' is not valid: %w", maintainer, err) + } + } + + return nil +} + +func (localConfig Nip34Config) ToRepository() nip34.Repository { + owner, err := parsePubKey(localConfig.Owner) + if err != nil { + panic(err) + } + + localRepo := nip34.Repository{ + ID: localConfig.Identifier, + Name: localConfig.Name, + Description: localConfig.Description, + Web: localConfig.Web, + EarliestUniqueCommitID: localConfig.EarliestUniqueCommit, + Maintainers: []nostr.PubKey{}, + Event: nostr.Event{ + PubKey: owner, + }, + } + for _, server := range localConfig.GraspServers { + graspServerURL := nostr.NormalizeURL(server) + url := fmt.Sprintf("http%s/%s/%s.git", + graspServerURL[2:], nip19.EncodeNpub(localRepo.PubKey), localConfig.Identifier) + localRepo.Clone = append(localRepo.Clone, url) + localRepo.Relays = append(localRepo.Relays, graspServerURL) + } + for _, maintainer := range localConfig.Maintainers { + pk, err := parsePubKey(maintainer) + if err != nil { + panic(err) + } + localRepo.Maintainers = append(localRepo.Maintainers, pk) + } + + return localRepo } var git = &cli.Command{ @@ -206,6 +250,10 @@ var gitInit = &cli.Command{ } } + if err := config.Validate(); err != nil { + return fmt.Errorf("invalid config: %w", err) + } + // write config file if err := writeNip34ConfigFile("", config); err != nil { return err @@ -213,34 +261,8 @@ var gitInit = &cli.Command{ log("created %s\n", color.GreenString("nip34.json")) - // parse owner to npub - owner, err := parsePubKey(config.Owner) - if err != nil { - return fmt.Errorf("invalid owner public key: %w", err) - } - - // convert local config to nip34.Repository for setting up remotes - localRepo := nip34.Repository{ - ID: config.Identifier, - Name: config.Name, - Description: config.Description, - Web: config.Web, - EarliestUniqueCommitID: config.EarliestUniqueCommit, - Maintainers: []nostr.PubKey{}, - Event: nostr.Event{PubKey: owner}, - } - for _, server := range config.GraspServers { - graspRelayURL := nostr.NormalizeURL(server) - localRepo.Relays = append(localRepo.Relays, graspRelayURL) - } - for _, maintainer := range config.Maintainers { - if pk, err := parsePubKey(maintainer); err == nil { - localRepo.Maintainers = append(localRepo.Maintainers, pk) - } - } - // setup git remotes - gitSetupRemotes(ctx, "", localRepo) + gitSetupRemotes(ctx, "", config.ToRepository()) // gitignore it excludeNip34ConfigFile("") @@ -399,6 +421,10 @@ var gitClone = &cli.Command{ localConfig.Maintainers = append(localConfig.Maintainers, nip19.EncodeNpub(m)) } + if err := localConfig.Validate(); err != nil { + return fmt.Errorf("invalid config: %w", err) + } + // write nip34.json if err := writeNip34ConfigFile(targetDir, localConfig); err != nil { return err @@ -781,48 +807,14 @@ func syncRepository(ctx context.Context, signer nostr.Signer) (nip34.Repository, if err != nil { logverbose("failed to fetch repository metadata: %v\n", err) // create a local repository object from config - repo = nip34.Repository{ - ID: localConfig.Identifier, - Name: localConfig.Name, - Description: localConfig.Description, - Web: localConfig.Web, - EarliestUniqueCommitID: localConfig.EarliestUniqueCommit, - Event: nostr.Event{PubKey: owner}, - Maintainers: []nostr.PubKey{}, - } - for _, server := range localConfig.GraspServers { - graspRelayURL := nostr.NormalizeURL(server) - repo.Relays = append(repo.Relays, graspRelayURL) - } - for _, maintainer := range localConfig.Maintainers { - if pk, err := parsePubKey(maintainer); err == nil { - repo.Maintainers = append(repo.Maintainers, pk) - } - } + repo = localConfig.ToRepository() } else { // check if local config differs from remote announcement // construct local repo from config for comparison - localRepo := nip34.Repository{ - ID: localConfig.Identifier, - Name: localConfig.Name, - Description: localConfig.Description, - Web: localConfig.Web, - EarliestUniqueCommitID: localConfig.EarliestUniqueCommit, - Maintainers: []nostr.PubKey{}, - Event: nostr.Event{PubKey: owner}, - } - for _, server := range localConfig.GraspServers { - graspRelayURL := nostr.NormalizeURL(server) - localRepo.Relays = append(localRepo.Relays, graspRelayURL) - } - for _, maintainer := range localConfig.Maintainers { - if pk, err := parsePubKey(maintainer); err == nil { - localRepo.Maintainers = append(localRepo.Maintainers, pk) - } - } + localRepo := localConfig.ToRepository() + // check if we need to update local config or publish new announcement if !repo.Equals(localRepo) { - // check if we need to update local config or publish new announcement // check modification times configPath := filepath.Join(findGitRoot(""), "nip34.json") if fi, err := os.Stat(configPath); err == nil { @@ -832,7 +824,7 @@ func syncRepository(ctx context.Context, signer nostr.Signer) (nip34.Repository, if configModTime.After(announcementTime) { // local config is newer, publish new announcement if signer is available if signer != nil { - log("local configuration is newer, publishing updated announcement...\n") + log("local configuration is newer, publishing updated repository announcement...\n") // prepare clone URLs for _, server := range localConfig.GraspServers { graspRelayURL := nostr.NormalizeURL(server) @@ -861,7 +853,7 @@ func syncRepository(ctx context.Context, signer nostr.Signer) (nip34.Repository, } } else { // remote is newer, update local config - log("remote announcement is newer, updating local configuration...\n") + log("remote announcement is newer than local, updating local configuration...\n") localConfig.Name = repo.Name localConfig.Description = repo.Description localConfig.Web = repo.Web @@ -944,14 +936,18 @@ func gitSetupRemotes(ctx context.Context, dir string, repo nip34.Repository) { // construct the git URL gitURL := fmt.Sprintf("http%s/%s/%s.git", - relayURL[2:], nip19.EncodeNpub(repo.Event.PubKey), repo.ID) + relayURL[2:], nip19.EncodeNpub(repo.PubKey), repo.ID) addCmd := exec.Command("git", "remote", "add", remoteName, gitURL) if dir != "" { addCmd.Dir = dir } - if err := addCmd.Run(); err != nil { - logverbose("failed to add remote %s: %v\n", remoteName, err) + if out, err := addCmd.Output(); err != nil { + var stderr string + if exiterr, ok := err.(*exec.ExitError); ok { + stderr = string(exiterr.Stderr) + } + logverbose("failed to add remote %s: %s %s\n", remoteName, stderr, string(out)) } } } @@ -1129,6 +1125,10 @@ func readNip34ConfigFile(baseDir string) (Nip34Config, error) { localConfig.GraspServers[i] = nostr.NormalizeURL(localConfig.GraspServers[i]) } + if err := localConfig.Validate(); err != nil { + return localConfig, fmt.Errorf("nip34.json is invalid: %w", err) + } + return localConfig, nil } diff --git a/helpers_key.go b/helpers_key.go index c578f77..466025c 100644 --- a/helpers_key.go +++ b/helpers_key.go @@ -77,7 +77,7 @@ func gatherSecretKeyOrBunkerFromArguments(ctx context.Context, c *cli.Command) ( clientKey = nostr.Generate() } - logverbose("[nip46]: connecting to %s with client key %s", bunkerURL, clientKey.Hex()) + logverbose("[nip46]: connecting to %s with client key %s\n", bunkerURL, clientKey.Hex()) bunker, err := nip46.ConnectBunker(ctx, clientKey, bunkerURL, nil, func(s string) { log(color.CyanString("[nip46]: open the following URL: %s"), s) From 75c1a883332b8f98960a9e50ce960c69387a712a Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sun, 23 Nov 2025 21:33:19 -0300 Subject: [PATCH 343/401] git: push needed to update refs from the state after pushing. --- git.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/git.go b/git.go index 363154d..4c0fcb0 100644 --- a/git.go +++ b/git.go @@ -588,6 +588,8 @@ var gitPush = &cli.Command{ return fmt.Errorf("failed to push to any remote") } + gitUpdateRefs(ctx, "", state) + return nil }, } From 9f8679591ea865b0445e4ba7f73cc15673cfcd2a Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sun, 23 Nov 2025 21:33:32 -0300 Subject: [PATCH 344/401] git: remove unused gitAnnounce. --- git.go | 115 --------------------------------------------------------- 1 file changed, 115 deletions(-) diff --git a/git.go b/git.go index 4c0fcb0..251f8d0 100644 --- a/git.go +++ b/git.go @@ -665,121 +665,6 @@ var gitFetch = &cli.Command{ }, } -var gitAnnounce = &cli.Command{ - Name: "announce", - Usage: "announce repository to Nostr", - Flags: defaultKeyFlags, - Action: func(ctx context.Context, c *cli.Command) error { - // check if current directory is a git repository - cmd := exec.Command("git", "rev-parse", "--git-dir") - if err := cmd.Run(); err != nil { - return fmt.Errorf("current directory is not a git repository") - } - - // read nip34.json configuration - localConfig, err := readNip34ConfigFile("") - if err != nil { - return err - } - - // parse owner - owner, err := parsePubKey(localConfig.Owner) - if err != nil { - return fmt.Errorf("invalid owner public key: %w", err) - } - - // setup signer - kr, _, err := gatherKeyerFromArguments(ctx, c) - if err != nil { - return fmt.Errorf("failed to gather keyer: %w", err) - } - currentPk, _ := kr.GetPublicKey(ctx) - - // current signer must match owner otherwise we can't announce - if currentPk != owner { - return fmt.Errorf("current user is not the owner of this repository, can't announce") - } - - // convert local config to nip34.Repository - localRepo := nip34.Repository{ - ID: localConfig.Identifier, - Name: localConfig.Name, - Description: localConfig.Description, - Web: localConfig.Web, - EarliestUniqueCommitID: localConfig.EarliestUniqueCommit, - Maintainers: []nostr.PubKey{}, - } - for _, server := range localConfig.GraspServers { - graspRelayURL := nostr.NormalizeURL(server) - url := fmt.Sprintf("http%s/%s/%s.git", - graspRelayURL[2:], nip19.EncodeNpub(owner), localConfig.Identifier) - localRepo.Clone = append(localRepo.Clone, url) - localRepo.Relays = append(localRepo.Relays, graspRelayURL) - } - for _, maintainer := range localConfig.Maintainers { - if pk, err := parsePubKey(maintainer); err == nil { - localRepo.Maintainers = append(localRepo.Maintainers, pk) - } else { - log(color.YellowString("invalid maintainer pubkey '%s': %v\n", maintainer, err)) - } - } - - // these are the relays where we'll publish the announcement to - relays := append(sys.FetchOutboxRelays(ctx, owner, 3), localConfig.GraspServers...) - for i := range relays { - relays[i] = nostr.NormalizeURL(relays[i]) - } - - // fetch repository announcement (30617) events - oks := make([]bool, len(relays)) - var repo nip34.Repository - results := sys.Pool.FetchMany(ctx, relays, nostr.Filter{ - Kinds: []nostr.Kind{30617}, - Tags: nostr.TagMap{ - "d": []string{localConfig.Identifier}, - }, - Limit: 1, - }, nostr.SubscriptionOptions{ - Label: "nak-git-announce", - CheckDuplicate: func(id nostr.ID, relay string) bool { return false }, // get the same event from multiple relays - }) - for ie := range results { - repo = nip34.ParseRepository(ie.Event) - - // check if this is ok or the announcement in this relay needs to be updated - if repo.Equals(localRepo) { - relayIdx := slices.Index(relays, ie.Relay.URL) - oks[relayIdx] = true - } - } - - // publish repository announcement if needed - if slices.Contains(oks, false) { - announcementEvent := localRepo.ToEvent() - if err := kr.SignEvent(ctx, &announcementEvent); err != nil { - return fmt.Errorf("failed to sign announcement event: %w", err) - } - - targets := make([]string, 0, len(oks)) - for i, ok := range oks { - if !ok { - targets = append(targets, relays[i]) - } - } - log("- publishing repository announcement to " + color.CyanString("%v", targets) + "\n") - for res := range sys.Pool.PublishMany(ctx, targets, announcementEvent) { - if res.Error != nil { - log("! error publishing announcement to relay %s: %v\n", color.YellowString(res.RelayURL), res.Error) - } else { - log("> published announcement to relay %s\n", color.GreenString(res.RelayURL)) - } - } - } - - return nil - }, -} - var gitSync = &cli.Command{ Name: "sync", Usage: "sync repository with relays", From 11a690b1c6cee1c9b6162bbc984696a29196b433 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Mon, 24 Nov 2025 06:37:42 -0300 Subject: [PATCH 345/401] git: fix sync publishing wrong repo event and always being mismatched. --- git.go | 48 +++++++++++++++++++++++------------------------- go.mod | 4 +--- go.sum | 17 ++++++++++++----- 3 files changed, 36 insertions(+), 33 deletions(-) diff --git a/git.go b/git.go index 251f8d0..eec2002 100644 --- a/git.go +++ b/git.go @@ -8,7 +8,6 @@ import ( "path/filepath" "slices" "strings" - "time" "fiatjaf.com/nostr" "fiatjaf.com/nostr/nip19" @@ -676,7 +675,7 @@ var gitSync = &cli.Command{ }, } -func syncRepository(ctx context.Context, signer nostr.Signer) (nip34.Repository, nip34.RepositoryState, error) { +func syncRepository(ctx context.Context, signer nostr.Keyer) (nip34.Repository, nip34.RepositoryState, error) { // read current nip34.json localConfig, err := readNip34ConfigFile("") if err != nil { @@ -706,35 +705,34 @@ func syncRepository(ctx context.Context, signer nostr.Signer) (nip34.Repository, configPath := filepath.Join(findGitRoot(""), "nip34.json") if fi, err := os.Stat(configPath); err == nil { configModTime := fi.ModTime() - announcementTime := time.Unix(int64(repo.Event.CreatedAt), 0) + announcementTime := repo.Event.CreatedAt.Time() if configModTime.After(announcementTime) { - // local config is newer, publish new announcement if signer is available + // local config is newer, publish new announcement if signer is available and matches owner if signer != nil { - log("local configuration is newer, publishing updated repository announcement...\n") - // prepare clone URLs - for _, server := range localConfig.GraspServers { - graspRelayURL := nostr.NormalizeURL(server) - url := fmt.Sprintf("http%s/%s/%s.git", - graspRelayURL[2:], nip19.EncodeNpub(owner), localConfig.Identifier) - localRepo.Clone = append(localRepo.Clone, url) - localRepo.Relays = append(localRepo.Relays, graspRelayURL) + signerPk, err := signer.GetPublicKey(ctx) + if err != nil { + return repo, state, fmt.Errorf("failed to get signer pubkey: %w", err) } - - announcementEvent := localRepo.ToEvent() - if err := signer.SignEvent(ctx, &announcementEvent); err != nil { - return repo, state, fmt.Errorf("failed to sign announcement: %w", err) - } - - relays := append(sys.FetchOutboxRelays(ctx, owner, 3), localConfig.GraspServers...) - for res := range sys.Pool.PublishMany(ctx, relays, announcementEvent) { - if res.Error != nil { - log("! error publishing to %s: %v\n", color.YellowString(res.RelayURL), res.Error) - } else { - log("> published to %s\n", color.GreenString(res.RelayURL)) + if signerPk != owner { + log("local configuration is newer, but signer pubkey does not match owner, skipping announcement publish\n") + } else { + log("local configuration is newer, publishing updated repository announcement...\n") + announcementEvent := localRepo.ToEvent() + if err := signer.SignEvent(ctx, &announcementEvent); err != nil { + return repo, state, fmt.Errorf("failed to sign announcement: %w", err) } + + relays := append(sys.FetchOutboxRelays(ctx, owner, 3), localConfig.GraspServers...) + for res := range sys.Pool.PublishMany(ctx, relays, announcementEvent) { + if res.Error != nil { + log("! error publishing to %s: %v\n", color.YellowString(res.RelayURL), res.Error) + } else { + log("> published to %s\n", color.GreenString(res.RelayURL)) + } + } + repo = nip34.ParseRepository(announcementEvent) } - repo = nip34.ParseRepository(announcementEvent) } else { log("local configuration is newer than remote, but no signer provided to publish update\n") } diff --git a/go.mod b/go.mod index 5133ab0..ac162ad 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.24.1 require ( fiatjaf.com/lib v0.3.1 - fiatjaf.com/nostr v0.0.0-20251117111008-078e9b4cc257 + fiatjaf.com/nostr v0.0.0-20251124002842-de54dd1fa4b8 github.com/bep/debounce v1.2.1 github.com/btcsuite/btcd/btcec/v2 v2.3.6 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e @@ -80,5 +80,3 @@ require ( gopkg.in/yaml.v3 v3.0.1 // indirect rsc.io/qr v0.2.0 // indirect ) - -replace fiatjaf.com/nostr => ../nostrlib diff --git a/go.sum b/go.sum index 38305ae..cc7bcf4 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ fiatjaf.com/lib v0.3.1 h1:/oFQwNtFRfV+ukmOCxfBEAuayoLwXp4wu2/fz5iHpwA= fiatjaf.com/lib v0.3.1/go.mod h1:Ycqq3+mJ9jAWu7XjbQI1cVr+OFgnHn79dQR5oTII47g= -fiatjaf.com/nostr v0.0.0-20251117111008-078e9b4cc257 h1:u8ah+Cc+5bXZ3qnBf8MRtzyRk6VAdhA0EYTY3+hVejs= -fiatjaf.com/nostr v0.0.0-20251117111008-078e9b4cc257/go.mod h1:pCbBk3hfs5x0+ND8k25mq9e50LEmQpAYMdTUe1M1Rt0= +fiatjaf.com/nostr v0.0.0-20251124002842-de54dd1fa4b8 h1:R16mnlJ3qvVar7G4rzY+Z+mEAf2O6wpHTlRlHAt2Od8= +fiatjaf.com/nostr v0.0.0-20251124002842-de54dd1fa4b8/go.mod h1:QEGyTgAjjTFwDx2BJGZiCdmoAcWA/G+sQy7wDqKzSPU= 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= @@ -96,8 +96,8 @@ github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -121,6 +121,10 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/liamg/magic v0.0.1 h1:Ru22ElY+sCh6RvRTWjQzKKCxsEco8hE0co8n1qe7TBM= @@ -163,6 +167,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg= github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 h1:D0vL7YNisV2yqE55+q0lFuGse6U8lxlg7fYTctlT5Gc= @@ -251,8 +257,9 @@ google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQ google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= From 59edaba5b89e7d260eef25894b20503f38e51171 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Mon, 24 Nov 2025 15:46:44 -0300 Subject: [PATCH 346/401] git: much nicer prompts on "init". --- git.go | 215 +++++++++++++++++++++++++++++++++++++++++---------------- go.mod | 3 + go.sum | 31 +++++++++ 3 files changed, 188 insertions(+), 61 deletions(-) diff --git a/git.go b/git.go index eec2002..bd2dc15 100644 --- a/git.go +++ b/git.go @@ -12,7 +12,7 @@ import ( "fiatjaf.com/nostr" "fiatjaf.com/nostr/nip19" "fiatjaf.com/nostr/nip34" - "github.com/chzyer/readline" + "github.com/AlecAivazis/survey/v2" "github.com/fatih/color" "github.com/urfave/cli/v3" ) @@ -274,76 +274,167 @@ var gitInit = &cli.Command{ }, } -func promptForConfig(config *Nip34Config) error { - rlConfig := &readline.Config{ - Stdout: os.Stderr, - InterruptPrompt: "^C", - DisableAutoSaveHistory: true, +func promptForStringList( + name string, + existing []string, + defaults []string, + normalize func(string) string, + validate func(string) bool, +) ([]string, error) { + options := make([]string, 0, len(defaults)+len(existing)+1) + options = append(options, defaults...) + options = append(options, "add another") + + // add existing not in options + for _, item := range existing { + if !slices.Contains(options, item) { + options = append(options, item) + } } - rl, err := readline.NewEx(rlConfig) + selected := make([]string, len(existing)) + copy(selected, existing) + + for { + prompt := &survey.MultiSelect{ + Message: name, + Options: options, + Default: selected, + PageSize: 20, + } + + if err := survey.AskOne(prompt, &selected); err != nil { + return nil, err + } + + if slices.Contains(selected, "add another") { + selected = slices.DeleteFunc(selected, func(s string) bool { return s == "add another" }) + + singular := name + if strings.HasSuffix(singular, "s") { + singular = singular[:len(singular)-1] + } + + newPrompt := &survey.Input{ + Message: fmt.Sprintf("enter new %s", singular), + } + var newItem string + if err := survey.AskOne(newPrompt, &newItem); err != nil { + return nil, err + } + + if newItem != "" { + if normalize != nil { + newItem = normalize(newItem) + } + if validate != nil && !validate(newItem) { + // invalid, ask again + continue + } + + if !slices.Contains(options, newItem) { + options = append(options, newItem) + // swap to put "add another" at end + options[len(options)-1], options[len(options)-2] = options[len(options)-2], options[len(options)-1] + } + if !slices.Contains(selected, newItem) { + selected = append(selected, newItem) + } + } + } else { + break + } + } + + return selected, nil +} + +func promptForConfig(config *Nip34Config) error { + log("\nenter repository details (use arrow keys to navigate, space to select/deselect, enter to confirm):\n\n") + + // prompt for identifier + identifierPrompt := &survey.Input{ + Message: "identifier", + Default: config.Identifier, + } + if err := survey.AskOne(identifierPrompt, &config.Identifier); err != nil { + return err + } + + // prompt for name + namePrompt := &survey.Input{ + Message: "name", + Default: config.Name, + } + if err := survey.AskOne(namePrompt, &config.Name); err != nil { + return err + } + + // prompt for description + descPrompt := &survey.Input{ + Message: "description", + Default: config.Description, + } + if err := survey.AskOne(descPrompt, &config.Description); err != nil { + return err + } + + // prompt for owner + for { + ownerPrompt := &survey.Input{ + Message: "owner (npub or hex)", + Default: config.Owner, + } + if err := survey.AskOne(ownerPrompt, &config.Owner); err != nil { + return err + } + if pubkey, err := parsePubKey(config.Owner); err == nil { + config.Owner = pubkey.Hex() + break + } + } + + // prompt for grasp servers + graspServers, err := promptForStringList("grasp servers", config.GraspServers, []string{ + "gitnostr.com", + "relay.ngit.dev", + "pyramid.fiatjaf.com", + "git.shakespeare.dyi", + }, graspServerHost, nil) if err != nil { return err } - defer rl.Close() + config.GraspServers = graspServers - promptString := func(currentVal *string, prompt string) error { - rl.SetPrompt(color.YellowString("%s [%s]: ", prompt, *currentVal)) - answer, err := rl.Readline() + // prompt for web URLs + webURLs, err := promptForStringList("web URLs", config.Web, []string{ + fmt.Sprintf("https://gitworkshop.dev/%s/%s", + nip19.EncodeNpub(nostr.MustPubKeyFromHex(config.Owner)), + config.Identifier, + ), + }, func(s string) string { + return "http" + nostr.NormalizeURL(s)[2:] + }, nil) + if err != nil { + return err + } + config.Web = webURLs + + // Prompt for maintainers + maintainers, err := promptForStringList("maintainers", config.Maintainers, []string{}, nil, func(s string) bool { + pk, err := parsePubKey(s) if err != nil { - return err + return false } - answer = strings.TrimSpace(answer) - if answer != "" { - *currentVal = answer + if pk.Hex() == config.Owner { + return false } - return nil - } - - promptSlice := func(currentVal *[]string, prompt string) error { - defaultStr := strings.Join(*currentVal, ", ") - rl.SetPrompt(color.YellowString("%s (comma-separated) [%s]: ", prompt, defaultStr)) - answer, err := rl.Readline() - if err != nil { - return err - } - answer = strings.TrimSpace(answer) - if answer != "" { - parts := strings.Split(answer, ",") - result := make([]string, 0, len(parts)) - for _, p := range parts { - if trimmed := strings.TrimSpace(p); trimmed != "" { - result = append(result, trimmed) - } - } - *currentVal = result - } - return nil - } - - log("\nenter repository details (press Enter to keep default):\n\n") - - if err := promptString(&config.Identifier, "identifier"); err != nil { - return err - } - if err := promptString(&config.Name, "name"); err != nil { - return err - } - if err := promptString(&config.Description, "description"); err != nil { - return err - } - if err := promptString(&config.Owner, "owner (npub or hex)"); err != nil { - return err - } - if err := promptSlice(&config.GraspServers, "grasp servers"); err != nil { - return err - } - if err := promptSlice(&config.Web, "web URLs"); err != nil { - return err - } - if err := promptSlice(&config.Maintainers, "other maintainers"); err != nil { + return true + }) + if err != nil { return err } + config.Maintainers = maintainers log("\n") return nil @@ -1007,7 +1098,7 @@ func readNip34ConfigFile(baseDir string) (Nip34Config, error) { // normalize grasp relay URLs for i := range localConfig.GraspServers { - localConfig.GraspServers[i] = nostr.NormalizeURL(localConfig.GraspServers[i]) + localConfig.GraspServers[i] = graspServerHost(localConfig.GraspServers[i]) } if err := localConfig.Validate(); err != nil { @@ -1220,3 +1311,5 @@ func figureOutBranches(c *cli.Command, refspec string, isPush bool) ( return localBranch, remoteBranch, nil } + +func graspServerHost(s string) string { return strings.SplitN(nostr.NormalizeURL(s), "/", 3)[2] } diff --git a/go.mod b/go.mod index ac162ad..4afffbf 100644 --- a/go.mod +++ b/go.mod @@ -28,6 +28,7 @@ require ( ) require ( + github.com/AlecAivazis/survey/v2 v2.3.7 // indirect 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 @@ -53,10 +54,12 @@ require ( github.com/hablullah/go-juliandays v1.0.0 // indirect github.com/jalaali/go-jalaali v0.0.0-20210801064154-80525e88d958 // indirect github.com/josharian/intern v1.0.0 // indirect + github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/klauspost/compress v1.18.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/magefile/mage v1.14.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect diff --git a/go.sum b/go.sum index cc7bcf4..7823daf 100644 --- a/go.sum +++ b/go.sum @@ -2,10 +2,13 @@ fiatjaf.com/lib v0.3.1 h1:/oFQwNtFRfV+ukmOCxfBEAuayoLwXp4wu2/fz5iHpwA= fiatjaf.com/lib v0.3.1/go.mod h1:Ycqq3+mJ9jAWu7XjbQI1cVr+OFgnHn79dQR5oTII47g= fiatjaf.com/nostr v0.0.0-20251124002842-de54dd1fa4b8 h1:R16mnlJ3qvVar7G4rzY+Z+mEAf2O6wpHTlRlHAt2Od8= fiatjaf.com/nostr v0.0.0-20251124002842-de54dd1fa4b8/go.mod h1:QEGyTgAjjTFwDx2BJGZiCdmoAcWA/G+sQy7wDqKzSPU= +github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ= +github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= 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/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= 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= @@ -53,6 +56,7 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWs github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g= github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= +github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -108,6 +112,7 @@ github.com/hablullah/go-juliandays v1.0.0 h1:A8YM7wIj16SzlKT0SRJc9CD29iiaUzpBLzh github.com/hablullah/go-juliandays v1.0.0/go.mod h1:0JOYq4oFOuDja+oospuc61YoX+uNEn7Z6uHYTbBzdGc= github.com/hanwen/go-fuse/v2 v2.7.2 h1:SbJP1sUP+n1UF8NXBA14BuojmTez+mDgOk0bC057HQw= github.com/hanwen/go-fuse/v2 v2.7.2/go.mod h1:ugNaD/iv5JYyS1Rcvi57Wz7/vrLQJo10mmketmoef48= +github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/jalaali/go-jalaali v0.0.0-20210801064154-80525e88d958 h1:qxLoi6CAcXVzjfvu+KXIXJOAsQB62LXjsfbOaErsVzE= github.com/jalaali/go-jalaali v0.0.0-20210801064154-80525e88d958/go.mod h1:Wqfu7mjUHj9WDzSSPI5KfBclTTEnLveRUFr/ujWnTgE= @@ -118,6 +123,8 @@ github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFF github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= @@ -137,14 +144,18 @@ github.com/mark3labs/mcp-go v0.8.3 h1:IzlyN8BaP4YwUMUDqxOGJhGdZXEDQiAPX43dNPgnzr github.com/mark3labs/mcp-go v0.8.3/go.mod h1:cjMlBU0cv/cj9kjlgmRhoJ5JREdS7YX83xeIG9Ko/jE= github.com/markusmobius/go-dateparser v1.2.3 h1:TvrsIvr5uk+3v6poDjaicnAFJ5IgtFHgLiuMY2Eb7Nw= github.com/markusmobius/go-dateparser v1.2.3/go.mod h1:cMwQRrBUQlK1UI5TIFHEcvpsMbkWrQLXuaPNMFzuYLk= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-tty v0.0.7 h1:KJ486B6qI8+wBO7kQxYgmmEFDaFEE96JMBQ7h400N8Q= github.com/mattn/go-tty v0.0.7/go.mod h1:f2i5ZOvXBU/tCABmLmOfzLz9azMo5wdAaElRNnJKr+k= github.com/mdp/qrterminal/v3 v3.2.1 h1:6+yQjiiOsSuXT5n9/m60E54vdgFsw0zhADHhHLrFet4= github.com/mdp/qrterminal/v3 v3.2.1/go.mod h1:jOTmXvnBsMy5xqLniO0R++Jmjs2sTm9dFSuQ5kpz/SU= +github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= +github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78= github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -210,27 +221,36 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.etcd.io/bbolt v1.4.2 h1:IrUHp260R8c+zYx/Tm8QZr04CX+qWS5PGfPdevhdm1I= go.etcd.io/bbolt v1.4.2/go.mod h1:Is8rSHO/b4f3XigBC0lL0+4FwAQv3HXEEIgFMuKHceM= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 h1:zfMcR1Cs4KNuomFFgGefv5N0czO2XZpUbxGUy8i8ug0= golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 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-20190222072716-a9d3bda3a223/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-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -238,17 +258,28 @@ golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= From c3cb59a94aed646d91e19e6e4d0b0693072269ce Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Mon, 24 Nov 2025 18:26:00 -0300 Subject: [PATCH 347/401] git: move things around, allow for nil state as a possible value, fix syncing when repository is not announced yet. --- git.go | 1145 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 576 insertions(+), 569 deletions(-) diff --git a/git.go b/git.go index bd2dc15..343c3ba 100644 --- a/git.go +++ b/git.go @@ -17,68 +17,6 @@ import ( "github.com/urfave/cli/v3" ) -type Nip34Config struct { - Identifier string `json:"identifier"` - Name string `json:"name"` - Description string `json:"description"` - Web []string `json:"web"` - Owner string `json:"owner"` - GraspServers []string `json:"grasp-servers"` - EarliestUniqueCommit string `json:"earliest-unique-commit"` - Maintainers []string `json:"maintainers"` -} - -func (localConfig Nip34Config) Validate() error { - _, err := parsePubKey(localConfig.Owner) - if err != nil { - return fmt.Errorf("owner pubkey '%s' is not valid: %w", localConfig.Owner, err) - } - - for _, maintainer := range localConfig.Maintainers { - _, err := parsePubKey(maintainer) - if err != nil { - return fmt.Errorf("maintainer pubkey '%s' is not valid: %w", maintainer, err) - } - } - - return nil -} - -func (localConfig Nip34Config) ToRepository() nip34.Repository { - owner, err := parsePubKey(localConfig.Owner) - if err != nil { - panic(err) - } - - localRepo := nip34.Repository{ - ID: localConfig.Identifier, - Name: localConfig.Name, - Description: localConfig.Description, - Web: localConfig.Web, - EarliestUniqueCommitID: localConfig.EarliestUniqueCommit, - Maintainers: []nostr.PubKey{}, - Event: nostr.Event{ - PubKey: owner, - }, - } - for _, server := range localConfig.GraspServers { - graspServerURL := nostr.NormalizeURL(server) - url := fmt.Sprintf("http%s/%s/%s.git", - graspServerURL[2:], nip19.EncodeNpub(localRepo.PubKey), localConfig.Identifier) - localRepo.Clone = append(localRepo.Clone, url) - localRepo.Relays = append(localRepo.Relays, graspServerURL) - } - for _, maintainer := range localConfig.Maintainers { - pk, err := parsePubKey(maintainer) - if err != nil { - panic(err) - } - localRepo.Maintainers = append(localRepo.Maintainers, pk) - } - - return localRepo -} - var git = &cli.Command{ Name: "git", Usage: "git-related operations", @@ -89,188 +27,498 @@ aside from those, there is also: - 'nak git sync' for getting the latest metadata update from nostr relays (called automatically by other commands) `, Commands: []*cli.Command{ - gitInit, - gitSync, - gitClone, - gitPush, - gitPull, - gitFetch, - }, -} + { + Name: "init", + Usage: "initialize a nip34 repository configuration", + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "interactive", + Aliases: []string{"i"}, + Usage: "prompt for repository details interactively", + }, + &cli.BoolFlag{ + Name: "force", + Aliases: []string{"f"}, + Usage: "overwrite existing nip34.json file", + }, + &cli.StringFlag{ + Name: "identifier", + Usage: "unique identifier for the repository", + }, + &cli.StringFlag{ + Name: "name", + Usage: "repository name", + }, + &cli.StringFlag{ + Name: "description", + Usage: "repository description", + }, + &cli.StringSliceFlag{ + Name: "web", + Usage: "web URLs for the repository (can be used multiple times)", + }, + &cli.StringFlag{ + Name: "owner", + Usage: "owner public key", + }, + &cli.StringSliceFlag{ + Name: "grasp-servers", + Usage: "grasp servers (can be used multiple times)", + }, + &cli.StringSliceFlag{ + Name: "relays", + Usage: "relay URLs to publish to (can be used multiple times)", + }, + &cli.StringSliceFlag{ + Name: "maintainers", + Usage: "maintainer public keys as npub or hex (can be used multiple times)", + }, + &cli.StringFlag{ + Name: "earliest-unique-commit", + Usage: "earliest unique commit of the repository", + }, + }, + Action: func(ctx context.Context, c *cli.Command) error { + // check if current directory is a git repository + cmd := exec.Command("git", "rev-parse", "--git-dir") + if err := cmd.Run(); err != nil { + // initialize a git repository + log("initializing git repository...\n") + initCmd := exec.Command("git", "init") + initCmd.Stderr = os.Stderr + initCmd.Stdout = os.Stdout + if err := initCmd.Run(); err != nil { + return fmt.Errorf("failed to initialize git repository: %w", err) + } + } -var gitInit = &cli.Command{ - Name: "init", - Usage: "initialize a nip34 repository configuration", - Flags: []cli.Flag{ - &cli.BoolFlag{ - Name: "interactive", - Aliases: []string{"i"}, - Usage: "prompt for repository details interactively", - }, - &cli.BoolFlag{ - Name: "force", - Aliases: []string{"f"}, - Usage: "overwrite existing nip34.json file", - }, - &cli.StringFlag{ - Name: "identifier", - Usage: "unique identifier for the repository", - }, - &cli.StringFlag{ - Name: "name", - Usage: "repository name", - }, - &cli.StringFlag{ - Name: "description", - Usage: "repository description", - }, - &cli.StringSliceFlag{ - Name: "web", - Usage: "web URLs for the repository (can be used multiple times)", - }, - &cli.StringFlag{ - Name: "owner", - Usage: "owner public key", - }, - &cli.StringSliceFlag{ - Name: "grasp-servers", - Usage: "grasp servers (can be used multiple times)", - }, - &cli.StringSliceFlag{ - Name: "relays", - Usage: "relay URLs to publish to (can be used multiple times)", - }, - &cli.StringSliceFlag{ - Name: "maintainers", - Usage: "maintainer public keys as npub or hex (can be used multiple times)", - }, - &cli.StringFlag{ - Name: "earliest-unique-commit", - Usage: "earliest unique commit of the repository", - }, - }, - Action: func(ctx context.Context, c *cli.Command) error { - // check if current directory is a git repository - cmd := exec.Command("git", "rev-parse", "--git-dir") - if err := cmd.Run(); err != nil { - // initialize a git repository - log("initializing git repository...\n") - initCmd := exec.Command("git", "init") - initCmd.Stderr = os.Stderr - initCmd.Stdout = os.Stdout - if err := initCmd.Run(); err != nil { - return fmt.Errorf("failed to initialize git repository: %w", err) - } - } + // check if nip34.json already exists + existingConfig, err := readNip34ConfigFile("") + if err == nil { + // file exists + if !c.Bool("force") && !c.Bool("interactive") { + return fmt.Errorf("nip34.json already exists, use --force to overwrite or --interactive to update") + } + } - // check if nip34.json already exists - existingConfig, err := readNip34ConfigFile("") - if err == nil { - // file exists - if !c.Bool("force") && !c.Bool("interactive") { - return fmt.Errorf("nip34.json already exists, use --force to overwrite or --interactive to update") - } - } + // get repository base directory name for defaults + cwd, err := os.Getwd() + if err != nil { + return fmt.Errorf("failed to get current directory: %w", err) + } + baseName := filepath.Base(cwd) - // get repository base directory name for defaults - cwd, err := os.Getwd() - if err != nil { - return fmt.Errorf("failed to get current directory: %w", err) - } - baseName := filepath.Base(cwd) + // get earliest unique commit + var earliestCommit string + if output, err := exec.Command("git", "rev-list", "--max-parents=0", "HEAD").Output(); err == nil { + earliest := strings.Split(strings.TrimSpace(string(output)), "\n") + if len(earliest) > 0 { + earliestCommit = earliest[0] + } + } - // get earliest unique commit - var earliestCommit string - if output, err := exec.Command("git", "rev-list", "--max-parents=0", "HEAD").Output(); err == nil { - earliest := strings.Split(strings.TrimSpace(string(output)), "\n") - if len(earliest) > 0 { - earliestCommit = earliest[0] - } - } - - // extract clone URLs from nostr:// git remotes - // (this is just for migrating from ngit) - var defaultCloneURLs []string - if output, err := exec.Command("git", "remote", "-v").Output(); err == nil { - remotes := strings.Split(strings.TrimSpace(string(output)), "\n") - for _, remote := range remotes { - if strings.Contains(remote, "nostr://") { - parts := strings.Fields(remote) - if len(parts) >= 2 { - nostrURL := parts[1] - // parse nostr://npub.../relay_hostname/identifier - if owner, identifier, relays, err := parseRepositoryAddress(ctx, nostrURL); err == nil && len(relays) > 0 { - relayURL := relays[0] - // convert to https://relay_hostname/npub.../identifier.git - cloneURL := fmt.Sprintf("http%s/%s/%s.git", - relayURL[2:], nip19.EncodeNpub(owner), identifier) - defaultCloneURLs = appendUnique(defaultCloneURLs, cloneURL) + // extract clone URLs from nostr:// git remotes + // (this is just for migrating from ngit) + var defaultCloneURLs []string + if output, err := exec.Command("git", "remote", "-v").Output(); err == nil { + remotes := strings.Split(strings.TrimSpace(string(output)), "\n") + for _, remote := range remotes { + if strings.Contains(remote, "nostr://") { + parts := strings.Fields(remote) + if len(parts) >= 2 { + nostrURL := parts[1] + // parse nostr://npub.../relay_hostname/identifier + if owner, identifier, relays, err := parseRepositoryAddress(ctx, nostrURL); err == nil && len(relays) > 0 { + relayURL := relays[0] + // convert to https://relay_hostname/npub.../identifier.git + cloneURL := fmt.Sprintf("http%s/%s/%s.git", + relayURL[2:], nip19.EncodeNpub(owner), identifier) + defaultCloneURLs = appendUnique(defaultCloneURLs, cloneURL) + } + } } } } - } - } - // helper to get value from flags, existing config, or default - getValue := func(existingVal, flagVal, defaultVal string) string { - if flagVal != "" { - return flagVal - } - if existingVal != "" { - return existingVal - } - return defaultVal - } + // helper to get value from flags, existing config, or default + getValue := func(existingVal, flagVal, defaultVal string) string { + if flagVal != "" { + return flagVal + } + if existingVal != "" { + return existingVal + } + return defaultVal + } - getSliceValue := func(existingVals, flagVals, defaultVals []string) []string { - if len(flagVals) > 0 { - return flagVals - } - if len(existingVals) > 0 { - return existingVals - } - return defaultVals - } + getSliceValue := func(existingVals, flagVals, defaultVals []string) []string { + if len(flagVals) > 0 { + return flagVals + } + if len(existingVals) > 0 { + return existingVals + } + return defaultVals + } - config := Nip34Config{ - Identifier: getValue(existingConfig.Identifier, c.String("identifier"), baseName), - Name: getValue(existingConfig.Name, c.String("name"), baseName), - Description: getValue(existingConfig.Description, c.String("description"), ""), - Web: getSliceValue(existingConfig.Web, c.StringSlice("web"), []string{}), - Owner: getValue(existingConfig.Owner, c.String("owner"), ""), - GraspServers: getSliceValue(existingConfig.GraspServers, c.StringSlice("grasp-servers"), []string{"gitnostr.com", "relay.ngit.dev"}), - EarliestUniqueCommit: getValue(existingConfig.EarliestUniqueCommit, c.String("earliest-unique-commit"), earliestCommit), - Maintainers: getSliceValue(existingConfig.Maintainers, c.StringSlice("maintainers"), []string{}), - } + config := Nip34Config{ + Identifier: getValue(existingConfig.Identifier, c.String("identifier"), baseName), + Name: getValue(existingConfig.Name, c.String("name"), baseName), + Description: getValue(existingConfig.Description, c.String("description"), ""), + Web: getSliceValue(existingConfig.Web, c.StringSlice("web"), []string{}), + Owner: getValue(existingConfig.Owner, c.String("owner"), ""), + GraspServers: getSliceValue(existingConfig.GraspServers, c.StringSlice("grasp-servers"), []string{"gitnostr.com", "relay.ngit.dev"}), + EarliestUniqueCommit: getValue(existingConfig.EarliestUniqueCommit, c.String("earliest-unique-commit"), earliestCommit), + Maintainers: getSliceValue(existingConfig.Maintainers, c.StringSlice("maintainers"), []string{}), + } - if c.Bool("interactive") { - if err := promptForConfig(&config); err != nil { + if c.Bool("interactive") { + if err := promptForConfig(&config); err != nil { + return err + } + } + + if err := config.Validate(); err != nil { + return fmt.Errorf("invalid config: %w", err) + } + + // write config file + if err := writeNip34ConfigFile("", config); err != nil { + return err + } + + log("created %s\n", color.GreenString("nip34.json")) + + // setup git remotes + gitSetupRemotes(ctx, "", config.ToRepository()) + + // gitignore it + excludeNip34ConfigFile("") + + log("edit %s if needed, then run %s to publish.\n", + color.CyanString("nip34.json"), + color.CyanString("nak git announce")) + + return nil + }, + }, + { + Name: "sync", + Usage: "sync repository with relays", + Flags: defaultKeyFlags, + Action: func(ctx context.Context, c *cli.Command) error { + kr, _, _ := gatherKeyerFromArguments(ctx, c) + _, _, err := gitSync(ctx, kr) return err - } - } + }, + }, + { + Name: "clone", + Usage: "clone a NIP-34 repository from a nostr:// URI", + Description: `the parameter maybe in the form "/", ngit-style like "nostr:////" or an "naddr1..." code.`, + ArgsUsage: " [directory]", + Action: func(ctx context.Context, c *cli.Command) error { + args := c.Args() + if args.Len() == 0 { + return fmt.Errorf("missing repository address") + } - if err := config.Validate(); err != nil { - return fmt.Errorf("invalid config: %w", err) - } + owner, identifier, relayHints, err := parseRepositoryAddress(ctx, args.Get(0)) + if err != nil { + return fmt.Errorf("failed to parse remote url '%s': %s", args.Get(0), err) + } - // write config file - if err := writeNip34ConfigFile("", config); err != nil { - return err - } + // fetch repository metadata and state + repo, state, err := fetchRepositoryAndState(ctx, owner, identifier, relayHints) + if err != nil { + return err + } - log("created %s\n", color.GreenString("nip34.json")) + // determine target directory + targetDir := "" + if args.Len() >= 2 { + targetDir = args.Get(1) + } else { + targetDir = repo.ID + } + if targetDir == "" { + targetDir = repo.ID + } - // setup git remotes - gitSetupRemotes(ctx, "", config.ToRepository()) + // if targetDir exists and is non-empty, bail + if fi, err := os.Stat(targetDir); err == nil && fi.IsDir() { + entries, err := os.ReadDir(targetDir) + if err == nil && len(entries) > 0 { + return fmt.Errorf("target directory '%s' already exists and is not empty", targetDir) + } + } - // gitignore it - excludeNip34ConfigFile("") + // create directory + if err := os.MkdirAll(targetDir, 0755); err != nil { + return fmt.Errorf("failed to create directory '%s': %w", targetDir, err) + } - log("edit %s if needed, then run %s to publish.\n", - color.CyanString("nip34.json"), - color.CyanString("nak git announce")) + // initialize git inside the directory + initCmd := exec.Command("git", "init") + initCmd.Dir = targetDir + if err := initCmd.Run(); err != nil { + return fmt.Errorf("failed to initialize git repository: %w", err) + } - return nil + // write nip34.json inside cloned directory + localConfig := Nip34Config{ + Identifier: repo.ID, + Name: repo.Name, + Description: repo.Description, + Web: repo.Web, + Owner: nip19.EncodeNpub(repo.Event.PubKey), + GraspServers: make([]string, 0, len(repo.Relays)), + EarliestUniqueCommit: repo.EarliestUniqueCommitID, + Maintainers: make([]string, 0, len(repo.Maintainers)), + } + for _, r := range repo.Relays { + localConfig.GraspServers = append(localConfig.GraspServers, nostr.NormalizeURL(r)) + } + for _, m := range repo.Maintainers { + localConfig.Maintainers = append(localConfig.Maintainers, nip19.EncodeNpub(m)) + } + + if err := localConfig.Validate(); err != nil { + return fmt.Errorf("invalid config: %w", err) + } + + // write nip34.json + if err := writeNip34ConfigFile(targetDir, localConfig); err != nil { + return err + } + + // add nip34.json to .git/info/exclude in cloned repo + excludeNip34ConfigFile(targetDir) + + // setup git remotes + gitSetupRemotes(ctx, targetDir, repo) + + // fetch from each grasp remote + fetchFromRemotes(ctx, targetDir, repo) + + // if we have a state with a HEAD, try to reset to it + if state.Event.ID != nostr.ZeroID && state.HEAD != "" { + if headCommit, ok := state.Branches[state.HEAD]; ok { + // check if we have that commit + checkCmd := exec.Command("git", "cat-file", "-e", headCommit) + checkCmd.Dir = targetDir + if err := checkCmd.Run(); err == nil { + // commit exists, reset to it + log("resetting to commit %s...\n", color.CyanString(headCommit)) + resetCmd := exec.Command("git", "reset", "--hard", headCommit) + resetCmd.Dir = targetDir + resetCmd.Stderr = os.Stderr + if err := resetCmd.Run(); err != nil { + log("! failed to reset: %v\n", color.YellowString("%v", err)) + } + } + } + } + + // update refs from state + if state != nil { + gitUpdateRefs(ctx, targetDir, *state) + } + + log("cloned into %s\n", color.GreenString(targetDir)) + return nil + }, + }, + { + Name: "push", + Usage: "push git changes", + Flags: append(defaultKeyFlags, &cli.BoolFlag{ + Name: "force", + Aliases: []string{"f"}, + Usage: "force push to git remotes", + }), + Action: func(ctx context.Context, c *cli.Command) error { + // setup signer + kr, _, err := gatherKeyerFromArguments(ctx, c) + if err != nil { + return fmt.Errorf("failed to gather keyer: %w", err) + } + + // log publishing as npub + currentPk, _ := kr.GetPublicKey(ctx) + currentNpub := nip19.EncodeNpub(currentPk) + log("publishing as %s\n", color.CyanString(currentNpub)) + + // sync to ensure everything is up to date + repo, state, err := gitSync(ctx, kr) + if err != nil { + return fmt.Errorf("failed to sync: %w", err) + } + + // figure out which branches to push + localBranch, remoteBranch, err := figureOutBranches(c, c.Args().First(), true) + if err != nil { + return err + } + + // check if signer matches owner or is in maintainers + if currentPk != repo.Event.PubKey && !slices.Contains(repo.Maintainers, currentPk) { + return fmt.Errorf("current user '%s' is not allowed to push", nip19.EncodeNpub(currentPk)) + } + + // get commit for the local branch + res, err := exec.Command("git", "rev-parse", localBranch).Output() + if err != nil { + return fmt.Errorf("failed to get commit for branch %s: %w", localBranch, err) + } + currentCommit := strings.TrimSpace(string(res)) + + logverbose("pushing branch %s to remote branch %s, commit: %s\n", localBranch, remoteBranch, currentCommit) + + // create a new state if we didn't find any + if state == nil { + state = &nip34.RepositoryState{ + ID: repo.ID, + Branches: make(map[string]string), + Tags: make(map[string]string), + } + } + + // update the branch + if !c.Bool("force") { + if prevCommit, exists := state.Branches[remoteBranch]; exists { + // check if prevCommit is an ancestor of currentCommit (fast-forward check) + cmd := exec.Command("git", "merge-base", "--is-ancestor", prevCommit, currentCommit) + if err := cmd.Run(); err != nil { + return fmt.Errorf("non-fast-forward push not allowed, use --force to override") + } + } + } + state.Branches[remoteBranch] = currentCommit + log("- setting branch %s to commit %s\n", color.CyanString(remoteBranch), color.CyanString(currentCommit)) + + // set the HEAD to the local branch if none is set + if state.HEAD == "" { + state.HEAD = remoteBranch + log("- setting HEAD to branch %s\n", color.CyanString(remoteBranch)) + } + + // create and sign the new state event + newStateEvent := state.ToEvent() + err = kr.SignEvent(ctx, &newStateEvent) + if err != nil { + return fmt.Errorf("error signing state event: %w", err) + } + + log("- publishing updated repository state to " + color.CyanString("%v", repo.Relays) + "\n") + for res := range sys.Pool.PublishMany(ctx, repo.Relays, newStateEvent) { + if res.Error != nil { + log("! error publishing event to %s: %v\n", color.YellowString(res.RelayURL), res.Error) + } else { + log("> published to %s\n", color.GreenString(res.RelayURL)) + } + } + + // push to each grasp remote + pushSuccesses := 0 + for _, relay := range repo.Relays { + relayURL := nostr.NormalizeURL(relay) + remoteName := "nip34/grasp/" + strings.TrimPrefix(relayURL, "wss://") + remoteName = strings.TrimPrefix(remoteName, "ws://") + + log("pushing to %s...\n", color.CyanString(remoteName)) + pushArgs := []string{"push", remoteName, fmt.Sprintf("%s:refs/heads/%s", localBranch, remoteBranch)} + if c.Bool("force") { + pushArgs = append(pushArgs, "--force") + } + pushCmd := exec.Command("git", pushArgs...) + pushCmd.Stderr = os.Stderr + if err := pushCmd.Run(); err != nil { + log("! failed to push to %s: %v\n", color.YellowString(remoteName), err) + } else { + log("> pushed to %s\n", color.GreenString(remoteName)) + pushSuccesses++ + } + } + + if pushSuccesses == 0 { + return fmt.Errorf("failed to push to any remote") + } + + gitUpdateRefs(ctx, "", *state) + + return nil + }, + }, + { + Name: "pull", + Usage: "pull git changes", + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "rebase", + Usage: "rebase instead of merge", + }, + }, + Action: func(ctx context.Context, c *cli.Command) error { + // sync to fetch latest state and metadata + _, state, err := gitSync(ctx, nil) + if err != nil { + return fmt.Errorf("failed to sync: %w", err) + } + + // figure out which branches to pull + localBranch, remoteBranch, err := figureOutBranches(c, c.Args().First(), false) + if err != nil { + return err + } + + // get the commit from state for the remote branch + if state.Event.ID == nostr.ZeroID { + return fmt.Errorf("no repository state found") + } + + targetCommit, ok := state.Branches[remoteBranch] + if !ok { + return fmt.Errorf("branch '%s' not found in repository state", remoteBranch) + } + + // check if the commit exists locally + checkCmd := exec.Command("git", "cat-file", "-e", targetCommit) + if err := checkCmd.Run(); err != nil { + return fmt.Errorf("commit %s not found locally, try 'nak git fetch' first", targetCommit) + } + + // merge or rebase + if c.Bool("rebase") { + log("rebasing %s onto %s...\n", color.CyanString(localBranch), color.CyanString(targetCommit)) + rebaseCmd := exec.Command("git", "rebase", targetCommit) + rebaseCmd.Stderr = os.Stderr + rebaseCmd.Stdout = os.Stdout + if err := rebaseCmd.Run(); err != nil { + return fmt.Errorf("rebase failed: %w", err) + } + } else { + log("merging %s into %s...\n", color.CyanString(targetCommit), color.CyanString(localBranch)) + mergeCmd := exec.Command("git", "merge", targetCommit) + mergeCmd.Stderr = os.Stderr + mergeCmd.Stdout = os.Stdout + if err := mergeCmd.Run(); err != nil { + return fmt.Errorf("merge failed: %w", err) + } + } + + log("pull complete\n") + return nil + }, + }, + { + Name: "fetch", + Usage: "fetch git data", + Action: func(ctx context.Context, c *cli.Command) error { + _, _, err := gitSync(ctx, nil) + return err + }, + }, }, } @@ -440,351 +688,52 @@ func promptForConfig(config *Nip34Config) error { return nil } -var gitClone = &cli.Command{ - Name: "clone", - Usage: "clone a NIP-34 repository from a nostr:// URI", - Description: `the parameter maybe in the form "/", ngit-style like "nostr:////" or an "naddr1..." code.`, - ArgsUsage: " [directory]", - Action: func(ctx context.Context, c *cli.Command) error { - args := c.Args() - if args.Len() == 0 { - return fmt.Errorf("missing repository address") - } - - owner, identifier, relayHints, err := parseRepositoryAddress(ctx, args.Get(0)) - if err != nil { - return fmt.Errorf("failed to parse remote url '%s': %s", args.Get(0), err) - } - - // fetch repository metadata and state - repo, state, err := fetchRepositoryAndState(ctx, owner, identifier, relayHints) - if err != nil { - return err - } - - // determine target directory - targetDir := "" - if args.Len() >= 2 { - targetDir = args.Get(1) - } else { - targetDir = repo.ID - } - if targetDir == "" { - targetDir = repo.ID - } - - // if targetDir exists and is non-empty, bail - if fi, err := os.Stat(targetDir); err == nil && fi.IsDir() { - entries, err := os.ReadDir(targetDir) - if err == nil && len(entries) > 0 { - return fmt.Errorf("target directory '%s' already exists and is not empty", targetDir) - } - } - - // create directory - if err := os.MkdirAll(targetDir, 0755); err != nil { - return fmt.Errorf("failed to create directory '%s': %w", targetDir, err) - } - - // initialize git inside the directory - initCmd := exec.Command("git", "init") - initCmd.Dir = targetDir - if err := initCmd.Run(); err != nil { - return fmt.Errorf("failed to initialize git repository: %w", err) - } - - // write nip34.json inside cloned directory - localConfig := Nip34Config{ - Identifier: repo.ID, - Name: repo.Name, - Description: repo.Description, - Web: repo.Web, - Owner: nip19.EncodeNpub(repo.Event.PubKey), - GraspServers: make([]string, 0, len(repo.Relays)), - EarliestUniqueCommit: repo.EarliestUniqueCommitID, - Maintainers: make([]string, 0, len(repo.Maintainers)), - } - for _, r := range repo.Relays { - localConfig.GraspServers = append(localConfig.GraspServers, nostr.NormalizeURL(r)) - } - for _, m := range repo.Maintainers { - localConfig.Maintainers = append(localConfig.Maintainers, nip19.EncodeNpub(m)) - } - - if err := localConfig.Validate(); err != nil { - return fmt.Errorf("invalid config: %w", err) - } - - // write nip34.json - if err := writeNip34ConfigFile(targetDir, localConfig); err != nil { - return err - } - - // add nip34.json to .git/info/exclude in cloned repo - excludeNip34ConfigFile(targetDir) - - // setup git remotes - gitSetupRemotes(ctx, targetDir, repo) - - // fetch from each grasp remote - fetchFromRemotes(ctx, targetDir, repo) - - // if we have a state with a HEAD, try to reset to it - if state.Event.ID != nostr.ZeroID && state.HEAD != "" { - if headCommit, ok := state.Branches[state.HEAD]; ok { - // check if we have that commit - checkCmd := exec.Command("git", "cat-file", "-e", headCommit) - checkCmd.Dir = targetDir - if err := checkCmd.Run(); err == nil { - // commit exists, reset to it - log("resetting to commit %s...\n", color.CyanString(headCommit)) - resetCmd := exec.Command("git", "reset", "--hard", headCommit) - resetCmd.Dir = targetDir - resetCmd.Stderr = os.Stderr - if err := resetCmd.Run(); err != nil { - log("! failed to reset: %v\n", color.YellowString("%v", err)) - } - } - } - } - - // update refs from state - if state.Event.ID != nostr.ZeroID { - gitUpdateRefs(ctx, targetDir, state) - } - - log("cloned into %s\n", color.GreenString(targetDir)) - return nil - }, -} - -var gitPush = &cli.Command{ - Name: "push", - Usage: "push git changes", - Flags: append(defaultKeyFlags, &cli.BoolFlag{ - Name: "force", - Aliases: []string{"f"}, - Usage: "force push to git remotes", - }), - Action: func(ctx context.Context, c *cli.Command) error { - // setup signer - kr, _, err := gatherKeyerFromArguments(ctx, c) - if err != nil { - return fmt.Errorf("failed to gather keyer: %w", err) - } - - // log publishing as npub - currentPk, _ := kr.GetPublicKey(ctx) - currentNpub := nip19.EncodeNpub(currentPk) - log("publishing as %s\n", color.CyanString(currentNpub)) - - // sync to ensure everything is up to date - repo, state, err := syncRepository(ctx, kr) - if err != nil { - return fmt.Errorf("failed to sync: %w", err) - } - - // figure out which branches to push - localBranch, remoteBranch, err := figureOutBranches(c, c.Args().First(), true) - if err != nil { - return err - } - - // check if signer matches owner or is in maintainers - if currentPk != repo.Event.PubKey && !slices.Contains(repo.Maintainers, currentPk) { - return fmt.Errorf("current user '%s' is not allowed to push", nip19.EncodeNpub(currentPk)) - } - - if state.Event.ID != nostr.ZeroID { - logverbose("found state event: %s\n", state.Event.ID) - } - - // get commit for the local branch - res, err := exec.Command("git", "rev-parse", localBranch).Output() - if err != nil { - return fmt.Errorf("failed to get commit for branch %s: %w", localBranch, err) - } - currentCommit := strings.TrimSpace(string(res)) - - logverbose("pushing branch %s to remote branch %s, commit: %s\n", localBranch, remoteBranch, currentCommit) - - // create a new state if we didn't find any - if state.Event.ID == nostr.ZeroID { - state = nip34.RepositoryState{ - ID: repo.ID, - Branches: make(map[string]string), - Tags: make(map[string]string), - } - } - - // update the branch - if !c.Bool("force") { - if prevCommit, exists := state.Branches[remoteBranch]; exists { - // check if prevCommit is an ancestor of currentCommit (fast-forward check) - cmd := exec.Command("git", "merge-base", "--is-ancestor", prevCommit, currentCommit) - if err := cmd.Run(); err != nil { - return fmt.Errorf("non-fast-forward push not allowed, use --force to override") - } - } - } - state.Branches[remoteBranch] = currentCommit - log("- setting branch %s to commit %s\n", color.CyanString(remoteBranch), color.CyanString(currentCommit)) - - // set the HEAD to the local branch if none is set - if state.HEAD == "" { - state.HEAD = remoteBranch - log("- setting HEAD to branch %s\n", color.CyanString(remoteBranch)) - } - - // create and sign the new state event - newStateEvent := state.ToEvent() - err = kr.SignEvent(ctx, &newStateEvent) - if err != nil { - return fmt.Errorf("error signing state event: %w", err) - } - - log("- publishing updated repository state to " + color.CyanString("%v", repo.Relays) + "\n") - for res := range sys.Pool.PublishMany(ctx, repo.Relays, newStateEvent) { - if res.Error != nil { - log("! error publishing event to %s: %v\n", color.YellowString(res.RelayURL), res.Error) - } else { - log("> published to %s\n", color.GreenString(res.RelayURL)) - } - } - - // push to each grasp remote - pushSuccesses := 0 - for _, relay := range repo.Relays { - relayURL := nostr.NormalizeURL(relay) - remoteName := "nip34/grasp/" + strings.TrimPrefix(relayURL, "wss://") - remoteName = strings.TrimPrefix(remoteName, "ws://") - - log("pushing to %s...\n", color.CyanString(remoteName)) - pushArgs := []string{"push", remoteName, fmt.Sprintf("%s:refs/heads/%s", localBranch, remoteBranch)} - if c.Bool("force") { - pushArgs = append(pushArgs, "--force") - } - pushCmd := exec.Command("git", pushArgs...) - pushCmd.Stderr = os.Stderr - if err := pushCmd.Run(); err != nil { - log("! failed to push to %s: %v\n", color.YellowString(remoteName), err) - } else { - log("> pushed to %s\n", color.GreenString(remoteName)) - pushSuccesses++ - } - } - - if pushSuccesses == 0 { - return fmt.Errorf("failed to push to any remote") - } - - gitUpdateRefs(ctx, "", state) - - return nil - }, -} - -var gitPull = &cli.Command{ - Name: "pull", - Usage: "pull git changes", - Flags: []cli.Flag{ - &cli.BoolFlag{ - Name: "rebase", - Usage: "rebase instead of merge", - }, - }, - Action: func(ctx context.Context, c *cli.Command) error { - // sync to fetch latest state and metadata - _, state, err := syncRepository(ctx, nil) - if err != nil { - return fmt.Errorf("failed to sync: %w", err) - } - - // figure out which branches to pull - localBranch, remoteBranch, err := figureOutBranches(c, c.Args().First(), false) - if err != nil { - return err - } - - // get the commit from state for the remote branch - if state.Event.ID == nostr.ZeroID { - return fmt.Errorf("no repository state found") - } - - targetCommit, ok := state.Branches[remoteBranch] - if !ok { - return fmt.Errorf("branch '%s' not found in repository state", remoteBranch) - } - - // check if the commit exists locally - checkCmd := exec.Command("git", "cat-file", "-e", targetCommit) - if err := checkCmd.Run(); err != nil { - return fmt.Errorf("commit %s not found locally, try 'nak git fetch' first", targetCommit) - } - - // merge or rebase - if c.Bool("rebase") { - log("rebasing %s onto %s...\n", color.CyanString(localBranch), color.CyanString(targetCommit)) - rebaseCmd := exec.Command("git", "rebase", targetCommit) - rebaseCmd.Stderr = os.Stderr - rebaseCmd.Stdout = os.Stdout - if err := rebaseCmd.Run(); err != nil { - return fmt.Errorf("rebase failed: %w", err) - } - } else { - log("merging %s into %s...\n", color.CyanString(targetCommit), color.CyanString(localBranch)) - mergeCmd := exec.Command("git", "merge", targetCommit) - mergeCmd.Stderr = os.Stderr - mergeCmd.Stdout = os.Stdout - if err := mergeCmd.Run(); err != nil { - return fmt.Errorf("merge failed: %w", err) - } - } - - log("pull complete\n") - return nil - }, -} - -var gitFetch = &cli.Command{ - Name: "fetch", - Usage: "fetch git data", - Action: func(ctx context.Context, c *cli.Command) error { - _, _, err := syncRepository(ctx, nil) - return err - }, -} - -var gitSync = &cli.Command{ - Name: "sync", - Usage: "sync repository with relays", - Flags: defaultKeyFlags, - Action: func(ctx context.Context, c *cli.Command) error { - kr, _, _ := gatherKeyerFromArguments(ctx, c) - _, _, err := syncRepository(ctx, kr) - return err - }, -} - -func syncRepository(ctx context.Context, signer nostr.Keyer) (nip34.Repository, nip34.RepositoryState, error) { +func gitSync(ctx context.Context, signer nostr.Keyer) (nip34.Repository, *nip34.RepositoryState, error) { // read current nip34.json localConfig, err := readNip34ConfigFile("") if err != nil { - return nip34.Repository{}, nip34.RepositoryState{}, err + return nip34.Repository{}, nil, err } // parse owner owner, err := parsePubKey(localConfig.Owner) if err != nil { - return nip34.Repository{}, nip34.RepositoryState{}, fmt.Errorf("invalid owner public key: %w", err) + return nip34.Repository{}, nil, fmt.Errorf("invalid owner public key: %w", err) } // fetch repository announcement and state from relays repo, state, err := fetchRepositoryAndState(ctx, owner, localConfig.Identifier, localConfig.GraspServers) if err != nil { - logverbose("failed to fetch repository metadata: %v\n", err) - // create a local repository object from config - repo = localConfig.ToRepository() + log("couldn't fetch repository metadata (%s), will publish now\n", err) + // create a local repository object from config and publish it + localRepo := localConfig.ToRepository() + + if signer != nil { + signerPk, err := signer.GetPublicKey(ctx) + if err != nil { + return repo, nil, fmt.Errorf("failed to get signer pubkey: %w", err) + } + if signerPk != owner { + return repo, nil, fmt.Errorf("provided signer pubkey does not match owner, can't publish repository") + } else { + event := localRepo.ToEvent() + if err := signer.SignEvent(ctx, &event); err != nil { + return repo, state, fmt.Errorf("failed to sign announcement: %w", err) + } + + relays := append(sys.FetchOutboxRelays(ctx, owner, 3), localConfig.GraspServers...) + for res := range sys.Pool.PublishMany(ctx, relays, event) { + if res.Error != nil { + log("! error publishing to %s: %v\n", color.YellowString(res.RelayURL), res.Error) + } else { + log("> published to %s\n", color.GreenString(res.RelayURL)) + } + } + repo = localRepo + } + } else { + return repo, nil, fmt.Errorf("no signer provided to publish repository (run 'nak git sync' with the '--sec' flag)") + } } else { // check if local config differs from remote announcement // construct local repo from config for comparison @@ -853,8 +802,8 @@ func syncRepository(ctx context.Context, signer nostr.Keyer) (nip34.Repository, fetchFromRemotes(ctx, "", repo) // update refs from state - if state.Event.ID != nostr.ZeroID { - gitUpdateRefs(ctx, "", state) + if state != nil { + gitUpdateRefs(ctx, "", *state) } return repo, state, nil @@ -986,7 +935,7 @@ func fetchRepositoryAndState( pubkey nostr.PubKey, identifier string, relayHints []string, -) (repo nip34.Repository, state nip34.RepositoryState, err error) { +) (repo nip34.Repository, state *nip34.RepositoryState, err error) { // fetch repository announcement (30617) relays := appendUnique(relayHints, sys.FetchOutboxRelays(ctx, pubkey, 3)...) for ie := range sys.Pool.FetchMany(ctx, relays, nostr.Filter{ @@ -996,7 +945,7 @@ func fetchRepositoryAndState( "d": []string{identifier}, }, Limit: 2, - }, nostr.SubscriptionOptions{Label: "nak-git-clone-meta"}) { + }, nostr.SubscriptionOptions{Label: "nak-git"}) { if ie.Event.CreatedAt > repo.CreatedAt { repo = nip34.ParseRepository(ie.Event) } @@ -1006,7 +955,6 @@ func fetchRepositoryAndState( } // fetch repository state (30618) - var stateFound bool var stateErr error for ie := range sys.Pool.FetchMany(ctx, repo.Relays, nostr.Filter{ Kinds: []nostr.Kind{30618}, @@ -1015,10 +963,10 @@ func fetchRepositoryAndState( "d": []string{identifier}, }, Limit: 2, - }, nostr.SubscriptionOptions{Label: "nak-git-clone-meta"}) { - if ie.Event.CreatedAt > state.CreatedAt { - state = nip34.ParseRepositoryState(ie.Event) - stateFound = true + }, nostr.SubscriptionOptions{Label: "nak-git"}) { + if state == nil || ie.Event.CreatedAt > state.CreatedAt { + state_ := nip34.ParseRepositoryState(ie.Event) + state = &state_ if state.HEAD == "" { stateErr = fmt.Errorf("state is missing HEAD") @@ -1032,9 +980,6 @@ func fetchRepositoryAndState( stateErr = nil } } - if !stateFound { - return repo, state, fmt.Errorf("no repository state (kind 30618) found") - } if stateErr != nil { return repo, state, stateErr } @@ -1313,3 +1258,65 @@ func figureOutBranches(c *cli.Command, refspec string, isPush bool) ( } func graspServerHost(s string) string { return strings.SplitN(nostr.NormalizeURL(s), "/", 3)[2] } + +type Nip34Config struct { + Identifier string `json:"identifier"` + Name string `json:"name"` + Description string `json:"description"` + Web []string `json:"web"` + Owner string `json:"owner"` + GraspServers []string `json:"grasp-servers"` + EarliestUniqueCommit string `json:"earliest-unique-commit"` + Maintainers []string `json:"maintainers"` +} + +func (localConfig Nip34Config) Validate() error { + _, err := parsePubKey(localConfig.Owner) + if err != nil { + return fmt.Errorf("owner pubkey '%s' is not valid: %w", localConfig.Owner, err) + } + + for _, maintainer := range localConfig.Maintainers { + _, err := parsePubKey(maintainer) + if err != nil { + return fmt.Errorf("maintainer pubkey '%s' is not valid: %w", maintainer, err) + } + } + + return nil +} + +func (localConfig Nip34Config) ToRepository() nip34.Repository { + owner, err := parsePubKey(localConfig.Owner) + if err != nil { + panic(err) + } + + localRepo := nip34.Repository{ + ID: localConfig.Identifier, + Name: localConfig.Name, + Description: localConfig.Description, + Web: localConfig.Web, + EarliestUniqueCommitID: localConfig.EarliestUniqueCommit, + Maintainers: []nostr.PubKey{}, + Event: nostr.Event{ + PubKey: owner, + }, + } + for _, server := range localConfig.GraspServers { + graspServerURL := nostr.NormalizeURL(server) + url := fmt.Sprintf("http%s/%s/%s.git", + graspServerURL[2:], nip19.EncodeNpub(localRepo.PubKey), localConfig.Identifier) + localRepo.Clone = append(localRepo.Clone, url) + localRepo.Relays = append(localRepo.Relays, graspServerURL) + } + for _, maintainer := range localConfig.Maintainers { + pk, err := parsePubKey(maintainer) + if err != nil { + panic(err) + } + localRepo.Maintainers = append(localRepo.Maintainers, pk) + } + + return localRepo +} From 73d80203a0c3dc9454ad374f01583434bf2d28dd Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Tue, 25 Nov 2025 22:29:25 +0900 Subject: [PATCH 348/401] fix error message --- encrypt_decrypt.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/encrypt_decrypt.go b/encrypt_decrypt.go index 457546f..66d31a7 100644 --- a/encrypt_decrypt.go +++ b/encrypt_decrypt.go @@ -111,7 +111,7 @@ var decrypt = &cli.Command{ } plaintext, err := nip04.Decrypt(ciphertext, ss) if err != nil { - return fmt.Errorf("failed to encrypt as nip04: %w", err) + return fmt.Errorf("failed to decrypt as nip04: %w", err) } stdout(plaintext) } From e04861fceedb5e524168dca5df4d29f06ef4c83b Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 25 Nov 2025 08:54:41 -0300 Subject: [PATCH 349/401] git: allow gitSync to not fail if the state is broken. --- git.go | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/git.go b/git.go index 343c3ba..9857f98 100644 --- a/git.go +++ b/git.go @@ -302,7 +302,7 @@ aside from those, there is also: fetchFromRemotes(ctx, targetDir, repo) // if we have a state with a HEAD, try to reset to it - if state.Event.ID != nostr.ZeroID && state.HEAD != "" { + if state != nil && state.HEAD != "" { if headCommit, ok := state.Branches[state.HEAD]; ok { // check if we have that commit checkCmd := exec.Command("git", "cat-file", "-e", headCommit) @@ -703,7 +703,7 @@ func gitSync(ctx context.Context, signer nostr.Keyer) (nip34.Repository, *nip34. // fetch repository announcement and state from relays repo, state, err := fetchRepositoryAndState(ctx, owner, localConfig.Identifier, localConfig.GraspServers) - if err != nil { + if err != nil && repo.Event.ID == nostr.ZeroID { log("couldn't fetch repository metadata (%s), will publish now\n", err) // create a local repository object from config and publish it localRepo := localConfig.ToRepository() @@ -735,6 +735,15 @@ func gitSync(ctx context.Context, signer nostr.Keyer) (nip34.Repository, *nip34. return repo, nil, fmt.Errorf("no signer provided to publish repository (run 'nak git sync' with the '--sec' flag)") } } else { + if err != nil { + if _, ok := err.(StateErr); ok { + // some error with the state, just do nothing and proceed + } else { + // actually fail with this error we don't know about + return repo, nil, err + } + } + // check if local config differs from remote announcement // construct local repo from config for comparison localRepo := localConfig.ToRepository() @@ -955,7 +964,7 @@ func fetchRepositoryAndState( } // fetch repository state (30618) - var stateErr error + var stateErr *StateErr for ie := range sys.Pool.FetchMany(ctx, repo.Relays, nostr.Filter{ Kinds: []nostr.Kind{30618}, Authors: []nostr.PubKey{pubkey}, @@ -966,18 +975,18 @@ func fetchRepositoryAndState( }, nostr.SubscriptionOptions{Label: "nak-git"}) { if state == nil || ie.Event.CreatedAt > state.CreatedAt { state_ := nip34.ParseRepositoryState(ie.Event) - state = &state_ - if state.HEAD == "" { - stateErr = fmt.Errorf("state is missing HEAD") + if state_.HEAD == "" { + stateErr = &StateErr{"state is missing HEAD"} continue } - if _, ok := state.Branches[state.HEAD]; !ok { - stateErr = fmt.Errorf("state is missing commit for HEAD branch '%s'", state.HEAD) + if _, ok := state_.Branches[state_.HEAD]; !ok { + stateErr = &StateErr{fmt.Sprintf("state is missing commit for HEAD branch '%s'", state_.HEAD)} continue } stateErr = nil + state = &state_ } } if stateErr != nil { @@ -987,6 +996,10 @@ func fetchRepositoryAndState( return repo, state, nil } +type StateErr struct{ string } + +func (s StateErr) Error() string { return string(s.string) } + func findGitRoot(startDir string) string { if startDir == "" { var err error From 8df130a822149db0eeff55867abd20af862d1768 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 25 Nov 2025 14:51:11 -0300 Subject: [PATCH 350/401] git: handle "pull" modes correctly and stop deleting and recreating remotes all the time. --- git.go | 149 +++++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 123 insertions(+), 26 deletions(-) diff --git a/git.go b/git.go index 9857f98..89d8c62 100644 --- a/git.go +++ b/git.go @@ -458,6 +458,18 @@ aside from those, there is also: Name: "rebase", Usage: "rebase instead of merge", }, + &cli.BoolFlag{ + Name: "ff-only", + Usage: "only allow fast-forward merges", + }, + &cli.BoolFlag{ + Name: "ff", + Usage: "allow fast-forward merges", + }, + &cli.BoolFlag{ + Name: "no-ff", + Usage: "always perform a merge instead of fast-forwarding", + }, }, Action: func(ctx context.Context, c *cli.Command) error { // sync to fetch latest state and metadata @@ -488,8 +500,51 @@ aside from those, there is also: return fmt.Errorf("commit %s not found locally, try 'nak git fetch' first", targetCommit) } - // merge or rebase + // determine merge strategy + var strategy string + strategiesSpecified := 0 if c.Bool("rebase") { + strategy = "rebase" + strategiesSpecified++ + } + if c.Bool("ff-only") { + strategy = "ff-only" + strategiesSpecified++ + } + if c.Bool("no-ff") { + strategy = "no-ff" + strategiesSpecified++ + } + if c.Bool("ff") { + strategy = "ff" + strategiesSpecified++ + } + + if strategiesSpecified > 1 { + return fmt.Errorf("flags --rebase, --ff-only, --ff, --no-ff are mutually exclusive") + } + + if strategy == "" { + // check git config for pull.rebase + cmd := exec.Command("git", "config", "--get", "pull.rebase") + output, err := cmd.Output() + if err == nil && strings.TrimSpace(string(output)) == "true" { + strategy = "rebase" + } else if err == nil && strings.TrimSpace(string(output)) == "false" { + strategy = "ff" + } else { + // check git config for pull.ff + cmd := exec.Command("git", "config", "--get", "pull.ff") + output, err := cmd.Output() + if err == nil && strings.TrimSpace(string(output)) == "only" { + strategy = "ff-only" + } + } + } + + // execute the merge or rebase + switch strategy { + case "rebase": log("rebasing %s onto %s...\n", color.CyanString(localBranch), color.CyanString(targetCommit)) rebaseCmd := exec.Command("git", "rebase", targetCommit) rebaseCmd.Stderr = os.Stderr @@ -497,14 +552,52 @@ aside from those, there is also: if err := rebaseCmd.Run(); err != nil { return fmt.Errorf("rebase failed: %w", err) } - } else { - log("merging %s into %s...\n", color.CyanString(targetCommit), color.CyanString(localBranch)) - mergeCmd := exec.Command("git", "merge", targetCommit) + case "ff-only": + log("pulling %s into %s (fast-forward only)...\n", color.CyanString(targetCommit), color.CyanString(localBranch)) + mergeCmd := exec.Command("git", "merge", "--ff-only", targetCommit) mergeCmd.Stderr = os.Stderr mergeCmd.Stdout = os.Stdout if err := mergeCmd.Run(); err != nil { return fmt.Errorf("merge failed: %w", err) } + case "no-ff": + log("pulling %s into %s (no fast-forward)...\n", color.CyanString(targetCommit), color.CyanString(localBranch)) + mergeCmd := exec.Command("git", "merge", "--no-ff", targetCommit) + mergeCmd.Stderr = os.Stderr + mergeCmd.Stdout = os.Stdout + if err := mergeCmd.Run(); err != nil { + return fmt.Errorf("merge failed: %w", err) + } + case "ff": + log("pulling %s into %s...\n", color.CyanString(targetCommit), color.CyanString(localBranch)) + mergeCmd := exec.Command("git", "merge", "--ff", targetCommit) + mergeCmd.Stderr = os.Stderr + mergeCmd.Stdout = os.Stdout + if err := mergeCmd.Run(); err != nil { + return fmt.Errorf("merge failed: %w", err) + } + default: + // get current commit + res, err := exec.Command("git", "rev-parse", localBranch).Output() + if err != nil { + return fmt.Errorf("failed to get current commit for branch %s: %w", localBranch, err) + } + currentCommit := strings.TrimSpace(string(res)) + + // check if fast-forward possible + cmd := exec.Command("git", "merge-base", "--is-ancestor", currentCommit, targetCommit) + if err := cmd.Run(); err != nil { + return fmt.Errorf("fast-forward merge not possible, specify --rebase, --ff-only, --ff, or --no-ff; or use git config") + } + + // do fast-forward + log("fast-forwarding to %s...\n", color.CyanString(targetCommit)) + mergeCmd := exec.Command("git", "merge", "--ff-only", targetCommit) + mergeCmd.Stderr = os.Stderr + mergeCmd.Stdout = os.Stdout + if err := mergeCmd.Run(); err != nil { + return fmt.Errorf("fast-forward failed: %w", err) + } } log("pull complete\n") @@ -849,39 +942,43 @@ func gitSetupRemotes(ctx context.Context, dir string, repo nip34.Repository) { // delete all nip34/grasp/ remotes remotes := strings.Split(strings.TrimSpace(string(output)), "\n") - for _, remote := range remotes { + for i, remote := range remotes { remote = strings.TrimSpace(remote) + remotes[i] = remote + if strings.HasPrefix(remote, "nip34/grasp/") { - delCmd := exec.Command("git", "remote", "remove", remote) - if dir != "" { - delCmd.Dir = dir - } - if err := delCmd.Run(); err != nil { - logverbose("failed to remove remote %s: %v\n", remote, err) + if !slices.Contains(repo.Relays, nostr.NormalizeURL(remote[12:])) { + delCmd := exec.Command("git", "remote", "remove", remote) + if dir != "" { + delCmd.Dir = dir + } + if err := delCmd.Run(); err != nil { + logverbose("failed to remove remote %s: %v\n", remote, err) + } } } } // create new remotes for each grasp server for _, relay := range repo.Relays { - relayURL := nostr.NormalizeURL(relay) - remoteName := "nip34/grasp/" + strings.TrimPrefix(relayURL, "wss://") - remoteName = strings.TrimPrefix(remoteName, "ws://") + remote := "nip34/grasp/" + strings.TrimPrefix(relay, "wss://") - // construct the git URL - gitURL := fmt.Sprintf("http%s/%s/%s.git", - relayURL[2:], nip19.EncodeNpub(repo.PubKey), repo.ID) + if !slices.Contains(remotes, remote) { + // construct the git URL + gitURL := fmt.Sprintf("http%s/%s/%s.git", + relay[2:], nip19.EncodeNpub(repo.PubKey), repo.ID) - addCmd := exec.Command("git", "remote", "add", remoteName, gitURL) - if dir != "" { - addCmd.Dir = dir - } - if out, err := addCmd.Output(); err != nil { - var stderr string - if exiterr, ok := err.(*exec.ExitError); ok { - stderr = string(exiterr.Stderr) + addCmd := exec.Command("git", "remote", "add", remote, gitURL) + if dir != "" { + addCmd.Dir = dir + } + if out, err := addCmd.Output(); err != nil { + var stderr string + if exiterr, ok := err.(*exec.ExitError); ok { + stderr = string(exiterr.Stderr) + } + logverbose("failed to add remote %s: %s %s\n", remote, stderr, string(out)) } - logverbose("failed to add remote %s: %s %s\n", remoteName, stderr, string(out)) } } } From 03c1bf832ed3d59ee3cd3d787baf1c67fb3fbca1 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 25 Nov 2025 22:44:03 -0300 Subject: [PATCH 351/401] fix README misformatting. --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 8589c64..e03153c 100644 --- a/README.md +++ b/README.md @@ -188,7 +188,6 @@ you can also display a QR code for the bunker URI by adding the `--qrcode` flag: ~> nak bunker --persist --sec ncryptsec1... relay.nsec.app nos.lol ``` -```shell then later just ```shell From 2de3ff78ee866fd524ce083e5a77483744ec99fa Mon Sep 17 00:00:00 2001 From: reis <1l0@users.noreply.github.com> Date: Wed, 26 Nov 2025 21:02:47 +0900 Subject: [PATCH 352/401] Add `nip` command (#83) --- main.go | 1 + nip.go | 186 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 187 insertions(+) create mode 100644 nip.go diff --git a/main.go b/main.go index 79af04e..c3438e9 100644 --- a/main.go +++ b/main.go @@ -49,6 +49,7 @@ var app = &cli.Command{ fsCmd, publish, git, + nip, }, Version: version, Flags: []cli.Flag{ diff --git a/nip.go b/nip.go new file mode 100644 index 0000000..c7f75a1 --- /dev/null +++ b/nip.go @@ -0,0 +1,186 @@ +package main + +import ( + "context" + "fmt" + "io" + "net/http" + "os/exec" + "runtime" + "strings" + + "github.com/urfave/cli/v3" +) + +var nip = &cli.Command{ + Name: "nip", + Usage: "get the description of a NIP from its number", + Description: `fetches the NIPs README from GitHub and parses it to find the description of the given NIP number. + +example: + nak nip 1 + nak nip list + nak nip open 1`, + ArgsUsage: "", + Commands: []*cli.Command{ + { + Name: "list", + Usage: "list all NIPs", + Action: func(ctx context.Context, c *cli.Command) error { + return iterateNips(func(nip, desc, link string) bool { + stdout(nip + ": " + desc) + return true + }) + }, + }, + { + Name: "open", + Usage: "open the NIP page in the browser", + Action: func(ctx context.Context, c *cli.Command) error { + reqNum := c.Args().First() + if reqNum == "" { + return fmt.Errorf("missing NIP number") + } + + normalize := func(s string) string { + s = strings.ToLower(s) + s = strings.TrimPrefix(s, "nip-") + s = strings.TrimLeft(s, "0") + if s == "" { + s = "0" + } + return s + } + + reqNum = normalize(reqNum) + + foundLink := "" + err := iterateNips(func(nip, desc, link string) bool { + nipNum := normalize(nip) + if nipNum == reqNum { + foundLink = link + return false + } + return true + }) + + if err != nil { + return err + } + + if foundLink == "" { + return fmt.Errorf("NIP-%s not found", strings.ToUpper(reqNum)) + } + + url := "https://github.com/nostr-protocol/nips/blob/master/" + foundLink + fmt.Println("Opening " + url) + + var cmd *exec.Cmd + switch runtime.GOOS { + case "darwin": + cmd = exec.Command("open", url) + case "windows": + cmd = exec.Command("cmd", "/c", "start", url) + default: + cmd = exec.Command("xdg-open", url) + } + + return cmd.Start() + }, + }, + }, + Action: func(ctx context.Context, c *cli.Command) error { + reqNum := c.Args().First() + if reqNum == "" { + return fmt.Errorf("missing NIP number") + } + + normalize := func(s string) string { + s = strings.ToLower(s) + s = strings.TrimPrefix(s, "nip-") + s = strings.TrimLeft(s, "0") + if s == "" { + s = "0" + } + return s + } + + reqNum = normalize(reqNum) + + found := false + err := iterateNips(func(nip, desc, link string) bool { + nipNum := normalize(nip) + + if nipNum == reqNum { + stdout(strings.TrimSpace(desc)) + found = true + return false + } + return true + }) + + if err != nil { + return err + } + + if !found { + return fmt.Errorf("NIP-%s not found", strings.ToUpper(reqNum)) + } + return nil + }, +} + +func iterateNips(yield func(nip, desc, link string) bool) error { + resp, err := http.Get("https://raw.githubusercontent.com/nostr-protocol/nips/master/README.md") + if err != nil { + return fmt.Errorf("failed to fetch NIPs README: %w", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("failed to read NIPs README: %w", err) + } + bodyStr := string(body) + epoch := strings.Index(bodyStr, "## List") + + lines := strings.SplitSeq(bodyStr[epoch+8:], "\n") + for line := range lines { + line = strings.TrimSpace(line) + if strings.HasPrefix(line, "##") { + break + } + if !strings.HasPrefix(line, "- [NIP-") { + continue + } + + start := strings.Index(line, "[") + end := strings.Index(line, "]") + if start == -1 || end == -1 || end < start { + continue + } + + content := line[start+1 : end] + + parts := strings.SplitN(content, ":", 2) + if len(parts) != 2 { + continue + } + + nipPart := parts[0] + descPart := parts[1] + + rest := line[end+1:] + linkStart := strings.Index(rest, "(") + linkEnd := strings.Index(rest, ")") + link := "" + if linkStart != -1 && linkEnd != -1 && linkEnd > linkStart { + link = rest[linkStart+1 : linkEnd] + } + + if !yield(nipPart, strings.TrimSpace(descPart), link) { + break + } + } + return nil +} From 3ff4dbe196a8c35f044523ae378e07a29598c7ca Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Wed, 26 Nov 2025 07:09:29 -0300 Subject: [PATCH 353/401] force update golang version. fixes nostr:nevent1qvzqqqqqqypzqwlsccluhy6xxsr6l9a9uhhxf75g85g8a709tprjcn4e42h053vaqydhwumn8ghj7un9d3shjtnhv4ehgetjde38gcewvdhk6tcprfmhxue69uhhq7tjv9kkjepwve5kzar2v9nzucm0d5hsqgzdaekrxfhwrex49f6htd7rvmnfxs40ypga9mx7hvssaz347mxees2gpdzr --- go.mod | 6 +++--- go.sum | 7 +++++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index 4afffbf..1d52849 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,11 @@ module github.com/fiatjaf/nak -go 1.24.1 +go 1.25 require ( fiatjaf.com/lib v0.3.1 - fiatjaf.com/nostr v0.0.0-20251124002842-de54dd1fa4b8 + fiatjaf.com/nostr v0.0.0-20251126101225-44130595c606 + github.com/AlecAivazis/survey/v2 v2.3.7 github.com/bep/debounce v1.2.1 github.com/btcsuite/btcd/btcec/v2 v2.3.6 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e @@ -28,7 +29,6 @@ require ( ) require ( - github.com/AlecAivazis/survey/v2 v2.3.7 // indirect 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 diff --git a/go.sum b/go.sum index 7823daf..79ef597 100644 --- a/go.sum +++ b/go.sum @@ -1,13 +1,14 @@ fiatjaf.com/lib v0.3.1 h1:/oFQwNtFRfV+ukmOCxfBEAuayoLwXp4wu2/fz5iHpwA= fiatjaf.com/lib v0.3.1/go.mod h1:Ycqq3+mJ9jAWu7XjbQI1cVr+OFgnHn79dQR5oTII47g= -fiatjaf.com/nostr v0.0.0-20251124002842-de54dd1fa4b8 h1:R16mnlJ3qvVar7G4rzY+Z+mEAf2O6wpHTlRlHAt2Od8= -fiatjaf.com/nostr v0.0.0-20251124002842-de54dd1fa4b8/go.mod h1:QEGyTgAjjTFwDx2BJGZiCdmoAcWA/G+sQy7wDqKzSPU= +fiatjaf.com/nostr v0.0.0-20251126101225-44130595c606 h1:wQHJ0TFA0Fuq92p/6u6AbsBFq6ZVToSdxV6puXVIruI= +fiatjaf.com/nostr v0.0.0-20251126101225-44130595c606/go.mod h1:ue7yw0zHfZj23Ml2kVSdBx0ENEaZiuvGxs/8VEN93FU= github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ= github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= 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/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= 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= @@ -56,6 +57,7 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWs github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g= github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= +github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI= github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -112,6 +114,7 @@ github.com/hablullah/go-juliandays v1.0.0 h1:A8YM7wIj16SzlKT0SRJc9CD29iiaUzpBLzh github.com/hablullah/go-juliandays v1.0.0/go.mod h1:0JOYq4oFOuDja+oospuc61YoX+uNEn7Z6uHYTbBzdGc= github.com/hanwen/go-fuse/v2 v2.7.2 h1:SbJP1sUP+n1UF8NXBA14BuojmTez+mDgOk0bC057HQw= github.com/hanwen/go-fuse/v2 v2.7.2/go.mod h1:ugNaD/iv5JYyS1Rcvi57Wz7/vrLQJo10mmketmoef48= +github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/jalaali/go-jalaali v0.0.0-20210801064154-80525e88d958 h1:qxLoi6CAcXVzjfvu+KXIXJOAsQB62LXjsfbOaErsVzE= From 16916d7d95a2e6bf0d6dc4cb7e6ce9ef3948f6c7 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Thu, 27 Nov 2025 12:14:02 -0300 Subject: [PATCH 354/401] nip: display markdown directly, default to list. --- go.mod | 21 +++++++ go.sum | 46 ++++++++++++++ nip.go | 193 +++++++++++++++++++++++++++++++-------------------------- 3 files changed, 171 insertions(+), 89 deletions(-) diff --git a/go.mod b/go.mod index 1d52849..8fa58af 100644 --- a/go.mod +++ b/go.mod @@ -31,18 +31,29 @@ require ( require ( github.com/FastFilter/xorfilter v0.2.1 // indirect github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3 // indirect + github.com/alecthomas/chroma/v2 v2.14.0 // indirect github.com/andybalholm/brotli v1.1.1 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/aymerick/douceur v0.2.0 // indirect github.com/bluekeyes/go-gitdiff v0.7.1 // indirect github.com/btcsuite/btcd v0.24.2 // indirect github.com/btcsuite/btcd/btcutil v1.1.5 // indirect github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect + github.com/charmbracelet/glamour v0.10.0 // indirect + github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 // indirect + github.com/charmbracelet/x/ansi v0.8.0 // indirect + github.com/charmbracelet/x/cellbuf v0.0.13 // indirect + github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect + github.com/charmbracelet/x/term v0.2.1 // indirect github.com/chzyer/logex v1.1.10 // indirect github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect github.com/coder/websocket v1.8.14 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect github.com/dgraph-io/ristretto/v2 v2.3.0 // indirect + github.com/dlclark/regexp2 v1.11.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/elliotchance/pie/v2 v2.7.0 // indirect github.com/elnosh/gonuts v0.4.2 // indirect @@ -50,6 +61,7 @@ require ( github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/go-git/go-git/v5 v5.16.3 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/css v1.0.1 // indirect github.com/hablullah/go-hijri v1.0.2 // indirect github.com/hablullah/go-juliandays v1.0.0 // indirect github.com/jalaali/go-jalaali v0.0.0-20210801064154-80525e88d958 // indirect @@ -57,12 +69,18 @@ require ( github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/klauspost/compress v1.18.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/magefile/mage v1.14.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect + github.com/microcosm-cc/bluemonday v1.0.27 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/muesli/reflow v0.3.0 // indirect + github.com/muesli/termenv v0.16.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/rs/cors v1.11.1 // indirect github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 // indirect github.com/templexxx/cpu v0.0.1 // indirect @@ -75,6 +93,9 @@ require ( github.com/valyala/fasthttp v1.59.0 // indirect github.com/wasilibs/go-re2 v1.3.0 // indirect github.com/x448/float16 v0.8.4 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + github.com/yuin/goldmark v1.7.8 // indirect + github.com/yuin/goldmark-emoji v1.0.5 // indirect go.etcd.io/bbolt v1.4.2 // indirect golang.org/x/crypto v0.39.0 // indirect golang.org/x/net v0.41.0 // indirect diff --git a/go.sum b/go.sum index 79ef597..875f706 100644 --- a/go.sum +++ b/go.sum @@ -13,8 +13,14 @@ github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDe 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/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E= +github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I= github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= +github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= github.com/bluekeyes/go-gitdiff v0.7.1 h1:graP4ElLRshr8ecu0UtqfNTCHrtSyZd3DABQm/DWesQ= @@ -49,6 +55,20 @@ 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/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= +github.com/charmbracelet/glamour v0.10.0 h1:MtZvfwsYCx8jEPFJm3rIBFIMZUfUJ765oX8V6kXldcY= +github.com/charmbracelet/glamour v0.10.0/go.mod h1:f+uf+I/ChNmqo087elLnVdCiVgjSKWuXa/l6NU2ndYk= +github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE= +github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA= +github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= +github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= +github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k= +github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf h1:rLG0Yb6MQSDKdB52aGX55JT1oi0P0Kuaj7wi1bLUpnI= +github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf/go.mod h1:B3UgsnsBZS/eX42BlaNiJkD1pPOUa+oF1IYC6Yd2CEU= +github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= +github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= @@ -74,6 +94,8 @@ github.com/dgraph-io/ristretto/v2 v2.3.0 h1:qTQ38m7oIyd4GAed/QkUZyPFNMnvVWyazGXR github.com/dgraph-io/ristretto/v2 v2.3.0/go.mod h1:gpoRV3VzrEY1a9dWAYV6T1U7YzfgttXdd/ZzL1s9OZM= github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38= github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= +github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dvyukov/go-fuzz v0.0.0-20200318091601-be3528f3a813/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw= @@ -107,6 +129,8 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= +github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hablullah/go-hijri v1.0.2 h1:drT/MZpSZJQXo7jftf5fthArShcaMtsal0Zf/dnmp6k= github.com/hablullah/go-hijri v1.0.2/go.mod h1:OS5qyYLDjORXzK4O1adFw9Q5WfhOcMdAKglDkcTxgWQ= @@ -139,6 +163,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/liamg/magic v0.0.1 h1:Ru22ElY+sCh6RvRTWjQzKKCxsEco8hE0co8n1qe7TBM= github.com/liamg/magic v0.0.1/go.mod h1:yQkOmZZI52EA+SQ2xyHpVw8fNvTBruF873Y+Vt6S+fk= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo= github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= @@ -153,12 +179,17 @@ github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stg github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-tty v0.0.7 h1:KJ486B6qI8+wBO7kQxYgmmEFDaFEE96JMBQ7h400N8Q= github.com/mattn/go-tty v0.0.7/go.mod h1:f2i5ZOvXBU/tCABmLmOfzLz9azMo5wdAaElRNnJKr+k= github.com/mdp/qrterminal/v3 v3.2.1 h1:6+yQjiiOsSuXT5n9/m60E54vdgFsw0zhADHhHLrFet4= github.com/mdp/qrterminal/v3 v3.2.1/go.mod h1:jOTmXvnBsMy5xqLniO0R++Jmjs2sTm9dFSuQ5kpz/SU= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= +github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78= github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -166,6 +197,10 @@ 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/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= +github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= +github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= 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= @@ -181,6 +216,10 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg= github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= @@ -222,9 +261,16 @@ github.com/wasilibs/nottinygc v0.4.0 h1:h1TJMihMC4neN6Zq+WKpLxgd9xCFMw7O9ETLwY2e github.com/wasilibs/nottinygc v0.4.0/go.mod h1:oDcIotskuYNMpqMF23l7Z8uzD4TC0WXHK8jetlB3HIo= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= +github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic= +github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= +github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk= +github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U= go.etcd.io/bbolt v1.4.2 h1:IrUHp260R8c+zYx/Tm8QZr04CX+qWS5PGfPdevhdm1I= go.etcd.io/bbolt v1.4.2/go.mod h1:Is8rSHO/b4f3XigBC0lL0+4FwAQv3HXEEIgFMuKHceM= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= diff --git a/nip.go b/nip.go index c7f75a1..66e7089 100644 --- a/nip.go +++ b/nip.go @@ -9,30 +9,25 @@ import ( "runtime" "strings" + "github.com/charmbracelet/glamour" "github.com/urfave/cli/v3" ) +type nipInfo struct { + nip, desc, link string +} + var nip = &cli.Command{ Name: "nip", - Usage: "get the description of a NIP from its number", - Description: `fetches the NIPs README from GitHub and parses it to find the description of the given NIP number. - -example: - nak nip 1 - nak nip list - nak nip open 1`, - ArgsUsage: "", + Usage: "list NIPs or get the description of a NIP from its number", + Description: `lists NIPs, fetches and displays NIP text, or opens a NIP page in the browser. + +examples: + nak nip # list all NIPs + nak nip 29 # shows nip29 details + nak nip open 29 # opens nip29 in browser`, + ArgsUsage: "[NIP number]", Commands: []*cli.Command{ - { - Name: "list", - Usage: "list all NIPs", - Action: func(ctx context.Context, c *cli.Command) error { - return iterateNips(func(nip, desc, link string) bool { - stdout(nip + ": " + desc) - return true - }) - }, - }, { Name: "open", Usage: "open the NIP page in the browser", @@ -55,17 +50,12 @@ example: reqNum = normalize(reqNum) foundLink := "" - err := iterateNips(func(nip, desc, link string) bool { - nipNum := normalize(nip) + for info := range listnips() { + nipNum := normalize(info.nip) if nipNum == reqNum { - foundLink = link - return false + foundLink = info.link + break } - return true - }) - - if err != nil { - return err } if foundLink == "" { @@ -92,7 +82,11 @@ example: Action: func(ctx context.Context, c *cli.Command) error { reqNum := c.Args().First() if reqNum == "" { - return fmt.Errorf("missing NIP number") + // list all NIPs + for info := range listnips() { + stdout(info.nip + ": " + info.desc) + } + return nil } normalize := func(s string) string { @@ -107,80 +101,101 @@ example: reqNum = normalize(reqNum) - found := false - err := iterateNips(func(nip, desc, link string) bool { - nipNum := normalize(nip) + var foundLink string + for info := range listnips() { + nipNum := normalize(info.nip) if nipNum == reqNum { - stdout(strings.TrimSpace(desc)) - found = true - return false + foundLink = info.link + break } - return true - }) - - if err != nil { - return err } - if !found { + if foundLink == "" { return fmt.Errorf("NIP-%s not found", strings.ToUpper(reqNum)) } + + // fetch the NIP markdown + url := "https://raw.githubusercontent.com/nostr-protocol/nips/master/" + foundLink + resp, err := http.Get(url) + if err != nil { + return fmt.Errorf("failed to fetch NIP: %w", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("failed to read NIP: %w", err) + } + + // render markdown + rendered, err := glamour.Render(string(body), "auto") + if err != nil { + return fmt.Errorf("failed to render markdown: %w", err) + } + + fmt.Print(rendered) return nil }, } -func iterateNips(yield func(nip, desc, link string) bool) error { - resp, err := http.Get("https://raw.githubusercontent.com/nostr-protocol/nips/master/README.md") - if err != nil { - return fmt.Errorf("failed to fetch NIPs README: %w", err) - } - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - return fmt.Errorf("failed to read NIPs README: %w", err) - } - bodyStr := string(body) - epoch := strings.Index(bodyStr, "## List") - - lines := strings.SplitSeq(bodyStr[epoch+8:], "\n") - for line := range lines { - line = strings.TrimSpace(line) - if strings.HasPrefix(line, "##") { - break +func listnips() <-chan nipInfo { + ch := make(chan nipInfo) + go func() { + defer close(ch) + resp, err := http.Get("https://raw.githubusercontent.com/nostr-protocol/nips/master/README.md") + if err != nil { + // TODO: handle error? but since chan, maybe send error somehow, but for now, just close + return } - if !strings.HasPrefix(line, "- [NIP-") { - continue + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return + } + bodyStr := string(body) + epoch := strings.Index(bodyStr, "## List") + if epoch == -1 { + return } - start := strings.Index(line, "[") - end := strings.Index(line, "]") - if start == -1 || end == -1 || end < start { - continue + lines := strings.SplitSeq(bodyStr[epoch+8:], "\n") + for line := range lines { + line = strings.TrimSpace(line) + if strings.HasPrefix(line, "##") { + break + } + if !strings.HasPrefix(line, "- [NIP-") { + continue + } + + start := strings.Index(line, "[") + end := strings.Index(line, "]") + if start == -1 || end == -1 || end < start { + continue + } + + content := line[start+1 : end] + + parts := strings.SplitN(content, ":", 2) + if len(parts) != 2 { + continue + } + + nipPart := parts[0] + descPart := parts[1] + + rest := line[end+1:] + linkStart := strings.Index(rest, "(") + linkEnd := strings.Index(rest, ")") + link := "" + if linkStart != -1 && linkEnd != -1 && linkEnd > linkStart { + link = rest[linkStart+1 : linkEnd] + } + + ch <- nipInfo{nipPart, strings.TrimSpace(descPart), link} } - - content := line[start+1 : end] - - parts := strings.SplitN(content, ":", 2) - if len(parts) != 2 { - continue - } - - nipPart := parts[0] - descPart := parts[1] - - rest := line[end+1:] - linkStart := strings.Index(rest, "(") - linkEnd := strings.Index(rest, ")") - link := "" - if linkStart != -1 && linkEnd != -1 && linkEnd > linkStart { - link = rest[linkStart+1 : linkEnd] - } - - if !yield(nipPart, strings.TrimSpace(descPart), link) { - break - } - } - return nil + }() + return ch } From f9335b0ab4a41f1033060ca9f3dc31fb8e64c2b1 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Thu, 27 Nov 2025 23:59:46 -0300 Subject: [PATCH 355/401] git: fetch repo from owner+identifier on init, and other things. --- git.go | 288 +++++++++++++++++++++++++++++++++------------------------ 1 file changed, 166 insertions(+), 122 deletions(-) diff --git a/git.go b/git.go index 89d8c62..262ba78 100644 --- a/git.go +++ b/git.go @@ -8,6 +8,7 @@ import ( "path/filepath" "slices" "strings" + "time" "fiatjaf.com/nostr" "fiatjaf.com/nostr/nip19" @@ -117,6 +118,60 @@ aside from those, there is also: } } + // prompt for identifier first + var identifier string + if c.String("identifier") != "" { + identifier = c.String("identifier") + } else if c.Bool("interactive") { + identifierPrompt := &survey.Input{ + Message: "identifier", + Default: baseName, + } + if err := survey.AskOne(identifierPrompt, &identifier); err != nil { + return err + } + } else { + identifier = baseName + } + + // prompt for owner pubkey + var owner nostr.PubKey + var ownerStr string + if c.String("owner") != "" { + owner, err = parsePubKey(ownerStr) + if err != nil { + return fmt.Errorf("invalid owner pubkey: %w", err) + } + ownerStr = nip19.EncodeNpub(owner) + } else if c.Bool("interactive") { + for { + ownerPrompt := &survey.Input{ + Message: "owner (npub or hex)", + } + if err := survey.AskOne(ownerPrompt, &ownerStr); err != nil { + return err + } + owner, err = parsePubKey(ownerStr) + if err == nil { + ownerStr = nip19.EncodeNpub(owner) + break + } + } + } else { + return fmt.Errorf("owner pubkey is required (use --owner or --interactive)") + } + + // try to fetch existing repository announcement (kind 30617) + log(" searching for existing events... ") + repo, _, err := fetchRepositoryAndState(ctx, owner, identifier, nil) + var fetchedRepo *nip34.Repository + if err == nil && repo.Event.ID != nostr.ZeroID { + fetchedRepo = &repo + log("found one from %s.\n", repo.Event.CreatedAt.Time().Format(time.DateOnly)) + } else { + log("none found.\n") + } + // extract clone URLs from nostr:// git remotes // (this is just for migrating from ngit) var defaultCloneURLs []string @@ -128,11 +183,11 @@ aside from those, there is also: if len(parts) >= 2 { nostrURL := parts[1] // parse nostr://npub.../relay_hostname/identifier - if owner, identifier, relays, err := parseRepositoryAddress(ctx, nostrURL); err == nil && len(relays) > 0 { + if remoteOwner, remoteIdentifier, relays, err := parseRepositoryAddress(ctx, nostrURL); err == nil && len(relays) > 0 { relayURL := relays[0] // convert to https://relay_hostname/npub.../identifier.git cloneURL := fmt.Sprintf("http%s/%s/%s.git", - relayURL[2:], nip19.EncodeNpub(owner), identifier) + relayURL[2:], nip19.EncodeNpub(remoteOwner), remoteIdentifier) defaultCloneURLs = appendUnique(defaultCloneURLs, cloneURL) } } @@ -161,21 +216,95 @@ aside from those, there is also: return defaultVals } - config := Nip34Config{ - Identifier: getValue(existingConfig.Identifier, c.String("identifier"), baseName), - Name: getValue(existingConfig.Name, c.String("name"), baseName), - Description: getValue(existingConfig.Description, c.String("description"), ""), - Web: getSliceValue(existingConfig.Web, c.StringSlice("web"), []string{}), - Owner: getValue(existingConfig.Owner, c.String("owner"), ""), - GraspServers: getSliceValue(existingConfig.GraspServers, c.StringSlice("grasp-servers"), []string{"gitnostr.com", "relay.ngit.dev"}), - EarliestUniqueCommit: getValue(existingConfig.EarliestUniqueCommit, c.String("earliest-unique-commit"), earliestCommit), - Maintainers: getSliceValue(existingConfig.Maintainers, c.StringSlice("maintainers"), []string{}), + // set config with fetched values or defaults + var config Nip34Config + if fetchedRepo != nil { + config = RepositoryToConfig(*fetchedRepo) + } else { + config = Nip34Config{ + Identifier: identifier, + Owner: ownerStr, + Name: baseName, + Description: "", + Web: []string{}, + GraspServers: []string{"gitnostr.com", "relay.ngit.dev"}, + EarliestUniqueCommit: earliestCommit, + Maintainers: []string{}, + } } + // override with flags and existing config + config.Identifier = getValue(existingConfig.Identifier, c.String("identifier"), config.Identifier) + config.Name = getValue(existingConfig.Name, c.String("name"), config.Name) + config.Description = getValue(existingConfig.Description, c.String("description"), config.Description) + config.Web = getSliceValue(existingConfig.Web, c.StringSlice("web"), config.Web) + config.Owner = getValue(existingConfig.Owner, c.String("owner"), config.Owner) + config.GraspServers = getSliceValue(existingConfig.GraspServers, c.StringSlice("grasp-servers"), config.GraspServers) + config.EarliestUniqueCommit = getValue(existingConfig.EarliestUniqueCommit, c.String("earliest-unique-commit"), config.EarliestUniqueCommit) + config.Maintainers = getSliceValue(existingConfig.Maintainers, c.StringSlice("maintainers"), config.Maintainers) + if c.Bool("interactive") { - if err := promptForConfig(&config); err != nil { + // prompt for name + namePrompt := &survey.Input{ + Message: "name", + Default: config.Name, + } + if err := survey.AskOne(namePrompt, &config.Name); err != nil { return err } + + // prompt for description + descPrompt := &survey.Input{ + Message: "description", + Default: config.Description, + } + if err := survey.AskOne(descPrompt, &config.Description); err != nil { + return err + } + + // prompt for grasp servers + graspServers, err := promptForStringList("grasp servers", config.GraspServers, []string{ + "gitnostr.com", + "relay.ngit.dev", + "pyramid.fiatjaf.com", + "git.shakespeare.dyi", + }, graspServerHost, nil) + if err != nil { + return err + } + config.GraspServers = graspServers + + // prompt for web URLs + webURLs, err := promptForStringList("web URLs", config.Web, []string{ + fmt.Sprintf("https://gitworkshop.dev/%s/%s", + nip19.EncodeNpub(nostr.MustPubKeyFromHex(config.Owner)), + config.Identifier, + ), + }, func(s string) string { + return "http" + nostr.NormalizeURL(s)[2:] + }, nil) + if err != nil { + return err + } + config.Web = webURLs + + // Prompt for maintainers + maintainers, err := promptForStringList("maintainers", config.Maintainers, []string{}, nil, func(s string) bool { + pk, err := parsePubKey(s) + if err != nil { + return false + } + if pk.Hex() == config.Owner { + return false + } + return true + }) + if err != nil { + return err + } + config.Maintainers = maintainers + + log("\n") } if err := config.Validate(); err != nil { @@ -197,7 +326,7 @@ aside from those, there is also: log("edit %s if needed, then run %s to publish.\n", color.CyanString("nip34.json"), - color.CyanString("nak git announce")) + color.CyanString("nak git sync")) return nil }, @@ -266,22 +395,7 @@ aside from those, there is also: } // write nip34.json inside cloned directory - localConfig := Nip34Config{ - Identifier: repo.ID, - Name: repo.Name, - Description: repo.Description, - Web: repo.Web, - Owner: nip19.EncodeNpub(repo.Event.PubKey), - GraspServers: make([]string, 0, len(repo.Relays)), - EarliestUniqueCommit: repo.EarliestUniqueCommitID, - Maintainers: make([]string, 0, len(repo.Maintainers)), - } - for _, r := range repo.Relays { - localConfig.GraspServers = append(localConfig.GraspServers, nostr.NormalizeURL(r)) - } - for _, m := range repo.Maintainers { - localConfig.Maintainers = append(localConfig.Maintainers, nip19.EncodeNpub(m)) - } + localConfig := RepositoryToConfig(repo) if err := localConfig.Validate(); err != nil { return fmt.Errorf("invalid config: %w", err) @@ -423,7 +537,7 @@ aside from those, there is also: pushSuccesses := 0 for _, relay := range repo.Relays { relayURL := nostr.NormalizeURL(relay) - remoteName := "nip34/grasp/" + strings.TrimPrefix(relayURL, "wss://") + remoteName := "nip34/grasp/" + graspServerHost(relayURL) remoteName = strings.TrimPrefix(remoteName, "ws://") log("pushing to %s...\n", color.CyanString(remoteName)) @@ -624,7 +738,6 @@ func promptForStringList( ) ([]string, error) { options := make([]string, 0, len(defaults)+len(existing)+1) options = append(options, defaults...) - options = append(options, "add another") // add existing not in options for _, item := range existing { @@ -633,6 +746,8 @@ func promptForStringList( } } + options = append(options, "add another") + selected := make([]string, len(existing)) copy(selected, existing) @@ -690,97 +805,6 @@ func promptForStringList( return selected, nil } -func promptForConfig(config *Nip34Config) error { - log("\nenter repository details (use arrow keys to navigate, space to select/deselect, enter to confirm):\n\n") - - // prompt for identifier - identifierPrompt := &survey.Input{ - Message: "identifier", - Default: config.Identifier, - } - if err := survey.AskOne(identifierPrompt, &config.Identifier); err != nil { - return err - } - - // prompt for name - namePrompt := &survey.Input{ - Message: "name", - Default: config.Name, - } - if err := survey.AskOne(namePrompt, &config.Name); err != nil { - return err - } - - // prompt for description - descPrompt := &survey.Input{ - Message: "description", - Default: config.Description, - } - if err := survey.AskOne(descPrompt, &config.Description); err != nil { - return err - } - - // prompt for owner - for { - ownerPrompt := &survey.Input{ - Message: "owner (npub or hex)", - Default: config.Owner, - } - if err := survey.AskOne(ownerPrompt, &config.Owner); err != nil { - return err - } - if pubkey, err := parsePubKey(config.Owner); err == nil { - config.Owner = pubkey.Hex() - break - } - } - - // prompt for grasp servers - graspServers, err := promptForStringList("grasp servers", config.GraspServers, []string{ - "gitnostr.com", - "relay.ngit.dev", - "pyramid.fiatjaf.com", - "git.shakespeare.dyi", - }, graspServerHost, nil) - if err != nil { - return err - } - config.GraspServers = graspServers - - // prompt for web URLs - webURLs, err := promptForStringList("web URLs", config.Web, []string{ - fmt.Sprintf("https://gitworkshop.dev/%s/%s", - nip19.EncodeNpub(nostr.MustPubKeyFromHex(config.Owner)), - config.Identifier, - ), - }, func(s string) string { - return "http" + nostr.NormalizeURL(s)[2:] - }, nil) - if err != nil { - return err - } - config.Web = webURLs - - // Prompt for maintainers - maintainers, err := promptForStringList("maintainers", config.Maintainers, []string{}, nil, func(s string) bool { - pk, err := parsePubKey(s) - if err != nil { - return false - } - if pk.Hex() == config.Owner { - return false - } - return true - }) - if err != nil { - return err - } - config.Maintainers = maintainers - - log("\n") - return nil -} - func gitSync(ctx context.Context, signer nostr.Keyer) (nip34.Repository, *nip34.RepositoryState, error) { // read current nip34.json localConfig, err := readNip34ConfigFile("") @@ -1380,6 +1404,26 @@ type Nip34Config struct { Maintainers []string `json:"maintainers"` } +func RepositoryToConfig(repo nip34.Repository) Nip34Config { + config := Nip34Config{ + Identifier: repo.ID, + Name: repo.Name, + Description: repo.Description, + Web: repo.Web, + Owner: nip19.EncodeNpub(repo.Event.PubKey), + GraspServers: make([]string, 0, len(repo.Relays)), + EarliestUniqueCommit: repo.EarliestUniqueCommitID, + Maintainers: make([]string, 0, len(repo.Maintainers)), + } + for _, r := range repo.Relays { + config.GraspServers = append(config.GraspServers, graspServerHost(r)) + } + for _, m := range repo.Maintainers { + config.Maintainers = append(config.Maintainers, nip19.EncodeNpub(m)) + } + return config +} + func (localConfig Nip34Config) Validate() error { _, err := parsePubKey(localConfig.Owner) if err != nil { From 210cf66d5f3f34950247b942227e1a3217da1386 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sun, 30 Nov 2025 08:57:27 -0300 Subject: [PATCH 356/401] git: fix a bunch of small bugs. --- git.go | 209 +++++++++++++++++++++++++++++++-------------------------- 1 file changed, 113 insertions(+), 96 deletions(-) diff --git a/git.go b/git.go index 262ba78..a6f5121 100644 --- a/git.go +++ b/git.go @@ -93,6 +93,9 @@ aside from those, there is also: } } + var defaultOwner string + var defaultIdentifier string + // check if nip34.json already exists existingConfig, err := readNip34ConfigFile("") if err == nil { @@ -100,22 +103,36 @@ aside from those, there is also: if !c.Bool("force") && !c.Bool("interactive") { return fmt.Errorf("nip34.json already exists, use --force to overwrite or --interactive to update") } + + defaultIdentifier = existingConfig.Identifier + defaultOwner = existingConfig.Owner + } else { + // extract info from nostr:// git remotes (this is just for migrating from ngit) + if output, err := exec.Command("git", "remote", "-v").Output(); err == nil { + remotes := strings.Split(strings.TrimSpace(string(output)), "\n") + for _, remote := range remotes { + if strings.Contains(remote, "nostr://") { + parts := strings.Fields(remote) + if len(parts) >= 2 { + nostrURL := parts[1] + // parse nostr://npub.../relay_hostname/identifier + if remoteOwner, remoteIdentifier, relays, err := parseRepositoryAddress(ctx, nostrURL); err == nil && len(relays) > 0 { + defaultIdentifier = remoteIdentifier + defaultOwner = nip19.EncodeNpub(remoteOwner) + } + } + } + } + } } // get repository base directory name for defaults - cwd, err := os.Getwd() - if err != nil { - return fmt.Errorf("failed to get current directory: %w", err) - } - baseName := filepath.Base(cwd) - - // get earliest unique commit - var earliestCommit string - if output, err := exec.Command("git", "rev-list", "--max-parents=0", "HEAD").Output(); err == nil { - earliest := strings.Split(strings.TrimSpace(string(output)), "\n") - if len(earliest) > 0 { - earliestCommit = earliest[0] + if defaultIdentifier == "" { + cwd, err := os.Getwd() + if err != nil { + return fmt.Errorf("failed to get current directory: %w", err) } + defaultIdentifier = filepath.Base(cwd) } // prompt for identifier first @@ -123,15 +140,14 @@ aside from those, there is also: if c.String("identifier") != "" { identifier = c.String("identifier") } else if c.Bool("interactive") { - identifierPrompt := &survey.Input{ + if err := survey.AskOne(&survey.Input{ Message: "identifier", - Default: baseName, - } - if err := survey.AskOne(identifierPrompt, &identifier); err != nil { + Default: defaultIdentifier, + }, &identifier); err != nil { return err } } else { - identifier = baseName + identifier = defaultIdentifier } // prompt for owner pubkey @@ -145,10 +161,10 @@ aside from those, there is also: ownerStr = nip19.EncodeNpub(owner) } else if c.Bool("interactive") { for { - ownerPrompt := &survey.Input{ + if err := survey.AskOne(&survey.Input{ Message: "owner (npub or hex)", - } - if err := survey.AskOne(ownerPrompt, &ownerStr); err != nil { + Default: defaultOwner, + }, &ownerStr); err != nil { return err } owner, err = parsePubKey(ownerStr) @@ -162,36 +178,40 @@ aside from those, there is also: } // try to fetch existing repository announcement (kind 30617) - log(" searching for existing events... ") - repo, _, err := fetchRepositoryAndState(ctx, owner, identifier, nil) var fetchedRepo *nip34.Repository - if err == nil && repo.Event.ID != nostr.ZeroID { - fetchedRepo = &repo - log("found one from %s.\n", repo.Event.CreatedAt.Time().Format(time.DateOnly)) - } else { - log("none found.\n") + if existingConfig.Identifier == "" { + log(" searching for existing events... ") + repo, _, err := fetchRepositoryAndState(ctx, owner, identifier, nil) + if err == nil && repo.Event.ID != nostr.ZeroID { + fetchedRepo = &repo + log("found one from %s.\n", repo.Event.CreatedAt.Time().Format(time.DateOnly)) + } else { + log("none found.\n") + } } - // extract clone URLs from nostr:// git remotes - // (this is just for migrating from ngit) - var defaultCloneURLs []string - if output, err := exec.Command("git", "remote", "-v").Output(); err == nil { - remotes := strings.Split(strings.TrimSpace(string(output)), "\n") - for _, remote := range remotes { - if strings.Contains(remote, "nostr://") { - parts := strings.Fields(remote) - if len(parts) >= 2 { - nostrURL := parts[1] - // parse nostr://npub.../relay_hostname/identifier - if remoteOwner, remoteIdentifier, relays, err := parseRepositoryAddress(ctx, nostrURL); err == nil && len(relays) > 0 { - relayURL := relays[0] - // convert to https://relay_hostname/npub.../identifier.git - cloneURL := fmt.Sprintf("http%s/%s/%s.git", - relayURL[2:], nip19.EncodeNpub(remoteOwner), remoteIdentifier) - defaultCloneURLs = appendUnique(defaultCloneURLs, cloneURL) - } - } - } + // set config with fetched values or defaults + var config Nip34Config + if fetchedRepo != nil { + config = RepositoryToConfig(*fetchedRepo) + } else if existingConfig.Identifier != "" { + config = existingConfig + } else { + // get earliest unique commit + var earliestCommit string + if output, err := exec.Command("git", "rev-list", "--max-parents=0", "HEAD").Output(); err == nil { + earliestCommit = strings.TrimSpace(string(output)) + } + + config = Nip34Config{ + Identifier: identifier, + Owner: ownerStr, + Name: identifier, + Description: "", + Web: []string{}, + GraspServers: []string{"gitnostr.com", "relay.ngit.dev"}, + EarliestUniqueCommit: earliestCommit, + Maintainers: []string{}, } } @@ -216,23 +236,6 @@ aside from those, there is also: return defaultVals } - // set config with fetched values or defaults - var config Nip34Config - if fetchedRepo != nil { - config = RepositoryToConfig(*fetchedRepo) - } else { - config = Nip34Config{ - Identifier: identifier, - Owner: ownerStr, - Name: baseName, - Description: "", - Web: []string{}, - GraspServers: []string{"gitnostr.com", "relay.ngit.dev"}, - EarliestUniqueCommit: earliestCommit, - Maintainers: []string{}, - } - } - // override with flags and existing config config.Identifier = getValue(existingConfig.Identifier, c.String("identifier"), config.Identifier) config.Name = getValue(existingConfig.Name, c.String("name"), config.Name) @@ -245,20 +248,18 @@ aside from those, there is also: if c.Bool("interactive") { // prompt for name - namePrompt := &survey.Input{ + if err := survey.AskOne(&survey.Input{ Message: "name", Default: config.Name, - } - if err := survey.AskOne(namePrompt, &config.Name); err != nil { + }, &config.Name); err != nil { return err } // prompt for description - descPrompt := &survey.Input{ + if err := survey.AskOne(&survey.Input{ Message: "description", Default: config.Description, - } - if err := survey.AskOne(descPrompt, &config.Description); err != nil { + }, &config.Description); err != nil { return err } @@ -288,6 +289,14 @@ aside from those, there is also: } config.Web = webURLs + // prompt for earliest unique commit + if err := survey.AskOne(&survey.Input{ + Message: "earliest unique commit", + Default: config.EarliestUniqueCommit, + }, &config.EarliestUniqueCommit); err != nil { + return err + } + // Prompt for maintainers maintainers, err := promptForStringList("maintainers", config.Maintainers, []string{}, nil, func(s string) bool { pk, err := parsePubKey(s) @@ -537,8 +546,7 @@ aside from those, there is also: pushSuccesses := 0 for _, relay := range repo.Relays { relayURL := nostr.NormalizeURL(relay) - remoteName := "nip34/grasp/" + graspServerHost(relayURL) - remoteName = strings.TrimPrefix(remoteName, "ws://") + remoteName := gitRemoteName(relayURL) log("pushing to %s...\n", color.CyanString(remoteName)) pushArgs := []string{"push", remoteName, fmt.Sprintf("%s:refs/heads/%s", localBranch, remoteBranch)} @@ -731,16 +739,16 @@ aside from those, there is also: func promptForStringList( name string, - existing []string, defaults []string, + alternatives []string, normalize func(string) string, validate func(string) bool, ) ([]string, error) { - options := make([]string, 0, len(defaults)+len(existing)+1) + options := make([]string, 0, len(defaults)+len(alternatives)+1) options = append(options, defaults...) // add existing not in options - for _, item := range existing { + for _, item := range alternatives { if !slices.Contains(options, item) { options = append(options, item) } @@ -748,34 +756,28 @@ func promptForStringList( options = append(options, "add another") - selected := make([]string, len(existing)) - copy(selected, existing) + selected := make([]string, len(defaults)) + copy(selected, defaults) for { - prompt := &survey.MultiSelect{ + newSelected := []string{} + if err := survey.AskOne(&survey.MultiSelect{ Message: name, Options: options, Default: selected, PageSize: 20, - } - - if err := survey.AskOne(prompt, &selected); err != nil { + }, &newSelected); err != nil { return nil, err } + selected = newSelected if slices.Contains(selected, "add another") { selected = slices.DeleteFunc(selected, func(s string) bool { return s == "add another" }) - singular := name - if strings.HasSuffix(singular, "s") { - singular = singular[:len(singular)-1] - } - - newPrompt := &survey.Input{ - Message: fmt.Sprintf("enter new %s", singular), - } var newItem string - if err := survey.AskOne(newPrompt, &newItem); err != nil { + if err := survey.AskOne(&survey.Input{ + Message: fmt.Sprintf("enter new %s", strings.TrimSuffix(name, "s")), + }, &newItem); err != nil { return nil, err } @@ -938,7 +940,7 @@ func gitSync(ctx context.Context, signer nostr.Keyer) (nip34.Repository, *nip34. func fetchFromRemotes(ctx context.Context, targetDir string, repo nip34.Repository) { // fetch from each grasp remote for _, grasp := range repo.Relays { - remoteName := "nip34/grasp/" + strings.Split(grasp, "/")[2] + remoteName := gitRemoteName(grasp) logverbose("fetching from %s...\n", remoteName) fetchCmd := exec.Command("git", "fetch", remoteName) @@ -964,14 +966,16 @@ func gitSetupRemotes(ctx context.Context, dir string, repo nip34.Repository) { return } - // delete all nip34/grasp/ remotes + // delete all nip34/grasp/ remotes that we don't have anymore in repo remotes := strings.Split(strings.TrimSpace(string(output)), "\n") for i, remote := range remotes { remote = strings.TrimSpace(remote) remotes[i] = remote if strings.HasPrefix(remote, "nip34/grasp/") { - if !slices.Contains(repo.Relays, nostr.NormalizeURL(remote[12:])) { + graspURL := rebuildGraspURLFromRemote(remote) + + if !slices.Contains(repo.Relays, nostr.NormalizeURL(graspURL)) { delCmd := exec.Command("git", "remote", "remove", remote) if dir != "" { delCmd.Dir = dir @@ -985,7 +989,7 @@ func gitSetupRemotes(ctx context.Context, dir string, repo nip34.Repository) { // create new remotes for each grasp server for _, relay := range repo.Relays { - remote := "nip34/grasp/" + strings.TrimPrefix(relay, "wss://") + remote := gitRemoteName(relay) if !slices.Contains(remotes, remote) { // construct the git URL @@ -1391,8 +1395,6 @@ func figureOutBranches(c *cli.Command, refspec string, isPush bool) ( return localBranch, remoteBranch, nil } -func graspServerHost(s string) string { return strings.SplitN(nostr.NormalizeURL(s), "/", 3)[2] } - type Nip34Config struct { Identifier string `json:"identifier"` Name string `json:"name"` @@ -1474,3 +1476,18 @@ func (localConfig Nip34Config) ToRepository() nip34.Repository { return localRepo } + +func gitRemoteName(graspURL string) string { + host := graspServerHost(graspURL) + host = strings.Replace(host, ":", "__", 1) + return "nip34/grasp/" + host +} + +func rebuildGraspURLFromRemote(remoteName string) string { + host := strings.TrimPrefix(remoteName, "nip34/grasp/") + return strings.Replace(host, "__", ":", 1) +} + +func graspServerHost(s string) string { + return strings.SplitN(nostr.NormalizeURL(s), "/", 3)[2] +} From 852fe6bdfbb1ba866f2576ae510270c9369265f2 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sun, 30 Nov 2025 22:21:56 -0300 Subject: [PATCH 357/401] git: more resiliency when updating nip34.json --- git.go | 114 +++++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 82 insertions(+), 32 deletions(-) diff --git a/git.go b/git.go index a6f5121..4cad493 100644 --- a/git.go +++ b/git.go @@ -181,7 +181,7 @@ aside from those, there is also: var fetchedRepo *nip34.Repository if existingConfig.Identifier == "" { log(" searching for existing events... ") - repo, _, err := fetchRepositoryAndState(ctx, owner, identifier, nil) + repo, _, _, err := fetchRepositoryAndState(ctx, owner, identifier, nil) if err == nil && repo.Event.ID != nostr.ZeroID { fetchedRepo = &repo log("found one from %s.\n", repo.Event.CreatedAt.Time().Format(time.DateOnly)) @@ -367,7 +367,7 @@ aside from those, there is also: } // fetch repository metadata and state - repo, state, err := fetchRepositoryAndState(ctx, owner, identifier, relayHints) + repo, _, state, err := fetchRepositoryAndState(ctx, owner, identifier, relayHints) if err != nil { return err } @@ -555,6 +555,7 @@ aside from those, there is also: } pushCmd := exec.Command("git", pushArgs...) pushCmd.Stderr = os.Stderr + pushCmd.Stdout = os.Stdout if err := pushCmd.Run(); err != nil { log("! failed to push to %s: %v\n", color.YellowString(remoteName), err) } else { @@ -821,9 +822,26 @@ func gitSync(ctx context.Context, signer nostr.Keyer) (nip34.Repository, *nip34. } // fetch repository announcement and state from relays - repo, state, err := fetchRepositoryAndState(ctx, owner, localConfig.Identifier, localConfig.GraspServers) - if err != nil && repo.Event.ID == nostr.ZeroID { - log("couldn't fetch repository metadata (%s), will publish now\n", err) + repo, upToDateRelays, state, err := fetchRepositoryAndState(ctx, owner, localConfig.Identifier, localConfig.GraspServers) + notUpToDate := func(graspServer string) bool { + return !slices.Contains(upToDateRelays, nostr.NormalizeURL(graspServer)) + } + if upToDateRelays == nil || slices.ContainsFunc(localConfig.GraspServers, notUpToDate) { + var relays []string + if upToDateRelays == nil { + // condition 1 + relays = append(sys.FetchOutboxRelays(ctx, owner, 3), localConfig.GraspServers...) + log("couldn't fetch repository metadata (%s), will publish now\n", err) + } else { + // condition 2 + relays = make([]string, 0, len(localConfig.GraspServers)-1) + for _, gs := range localConfig.GraspServers { + if notUpToDate(gs) { + relays = append(relays, graspServerHost(gs)) + } + } + log("some grasp servers (%v) are not up-to-date, will publish to them\n", relays) + } // create a local repository object from config and publish it localRepo := localConfig.ToRepository() @@ -840,7 +858,6 @@ func gitSync(ctx context.Context, signer nostr.Keyer) (nip34.Repository, *nip34. return repo, state, fmt.Errorf("failed to sign announcement: %w", err) } - relays := append(sys.FetchOutboxRelays(ctx, owner, 3), localConfig.GraspServers...) for res := range sys.Pool.PublishMany(ctx, relays, event) { if res.Error != nil { log("! error publishing to %s: %v\n", color.YellowString(res.RelayURL), res.Error) @@ -975,38 +992,60 @@ func gitSetupRemotes(ctx context.Context, dir string, repo nip34.Repository) { if strings.HasPrefix(remote, "nip34/grasp/") { graspURL := rebuildGraspURLFromRemote(remote) + getUrlCmd := exec.Command("git", "remote", "get-url", remote) + if dir != "" { + getUrlCmd.Dir = dir + } + if output, err := getUrlCmd.Output(); err != nil { + panic(fmt.Errorf("failed to read remote (%s) url from git: %s", remote, err)) + } else { + // check if the remote url is correct so we can update it if not + gitURL := fmt.Sprintf("http%s/%s/%s.git", nostr.NormalizeURL(graspURL)[2:], nip19.EncodeNpub(repo.PubKey), repo.ID) + if strings.TrimSpace(string(output)) != gitURL { + goto delete + } + } + + // check if this remote is not present in our grasp list anymore if !slices.Contains(repo.Relays, nostr.NormalizeURL(graspURL)) { - delCmd := exec.Command("git", "remote", "remove", remote) - if dir != "" { - delCmd.Dir = dir - } - if err := delCmd.Run(); err != nil { - logverbose("failed to remove remote %s: %v\n", remote, err) - } + goto delete + } + + continue + + delete: + logverbose("deleting remote %s\n", remote) + delCmd := exec.Command("git", "remote", "remove", remote) + if dir != "" { + delCmd.Dir = dir + } + if err := delCmd.Run(); err != nil { + logverbose("failed to remove remote %s: %v\n", remote, err) } } } // create new remotes for each grasp server + remotes = strings.Split(strings.TrimSpace(string(output)), "\n") for _, relay := range repo.Relays { remote := gitRemoteName(relay) + gitURL := fmt.Sprintf("http%s/%s/%s.git", nostr.NormalizeURL(relay)[2:], nip19.EncodeNpub(repo.PubKey), repo.ID) - if !slices.Contains(remotes, remote) { - // construct the git URL - gitURL := fmt.Sprintf("http%s/%s/%s.git", - relay[2:], nip19.EncodeNpub(repo.PubKey), repo.ID) + if slices.Contains(remotes, remote) { + continue + } - addCmd := exec.Command("git", "remote", "add", remote, gitURL) - if dir != "" { - addCmd.Dir = dir - } - if out, err := addCmd.Output(); err != nil { - var stderr string - if exiterr, ok := err.(*exec.ExitError); ok { - stderr = string(exiterr.Stderr) - } - logverbose("failed to add remote %s: %s %s\n", remote, stderr, string(out)) + logverbose("adding new remote for '%s'\n", relay) + addCmd := exec.Command("git", "remote", "add", remote, gitURL) + if dir != "" { + addCmd.Dir = dir + } + if out, err := addCmd.Output(); err != nil { + var stderr string + if exiterr, ok := err.(*exec.ExitError); ok { + stderr = string(exiterr.Stderr) } + logverbose("failed to add remote %s: %s %s\n", remote, stderr, string(out)) } } } @@ -1069,7 +1108,7 @@ func fetchRepositoryAndState( pubkey nostr.PubKey, identifier string, relayHints []string, -) (repo nip34.Repository, state *nip34.RepositoryState, err error) { +) (repo nip34.Repository, upToDateRelays []string, state *nip34.RepositoryState, err error) { // fetch repository announcement (30617) relays := appendUnique(relayHints, sys.FetchOutboxRelays(ctx, pubkey, 3)...) for ie := range sys.Pool.FetchMany(ctx, relays, nostr.Filter{ @@ -1079,13 +1118,24 @@ func fetchRepositoryAndState( "d": []string{identifier}, }, Limit: 2, - }, nostr.SubscriptionOptions{Label: "nak-git"}) { + }, nostr.SubscriptionOptions{ + Label: "nak-git", + CheckDuplicate: func(id nostr.ID, relay string) bool { + return false + }, + }) { if ie.Event.CreatedAt > repo.CreatedAt { repo = nip34.ParseRepository(ie.Event) + + // reset this list as the previous was for relays with the older version + upToDateRelays = []string{ie.Relay.URL} + } else if ie.Event.CreatedAt == repo.CreatedAt { + // we discard this because it's the same, but this relay is up-to-date + upToDateRelays = append(upToDateRelays, ie.Relay.URL) } } if repo.Event.ID == nostr.ZeroID { - return repo, state, fmt.Errorf("no repository announcement (kind 30617) found for %s", identifier) + return repo, upToDateRelays, state, fmt.Errorf("no repository announcement (kind 30617) found for %s", identifier) } // fetch repository state (30618) @@ -1115,10 +1165,10 @@ func fetchRepositoryAndState( } } if stateErr != nil { - return repo, state, stateErr + return repo, upToDateRelays, state, stateErr } - return repo, state, nil + return repo, upToDateRelays, state, nil } type StateErr struct{ string } From a422b5f708703044c8934e698d1b512b766f0bcf Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Mon, 1 Dec 2025 20:33:13 -0300 Subject: [PATCH 358/401] sync command for using a negentropy hack to sync two relays with each other. closes https://github.com/fiatjaf/nak/issues/84 --- go.mod | 4 +- go.sum | 14 +- main.go | 1 + sync.go | 464 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 479 insertions(+), 4 deletions(-) create mode 100644 sync.go diff --git a/go.mod b/go.mod index 8fa58af..9ff63dd 100644 --- a/go.mod +++ b/go.mod @@ -4,10 +4,11 @@ go 1.25 require ( fiatjaf.com/lib v0.3.1 - fiatjaf.com/nostr v0.0.0-20251126101225-44130595c606 + fiatjaf.com/nostr v0.0.0-20251201232830-91548fa0a157 github.com/AlecAivazis/survey/v2 v2.3.7 github.com/bep/debounce v1.2.1 github.com/btcsuite/btcd/btcec/v2 v2.3.6 + github.com/charmbracelet/glamour v0.10.0 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 @@ -41,7 +42,6 @@ require ( github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect - github.com/charmbracelet/glamour v0.10.0 // indirect github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 // indirect github.com/charmbracelet/x/ansi v0.8.0 // indirect github.com/charmbracelet/x/cellbuf v0.0.13 // indirect diff --git a/go.sum b/go.sum index 875f706..b1a3a95 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ fiatjaf.com/lib v0.3.1 h1:/oFQwNtFRfV+ukmOCxfBEAuayoLwXp4wu2/fz5iHpwA= fiatjaf.com/lib v0.3.1/go.mod h1:Ycqq3+mJ9jAWu7XjbQI1cVr+OFgnHn79dQR5oTII47g= -fiatjaf.com/nostr v0.0.0-20251126101225-44130595c606 h1:wQHJ0TFA0Fuq92p/6u6AbsBFq6ZVToSdxV6puXVIruI= -fiatjaf.com/nostr v0.0.0-20251126101225-44130595c606/go.mod h1:ue7yw0zHfZj23Ml2kVSdBx0ENEaZiuvGxs/8VEN93FU= +fiatjaf.com/nostr v0.0.0-20251201232830-91548fa0a157 h1:14yLsO2HwpS2CLIKFvLMDp8tVEDahwdC8OeG6NGaL+M= +fiatjaf.com/nostr v0.0.0-20251201232830-91548fa0a157/go.mod h1:ue7yw0zHfZj23Ml2kVSdBx0ENEaZiuvGxs/8VEN93FU= github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ= github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= github.com/FastFilter/xorfilter v0.2.1 h1:lbdeLG9BdpquK64ZsleBS8B4xO/QW1IM0gMzF7KaBKc= @@ -13,12 +13,18 @@ github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDe 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/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE= +github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E= github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I= +github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= +github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= +github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= @@ -65,6 +71,8 @@ github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2ll github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k= github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a h1:G99klV19u0QnhiizODirwVksQB91TJKV/UaTnACcG30= +github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf h1:rLG0Yb6MQSDKdB52aGX55JT1oi0P0Kuaj7wi1bLUpnI= github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf/go.mod h1:B3UgsnsBZS/eX42BlaNiJkD1pPOUa+oF1IYC6Yd2CEU= github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= @@ -138,6 +146,8 @@ github.com/hablullah/go-juliandays v1.0.0 h1:A8YM7wIj16SzlKT0SRJc9CD29iiaUzpBLzh github.com/hablullah/go-juliandays v1.0.0/go.mod h1:0JOYq4oFOuDja+oospuc61YoX+uNEn7Z6uHYTbBzdGc= github.com/hanwen/go-fuse/v2 v2.7.2 h1:SbJP1sUP+n1UF8NXBA14BuojmTez+mDgOk0bC057HQw= github.com/hanwen/go-fuse/v2 v2.7.2/go.mod h1:ugNaD/iv5JYyS1Rcvi57Wz7/vrLQJo10mmketmoef48= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= diff --git a/main.go b/main.go index c3438e9..aac465d 100644 --- a/main.go +++ b/main.go @@ -50,6 +50,7 @@ var app = &cli.Command{ publish, git, nip, + syncCmd, }, Version: version, Flags: []cli.Flag{ diff --git a/sync.go b/sync.go new file mode 100644 index 0000000..348d97f --- /dev/null +++ b/sync.go @@ -0,0 +1,464 @@ +package main + +import ( + "bytes" + "context" + "errors" + "fmt" + "sync" + + "fiatjaf.com/nostr" + "fiatjaf.com/nostr/nip77" + "fiatjaf.com/nostr/nip77/negentropy" + "fiatjaf.com/nostr/nip77/negentropy/storage" + "github.com/urfave/cli/v3" +) + +var syncCmd = &cli.Command{ + Name: "sync", + Usage: "sync events between two relays using negentropy", + Description: `uses nip77 negentropy to sync events between two relays`, + ArgsUsage: " ", + Flags: reqFilterFlags, + Action: func(ctx context.Context, c *cli.Command) error { + args := c.Args().Slice() + if len(args) != 2 { + return fmt.Errorf("need exactly two relay URLs: source and target") + } + + filter := nostr.Filter{} + if err := applyFlagsToFilter(c, &filter); err != nil { + return err + } + + peerA, err := NewRelayThirdPartyRemote(ctx, args[0]) + if err != nil { + return fmt.Errorf("error setting up %s: %w", args[0], err) + } + + peerB, err := NewRelayThirdPartyRemote(ctx, args[1]) + if err != nil { + return fmt.Errorf("error setting up %s: %w", args[1], err) + } + + tpn := NewThirdPartyNegentropy( + peerA, + peerB, + filter, + ) + + wg := sync.WaitGroup{} + + wg.Go(func() { + err = tpn.Run(ctx) + }) + + wg.Go(func() { + type op struct { + src *nostr.Relay + dst *nostr.Relay + ids []nostr.ID + } + + pending := []op{ + {peerA.relay, peerB.relay, make([]nostr.ID, 0, 30)}, + {peerB.relay, peerA.relay, make([]nostr.ID, 0, 30)}, + } + + for delta := range tpn.Deltas { + have := delta.Have.relay + havenot := delta.HaveNot.relay + logverbose("%s has %s, %s doesn't.\n", have.URL, delta.ID.Hex(), havenot.URL) + + idx := 0 // peerA + if have == peerB.relay { + idx = 1 // peerB + } + pending[idx].ids = append(pending[idx].ids, delta.ID) + + // every 30 ids do a fetch-and-publish + if len(pending[idx].ids) == 30 { + for evt := range pending[idx].src.QueryEvents(nostr.Filter{IDs: pending[idx].ids}) { + pending[idx].dst.Publish(ctx, evt) + } + pending[idx].ids = pending[idx].ids[:0] + } + } + + // do it for the remaining ids + for _, op := range pending { + if len(op.ids) > 0 { + for evt := range op.src.QueryEvents(nostr.Filter{IDs: op.ids}) { + op.dst.Publish(ctx, evt) + } + } + } + }) + + wg.Wait() + + return err + }, +} + +type ThirdPartyNegentropy struct { + PeerA *RelayThirdPartyRemote + PeerB *RelayThirdPartyRemote + Filter nostr.Filter + + Deltas chan Delta +} + +type Delta struct { + ID nostr.ID + Have *RelayThirdPartyRemote + HaveNot *RelayThirdPartyRemote +} + +type boundKey string + +func getBoundKey(b negentropy.Bound) boundKey { + return boundKey(fmt.Sprintf("%d:%x", b.Timestamp, b.IDPrefix)) +} + +type RelayThirdPartyRemote struct { + relay *nostr.Relay + messages chan string + err error +} + +func NewRelayThirdPartyRemote(ctx context.Context, url string) (*RelayThirdPartyRemote, error) { + rtpr := &RelayThirdPartyRemote{ + messages: make(chan string, 3), + } + + var err error + rtpr.relay, err = nostr.RelayConnect(ctx, url, nostr.RelayOptions{ + CustomHandler: func(data string) { + envelope := nip77.ParseNegMessage(data) + if envelope == nil { + return + } + switch env := envelope.(type) { + case *nip77.OpenEnvelope, *nip77.CloseEnvelope: + rtpr.err = fmt.Errorf("unexpected %s received from relay", env.Label()) + return + case *nip77.ErrorEnvelope: + rtpr.err = fmt.Errorf("relay returned a %s: %s", env.Label(), env.Reason) + return + case *nip77.MessageEnvelope: + rtpr.messages <- env.Message + } + }, + }) + if err != nil { + return nil, err + } + + return rtpr, nil +} + +func (rtpr *RelayThirdPartyRemote) SendInitialMessage(filter nostr.Filter, msg string) error { + msgj, _ := json.Marshal(nip77.OpenEnvelope{ + SubscriptionID: "sync3", + Filter: filter, + Message: msg, + }) + return rtpr.relay.WriteWithError(msgj) +} + +func (rtpr *RelayThirdPartyRemote) SendMessage(msg string) error { + msgj, _ := json.Marshal(nip77.MessageEnvelope{ + SubscriptionID: "sync3", + Message: msg, + }) + return rtpr.relay.WriteWithError(msgj) +} + +func (rtpr *RelayThirdPartyRemote) SendClose() error { + msgj, _ := json.Marshal(nip77.CloseEnvelope{ + SubscriptionID: "sync3", + }) + return rtpr.relay.WriteWithError(msgj) +} + +var thirdPartyRemoteEndOfMessages = errors.New("the-end") + +func (rtpr *RelayThirdPartyRemote) Receive() (string, error) { + if rtpr.err != nil { + return "", rtpr.err + } + if msg, ok := <-rtpr.messages; ok { + return msg, nil + } + return "", thirdPartyRemoteEndOfMessages +} + +func NewThirdPartyNegentropy(peerA, peerB *RelayThirdPartyRemote, filter nostr.Filter) *ThirdPartyNegentropy { + return &ThirdPartyNegentropy{ + PeerA: peerA, + PeerB: peerB, + Filter: filter, + Deltas: make(chan Delta, 100), + } +} + +func (n *ThirdPartyNegentropy) Run(ctx context.Context) error { + peerAIds := make(map[nostr.ID]struct{}) + peerBIds := make(map[nostr.ID]struct{}) + peerASkippedBounds := make(map[boundKey]struct{}) + peerBSkippedBounds := make(map[boundKey]struct{}) + + // send an empty message to A to start things up + initialMsg := createInitialMessage() + err := n.PeerA.SendInitialMessage(n.Filter, initialMsg) + if err != nil { + return err + } + + hasSentInitialMessageToB := false + + for { + // receive message from A + msgA, err := n.PeerA.Receive() + if err != nil { + return err + } + msgAb, _ := nostr.HexDecodeString(msgA) + if len(msgAb) == 1 { + break + } + + msgToB, err := parseMessageBuildNext( + msgA, + peerBSkippedBounds, + func(id nostr.ID) { + if _, exists := peerBIds[id]; exists { + delete(peerBIds, id) + } else { + peerAIds[id] = struct{}{} + } + }, + func(boundKey boundKey) { + peerASkippedBounds[boundKey] = struct{}{} + }, + ) + if err != nil { + return err + } + + // emit deltas from B after receiving message from A + for id := range peerBIds { + select { + case n.Deltas <- Delta{ID: id, Have: n.PeerB, HaveNot: n.PeerA}: + case <-ctx.Done(): + return context.Cause(ctx) + } + delete(peerBIds, id) + } + + if len(msgToB) == 2 { + // exit condition (no more messages to send) + break + } + + // send message to B + if hasSentInitialMessageToB { + err = n.PeerB.SendMessage(msgToB) + } else { + err = n.PeerB.SendInitialMessage(n.Filter, msgToB) + hasSentInitialMessageToB = true + } + if err != nil { + return err + } + + // receive message from B + msgB, err := n.PeerB.Receive() + if err != nil { + return err + } + msgBb, _ := nostr.HexDecodeString(msgB) + if len(msgBb) == 1 { + break + } + + msgToA, err := parseMessageBuildNext( + msgB, + peerASkippedBounds, + func(id nostr.ID) { + if _, exists := peerAIds[id]; exists { + delete(peerAIds, id) + } else { + peerBIds[id] = struct{}{} + } + }, + func(boundKey boundKey) { + peerBSkippedBounds[boundKey] = struct{}{} + }, + ) + if err != nil { + return err + } + + // emit deltas from A after receiving message from B + for id := range peerAIds { + select { + case n.Deltas <- Delta{ID: id, Have: n.PeerA, HaveNot: n.PeerB}: + case <-ctx.Done(): + return context.Cause(ctx) + } + delete(peerAIds, id) + } + + if len(msgToA) == 2 { + // exit condition (no more messages to send) + break + } + + // send message to A + err = n.PeerA.SendMessage(msgToA) + if err != nil { + return err + } + } + + // emit remaining deltas before exit + for id := range peerAIds { + select { + case n.Deltas <- Delta{ID: id, Have: n.PeerA, HaveNot: n.PeerB}: + case <-ctx.Done(): + return context.Cause(ctx) + } + } + for id := range peerBIds { + select { + case n.Deltas <- Delta{ID: id, Have: n.PeerB, HaveNot: n.PeerA}: + case <-ctx.Done(): + return context.Cause(ctx) + } + } + + n.PeerA.SendClose() + n.PeerB.SendClose() + close(n.Deltas) + + return nil +} + +func createInitialMessage() string { + output := bytes.NewBuffer(make([]byte, 0, 64)) + output.WriteByte(negentropy.ProtocolVersion) + + dummy := negentropy.BoundWriter{} + dummy.WriteBound(output, negentropy.InfiniteBound) + output.WriteByte(byte(negentropy.FingerprintMode)) + + // hardcoded random fingerprint + fingerprint := [negentropy.FingerprintSize]byte{ + 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, + 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, + } + output.Write(fingerprint[:]) + + return nostr.HexEncodeToString(output.Bytes()) +} + +func parseMessageBuildNext( + msg string, + skippedBounds map[boundKey]struct{}, + idCallback func(id nostr.ID), + skipCallback func(boundKey boundKey), +) (string, error) { + msgb, err := nostr.HexDecodeString(msg) + if err != nil { + return "", err + } + + br := &negentropy.BoundReader{} + bw := &negentropy.BoundWriter{} + + nextMsg := bytes.NewBuffer(make([]byte, 0, len(msgb))) + acc := &storage.Accumulator{} // this will be used for building our own fingerprints and also as a placeholder + + reader := bytes.NewReader(msgb) + pv, err := reader.ReadByte() + if err != nil { + return "", err + } + if pv != negentropy.ProtocolVersion { + return "", fmt.Errorf("unsupported protocol version %v", pv) + } + + nextMsg.WriteByte(pv) + + for reader.Len() > 0 { + bound, err := br.ReadBound(reader) + if err != nil { + return "", err + } + + modeVal, err := negentropy.ReadVarInt(reader) + if err != nil { + return "", err + } + mode := negentropy.Mode(modeVal) + + switch mode { + case negentropy.SkipMode: + skipCallback(getBoundKey(bound)) + if _, skipped := skippedBounds[getBoundKey(bound)]; !skipped { + bw.WriteBound(nextMsg, bound) + negentropy.WriteVarInt(nextMsg, int(negentropy.SkipMode)) + } + + case negentropy.FingerprintMode: + _, err = reader.Read(acc.Buf[0:negentropy.FingerprintSize] /* use this buffer as a dummy */) + if err != nil { + return "", err + } + + if _, skipped := skippedBounds[getBoundKey(bound)]; !skipped { + bw.WriteBound(nextMsg, bound) + negentropy.WriteVarInt(nextMsg, int(negentropy.FingerprintMode)) + nextMsg.Write(acc.Buf[0:negentropy.FingerprintSize] /* idem */) + } + case negentropy.IdListMode: + // when receiving an idlist we will never send this bound again to this peer + skipCallback(getBoundKey(bound)) + + // and instead of sending these ids to the other peer we'll send a fingerprint + acc.Reset() + + numIds, err := negentropy.ReadVarInt(reader) + if err != nil { + return "", err + } + + for range numIds { + id := nostr.ID{} + + _, err = reader.Read(id[:]) + if err != nil { + return "", err + } + + idCallback(id) + + acc.AddBytes(id[:]) + } + + if _, skipped := skippedBounds[getBoundKey(bound)]; !skipped { + fingerprint := acc.GetFingerprint(numIds) + + bw.WriteBound(nextMsg, bound) + negentropy.WriteVarInt(nextMsg, int(negentropy.FingerprintMode)) + nextMsg.Write(fingerprint[:]) + } + default: + return "", fmt.Errorf("unknown mode %v", mode) + } + } + + return nostr.HexEncodeToString(nextMsg.Bytes()), nil +} From 11228d7082d0f8cb9d986e85f854f72d926c5f96 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Mon, 1 Dec 2025 20:50:41 -0300 Subject: [PATCH 359/401] gift-wrap. --- encrypt_decrypt.go | 4 +- gift.go | 192 +++++++++++++++++++++++++++++++++++++++++++++ main.go | 1 + 3 files changed, 195 insertions(+), 2 deletions(-) create mode 100644 gift.go diff --git a/encrypt_decrypt.go b/encrypt_decrypt.go index 66d31a7..b67391f 100644 --- a/encrypt_decrypt.go +++ b/encrypt_decrypt.go @@ -17,7 +17,7 @@ var encrypt = &cli.Command{ defaultKeyFlags, &PubKeyFlag{ Name: "recipient-pubkey", - Aliases: []string{"p", "tgt", "target", "pubkey"}, + Aliases: []string{"p", "tgt", "target", "pubkey", "to"}, Required: true, }, &cli.BoolFlag{ @@ -79,7 +79,7 @@ var decrypt = &cli.Command{ defaultKeyFlags, &PubKeyFlag{ Name: "sender-pubkey", - Aliases: []string{"p", "src", "source", "pubkey"}, + Aliases: []string{"p", "src", "source", "pubkey", "from"}, Required: true, }, &cli.BoolFlag{ diff --git a/gift.go b/gift.go new file mode 100644 index 0000000..dbb6097 --- /dev/null +++ b/gift.go @@ -0,0 +1,192 @@ +package main + +import ( + "context" + "fmt" + "math/rand" + "time" + + "fiatjaf.com/nostr" + "fiatjaf.com/nostr/nip44" + "github.com/mailru/easyjson" + "github.com/urfave/cli/v3" +) + +var gift = &cli.Command{ + Name: "gift", + Usage: "gift-wraps (or unwraps) an event according to NIP-59", + Description: `example: + nak event | nak gift wrap --sec -p | nak gift unwrap --sec --from `, + DisableSliceFlagSeparator: true, + Commands: []*cli.Command{ + { + Name: "wrap", + Flags: append( + defaultKeyFlags, + &PubKeyFlag{ + Name: "recipient-pubkey", + Aliases: []string{"p", "tgt", "target", "pubkey", "to"}, + Required: true, + }, + ), + Usage: "turns an event into a rumor (unsigned) then gift-wraps it to the recipient", + Description: `example: + nak event -c 'hello' | nak gift wrap --sec -p `, + Action: func(ctx context.Context, c *cli.Command) error { + kr, _, err := gatherKeyerFromArguments(ctx, c) + if err != nil { + return err + } + + recipient := getPubKey(c, "recipient-pubkey") + + // get sender pubkey + sender, err := kr.GetPublicKey(ctx) + if err != nil { + return fmt.Errorf("failed to get sender pubkey: %w", err) + } + + // read event from stdin + for eventJSON := range getJsonsOrBlank() { + if eventJSON == "{}" { + continue + } + + var originalEvent nostr.Event + if err := easyjson.Unmarshal([]byte(eventJSON), &originalEvent); err != nil { + return fmt.Errorf("invalid event JSON: %w", err) + } + + // turn into rumor (unsigned event) + rumor := originalEvent + rumor.Sig = [64]byte{} // remove signature + rumor.PubKey = sender + rumor.ID = rumor.GetID() // compute ID + + // create seal + rumorJSON, _ := easyjson.Marshal(rumor) + encryptedRumor, err := kr.Encrypt(ctx, string(rumorJSON), recipient) + if err != nil { + return fmt.Errorf("failed to encrypt rumor: %w", err) + } + seal := &nostr.Event{ + Kind: 13, + Content: encryptedRumor, + PubKey: sender, + CreatedAt: randomNow(), + Tags: nostr.Tags{}, + } + if err := kr.SignEvent(ctx, seal); err != nil { + return fmt.Errorf("failed to sign seal: %w", err) + } + + // create gift wrap + ephemeral := nostr.Generate() + sealJSON, _ := easyjson.Marshal(seal) + convkey, err := nip44.GenerateConversationKey(recipient, ephemeral) + if err != nil { + return fmt.Errorf("failed to generate conversation key: %w", err) + } + encryptedSeal, err := nip44.Encrypt(string(sealJSON), convkey) + if err != nil { + return fmt.Errorf("failed to encrypt seal: %w", err) + } + wrap := &nostr.Event{ + Kind: 1059, + Content: encryptedSeal, + CreatedAt: randomNow(), + Tags: nostr.Tags{{"p", recipient.Hex()}}, + } + wrap.Sign(ephemeral) + + // print the gift-wrap + wrapJSON, err := easyjson.Marshal(wrap) + if err != nil { + return fmt.Errorf("failed to marshal gift wrap: %w", err) + } + stdout(string(wrapJSON)) + } + + return nil + }, + }, + { + Name: "unwrap", + Usage: "decrypts a gift-wrap event sent by the sender to us and exposes its internal rumor (unsigned event).", + Description: `example: + nak req -p -k 1059 dmrelay.com | nak gift unwrap --sec --from `, + Flags: append( + defaultKeyFlags, + &PubKeyFlag{ + Name: "sender-pubkey", + Aliases: []string{"p", "src", "source", "pubkey", "from"}, + Required: true, + }, + ), + Action: func(ctx context.Context, c *cli.Command) error { + kr, _, err := gatherKeyerFromArguments(ctx, c) + if err != nil { + return err + } + + sender := getPubKey(c, "sender-pubkey") + + // read gift-wrapped event from stdin + for wrapJSON := range getJsonsOrBlank() { + if wrapJSON == "{}" { + continue + } + + var wrap nostr.Event + if err := easyjson.Unmarshal([]byte(wrapJSON), &wrap); err != nil { + return fmt.Errorf("invalid gift wrap JSON: %w", err) + } + + if wrap.Kind != 1059 { + return fmt.Errorf("not a gift wrap event (kind %d)", wrap.Kind) + } + + ephemeralPubkey := wrap.PubKey + + // decrypt seal + sealJSON, err := kr.Decrypt(ctx, wrap.Content, ephemeralPubkey) + if err != nil { + return fmt.Errorf("failed to decrypt seal: %w", err) + } + + var seal nostr.Event + if err := easyjson.Unmarshal([]byte(sealJSON), &seal); err != nil { + return fmt.Errorf("invalid seal JSON: %w", err) + } + + if seal.Kind != 13 { + return fmt.Errorf("not a seal event (kind %d)", seal.Kind) + } + + // decrypt rumor + rumorJSON, err := kr.Decrypt(ctx, seal.Content, sender) + if err != nil { + return fmt.Errorf("failed to decrypt rumor: %w", err) + } + + var rumor nostr.Event + if err := easyjson.Unmarshal([]byte(rumorJSON), &rumor); err != nil { + return fmt.Errorf("invalid rumor JSON: %w", err) + } + + // output the unwrapped event (rumor) + stdout(rumorJSON) + } + + return nil + }, + }, + }, +} + +func randomNow() nostr.Timestamp { + const twoDays = 2 * 24 * 60 * 60 + now := time.Now().Unix() + randomOffset := rand.Int63n(twoDays) + return nostr.Timestamp(now - randomOffset) +} diff --git a/main.go b/main.go index aac465d..6963296 100644 --- a/main.go +++ b/main.go @@ -42,6 +42,7 @@ var app = &cli.Command{ blossomCmd, encrypt, decrypt, + gift, outbox, wallet, mcpServer, From 1dab81f77cf61ac839fee2ce7f14df13125db7b0 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Mon, 1 Dec 2025 21:15:58 -0300 Subject: [PATCH 360/401] add examples to README. --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index e03153c..20a6dc7 100644 --- a/README.md +++ b/README.md @@ -324,6 +324,21 @@ echo "#surely you're joking, mr npub1l2vyh47mk2p0qlsku7hg0vn29faehy9hy34ygaclpn6 ffmpeg -f alsa -i default -f webm -t 00:00:03 pipe:1 | nak blossom --server blossom.primal.net upload | jq -rc '{content: .url}' | nak event -k 1222 --sec 'bunker://urlgoeshere' pyramid.fiatjaf.com nostr.wine ``` +### gift-wrap an event to a recipient and publish it somewhere +```shell +~> nak event -c 'secret message' | nak gift wrap --sec -p | nak event wss://dmrelay.com +``` + +### download a gift-wrap event and unwrap it +```shell +~> nak req -p -k 1059 relay.com | nak gift unwrap --sec --from +``` + +### sync events between two relays using negentropy +```shell +~> nak sync relay1.com relay2.com +``` + ### from a file with events get only those that have kind 1111 and were created by a given pubkey ```shell ~> cat all.jsonl | nak filter -k 1111 -a 117673e191b10fe1aedf1736ee74de4cffd4c132ca701960b70a5abad5870faa > filtered.jsonl From df491be2326516e8abf34d30ab099fa63e662bfb Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 2 Dec 2025 15:53:18 -0300 Subject: [PATCH 361/401] serve: --grasp-path (hidden). --- serve.go | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/serve.go b/serve.go index 3a8147c..ca800e7 100644 --- a/serve.go +++ b/serve.go @@ -51,6 +51,12 @@ var serve = &cli.Command{ Name: "grasp", Usage: "enable grasp server", }, + &cli.StringFlag{ + Name: "grasp-path", + Usage: "where to store the repositories", + TakesFile: true, + Hidden: true, + }, &cli.BoolFlag{ Name: "blossom", Usage: "enable blossom server", @@ -135,10 +141,13 @@ var serve = &cli.Command{ } if c.Bool("grasp") { - var err error - repoDir, err = os.MkdirTemp("", "nak-serve-grasp-repos-") - if err != nil { - return fmt.Errorf("failed to create grasp repos directory: %w", err) + repoDir = c.String("grasp-path") + if repoDir == "" { + var err error + repoDir, err = os.MkdirTemp("", "nak-serve-grasp-repos-") + if err != nil { + return fmt.Errorf("failed to create grasp repos directory: %w", err) + } } g := grasp.New(rl, repoDir) g.OnRead = func(ctx context.Context, pubkey nostr.PubKey, repo string) (reject bool, reason string) { From 4b8b6bb3def702e38da16fea6a8ea5a5f4985b10 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Wed, 3 Dec 2025 23:08:59 -0300 Subject: [PATCH 362/401] dekey: nip4e (untested). --- dekey.go | 282 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ main.go | 1 + 2 files changed, 283 insertions(+) create mode 100644 dekey.go diff --git a/dekey.go b/dekey.go new file mode 100644 index 0000000..3961e2a --- /dev/null +++ b/dekey.go @@ -0,0 +1,282 @@ +package main + +import ( + "context" + "fmt" + "os" + "path/filepath" + "slices" + + "fiatjaf.com/nostr" + "fiatjaf.com/nostr/nip44" + "github.com/urfave/cli/v3" +) + +var dekey = &cli.Command{ + Name: "dekey", + Usage: "handles NIP-4E decoupled encryption keys", + Description: "maybe this picture will explain better than I can do here for now: https://cdn.azzamo.net/89c543d261ad0d665c1dea78f91e527c2e39e7fe503b440265a3c47e63c9139f.png", + DisableSliceFlagSeparator: true, + Flags: append(defaultKeyFlags, + &cli.StringFlag{ + Name: "device-name", + Usage: "name of this device that will be published and displayed on other clients", + Value: func() string { + if hostname, err := os.Hostname(); err == nil { + return "nak@" + hostname + } + return "nak@unknown" + }(), + }, + ), + Action: func(ctx context.Context, c *cli.Command) error { + kr, _, err := gatherKeyerFromArguments(ctx, c) + if err != nil { + return err + } + + userPub, err := kr.GetPublicKey(ctx) + if err != nil { + return fmt.Errorf("failed to get user public key: %w", err) + } + + configPath := c.String("config-path") + deviceName := c.String("device-name") + + // check if we already have a local-device secret key + deviceKeyPath := filepath.Join(configPath, "dekey", "device-key") + var deviceSec nostr.SecretKey + if data, err := os.ReadFile(deviceKeyPath); err == nil { + deviceSec, err = nostr.SecretKeyFromHex(string(data)) + if err != nil { + return fmt.Errorf("invalid device key in %s: %w", deviceKeyPath, err) + } + } else { + // create one + deviceSec = nostr.Generate() + os.MkdirAll(filepath.Dir(deviceKeyPath), 0700) + if err := os.WriteFile(deviceKeyPath, []byte(deviceSec.Hex()), 0600); err != nil { + return fmt.Errorf("failed to write device key: %w", err) + } + } + devicePub := deviceSec.Public() + + // get relays for the user + relays := sys.FetchWriteRelays(ctx, userPub) + relayList := connectToAllRelays(ctx, c, relays, nil, nostr.PoolOptions{}) + if len(relayList) == 0 { + return fmt.Errorf("no relays to use") + } + + // check if kind:4454 is already published + events := sys.Pool.FetchMany(ctx, relays, nostr.Filter{ + Kinds: []nostr.Kind{4454}, + Authors: []nostr.PubKey{userPub}, + Tags: nostr.TagMap{ + "pubkey": []string{devicePub.Hex()}, + }, + }, nostr.SubscriptionOptions{Label: "nak-nip4e"}) + if len(events) == 0 { + // publish kind:4454 + evt := nostr.Event{ + Kind: 4454, + Content: "", + CreatedAt: nostr.Now(), + Tags: nostr.Tags{ + {"client", deviceName}, + {"pubkey", devicePub.Hex()}, + }, + } + + // sign with main key + if err := kr.SignEvent(ctx, &evt); err != nil { + return fmt.Errorf("failed to sign device event: %w", err) + } + + // publish + if err := publishFlow(ctx, c, kr, evt, relayList); err != nil { + return err + } + } + + // check for kind:10044 + userKeyEventDate := nostr.Now() + userKeyResult := sys.Pool.FetchManyReplaceable(ctx, relays, nostr.Filter{ + Kinds: []nostr.Kind{10044}, + Authors: []nostr.PubKey{userPub}, + }, nostr.SubscriptionOptions{Label: "nak-nip4e"}) + var eSec nostr.SecretKey + var ePub nostr.PubKey + if userKeyEvent, ok := userKeyResult.Load(nostr.ReplaceableKey{PubKey: userPub, D: ""}); !ok { + // generate main secret key + eSec = nostr.Generate() + ePub := eSec.Public() + + // store it + eKeyPath := filepath.Join(configPath, "dekey", "e", ePub.Hex()) + os.MkdirAll(filepath.Dir(eKeyPath), 0700) + if err := os.WriteFile(eKeyPath, []byte(eSec.Hex()), 0600); err != nil { + return fmt.Errorf("failed to write user encryption key: %w", err) + } + + // publish kind:10044 + evt10044 := nostr.Event{ + Kind: 10044, + Content: "", + CreatedAt: userKeyEventDate, + Tags: nostr.Tags{ + {"n", ePub.Hex()}, + }, + } + if err := kr.SignEvent(ctx, &evt10044); err != nil { + return fmt.Errorf("failed to sign kind:10044: %w", err) + } + + if err := publishFlow(ctx, c, kr, evt10044, relayList); err != nil { + return err + } + } else { + userKeyEventDate = userKeyEvent.CreatedAt + + // get the pub from the tag + for _, tag := range userKeyEvent.Tags { + if len(tag) >= 2 && tag[0] == "n" { + ePub, _ = nostr.PubKeyFromHex(tag[1]) + break + } + } + if ePub == nostr.ZeroPK { + return fmt.Errorf("invalid kind:10044 event, no 'n' tag") + } + + // check if we have the key + eKeyPath := filepath.Join(configPath, "dekey", "e", ePub.Hex()) + if data, err := os.ReadFile(eKeyPath); err == nil { + eSec, err = nostr.SecretKeyFromHex(string(data)) + if err != nil { + return fmt.Errorf("invalid main key: %w", err) + } + if eSec.Public() != ePub { + return fmt.Errorf("stored user encryption key is corrupted: %w", err) + } + } else { + // try to decrypt from kind:4455 + for eKeyMsg := range sys.Pool.FetchMany(ctx, relays, nostr.Filter{ + Kinds: []nostr.Kind{4455}, + Tags: nostr.TagMap{ + "p": []string{devicePub.Hex()}, + }, + }, nostr.SubscriptionOptions{Label: "nak-nip4e"}) { + var senderPub nostr.PubKey + for _, tag := range eKeyMsg.Tags { + if len(tag) >= 2 && tag[0] == "P" { + senderPub, _ = nostr.PubKeyFromHex(tag[1]) + break + } + } + if senderPub == nostr.ZeroPK { + continue + } + ss, err := nip44.GenerateConversationKey(senderPub, deviceSec) + if err != nil { + continue + } + eSecHex, err := nip44.Decrypt(eKeyMsg.Content, ss) + if err != nil { + continue + } + eSec, err = nostr.SecretKeyFromHex(eSecHex) + if err != nil { + continue + } + // check if it matches mainPub + if eSec.Public() == ePub { + // store it + os.MkdirAll(filepath.Dir(eKeyPath), 0700) + os.WriteFile(eKeyPath, []byte(eSecHex), 0600) + break + } + } + } + } + + if eSec == [32]byte{} { + log("main secret key not available, must authorize on another device\n") + return nil + } + + // now we have mainSec, check for other kind:4454 events newer than the 10044 + keyMsgs := make([]string, 0, 5) + for keyOrDeviceEvt := range sys.Pool.FetchMany(ctx, relays, nostr.Filter{ + Kinds: []nostr.Kind{4454, 4455}, + Authors: []nostr.PubKey{userPub}, + Since: userKeyEventDate, + }, nostr.SubscriptionOptions{Label: "nak-nip4e"}) { + if keyOrDeviceEvt.Kind == 4455 { + // key event + + // skip ourselves + if keyOrDeviceEvt.Tags.FindWithValue("p", devicePub.Hex()) != nil { + continue + } + + // assume a key msg will always come before its associated devicemsg + // so just store them here: + pubkeyTag := keyOrDeviceEvt.Tags.Find("p") + if pubkeyTag == nil { + continue + } + keyMsgs = append(keyMsgs, pubkeyTag[1]) + } else if keyOrDeviceEvt.Kind == 4454 { + // device event + + // skip ourselves + if keyOrDeviceEvt.Tags.FindWithValue("pubkey", devicePub.Hex()) != nil { + continue + } + + // if this already has a corresponding keyMsg then skip it + pubkeyTag := keyOrDeviceEvt.Tags.Find("pubkey") + if pubkeyTag == nil { + continue + } + if slices.Contains(keyMsgs, pubkeyTag[1]) { + continue + } + + // here we know we're dealing with a deviceMsg without a corresponding keyMsg + // so we have to build a keyMsg for them + theirDevice, err := nostr.PubKeyFromHex(pubkeyTag[1]) + if err != nil { + continue + } + + ss, err := nip44.GenerateConversationKey(theirDevice, deviceSec) + if err != nil { + continue + } + ciphertext, err := nip44.Encrypt(eSec.Hex(), ss) + if err != nil { + continue + } + + evt4455 := nostr.Event{ + Kind: 4455, + Content: ciphertext, + CreatedAt: nostr.Now(), + Tags: nostr.Tags{ + {"p", theirDevice.Hex()}, + {"P", devicePub.Hex()}, + }, + } + if err := kr.SignEvent(ctx, &evt4455); err != nil { + continue + } + + publishFlow(ctx, c, kr, evt4455, relayList) + } + } + + return nil + }, +} diff --git a/main.go b/main.go index 6963296..a960443 100644 --- a/main.go +++ b/main.go @@ -40,6 +40,7 @@ var app = &cli.Command{ bunker, serve, blossomCmd, + dekey, encrypt, decrypt, gift, From 252612b12f6169202043e8d09a9b272670510d3b Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Thu, 4 Dec 2025 08:46:20 -0300 Subject: [PATCH 363/401] add pee trick. --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 20a6dc7..4048bfa 100644 --- a/README.md +++ b/README.md @@ -348,3 +348,10 @@ ffmpeg -f alsa -i default -f webm -t 00:00:03 pipe:1 | nak blossom --server blos ```shell ~> nak req --ids-only -k 1111 -a npub1vyrx2prp0mne8pczrcvv38ahn5wahsl8hlceeu3f3aqyvmu8zh5s7kfy55 relay.damus.io ``` + +### generate a new random key and print its associated public key at the same time +```shell +~> nak key generate | pee 'cat' 'nak key public' +1a851afaa70a26faa82c5b4422ce967c07e278efc56a1413b9719b662f86551a +8031621a54b2502f5bd4dbb87c971c0a69675d252a64d69e22224f3aee6dd2b2 +``` From b973b476bcf54de938244527c904fefc7dcca1a1 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Thu, 4 Dec 2025 09:23:31 -0300 Subject: [PATCH 364/401] req: print CLOSED messages. --- go.mod | 2 +- go.sum | 2 ++ req.go | 23 +++++++++++++++++------ 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index 9ff63dd..f5d21ae 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.25 require ( fiatjaf.com/lib v0.3.1 - fiatjaf.com/nostr v0.0.0-20251201232830-91548fa0a157 + fiatjaf.com/nostr v0.0.0-20251204122254-07061404918d github.com/AlecAivazis/survey/v2 v2.3.7 github.com/bep/debounce v1.2.1 github.com/btcsuite/btcd/btcec/v2 v2.3.6 diff --git a/go.sum b/go.sum index b1a3a95..4951c40 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ fiatjaf.com/lib v0.3.1 h1:/oFQwNtFRfV+ukmOCxfBEAuayoLwXp4wu2/fz5iHpwA= fiatjaf.com/lib v0.3.1/go.mod h1:Ycqq3+mJ9jAWu7XjbQI1cVr+OFgnHn79dQR5oTII47g= fiatjaf.com/nostr v0.0.0-20251201232830-91548fa0a157 h1:14yLsO2HwpS2CLIKFvLMDp8tVEDahwdC8OeG6NGaL+M= fiatjaf.com/nostr v0.0.0-20251201232830-91548fa0a157/go.mod h1:ue7yw0zHfZj23Ml2kVSdBx0ENEaZiuvGxs/8VEN93FU= +fiatjaf.com/nostr v0.0.0-20251204122254-07061404918d h1:xROmiuT7LrZk+/iGGeTqRI4liqJZrc87AWjsyHtbqDg= +fiatjaf.com/nostr v0.0.0-20251204122254-07061404918d/go.mod h1:ue7yw0zHfZj23Ml2kVSdBx0ENEaZiuvGxs/8VEN93FU= github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ= github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= github.com/FastFilter/xorfilter v0.2.1 h1:lbdeLG9BdpquK64ZsleBS8B4xO/QW1IM0gMzF7KaBKc= diff --git a/req.go b/req.go index 7b28049..34ba54a 100644 --- a/req.go +++ b/req.go @@ -227,6 +227,8 @@ example: } } else { var results chan nostr.RelayEvent + var closeds chan nostr.RelayClosed + opts := nostr.SubscriptionOptions{ Label: "nak-req", } @@ -294,20 +296,29 @@ example: errg.Wait() if c.Bool("stream") { - results = sys.Pool.BatchedSubscribeMany(ctx, defs, opts) + results, closeds = sys.Pool.BatchedSubscribeManyNotifyClosed(ctx, defs, opts) } else { - results = sys.Pool.BatchedQueryMany(ctx, defs, opts) + results, closeds = sys.Pool.BatchedQueryManyNotifyClosed(ctx, defs, opts) } } else { if c.Bool("stream") { - results = sys.Pool.SubscribeMany(ctx, relayUrls, filter, opts) + results, closeds = sys.Pool.SubscribeManyNotifyClosed(ctx, relayUrls, filter, opts) } else { - results = sys.Pool.FetchMany(ctx, relayUrls, filter, opts) + results, closeds = sys.Pool.FetchManyNotifyClosed(ctx, relayUrls, filter, opts) } } - for ie := range results { - stdout(ie.Event) + for { + select { + case ie := <-results: + stdout(ie.Event) + case closed := <-closeds: + if closed.HandledAuth { + logverbose("%s CLOSED: %s\n", closed.Relay.URL, closed.Reason) + } else { + log("%s CLOSED: %s\n", closed.Relay.URL, closed.Reason) + } + } } } } else { From 5ee7670ba895e557e54db40e908d12b253db21dd Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Thu, 4 Dec 2025 13:21:43 -0300 Subject: [PATCH 365/401] req: fix infinite loop when events channel is exhausted. --- req.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/req.go b/req.go index 34ba54a..5ee988c 100644 --- a/req.go +++ b/req.go @@ -308,9 +308,13 @@ example: } } + readevents: for { select { - case ie := <-results: + case ie, ok := <-results: + if !ok { + break readevents + } stdout(ie.Event) case closed := <-closeds: if closed.HandledAuth { @@ -318,6 +322,8 @@ example: } else { log("%s CLOSED: %s\n", closed.Relay.URL, closed.Reason) } + case <-ctx.Done(): + break readevents } } } From a288cc47a4bb00a3ae47151f76fe92a8bcd01685 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Fri, 5 Dec 2025 22:02:39 -0300 Subject: [PATCH 366/401] add example of compilation with `-tags debug` to README. --- README.md | 74 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/README.md b/README.md index 4048bfa..c2b7113 100644 --- a/README.md +++ b/README.md @@ -339,6 +339,80 @@ ffmpeg -f alsa -i default -f webm -t 00:00:03 pipe:1 | nak blossom --server blos ~> nak sync relay1.com relay2.com ``` +### get nak to be very verbose about all messages sent and received to relays +```shell +~> go install -tags=debug github.com/fiatjaf/nak@latest +~> # +~> # then, for example: +~> nak req -k 30617 -k 30618 pyramid.treegaze.com gitnostr.com -a bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747 +``` + +
+output (mixing stdin and stderr) +
+
+pyramid.treegaze.com... ok.
+gitnostr.com... ok.
+[nl][debug] 2025/12/05 22:00:53 {wss://gitnostr.com} sending '["REQ","1:nak-req",{"kinds":[30617,30618],"authors":["bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747"]}]'
+[nl][debug] 2025/12/05 22:00:53 {wss://pyramid.treegaze.com} sending '["REQ","2:nak-req",{"kinds":[30617,30618],"authors":["bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747"]}]'
+[nl][debug] 2025/12/05 22:00:53 {wss://pyramid.treegaze.com} received ["EVENT","2:nak-req",{"kind":30618,"id":"001d5525ef7b529a40a8c1a74a1a21bbe8cf6b525a82814ea71438452ae4159e","pubkey":"bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747","created_at":1764973164,"tags":[["d","loom-worker"],["HEAD","ref: refs/heads/main"],["refs/heads/main","fd4fd997ab0c671ad1eb5218f7d37e7fdc6f9e96"]],"content":"","sig":"29cd9542bab9e1fcae1f0dcf296b7874edfca3b5a55472fad5a3079925f40a69fbcacd9dbfa948b6f625297fa2bd5f7aca6639f2cc19015843faab676dbf90ec"}]
+[nl][debug] 2025/12/05 22:00:53 {wss://gitnostr.com} received ["EVENT","1:nak-req",{"kind":30618,"id":"001d5525ef7b529a40a8c1a74a1a21bbe8cf6b525a82814ea71438452ae4159e","pubkey":"bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747","created_at":1764973164,"tags":[["d","loom-worker"],["HEAD","ref: refs/heads/main"],["refs/heads/main","fd4fd997ab0c671ad1eb5218f7d37e7fdc6f9e96"]],"content":"","sig":"29cd9542bab9e1fcae1f0dcf296b7874edfca3b5a55472fad5a3079925f40a69fbcacd9dbfa948b6f625297fa2bd5f7aca6639f2cc19015843faab676dbf90ec"}]
+[nl][debug] 2025/12/05 22:00:53 {wss://gitnostr.com} received ["EVENT","1:nak-req",{"kind":30617,"id":"ba4b194e2611946752383ffc8fd1ea225305527c29193d86f8556b66e1837bf7","pubkey":"bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747","created_at":1764973163,"tags":[["d","loom-worker"],["r","7ad47289a1013edcd7778af2dd8c70063f756f7b","euc"],["name","loom-worker"],["description","Weaving Your Threads, Together."],["clone","https://relay.ngit.dev/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/loom-worker.git","https://gitnostr.com/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/loom-worker.git","https://pyramid.treegaze.com/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/loom-worker.git","https://pyramid.fiatjaf.com/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/loom-worker.git"],["web","https://gitworkshop.dev/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/relay.ngit.dev/loom-worker"],["relays","wss://relay.ngit.dev","wss://gitnostr.com","wss://pyramid.treegaze.com","wss://pyramid.fiatjaf.com","wss://relay.damus.io","wss://nos.lol","wss://relay.nostr.band"],["maintainers","bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747"],["alt","git repository: loom-worker"],["blossoms","https://relay.ngit.dev","https://gitnostr.com","https://pyramid.treegaze.com","https://pyramid.fiatjaf.com"]],"content":"","sig":"8f3317d0754fca792797d9d2e293f801b4cc5927a40d39eeb0d1b8a4fbb18db8ee0017b1727d663fce39535e5b3afb6f8757b7643516d1c97b3753304920e0b0"}]
+[nl][debug] 2025/12/05 22:00:53 {wss://pyramid.treegaze.com} received ["EVENT","2:nak-req",{"kind":30617,"id":"ba4b194e2611946752383ffc8fd1ea225305527c29193d86f8556b66e1837bf7","pubkey":"bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747","created_at":1764973163,"tags":[["d","loom-worker"],["r","7ad47289a1013edcd7778af2dd8c70063f756f7b","euc"],["name","loom-worker"],["description","Weaving Your Threads, Together."],["clone","https://relay.ngit.dev/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/loom-worker.git","https://gitnostr.com/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/loom-worker.git","https://pyramid.treegaze.com/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/loom-worker.git","https://pyramid.fiatjaf.com/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/loom-worker.git"],["web","https://gitworkshop.dev/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/relay.ngit.dev/loom-worker"],["relays","wss://relay.ngit.dev","wss://gitnostr.com","wss://pyramid.treegaze.com","wss://pyramid.fiatjaf.com","wss://relay.damus.io","wss://nos.lol","wss://relay.nostr.band"],["maintainers","bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747"],["alt","git repository: loom-worker"],["blossoms","https://relay.ngit.dev","https://gitnostr.com","https://pyramid.treegaze.com","https://pyramid.fiatjaf.com"]],"content":"","sig":"8f3317d0754fca792797d9d2e293f801b4cc5927a40d39eeb0d1b8a4fbb18db8ee0017b1727d663fce39535e5b3afb6f8757b7643516d1c97b3753304920e0b0"}]
+[nl][debug] 2025/12/05 22:00:53 {wss://gitnostr.com} received ["EVENT","1:nak-req",{"kind":30618,"id":"e3a6c1031a929a6b8a9f7bad51a067968d76fa1f9631920b44caef45e98cbc04","pubkey":"bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747","created_at":1764971638,"tags":[["d","loom-site"],["HEAD","ref: refs/heads/main"],["refs/heads/main","c32a42e92337e8b6f6c6db0eb015877976800098"]],"content":"","sig":"4ed682dd5c4dc77bec34dc15ae9f60362f2c2ea73ed24e38266be4cee4af5f8959806c8e9fc6cc6266ea6be0c6a77a3c878af1bf4207caec9e7f38399654a0e0"}]
+[nl][debug] 2025/12/05 22:00:53 {wss://pyramid.treegaze.com} received ["EOSE","2:nak-req"]
+{"kind":30618,"id":"001d5525ef7b529a40a8c1a74a1a21bbe8cf6b525a82814ea71438452ae4159e","pubkey":"bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747","created_at":1764973164,"tags":[["d","loom-worker"],["HEAD","ref: refs/heads/main"],["refs/heads/main","fd4fd997ab0c671ad1eb5218f7d37e7fdc6f9e96"]],"content":"","sig":"29cd9542bab9e1fcae1f0dcf296b7874edfca3b5a55472fad5a3079925f40a69fbcacd9dbfa948b6f625297fa2bd5f7aca6639f2cc19015843faab676dbf90ec"}
+{"kind":30617,"id":"ba4b194e2611946752383ffc8fd1ea225305527c29193d86f8556b66e1837bf7","pubkey":"bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747","created_at":1764973163,"tags":[["d","loom-worker"],["r","7ad47289a1013edcd7778af2dd8c70063f756f7b","euc"],["name","loom-worker"],["description","Weaving Your Threads, Together."],["clone","https://relay.ngit.dev/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/loom-worker.git","https://gitnostr.com/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/loom-worker.git","https://pyramid.treegaze.com/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/loom-worker.git","https://pyramid.fiatjaf.com/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/loom-worker.git"],["web","https://gitworkshop.dev/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/relay.ngit.dev/loom-worker"],["relays","wss://relay.ngit.dev","wss://gitnostr.com","wss://pyramid.treegaze.com","wss://pyramid.fiatjaf.com","wss://relay.damus.io","wss://nos.lol","wss://relay.nostr.band"],["maintainers","bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747"],["alt","git repository: loom-worker"],["blossoms","https://relay.ngit.dev","https://gitnostr.com","https://pyramid.treegaze.com","https://pyramid.fiatjaf.com"]],"content":"","sig":"8f3317d0754fca792797d9d2e293f801b4cc5927a40d39eeb0d1b8a4fbb18db8ee0017b1727d663fce39535e5b3afb6f8757b7643516d1c97b3753304920e0b0"}
+[nl][debug] 2025/12/05 22:00:53 {wss://gitnostr.com} received ["EVENT","1:nak-req",{"kind":30617,"id":"30a2bb8fdd21e89f4fe329c3e3c47b1597daea33d905fd679754945d63067451","pubkey":"bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747","created_at":1764723101,"tags":[["d","loom-site"],["r","23c628196368089bb28a249c2e31d14c357b5031","euc"],["name","loom-site"],["description","Weaving Your Threads, Together."],["clone","https://relay.ngit.dev/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/loom-site.git","https://gitnostr.com/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/loom-site.git","https://pyramid.treegaze.com/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/loom-site.git"],["web","https://gitworkshop.dev/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/relay.ngit.dev/loom-site"],["relays","wss://relay.ngit.dev","wss://pyramid.treegaze.com","wss://gitnostr.com","wss://relay.damus.io","wss://nos.lol","wss://relay.nostr.band"],["maintainers","bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747"],["alt","git repository: loom-site"],["blossoms","https://relay.ngit.dev","https://gitnostr.com","https://pyramid.treegaze.com"]],"content":"","sig":"81ba665abe0195f07dfcfe4f5a306c7c4d1ccf5dcb0ffd270f07c96a98695142cb934fd52246c85d2e3e24d20bcce7630a3433b05324e1f8022f334ce2c6846b"}]
+{"kind":30618,"id":"e3a6c1031a929a6b8a9f7bad51a067968d76fa1f9631920b44caef45e98cbc04","pubkey":"bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747","created_at":1764971638,"tags":[["d","loom-site"],["HEAD","ref: refs/heads/main"],["refs/heads/main","c32a42e92337e8b6f6c6db0eb015877976800098"]],"content":"","sig":"4ed682dd5c4dc77bec34dc15ae9f60362f2c2ea73ed24e38266be4cee4af5f8959806c8e9fc6cc6266ea6be0c6a77a3c878af1bf4207caec9e7f38399654a0e0"}
+[nl][debug] 2025/12/05 22:00:53 {wss://gitnostr.com} received ["EVENT","1:nak-req",{"kind":30618,"id":"033e63e83a7ed21de8ae55c13d9c8e9ae378ab9db870773d9c224f71be9db608","pubkey":"bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747","created_at":1764612202,"tags":[["d","loom-protocol"],["HEAD","ref: refs/heads/main"],["refs/heads/main","97d1656c1fdaa9a22abbbe220966a8a4397a5bbe"]],"content":"","sig":"873c2fcc9edbca85f19020e52bc894881b18b904edec605eb490cec34df2ff541520ea1bc7ca88b3bb8bc91f527f614baf96280e679c5d26822678258c96c28e"}]
+{"kind":30617,"id":"30a2bb8fdd21e89f4fe329c3e3c47b1597daea33d905fd679754945d63067451","pubkey":"bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747","created_at":1764723101,"tags":[["d","loom-site"],["r","23c628196368089bb28a249c2e31d14c357b5031","euc"],["name","loom-site"],["description","Weaving Your Threads, Together."],["clone","https://relay.ngit.dev/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/loom-site.git","https://gitnostr.com/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/loom-site.git","https://pyramid.treegaze.com/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/loom-site.git"],["web","https://gitworkshop.dev/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/relay.ngit.dev/loom-site"],["relays","wss://relay.ngit.dev","wss://pyramid.treegaze.com","wss://gitnostr.com","wss://relay.damus.io","wss://nos.lol","wss://relay.nostr.band"],["maintainers","bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747"],["alt","git repository: loom-site"],["blossoms","https://relay.ngit.dev","https://gitnostr.com","https://pyramid.treegaze.com"]],"content":"","sig":"81ba665abe0195f07dfcfe4f5a306c7c4d1ccf5dcb0ffd270f07c96a98695142cb934fd52246c85d2e3e24d20bcce7630a3433b05324e1f8022f334ce2c6846b"}
+[nl][debug] 2025/12/05 22:00:53 {wss://gitnostr.com} received ["EVENT","1:nak-req",{"kind":30617,"id":"3fbb928f9f09f5713f7fc75aa5d6177fa127d34fcb098a445a2a70f20715070d","pubkey":"bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747","created_at":1764612187,"tags":[["d","loom-protocol"],["r","d100e867498703c46f752b815e2dbf0d3e01e0fb","euc"],["name","loom-protocol"],["description","Weaving Your Threads, Together."],["clone","https://relay.ngit.dev/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/loom-protocol.git","https://gitnostr.com/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/loom-protocol.git","https://pyramid.treegaze.com/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/loom-protocol.git"],["web","https://gitworkshop.dev/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/relay.ngit.dev/loom-protocol"],["relays","wss://relay.ngit.dev","wss://gitnostr.com","wss://pyramid.treegaze.com","wss://relay.damus.io","wss://nos.lol","wss://relay.nostr.band"],["maintainers","bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747"],["alt","git repository: loom-protocol"],["blossoms","https://relay.ngit.dev","https://gitnostr.com","https://pyramid.treegaze.com"]],"content":"","sig":"02776d505592095a3d1c08e8e7f1e0406df75dcdbb8a77926ee3177307f567276a81ad755622f25c81407755921e3f4662ca5277f0a5d32c7e1d467d53d08b6e"}]
+{"kind":30618,"id":"033e63e83a7ed21de8ae55c13d9c8e9ae378ab9db870773d9c224f71be9db608","pubkey":"bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747","created_at":1764612202,"tags":[["d","loom-protocol"],["HEAD","ref: refs/heads/main"],["refs/heads/main","97d1656c1fdaa9a22abbbe220966a8a4397a5bbe"]],"content":"","sig":"873c2fcc9edbca85f19020e52bc894881b18b904edec605eb490cec34df2ff541520ea1bc7ca88b3bb8bc91f527f614baf96280e679c5d26822678258c96c28e"}
+[nl][debug] 2025/12/05 22:00:53 {wss://gitnostr.com} received ["EVENT","1:nak-req",{"kind":30618,"id":"565481a335838a3f241b0063602451e100ca3d15ddcbd0b98a4253ab43f40c3e","pubkey":"bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747","created_at":1764609644,"tags":[["d","noflow"],["HEAD","ref: refs/heads/main"],["refs/heads/main","b5d3cdf5708712484a14c41d7f92a59f3161dec6"]],"content":"","sig":"b8ff2848a57ef05f7cd1aff03451b0e6500bd73cc1cdeb4ad49c5080b447a9696401e06983ff36f9e3274b055045ba7b0505e328be116dfd8881f8c1ab084fdc"}]
+{"kind":30617,"id":"3fbb928f9f09f5713f7fc75aa5d6177fa127d34fcb098a445a2a70f20715070d","pubkey":"bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747","created_at":1764612187,"tags":[["d","loom-protocol"],["r","d100e867498703c46f752b815e2dbf0d3e01e0fb","euc"],["name","loom-protocol"],["description","Weaving Your Threads, Together."],["clone","https://relay.ngit.dev/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/loom-protocol.git","https://gitnostr.com/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/loom-protocol.git","https://pyramid.treegaze.com/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/loom-protocol.git"],["web","https://gitworkshop.dev/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/relay.ngit.dev/loom-protocol"],["relays","wss://relay.ngit.dev","wss://gitnostr.com","wss://pyramid.treegaze.com","wss://relay.damus.io","wss://nos.lol","wss://relay.nostr.band"],["maintainers","bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747"],["alt","git repository: loom-protocol"],["blossoms","https://relay.ngit.dev","https://gitnostr.com","https://pyramid.treegaze.com"]],"content":"","sig":"02776d505592095a3d1c08e8e7f1e0406df75dcdbb8a77926ee3177307f567276a81ad755622f25c81407755921e3f4662ca5277f0a5d32c7e1d467d53d08b6e"}
+[nl][debug] 2025/12/05 22:00:53 {wss://gitnostr.com} received ["EVENT","1:nak-req",{"kind":30617,"id":"f66cc26f7693316db05cff6789333b9312468474a10b2511d439b51ecc0a1436","pubkey":"bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747","created_at":1764609631,"tags":[["d","noflow"],["r","aaf1722fde2f125862eb3353bb1810f0f653042f","euc"],["name","loom-protocol"],["description","Weaving Your Threads, Together."],["clone","https://relay.ngit.dev/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/noflow.git","https://gitnostr.com/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/noflow.git"],["web","https://gitworkshop.dev/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/relay.ngit.dev/noflow"],["relays","wss://relay.ngit.dev","wss://gitnostr.com","wss://relay.damus.io","wss://nos.lol","wss://relay.nostr.band"],["maintainers","bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747"],["alt","git repository: loom-protocol"],["blossoms","https://relay.ngit.dev","https://gitnostr.com"]],"content":"","sig":"05f8381b9cd4dfa0ca564f618a1c39a05bea452867a2208f4f5bf6b51968589eb92728748e175b81c1934ae8275a3e2dd16dec663684abed64f5e7d994e622cb"}]
+{"kind":30618,"id":"565481a335838a3f241b0063602451e100ca3d15ddcbd0b98a4253ab43f40c3e","pubkey":"bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747","created_at":1764609644,"tags":[["d","noflow"],["HEAD","ref: refs/heads/main"],["refs/heads/main","b5d3cdf5708712484a14c41d7f92a59f3161dec6"]],"content":"","sig":"b8ff2848a57ef05f7cd1aff03451b0e6500bd73cc1cdeb4ad49c5080b447a9696401e06983ff36f9e3274b055045ba7b0505e328be116dfd8881f8c1ab084fdc"}
+[nl][debug] 2025/12/05 22:00:53 {wss://gitnostr.com} received ["EVENT","1:nak-req",{"kind":30618,"id":"cf80564e447768bfe0562f09e9ecacfb4f6c1f125f201b523ad81e56fd5fd8ac","pubkey":"bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747","created_at":1764343728,"tags":[["d","nutsucker-2000"],["refs/heads/main","a113781b8a32eaab64c44ebc68d3447f91fa4bec"],["HEAD","ref: refs/heads/main"]],"content":"","sig":"8d99262609796e477d1b55831da0c7cd16923b22191328149a59e3f151e52333a3678a2b9451bdfed7a3ff6fd93aec799fda23597ea48b0f3ea901d73549fc9e"}]
+{"kind":30617,"id":"f66cc26f7693316db05cff6789333b9312468474a10b2511d439b51ecc0a1436","pubkey":"bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747","created_at":1764609631,"tags":[["d","noflow"],["r","aaf1722fde2f125862eb3353bb1810f0f653042f","euc"],["name","loom-protocol"],["description","Weaving Your Threads, Together."],["clone","https://relay.ngit.dev/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/noflow.git","https://gitnostr.com/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/noflow.git"],["web","https://gitworkshop.dev/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/relay.ngit.dev/noflow"],["relays","wss://relay.ngit.dev","wss://gitnostr.com","wss://relay.damus.io","wss://nos.lol","wss://relay.nostr.band"],["maintainers","bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747"],["alt","git repository: loom-protocol"],["blossoms","https://relay.ngit.dev","https://gitnostr.com"]],"content":"","sig":"05f8381b9cd4dfa0ca564f618a1c39a05bea452867a2208f4f5bf6b51968589eb92728748e175b81c1934ae8275a3e2dd16dec663684abed64f5e7d994e622cb"}
+[nl][debug] 2025/12/05 22:00:53 {wss://gitnostr.com} received ["EVENT","1:nak-req",{"kind":30617,"id":"4410562b0f8c86905b6a45223a7a5c646c09ffe3e0dfc8039532b9b93e4114ef","pubkey":"bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747","created_at":1764343273,"tags":[["d","nutsucker-2000"],["r","cd75fc0d6843cbba07c2731abe66b58ae2c81bcc","euc"],["name","nutsucker-2000"],["description","It sucks"],["clone","https://relay.ngit.dev/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/nutsucker-2000.git","https://gitnostr.com/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/nutsucker-2000.git"],["web","https://gitworkshop.dev/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/relay.ngit.dev/nutsucker-2000"],["relays","wss://relay.ngit.dev","wss://gitnostr.com","wss://relay.damus.io","wss://nos.lol","wss://relay.nostr.band"],["maintainers","bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747"],["alt","git repository: nutsucker-2000"],["blossoms","https://relay.ngit.dev","https://gitnostr.com"]],"content":"","sig":"a6a8bef7610f41c8776d614ca80ed3628a910686cd874cd9b31f50319999f9ebac59d1151b534f527541e1b36b1d28ca20279f46b29d80f12bf71f2a2315e61c"}]
+{"kind":30618,"id":"cf80564e447768bfe0562f09e9ecacfb4f6c1f125f201b523ad81e56fd5fd8ac","pubkey":"bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747","created_at":1764343728,"tags":[["d","nutsucker-2000"],["refs/heads/main","a113781b8a32eaab64c44ebc68d3447f91fa4bec"],["HEAD","ref: refs/heads/main"]],"content":"","sig":"8d99262609796e477d1b55831da0c7cd16923b22191328149a59e3f151e52333a3678a2b9451bdfed7a3ff6fd93aec799fda23597ea48b0f3ea901d73549fc9e"}
+[nl][debug] 2025/12/05 22:00:53 {wss://gitnostr.com} received ["EVENT","1:nak-req",{"kind":30618,"id":"de314f9dc64505815f3f7dd4e8179532657e8761eb9c38d7d91c565c8c520eee","pubkey":"bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747","created_at":1761217474,"tags":[["d","nostr-notary"],["HEAD","ref: refs/heads/main"],["refs/heads/main","1b4358d04c7e3e7ec7295b30462efffb7960b6e1"]],"content":"","sig":"dd2481285e185cf1bac1b5057b54813fee47415ad1dec80ef87b30dbdc929524c5eb86453dcb86146579fc4117eed92b64b2159099d143440b5d0f6dfb493248"}]
+{"kind":30617,"id":"4410562b0f8c86905b6a45223a7a5c646c09ffe3e0dfc8039532b9b93e4114ef","pubkey":"bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747","created_at":1764343273,"tags":[["d","nutsucker-2000"],["r","cd75fc0d6843cbba07c2731abe66b58ae2c81bcc","euc"],["name","nutsucker-2000"],["description","It sucks"],["clone","https://relay.ngit.dev/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/nutsucker-2000.git","https://gitnostr.com/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/nutsucker-2000.git"],["web","https://gitworkshop.dev/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/relay.ngit.dev/nutsucker-2000"],["relays","wss://relay.ngit.dev","wss://gitnostr.com","wss://relay.damus.io","wss://nos.lol","wss://relay.nostr.band"],["maintainers","bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747"],["alt","git repository: nutsucker-2000"],["blossoms","https://relay.ngit.dev","https://gitnostr.com"]],"content":"","sig":"a6a8bef7610f41c8776d614ca80ed3628a910686cd874cd9b31f50319999f9ebac59d1151b534f527541e1b36b1d28ca20279f46b29d80f12bf71f2a2315e61c"}
+[nl][debug] 2025/12/05 22:00:53 {wss://gitnostr.com} received ["EVENT","1:nak-req",{"kind":30617,"id":"6198dc9563416e2b8841b927218d290ff3911ba24e671246b86c02ec808f6f1b","pubkey":"bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747","created_at":1761217442,"tags":[["d","nostr-notary"],["r","1b4358d04c7e3e7ec7295b30462efffb7960b6e1","euc"],["name","nostr-notary"],["description","Nostr hackday POC of notarizing nostr messages"],["clone","https://relay.ngit.dev/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/nostr-notary.git","https://gitnostr.com/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/nostr-notary.git"],["web","https://gitworkshop.dev/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/relay.ngit.dev/nostr-notary"],["relays","wss://relay.ngit.dev","wss://gitnostr.com","wss://relay.damus.io","wss://nos.lol","wss://relay.nostr.band"],["maintainers","bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747"],["alt","git repository: nostr-notary"],["blossoms","https://relay.ngit.dev","https://gitnostr.com"]],"content":"","sig":"f883912c9dc1278f60471e73ea8cc591e9745b5014a92b478c4a1f5fd75a5452745232718b74aa859b13c14e4e3d64fb333a55f16bdbeeff07dd17330ddc680c"}]
+{"kind":30618,"id":"de314f9dc64505815f3f7dd4e8179532657e8761eb9c38d7d91c565c8c520eee","pubkey":"bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747","created_at":1761217474,"tags":[["d","nostr-notary"],["HEAD","ref: refs/heads/main"],["refs/heads/main","1b4358d04c7e3e7ec7295b30462efffb7960b6e1"]],"content":"","sig":"dd2481285e185cf1bac1b5057b54813fee47415ad1dec80ef87b30dbdc929524c5eb86453dcb86146579fc4117eed92b64b2159099d143440b5d0f6dfb493248"}
+{"kind":30617,"id":"6198dc9563416e2b8841b927218d290ff3911ba24e671246b86c02ec808f6f1b","pubkey":"bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747","created_at":1761217442,"tags":[["d","nostr-notary"],["r","1b4358d04c7e3e7ec7295b30462efffb7960b6e1","euc"],["name","nostr-notary"],["description","Nostr hackday POC of notarizing nostr messages"],["clone","https://relay.ngit.dev/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/nostr-notary.git","https://gitnostr.com/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/nostr-notary.git"],["web","https://gitworkshop.dev/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/relay.ngit.dev/nostr-notary"],["relays","wss://relay.ngit.dev","wss://gitnostr.com","wss://relay.damus.io","wss://nos.lol","wss://relay.nostr.band"],["maintainers","bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747"],["alt","git repository: nostr-notary"],["blossoms","https://relay.ngit.dev","https://gitnostr.com"]],"content":"","sig":"f883912c9dc1278f60471e73ea8cc591e9745b5014a92b478c4a1f5fd75a5452745232718b74aa859b13c14e4e3d64fb333a55f16bdbeeff07dd17330ddc680c"}
+[nl][debug] 2025/12/05 22:00:53 {wss://gitnostr.com} received ["EVENT","1:nak-req",{"kind":30618,"id":"145cb4fe81530a2bc1688cf20e9ff3ce2c4c29e66c01ddf231d9639dcb3dfdee","pubkey":"bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747","created_at":1761043127,"tags":[["d","fips-poc-sec"],["HEAD","ref: refs/heads/main"],["refs/heads/main","826b52ab29d9ca54e82c327f3aee0fee1d8214f7"]],"content":"","sig":"c45d6b67a32ee077166777421098ccfdd37545d4a111670661fd085a7512b05d4577eb2dd19f9b9745204a2e7d1882168340262a847e203586f3d439322bac51"}]
+[nl][debug] 2025/12/05 22:00:53 {wss://gitnostr.com} received ["EVENT","1:nak-req",{"kind":30617,"id":"20121d99e2cfde9f6ead27a57efe4cc4d12da4cc7aae6ca9721033d22815adfa","pubkey":"bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747","created_at":1761043073,"tags":[["d","fips-poc-sec"],["r","47adff995ba9c6780066932f3a659a6943b3e5a3","euc"],["name","fips-poc-sec"],["description","POC for routing with nostr pubkeys"],["clone","https://relay.ngit.dev/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/fips-poc-sec.git","https://gitnostr.com/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/fips-poc-sec.git"],["web","https://gitworkshop.dev/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/relay.ngit.dev/fips-poc-sec"],["relays","wss://relay.ngit.dev","wss://gitnostr.com","wss://relay.damus.io","wss://nos.lol","wss://relay.nostr.band"],["maintainers","bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747"],["alt","git repository: fips-poc-sec"],["blossoms","https://relay.ngit.dev","https://gitnostr.com"]],"content":"","sig":"363894244c14a152d7388a563d6c6ca5a0014f2070b819091b945c975488c678077ab1220dd4c2a0223a91d417dcba3954c5292772af5138f548144189989681"}]
+{"kind":30618,"id":"145cb4fe81530a2bc1688cf20e9ff3ce2c4c29e66c01ddf231d9639dcb3dfdee","pubkey":"bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747","created_at":1761043127,"tags":[["d","fips-poc-sec"],["HEAD","ref: refs/heads/main"],["refs/heads/main","826b52ab29d9ca54e82c327f3aee0fee1d8214f7"]],"content":"","sig":"c45d6b67a32ee077166777421098ccfdd37545d4a111670661fd085a7512b05d4577eb2dd19f9b9745204a2e7d1882168340262a847e203586f3d439322bac51"}
+[nl][debug] 2025/12/05 22:00:53 {wss://gitnostr.com} received ["EVENT","1:nak-req",{"kind":30618,"id":"d917b65b1c75d2da5cb8f4ec87eb36e83fb81105b230f1162ab567ad442c5a95","pubkey":"bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747","created_at":1758995569,"tags":[["d","noports"],["refs/heads/main","68ad2c84c94271a93786846987d192c5d5df7a9c"],["HEAD","ref: refs/heads/main"]],"content":"","sig":"c267c2a234ecd3d03572832b9d49db94eb9e5abb7006ea61f307e866d65fd4a0cbb164b5e0052916da11f22746b2107da2e80fdec3dcacd31f359516148dd032"}]
+{"kind":30617,"id":"20121d99e2cfde9f6ead27a57efe4cc4d12da4cc7aae6ca9721033d22815adfa","pubkey":"bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747","created_at":1761043073,"tags":[["d","fips-poc-sec"],["r","47adff995ba9c6780066932f3a659a6943b3e5a3","euc"],["name","fips-poc-sec"],["description","POC for routing with nostr pubkeys"],["clone","https://relay.ngit.dev/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/fips-poc-sec.git","https://gitnostr.com/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/fips-poc-sec.git"],["web","https://gitworkshop.dev/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/relay.ngit.dev/fips-poc-sec"],["relays","wss://relay.ngit.dev","wss://gitnostr.com","wss://relay.damus.io","wss://nos.lol","wss://relay.nostr.band"],["maintainers","bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747"],["alt","git repository: fips-poc-sec"],["blossoms","https://relay.ngit.dev","https://gitnostr.com"]],"content":"","sig":"363894244c14a152d7388a563d6c6ca5a0014f2070b819091b945c975488c678077ab1220dd4c2a0223a91d417dcba3954c5292772af5138f548144189989681"}
+[nl][debug] 2025/12/05 22:00:53 {wss://gitnostr.com} received ["EVENT","1:nak-req",{"kind":30617,"id":"57d949fcbf0c0486c55d4a4bf3afaf7f63b8509529735455573a232954c13924","pubkey":"bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747","created_at":1758995549,"tags":[["d","noports"],["r","3f93f168394216d694e8b79e454f432ee30e18cd","euc"],["name","noports"],["description","Buy a public IP ingress controller for sats"],["clone","https://relay.ngit.dev/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/noports.git","https://gitnostr.com/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/noports.git"],["web","https://gitworkshop.dev/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/relay.ngit.dev/noports"],["relays","wss://relay.ngit.dev","wss://gitnostr.com","wss://relay.damus.io","wss://nos.lol","wss://relay.nostr.band"],["maintainers","bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747"],["alt","git repository: noports"],["blossoms","https://relay.ngit.dev","https://gitnostr.com"]],"content":"","sig":"4c105633c547fc54deaa17e6f3831749e21df6966be7ef8638dae5e9e8609cc80800f49e3debdc456a511fb028cc742b12386a97c3a5bd6d77183a2390e6a4b5"}]
+{"kind":30618,"id":"d917b65b1c75d2da5cb8f4ec87eb36e83fb81105b230f1162ab567ad442c5a95","pubkey":"bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747","created_at":1758995569,"tags":[["d","noports"],["refs/heads/main","68ad2c84c94271a93786846987d192c5d5df7a9c"],["HEAD","ref: refs/heads/main"]],"content":"","sig":"c267c2a234ecd3d03572832b9d49db94eb9e5abb7006ea61f307e866d65fd4a0cbb164b5e0052916da11f22746b2107da2e80fdec3dcacd31f359516148dd032"}
+[nl][debug] 2025/12/05 22:00:53 {wss://gitnostr.com} received ["EVENT","1:nak-req",{"kind":30617,"id":"1db7af4ee4aba89c7ccf75c73b47efcf5f2490586617b44f2ee3e0d028d56c60","pubkey":"bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747","created_at":1757667310,"tags":[["d","nostr-dns"],["r","4a114a7236aeb18a98b8e6e63b77c0a4b61fdbd8","euc"],["name","nostr-dns"],["description","resolves npub.nostr and npub.net (browser) using nip137 announcements"],["clone","https://relay.ngit.dev/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/nostr-dns.git","https://gitnostr.com/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/nostr-dns.git"],["web","https://gitworkshop.dev/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/relay.ngit.dev/nostr-dns"],["relays","wss://relay.ngit.dev","wss://gitnostr.com","wss://relay.damus.io","wss://nos.lol","wss://relay.nostr.band"],["maintainers","bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747"],["alt","git repository: nostr-dns"],["blossoms","https://relay.ngit.dev","https://gitnostr.com"]],"content":"","sig":"d682ca1e59207c97169f44db30f9861e7a219270e92f474cefda8e2f60eb2d3198d5cea0f2bfc7e9e87ae2eb76b14bb488e614e63977b4ceea839a198a376594"}]
+{"kind":30617,"id":"57d949fcbf0c0486c55d4a4bf3afaf7f63b8509529735455573a232954c13924","pubkey":"bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747","created_at":1758995549,"tags":[["d","noports"],["r","3f93f168394216d694e8b79e454f432ee30e18cd","euc"],["name","noports"],["description","Buy a public IP ingress controller for sats"],["clone","https://relay.ngit.dev/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/noports.git","https://gitnostr.com/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/noports.git"],["web","https://gitworkshop.dev/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/relay.ngit.dev/noports"],["relays","wss://relay.ngit.dev","wss://gitnostr.com","wss://relay.damus.io","wss://nos.lol","wss://relay.nostr.band"],["maintainers","bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747"],["alt","git repository: noports"],["blossoms","https://relay.ngit.dev","https://gitnostr.com"]],"content":"","sig":"4c105633c547fc54deaa17e6f3831749e21df6966be7ef8638dae5e9e8609cc80800f49e3debdc456a511fb028cc742b12386a97c3a5bd6d77183a2390e6a4b5"}
+{"kind":30617,"id":"1db7af4ee4aba89c7ccf75c73b47efcf5f2490586617b44f2ee3e0d028d56c60","pubkey":"bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747","created_at":1757667310,"tags":[["d","nostr-dns"],["r","4a114a7236aeb18a98b8e6e63b77c0a4b61fdbd8","euc"],["name","nostr-dns"],["description","resolves npub.nostr and npub.net (browser) using nip137 announcements"],["clone","https://relay.ngit.dev/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/nostr-dns.git","https://gitnostr.com/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/nostr-dns.git"],["web","https://gitworkshop.dev/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/relay.ngit.dev/nostr-dns"],["relays","wss://relay.ngit.dev","wss://gitnostr.com","wss://relay.damus.io","wss://nos.lol","wss://relay.nostr.band"],["maintainers","bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747"],["alt","git repository: nostr-dns"],["blossoms","https://relay.ngit.dev","https://gitnostr.com"]],"content":"","sig":"d682ca1e59207c97169f44db30f9861e7a219270e92f474cefda8e2f60eb2d3198d5cea0f2bfc7e9e87ae2eb76b14bb488e614e63977b4ceea839a198a376594"}
+[nl][debug] 2025/12/05 22:00:53 {wss://gitnostr.com} received ["EVENT","1:nak-req",{"kind":30618,"id":"cf7873cbc970269ce6e7abc9f7a670e8c2a842e0b1a49a29e5dbb11b8343f691","pubkey":"bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747","created_at":1755695661,"tags":[["d","tollgate-module-basic-go"],["refs/heads/featrue-buy-from-upstream","ee7c8d4728913069fb839c4ffeea83e9e586d56f"],["refs/heads/bragging_mode","22a9d511b59f747cdc0edd7c8f1acad6b31bd1fc"],["refs/heads/build-with-docker","259e5312566c85c8a479efe48f13151c74001e04"],["refs/heads/just_janitor_2nd_try_connectivity_issues","1cac60857a1e3149d899521376d17cf0b2679b5a"],["refs/heads/multiple_mints_persistent_config","d104aadc86df74b8558c9a82617bd56ba432671e"],["refs/heads/feature/continous-automatic-verification","bb6261ecbf0bd1b9779a1d347d5e1a03c2c6b19a"],["refs/heads/feature-add-crowsnest","5252dcd5a649b403e69b23ec51a1037292940579"],["refs/heads/session-extension","2855546ace49123b37dfc026f95814671695b125"],["refs/heads/add_init.d_minimal_squashed","d7eb6edb29da1ad639a6722c02c4bf9376516fda"],["refs/heads/debug-old-version-for-os","9a2d00fe4611f411d9f9e4c1fdc286429c28197a"],["refs/heads/build-for-23","6a06fb5386598b76ecdd37a05fcb23f31379a70f"],["refs/heads/main","dbcb151103e91526e136475c037fe1304b8bfab9"],["refs/heads/feature/lightning-payments","dd289f42f5e140be5a4b43df98bbb7560e7f2407"],["refs/heads/janitor","8c7a9ab1664140841fb1bfeceb1686a7b225cd52"],["refs/heads/feature/pay-usptream-squashed","a5e0bce9ff50189516b2a99e87f84524119b49aa"],["refs/heads/add_tags","d4856c7b1443d64d2afcef2333f6bf79337fcbd3"],["refs/heads/add_init.d_minimal_squashed_rebased","a16c98414a98f005d3d114b5f5df5daba176a275"],["refs/heads/fix_ammount","b93494bbd88fceccebe6f54d1ec784ca2d017020"],["refs/heads/matrix","993f6054de8641a2c726d80fd298b5761bc97157"],["refs/heads/feature/discover-upstream","db834abe51405db680e759c2d5256493dcecd066"],["refs/heads/fix/install-config-nil","1a342af6f3096db1be4cfb76fbe2bcabbee953ed"],["refs/heads/feature/test-joel-frontend","2b0f0588eeea71832375c65da1f7cbaf81354128"],["refs/heads/add_gle750","bb341fd3e4fba6e88b6086f73f60299595b2eba2"],["refs/heads/feature-purchase-upstream","19979c05308f34cad0526d4db5a1ec945d7a2e93"],["refs/heads/banner_and_nodogsplash_restart","be3c9f0dd5cbddf14b2dd7511d42658e85a5e841"],["refs/heads/architecture-matrix","b608c8b45ae69700a938c8ac3c131b0ad0cca221"],["refs/heads/x1860","e746a1b96cb4a42b6c71863319f928ddf00d16bc"],["refs/heads/multiple_mints","301a8adba022c636787ac1ca35560d52ded41c81"],["refs/heads/bragging_mode_squashed","0cb147abfbd9e4a8651a74fa2f97ea2698f2c66d"],["refs/heads/refactor/merchant-testcases","15ce4da0ac3fd691e4474d816660167960524b1e"],["refs/heads/feature/joels-new-captive-portal","cfe3f914161ac3c6792630404a54c3532dc7008d"],["refs/heads/config-versioning","75b98a4b499a8ac723fb6f6de2075c0d7c6b1842"],["refs/heads/nobanner","f248e29d6533c43c930a162d6963635667d89e8b"],["refs/heads/feature/identity-config","6920c582d9963bb7f5b8a9bafc79038febec6dcc"],["refs/heads/just_janitor_2nd_try_squashed","e030f04f8aa722d421c1536d07e0b17919bfb6c4"],["refs/heads/nip94_os_event","9dd48d748558db8c90f83f75fb1093be87314cff"],["refs/heads/multi_arch","e5a7b51c8501b0fb7e8bbd03bda8efc82fb7408b"],["refs/heads/just_janitor","b6f8105a2f78174e57aab2ce0f22ed3230b14ea1"],["refs/heads/mt6000","646efc2b8acc9ec9b5464d5c8ce19605a6633478"],["refs/heads/fix/indentation-shell-scripts","6b77eda639be36907636d93b238d1e0d4b210324"],["refs/heads/feature/gateway-detection","b4f1e4384d1a010d9baa39b1442cb22d3c73458a"],["refs/heads/fix/losing-gateway-access","cd711463f17859e23e704cdcd2506181e2eefba8"],["refs/heads/add_init.d_minimal","450cff1cbf01e8a38cdeeab5df6bee18f0182772"],["refs/heads/add_init.d","eb0b4d80ecde4f663f1941c1102272f9cd2c41f6"],["refs/heads/fix_ecash_path","18ee765bafebc1b7ca02aaf9fbade34e01228d40"],["refs/heads/fix/identities.json","b90a787f192674be38e4b3f2bd20af2f7566d517"],["refs/heads/just_janitor_2nd_try","01e9d4d429923775bf14e397bdf6c75107d061c3"],["refs/heads/add_ar300m","3baffd810438cf9be3abecdf3324ed4fbc9d8c84"],["refs/heads/new-nip94","9cef6299b0e7b03d990e52daa61b776c5d8b79bc"],["refs/heads/nip94_os_event_squash","2f5f1ad99b8b0ad1ae5bafbedc43c7429c6018de"],["refs/heads/feature-add-crowsnest-squashed","87c269d018310643953587ae0d4c7fdbad7a76be"],["refs/heads/fix/identities.json-working","644ed0e463b99617d320b78a1fbe250af301e463"],["refs/heads/build_sdk","c33faa1910ff7651bc29e162d195635bb026cba4"],["refs/heads/fix/multiple_ssid_same_radio","efa9df48d69cab9583e9ecb03dbb52ce33b2cb70"],["HEAD","ref: refs/heads/main"],["refs/heads/restart-on-crash","a6b790f57a466ec2e712f15255cf6f73b577db5f"],["refs/heads/build_sdk_squashed","e40802471d83d4e64cc8a9bfcd1395c13aeb6e03"],["refs/heads/more_architectures","6d6654899c4b1f263358a9a5996fd3ac197075b6"],["refs/heads/janitor_works_on_laptop","c1285f0bba81f6a261b732199310b87543e9ff76"],["refs/heads/simplify/janitor-using-version-number-scheme","dd62cb5d6184dfad747f79480bfc39226a070ecb"]],"content":"","sig":"2ec86d8c1c2d0607f7617206f15e258a0f29f61c95766075c764c0e39f87c1fd80b33e150a32d0516256090d90e34d9b74a678cfee316f80bcf4787b6637f6f4"}]
+[nl][debug] 2025/12/05 22:00:53 {wss://gitnostr.com} received ["EVENT","1:nak-req",{"kind":30617,"id":"799bbf456fa36492f9b62c550b9ba3d7626c3e072a6b1d7dcd58034ba26af822","pubkey":"bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747","created_at":1754475921,"tags":[["d","tollgate-module-basic-go"],["r","bdbd4457066573f1b1efe17be9dfee3c05569a76","euc"],["name","tollgate-module-basic-go"],["description","Basic TollGate functionality"],["clone","https://github.com/OpenTollGate/tollgate-module-basic-go.git","https://relay.ngit.dev/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/tollgate-module-basic-go.git","https://gitnostr.com/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/tollgate-module-basic-go.git"],["web","https://gitworkshop.dev/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/relay.ngit.dev/tollgate-module-basic-go"],["relays","wss://relay.ngit.dev","wss://gitnostr.com","wss://nos.lol/","wss://bitcoiner.social/","wss://relay.damus.io/","wss://relay.primal.net/"],["maintainers","bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747","3539591f412ee5a76aca22d727e95420e8596c2d0d2527db504d21b539e6cf50","c3e23eb5e3d00f18b2f4f588d8cdbc548648be761bdd90812186df4603d7caa9"],["alt","git repository: tollgate-module-basic-go"],["blossoms","https://relay.ngit.dev","https://gitnostr.com"]],"content":"","sig":"4e1039f5b49c710d58469f13ccadd538892d8266ad2bb3b6f2b297e9fd110ef15170d9ffd4a60f8e05d8c285b53500ff39b90e514b594cd20fbf0627af881d0f"}]
+{"kind":30618,"id":"cf7873cbc970269ce6e7abc9f7a670e8c2a842e0b1a49a29e5dbb11b8343f691","pubkey":"bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747","created_at":1755695661,"tags":[["d","tollgate-module-basic-go"],["refs/heads/featrue-buy-from-upstream","ee7c8d4728913069fb839c4ffeea83e9e586d56f"],["refs/heads/bragging_mode","22a9d511b59f747cdc0edd7c8f1acad6b31bd1fc"],["refs/heads/build-with-docker","259e5312566c85c8a479efe48f13151c74001e04"],["refs/heads/just_janitor_2nd_try_connectivity_issues","1cac60857a1e3149d899521376d17cf0b2679b5a"],["refs/heads/multiple_mints_persistent_config","d104aadc86df74b8558c9a82617bd56ba432671e"],["refs/heads/feature/continous-automatic-verification","bb6261ecbf0bd1b9779a1d347d5e1a03c2c6b19a"],["refs/heads/feature-add-crowsnest","5252dcd5a649b403e69b23ec51a1037292940579"],["refs/heads/session-extension","2855546ace49123b37dfc026f95814671695b125"],["refs/heads/add_init.d_minimal_squashed","d7eb6edb29da1ad639a6722c02c4bf9376516fda"],["refs/heads/debug-old-version-for-os","9a2d00fe4611f411d9f9e4c1fdc286429c28197a"],["refs/heads/build-for-23","6a06fb5386598b76ecdd37a05fcb23f31379a70f"],["refs/heads/main","dbcb151103e91526e136475c037fe1304b8bfab9"],["refs/heads/feature/lightning-payments","dd289f42f5e140be5a4b43df98bbb7560e7f2407"],["refs/heads/janitor","8c7a9ab1664140841fb1bfeceb1686a7b225cd52"],["refs/heads/feature/pay-usptream-squashed","a5e0bce9ff50189516b2a99e87f84524119b49aa"],["refs/heads/add_tags","d4856c7b1443d64d2afcef2333f6bf79337fcbd3"],["refs/heads/add_init.d_minimal_squashed_rebased","a16c98414a98f005d3d114b5f5df5daba176a275"],["refs/heads/fix_ammount","b93494bbd88fceccebe6f54d1ec784ca2d017020"],["refs/heads/matrix","993f6054de8641a2c726d80fd298b5761bc97157"],["refs/heads/feature/discover-upstream","db834abe51405db680e759c2d5256493dcecd066"],["refs/heads/fix/install-config-nil","1a342af6f3096db1be4cfb76fbe2bcabbee953ed"],["refs/heads/feature/test-joel-frontend","2b0f0588eeea71832375c65da1f7cbaf81354128"],["refs/heads/add_gle750","bb341fd3e4fba6e88b6086f73f60299595b2eba2"],["refs/heads/feature-purchase-upstream","19979c05308f34cad0526d4db5a1ec945d7a2e93"],["refs/heads/banner_and_nodogsplash_restart","be3c9f0dd5cbddf14b2dd7511d42658e85a5e841"],["refs/heads/architecture-matrix","b608c8b45ae69700a938c8ac3c131b0ad0cca221"],["refs/heads/x1860","e746a1b96cb4a42b6c71863319f928ddf00d16bc"],["refs/heads/multiple_mints","301a8adba022c636787ac1ca35560d52ded41c81"],["refs/heads/bragging_mode_squashed","0cb147abfbd9e4a8651a74fa2f97ea2698f2c66d"],["refs/heads/refactor/merchant-testcases","15ce4da0ac3fd691e4474d816660167960524b1e"],["refs/heads/feature/joels-new-captive-portal","cfe3f914161ac3c6792630404a54c3532dc7008d"],["refs/heads/config-versioning","75b98a4b499a8ac723fb6f6de2075c0d7c6b1842"],["refs/heads/nobanner","f248e29d6533c43c930a162d6963635667d89e8b"],["refs/heads/feature/identity-config","6920c582d9963bb7f5b8a9bafc79038febec6dcc"],["refs/heads/just_janitor_2nd_try_squashed","e030f04f8aa722d421c1536d07e0b17919bfb6c4"],["refs/heads/nip94_os_event","9dd48d748558db8c90f83f75fb1093be87314cff"],["refs/heads/multi_arch","e5a7b51c8501b0fb7e8bbd03bda8efc82fb7408b"],["refs/heads/just_janitor","b6f8105a2f78174e57aab2ce0f22ed3230b14ea1"],["refs/heads/mt6000","646efc2b8acc9ec9b5464d5c8ce19605a6633478"],["refs/heads/fix/indentation-shell-scripts","6b77eda639be36907636d93b238d1e0d4b210324"],["refs/heads/feature/gateway-detection","b4f1e4384d1a010d9baa39b1442cb22d3c73458a"],["refs/heads/fix/losing-gateway-access","cd711463f17859e23e704cdcd2506181e2eefba8"],["refs/heads/add_init.d_minimal","450cff1cbf01e8a38cdeeab5df6bee18f0182772"],["refs/heads/add_init.d","eb0b4d80ecde4f663f1941c1102272f9cd2c41f6"],["refs/heads/fix_ecash_path","18ee765bafebc1b7ca02aaf9fbade34e01228d40"],["refs/heads/fix/identities.json","b90a787f192674be38e4b3f2bd20af2f7566d517"],["refs/heads/just_janitor_2nd_try","01e9d4d429923775bf14e397bdf6c75107d061c3"],["refs/heads/add_ar300m","3baffd810438cf9be3abecdf3324ed4fbc9d8c84"],["refs/heads/new-nip94","9cef6299b0e7b03d990e52daa61b776c5d8b79bc"],["refs/heads/nip94_os_event_squash","2f5f1ad99b8b0ad1ae5bafbedc43c7429c6018de"],["refs/heads/feature-add-crowsnest-squashed","87c269d018310643953587ae0d4c7fdbad7a76be"],["refs/heads/fix/identities.json-working","644ed0e463b99617d320b78a1fbe250af301e463"],["refs/heads/build_sdk","c33faa1910ff7651bc29e162d195635bb026cba4"],["refs/heads/fix/multiple_ssid_same_radio","efa9df48d69cab9583e9ecb03dbb52ce33b2cb70"],["HEAD","ref: refs/heads/main"],["refs/heads/restart-on-crash","a6b790f57a466ec2e712f15255cf6f73b577db5f"],["refs/heads/build_sdk_squashed","e40802471d83d4e64cc8a9bfcd1395c13aeb6e03"],["refs/heads/more_architectures","6d6654899c4b1f263358a9a5996fd3ac197075b6"],["refs/heads/janitor_works_on_laptop","c1285f0bba81f6a261b732199310b87543e9ff76"],["refs/heads/simplify/janitor-using-version-number-scheme","dd62cb5d6184dfad747f79480bfc39226a070ecb"]],"content":"","sig":"2ec86d8c1c2d0607f7617206f15e258a0f29f61c95766075c764c0e39f87c1fd80b33e150a32d0516256090d90e34d9b74a678cfee316f80bcf4787b6637f6f4"}
+{"kind":30617,"id":"799bbf456fa36492f9b62c550b9ba3d7626c3e072a6b1d7dcd58034ba26af822","pubkey":"bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747","created_at":1754475921,"tags":[["d","tollgate-module-basic-go"],["r","bdbd4457066573f1b1efe17be9dfee3c05569a76","euc"],["name","tollgate-module-basic-go"],["description","Basic TollGate functionality"],["clone","https://github.com/OpenTollGate/tollgate-module-basic-go.git","https://relay.ngit.dev/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/tollgate-module-basic-go.git","https://gitnostr.com/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/tollgate-module-basic-go.git"],["web","https://gitworkshop.dev/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/relay.ngit.dev/tollgate-module-basic-go"],["relays","wss://relay.ngit.dev","wss://gitnostr.com","wss://nos.lol/","wss://bitcoiner.social/","wss://relay.damus.io/","wss://relay.primal.net/"],["maintainers","bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747","3539591f412ee5a76aca22d727e95420e8596c2d0d2527db504d21b539e6cf50","c3e23eb5e3d00f18b2f4f588d8cdbc548648be761bdd90812186df4603d7caa9"],["alt","git repository: tollgate-module-basic-go"],["blossoms","https://relay.ngit.dev","https://gitnostr.com"]],"content":"","sig":"4e1039f5b49c710d58469f13ccadd538892d8266ad2bb3b6f2b297e9fd110ef15170d9ffd4a60f8e05d8c285b53500ff39b90e514b594cd20fbf0627af881d0f"}
+[nl][debug] 2025/12/05 22:00:53 {wss://gitnostr.com} received ["EVENT","1:nak-req",{"kind":30618,"id":"14f05c0749b1600824452c0a6c2f9f01fc6a2b1729e8616ffcb5f9c10f00b091","pubkey":"bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747","created_at":1753540249,"tags":[["d","tollgate-management-portal-site"],["refs/heads/main","6f734531ea1586b8e3bd03b0a7f0895ca66a2cf0"]],"content":"","sig":"d1a92f39110705465d7f8eba036141fadca34d0bb301a3d80027da3fc0cf2863a5a6e51b7261c8a5b104618d594e1732ab0c261004973df0f3cbd9e638dcf6d3"}]
+[nl][debug] 2025/12/05 22:00:53 {wss://gitnostr.com} received ["EVENT","1:nak-req",{"kind":30617,"id":"07103bed0b46b1d2652bb9cd0e8870c930d2280117d11f37f27e10cf6f60ec6e","pubkey":"bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747","created_at":1753540229,"tags":[["d","tollgate-management-portal-site"],["r","6f734531ea1586b8e3bd03b0a7f0895ca66a2cf0","euc"],["name","tollgate-management-portal-site"],["description","TollGate Portal for managing your device and user session info"],["clone","https://relay.ngit.dev/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/tollgate-management-portal-site.git","https://gitnostr.com/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/tollgate-management-portal-site.git"],["web","https://gitworkshop.dev/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/relay.ngit.dev/tollgate-management-portal-site"],["relays","wss://relay.ngit.dev","wss://gitnostr.com","wss://relay.damus.io","wss://nos.lol","wss://relay.nostr.band"],["maintainers","bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747","c3e23eb5e3d00f18b2f4f588d8cdbc548648be761bdd90812186df4603d7caa9","53a91e3a64d1f658e983ac1e4f9e0c697f8f33e01d8debe439f4c1a92113f592"],["alt","git repository: tollgate-management-portal-site"],["blossoms","https://relay.ngit.dev","https://gitnostr.com"]],"content":"","sig":"f39b6985d5216e3c6d13e7a1bc9b83723bc978bde421068f326242c231fa9aebffda2a56ce145d102305d1432cf23b9408b196dabcd25e19c54f243d2936c95f"}]
+{"kind":30617,"id":"07103bed0b46b1d2652bb9cd0e8870c930d2280117d11f37f27e10cf6f60ec6e","pubkey":"bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747","created_at":1753540229,"tags":[["d","tollgate-management-portal-site"],["r","6f734531ea1586b8e3bd03b0a7f0895ca66a2cf0","euc"],["name","tollgate-management-portal-site"],["description","TollGate Portal for managing your device and user session info"],["clone","https://relay.ngit.dev/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/tollgate-management-portal-site.git","https://gitnostr.com/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/tollgate-management-portal-site.git"],["web","https://gitworkshop.dev/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/relay.ngit.dev/tollgate-management-portal-site"],["relays","wss://relay.ngit.dev","wss://gitnostr.com","wss://relay.damus.io","wss://nos.lol","wss://relay.nostr.band"],["maintainers","bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747","c3e23eb5e3d00f18b2f4f588d8cdbc548648be761bdd90812186df4603d7caa9","53a91e3a64d1f658e983ac1e4f9e0c697f8f33e01d8debe439f4c1a92113f592"],["alt","git repository: tollgate-management-portal-site"],["blossoms","https://relay.ngit.dev","https://gitnostr.com"]],"content":"","sig":"f39b6985d5216e3c6d13e7a1bc9b83723bc978bde421068f326242c231fa9aebffda2a56ce145d102305d1432cf23b9408b196dabcd25e19c54f243d2936c95f"}
+{"kind":30618,"id":"14f05c0749b1600824452c0a6c2f9f01fc6a2b1729e8616ffcb5f9c10f00b091","pubkey":"bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747","created_at":1753540249,"tags":[["d","tollgate-management-portal-site"],["refs/heads/main","6f734531ea1586b8e3bd03b0a7f0895ca66a2cf0"]],"content":"","sig":"d1a92f39110705465d7f8eba036141fadca34d0bb301a3d80027da3fc0cf2863a5a6e51b7261c8a5b104618d594e1732ab0c261004973df0f3cbd9e638dcf6d3"}
+[nl][debug] 2025/12/05 22:00:53 {wss://gitnostr.com} received ["EVENT","1:nak-req",{"kind":30618,"id":"1eeeea19e0d72261658ca18334c8a35e50215fd64c010354de909cff477eedf5","pubkey":"bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747","created_at":1751360305,"tags":[["d","ngit-repo-explorer"],["refs/heads/commit-history","3f0dc54c664795583213118c27c218f52b738428"],["refs/heads/main","d8e6ada5051db1f205e580ce1d5ac12cbe0f0bc2"],["refs/heads/master","e00e8101a8310070cc221ff15f0818d88cc68b1d"],["HEAD","ref: refs/heads/main"],["refs/heads/next","d8e6ada5051db1f205e580ce1d5ac12cbe0f0bc2"]],"content":"","sig":"9b6ec9bb74e8249aa8db54cae02caae9270da1a01490b9b2753386dc621e0cfdcb0c018b532b6bf4b7bb4d50ee3a4cc3310b5505a4cc42999a1cf7e4a73c06f5"}]
+[nl][debug] 2025/12/05 22:00:53 {wss://gitnostr.com} received ["EVENT","1:nak-req",{"kind":30617,"id":"13db013898f0c2e63d523213fd5f81f4201a90de183928580ac7d431524cd57c","pubkey":"bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747","created_at":1750455475,"tags":[["d","ngit-repo-explorer"],["r","b540fe617eadbb92800999a4e57a91ad8cabd2f0","euc"],["name","treegaze"],["description","Explore and browse files of Nostr-native Git repostiories"],["clone","https://relay.ngit.dev/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/ngit-repo-explorer.git","https://gitnostr.com/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/ngit-repo-explorer.git"],["web","https://gitworkshop.dev/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/relay.ngit.dev/ngit-repo-explorer"],["relays","wss://relay.ngit.dev","wss://gitnostr.com","wss://relay.damus.io","wss://nos.lol","wss://relay.nostr.band"],["maintainers","bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747"],["alt","git repository: treegaze"],["blossoms","https://relay.ngit.dev","https://gitnostr.com"]],"content":"","sig":"85a496e2b8dd1495de814f2f13a9468a2b4d8403122b98f6189b975f5433ef3ee181b81c7605cdf98c0329b72807c512f9040f7f6360eaf97b2fca025f2ae506"}]
+{"kind":30618,"id":"1eeeea19e0d72261658ca18334c8a35e50215fd64c010354de909cff477eedf5","pubkey":"bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747","created_at":1751360305,"tags":[["d","ngit-repo-explorer"],["refs/heads/commit-history","3f0dc54c664795583213118c27c218f52b738428"],["refs/heads/main","d8e6ada5051db1f205e580ce1d5ac12cbe0f0bc2"],["refs/heads/master","e00e8101a8310070cc221ff15f0818d88cc68b1d"],["HEAD","ref: refs/heads/main"],["refs/heads/next","d8e6ada5051db1f205e580ce1d5ac12cbe0f0bc2"]],"content":"","sig":"9b6ec9bb74e8249aa8db54cae02caae9270da1a01490b9b2753386dc621e0cfdcb0c018b532b6bf4b7bb4d50ee3a4cc3310b5505a4cc42999a1cf7e4a73c06f5"}
+[nl][debug] 2025/12/05 22:00:53 {wss://gitnostr.com} received ["EVENT","1:nak-req",{"kind":30617,"id":"1acc9786fe08b764ff2c4b80c97904ce9431d9fc749ec2798f2c4baad68a5650","pubkey":"bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747","created_at":1750247391,"tags":[["d","tollgate-desktop-client"],["r","7fbea28cbc3c00500630284a1572ba43abd5d950","euc"],["name","tollgate-desktop-client"],["description","Desktop client for auto-purchasing internet access"],["clone","https://relay.ngit.dev/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/tollgate-desktop-client.git","https://gitnostr.com/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/tollgate-desktop-client.git"],["web","https://gitworkshop.dev/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/relay.ngit.dev/tollgate-desktop-client"],["relays","wss://relay.ngit.dev","wss://gitnostr.com","wss://relay.damus.io","wss://nos.lol","wss://relay.nostr.band"],["maintainers","bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747"],["alt","git repository: tollgate-desktop-client"],["blossoms","https://relay.ngit.dev","https://gitnostr.com"]],"content":"","sig":"14a06df253303372b1c8347181e2b1197827df9519643e068896ab51b85267c72ab98e03bc6f7bcee879163854f4ef1b40a177182c1c71831b8d6edaf8d96c36"}]
+{"kind":30617,"id":"13db013898f0c2e63d523213fd5f81f4201a90de183928580ac7d431524cd57c","pubkey":"bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747","created_at":1750455475,"tags":[["d","ngit-repo-explorer"],["r","b540fe617eadbb92800999a4e57a91ad8cabd2f0","euc"],["name","treegaze"],["description","Explore and browse files of Nostr-native Git repostiories"],["clone","https://relay.ngit.dev/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/ngit-repo-explorer.git","https://gitnostr.com/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/ngit-repo-explorer.git"],["web","https://gitworkshop.dev/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/relay.ngit.dev/ngit-repo-explorer"],["relays","wss://relay.ngit.dev","wss://gitnostr.com","wss://relay.damus.io","wss://nos.lol","wss://relay.nostr.band"],["maintainers","bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747"],["alt","git repository: treegaze"],["blossoms","https://relay.ngit.dev","https://gitnostr.com"]],"content":"","sig":"85a496e2b8dd1495de814f2f13a9468a2b4d8403122b98f6189b975f5433ef3ee181b81c7605cdf98c0329b72807c512f9040f7f6360eaf97b2fca025f2ae506"}
+[nl][debug] 2025/12/05 22:00:53 {wss://gitnostr.com} received ["EVENT","1:nak-req",{"kind":30618,"id":"7c0e7d178b51c37d0fe886271b41b98170358cf5338fe3f161a89434e4a397ff","pubkey":"bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747","created_at":1750242566,"tags":[["d","tollgate-desktop-client"],["refs/heads/main","f30c7c8e67451582c3cd7b1d361caaa9f90afec0"]],"content":"","sig":"fed7b1967f9fa37f19f0ada206a8ea7bbb3e89ce1fb3cbbd0de897c343e6ce9a5c951d0c05923c4bfa3942127761d1d37a573d1ff9a0ba33932e107292b90636"}]
+{"kind":30617,"id":"1acc9786fe08b764ff2c4b80c97904ce9431d9fc749ec2798f2c4baad68a5650","pubkey":"bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747","created_at":1750247391,"tags":[["d","tollgate-desktop-client"],["r","7fbea28cbc3c00500630284a1572ba43abd5d950","euc"],["name","tollgate-desktop-client"],["description","Desktop client for auto-purchasing internet access"],["clone","https://relay.ngit.dev/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/tollgate-desktop-client.git","https://gitnostr.com/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/tollgate-desktop-client.git"],["web","https://gitworkshop.dev/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr/relay.ngit.dev/tollgate-desktop-client"],["relays","wss://relay.ngit.dev","wss://gitnostr.com","wss://relay.damus.io","wss://nos.lol","wss://relay.nostr.band"],["maintainers","bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747"],["alt","git repository: tollgate-desktop-client"],["blossoms","https://relay.ngit.dev","https://gitnostr.com"]],"content":"","sig":"14a06df253303372b1c8347181e2b1197827df9519643e068896ab51b85267c72ab98e03bc6f7bcee879163854f4ef1b40a177182c1c71831b8d6edaf8d96c36"}
+[nl][debug] 2025/12/05 22:00:53 {wss://gitnostr.com} received ["EOSE","1:nak-req"]
+{"kind":30618,"id":"7c0e7d178b51c37d0fe886271b41b98170358cf5338fe3f161a89434e4a397ff","pubkey":"bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747","created_at":1750242566,"tags":[["d","tollgate-desktop-client"],["refs/heads/main","f30c7c8e67451582c3cd7b1d361caaa9f90afec0"]],"content":"","sig":"fed7b1967f9fa37f19f0ada206a8ea7bbb3e89ce1fb3cbbd0de897c343e6ce9a5c951d0c05923c4bfa3942127761d1d37a573d1ff9a0ba33932e107292b90636"}
+
+
+
+ ### from a file with events get only those that have kind 1111 and were created by a given pubkey ```shell ~> cat all.jsonl | nak filter -k 1111 -a 117673e191b10fe1aedf1736ee74de4cffd4c132ca701960b70a5abad5870faa > filtered.jsonl From a83b23d76bd74a4d71441c6f054283461a92e8a3 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Fri, 5 Dec 2025 22:15:08 -0300 Subject: [PATCH 367/401] add nak git demo to README. --- README.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c2b7113..6caf8e3 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,7 @@ ok. {"kind":1,"id":"0000000a5109c9747e3847282fcaef3d221d1be5e864ced7b2099d416a18d15a","pubkey":"7bdef7be22dd8e59f4600e044aa53a1cf975a9dc7d27df5833bc77db784a5805","created_at":1703869609,"tags":[["nonce","12912720851599460299","25"]],"content":"https://image.nostr.build/5eb40d3cae799bc572763b8f8bee95643344fa392d280efcb0fd28a935879e2a.png\n\nNostr is not dying.\nIt is just a physiological and healthy slowdown on the part of all those who have made this possible in such a short time, sharing extraordinary enthusiasm. This is necessary to regain a little energy, it will allow some things to be cleaned up and more focused goals to be set.\n\nIt is like the caterpillar that is about to become a butterfly, it has to stop moving, acting, doing all the time; it has to do one last silent work and rest, letting time go by. And then a new phase of life can begin.\n\nWe have an amazing 2024 ahead.\nThank you all, who have given so much and believe in Nostr.\n\nPS: an interesting cue suggested by this image, you cannot have both silk and butterfly, you have to choose: a precious and sophisticated ornament, or the living, colorful beauty of freedom.","sig":"16fe157fb13dba2474d510db5253edc409b465515371015a91b26b8f39e5aa873453bc366947c37463c49466f5fceb7dea0485432f979a03471c8f76b73e553c"} {"kind":1,"id":"ac0cc72dfee39f41d94568f574e7b613d3979facbd7b477a16b52eb763db4b6e","pubkey":"2250f69694c2a43929e77e5de0f6a61ae5e37a1ee6d6a3baef1706ed9901248b","created_at":1703873865,"tags":[["r","https://zine.wavlake.com/2023-year-in-review/"]],"content":"It's been an incredible year for us here at Wavlake and we wanted to take a moment to look back and see how far we've come since launch. Read more.. https://zine.wavlake.com/2023-year-in-review/","sig":"189e354f67f48f3046fd762c83f9bf3a776d502d514e2839a1b459c30107a02453304ef695cdc7d254724041feec3800806b21eb76259df87144aaef821ace5b"} {"kind":1,"id":"6215766c5aadfaf51488134682f7d28f237218b5405da2fc11d1fefe1ebf8154","pubkey":"4ce6abbd68dab6e9fdf6e8e9912a8e12f9b539e078c634c55a9bff2994a514dd","created_at":1703879775,"tags":[["imeta","url https://video.nostr.build/7b4e7c326fa4fcba58a40914ce9db4f060bd917878f2194f6d139948b085ebb9.mp4","blurhash eHD,QG_4ogMu_3to%O-:MwM_IWRjx^-pIUoe-;t7%Nt7%gV?M{WBxu","dim 480x268"],["t","zaps"],["t","powakili23"],["p","4f82bced42584a6acfced2a657b5acabc4f90d75a95ed3ff888f3b04b8928630"],["p","ce75bae2349804caa5f4de8ae8f775bb558135f412441d9e32f88e4226c5d165"],["p","94bd495b78f8f6e5aff8ebc90e052d3a409d1f9d82e43ab56ca2cafb81b18ddf"],["p","50ff5b7ebeac1cc0d03dc878be8a59f1b63d45a7d5e60ade4b6f6f31eca25954"],["p","f300cf2bdf9808ed229dfa468260753a0b179935bdb87612b6d4f5b9fe3fc7cf"],["r","https://geyser.fund/entry/2636"],["r","https://video.nostr.build/7b4e7c326fa4fcba58a40914ce9db4f060bd917878f2194f6d139948b085ebb9.mp4"]],"content":"POWA - HQ UPDATE - DEC 2023\nTLDR: plan to open January 2024, 1 million Sats to go to reach milestone. #zaps go to fund this project. ⚡️powa@geyser.fund\n\nHello,\n\nFirst and foremost, I’d like to thank you for the incredible support shown for this project. It’s been an absolute honor to oversee this Proof of Work initiative.\n\nI am thrilled to announce that we are right on track for the grand opening in January 2024.\n\nCurrently, we're just over 1 million Sats away from reaching our target for this phase.\n\nPlease take a moment to enjoy the video and stay tuned for further updates about POWA. \n\nMan Like Who?\nMan Like Kweks!\n🇹🇿⚡️💜🏔️\n#powakili23\nnostr:npub1f7ptem2ztp9x4n7w62n90ddv40z0jrt4490d8lug3uasfwyjsccqkknerm nostr:npub1ee6m4c35nqzv4f05m69w3am4hd2czd05zfzpm83jlz8yyfk969js78tfcv nostr:npub1jj75jkmclrmwttlca0ysupfd8fqf68uastjr4dtv5t90hqd33h0s4gcksp nostr:npub12rl4kl474swvp5paeputazje7xmr63d86hnq4hjtdahnrm9zt92qgq500s nostr:npub17vqv727lnqyw6g5alfrgycr48g930xf4hku8vy4k6n6mnl3lcl8sglecc5 \n\nhttps://geyser.fund/entry/2636 https://video.nostr.build/7b4e7c326fa4fcba58a40914ce9db4f060bd917878f2194f6d139948b085ebb9.mp4 ","sig":"97d13c17d91c319f343cc770222d6d4a0a714d0e7e4ef43373adaf215a4c077f0bdf12bac488c74dbd4d55718d46c17a617b93c8660736b70bcd61a8820ece67"} -... +# and so on... ``` ### sign an event collaboratively with multiple parties using musig2 @@ -423,6 +423,18 @@ gitnostr.com... ok. ~> nak req --ids-only -k 1111 -a npub1vyrx2prp0mne8pczrcvv38ahn5wahsl8hlceeu3f3aqyvmu8zh5s7kfy55 relay.damus.io ``` +### manage nip34/grasp git repositories +```shell +~> nak git clone +~> nak git init +~> nak git sync +~> nak git fetch +~> nak git pull +~> nak git push +``` + +[demo screencast](https://njump.me/nevent1qvzqqqqqqypzqwlsccluhy6xxsr6l9a9uhhxf75g85g8a709tprjcn4e42h053vaqqswfth72qet7p4tdgvd92wpq4zcerseu3ecwqkac622xad5wqln6jsta5zpv). + ### generate a new random key and print its associated public key at the same time ```shell ~> nak key generate | pee 'cat' 'nak key public' From 68bbece3dbfd3336dce17b3718afd54f925d741a Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 16 Dec 2025 13:12:56 -0300 Subject: [PATCH 368/401] update keypair pee example on readme. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6caf8e3..ae45760 100644 --- a/README.md +++ b/README.md @@ -437,7 +437,7 @@ gitnostr.com... ok. ### generate a new random key and print its associated public key at the same time ```shell -~> nak key generate | pee 'cat' 'nak key public' +~> nak key generate | pee 'nak encode nsec' 'nak key public | nak encode npub' 1a851afaa70a26faa82c5b4422ce967c07e278efc56a1413b9719b662f86551a 8031621a54b2502f5bd4dbb87c971c0a69675d252a64d69e22224f3aee6dd2b2 ``` From 6f00ff4c73af625414b59890c1b3d42830107819 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 16 Dec 2025 13:13:12 -0300 Subject: [PATCH 369/401] bunker: fix a halting waitgroup issue. --- bunker.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bunker.go b/bunker.go index 850e2f9..1a5b2bd 100644 --- a/bunker.go +++ b/bunker.go @@ -366,6 +366,7 @@ var bunker = &cli.Command{ handlerWg.Add(len(relayURLs)) for _, relayURL := range relayURLs { go func(relayURL string) { + defer handlerWg.Done() if relay, _ := sys.Pool.EnsureRelay(relayURL); relay != nil { err := relay.Publish(ctx, eventResponse) printLock.Lock() @@ -375,7 +376,6 @@ var bunker = &cli.Command{ log("* failed to send response: %s\n", err) } printLock.Unlock() - handlerWg.Done() } }(relayURL) } From c1d1682d6e9da27fd64083b51784a5bd7422b7a9 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Fri, 19 Dec 2025 14:50:59 -0300 Subject: [PATCH 370/401] dekey: add logs. --- dekey.go | 37 +++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/dekey.go b/dekey.go index 3961e2a..25746bc 100644 --- a/dekey.go +++ b/dekey.go @@ -9,6 +9,7 @@ import ( "fiatjaf.com/nostr" "fiatjaf.com/nostr/nip44" + "github.com/fatih/color" "github.com/urfave/cli/v3" ) @@ -30,11 +31,13 @@ var dekey = &cli.Command{ }, ), Action: func(ctx context.Context, c *cli.Command) error { + log(color.CyanString("gathering keyer from arguments...\n")) kr, _, err := gatherKeyerFromArguments(ctx, c) if err != nil { return err } + log(color.CyanString("getting user public key...\n")) userPub, err := kr.GetPublicKey(ctx) if err != nil { return fmt.Errorf("failed to get user public key: %w", err) @@ -43,32 +46,40 @@ var dekey = &cli.Command{ configPath := c.String("config-path") deviceName := c.String("device-name") + log(color.YellowString("handling device key for %s...\n"), deviceName) // check if we already have a local-device secret key deviceKeyPath := filepath.Join(configPath, "dekey", "device-key") var deviceSec nostr.SecretKey if data, err := os.ReadFile(deviceKeyPath); err == nil { + log(color.GreenString("found existing device key\n")) deviceSec, err = nostr.SecretKeyFromHex(string(data)) if err != nil { return fmt.Errorf("invalid device key in %s: %w", deviceKeyPath, err) } } else { + log(color.YellowString("generating new device key...\n")) // create one deviceSec = nostr.Generate() os.MkdirAll(filepath.Dir(deviceKeyPath), 0700) if err := os.WriteFile(deviceKeyPath, []byte(deviceSec.Hex()), 0600); err != nil { return fmt.Errorf("failed to write device key: %w", err) } + log(color.GreenString("device key generated and stored\n")) } devicePub := deviceSec.Public() // get relays for the user + log(color.CyanString("fetching write relays for user...\n")) relays := sys.FetchWriteRelays(ctx, userPub) + log(color.CyanString("connecting to %d relays...\n"), len(relays)) relayList := connectToAllRelays(ctx, c, relays, nil, nostr.PoolOptions{}) if len(relayList) == 0 { return fmt.Errorf("no relays to use") } + log(color.GreenString("connected to %d relays\n"), len(relayList)) // check if kind:4454 is already published + log(color.CyanString("checking for existing device registration (kind:4454)...\n")) events := sys.Pool.FetchMany(ctx, relays, nostr.Filter{ Kinds: []nostr.Kind{4454}, Authors: []nostr.PubKey{userPub}, @@ -77,6 +88,7 @@ var dekey = &cli.Command{ }, }, nostr.SubscriptionOptions{Label: "nak-nip4e"}) if len(events) == 0 { + log(color.YellowString("no device registration found, publishing kind:4454...\n")) // publish kind:4454 evt := nostr.Event{ Kind: 4454, @@ -97,9 +109,13 @@ var dekey = &cli.Command{ if err := publishFlow(ctx, c, kr, evt, relayList); err != nil { return err } + log(color.GreenString("device registration published\n")) + } else { + log(color.GreenString("device already registered\n")) } // check for kind:10044 + log(color.CyanString("checking for user encryption key (kind:10044)...\n")) userKeyEventDate := nostr.Now() userKeyResult := sys.Pool.FetchManyReplaceable(ctx, relays, nostr.Filter{ Kinds: []nostr.Kind{10044}, @@ -108,6 +124,7 @@ var dekey = &cli.Command{ var eSec nostr.SecretKey var ePub nostr.PubKey if userKeyEvent, ok := userKeyResult.Load(nostr.ReplaceableKey{PubKey: userPub, D: ""}); !ok { + log(color.YellowString("no user encryption key found, generating new one...\n")) // generate main secret key eSec = nostr.Generate() ePub := eSec.Public() @@ -118,8 +135,10 @@ var dekey = &cli.Command{ if err := os.WriteFile(eKeyPath, []byte(eSec.Hex()), 0600); err != nil { return fmt.Errorf("failed to write user encryption key: %w", err) } + log(color.GreenString("user encryption key generated and stored\n")) // publish kind:10044 + log(color.YellowString("publishing user encryption key (kind:10044)...\n")) evt10044 := nostr.Event{ Kind: 10044, Content: "", @@ -135,7 +154,9 @@ var dekey = &cli.Command{ if err := publishFlow(ctx, c, kr, evt10044, relayList); err != nil { return err } + log(color.GreenString("user encryption key published\n")) } else { + log(color.GreenString("found existing user encryption key\n")) userKeyEventDate = userKeyEvent.CreatedAt // get the pub from the tag @@ -152,6 +173,7 @@ var dekey = &cli.Command{ // check if we have the key eKeyPath := filepath.Join(configPath, "dekey", "e", ePub.Hex()) if data, err := os.ReadFile(eKeyPath); err == nil { + log(color.GreenString("found stored user encryption key\n")) eSec, err = nostr.SecretKeyFromHex(string(data)) if err != nil { return fmt.Errorf("invalid main key: %w", err) @@ -160,6 +182,7 @@ var dekey = &cli.Command{ return fmt.Errorf("stored user encryption key is corrupted: %w", err) } } else { + log(color.YellowString("user encryption key not stored locally, attempting to decrypt from other devices...\n")) // try to decrypt from kind:4455 for eKeyMsg := range sys.Pool.FetchMany(ctx, relays, nostr.Filter{ Kinds: []nostr.Kind{4455}, @@ -191,6 +214,7 @@ var dekey = &cli.Command{ } // check if it matches mainPub if eSec.Public() == ePub { + log(color.GreenString("successfully decrypted user encryption key from another device\n")) // store it os.MkdirAll(filepath.Dir(eKeyPath), 0700) os.WriteFile(eKeyPath, []byte(eSecHex), 0600) @@ -201,11 +225,13 @@ var dekey = &cli.Command{ } if eSec == [32]byte{} { - log("main secret key not available, must authorize on another device\n") + log(color.RedString("main secret key not available, must authorize on another device\n")) return nil } + log(color.GreenString("user encryption key ready\n")) // now we have mainSec, check for other kind:4454 events newer than the 10044 + log(color.CyanString("checking for other devices and key messages...\n")) keyMsgs := make([]string, 0, 5) for keyOrDeviceEvt := range sys.Pool.FetchMany(ctx, relays, nostr.Filter{ Kinds: []nostr.Kind{4454, 4455}, @@ -214,6 +240,7 @@ var dekey = &cli.Command{ }, nostr.SubscriptionOptions{Label: "nak-nip4e"}) { if keyOrDeviceEvt.Kind == 4455 { // key event + log(color.BlueString("received key message (kind:4455)\n")) // skip ourselves if keyOrDeviceEvt.Tags.FindWithValue("p", devicePub.Hex()) != nil { @@ -229,6 +256,7 @@ var dekey = &cli.Command{ keyMsgs = append(keyMsgs, pubkeyTag[1]) } else if keyOrDeviceEvt.Kind == 4454 { // device event + log(color.BlueString("received device registration (kind:4454)\n")) // skip ourselves if keyOrDeviceEvt.Tags.FindWithValue("pubkey", devicePub.Hex()) != nil { @@ -246,6 +274,7 @@ var dekey = &cli.Command{ // here we know we're dealing with a deviceMsg without a corresponding keyMsg // so we have to build a keyMsg for them + log(color.YellowString("sending encryption key to new device...\n")) theirDevice, err := nostr.PubKeyFromHex(pubkeyTag[1]) if err != nil { continue @@ -273,7 +302,11 @@ var dekey = &cli.Command{ continue } - publishFlow(ctx, c, kr, evt4455, relayList) + if err := publishFlow(ctx, c, kr, evt4455, relayList); err != nil { + log(color.RedString("failed to publish key message: %v\n"), err) + } else { + log(color.GreenString("encryption key sent to device\n")) + } } } From 8396738fe20ab92136514fcf1bad58a121b28695 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sat, 20 Dec 2025 13:09:52 -0300 Subject: [PATCH 371/401] git: push --tags support. --- git.go | 50 +++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 45 insertions(+), 5 deletions(-) diff --git a/git.go b/git.go index 4cad493..e599b97 100644 --- a/git.go +++ b/git.go @@ -455,11 +455,17 @@ aside from those, there is also: { Name: "push", Usage: "push git changes", - Flags: append(defaultKeyFlags, &cli.BoolFlag{ - Name: "force", - Aliases: []string{"f"}, - Usage: "force push to git remotes", - }), + Flags: append(defaultKeyFlags, + &cli.BoolFlag{ + Name: "force", + Aliases: []string{"f"}, + Usage: "force push to git remotes", + }, + &cli.BoolFlag{ + Name: "tags", + Usage: "push all refs under refs/tags", + }, + ), Action: func(ctx context.Context, c *cli.Command) error { // setup signer kr, _, err := gatherKeyerFromArguments(ctx, c) @@ -526,6 +532,37 @@ aside from those, there is also: log("- setting HEAD to branch %s\n", color.CyanString(remoteBranch)) } + // add all refs/tags + output, err := exec.Command("git", "show-ref", "--tags").Output() + if err != nil { + return fmt.Errorf("failed to get local tags: %s", err) + } else { + lines := strings.Split(strings.TrimSpace(string(output)), "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" { + continue + } + parts := strings.Fields(line) + if len(parts) != 2 { + continue + } + commitHash := parts[0] + ref := parts[1] + + tagName := strings.TrimPrefix(ref, "refs/tags/") + + if !c.Bool("force") { + // if --force is not passed then we can't overwrite tags + if existingHash, exists := state.Tags[tagName]; exists && existingHash != commitHash { + return fmt.Errorf("tag %s that is already published pointing to %s, call with --force to overwrite", tagName, existingHash) + } + } + state.Tags[tagName] = commitHash + log("- setting tag %s to commit %s\n", color.CyanString(tagName), color.CyanString(commitHash)) + } + } + // create and sign the new state event newStateEvent := state.ToEvent() err = kr.SignEvent(ctx, &newStateEvent) @@ -553,6 +590,9 @@ aside from those, there is also: if c.Bool("force") { pushArgs = append(pushArgs, "--force") } + if c.Bool("tags") { + pushArgs = append(pushArgs, "--tags") + } pushCmd := exec.Command("git", pushArgs...) pushCmd.Stderr = os.Stderr pushCmd.Stdout = os.Stdout From 9bf728d8508f11796573b7f859305c8834af2243 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sat, 20 Dec 2025 13:10:17 -0300 Subject: [PATCH 372/401] git: nip34 state as fake heads instead of fake remotes. --- git.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/git.go b/git.go index e599b97..36ccf68 100644 --- a/git.go +++ b/git.go @@ -1101,7 +1101,7 @@ func gitUpdateRefs(ctx context.Context, dir string, state nip34.RepositoryState) lines := strings.Split(string(output), "\n") for _, line := range lines { parts := strings.Fields(line) - if len(parts) >= 2 && strings.Contains(parts[1], "refs/remotes/nip34/state/") { + if len(parts) >= 2 && strings.Contains(parts[1], "refs/heads/nip34/state/") { delCmd := exec.Command("git", "update-ref", "-d", parts[1]) if dir != "" { delCmd.Dir = dir @@ -1118,7 +1118,7 @@ func gitUpdateRefs(ctx context.Context, dir string, state nip34.RepositoryState) branchName = "refs/heads/" + branchName } - refName := "refs/remotes/nip34/state/" + strings.TrimPrefix(branchName, "refs/heads/") + refName := "refs/heads/nip34/state/" + strings.TrimPrefix(branchName, "refs/heads/") updateCmd := exec.Command("git", "update-ref", refName, commit) if dir != "" { updateCmd.Dir = dir @@ -1131,7 +1131,7 @@ func gitUpdateRefs(ctx context.Context, dir string, state nip34.RepositoryState) // create ref for HEAD if state.HEAD != "" { if headCommit, ok := state.Branches[state.HEAD]; ok { - headRefName := "refs/remotes/nip34/state/HEAD" + headRefName := "refs/heads/nip34/state/HEAD" updateCmd := exec.Command("git", "update-ref", headRefName, headCommit) if dir != "" { updateCmd.Dir = dir From 8f384681039de6d9451bb5d58033549687890427 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Fri, 19 Dec 2025 15:16:22 -0300 Subject: [PATCH 373/401] basic grimoire spell support. --- filter.go | 2 +- go.mod | 2 + main.go | 3 +- req.go | 224 ++++++++++++++++-------------- spell.go | 398 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 522 insertions(+), 107 deletions(-) create mode 100644 spell.go diff --git a/filter.go b/filter.go index 75e2c41..7272a24 100644 --- a/filter.go +++ b/filter.go @@ -9,7 +9,7 @@ import ( "github.com/urfave/cli/v3" ) -var filter = &cli.Command{ +var filterCmd = &cli.Command{ Name: "filter", Usage: "applies an event filter to an event to see if it matches.", Description: ` diff --git a/go.mod b/go.mod index f5d21ae..bad41df 100644 --- a/go.mod +++ b/go.mod @@ -104,3 +104,5 @@ require ( gopkg.in/yaml.v3 v3.0.1 // indirect rsc.io/qr v0.2.0 // indirect ) + +replace fiatjaf.com/nostr => ../nostrlib diff --git a/main.go b/main.go index a960443..2ed12c3 100644 --- a/main.go +++ b/main.go @@ -28,7 +28,7 @@ var app = &cli.Command{ Commands: []*cli.Command{ event, req, - filter, + filterCmd, fetch, count, decode, @@ -53,6 +53,7 @@ var app = &cli.Command{ git, nip, syncCmd, + spell, }, Version: version, Flags: []cli.Flag{ diff --git a/req.go b/req.go index 5ee988c..3ca109c 100644 --- a/req.go +++ b/req.go @@ -9,6 +9,7 @@ import ( "slices" "strings" "sync" + "time" "fiatjaf.com/nostr" "fiatjaf.com/nostr/eventstore" @@ -77,11 +78,6 @@ example: Name: "paginate-interval", Usage: "time between queries when using --paginate", }, - &cli.UintFlag{ - Name: "paginate-global-limit", - Usage: "global limit at which --paginate should stop", - DefaultText: "uses the value given by --limit/-l or infinite", - }, &cli.BoolFlag{ Name: "bare", Usage: "when printing the filter, print just the filter, not enveloped in a [\"REQ\", ...] array", @@ -226,106 +222,7 @@ example: } } } else { - var results chan nostr.RelayEvent - var closeds chan nostr.RelayClosed - - opts := nostr.SubscriptionOptions{ - Label: "nak-req", - } - - if c.Bool("paginate") { - paginator := sys.Pool.PaginatorWithInterval(c.Duration("paginate-interval")) - results = paginator(ctx, relayUrls, filter, opts) - } else if c.Bool("outbox") { - defs := make([]nostr.DirectedFilter, 0, len(filter.Authors)*2) - - // hardcoded relays, if any - for _, relayUrl := range relayUrls { - defs = append(defs, nostr.DirectedFilter{ - Filter: filter, - Relay: relayUrl, - }) - } - - // relays for each pubkey - errg := errgroup.Group{} - errg.SetLimit(16) - mu := sync.Mutex{} - for _, pubkey := range filter.Authors { - errg.Go(func() error { - n := int(c.Uint("outbox-relays-per-pubkey")) - for _, url := range sys.FetchOutboxRelays(ctx, pubkey, n) { - if slices.Contains(relayUrls, url) { - // already hardcoded, ignore - continue - } - if !nostr.IsValidRelayURL(url) { - continue - } - - matchUrl := func(def nostr.DirectedFilter) bool { return def.Relay == url } - idx := slices.IndexFunc(defs, matchUrl) - if idx == -1 { - // new relay, add it - mu.Lock() - // check again after locking to prevent races - idx = slices.IndexFunc(defs, matchUrl) - if idx == -1 { - // then add it - filter := filter.Clone() - filter.Authors = []nostr.PubKey{pubkey} - defs = append(defs, nostr.DirectedFilter{ - Filter: filter, - Relay: url, - }) - mu.Unlock() - continue // done with this relay url - } - - // otherwise we'll just use the idx - mu.Unlock() - } - - // existing relay, add this pubkey - defs[idx].Authors = append(defs[idx].Authors, pubkey) - } - - return nil - }) - } - errg.Wait() - - if c.Bool("stream") { - results, closeds = sys.Pool.BatchedSubscribeManyNotifyClosed(ctx, defs, opts) - } else { - results, closeds = sys.Pool.BatchedQueryManyNotifyClosed(ctx, defs, opts) - } - } else { - if c.Bool("stream") { - results, closeds = sys.Pool.SubscribeManyNotifyClosed(ctx, relayUrls, filter, opts) - } else { - results, closeds = sys.Pool.FetchManyNotifyClosed(ctx, relayUrls, filter, opts) - } - } - - readevents: - for { - select { - case ie, ok := <-results: - if !ok { - break readevents - } - stdout(ie.Event) - case closed := <-closeds: - if closed.HandledAuth { - logverbose("%s CLOSED: %s\n", closed.Relay.URL, closed.Reason) - } else { - log("%s CLOSED: %s\n", closed.Relay.URL, closed.Reason) - } - case <-ctx.Done(): - break readevents - } - } + performReq(ctx, filter, relayUrls, c.Bool("stream"), c.Bool("outbox"), c.Uint("outbox-relays-per-pubkey"), c.Bool("paginate"), c.Duration("paginate-interval"), "nak-req") } } else { // no relays given, will just print the filter @@ -346,6 +243,123 @@ example: }, } +func performReq( + ctx context.Context, + filter nostr.Filter, + relayUrls []string, + stream bool, + outbox bool, + outboxRelaysPerPubKey uint64, + paginate bool, + paginateInterval time.Duration, + label string, +) { + var results chan nostr.RelayEvent + var closeds chan nostr.RelayClosed + + opts := nostr.SubscriptionOptions{ + Label: label, + } + + if paginate { + paginator := sys.Pool.PaginatorWithInterval(paginateInterval) + results = paginator(ctx, relayUrls, filter, opts) + } else if outbox { + defs := make([]nostr.DirectedFilter, 0, len(filter.Authors)*2) + + for _, relayUrl := range relayUrls { + defs = append(defs, nostr.DirectedFilter{ + Filter: filter, + Relay: relayUrl, + }) + } + + // relays for each pubkey + errg := errgroup.Group{} + errg.SetLimit(16) + mu := sync.Mutex{} + logverbose("gathering outbox relays for %d authors...\n", len(filter.Authors)) + for _, pubkey := range filter.Authors { + errg.Go(func() error { + n := int(outboxRelaysPerPubKey) + for _, url := range sys.FetchOutboxRelays(ctx, pubkey, n) { + if slices.Contains(relayUrls, url) { + // already specified globally, ignore + continue + } + if !nostr.IsValidRelayURL(url) { + continue + } + + matchUrl := func(def nostr.DirectedFilter) bool { return def.Relay == url } + idx := slices.IndexFunc(defs, matchUrl) + if idx == -1 { + // new relay, add it + mu.Lock() + // check again after locking to prevent races + idx = slices.IndexFunc(defs, matchUrl) + if idx == -1 { + // then add it + filter := filter.Clone() + filter.Authors = []nostr.PubKey{pubkey} + defs = append(defs, nostr.DirectedFilter{ + Filter: filter, + Relay: url, + }) + mu.Unlock() + continue // done with this relay url + } + + // otherwise we'll just use the idx + mu.Unlock() + } + + // existing relay, add this pubkey + defs[idx].Authors = append(defs[idx].Authors, pubkey) + } + + return nil + }) + } + errg.Wait() + + if stream { + logverbose("running subscription with %d directed filters...\n", len(defs)) + results, closeds = sys.Pool.BatchedSubscribeManyNotifyClosed(ctx, defs, opts) + } else { + logverbose("running query with %d directed filters...\n", len(defs)) + results, closeds = sys.Pool.BatchedQueryManyNotifyClosed(ctx, defs, opts) + } + } else { + if stream { + logverbose("running subscription to %d relays...\n", len(relayUrls)) + results, closeds = sys.Pool.SubscribeManyNotifyClosed(ctx, relayUrls, filter, opts) + } else { + logverbose("running query to %d relays...\n", len(relayUrls)) + results, closeds = sys.Pool.FetchManyNotifyClosed(ctx, relayUrls, filter, opts) + } + } + +readevents: + for { + select { + case ie, ok := <-results: + if !ok { + break readevents + } + stdout(ie.Event) + case closed := <-closeds: + if closed.HandledAuth { + logverbose("%s CLOSED: %s\n", closed.Relay.URL, closed.Reason) + } else { + log("%s CLOSED: %s\n", closed.Relay.URL, closed.Reason) + } + case <-ctx.Done(): + break readevents + } + } +} + var reqFilterFlags = []cli.Flag{ &PubKeySliceFlag{ Name: "author", diff --git a/spell.go b/spell.go new file mode 100644 index 0000000..61b910b --- /dev/null +++ b/spell.go @@ -0,0 +1,398 @@ +package main + +import ( + "bufio" + "context" + "fmt" + "os" + "path/filepath" + "strconv" + "strings" + "time" + + "fiatjaf.com/nostr" + "fiatjaf.com/nostr/nip19" + "fiatjaf.com/nostr/sdk/hints" + "github.com/fatih/color" + "github.com/markusmobius/go-dateparser" + "github.com/urfave/cli/v3" +) + +var spell = &cli.Command{ + Name: "spell", + Usage: "downloads a spell event and executes its REQ request", + ArgsUsage: "[nevent_code]", + Description: `fetches a spell event (kind 777) and executes REQ command encoded in its tags.`, + Flags: append(defaultKeyFlags, + &cli.UintFlag{ + Name: "outbox-relays-per-pubkey", + Aliases: []string{"n"}, + Usage: "number of outbox relays to use for each pubkey", + Value: 3, + }, + ), + Action: func(ctx context.Context, c *cli.Command) error { + // load history from file + var history []SpellHistoryEntry + historyPath, err := getSpellHistoryPath() + if err == nil { + file, err := os.Open(historyPath) + if err == nil { + defer file.Close() + scanner := bufio.NewScanner(file) + for scanner.Scan() { + var entry SpellHistoryEntry + if err := json.Unmarshal([]byte(scanner.Text()), &entry); err != nil { + continue // skip invalid entries + } + history = append(history, entry) + } + } + } + + if c.Args().Len() == 0 { + log("recent spells:\n") + for i, entry := range history { + if i >= 10 { + break + } + + displayName := entry.Name + if displayName == "" { + displayName = entry.Content + if len(displayName) > 28 { + displayName = displayName[:27] + "…" + } + } + if displayName != "" { + displayName = displayName + ": " + } + + desc := entry.Content + if len(desc) > 50 { + desc = desc[0:49] + "…" + } + + lastUsed := entry.LastUsed.Format("2006-01-02 15:04") + stdout(fmt.Sprintf(" %s %s%s - %s\n", + color.BlueString(entry.Identifier), + displayName, + color.YellowString(lastUsed), + desc, + )) + } + return nil + } + + // decode nevent to get the spell event + var pointer nostr.EventPointer + identifier := c.Args().First() + prefix, value, err := nip19.Decode(identifier) + if err == nil { + if prefix != "nevent" { + return fmt.Errorf("expected nevent code, got %s", prefix) + } + pointer = value.(nostr.EventPointer) + } else { + // search our history + for _, entry := range history { + if entry.Identifier == identifier { + pointer = entry.Pointer + break + } + } + } + + if pointer.ID == nostr.ZeroID { + return fmt.Errorf("invalid spell reference") + } + + // fetch spell + relays := pointer.Relays + if pointer.Author != nostr.ZeroPK { + for _, url := range relays { + sys.Hints.Save(pointer.Author, nostr.NormalizeURL(url), hints.LastInHint, nostr.Now()) + } + relays = append(relays, sys.FetchOutboxRelays(ctx, pointer.Author, 3)...) + } + spell := sys.Pool.QuerySingle(ctx, relays, nostr.Filter{IDs: []nostr.ID{pointer.ID}}, + nostr.SubscriptionOptions{Label: "nak-spell-f"}) + if spell == nil { + return fmt.Errorf("spell event not found") + } + if spell.Kind != 777 { + return fmt.Errorf("event is not a spell (expected kind 777, got %d)", spell.Kind) + } + + // parse spell tags to build REQ filter + spellFilter, err := buildSpellReq(ctx, c, spell.Tags) + if err != nil { + return fmt.Errorf("failed to parse spell tags: %w", err) + } + + // determine relays to query + var spellRelays []string + var outbox bool + relaysTag := spell.Event.Tags.Find("relays") + if relaysTag == nil { + // if this tag doesn't exist assume $outbox + relaysTag = nostr.Tag{"relays", "$outbox"} + } + for i := 1; i < len(relaysTag); i++ { + switch relaysTag[i] { + case "$outbox": + outbox = true + default: + relays = append(relays, relaysTag[i]) + } + } + + stream := !spell.Tags.Has("close-on-eose") + + // fill in the author if we didn't have it + pointer.Author = spell.PubKey + + // add to history before execution + { + idStr := nip19.EncodeNevent(spell.ID, nil, nostr.ZeroPK) + identifier = "spell" + idStr[len(idStr)-7:] + nameTag := spell.Tags.Find("name") + var name string + if nameTag != nil { + name = nameTag[1] + } + if len(history) > 100 { + history = history[:100] + } + // write back to file + file, err := os.Create(historyPath) + if err != nil { + return err + } + data, _ := json.Marshal(SpellHistoryEntry{ + Identifier: identifier, + Name: name, + Content: spell.Content, + LastUsed: time.Now(), + Pointer: pointer, + }) + file.Write(data) + file.Write([]byte{'\n'}) + for i, entry := range history { + // limit history size (keep last 100) + if i == 100 { + break + } + + data, _ := json.Marshal(entry) + file.Write(data) + file.Write([]byte{'\n'}) + } + file.Close() + + logverbose("executing %s: %s relays=%v outbox=%v stream=%v\n", + identifier, spellFilter, spellRelays, outbox, stream) + } + + // execute + performReq(ctx, spellFilter, spellRelays, stream, outbox, c.Uint("outbox-relays-per-pubkey"), false, 0, "nak-spell") + + return nil + }, +} + +func buildSpellReq(ctx context.Context, c *cli.Command, tags nostr.Tags) (nostr.Filter, error) { + filter := nostr.Filter{} + + getMe := func() (nostr.PubKey, error) { + kr, _, err := gatherKeyerFromArguments(ctx, c) + if err != nil { + return nostr.ZeroPK, fmt.Errorf("failed to get keyer: %w", err) + } + + pubkey, err := kr.GetPublicKey(ctx) + if err != nil { + return nostr.ZeroPK, fmt.Errorf("failed to get public key from keyer: %w", err) + } + + return pubkey, nil + } + + for _, tag := range tags { + if len(tag) == 0 { + continue + } + + switch tag[0] { + case "cmd": + if len(tag) < 2 || tag[1] != "REQ" { + return nostr.Filter{}, fmt.Errorf("only REQ commands are supported") + } + + case "k": + for i := 1; i < len(tag); i++ { + if kind, err := strconv.Atoi(tag[i]); err == nil { + filter.Kinds = append(filter.Kinds, nostr.Kind(kind)) + } + } + + case "authors": + for i := 1; i < len(tag); i++ { + switch tag[i] { + case "$me": + me, err := getMe() + if err != nil { + return nostr.Filter{}, err + } + filter.Authors = append(filter.Authors, me) + case "$contacts": + me, err := getMe() + if err != nil { + return nostr.Filter{}, err + } + for _, f := range sys.FetchFollowList(ctx, me).Items { + filter.Authors = append(filter.Authors, f.Pubkey) + } + default: + pubkey, err := nostr.PubKeyFromHex(tag[i]) + if err != nil { + return nostr.Filter{}, fmt.Errorf("invalid pubkey '%s' in 'authors': %w", tag[i], err) + } + filter.Authors = append(filter.Authors, pubkey) + } + } + + case "ids": + for i := 1; i < len(tag); i++ { + id, err := nostr.IDFromHex(tag[i]) + if err != nil { + return nostr.Filter{}, fmt.Errorf("invalid id '%s' in 'authors': %w", tag[i], err) + } + filter.IDs = append(filter.IDs, id) + } + + case "tag": + if len(tag) < 3 { + continue + } + tagName := tag[1] + if filter.Tags == nil { + filter.Tags = make(nostr.TagMap) + } + for i := 2; i < len(tag); i++ { + switch tag[i] { + case "$me": + me, err := getMe() + if err != nil { + return nostr.Filter{}, err + } + filter.Tags[tagName] = append(filter.Tags[tagName], me.Hex()) + case "$contacts": + me, err := getMe() + if err != nil { + return nostr.Filter{}, err + } + for _, f := range sys.FetchFollowList(ctx, me).Items { + filter.Tags[tagName] = append(filter.Tags[tagName], f.Pubkey.Hex()) + } + default: + filter.Tags[tagName] = append(filter.Tags[tagName], tag[i]) + } + } + + case "limit": + if len(tag) >= 2 { + if limit, err := strconv.Atoi(tag[1]); err == nil { + filter.Limit = limit + } + } + + case "since": + if len(tag) >= 2 { + date, err := dateparser.Parse(&dateparser.Configuration{ + DefaultTimezone: time.Local, + CurrentTime: time.Now(), + }, tag[1]) + if err != nil { + return nostr.Filter{}, fmt.Errorf("invalid date %s: %w", tag[1], err) + } + filter.Since = nostr.Timestamp(date.Time.Unix()) + } + + case "until": + if len(tag) >= 2 { + date, err := dateparser.Parse(&dateparser.Configuration{ + DefaultTimezone: time.Local, + CurrentTime: time.Now(), + }, tag[1]) + if err != nil { + return nostr.Filter{}, fmt.Errorf("invalid date %s: %w", tag[1], err) + } + filter.Until = nostr.Timestamp(date.Time.Unix()) + } + + case "search": + if len(tag) >= 2 { + filter.Search = tag[1] + } + } + } + + return filter, nil +} + +func parseRelativeTime(timeStr string) (nostr.Timestamp, error) { + // Handle special cases + switch timeStr { + case "now": + return nostr.Now(), nil + } + + // Try to parse as relative time (e.g., "7d", "1h", "30m") + if strings.HasSuffix(timeStr, "d") { + days := strings.TrimSuffix(timeStr, "d") + if daysInt, err := strconv.Atoi(days); err == nil { + return nostr.Now() - nostr.Timestamp(daysInt*24*60*60), nil + } + } else if strings.HasSuffix(timeStr, "h") { + hours := strings.TrimSuffix(timeStr, "h") + if hoursInt, err := strconv.Atoi(hours); err == nil { + return nostr.Now() - nostr.Timestamp(hoursInt*60*60), nil + } + } else if strings.HasSuffix(timeStr, "m") { + minutes := strings.TrimSuffix(timeStr, "m") + if minutesInt, err := strconv.Atoi(minutes); err == nil { + return nostr.Now() - nostr.Timestamp(minutesInt*60), nil + } + } + + // try to parse as direct timestamp + if ts, err := strconv.ParseInt(timeStr, 10, 64); err == nil { + return nostr.Timestamp(ts), nil + } + + return 0, fmt.Errorf("invalid time format: %s", timeStr) +} + +type SpellHistoryEntry struct { + Identifier string `json:"_id"` + Name string `json:"name,omitempty"` + Content string `json:"content,omitempty"` + LastUsed time.Time `json:"last_used"` + Pointer nostr.EventPointer `json:"pointer"` +} + +func getSpellHistoryPath() (string, error) { + home, err := os.UserHomeDir() + if err != nil { + return "", err + } + historyDir := filepath.Join(home, ".config", "nak", "spells") + + // create directory if it doesn't exist + if err := os.MkdirAll(historyDir, 0755); err != nil { + return "", err + } + + return filepath.Join(historyDir, "history"), nil +} From 1b7f3162b5254454893180c62188b6bf6bcfe773 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sun, 21 Dec 2025 21:58:15 -0300 Subject: [PATCH 374/401] automatically create hints, store, and kvstore for system always. --- main.go | 33 +++++++++++++++++++++++++----- outbox.go | 61 +------------------------------------------------------ 2 files changed, 29 insertions(+), 65 deletions(-) diff --git a/main.go b/main.go index 2ed12c3..e0124c1 100644 --- a/main.go +++ b/main.go @@ -2,14 +2,17 @@ package main import ( "context" - "fmt" "net/http" "net/textproto" "os" "path/filepath" "fiatjaf.com/nostr" + "fiatjaf.com/nostr/eventstore/boltdb" + "fiatjaf.com/nostr/eventstore/nullstore" "fiatjaf.com/nostr/sdk" + "fiatjaf.com/nostr/sdk/hints/bbolth" + "fiatjaf.com/nostr/sdk/kvstore/bbolt" "github.com/fatih/color" "github.com/urfave/cli/v3" ) @@ -44,7 +47,7 @@ var app = &cli.Command{ encrypt, decrypt, gift, - outbox, + outboxCmd, wallet, mcpServer, curl, @@ -64,7 +67,7 @@ var app = &cli.Command{ if home, err := os.UserHomeDir(); err == nil { return filepath.Join(home, ".config/nak") } else { - return filepath.Join("/dev/null") + return "" } })(), }, @@ -100,8 +103,28 @@ var app = &cli.Command{ Before: func(ctx context.Context, c *cli.Command) (context.Context, error) { sys = sdk.NewSystem() - if err := initializeOutboxHintsDB(c, sys); err != nil { - return ctx, fmt.Errorf("failed to initialize outbox hints: %w", err) + configPath := c.String("config-path") + if configPath != "" { + os.MkdirAll(filepath.Join("outbox"), 0755) + hintsFilePath := filepath.Join(configPath, "outbox/hints.db") + _, err := bbolth.NewBoltHints(hintsFilePath) + if err != nil { + log("failed to create bolt hints db at '%s': %s\n", hintsFilePath, err) + } + + eventsPath := filepath.Join(configPath, "events") + sys.Store = &boltdb.BoltBackend{Path: eventsPath} + if err := sys.Store.Init(); err != nil { + log("failed to create boltdb events db at '%s': %s\n", eventsPath, err) + sys.Store = &nullstore.NullStore{} + } + + kvPath := filepath.Join(configPath, "kvstore") + if kv, err := bbolt.NewStore(kvPath); err != nil { + log("failed to create boltdb kvstore db at '%s': %s\n", kvPath, err) + } else { + sys.KVStore = kv + } } sys.Pool = nostr.NewPool(nostr.PoolOptions{ diff --git a/outbox.go b/outbox.go index 82fc775..9d95329 100644 --- a/outbox.go +++ b/outbox.go @@ -3,80 +3,21 @@ package main import ( "context" "fmt" - "os" - "path/filepath" - "fiatjaf.com/nostr/sdk" - "fiatjaf.com/nostr/sdk/hints/bbolth" - "github.com/fatih/color" "github.com/urfave/cli/v3" ) -var ( - hintsFilePath string - hintsFileExists bool -) - -func initializeOutboxHintsDB(c *cli.Command, sys *sdk.System) error { - configPath := c.String("config-path") - if configPath != "" { - hintsFilePath = filepath.Join(configPath, "outbox/hints.db") - } - if hintsFilePath != "" { - if _, err := os.Stat(hintsFilePath); err == nil { - hintsFileExists = true - } else if !os.IsNotExist(err) { - return err - } - } - if hintsFileExists && hintsFilePath != "" { - hintsdb, err := bbolth.NewBoltHints(hintsFilePath) - if err == nil { - sys.Hints = hintsdb - } - } - - return nil -} - -var outbox = &cli.Command{ +var outboxCmd = &cli.Command{ Name: "outbox", Usage: "manage outbox relay hints database", DisableSliceFlagSeparator: true, Commands: []*cli.Command{ - { - Name: "init", - Usage: "initialize the outbox hints database", - DisableSliceFlagSeparator: true, - Action: func(ctx context.Context, c *cli.Command) error { - if hintsFileExists { - return nil - } - if hintsFilePath == "" { - return fmt.Errorf("couldn't find a place to store the hints, pass --config-path to fix.") - } - - os.MkdirAll(hintsFilePath, 0755) - _, err := bbolth.NewBoltHints(hintsFilePath) - if err != nil { - return fmt.Errorf("failed to create bolt hints db at '%s': %w", hintsFilePath, err) - } - - log("initialized hints database at %s\n", hintsFilePath) - return nil - }, - }, { Name: "list", Usage: "list outbox relays for a given pubkey", ArgsUsage: "", DisableSliceFlagSeparator: true, Action: func(ctx context.Context, c *cli.Command) error { - if !hintsFileExists { - log(color.YellowString("running with temporary fragile data.\n")) - log(color.YellowString("call `nak outbox init` to setup persistence.\n")) - } - if c.Args().Len() != 1 { return fmt.Errorf("expected exactly one argument (pubkey)") } From 21423b4a21954c1a5ca8b85c2b54f202d9e3165e Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sun, 21 Dec 2025 21:59:44 -0300 Subject: [PATCH 375/401] spell: execute from stdin event. --- spell.go | 81 ++++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 55 insertions(+), 26 deletions(-) diff --git a/spell.go b/spell.go index 61b910b..2b70bc2 100644 --- a/spell.go +++ b/spell.go @@ -32,25 +32,69 @@ var spell = &cli.Command{ }, ), Action: func(ctx context.Context, c *cli.Command) error { + configPath := c.String("config-path") + os.MkdirAll(filepath.Join(configPath, "spells"), 0755) + // load history from file var history []SpellHistoryEntry - historyPath, err := getSpellHistoryPath() + historyPath := filepath.Join(configPath, "spells/history") + file, err := os.Open(historyPath) if err == nil { - file, err := os.Open(historyPath) - if err == nil { - defer file.Close() - scanner := bufio.NewScanner(file) - for scanner.Scan() { - var entry SpellHistoryEntry - if err := json.Unmarshal([]byte(scanner.Text()), &entry); err != nil { - continue // skip invalid entries - } - history = append(history, entry) + defer file.Close() + scanner := bufio.NewScanner(file) + for scanner.Scan() { + var entry SpellHistoryEntry + if err := json.Unmarshal([]byte(scanner.Text()), &entry); err != nil { + continue // skip invalid entries } + history = append(history, entry) } } if c.Args().Len() == 0 { + // check if we have input from stdin + for stdinEvent := range getJsonsOrBlank() { + var spell nostr.Event + if err := json.Unmarshal([]byte(stdinEvent), &spell); err != nil { + return fmt.Errorf("failed to parse spell event from stdin: %w", err) + } + if spell.Kind != 777 { + return fmt.Errorf("event is not a spell (expected kind 777, got %d)", spell.Kind) + } + // parse spell tags to build REQ filter + spellFilter, err := buildSpellReq(ctx, c, spell.Tags) + if err != nil { + return fmt.Errorf("failed to parse spell tags: %w", err) + } + // determine relays to query + var spellRelays []string + var outbox bool + relaysTag := spell.Tags.Find("relays") + if relaysTag == nil { + // if this tag doesn't exist assume $outbox + relaysTag = nostr.Tag{"relays", "$outbox"} + } + for i := 1; i < len(relaysTag); i++ { + switch relaysTag[i] { + case "$outbox": + outbox = true + default: + spellRelays = append(spellRelays, relaysTag[i]) + } + } + + stream := !spell.Tags.Has("close-on-eose") + + logverbose("executing spell from stdin: %s relays=%v outbox=%v stream=%v\n", + spellFilter, spellRelays, outbox, stream) + + // execute without adding to history + performReq(ctx, spellFilter, spellRelays, stream, outbox, c.Uint("outbox-relays-per-pubkey"), false, 0, "nak-spell") + + return nil + } + + // no stdin input, show recent spells log("recent spells:\n") for i, entry := range history { if i >= 10 { @@ -381,18 +425,3 @@ type SpellHistoryEntry struct { LastUsed time.Time `json:"last_used"` Pointer nostr.EventPointer `json:"pointer"` } - -func getSpellHistoryPath() (string, error) { - home, err := os.UserHomeDir() - if err != nil { - return "", err - } - historyDir := filepath.Join(home, ".config", "nak", "spells") - - // create directory if it doesn't exist - if err := os.MkdirAll(historyDir, 0755); err != nil { - return "", err - } - - return filepath.Join(historyDir, "history"), nil -} From e91d4429ec5ff021554bc5c3f7af5edaa858b1cd Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Mon, 22 Dec 2025 00:18:43 -0300 Subject: [PATCH 376/401] switch the local databases to lmdb so they can be accessed by multiple nak instances at the same time. --- event.go | 2 +- go.mod | 2 +- go.sum | 6 ------ main.go | 22 ++++++++++++---------- 4 files changed, 14 insertions(+), 18 deletions(-) diff --git a/event.go b/event.go index fb4de9f..1f351ea 100644 --- a/event.go +++ b/event.go @@ -24,7 +24,7 @@ const ( CATEGORY_EXTRAS = "EXTRAS" ) -var event = &cli.Command{ +var eventCmd = &cli.Command{ Name: "event", Usage: "generates an encoded event and either prints it or sends it to a set of relays", Description: `outputs an event built with the flags. if one or more relays are given as arguments, an attempt is also made to publish the event to these relays. diff --git a/go.mod b/go.mod index bad41df..530648a 100644 --- a/go.mod +++ b/go.mod @@ -32,6 +32,7 @@ require ( require ( github.com/FastFilter/xorfilter v0.2.1 // indirect github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3 // indirect + github.com/PowerDNS/lmdb-go v1.9.3 // indirect github.com/alecthomas/chroma/v2 v2.14.0 // indirect github.com/andybalholm/brotli v1.1.1 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect @@ -96,7 +97,6 @@ require ( github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/yuin/goldmark v1.7.8 // indirect github.com/yuin/goldmark-emoji v1.0.5 // indirect - go.etcd.io/bbolt v1.4.2 // indirect golang.org/x/crypto v0.39.0 // indirect golang.org/x/net v0.41.0 // indirect golang.org/x/sys v0.35.0 // indirect diff --git a/go.sum b/go.sum index 4951c40..df40a3d 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,5 @@ fiatjaf.com/lib v0.3.1 h1:/oFQwNtFRfV+ukmOCxfBEAuayoLwXp4wu2/fz5iHpwA= fiatjaf.com/lib v0.3.1/go.mod h1:Ycqq3+mJ9jAWu7XjbQI1cVr+OFgnHn79dQR5oTII47g= -fiatjaf.com/nostr v0.0.0-20251201232830-91548fa0a157 h1:14yLsO2HwpS2CLIKFvLMDp8tVEDahwdC8OeG6NGaL+M= -fiatjaf.com/nostr v0.0.0-20251201232830-91548fa0a157/go.mod h1:ue7yw0zHfZj23Ml2kVSdBx0ENEaZiuvGxs/8VEN93FU= -fiatjaf.com/nostr v0.0.0-20251204122254-07061404918d h1:xROmiuT7LrZk+/iGGeTqRI4liqJZrc87AWjsyHtbqDg= -fiatjaf.com/nostr v0.0.0-20251204122254-07061404918d/go.mod h1:ue7yw0zHfZj23Ml2kVSdBx0ENEaZiuvGxs/8VEN93FU= github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ= github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= github.com/FastFilter/xorfilter v0.2.1 h1:lbdeLG9BdpquK64ZsleBS8B4xO/QW1IM0gMzF7KaBKc= @@ -283,8 +279,6 @@ github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic= github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk= github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U= -go.etcd.io/bbolt v1.4.2 h1:IrUHp260R8c+zYx/Tm8QZr04CX+qWS5PGfPdevhdm1I= -go.etcd.io/bbolt v1.4.2/go.mod h1:Is8rSHO/b4f3XigBC0lL0+4FwAQv3HXEEIgFMuKHceM= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= diff --git a/main.go b/main.go index e0124c1..b20a338 100644 --- a/main.go +++ b/main.go @@ -8,11 +8,11 @@ import ( "path/filepath" "fiatjaf.com/nostr" - "fiatjaf.com/nostr/eventstore/boltdb" + "fiatjaf.com/nostr/eventstore/lmdb" "fiatjaf.com/nostr/eventstore/nullstore" "fiatjaf.com/nostr/sdk" - "fiatjaf.com/nostr/sdk/hints/bbolth" - "fiatjaf.com/nostr/sdk/kvstore/bbolt" + "fiatjaf.com/nostr/sdk/hints/lmdbh" + lmdbkv "fiatjaf.com/nostr/sdk/kvstore/lmdb" "github.com/fatih/color" "github.com/urfave/cli/v3" ) @@ -29,7 +29,7 @@ var app = &cli.Command{ Usage: "the nostr army knife command-line tool", DisableSliceFlagSeparator: true, Commands: []*cli.Command{ - event, + eventCmd, req, filterCmd, fetch, @@ -105,22 +105,24 @@ var app = &cli.Command{ configPath := c.String("config-path") if configPath != "" { - os.MkdirAll(filepath.Join("outbox"), 0755) - hintsFilePath := filepath.Join(configPath, "outbox/hints.db") - _, err := bbolth.NewBoltHints(hintsFilePath) + hintsPath := filepath.Join(configPath, "outbox/hints") + os.MkdirAll(hintsPath, 0755) + _, err := lmdbh.NewLMDBHints(hintsPath) if err != nil { - log("failed to create bolt hints db at '%s': %s\n", hintsFilePath, err) + log("failed to create lmdb hints db at '%s': %s\n", hintsPath, err) } eventsPath := filepath.Join(configPath, "events") - sys.Store = &boltdb.BoltBackend{Path: eventsPath} + os.MkdirAll(eventsPath, 0755) + sys.Store = &lmdb.LMDBBackend{Path: eventsPath} if err := sys.Store.Init(); err != nil { log("failed to create boltdb events db at '%s': %s\n", eventsPath, err) sys.Store = &nullstore.NullStore{} } kvPath := filepath.Join(configPath, "kvstore") - if kv, err := bbolt.NewStore(kvPath); err != nil { + os.MkdirAll(kvPath, 0755) + if kv, err := lmdbkv.NewStore(kvPath); err != nil { log("failed to create boltdb kvstore db at '%s': %s\n", kvPath, err) } else { sys.KVStore = kv From e01cfbde47e7cdad589df10b802742c4c2f41317 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Mon, 22 Dec 2025 00:19:10 -0300 Subject: [PATCH 377/401] print spell details before running. --- spell.go | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/spell.go b/spell.go index 2b70bc2..7238b18 100644 --- a/spell.go +++ b/spell.go @@ -89,6 +89,7 @@ var spell = &cli.Command{ spellFilter, spellRelays, outbox, stream) // execute without adding to history + logSpellDetails(spell) performReq(ctx, spellFilter, spellRelays, stream, outbox, c.Uint("outbox-relays-per-pubkey"), false, 0, "nak-spell") return nil @@ -109,7 +110,7 @@ var spell = &cli.Command{ } } if displayName != "" { - displayName = displayName + ": " + displayName = color.HiMagentaString(displayName) + ": " } desc := entry.Content @@ -239,6 +240,7 @@ var spell = &cli.Command{ } // execute + logSpellDetails(spell.Event) performReq(ctx, spellFilter, spellRelays, stream, outbox, c.Uint("outbox-relays-per-pubkey"), false, 0, "nak-spell") return nil @@ -425,3 +427,31 @@ type SpellHistoryEntry struct { LastUsed time.Time `json:"last_used"` Pointer nostr.EventPointer `json:"pointer"` } + +func logSpellDetails(spell nostr.Event) { + nameTag := spell.Tags.Find("name") + name := "" + if nameTag != nil { + name = nameTag[1] + if len(name) > 28 { + name = name[:27] + "…" + } + } + if name != "" { + name = ": " + color.HiMagentaString(name) + } + + desc := spell.Content + if len(desc) > 50 { + desc = desc[0:49] + "…" + } + + idStr := nip19.EncodeNevent(spell.ID, nil, nostr.ZeroPK) + identifier := "spell" + idStr[len(idStr)-7:] + + log("running %s%s - %s\n", + color.BlueString(identifier), + name, + desc, + ) +} From 3be80c29dfe803c687c6fa57818b1d931797bcb6 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Mon, 22 Dec 2025 00:22:14 -0300 Subject: [PATCH 378/401] spell: fix listing recent. --- spell.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/spell.go b/spell.go index 7238b18..55092d1 100644 --- a/spell.go +++ b/spell.go @@ -54,6 +54,10 @@ var spell = &cli.Command{ if c.Args().Len() == 0 { // check if we have input from stdin for stdinEvent := range getJsonsOrBlank() { + if stdinEvent == "{}" { + break + } + var spell nostr.Event if err := json.Unmarshal([]byte(stdinEvent), &spell); err != nil { return fmt.Errorf("failed to parse spell event from stdin: %w", err) From b95665d986a5c4476d0f4ce3ec22c665adbdd5bf Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Mon, 22 Dec 2025 11:59:17 -0300 Subject: [PATCH 379/401] spell: store spells locally and add stdin spell to history. --- spell.go | 220 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 112 insertions(+), 108 deletions(-) diff --git a/spell.go b/spell.go index 55092d1..6804031 100644 --- a/spell.go +++ b/spell.go @@ -65,38 +65,8 @@ var spell = &cli.Command{ if spell.Kind != 777 { return fmt.Errorf("event is not a spell (expected kind 777, got %d)", spell.Kind) } - // parse spell tags to build REQ filter - spellFilter, err := buildSpellReq(ctx, c, spell.Tags) - if err != nil { - return fmt.Errorf("failed to parse spell tags: %w", err) - } - // determine relays to query - var spellRelays []string - var outbox bool - relaysTag := spell.Tags.Find("relays") - if relaysTag == nil { - // if this tag doesn't exist assume $outbox - relaysTag = nostr.Tag{"relays", "$outbox"} - } - for i := 1; i < len(relaysTag); i++ { - switch relaysTag[i] { - case "$outbox": - outbox = true - default: - spellRelays = append(spellRelays, relaysTag[i]) - } - } - stream := !spell.Tags.Has("close-on-eose") - - logverbose("executing spell from stdin: %s relays=%v outbox=%v stream=%v\n", - spellFilter, spellRelays, outbox, stream) - - // execute without adding to history - logSpellDetails(spell) - performReq(ctx, spellFilter, spellRelays, stream, outbox, c.Uint("outbox-relays-per-pubkey"), false, 0, "nak-spell") - - return nil + return runSpell(ctx, c, historyPath, history, nostr.EventPointer{ID: spell.ID}, spell) } // no stdin input, show recent spells @@ -123,13 +93,14 @@ var spell = &cli.Command{ } lastUsed := entry.LastUsed.Format("2006-01-02 15:04") - stdout(fmt.Sprintf(" %s %s%s - %s\n", + stdout(fmt.Sprintf(" %s %s%s - %s", color.BlueString(entry.Identifier), displayName, color.YellowString(lastUsed), desc, )) } + return nil } @@ -156,99 +127,132 @@ var spell = &cli.Command{ return fmt.Errorf("invalid spell reference") } - // fetch spell - relays := pointer.Relays - if pointer.Author != nostr.ZeroPK { - for _, url := range relays { - sys.Hints.Save(pointer.Author, nostr.NormalizeURL(url), hints.LastInHint, nostr.Now()) - } - relays = append(relays, sys.FetchOutboxRelays(ctx, pointer.Author, 3)...) + // first try to fetch spell from sys.Store + var spell nostr.Event + found := false + for evt := range sys.Store.QueryEvents(nostr.Filter{IDs: []nostr.ID{pointer.ID}}, 1) { + spell = evt + found = true + break } - spell := sys.Pool.QuerySingle(ctx, relays, nostr.Filter{IDs: []nostr.ID{pointer.ID}}, - nostr.SubscriptionOptions{Label: "nak-spell-f"}) - if spell == nil { - return fmt.Errorf("spell event not found") + + var relays []string + if !found { + // if not found in store, fetch from external relays + relays = pointer.Relays + if pointer.Author != nostr.ZeroPK { + for _, url := range relays { + sys.Hints.Save(pointer.Author, nostr.NormalizeURL(url), hints.LastInHint, nostr.Now()) + } + relays = append(relays, sys.FetchOutboxRelays(ctx, pointer.Author, 3)...) + } + result := sys.Pool.QuerySingle(ctx, relays, nostr.Filter{IDs: []nostr.ID{pointer.ID}}, + nostr.SubscriptionOptions{Label: "nak-spell-f"}) + if result == nil { + return fmt.Errorf("spell event not found") + } + spell = result.Event } if spell.Kind != 777 { return fmt.Errorf("event is not a spell (expected kind 777, got %d)", spell.Kind) } - // parse spell tags to build REQ filter - spellFilter, err := buildSpellReq(ctx, c, spell.Tags) + return runSpell(ctx, c, historyPath, history, pointer, spell) + }, +} + +func runSpell( + ctx context.Context, + c *cli.Command, + historyPath string, + history []SpellHistoryEntry, + pointer nostr.EventPointer, + spell nostr.Event, +) error { + // parse spell tags to build REQ filter + spellFilter, err := buildSpellReq(ctx, c, spell.Tags) + if err != nil { + return fmt.Errorf("failed to parse spell tags: %w", err) + } + + // determine relays to query + var spellRelays []string + var outbox bool + relaysTag := spell.Tags.Find("relays") + if relaysTag == nil { + // if this tag doesn't exist assume $outbox + relaysTag = nostr.Tag{"relays", "$outbox"} + } + for i := 1; i < len(relaysTag); i++ { + switch relaysTag[i] { + case "$outbox": + outbox = true + default: + spellRelays = append(spellRelays, relaysTag[i]) + } + } + + stream := !spell.Tags.Has("close-on-eose") + + // fill in the author if we didn't have it + pointer.Author = spell.PubKey + + // save spell to sys.Store + if err := sys.Store.SaveEvent(spell); err != nil { + logverbose("failed to save spell to store: %v\n", err) + } + + // add to history before execution + { + idStr := nip19.EncodeNevent(spell.ID, nil, nostr.ZeroPK) + identifier := "spell" + idStr[len(idStr)-7:] + nameTag := spell.Tags.Find("name") + var name string + if nameTag != nil { + name = nameTag[1] + } + if len(history) > 100 { + history = history[:100] + } + // write back to file + file, err := os.Create(historyPath) if err != nil { - return fmt.Errorf("failed to parse spell tags: %w", err) + return err } - - // determine relays to query - var spellRelays []string - var outbox bool - relaysTag := spell.Event.Tags.Find("relays") - if relaysTag == nil { - // if this tag doesn't exist assume $outbox - relaysTag = nostr.Tag{"relays", "$outbox"} - } - for i := 1; i < len(relaysTag); i++ { - switch relaysTag[i] { - case "$outbox": - outbox = true - default: - relays = append(relays, relaysTag[i]) + data, _ := json.Marshal(SpellHistoryEntry{ + Identifier: identifier, + Name: name, + Content: spell.Content, + LastUsed: time.Now(), + Pointer: pointer, + }) + file.Write(data) + file.Write([]byte{'\n'}) + for i, entry := range history { + if entry.Identifier == identifier { + continue } - } - stream := !spell.Tags.Has("close-on-eose") - - // fill in the author if we didn't have it - pointer.Author = spell.PubKey - - // add to history before execution - { - idStr := nip19.EncodeNevent(spell.ID, nil, nostr.ZeroPK) - identifier = "spell" + idStr[len(idStr)-7:] - nameTag := spell.Tags.Find("name") - var name string - if nameTag != nil { - name = nameTag[1] - } - if len(history) > 100 { - history = history[:100] - } - // write back to file - file, err := os.Create(historyPath) - if err != nil { - return err - } - data, _ := json.Marshal(SpellHistoryEntry{ - Identifier: identifier, - Name: name, - Content: spell.Content, - LastUsed: time.Now(), - Pointer: pointer, - }) + data, _ := json.Marshal(entry) file.Write(data) file.Write([]byte{'\n'}) - for i, entry := range history { - // limit history size (keep last 100) - if i == 100 { - break - } - data, _ := json.Marshal(entry) - file.Write(data) - file.Write([]byte{'\n'}) + // limit history size (keep last 100) + if i == 100 { + break } - file.Close() - - logverbose("executing %s: %s relays=%v outbox=%v stream=%v\n", - identifier, spellFilter, spellRelays, outbox, stream) } + file.Close() - // execute - logSpellDetails(spell.Event) - performReq(ctx, spellFilter, spellRelays, stream, outbox, c.Uint("outbox-relays-per-pubkey"), false, 0, "nak-spell") + logverbose("executing %s: %s relays=%v outbox=%v stream=%v\n", + identifier, spellFilter, spellRelays, outbox, stream) + } - return nil - }, + // execute + logSpellDetails(spell) + performReq(ctx, spellFilter, spellRelays, stream, outbox, c.Uint("outbox-relays-per-pubkey"), false, 0, "nak-spell") + + return nil } func buildSpellReq(ctx context.Context, c *cli.Command, tags nostr.Tags) (nostr.Filter, error) { From 5d4fe434c37b799f98be9c43669e4f0fa034bbbc Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Mon, 22 Dec 2025 12:11:27 -0300 Subject: [PATCH 380/401] spell: also take a --pub to run spells in the context of other users. --- spell.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/spell.go b/spell.go index 6804031..e6d3252 100644 --- a/spell.go +++ b/spell.go @@ -24,6 +24,10 @@ var spell = &cli.Command{ ArgsUsage: "[nevent_code]", Description: `fetches a spell event (kind 777) and executes REQ command encoded in its tags.`, Flags: append(defaultKeyFlags, + &cli.StringFlag{ + Name: "pub", + Usage: "public key to run spells in the context of (if you don't want to pass a --sec)", + }, &cli.UintFlag{ Name: "outbox-relays-per-pubkey", Aliases: []string{"n"}, @@ -259,6 +263,10 @@ func buildSpellReq(ctx context.Context, c *cli.Command, tags nostr.Tags) (nostr. filter := nostr.Filter{} getMe := func() (nostr.PubKey, error) { + if !c.IsSet("sec") && !c.IsSet("prompt-sec") && c.IsSet("pub") { + return parsePubKey(c.String("pub")) + } + kr, _, err := gatherKeyerFromArguments(ctx, c) if err != nil { return nostr.ZeroPK, fmt.Errorf("failed to get keyer: %w", err) From 5b64795015c7f320c5a9839f9f8e54f7a1ee9e23 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Mon, 22 Dec 2025 12:24:03 -0300 Subject: [PATCH 381/401] git: fix --tags --- git.go | 55 +++++++++++++++++++++++++++++-------------------------- 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/git.go b/git.go index 36ccf68..081ef36 100644 --- a/git.go +++ b/git.go @@ -532,34 +532,37 @@ aside from those, there is also: log("- setting HEAD to branch %s\n", color.CyanString(remoteBranch)) } - // add all refs/tags - output, err := exec.Command("git", "show-ref", "--tags").Output() - if err != nil { - return fmt.Errorf("failed to get local tags: %s", err) - } else { - lines := strings.Split(strings.TrimSpace(string(output)), "\n") - for _, line := range lines { - line = strings.TrimSpace(line) - if line == "" { - continue - } - parts := strings.Fields(line) - if len(parts) != 2 { - continue - } - commitHash := parts[0] - ref := parts[1] - - tagName := strings.TrimPrefix(ref, "refs/tags/") - - if !c.Bool("force") { - // if --force is not passed then we can't overwrite tags - if existingHash, exists := state.Tags[tagName]; exists && existingHash != commitHash { - return fmt.Errorf("tag %s that is already published pointing to %s, call with --force to overwrite", tagName, existingHash) + if c.Bool("tags") { + // add all refs/tags + output, err := exec.Command("git", "show-ref", "--tags").Output() + if err != nil && err.Error() != "exit status 1" { + // exit status 1 is returned when there are no tags, which should be ok for us + return fmt.Errorf("failed to get local tags: %s", err) + } else { + lines := strings.Split(strings.TrimSpace(string(output)), "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" { + continue } + parts := strings.Fields(line) + if len(parts) != 2 { + continue + } + commitHash := parts[0] + ref := parts[1] + + tagName := strings.TrimPrefix(ref, "refs/tags/") + + if !c.Bool("force") { + // if --force is not passed then we can't overwrite tags + if existingHash, exists := state.Tags[tagName]; exists && existingHash != commitHash { + return fmt.Errorf("tag %s that is already published pointing to %s, call with --force to overwrite", tagName, existingHash) + } + } + state.Tags[tagName] = commitHash + log("- setting tag %s to commit %s\n", color.CyanString(tagName), color.CyanString(commitHash)) } - state.Tags[tagName] = commitHash - log("- setting tag %s to commit %s\n", color.CyanString(tagName), color.CyanString(commitHash)) } } From 2e4079f92c6684f3fcea54a56e1aef9d42b08d93 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Mon, 22 Dec 2025 12:25:32 -0300 Subject: [PATCH 382/401] freeze nostrlib version. --- go.mod | 4 +--- go.sum | 2 ++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 530648a..ff3854c 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.25 require ( fiatjaf.com/lib v0.3.1 - fiatjaf.com/nostr v0.0.0-20251204122254-07061404918d + fiatjaf.com/nostr v0.0.0-20251222025842-099569ea4feb github.com/AlecAivazis/survey/v2 v2.3.7 github.com/bep/debounce v1.2.1 github.com/btcsuite/btcd/btcec/v2 v2.3.6 @@ -104,5 +104,3 @@ require ( gopkg.in/yaml.v3 v3.0.1 // indirect rsc.io/qr v0.2.0 // indirect ) - -replace fiatjaf.com/nostr => ../nostrlib diff --git a/go.sum b/go.sum index df40a3d..dde7130 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ fiatjaf.com/lib v0.3.1 h1:/oFQwNtFRfV+ukmOCxfBEAuayoLwXp4wu2/fz5iHpwA= fiatjaf.com/lib v0.3.1/go.mod h1:Ycqq3+mJ9jAWu7XjbQI1cVr+OFgnHn79dQR5oTII47g= +fiatjaf.com/nostr v0.0.0-20251222025842-099569ea4feb h1:GuqPn1g0JRD/dGxFRxEwEFxvbcT3vyvMjP3OoeLIIh0= +fiatjaf.com/nostr v0.0.0-20251222025842-099569ea4feb/go.mod h1:ue7yw0zHfZj23Ml2kVSdBx0ENEaZiuvGxs/8VEN93FU= github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ= github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= github.com/FastFilter/xorfilter v0.2.1 h1:lbdeLG9BdpquK64ZsleBS8B4xO/QW1IM0gMzF7KaBKc= From 965a312b460f5e0b9a1a6e26b022024c86ee1142 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Mon, 22 Dec 2025 12:42:54 -0300 Subject: [PATCH 383/401] only build with lmdb on linux. --- lmdb.go | 43 +++++++++++++++++++++++++++++++++++++++++++ main.go | 30 +----------------------------- non_lmdb.go | 11 +++++++++++ 3 files changed, 55 insertions(+), 29 deletions(-) create mode 100644 lmdb.go create mode 100644 non_lmdb.go diff --git a/lmdb.go b/lmdb.go new file mode 100644 index 0000000..40f4906 --- /dev/null +++ b/lmdb.go @@ -0,0 +1,43 @@ +//go:build linux && !riscv64 && !arm64 + +package main + +import ( + "os" + "path/filepath" + + "fiatjaf.com/nostr/eventstore/lmdb" + "fiatjaf.com/nostr/eventstore/nullstore" + "fiatjaf.com/nostr/sdk" + "fiatjaf.com/nostr/sdk/hints/lmdbh" + lmdbkv "fiatjaf.com/nostr/sdk/kvstore/lmdb" + "github.com/urfave/cli/v3" +) + +func setupLocalDatabases(c *cli.Command, sys *sdk.System) { + configPath := c.String("config-path") + if configPath != "" { + hintsPath := filepath.Join(configPath, "outbox/hints") + os.MkdirAll(hintsPath, 0755) + _, err := lmdbh.NewLMDBHints(hintsPath) + if err != nil { + log("failed to create lmdb hints db at '%s': %s\n", hintsPath, err) + } + + eventsPath := filepath.Join(configPath, "events") + os.MkdirAll(eventsPath, 0755) + sys.Store = &lmdb.LMDBBackend{Path: eventsPath} + if err := sys.Store.Init(); err != nil { + log("failed to create boltdb events db at '%s': %s\n", eventsPath, err) + sys.Store = &nullstore.NullStore{} + } + + kvPath := filepath.Join(configPath, "kvstore") + os.MkdirAll(kvPath, 0755) + if kv, err := lmdbkv.NewStore(kvPath); err != nil { + log("failed to create boltdb kvstore db at '%s': %s\n", kvPath, err) + } else { + sys.KVStore = kv + } + } +} diff --git a/main.go b/main.go index b20a338..c990217 100644 --- a/main.go +++ b/main.go @@ -8,11 +8,7 @@ import ( "path/filepath" "fiatjaf.com/nostr" - "fiatjaf.com/nostr/eventstore/lmdb" - "fiatjaf.com/nostr/eventstore/nullstore" "fiatjaf.com/nostr/sdk" - "fiatjaf.com/nostr/sdk/hints/lmdbh" - lmdbkv "fiatjaf.com/nostr/sdk/kvstore/lmdb" "github.com/fatih/color" "github.com/urfave/cli/v3" ) @@ -103,31 +99,7 @@ var app = &cli.Command{ Before: func(ctx context.Context, c *cli.Command) (context.Context, error) { sys = sdk.NewSystem() - configPath := c.String("config-path") - if configPath != "" { - hintsPath := filepath.Join(configPath, "outbox/hints") - os.MkdirAll(hintsPath, 0755) - _, err := lmdbh.NewLMDBHints(hintsPath) - if err != nil { - log("failed to create lmdb hints db at '%s': %s\n", hintsPath, err) - } - - eventsPath := filepath.Join(configPath, "events") - os.MkdirAll(eventsPath, 0755) - sys.Store = &lmdb.LMDBBackend{Path: eventsPath} - if err := sys.Store.Init(); err != nil { - log("failed to create boltdb events db at '%s': %s\n", eventsPath, err) - sys.Store = &nullstore.NullStore{} - } - - kvPath := filepath.Join(configPath, "kvstore") - os.MkdirAll(kvPath, 0755) - if kv, err := lmdbkv.NewStore(kvPath); err != nil { - log("failed to create boltdb kvstore db at '%s': %s\n", kvPath, err) - } else { - sys.KVStore = kv - } - } + setupLocalDatabases(c, sys) sys.Pool = nostr.NewPool(nostr.PoolOptions{ AuthorKindQueryMiddleware: sys.TrackQueryAttempts, diff --git a/non_lmdb.go b/non_lmdb.go new file mode 100644 index 0000000..82423f9 --- /dev/null +++ b/non_lmdb.go @@ -0,0 +1,11 @@ +//go:build !linux || riscv64 || arm64 + +package main + +import ( + "fiatjaf.com/nostr/sdk" + "github.com/urfave/cli/v3" +) + +func setupLocalDatabases(c *cli.Command, sys *sdk.System) { +} From e9c4deaf6d9a6d01a938303ba7d35e2c20e6ffa7 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 23 Dec 2025 21:31:54 -0300 Subject: [PATCH 384/401] nak req --spell for creating spells. --- req.go | 30 ++++++++++++++++++++--- spell.go | 74 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+), 3 deletions(-) diff --git a/req.go b/req.go index 3ca109c..b43efb3 100644 --- a/req.go +++ b/req.go @@ -92,6 +92,10 @@ example: Usage: "after connecting, for a nip42 \"AUTH\" message to be received, act on it and only then send the \"REQ\"", Category: CATEGORY_SIGNER, }, + &cli.BoolFlag{ + Name: "spell", + Usage: "output a spell event (kind 777) instead of a filter", + }, )..., ), ArgsUsage: "[relay...]", @@ -111,7 +115,16 @@ example: return fmt.Errorf("incompatible flags --paginate and --outbox") } + if c.Bool("bare") && c.Bool("spell") { + return fmt.Errorf("incompatible flags --bare and --spell") + } + relayUrls := c.Args().Slice() + + if len(relayUrls) > 0 && (c.Bool("bare") || c.Bool("spell")) { + return fmt.Errorf("relay URLs are incompatible with --bare or --spell") + } + if len(relayUrls) > 0 && !negentropy { // this is used both for the normal AUTH (after "auth-required:" is received) or forced pre-auth // connect to all relays we expect to use in this call in parallel @@ -225,15 +238,26 @@ example: performReq(ctx, filter, relayUrls, c.Bool("stream"), c.Bool("outbox"), c.Uint("outbox-relays-per-pubkey"), c.Bool("paginate"), c.Duration("paginate-interval"), "nak-req") } } else { - // no relays given, will just print the filter + // no relays given, will just print the filter or spell var result string - if c.Bool("bare") { + if c.Bool("spell") { + // output a spell event instead of a filter + kr, _, err := gatherKeyerFromArguments(ctx, c) + if err != nil { + return err + } + spellEvent := createSpellEvent(ctx, filter, kr) + j, _ := json.Marshal(spellEvent) + result = string(j) + } else if c.Bool("bare") { + // bare filter output result = filter.String() } else { + // normal filter j, _ := json.Marshal(nostr.ReqEnvelope{SubscriptionID: "nak", Filters: []nostr.Filter{filter}}) result = string(j) - } + } stdout(result) } } diff --git a/spell.go b/spell.go index e6d3252..146a62b 100644 --- a/spell.go +++ b/spell.go @@ -471,3 +471,77 @@ func logSpellDetails(spell nostr.Event) { desc, ) } + +func createSpellEvent(ctx context.Context, filter nostr.Filter, kr nostr.Keyer) nostr.Event { + spell := nostr.Event{ + Kind: 777, + Tags: make(nostr.Tags, 0), + } + + // add cmd tag + spell.Tags = append(spell.Tags, nostr.Tag{"cmd", "REQ"}) + + // add kinds + if len(filter.Kinds) > 0 { + kindTag := nostr.Tag{"k"} + for _, kind := range filter.Kinds { + kindTag = append(kindTag, strconv.Itoa(int(kind))) + } + spell.Tags = append(spell.Tags, kindTag) + } + + // add authors + if len(filter.Authors) > 0 { + authorsTag := nostr.Tag{"authors"} + for _, author := range filter.Authors { + authorsTag = append(authorsTag, author.Hex()) + } + spell.Tags = append(spell.Tags, authorsTag) + } + + // add ids + if len(filter.IDs) > 0 { + idsTag := nostr.Tag{"ids"} + for _, id := range filter.IDs { + idsTag = append(idsTag, id.Hex()) + } + spell.Tags = append(spell.Tags, idsTag) + } + + // add tags + for tagName, values := range filter.Tags { + if len(values) > 0 { + tag := nostr.Tag{"tag", tagName} + for _, value := range values { + tag = append(tag, value) + } + spell.Tags = append(spell.Tags, tag) + } + } + + // add limit + if filter.Limit > 0 { + spell.Tags = append(spell.Tags, nostr.Tag{"limit", strconv.Itoa(filter.Limit)}) + } + + // add since + if filter.Since > 0 { + spell.Tags = append(spell.Tags, nostr.Tag{"since", strconv.FormatInt(int64(filter.Since), 10)}) + } + + // add until + if filter.Until > 0 { + spell.Tags = append(spell.Tags, nostr.Tag{"until", strconv.FormatInt(int64(filter.Until), 10)}) + } + + // add search + if filter.Search != "" { + spell.Tags = append(spell.Tags, nostr.Tag{"search", filter.Search}) + } + + if err := kr.SignEvent(ctx, &spell); err != nil { + log("failed to sign spell: %s\n", err) + } + + return spell +} From 6d878878555340d9efeed13ace716ba5b85cd223 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 23 Dec 2025 21:32:54 -0300 Subject: [PATCH 385/401] spell: execute from history using the name, not only the id. --- spell.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spell.go b/spell.go index 146a62b..a65e7e6 100644 --- a/spell.go +++ b/spell.go @@ -120,7 +120,7 @@ var spell = &cli.Command{ } else { // search our history for _, entry := range history { - if entry.Identifier == identifier { + if entry.Identifier == identifier || entry.Name == identifier { pointer = entry.Pointer break } From 9b684f2c654b09c8f194f8b6aa025679ee732d9f Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 23 Dec 2025 23:50:11 -0300 Subject: [PATCH 386/401] dekey: make it better and fix things, --rotate and other flags, prompts by default etc. --- dekey.go | 201 +++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 144 insertions(+), 57 deletions(-) diff --git a/dekey.go b/dekey.go index 25746bc..7bb8ad0 100644 --- a/dekey.go +++ b/dekey.go @@ -8,7 +8,9 @@ import ( "slices" "fiatjaf.com/nostr" + "fiatjaf.com/nostr/nip19" "fiatjaf.com/nostr/nip44" + "github.com/AlecAivazis/survey/v2" "github.com/fatih/color" "github.com/urfave/cli/v3" ) @@ -20,7 +22,7 @@ var dekey = &cli.Command{ DisableSliceFlagSeparator: true, Flags: append(defaultKeyFlags, &cli.StringFlag{ - Name: "device-name", + Name: "device", Usage: "name of this device that will be published and displayed on other clients", Value: func() string { if hostname, err := os.Hostname(); err == nil { @@ -29,24 +31,38 @@ var dekey = &cli.Command{ return "nak@unknown" }(), }, + &cli.BoolFlag{ + Name: "rotate", + Usage: "force the creation of a new encryption key, effectively invalidating any previous ones", + }, + &cli.BoolFlag{ + Name: "authorize-all", + Aliases: []string{"yolo"}, + Usage: "do not ask for confirmation, just automatically send the encryption key to all devices that exist", + }, + &cli.BoolFlag{ + Name: "reject-all", + Usage: "do not ask for confirmation, just not send the encryption key to any device", + }, ), Action: func(ctx context.Context, c *cli.Command) error { - log(color.CyanString("gathering keyer from arguments...\n")) kr, _, err := gatherKeyerFromArguments(ctx, c) if err != nil { return err } - log(color.CyanString("getting user public key...\n")) userPub, err := kr.GetPublicKey(ctx) if err != nil { return fmt.Errorf("failed to get user public key: %w", err) } configPath := c.String("config-path") - deviceName := c.String("device-name") + deviceName := c.String("device") - log(color.YellowString("handling device key for %s...\n"), deviceName) + log("handling device key for %s as %s\n", + color.YellowString(deviceName), + color.CyanString(nip19.EncodeNpub(userPub)), + ) // check if we already have a local-device secret key deviceKeyPath := filepath.Join(configPath, "dekey", "device-key") var deviceSec nostr.SecretKey @@ -57,7 +73,7 @@ var dekey = &cli.Command{ return fmt.Errorf("invalid device key in %s: %w", deviceKeyPath, err) } } else { - log(color.YellowString("generating new device key...\n")) + log(color.YellowString("generating new device key\n")) // create one deviceSec = nostr.Generate() os.MkdirAll(filepath.Dir(deviceKeyPath), 0700) @@ -69,26 +85,29 @@ var dekey = &cli.Command{ devicePub := deviceSec.Public() // get relays for the user - log(color.CyanString("fetching write relays for user...\n")) + log("fetching write relays for %s\n", color.CyanString(nip19.EncodeNpub(userPub))) relays := sys.FetchWriteRelays(ctx, userPub) - log(color.CyanString("connecting to %d relays...\n"), len(relays)) relayList := connectToAllRelays(ctx, c, relays, nil, nostr.PoolOptions{}) if len(relayList) == 0 { return fmt.Errorf("no relays to use") } - log(color.GreenString("connected to %d relays\n"), len(relayList)) // check if kind:4454 is already published - log(color.CyanString("checking for existing device registration (kind:4454)...\n")) - events := sys.Pool.FetchMany(ctx, relays, nostr.Filter{ + log("- checking for existing device registration (kind:4454)\n") + events := make([]nostr.Event, 0, 1) + for evt := range sys.Pool.FetchMany(ctx, relays, nostr.Filter{ Kinds: []nostr.Kind{4454}, Authors: []nostr.PubKey{userPub}, Tags: nostr.TagMap{ - "pubkey": []string{devicePub.Hex()}, + "P": []string{devicePub.Hex()}, }, - }, nostr.SubscriptionOptions{Label: "nak-nip4e"}) + Limit: 1, + }, nostr.SubscriptionOptions{Label: "nak-nip4e"}) { + events = append(events, evt.Event) + } + if len(events) == 0 { - log(color.YellowString("no device registration found, publishing kind:4454...\n")) + log(". no device registration found, publishing kind:4454 for %s\n", color.YellowString(deviceName)) // publish kind:4454 evt := nostr.Event{ Kind: 4454, @@ -96,7 +115,7 @@ var dekey = &cli.Command{ CreatedAt: nostr.Now(), Tags: nostr.Tags{ {"client", deviceName}, - {"pubkey", devicePub.Hex()}, + {"P", devicePub.Hex()}, }, } @@ -109,13 +128,13 @@ var dekey = &cli.Command{ if err := publishFlow(ctx, c, kr, evt, relayList); err != nil { return err } - log(color.GreenString("device registration published\n")) + log(color.GreenString(". device registration published\n")) } else { - log(color.GreenString("device already registered\n")) + log(color.GreenString(". device already registered\n")) } // check for kind:10044 - log(color.CyanString("checking for user encryption key (kind:10044)...\n")) + log("- checking for user encryption key (kind:10044)\n") userKeyEventDate := nostr.Now() userKeyResult := sys.Pool.FetchManyReplaceable(ctx, relays, nostr.Filter{ Kinds: []nostr.Kind{10044}, @@ -123,22 +142,46 @@ var dekey = &cli.Command{ }, nostr.SubscriptionOptions{Label: "nak-nip4e"}) var eSec nostr.SecretKey var ePub nostr.PubKey - if userKeyEvent, ok := userKeyResult.Load(nostr.ReplaceableKey{PubKey: userPub, D: ""}); !ok { - log(color.YellowString("no user encryption key found, generating new one...\n")) + + var generateNewEncryptionKey bool + userKeyEvent, ok := userKeyResult.Load(nostr.ReplaceableKey{PubKey: userPub, D: ""}) + if !ok { + log("- no user encryption key found, generating new one\n") + generateNewEncryptionKey = true + } else { + // get the pub from the tag + for _, tag := range userKeyEvent.Tags { + if len(tag) >= 2 && tag[0] == "n" { + ePub, _ = nostr.PubKeyFromHex(tag[1]) + break + } + } + if ePub == nostr.ZeroPK { + return fmt.Errorf("got invalid kind:10044 event, no 'n' tag") + } + + log(". an encryption public key already exists: %s\n", color.CyanString(ePub.Hex())) + if c.Bool("rotate") { + log(color.GreenString("rotating it by generating a new one\n")) + generateNewEncryptionKey = true + } + } + + if generateNewEncryptionKey { // generate main secret key eSec = nostr.Generate() ePub := eSec.Public() // store it - eKeyPath := filepath.Join(configPath, "dekey", "e", ePub.Hex()) + eKeyPath := filepath.Join(configPath, "dekey", "p", userPub.Hex(), "e", ePub.Hex()) os.MkdirAll(filepath.Dir(eKeyPath), 0700) if err := os.WriteFile(eKeyPath, []byte(eSec.Hex()), 0600); err != nil { return fmt.Errorf("failed to write user encryption key: %w", err) } - log(color.GreenString("user encryption key generated and stored\n")) + log("user encryption key generated and stored, public key: %s\n", color.CyanString(ePub.Hex())) // publish kind:10044 - log(color.YellowString("publishing user encryption key (kind:10044)...\n")) + log("publishing user encryption public key (kind:10044)\n") evt10044 := nostr.Event{ Kind: 10044, Content: "", @@ -150,30 +193,16 @@ var dekey = &cli.Command{ if err := kr.SignEvent(ctx, &evt10044); err != nil { return fmt.Errorf("failed to sign kind:10044: %w", err) } - if err := publishFlow(ctx, c, kr, evt10044, relayList); err != nil { return err } - log(color.GreenString("user encryption key published\n")) } else { - log(color.GreenString("found existing user encryption key\n")) userKeyEventDate = userKeyEvent.CreatedAt - // get the pub from the tag - for _, tag := range userKeyEvent.Tags { - if len(tag) >= 2 && tag[0] == "n" { - ePub, _ = nostr.PubKeyFromHex(tag[1]) - break - } - } - if ePub == nostr.ZeroPK { - return fmt.Errorf("invalid kind:10044 event, no 'n' tag") - } - // check if we have the key - eKeyPath := filepath.Join(configPath, "dekey", "e", ePub.Hex()) + eKeyPath := filepath.Join(configPath, "dekey", "p", userPub.Hex(), "e", ePub.Hex()) if data, err := os.ReadFile(eKeyPath); err == nil { - log(color.GreenString("found stored user encryption key\n")) + log(color.GreenString("- and we have it locally already\n")) eSec, err = nostr.SecretKeyFromHex(string(data)) if err != nil { return fmt.Errorf("invalid main key: %w", err) @@ -182,13 +211,14 @@ var dekey = &cli.Command{ return fmt.Errorf("stored user encryption key is corrupted: %w", err) } } else { - log(color.YellowString("user encryption key not stored locally, attempting to decrypt from other devices...\n")) + log("- encryption key not stored locally, attempting to fetch the key from other devices\n") // try to decrypt from kind:4455 for eKeyMsg := range sys.Pool.FetchMany(ctx, relays, nostr.Filter{ Kinds: []nostr.Kind{4455}, Tags: nostr.TagMap{ "p": []string{devicePub.Hex()}, }, + Since: userKeyEventDate, }, nostr.SubscriptionOptions{Label: "nak-nip4e"}) { var senderPub nostr.PubKey for _, tag := range eKeyMsg.Tags { @@ -214,7 +244,7 @@ var dekey = &cli.Command{ } // check if it matches mainPub if eSec.Public() == ePub { - log(color.GreenString("successfully decrypted user encryption key from another device\n")) + log(color.GreenString("successfully decrypted encryption key from another device\n")) // store it os.MkdirAll(filepath.Dir(eKeyPath), 0700) os.WriteFile(eKeyPath, []byte(eSecHex), 0600) @@ -225,61 +255,118 @@ var dekey = &cli.Command{ } if eSec == [32]byte{} { - log(color.RedString("main secret key not available, must authorize on another device\n")) + log("encryption secret key not available, must be sent from another device to %s first\n", + color.YellowString(deviceName)) return nil } - log(color.GreenString("user encryption key ready\n")) + log(color.GreenString("- encryption key ready\n")) // now we have mainSec, check for other kind:4454 events newer than the 10044 - log(color.CyanString("checking for other devices and key messages...\n")) + log("- checking for other devices and key messages so we can send the key\n") keyMsgs := make([]string, 0, 5) for keyOrDeviceEvt := range sys.Pool.FetchMany(ctx, relays, nostr.Filter{ Kinds: []nostr.Kind{4454, 4455}, Authors: []nostr.PubKey{userPub}, Since: userKeyEventDate, }, nostr.SubscriptionOptions{Label: "nak-nip4e"}) { - if keyOrDeviceEvt.Kind == 4455 { - // key event - log(color.BlueString("received key message (kind:4455)\n")) + fmt.Println("~~~", keyOrDeviceEvt.Kind, keyOrDeviceEvt) - // skip ourselves - if keyOrDeviceEvt.Tags.FindWithValue("p", devicePub.Hex()) != nil { - continue - } + if keyOrDeviceEvt.Kind == 4455 { + // got key event + keyEvent := keyOrDeviceEvt // assume a key msg will always come before its associated devicemsg // so just store them here: - pubkeyTag := keyOrDeviceEvt.Tags.Find("p") + pubkeyTag := keyEvent.Tags.Find("p") if pubkeyTag == nil { continue } keyMsgs = append(keyMsgs, pubkeyTag[1]) } else if keyOrDeviceEvt.Kind == 4454 { // device event - log(color.BlueString("received device registration (kind:4454)\n")) + deviceEvt := keyOrDeviceEvt // skip ourselves - if keyOrDeviceEvt.Tags.FindWithValue("pubkey", devicePub.Hex()) != nil { + if deviceEvt.Tags.FindWithValue("P", devicePub.Hex()) != nil { + continue + } + + // if there is a clock skew (current time is earlier than the time of this device's announcement) skip it + if nostr.Now() < deviceEvt.CreatedAt { continue } // if this already has a corresponding keyMsg then skip it - pubkeyTag := keyOrDeviceEvt.Tags.Find("pubkey") + pubkeyTag := deviceEvt.Tags.Find("P") if pubkeyTag == nil { continue } + + fmt.Println("KEYMSGS", keyMsgs) + fmt.Println(">>", pubkeyTag[1]) + fmt.Println(">>", deviceEvt.Tags.Find("p")) if slices.Contains(keyMsgs, pubkeyTag[1]) { continue } + deviceTag := deviceEvt.Tags.Find("client") + if deviceTag == nil { + continue + } + // here we know we're dealing with a deviceMsg without a corresponding keyMsg // so we have to build a keyMsg for them - log(color.YellowString("sending encryption key to new device...\n")) theirDevice, err := nostr.PubKeyFromHex(pubkeyTag[1]) if err != nil { continue } + log("- sending encryption key to new device %s\n", color.YellowString(deviceTag[1])) + if c.Bool("authorize-all") { + // will proceed + } else if c.Bool("reject-all") { + continue + } else { + var proceed bool + if err := survey.AskOne(&survey.Confirm{ + Message: "authorize?", + }, &proceed); err != nil { + return err + } + if proceed { + // will proceed + } else { + // won't proceed + var deleteDevice bool + if err := survey.AskOne(&survey.Confirm{ + Message: " delete this device announcement?", + }, &deleteDevice); err != nil { + return err + } + + if deleteDevice { + log(" - deleting %s\n", color.YellowString(deviceTag[1])) + deletion := nostr.Event{ + CreatedAt: nostr.Now(), + Kind: 5, + Tags: nostr.Tags{ + {"e", deviceEvt.ID.Hex()}, + }, + } + if err := kr.SignEvent(ctx, &deletion); err != nil { + return fmt.Errorf("failed to sign deletion '%s': %w", deletion.GetID().Hex(), err) + } + if err := publishFlow(ctx, c, kr, deletion, relayList); err != nil { + return fmt.Errorf("publish flow failed: %w", err) + } + } else { + log(" - skipped\n") + } + + continue + } + } + ss, err := nip44.GenerateConversationKey(theirDevice, deviceSec) if err != nil { continue @@ -305,7 +392,7 @@ var dekey = &cli.Command{ if err := publishFlow(ctx, c, kr, evt4455, relayList); err != nil { log(color.RedString("failed to publish key message: %v\n"), err) } else { - log(color.GreenString("encryption key sent to device\n")) + log(" - encryption key sent to %s\n", color.GreenString(deviceTag[1])) } } } From a19a179548e194c4eddd025fd636d8c8bf1e3523 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Wed, 24 Dec 2025 11:38:13 -0300 Subject: [PATCH 387/401] dekey: don't publish device announcements when not necessary, delete them when they become unnecessary. --- dekey.go | 155 +++++++++++++++++++++++++++++++++---------------------- 1 file changed, 94 insertions(+), 61 deletions(-) diff --git a/dekey.go b/dekey.go index 7bb8ad0..f7be06f 100644 --- a/dekey.go +++ b/dekey.go @@ -92,51 +92,9 @@ var dekey = &cli.Command{ return fmt.Errorf("no relays to use") } - // check if kind:4454 is already published - log("- checking for existing device registration (kind:4454)\n") - events := make([]nostr.Event, 0, 1) - for evt := range sys.Pool.FetchMany(ctx, relays, nostr.Filter{ - Kinds: []nostr.Kind{4454}, - Authors: []nostr.PubKey{userPub}, - Tags: nostr.TagMap{ - "P": []string{devicePub.Hex()}, - }, - Limit: 1, - }, nostr.SubscriptionOptions{Label: "nak-nip4e"}) { - events = append(events, evt.Event) - } - - if len(events) == 0 { - log(". no device registration found, publishing kind:4454 for %s\n", color.YellowString(deviceName)) - // publish kind:4454 - evt := nostr.Event{ - Kind: 4454, - Content: "", - CreatedAt: nostr.Now(), - Tags: nostr.Tags{ - {"client", deviceName}, - {"P", devicePub.Hex()}, - }, - } - - // sign with main key - if err := kr.SignEvent(ctx, &evt); err != nil { - return fmt.Errorf("failed to sign device event: %w", err) - } - - // publish - if err := publishFlow(ctx, c, kr, evt, relayList); err != nil { - return err - } - log(color.GreenString(". device registration published\n")) - } else { - log(color.GreenString(". device already registered\n")) - } - // check for kind:10044 log("- checking for user encryption key (kind:10044)\n") - userKeyEventDate := nostr.Now() - userKeyResult := sys.Pool.FetchManyReplaceable(ctx, relays, nostr.Filter{ + keyAnnouncementResult := sys.Pool.FetchManyReplaceable(ctx, relays, nostr.Filter{ Kinds: []nostr.Kind{10044}, Authors: []nostr.PubKey{userPub}, }, nostr.SubscriptionOptions{Label: "nak-nip4e"}) @@ -144,13 +102,13 @@ var dekey = &cli.Command{ var ePub nostr.PubKey var generateNewEncryptionKey bool - userKeyEvent, ok := userKeyResult.Load(nostr.ReplaceableKey{PubKey: userPub, D: ""}) + keyAnnouncementEvent, ok := keyAnnouncementResult.Load(nostr.ReplaceableKey{PubKey: userPub, D: ""}) if !ok { log("- no user encryption key found, generating new one\n") generateNewEncryptionKey = true } else { // get the pub from the tag - for _, tag := range userKeyEvent.Tags { + for _, tag := range keyAnnouncementEvent.Tags { if len(tag) >= 2 && tag[0] == "n" { ePub, _ = nostr.PubKeyFromHex(tag[1]) break @@ -170,7 +128,7 @@ var dekey = &cli.Command{ if generateNewEncryptionKey { // generate main secret key eSec = nostr.Generate() - ePub := eSec.Public() + ePub = eSec.Public() // store it eKeyPath := filepath.Join(configPath, "dekey", "p", userPub.Hex(), "e", ePub.Hex()) @@ -185,7 +143,7 @@ var dekey = &cli.Command{ evt10044 := nostr.Event{ Kind: 10044, Content: "", - CreatedAt: userKeyEventDate, + CreatedAt: nostr.Now(), Tags: nostr.Tags{ {"n", ePub.Hex()}, }, @@ -197,8 +155,6 @@ var dekey = &cli.Command{ return err } } else { - userKeyEventDate = userKeyEvent.CreatedAt - // check if we have the key eKeyPath := filepath.Join(configPath, "dekey", "p", userPub.Hex(), "e", ePub.Hex()) if data, err := os.ReadFile(eKeyPath); err == nil { @@ -211,14 +167,56 @@ var dekey = &cli.Command{ return fmt.Errorf("stored user encryption key is corrupted: %w", err) } } else { - log("- encryption key not stored locally, attempting to fetch the key from other devices\n") - // try to decrypt from kind:4455 + log("- encryption key not found locally, attempting to fetch the key from other devices\n") + + // check if our kind:4454 is already published + log("- checking for existing device announcement (kind:4454)\n") + ourDeviceAnnouncementEvents := make([]nostr.Event, 0, 1) + for evt := range sys.Pool.FetchMany(ctx, relays, nostr.Filter{ + Kinds: []nostr.Kind{4454}, + Authors: []nostr.PubKey{userPub}, + Tags: nostr.TagMap{ + "P": []string{devicePub.Hex()}, + }, + Limit: 1, + }, nostr.SubscriptionOptions{Label: "nak-nip4e"}) { + ourDeviceAnnouncementEvents = append(ourDeviceAnnouncementEvents, evt.Event) + } + if len(ourDeviceAnnouncementEvents) == 0 { + log(". no device announcement found, publishing kind:4454 for %s\n", color.YellowString(deviceName)) + // publish kind:4454 + evt := nostr.Event{ + Kind: 4454, + Content: "", + CreatedAt: nostr.Now(), + Tags: nostr.Tags{ + {"client", deviceName}, + {"P", devicePub.Hex()}, + }, + } + + // sign with main key + if err := kr.SignEvent(ctx, &evt); err != nil { + return fmt.Errorf("failed to sign device event: %w", err) + } + + // publish + if err := publishFlow(ctx, c, kr, evt, relayList); err != nil { + return err + } + log(color.GreenString(". device announcement published\n")) + ourDeviceAnnouncementEvents = append(ourDeviceAnnouncementEvents, evt) + } else { + log(color.GreenString(". device already registered\n")) + } + + // see if some other device has shared the key with us from kind:4455 for eKeyMsg := range sys.Pool.FetchMany(ctx, relays, nostr.Filter{ Kinds: []nostr.Kind{4455}, Tags: nostr.TagMap{ "p": []string{devicePub.Hex()}, }, - Since: userKeyEventDate, + Since: keyAnnouncementEvent.CreatedAt + 1, }, nostr.SubscriptionOptions{Label: "nak-nip4e"}) { var senderPub nostr.PubKey for _, tag := range eKeyMsg.Tags { @@ -248,6 +246,43 @@ var dekey = &cli.Command{ // store it os.MkdirAll(filepath.Dir(eKeyPath), 0700) os.WriteFile(eKeyPath, []byte(eSecHex), 0600) + + // delete our 4454 if we had one, since we received the key + if len(ourDeviceAnnouncementEvents) > 0 { + log("deleting our device announcement (kind:4454) since we received the encryption key\n") + deletion4454 := nostr.Event{ + CreatedAt: nostr.Now(), + Kind: 5, + Tags: nostr.Tags{ + {"e", ourDeviceAnnouncementEvents[0].ID.Hex()}, + }, + } + if err := kr.SignEvent(ctx, &deletion4454); err != nil { + log(color.RedString("failed to sign 4454 deletion: %v\n"), err) + } else if err := publishFlow(ctx, c, kr, deletion4454, relayList); err != nil { + log(color.RedString("failed to publish 4454 deletion: %v\n"), err) + } else { + log(color.GreenString("- device announcement deleted\n")) + } + } + + // delete the 4455 we just decrypted + log("deleting the key message (kind:4455) we just decrypted\n") + deletion4455 := nostr.Event{ + CreatedAt: nostr.Now(), + Kind: 5, + Tags: nostr.Tags{ + {"e", eKeyMsg.ID.Hex()}, + }, + } + if err := kr.SignEvent(ctx, &deletion4455); err != nil { + log(color.RedString("failed to sign 4455 deletion: %v\n"), err) + } else if err := publishFlow(ctx, c, kr, deletion4455, relayList); err != nil { + log(color.RedString("failed to publish 4455 deletion: %v\n"), err) + } else { + log(color.GreenString("- key message deleted\n")) + } + break } } @@ -267,10 +302,8 @@ var dekey = &cli.Command{ for keyOrDeviceEvt := range sys.Pool.FetchMany(ctx, relays, nostr.Filter{ Kinds: []nostr.Kind{4454, 4455}, Authors: []nostr.PubKey{userPub}, - Since: userKeyEventDate, + Since: keyAnnouncementEvent.CreatedAt + 1, }, nostr.SubscriptionOptions{Label: "nak-nip4e"}) { - fmt.Println("~~~", keyOrDeviceEvt.Kind, keyOrDeviceEvt) - if keyOrDeviceEvt.Kind == 4455 { // got key event keyEvent := keyOrDeviceEvt @@ -302,9 +335,6 @@ var dekey = &cli.Command{ continue } - fmt.Println("KEYMSGS", keyMsgs) - fmt.Println(">>", pubkeyTag[1]) - fmt.Println(">>", deviceEvt.Tags.Find("p")) if slices.Contains(keyMsgs, pubkeyTag[1]) { continue } @@ -321,15 +351,16 @@ var dekey = &cli.Command{ continue } - log("- sending encryption key to new device %s\n", color.YellowString(deviceTag[1])) if c.Bool("authorize-all") { // will proceed } else if c.Bool("reject-all") { + log(" - skipping %s\n", color.YellowString(deviceTag[1])) continue } else { var proceed bool if err := survey.AskOne(&survey.Confirm{ - Message: "authorize?", + Message: fmt.Sprintf("share encryption key with %s"+colors.bold("?"), + color.YellowString(deviceTag[1])), }, &proceed); err != nil { return err } @@ -339,7 +370,7 @@ var dekey = &cli.Command{ // won't proceed var deleteDevice bool if err := survey.AskOne(&survey.Confirm{ - Message: " delete this device announcement?", + Message: fmt.Sprintf(" delete %s"+colors.bold("'s announcement?"), color.YellowString(deviceTag[1])), }, &deleteDevice); err != nil { return err } @@ -367,6 +398,7 @@ var dekey = &cli.Command{ } } + log("- sending encryption key to new device %s\n", color.YellowString(deviceTag[1])) ss, err := nip44.GenerateConversationKey(theirDevice, deviceSec) if err != nil { continue @@ -397,6 +429,7 @@ var dekey = &cli.Command{ } } + stdout(ePub.Hex()) return nil }, } From 32999917b43e0c9709dfaec7f9cb4251bad02f76 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sat, 27 Dec 2025 14:22:43 -0300 Subject: [PATCH 388/401] actually run the smoke test. --- .github/workflows/release-cli.yml | 89 ++++++++++++++++++++++ .github/workflows/smoke-test-release.yml | 97 ------------------------ 2 files changed, 89 insertions(+), 97 deletions(-) delete mode 100644 .github/workflows/smoke-test-release.yml diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml index 168f9d6..620debb 100644 --- a/.github/workflows/release-cli.yml +++ b/.github/workflows/release-cli.yml @@ -47,3 +47,92 @@ jobs: md5sum: false sha256sum: false compress_assets: false + smoke-test-linux-amd64: + runs-on: ubuntu-latest + needs: + - build-all-for-all + steps: + - name: download and smoke test latest binary + run: | + set -eo pipefail # exit on error, and on pipe failures + + echo "downloading nak binary from releases" + RELEASE_URL="https://api.github.com/repos/fiatjaf/nak/releases/latest" + wget $(wget -q -O - ${RELEASE_URL} | jq -r '.assets[] | select(.name | contains("linux-amd64")) | .browser_download_url') -O nak -nv + chmod +x nak + + echo "printing version..." + ./nak --version + + # generate and manipulate keys + echo "testing key operations..." + SECRET_KEY=$(./nak key generate) + PUBLIC_KEY=$(echo $SECRET_KEY | ./nak key public) + echo "generated key pair: $SECRET_KEY => $PUBLIC_KEY" + + # create events + echo "testing event creation..." + ./nak event -c "hello world" + HELLOWORLD=$(./nak event -c "hello world") + echo " hello world again: $HELLOWORLD" + ./nak event --ts "2 days ago" -c "event with timestamp" + ./nak event -k 1 -t "t=test" -c "event with tag" + + # test NIP-19 encoding/decoding + echo "testing NIP-19 encoding/decoding..." + NSEC=$(echo $SECRET_KEY | ./nak encode nsec) + echo "encoded nsec: $NSEC" + ./nak encode npub 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 + EVENT_ID="5ae731bbc7711f78513da14927c48cc7143a91e6cad0565fdc4d73b8967a7d59" + NEVENT1=$(./nak encode nevent $EVENT_ID) + echo "encoded nevent1: $NEVENT1" + ./nak decode $NEVENT1 + ./nak decode npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6 + + # test event verification + echo "testing event verification..." + # create an event and verify it + VERIFY_EVENT=$(./nak event -c "verify me") + echo $VERIFY_EVENT | ./nak verify + + # test PoW + echo "testing pow..." + ./nak event -c "testing pow" --pow 8 + + # test NIP-49 key encryption/decryption + echo "testing NIP-49 key encryption/decryption..." + ENCRYPTED_KEY=$(./nak key encrypt $SECRET_KEY "testpassword") + echo "encrypted key: ${ENCRYPTED_KEY:0:20}..." + DECRYPTED_KEY=$(./nak key decrypt $ENCRYPTED_KEY "testpassword") + if [ "$DECRYPTED_KEY" != "$SECRET_KEY" ]; then + echo "nip-49 encryption/decryption test failed!" + exit 1 + fi + + # test multi-value tags + echo "testing multi-value tags..." + ./nak event --ts "yesterday" -t "e=f59911b561c37c90b01e9e5c2557307380835c83399756f4d62d8167227e420a;wss://relay.example.com;root" -c "testing multi-value tags" + + # test relay operations (with a public relay) + echo "testing publishing..." + # publish a simple event to a public relay + EVENT_JSON=$(./nak event --sec $SECRET_KEY -c "test from nak smoke test" nos.lol) + EVENT_ID=$(echo $EVENT_JSON | jq -r .id) + echo "published event ID: $EVENT_ID" + + # wait a moment for propagation + sleep 2 + + # fetch the event we just published + ./nak req -i $EVENT_ID nos.lol + + # test serving (just start and immediately kill) + echo "testing serve command..." + timeout 2s ./nak serve || true + + # test filesystem mount (just start and immediately kill) + echo "testing fs mount command..." + mkdir -p /tmp/nostr-mount + timeout 2s ./nak fs --sec $SECRET_KEY /tmp/nostr-mount || true + + echo "all tests passed" diff --git a/.github/workflows/smoke-test-release.yml b/.github/workflows/smoke-test-release.yml deleted file mode 100644 index 6b6ebba..0000000 --- a/.github/workflows/smoke-test-release.yml +++ /dev/null @@ -1,97 +0,0 @@ -name: Smoke test the binary - -on: - workflow_run: - workflows: ["build cli for all platforms"] - types: - - completed - branches: - - master - -jobs: - smoke-test-linux-amd64: - runs-on: ubuntu-latest - if: ${{ github.event.workflow_run.conclusion == 'success' }} - steps: - - name: Download and smoke test latest binary - run: | - set -eo pipefail # Exit on error, and on pipe failures - - echo "Downloading nak binary from releases" - RELEASE_URL="https://api.github.com/repos/fiatjaf/nak/releases/latest" - wget $(wget -q -O - ${RELEASE_URL} | jq -r '.assets[] | select(.name | contains("linux-amd64")) | .browser_download_url') -O nak -nv - chmod +x nak - - echo "Running basic tests..." - ./nak --version - - # Generate and manipulate keys - echo "Testing key operations..." - SECRET_KEY=$(./nak key generate) - PUBLIC_KEY=$(echo $SECRET_KEY | ./nak key public) - echo "Generated key pair: $PUBLIC_KEY" - - # Create events - echo "Testing event creation..." - ./nak event -c "hello world" - ./nak event --ts "2 days ago" -c "event with timestamp" - ./nak event -k 1 -t "t=test" -c "event with tag" - - # Test NIP-19 encoding/decoding - echo "Testing NIP-19 encoding/decoding..." - NSEC=$(echo $SECRET_KEY | ./nak encode nsec) - echo "Encoded nsec: $NSEC" - ./nak encode npub 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 - NOTE_ID="5ae731bbc7711f78513da14927c48cc7143a91e6cad0565fdc4d73b8967a7d59" - NOTE1=$(./nak encode note $NOTE_ID) - echo "Encoded note1: $NOTE1" - ./nak decode $NOTE1 - ./nak decode npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6 - - # Test event verification - echo "Testing event verification..." - # Create an event and verify it - VERIFY_EVENT=$(./nak event -c "verify me") - echo $VERIFY_EVENT | ./nak verify - - # Test PoW - echo "Testing PoW..." - ./nak event -c "testing pow" --pow 8 - - # Test NIP-49 key encryption/decryption - echo "Testing NIP-49 key encryption/decryption..." - ENCRYPTED_KEY=$(./nak key encrypt $SECRET_KEY "testpassword") - echo "Encrypted key: ${ENCRYPTED_KEY:0:20}..." - DECRYPTED_KEY=$(./nak key decrypt $ENCRYPTED_KEY "testpassword") - if [ "$DECRYPTED_KEY" != "$SECRET_KEY" ]; then - echo "NIP-49 encryption/decryption test failed!" - exit 1 - fi - - # Test multi-value tags - echo "Testing multi-value tags..." - ./nak event --ts "yesterday" -t "e=f59911b561c37c90b01e9e5c2557307380835c83399756f4d62d8167227e420a;wss://relay.example.com;root" -c "Testing multi-value tags" - - # Test relay operations (with a public relay) - echo "Testing relay operations..." - # Publish a simple event to a public relay - EVENT_JSON=$(./nak event --sec $SECRET_KEY -c "Test from nak smoke test" nos.lol) - EVENT_ID=$(echo $EVENT_JSON | jq -r .id) - echo "Published event ID: $EVENT_ID" - - # Wait a moment for propagation - sleep 2 - - # Fetch the event we just published - ./nak req -i $EVENT_ID nos.lol - - # Test serving (just start and immediately kill) - echo "Testing serve command..." - timeout 2s ./nak serve || true - - # Test filesystem mount (just start and immediately kill) - echo "Testing fs mount command..." - mkdir -p /tmp/nostr-mount - timeout 2s ./nak fs --sec $SECRET_KEY /tmp/nostr-mount || true - - echo "All tests passed" From 87f27e214ebd25dcedc35a995a238303a7d31e4e Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sun, 28 Dec 2025 12:49:53 -0300 Subject: [PATCH 389/401] use dekey by default on gift wrap and unwrap. --- dekey.go | 36 +++++++++---------- gift.go | 107 ++++++++++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 112 insertions(+), 31 deletions(-) diff --git a/dekey.go b/dekey.go index f7be06f..5c7fdf3 100644 --- a/dekey.go +++ b/dekey.go @@ -33,16 +33,16 @@ var dekey = &cli.Command{ }, &cli.BoolFlag{ Name: "rotate", - Usage: "force the creation of a new encryption key, effectively invalidating any previous ones", + Usage: "force the creation of a new decoupled encryption key, effectively invalidating any previous ones", }, &cli.BoolFlag{ Name: "authorize-all", Aliases: []string{"yolo"}, - Usage: "do not ask for confirmation, just automatically send the encryption key to all devices that exist", + Usage: "do not ask for confirmation, just automatically send the decoupled encryption key to all devices that exist", }, &cli.BoolFlag{ Name: "reject-all", - Usage: "do not ask for confirmation, just not send the encryption key to any device", + Usage: "do not ask for confirmation, just not send the decoupled encryption key to any device", }, ), Action: func(ctx context.Context, c *cli.Command) error { @@ -93,7 +93,7 @@ var dekey = &cli.Command{ } // check for kind:10044 - log("- checking for user encryption key (kind:10044)\n") + log("- checking for decoupled encryption key (kind:10044)\n") keyAnnouncementResult := sys.Pool.FetchManyReplaceable(ctx, relays, nostr.Filter{ Kinds: []nostr.Kind{10044}, Authors: []nostr.PubKey{userPub}, @@ -104,7 +104,7 @@ var dekey = &cli.Command{ var generateNewEncryptionKey bool keyAnnouncementEvent, ok := keyAnnouncementResult.Load(nostr.ReplaceableKey{PubKey: userPub, D: ""}) if !ok { - log("- no user encryption key found, generating new one\n") + log("- no decoupled encryption key found, generating new one\n") generateNewEncryptionKey = true } else { // get the pub from the tag @@ -118,7 +118,7 @@ var dekey = &cli.Command{ return fmt.Errorf("got invalid kind:10044 event, no 'n' tag") } - log(". an encryption public key already exists: %s\n", color.CyanString(ePub.Hex())) + log(". a decoupled encryption public key already exists: %s\n", color.CyanString(ePub.Hex())) if c.Bool("rotate") { log(color.GreenString("rotating it by generating a new one\n")) generateNewEncryptionKey = true @@ -134,12 +134,12 @@ var dekey = &cli.Command{ eKeyPath := filepath.Join(configPath, "dekey", "p", userPub.Hex(), "e", ePub.Hex()) os.MkdirAll(filepath.Dir(eKeyPath), 0700) if err := os.WriteFile(eKeyPath, []byte(eSec.Hex()), 0600); err != nil { - return fmt.Errorf("failed to write user encryption key: %w", err) + return fmt.Errorf("failed to write decoupled encryption key: %w", err) } - log("user encryption key generated and stored, public key: %s\n", color.CyanString(ePub.Hex())) + log("decoupled encryption key generated and stored, public key: %s\n", color.CyanString(ePub.Hex())) // publish kind:10044 - log("publishing user encryption public key (kind:10044)\n") + log("publishing decoupled encryption public key (kind:10044)\n") evt10044 := nostr.Event{ Kind: 10044, Content: "", @@ -164,10 +164,10 @@ var dekey = &cli.Command{ return fmt.Errorf("invalid main key: %w", err) } if eSec.Public() != ePub { - return fmt.Errorf("stored user encryption key is corrupted: %w", err) + return fmt.Errorf("stored decoupled encryption key is corrupted: %w", err) } } else { - log("- encryption key not found locally, attempting to fetch the key from other devices\n") + log("- decoupled encryption key not found locally, attempting to fetch the key from other devices\n") // check if our kind:4454 is already published log("- checking for existing device announcement (kind:4454)\n") @@ -242,14 +242,14 @@ var dekey = &cli.Command{ } // check if it matches mainPub if eSec.Public() == ePub { - log(color.GreenString("successfully decrypted encryption key from another device\n")) + log(color.GreenString("successfully received decoupled encryption key from another device\n")) // store it os.MkdirAll(filepath.Dir(eKeyPath), 0700) os.WriteFile(eKeyPath, []byte(eSecHex), 0600) // delete our 4454 if we had one, since we received the key if len(ourDeviceAnnouncementEvents) > 0 { - log("deleting our device announcement (kind:4454) since we received the encryption key\n") + log("deleting our device announcement (kind:4454) since we received the decoupled encryption key\n") deletion4454 := nostr.Event{ CreatedAt: nostr.Now(), Kind: 5, @@ -290,11 +290,11 @@ var dekey = &cli.Command{ } if eSec == [32]byte{} { - log("encryption secret key not available, must be sent from another device to %s first\n", + log("decoupled encryption secret key not available, must be sent from another device to %s first\n", color.YellowString(deviceName)) return nil } - log(color.GreenString("- encryption key ready\n")) + log(color.GreenString("- decoupled encryption key ready\n")) // now we have mainSec, check for other kind:4454 events newer than the 10044 log("- checking for other devices and key messages so we can send the key\n") @@ -359,7 +359,7 @@ var dekey = &cli.Command{ } else { var proceed bool if err := survey.AskOne(&survey.Confirm{ - Message: fmt.Sprintf("share encryption key with %s"+colors.bold("?"), + Message: fmt.Sprintf("share decoupled encryption key with %s"+colors.bold("?"), color.YellowString(deviceTag[1])), }, &proceed); err != nil { return err @@ -398,7 +398,7 @@ var dekey = &cli.Command{ } } - log("- sending encryption key to new device %s\n", color.YellowString(deviceTag[1])) + log("- sending decoupled encryption key to new device %s\n", color.YellowString(deviceTag[1])) ss, err := nip44.GenerateConversationKey(theirDevice, deviceSec) if err != nil { continue @@ -424,7 +424,7 @@ var dekey = &cli.Command{ if err := publishFlow(ctx, c, kr, evt4455, relayList); err != nil { log(color.RedString("failed to publish key message: %v\n"), err) } else { - log(" - encryption key sent to %s\n", color.GreenString(deviceTag[1])) + log(" - decoupled encryption key sent to %s\n", color.GreenString(deviceTag[1])) } } } diff --git a/gift.go b/gift.go index dbb6097..a007a0d 100644 --- a/gift.go +++ b/gift.go @@ -4,10 +4,14 @@ import ( "context" "fmt" "math/rand" + "os" + "path/filepath" "time" "fiatjaf.com/nostr" + "fiatjaf.com/nostr/keyer" "fiatjaf.com/nostr/nip44" + "github.com/fatih/color" "github.com/mailru/easyjson" "github.com/urfave/cli/v3" ) @@ -16,19 +20,27 @@ var gift = &cli.Command{ Name: "gift", Usage: "gift-wraps (or unwraps) an event according to NIP-59", Description: `example: - nak event | nak gift wrap --sec -p | nak gift unwrap --sec --from `, + nak event | nak gift wrap --sec -p | nak gift unwrap --sec --from + +a decoupled key (if it has been created or received with "nak dekey" previously) will be used by default.`, DisableSliceFlagSeparator: true, + Flags: append( + defaultKeyFlags, + &cli.BoolFlag{ + Name: "use-direct", + Usage: "Use the key given to --sec directly even when a decoupled key exists.", + }, + ), Commands: []*cli.Command{ { Name: "wrap", - Flags: append( - defaultKeyFlags, + Flags: []cli.Flag{ &PubKeyFlag{ Name: "recipient-pubkey", Aliases: []string{"p", "tgt", "target", "pubkey", "to"}, Required: true, }, - ), + }, Usage: "turns an event into a rumor (unsigned) then gift-wraps it to the recipient", Description: `example: nak event -c 'hello' | nak gift wrap --sec -p `, @@ -38,14 +50,25 @@ var gift = &cli.Command{ return err } - recipient := getPubKey(c, "recipient-pubkey") - - // get sender pubkey + // get sender pubkey (ourselves) sender, err := kr.GetPublicKey(ctx) if err != nil { return fmt.Errorf("failed to get sender pubkey: %w", err) } + var cipher nostr.Cipher = kr + // use decoupled key if it exists + configPath := c.String("config-path") + eSec, has, err := getDecoupledEncryptionKey(ctx, configPath, sender) + if has { + if err != nil { + return fmt.Errorf("decoupled encryption key exists, but we failed to get it: %w; call `nak dekey` to attempt a fix or call this again with --use-direct to bypass", err) + } + cipher = keyer.NewPlainKeySigner(eSec) + } + + recipient := getPubKey(c, "recipient-pubkey") + // read event from stdin for eventJSON := range getJsonsOrBlank() { if eventJSON == "{}" { @@ -65,7 +88,7 @@ var gift = &cli.Command{ // create seal rumorJSON, _ := easyjson.Marshal(rumor) - encryptedRumor, err := kr.Encrypt(ctx, string(rumorJSON), recipient) + encryptedRumor, err := cipher.Encrypt(ctx, string(rumorJSON), recipient) if err != nil { return fmt.Errorf("failed to encrypt rumor: %w", err) } @@ -115,20 +138,36 @@ var gift = &cli.Command{ Usage: "decrypts a gift-wrap event sent by the sender to us and exposes its internal rumor (unsigned event).", Description: `example: nak req -p -k 1059 dmrelay.com | nak gift unwrap --sec --from `, - Flags: append( - defaultKeyFlags, + Flags: []cli.Flag{ &PubKeyFlag{ Name: "sender-pubkey", Aliases: []string{"p", "src", "source", "pubkey", "from"}, Required: true, }, - ), + }, Action: func(ctx context.Context, c *cli.Command) error { kr, _, err := gatherKeyerFromArguments(ctx, c) if err != nil { return err } + // get receiver public key (ourselves) + receiver, err := kr.GetPublicKey(ctx) + if err != nil { + return err + } + + var cipher nostr.Cipher = kr + // use decoupled key if it exists + configPath := c.String("config-path") + eSec, has, err := getDecoupledEncryptionKey(ctx, configPath, receiver) + if has { + if err != nil { + return fmt.Errorf("decoupled encryption key exists, but we failed to get it: %w; call `nak dekey` to attempt a fix or call this again with --use-direct to bypass", err) + } + cipher = keyer.NewPlainKeySigner(eSec) + } + sender := getPubKey(c, "sender-pubkey") // read gift-wrapped event from stdin @@ -149,7 +188,7 @@ var gift = &cli.Command{ ephemeralPubkey := wrap.PubKey // decrypt seal - sealJSON, err := kr.Decrypt(ctx, wrap.Content, ephemeralPubkey) + sealJSON, err := cipher.Decrypt(ctx, wrap.Content, ephemeralPubkey) if err != nil { return fmt.Errorf("failed to decrypt seal: %w", err) } @@ -164,7 +203,7 @@ var gift = &cli.Command{ } // decrypt rumor - rumorJSON, err := kr.Decrypt(ctx, seal.Content, sender) + rumorJSON, err := cipher.Decrypt(ctx, seal.Content, sender) if err != nil { return fmt.Errorf("failed to decrypt rumor: %w", err) } @@ -190,3 +229,45 @@ func randomNow() nostr.Timestamp { randomOffset := rand.Int63n(twoDays) return nostr.Timestamp(now - randomOffset) } + +func getDecoupledEncryptionKey(ctx context.Context, configPath string, pubkey nostr.PubKey) (nostr.SecretKey, bool, error) { + relays := sys.FetchWriteRelays(ctx, pubkey) + + keyAnnouncementResult := sys.Pool.FetchManyReplaceable(ctx, relays, nostr.Filter{ + Kinds: []nostr.Kind{10044}, + Authors: []nostr.PubKey{pubkey}, + }, nostr.SubscriptionOptions{Label: "nak-nip4e-gift"}) + var eSec nostr.SecretKey + var ePub nostr.PubKey + + keyAnnouncementEvent, ok := keyAnnouncementResult.Load(nostr.ReplaceableKey{PubKey: pubkey, D: ""}) + if ok { + // get the pub from the tag + for _, tag := range keyAnnouncementEvent.Tags { + if len(tag) >= 2 && tag[0] == "n" { + ePub, _ = nostr.PubKeyFromHex(tag[1]) + break + } + } + if ePub == nostr.ZeroPK { + return [32]byte{}, true, fmt.Errorf("got invalid kind:10044 event, no 'n' tag") + } + + // check if we have the key + eKeyPath := filepath.Join(configPath, "dekey", "p", pubkey.Hex(), "e", ePub.Hex()) + if data, err := os.ReadFile(eKeyPath); err == nil { + log(color.GreenString("- and we have it locally already\n")) + eSec, err = nostr.SecretKeyFromHex(string(data)) + if err != nil { + return [32]byte{}, true, fmt.Errorf("invalid main key: %w", err) + } + if eSec.Public() != ePub { + return [32]byte{}, true, fmt.Errorf("stored decoupled encryption key is corrupted: %w", err) + } + + return eSec, true, nil + } + } + + return [32]byte{}, false, nil +} From 8334474f96953e4c15fdb0a7725f03bac4a0c4a1 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Mon, 29 Dec 2025 19:53:07 -0300 Subject: [PATCH 390/401] git: add viewsource.win to list of possible web views. --- git.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/git.go b/git.go index 081ef36..ddd43cc 100644 --- a/git.go +++ b/git.go @@ -277,6 +277,10 @@ aside from those, there is also: // prompt for web URLs webURLs, err := promptForStringList("web URLs", config.Web, []string{ + fmt.Sprintf("https://viewsource.win/%s/%s", + nip19.EncodeNpub(nostr.MustPubKeyFromHex(config.Owner)), + config.Identifier, + ), fmt.Sprintf("https://gitworkshop.dev/%s/%s", nip19.EncodeNpub(nostr.MustPubKeyFromHex(config.Owner)), config.Identifier, From 81524de04fb040265b88a8bfd2ba354d415e6d00 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 30 Dec 2025 15:28:06 -0300 Subject: [PATCH 391/401] gift: unwrap tries both decoupled and identity keys, wrap defaults to decoupled but accepts flags to change that. --- gift.go | 188 +++++++++++++++++++++++++++++++++++++++++--------------- go.mod | 2 +- go.sum | 4 +- 3 files changed, 140 insertions(+), 54 deletions(-) diff --git a/gift.go b/gift.go index a007a0d..d80bfed 100644 --- a/gift.go +++ b/gift.go @@ -24,13 +24,7 @@ var gift = &cli.Command{ a decoupled key (if it has been created or received with "nak dekey" previously) will be used by default.`, DisableSliceFlagSeparator: true, - Flags: append( - defaultKeyFlags, - &cli.BoolFlag{ - Name: "use-direct", - Usage: "Use the key given to --sec directly even when a decoupled key exists.", - }, - ), + Flags: defaultKeyFlags, Commands: []*cli.Command{ { Name: "wrap", @@ -40,6 +34,14 @@ a decoupled key (if it has been created or received with "nak dekey" previously) Aliases: []string{"p", "tgt", "target", "pubkey", "to"}, Required: true, }, + &cli.BoolFlag{ + Name: "use-our-identity-key", + Usage: "Encrypt with the key given to --sec directly even when a decoupled key exists for the sender.", + }, + &cli.BoolFlag{ + Name: "use-their-identity-key", + Usage: "Encrypt to the public key given as --recipient-pubkey directly even when a decoupled key exists for the receiver.", + }, }, Usage: "turns an event into a rumor (unsigned) then gift-wraps it to the recipient", Description: `example: @@ -56,18 +58,39 @@ a decoupled key (if it has been created or received with "nak dekey" previously) return fmt.Errorf("failed to get sender pubkey: %w", err) } + var using bool + var cipher nostr.Cipher = kr // use decoupled key if it exists - configPath := c.String("config-path") - eSec, has, err := getDecoupledEncryptionKey(ctx, configPath, sender) - if has { - if err != nil { - return fmt.Errorf("decoupled encryption key exists, but we failed to get it: %w; call `nak dekey` to attempt a fix or call this again with --use-direct to bypass", err) + using = false + if !c.Bool("use-our-identity-key") { + configPath := c.String("config-path") + eSec, has, err := getDecoupledEncryptionSecretKey(ctx, configPath, sender) + if has { + if err != nil { + return fmt.Errorf("our decoupled encryption key exists, but we failed to get it: %w; call `nak dekey` to attempt a fix or call this again with --encrypt-with-our-identity-key to bypass", err) + } + cipher = keyer.NewPlainKeySigner(eSec) + log("- using our decoupled encryption key %s\n", color.CyanString(eSec.Public().Hex())) + using = true } - cipher = keyer.NewPlainKeySigner(eSec) + } + if !using { + log("- using our identity key %s\n", color.CyanString(sender.Hex())) } recipient := getPubKey(c, "recipient-pubkey") + using = false + if !c.Bool("use-their-identity-key") { + if theirEPub, exists := getDecoupledEncryptionPublicKey(ctx, recipient); exists { + recipient = theirEPub + using = true + log("- using their decoupled encryption public key %s\n", color.CyanString(theirEPub.Hex())) + } + } + if !using { + log("- using their identity public key %s\n", color.CyanString(recipient.Hex())) + } // read event from stdin for eventJSON := range getJsonsOrBlank() { @@ -137,14 +160,7 @@ a decoupled key (if it has been created or received with "nak dekey" previously) Name: "unwrap", Usage: "decrypts a gift-wrap event sent by the sender to us and exposes its internal rumor (unsigned event).", Description: `example: - nak req -p -k 1059 dmrelay.com | nak gift unwrap --sec --from `, - Flags: []cli.Flag{ - &PubKeyFlag{ - Name: "sender-pubkey", - Aliases: []string{"p", "src", "source", "pubkey", "from"}, - Required: true, - }, - }, + nak req -p -k 1059 dmrelay.com | nak gift unwrap --sec `, Action: func(ctx context.Context, c *cli.Command) error { kr, _, err := gatherKeyerFromArguments(ctx, c) if err != nil { @@ -157,19 +173,18 @@ a decoupled key (if it has been created or received with "nak dekey" previously) return err } - var cipher nostr.Cipher = kr + ciphers := []nostr.Cipher{kr} // use decoupled key if it exists configPath := c.String("config-path") - eSec, has, err := getDecoupledEncryptionKey(ctx, configPath, receiver) + eSec, has, err := getDecoupledEncryptionSecretKey(ctx, configPath, receiver) if has { if err != nil { - return fmt.Errorf("decoupled encryption key exists, but we failed to get it: %w; call `nak dekey` to attempt a fix or call this again with --use-direct to bypass", err) + return fmt.Errorf("our decoupled encryption key exists, but we failed to get it: %w; call `nak dekey` to attempt a fix or call this again with --use-direct to bypass", err) } - cipher = keyer.NewPlainKeySigner(eSec) + ciphers = append(ciphers, kr) + ciphers[0] = keyer.NewPlainKeySigner(eSec) // pub decoupled key first } - sender := getPubKey(c, "sender-pubkey") - // read gift-wrapped event from stdin for wrapJSON := range getJsonsOrBlank() { if wrapJSON == "{}" { @@ -185,36 +200,79 @@ a decoupled key (if it has been created or received with "nak dekey" previously) return fmt.Errorf("not a gift wrap event (kind %d)", wrap.Kind) } - ephemeralPubkey := wrap.PubKey - - // decrypt seal - sealJSON, err := cipher.Decrypt(ctx, wrap.Content, ephemeralPubkey) - if err != nil { - return fmt.Errorf("failed to decrypt seal: %w", err) - } - + // decrypt seal (in the process also find out if they encrypted it to our identity key or to our decoupled key) + var cipher nostr.Cipher var seal nostr.Event - if err := easyjson.Unmarshal([]byte(sealJSON), &seal); err != nil { - return fmt.Errorf("invalid seal JSON: %w", err) + + // try both the receiver identity key and decoupled key + err = nil + for c, potentialCipher := range ciphers { + switch c { + case 0: + log("- trying the receiver's decoupled encryption key %s\n", color.CyanString(eSec.Public().Hex())) + case 1: + log("- trying the receiver's identity key %s\n", color.CyanString(receiver.Hex())) + } + + sealj, thisErr := potentialCipher.Decrypt(ctx, wrap.Content, wrap.PubKey) + if thisErr != nil { + err = thisErr + continue + } + if thisErr := easyjson.Unmarshal([]byte(sealj), &seal); thisErr != nil { + err = fmt.Errorf("invalid seal JSON: %w", thisErr) + continue + } + + cipher = potentialCipher + break + } + if seal.ID == nostr.ZeroID { + // if both ciphers failed above we'll reach here + return fmt.Errorf("failed to decrypt seal: %w", err) } if seal.Kind != 13 { return fmt.Errorf("not a seal event (kind %d)", seal.Kind) } - // decrypt rumor - rumorJSON, err := cipher.Decrypt(ctx, seal.Content, sender) - if err != nil { + senderEncryptionPublicKeys := []nostr.PubKey{seal.PubKey} + if theirEPub, exists := getDecoupledEncryptionPublicKey(ctx, seal.PubKey); exists { + senderEncryptionPublicKeys = append(senderEncryptionPublicKeys, seal.PubKey) + senderEncryptionPublicKeys[0] = theirEPub // put decoupled key first + } + + // decrypt rumor (at this point we know what cipher is the one they encrypted to) + // (but we don't know if they have encrypted with their identity key or their decoupled key, so try both) + var rumor nostr.Event + err = nil + for s, senderEncryptionPublicKey := range senderEncryptionPublicKeys { + switch s { + case 0: + log("- trying the sender's decoupled encryption public key %s\n", color.CyanString(senderEncryptionPublicKey.Hex())) + case 1: + log("- trying the sender's identity public key %s\n", color.CyanString(senderEncryptionPublicKey.Hex())) + } + + rumorj, thisErr := cipher.Decrypt(ctx, seal.Content, senderEncryptionPublicKey) + if thisErr != nil { + err = fmt.Errorf("failed to decrypt rumor: %w", thisErr) + continue + } + if thisErr := easyjson.Unmarshal([]byte(rumorj), &rumor); thisErr != nil { + err = fmt.Errorf("invalid rumor JSON: %w", thisErr) + continue + } + + break + } + + if rumor.ID == nostr.ZeroID { return fmt.Errorf("failed to decrypt rumor: %w", err) } - var rumor nostr.Event - if err := easyjson.Unmarshal([]byte(rumorJSON), &rumor); err != nil { - return fmt.Errorf("invalid rumor JSON: %w", err) - } - // output the unwrapped event (rumor) - stdout(rumorJSON) + stdout(rumor.String()) } return nil @@ -230,18 +288,18 @@ func randomNow() nostr.Timestamp { return nostr.Timestamp(now - randomOffset) } -func getDecoupledEncryptionKey(ctx context.Context, configPath string, pubkey nostr.PubKey) (nostr.SecretKey, bool, error) { +func getDecoupledEncryptionSecretKey(ctx context.Context, configPath string, pubkey nostr.PubKey) (nostr.SecretKey, bool, error) { relays := sys.FetchWriteRelays(ctx, pubkey) keyAnnouncementResult := sys.Pool.FetchManyReplaceable(ctx, relays, nostr.Filter{ Kinds: []nostr.Kind{10044}, Authors: []nostr.PubKey{pubkey}, }, nostr.SubscriptionOptions{Label: "nak-nip4e-gift"}) - var eSec nostr.SecretKey - var ePub nostr.PubKey keyAnnouncementEvent, ok := keyAnnouncementResult.Load(nostr.ReplaceableKey{PubKey: pubkey, D: ""}) if ok { + var ePub nostr.PubKey + // get the pub from the tag for _, tag := range keyAnnouncementEvent.Tags { if len(tag) >= 2 && tag[0] == "n" { @@ -256,8 +314,7 @@ func getDecoupledEncryptionKey(ctx context.Context, configPath string, pubkey no // check if we have the key eKeyPath := filepath.Join(configPath, "dekey", "p", pubkey.Hex(), "e", ePub.Hex()) if data, err := os.ReadFile(eKeyPath); err == nil { - log(color.GreenString("- and we have it locally already\n")) - eSec, err = nostr.SecretKeyFromHex(string(data)) + eSec, err := nostr.SecretKeyFromHex(string(data)) if err != nil { return [32]byte{}, true, fmt.Errorf("invalid main key: %w", err) } @@ -271,3 +328,32 @@ func getDecoupledEncryptionKey(ctx context.Context, configPath string, pubkey no return [32]byte{}, false, nil } + +func getDecoupledEncryptionPublicKey(ctx context.Context, pubkey nostr.PubKey) (nostr.PubKey, bool) { + relays := sys.FetchWriteRelays(ctx, pubkey) + + keyAnnouncementResult := sys.Pool.FetchManyReplaceable(ctx, relays, nostr.Filter{ + Kinds: []nostr.Kind{10044}, + Authors: []nostr.PubKey{pubkey}, + }, nostr.SubscriptionOptions{Label: "nak-nip4e-gift"}) + + keyAnnouncementEvent, ok := keyAnnouncementResult.Load(nostr.ReplaceableKey{PubKey: pubkey, D: ""}) + if ok { + var ePub nostr.PubKey + + // get the pub from the tag + for _, tag := range keyAnnouncementEvent.Tags { + if len(tag) >= 2 && tag[0] == "n" { + ePub, _ = nostr.PubKeyFromHex(tag[1]) + break + } + } + if ePub == nostr.ZeroPK { + return nostr.ZeroPK, false + } + + return ePub, true + } + + return nostr.ZeroPK, false +} diff --git a/go.mod b/go.mod index ff3854c..4920459 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.25 require ( fiatjaf.com/lib v0.3.1 - fiatjaf.com/nostr v0.0.0-20251222025842-099569ea4feb + fiatjaf.com/nostr v0.0.0-20251230181913-e52ffa631bd6 github.com/AlecAivazis/survey/v2 v2.3.7 github.com/bep/debounce v1.2.1 github.com/btcsuite/btcd/btcec/v2 v2.3.6 diff --git a/go.sum b/go.sum index dde7130..934b351 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ fiatjaf.com/lib v0.3.1 h1:/oFQwNtFRfV+ukmOCxfBEAuayoLwXp4wu2/fz5iHpwA= fiatjaf.com/lib v0.3.1/go.mod h1:Ycqq3+mJ9jAWu7XjbQI1cVr+OFgnHn79dQR5oTII47g= -fiatjaf.com/nostr v0.0.0-20251222025842-099569ea4feb h1:GuqPn1g0JRD/dGxFRxEwEFxvbcT3vyvMjP3OoeLIIh0= -fiatjaf.com/nostr v0.0.0-20251222025842-099569ea4feb/go.mod h1:ue7yw0zHfZj23Ml2kVSdBx0ENEaZiuvGxs/8VEN93FU= +fiatjaf.com/nostr v0.0.0-20251230181913-e52ffa631bd6 h1:yH+cU9ZNgUdMCRa5eS3pmqTPP/QdZtSmQAIrN/U5nEc= +fiatjaf.com/nostr v0.0.0-20251230181913-e52ffa631bd6/go.mod h1:ue7yw0zHfZj23Ml2kVSdBx0ENEaZiuvGxs/8VEN93FU= github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ= github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= github.com/FastFilter/xorfilter v0.2.1 h1:lbdeLG9BdpquK64ZsleBS8B4xO/QW1IM0gMzF7KaBKc= From 69e4895e488129d21e3baf1b78957e1663583d8c Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Thu, 8 Jan 2026 22:15:54 -0300 Subject: [PATCH 392/401] --outbox flag for encode. --- encode.go | 55 ++++++++++++++++++++++++++++++++++++++++++++++-------- flags.go | 56 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+), 8 deletions(-) diff --git a/encode.go b/encode.go index d15bf90..438d600 100644 --- a/encode.go +++ b/encode.go @@ -25,13 +25,6 @@ var encode = &cli.Command{ "relays":["wss://nada.zero"], "author":"ebb6ff85430705651b311ed51328767078fd790b14f02d22efba68d5513376bc" } | nak encode`, - Flags: []cli.Flag{ - &cli.StringSliceFlag{ - Name: "relay", - Aliases: []string{"r"}, - Usage: "attach relay hints to naddr code", - }, - }, DisableSliceFlagSeparator: true, Action: func(ctx context.Context, c *cli.Command) error { if c.Args().Len() != 0 { @@ -126,7 +119,12 @@ var encode = &cli.Command{ &cli.StringSliceFlag{ Name: "relay", Aliases: []string{"r"}, - Usage: "attach relay hints to nprofile code", + Usage: "attach relay hints to the code", + }, + &BoolIntFlag{ + Name: "outbox", + Usage: "automatically appends outbox relays to the code", + Value: 3, }, }, DisableSliceFlagSeparator: true, @@ -139,6 +137,13 @@ var encode = &cli.Command{ } relays := c.StringSlice("relay") + + if getBoolInt(c, "outbox") > 0 { + for _, r := range sys.FetchOutboxRelays(ctx, pk, int(getBoolInt(c, "outbox"))) { + relays = appendUnique(relays, r) + } + } + if err := normalizeAndValidateRelayURLs(relays); err != nil { return err } @@ -159,6 +164,16 @@ var encode = &cli.Command{ Aliases: []string{"a"}, Usage: "attach an author pubkey as a hint to the nevent code", }, + &cli.StringSliceFlag{ + Name: "relay", + Aliases: []string{"r"}, + Usage: "attach relay hints to the code", + }, + &BoolIntFlag{ + Name: "outbox", + Usage: "automatically appends outbox relays to the code", + Value: 3, + }, }, DisableSliceFlagSeparator: true, Action: func(ctx context.Context, c *cli.Command) error { @@ -171,6 +186,13 @@ var encode = &cli.Command{ author := getPubKey(c, "author") relays := c.StringSlice("relay") + + if getBoolInt(c, "outbox") > 0 && author != nostr.ZeroPK { + for _, r := range sys.FetchOutboxRelays(ctx, author, int(getBoolInt(c, "outbox"))) { + relays = appendUnique(relays, r) + } + } + if err := normalizeAndValidateRelayURLs(relays); err != nil { return err } @@ -204,6 +226,16 @@ var encode = &cli.Command{ Usage: "kind of referred replaceable event", Required: true, }, + &cli.StringSliceFlag{ + Name: "relay", + Aliases: []string{"r"}, + Usage: "attach relay hints to the code", + }, + &BoolIntFlag{ + Name: "outbox", + Usage: "automatically appends outbox relays to the code", + Value: 3, + }, }, DisableSliceFlagSeparator: true, Action: func(ctx context.Context, c *cli.Command) error { @@ -224,6 +256,13 @@ var encode = &cli.Command{ } relays := c.StringSlice("relay") + + if getBoolInt(c, "outbox") > 0 { + for _, r := range sys.FetchOutboxRelays(ctx, pubkey, int(getBoolInt(c, "outbox"))) { + relays = appendUnique(relays, r) + } + } + if err := normalizeAndValidateRelayURLs(relays); err != nil { return err } diff --git a/flags.go b/flags.go index 671e2d2..895118d 100644 --- a/flags.go +++ b/flags.go @@ -11,6 +11,62 @@ import ( "github.com/urfave/cli/v3" ) +type ( + BoolIntFlag = cli.FlagBase[int, struct{}, boolIntValue] +) + +type boolIntValue struct { + int int + defaultWhenSet int + hasDefault bool + hasBeenSet bool +} + +var _ cli.ValueCreator[int, struct{}] = boolIntValue{} + +func (t boolIntValue) Create(val int, p *int, c struct{}) cli.Value { + *p = val + + return &boolIntValue{ + defaultWhenSet: val, + hasDefault: true, + } +} + +func (t boolIntValue) IsBoolFlag() bool { + return true +} + +func (t boolIntValue) ToString(b int) string { return "<<>>" } + +func (t *boolIntValue) Set(value string) error { + t.hasBeenSet = true + if value == "true" { + if t.hasDefault { + t.int = t.defaultWhenSet + } else { + t.int = 1 + } + return nil + } else { + var err error + t.int, err = strconv.Atoi(value) + return err + } +} + +func (t *boolIntValue) String() string { return fmt.Sprintf("%#v", t.int) } +func (t *boolIntValue) Value() int { return t.int } +func (t *boolIntValue) Get() any { return t.int } + +func getBoolInt(cmd *cli.Command, name string) int { + return cmd.Value(name).(int) +} + +// +// +// + type NaturalTimeFlag = cli.FlagBase[nostr.Timestamp, struct{}, naturalTimeValue] type naturalTimeValue struct { From fabcad3f613189c8d0abfb8e0bccf66ed638c35e Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sat, 10 Jan 2026 09:56:39 -0300 Subject: [PATCH 393/401] key: fix stupid error when passing nsec1 code to `nak key public`. --- key.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/key.go b/key.go index dd64bb1..2779740 100644 --- a/key.go +++ b/key.go @@ -279,12 +279,13 @@ func getSecretKeysFromStdinLinesOrSlice(ctx context.Context, _ *cli.Command, key continue } sk = data.(nostr.SecretKey) - } - - sk, err := nostr.SecretKeyFromHex(sec) - if err != nil { - ctx = lineProcessingError(ctx, "invalid hex key: %s", err) - continue + } else { + var err error + sk, err = nostr.SecretKeyFromHex(sec) + if err != nil { + ctx = lineProcessingError(ctx, "invalid hex key: %s", err) + continue + } } ch <- sk From 38775e0d93be699424b37c46259fec89f4c3fa09 Mon Sep 17 00:00:00 2001 From: mattn Date: Thu, 15 Jan 2026 10:41:14 +0900 Subject: [PATCH 394/401] Use cgofuse (#92) --- fs.go | 45 +- fs_other.go | 2 +- fs_windows.go | 141 ++++ go.mod | 4 +- go.sum | 10 +- nostrfs/asyncfile.go | 56 -- nostrfs/deterministicfile.go | 50 -- nostrfs/entitydir.go | 408 ------------ nostrfs/eventdir.go | 241 ------- nostrfs/npubdir.go | 261 -------- nostrfs/root.go | 1180 +++++++++++++++++++++++++++++++--- nostrfs/viewdir.go | 267 -------- nostrfs/writeablefile.go | 93 --- 13 files changed, 1278 insertions(+), 1480 deletions(-) create mode 100644 fs_windows.go delete mode 100644 nostrfs/asyncfile.go delete mode 100644 nostrfs/deterministicfile.go delete mode 100644 nostrfs/entitydir.go delete mode 100644 nostrfs/eventdir.go delete mode 100644 nostrfs/npubdir.go delete mode 100644 nostrfs/viewdir.go delete mode 100644 nostrfs/writeablefile.go diff --git a/fs.go b/fs.go index 029afdf..1f7f30c 100644 --- a/fs.go +++ b/fs.go @@ -14,9 +14,8 @@ import ( "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/urfave/cli/v3" + "github.com/winfsp/cgofuse/fuse" ) var fsCmd = &cli.Command{ @@ -83,21 +82,22 @@ var fsCmd = &cli.Command{ // create the server log("- mounting at %s... ", color.HiCyanString(mountpoint)) - timeout := time.Second * 120 - server, err := fs.Mount(mountpoint, root, &fs.Options{ - MountOptions: fuse.MountOptions{ - Debug: isVerbose, - Name: "nak", - FsName: "nak", - RememberInodes: true, - }, - AttrTimeout: &timeout, - EntryTimeout: &timeout, - Logger: nostr.DebugLogger, - }) - if err != nil { - return fmt.Errorf("mount failed: %w", err) + + // Create cgofuse host + host := fuse.NewFileSystemHost(root) + host.SetCapReaddirPlus(true) + host.SetUseIno(true) + + // Mount the filesystem + mountArgs := []string{"-s", mountpoint} + if isVerbose { + mountArgs = append([]string{"-d"}, mountArgs...) } + + go func() { + host.Mount("", mountArgs) + }() + log("ok.\n") // setup signal handling for clean unmount @@ -107,17 +107,12 @@ var fsCmd = &cli.Command{ go func() { <-ch log("- unmounting... ") - err := server.Unmount() - if err != nil { - chErr <- fmt.Errorf("unmount failed: %w", err) - } else { - log("ok\n") - chErr <- nil - } + // cgofuse doesn't have explicit unmount, it unmounts on process exit + log("ok\n") + chErr <- nil }() - // serve the filesystem until unmounted - server.Wait() + // wait for signals return <-chErr }, } diff --git a/fs_other.go b/fs_other.go index fba75fc..ccc2894 100644 --- a/fs_other.go +++ b/fs_other.go @@ -1,4 +1,4 @@ -//go:build windows || openbsd +//go:build openbsd package main diff --git a/fs_windows.go b/fs_windows.go new file mode 100644 index 0000000..8dfcc15 --- /dev/null +++ b/fs_windows.go @@ -0,0 +1,141 @@ +//go:build windows + +package main + +import ( + "context" + "fmt" + "os" + "path/filepath" + "time" + + "fiatjaf.com/nostr" + "fiatjaf.com/nostr/keyer" + "github.com/fatih/color" + "github.com/fiatjaf/nak/nostrfs" + "github.com/urfave/cli/v3" + "github.com/winfsp/cgofuse/fuse" +) + +var fsCmd = &cli.Command{ + Name: "fs", + Usage: "mount a FUSE filesystem that exposes Nostr events as files.", + Description: `(experimental)`, + ArgsUsage: "", + Flags: append(defaultKeyFlags, + &PubKeyFlag{ + Name: "pubkey", + Usage: "public key from where to to prepopulate directories", + }, + &cli.DurationFlag{ + Name: "auto-publish-notes", + Usage: "delay after which new notes will be auto-published, set to -1 to not publish.", + Value: time.Second * 30, + }, + &cli.DurationFlag{ + Name: "auto-publish-articles", + Usage: "delay after which edited articles will be auto-published.", + Value: time.Hour * 24 * 365 * 2, + DefaultText: "basically infinite", + }, + ), + DisableSliceFlagSeparator: true, + Action: func(ctx context.Context, c *cli.Command) error { + mountpoint := c.Args().First() + if mountpoint == "" { + return fmt.Errorf("must be called with a directory path to serve as the mountpoint as an argument") + } + + var kr nostr.User + if signer, _, err := gatherKeyerFromArguments(ctx, c); err == nil { + kr = signer + } else { + kr = keyer.NewReadOnlyUser(getPubKey(c, "pubkey")) + } + + apnt := c.Duration("auto-publish-notes") + if apnt < 0 { + apnt = time.Hour * 24 * 365 * 3 + } + apat := c.Duration("auto-publish-articles") + if apat < 0 { + apat = time.Hour * 24 * 365 * 3 + } + + root := nostrfs.NewNostrRoot( + context.WithValue( + context.WithValue( + ctx, + "log", log, + ), + "logverbose", logverbose, + ), + sys, + kr, + mountpoint, + nostrfs.Options{ + AutoPublishNotesTimeout: apnt, + AutoPublishArticlesTimeout: apat, + }, + ) + + // create the server + log("- mounting at %s... ", color.HiCyanString(mountpoint)) + + // Create cgofuse host + host := fuse.NewFileSystemHost(root) + host.SetCapReaddirPlus(true) + host.SetUseIno(true) + + // Mount the filesystem - Windows/WinFsp version + // Based on rclone cmount implementation + mountArgs := []string{ + "-o", "uid=-1", + "-o", "gid=-1", + "--FileSystemName=nak", + } + + // Check if mountpoint is a drive letter or directory + isDriveLetter := len(mountpoint) == 2 && mountpoint[1] == ':' + + if !isDriveLetter { + // WinFsp primarily supports drive letters on Windows + // Directory mounting may not work reliably + log("WARNING: directory mounting may not work on Windows (WinFsp limitation)\n") + log(" consider using a drive letter instead (e.g., 'nak fs Z:')\n") + + // For directory mounts, follow rclone's approach: + // 1. Check that mountpoint doesn't already exist + if _, err := os.Stat(mountpoint); err == nil { + return fmt.Errorf("mountpoint path already exists: %s (must not exist before mounting)", mountpoint) + } else if !os.IsNotExist(err) { + return fmt.Errorf("failed to check mountpoint: %w", err) + } + + // 2. Check that parent directory exists + parent := filepath.Join(mountpoint, "..") + if _, err := os.Stat(parent); err != nil { + if os.IsNotExist(err) { + return fmt.Errorf("parent of mountpoint directory does not exist: %s", parent) + } + return fmt.Errorf("failed to check parent directory: %w", err) + } + + // 3. Use network mode for directory mounts + mountArgs = append(mountArgs, "--VolumePrefix=\\nak\\"+filepath.Base(mountpoint)) + } + + if isVerbose { + mountArgs = append(mountArgs, "-o", "debug") + } + mountArgs = append(mountArgs, mountpoint) + + log("ok.\n") + + // Mount in main thread like hellofs + if !host.Mount("", mountArgs) { + return fmt.Errorf("failed to mount filesystem") + } + return nil + }, +} diff --git a/go.mod b/go.mod index 4920459..e1eb7b2 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( 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/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.1 @@ -24,6 +24,7 @@ require ( github.com/puzpuzpuz/xsync/v3 v3.5.1 github.com/stretchr/testify v1.10.0 github.com/urfave/cli/v3 v3.0.0-beta1 + github.com/winfsp/cgofuse v1.6.0 golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 golang.org/x/sync v0.18.0 golang.org/x/term v0.32.0 @@ -69,7 +70,6 @@ require ( github.com/josharian/intern v1.0.0 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/klauspost/compress v1.18.0 // indirect - github.com/kylelemons/godebug v1.1.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/magefile/mage v1.14.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect diff --git a/go.sum b/go.sum index 934b351..a94ebcc 100644 --- a/go.sum +++ b/go.sum @@ -144,8 +144,8 @@ github.com/hablullah/go-hijri v1.0.2 h1:drT/MZpSZJQXo7jftf5fthArShcaMtsal0Zf/dnm github.com/hablullah/go-hijri v1.0.2/go.mod h1:OS5qyYLDjORXzK4O1adFw9Q5WfhOcMdAKglDkcTxgWQ= github.com/hablullah/go-juliandays v1.0.0 h1:A8YM7wIj16SzlKT0SRJc9CD29iiaUzpBLzh5hr0/5p0= github.com/hablullah/go-juliandays v1.0.0/go.mod h1:0JOYq4oFOuDja+oospuc61YoX+uNEn7Z6uHYTbBzdGc= -github.com/hanwen/go-fuse/v2 v2.7.2 h1:SbJP1sUP+n1UF8NXBA14BuojmTez+mDgOk0bC057HQw= -github.com/hanwen/go-fuse/v2 v2.7.2/go.mod h1:ugNaD/iv5JYyS1Rcvi57Wz7/vrLQJo10mmketmoef48= +github.com/hanwen/go-fuse/v2 v2.9.0 h1:0AOGUkHtbOVeyGLr0tXupiid1Vg7QB7M6YUcdmVdC58= +github.com/hanwen/go-fuse/v2 v2.9.0/go.mod h1:yE6D2PqWwm3CbYRxFXV9xUd8Md5d6NG0WBs5spCswmI= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= @@ -200,8 +200,8 @@ github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1f github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= -github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78= -github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= +github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg= +github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -269,6 +269,8 @@ github.com/wasilibs/go-re2 v1.3.0 h1:LFhBNzoStM3wMie6rN2slD1cuYH2CGiHpvNL3UtcsMw github.com/wasilibs/go-re2 v1.3.0/go.mod h1:AafrCXVvGRJJOImMajgJ2M7rVmWyisVK7sFshbxnVrg= github.com/wasilibs/nottinygc v0.4.0 h1:h1TJMihMC4neN6Zq+WKpLxgd9xCFMw7O9ETLwY2exJQ= github.com/wasilibs/nottinygc v0.4.0/go.mod h1:oDcIotskuYNMpqMF23l7Z8uzD4TC0WXHK8jetlB3HIo= +github.com/winfsp/cgofuse v1.6.0 h1:re3W+HTd0hj4fISPBqfsrwyvPFpzqhDu8doJ9nOPDB0= +github.com/winfsp/cgofuse v1.6.0/go.mod h1:uxjoF2jEYT3+x+vC2KJddEGdk/LU8pRowXmyVMHSV5I= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= diff --git a/nostrfs/asyncfile.go b/nostrfs/asyncfile.go deleted file mode 100644 index 14d5b16..0000000 --- a/nostrfs/asyncfile.go +++ /dev/null @@ -1,56 +0,0 @@ -package nostrfs - -import ( - "context" - "sync/atomic" - "syscall" - - "github.com/hanwen/go-fuse/v2/fs" - "github.com/hanwen/go-fuse/v2/fuse" - "fiatjaf.com/nostr" -) - -type AsyncFile struct { - fs.Inode - ctx context.Context - fetched atomic.Bool - data []byte - ts nostr.Timestamp - load func() ([]byte, nostr.Timestamp) -} - -var ( - _ = (fs.NodeOpener)((*AsyncFile)(nil)) - _ = (fs.NodeGetattrer)((*AsyncFile)(nil)) -) - -func (af *AsyncFile) Getattr(ctx context.Context, f fs.FileHandle, out *fuse.AttrOut) syscall.Errno { - if af.fetched.CompareAndSwap(false, true) { - af.data, af.ts = af.load() - } - - out.Size = uint64(len(af.data)) - out.Mtime = uint64(af.ts) - return fs.OK -} - -func (af *AsyncFile) Open(ctx context.Context, flags uint32) (fs.FileHandle, uint32, syscall.Errno) { - if af.fetched.CompareAndSwap(false, true) { - af.data, af.ts = af.load() - } - - return nil, fuse.FOPEN_KEEP_CACHE, 0 -} - -func (af *AsyncFile) Read( - ctx context.Context, - f fs.FileHandle, - dest []byte, - off int64, -) (fuse.ReadResult, syscall.Errno) { - end := int(off) + len(dest) - if end > len(af.data) { - end = len(af.data) - } - return fuse.ReadResultData(af.data[off:end]), 0 -} diff --git a/nostrfs/deterministicfile.go b/nostrfs/deterministicfile.go deleted file mode 100644 index 95fe030..0000000 --- a/nostrfs/deterministicfile.go +++ /dev/null @@ -1,50 +0,0 @@ -package nostrfs - -import ( - "context" - "syscall" - "unsafe" - - "github.com/hanwen/go-fuse/v2/fs" - "github.com/hanwen/go-fuse/v2/fuse" -) - -type DeterministicFile struct { - fs.Inode - get func() (ctime, mtime uint64, data string) -} - -var ( - _ = (fs.NodeOpener)((*DeterministicFile)(nil)) - _ = (fs.NodeReader)((*DeterministicFile)(nil)) - _ = (fs.NodeGetattrer)((*DeterministicFile)(nil)) -) - -func (r *NostrRoot) NewDeterministicFile(get func() (ctime, mtime uint64, data string)) *DeterministicFile { - return &DeterministicFile{ - get: get, - } -} - -func (f *DeterministicFile) Open(ctx context.Context, flags uint32) (fh fs.FileHandle, fuseFlags uint32, errno syscall.Errno) { - return nil, fuse.FOPEN_KEEP_CACHE, fs.OK -} - -func (f *DeterministicFile) Getattr(ctx context.Context, fh fs.FileHandle, out *fuse.AttrOut) syscall.Errno { - var content string - out.Mode = 0444 - out.Ctime, out.Mtime, content = f.get() - out.Size = uint64(len(content)) - return fs.OK -} - -func (f *DeterministicFile) Read(ctx context.Context, fh fs.FileHandle, dest []byte, off int64) (fuse.ReadResult, syscall.Errno) { - _, _, content := f.get() - data := unsafe.Slice(unsafe.StringData(content), len(content)) - - end := int(off) + len(dest) - if end > len(data) { - end = len(data) - } - return fuse.ReadResultData(data[off:end]), fs.OK -} diff --git a/nostrfs/entitydir.go b/nostrfs/entitydir.go deleted file mode 100644 index 2b8a9c8..0000000 --- a/nostrfs/entitydir.go +++ /dev/null @@ -1,408 +0,0 @@ -package nostrfs - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "path/filepath" - "strconv" - "strings" - "syscall" - "time" - "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" -) - -type EntityDir struct { - fs.Inode - root *NostrRoot - - publisher *debouncer.Debouncer - event *nostr.Event - updating struct { - title string - content string - publishedAt uint64 - } -} - -var ( - _ = (fs.NodeOnAdder)((*EntityDir)(nil)) - _ = (fs.NodeGetattrer)((*EntityDir)(nil)) - _ = (fs.NodeSetattrer)((*EntityDir)(nil)) - _ = (fs.NodeCreater)((*EntityDir)(nil)) - _ = (fs.NodeUnlinker)((*EntityDir)(nil)) -) - -func (e *EntityDir) Getattr(_ context.Context, f fs.FileHandle, out *fuse.AttrOut) syscall.Errno { - out.Ctime = uint64(e.event.CreatedAt) - if e.updating.publishedAt != 0 { - out.Mtime = e.updating.publishedAt - } else { - out.Mtime = e.PublishedAt() - } - return fs.OK -} - -func (e *EntityDir) Create( - _ context.Context, - name string, - flags uint32, - mode uint32, - out *fuse.EntryOut, -) (node *fs.Inode, fh fs.FileHandle, fuseFlags uint32, errno syscall.Errno) { - if name == "publish" && e.publisher.IsRunning() { - // this causes the publish process to be triggered faster - log := e.root.ctx.Value("log").(func(msg string, args ...any)) - log("publishing now!\n") - e.publisher.Flush() - return nil, nil, 0, syscall.ENOTDIR - } - - return nil, nil, 0, syscall.ENOTSUP -} - -func (e *EntityDir) Unlink(ctx context.Context, name string) syscall.Errno { - switch name { - case "content" + kindToExtension(e.event.Kind): - e.updating.content = e.event.Content - return syscall.ENOTDIR - case "title": - e.updating.title = e.Title() - return syscall.ENOTDIR - default: - return syscall.EINTR - } -} - -func (e *EntityDir) Setattr(_ context.Context, _ fs.FileHandle, in *fuse.SetAttrIn, _ *fuse.AttrOut) syscall.Errno { - e.updating.publishedAt = in.Mtime - return fs.OK -} - -func (e *EntityDir) OnAdd(_ context.Context) { - log := e.root.ctx.Value("log").(func(msg string, args ...any)) - - e.AddChild("@author", e.NewPersistentInode( - e.root.ctx, - &fs.MemSymlink{ - Data: []byte(e.root.wd + "/" + nip19.EncodeNpub(e.event.PubKey)), - }, - fs.StableAttr{Mode: syscall.S_IFLNK}, - ), true) - - e.AddChild("event.json", e.NewPersistentInode( - e.root.ctx, - &DeterministicFile{ - get: func() (ctime uint64, mtime uint64, data string) { - eventj, _ := json.MarshalIndent(e.event, "", " ") - return uint64(e.event.CreatedAt), - uint64(e.event.CreatedAt), - unsafe.String(unsafe.SliceData(eventj), len(eventj)) - }, - }, - fs.StableAttr{}, - ), true) - - e.AddChild("identifier", e.NewPersistentInode( - e.root.ctx, - &fs.MemRegularFile{ - Data: []byte(e.event.Tags.GetD()), - Attr: fuse.Attr{ - Mode: 0444, - Ctime: uint64(e.event.CreatedAt), - Mtime: uint64(e.event.CreatedAt), - Size: uint64(len(e.event.Tags.GetD())), - }, - }, - fs.StableAttr{}, - ), true) - - if e.root.signer == nil || e.root.rootPubKey != e.event.PubKey { - // read-only - e.AddChild("title", e.NewPersistentInode( - e.root.ctx, - &DeterministicFile{ - get: func() (ctime uint64, mtime uint64, data string) { - return uint64(e.event.CreatedAt), e.PublishedAt(), e.Title() - }, - }, - fs.StableAttr{}, - ), true) - e.AddChild("content."+kindToExtension(e.event.Kind), e.NewPersistentInode( - e.root.ctx, - &DeterministicFile{ - get: func() (ctime uint64, mtime uint64, data string) { - return uint64(e.event.CreatedAt), e.PublishedAt(), e.event.Content - }, - }, - fs.StableAttr{}, - ), true) - } else { - // writeable - e.updating.title = e.Title() - e.updating.publishedAt = e.PublishedAt() - e.updating.content = e.event.Content - - e.AddChild("title", e.NewPersistentInode( - e.root.ctx, - e.root.NewWriteableFile(e.updating.title, uint64(e.event.CreatedAt), e.updating.publishedAt, func(s string) { - log("title updated") - e.updating.title = strings.TrimSpace(s) - e.handleWrite() - }), - fs.StableAttr{}, - ), true) - - e.AddChild("content."+kindToExtension(e.event.Kind), e.NewPersistentInode( - e.root.ctx, - e.root.NewWriteableFile(e.updating.content, uint64(e.event.CreatedAt), e.updating.publishedAt, func(s string) { - log("content updated") - e.updating.content = strings.TrimSpace(s) - e.handleWrite() - }), - fs.StableAttr{}, - ), true) - } - - var refsdir *fs.Inode - i := 0 - 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) - } - refsdir.AddChild(fmt.Sprintf("ref_%02d", i), refsdir.NewPersistentInode( - e.root.ctx, - &fs.MemSymlink{ - Data: []byte(e.root.wd + "/" + nip19.EncodePointer(ref.Pointer)), - }, - fs.StableAttr{Mode: syscall.S_IFLNK}, - ), true) - } - - var imagesdir *fs.Inode - addImage := func(url string) { - if imagesdir == nil { - in := &fs.Inode{} - imagesdir = e.NewPersistentInode(e.root.ctx, in, fs.StableAttr{Mode: syscall.S_IFDIR}) - e.AddChild("images", imagesdir, true) - } - imagesdir.AddChild(filepath.Base(url), imagesdir.NewPersistentInode( - e.root.ctx, - &AsyncFile{ - ctx: e.root.ctx, - load: func() ([]byte, nostr.Timestamp) { - ctx, cancel := context.WithTimeout(e.root.ctx, time.Second*20) - defer cancel() - r, err := http.NewRequestWithContext(ctx, "GET", url, nil) - if err != nil { - log("failed to load image %s: %s\n", url, err) - return nil, 0 - } - resp, err := http.DefaultClient.Do(r) - if err != nil { - log("failed to load image %s: %s\n", url, err) - return nil, 0 - } - defer resp.Body.Close() - if resp.StatusCode >= 300 { - log("failed to load image %s: %s\n", url, err) - return nil, 0 - } - w := &bytes.Buffer{} - io.Copy(w, resp.Body) - return w.Bytes(), 0 - }, - }, - fs.StableAttr{}, - ), true) - } - - images := nip92.ParseTags(e.event.Tags) - for _, imeta := range images { - if imeta.URL == "" { - continue - } - addImage(imeta.URL) - } - - if tag := e.event.Tags.Find("image"); tag != nil { - addImage(tag[1]) - } -} - -func (e *EntityDir) IsNew() bool { - return e.event.CreatedAt == 0 -} - -func (e *EntityDir) PublishedAt() uint64 { - if tag := e.event.Tags.Find("published_at"); tag != nil { - publishedAt, _ := strconv.ParseUint(tag[1], 10, 64) - return publishedAt - } - return uint64(e.event.CreatedAt) -} - -func (e *EntityDir) Title() string { - if tag := e.event.Tags.Find("title"); tag != nil { - return tag[1] - } - return "" -} - -func (e *EntityDir) handleWrite() { - log := e.root.ctx.Value("log").(func(msg string, args ...any)) - logverbose := e.root.ctx.Value("logverbose").(func(msg string, args ...any)) - - if e.root.opts.AutoPublishArticlesTimeout.Hours() < 24*365 { - if e.publisher.IsRunning() { - log(", timer reset") - } - log(", publishing the ") - if e.IsNew() { - log("new") - } else { - log("updated") - } - log(" event in %d seconds...\n", int(e.root.opts.AutoPublishArticlesTimeout.Seconds())) - } else { - log(".\n") - } - if !e.publisher.IsRunning() { - log("- `touch publish` to publish immediately\n") - log("- `rm title content." + kindToExtension(e.event.Kind) + "` to erase and cancel the edits\n") - } - - e.publisher.Call(func() { - if e.Title() == e.updating.title && e.event.Content == e.updating.content { - log("not modified, publish canceled.\n") - return - } - - evt := nostr.Event{ - Kind: e.event.Kind, - Content: e.updating.content, - Tags: make(nostr.Tags, len(e.event.Tags)), - CreatedAt: nostr.Now(), - } - copy(evt.Tags, e.event.Tags) // copy tags because that's the rule - if e.updating.title != "" { - if titleTag := evt.Tags.Find("title"); titleTag != nil { - titleTag[1] = e.updating.title - } else { - evt.Tags = append(evt.Tags, nostr.Tag{"title", e.updating.title}) - } - } - - // "published_at" tag - publishedAtStr := strconv.FormatUint(e.updating.publishedAt, 10) - if publishedAtStr != "0" { - if publishedAtTag := evt.Tags.Find("published_at"); publishedAtTag != nil { - publishedAtTag[1] = publishedAtStr - } else { - evt.Tags = append(evt.Tags, nostr.Tag{"published_at", publishedAtStr}) - } - } - - // add "p" tags from people mentioned and "q" tags from events mentioned - 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] - if key == "e" || key == "a" { - key = "q" - } - if existing := evt.Tags.FindWithValue(key, val); existing == nil { - evt.Tags = append(evt.Tags, tag) - } - } - - // sign and publish - if err := e.root.signer.SignEvent(e.root.ctx, &evt); err != nil { - log("failed to sign: '%s'.\n", err) - return - } - logverbose("%s\n", evt) - - 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) - } - - log("publishing to %d relays... ", len(relays)) - success := false - first := true - for res := range e.root.sys.Pool.PublishMany(e.root.ctx, relays, evt) { - cleanUrl, _ := strings.CutPrefix(res.RelayURL, "wss://") - if !first { - log(", ") - } - first = false - - if res.Error != nil { - log("%s: %s", color.RedString(cleanUrl), res.Error) - } else { - success = true - log("%s: ok", color.GreenString(cleanUrl)) - } - } - log("\n") - - if success { - e.event = &evt - log("event updated locally.\n") - e.updating.publishedAt = uint64(evt.CreatedAt) // set this so subsequent edits get the correct value - } else { - log("failed.\n") - } - }) -} - -func (r *NostrRoot) FetchAndCreateEntityDir( - parent fs.InodeEmbedder, - extension string, - pointer nostr.EntityPointer, -) (*fs.Inode, error) { - event, _, err := r.sys.FetchSpecificEvent(r.ctx, pointer, sdk.FetchSpecificEventParameters{ - WithRelays: false, - }) - if err != nil { - return nil, fmt.Errorf("failed to fetch: %w", err) - } - - return r.CreateEntityDir(parent, event), nil -} - -func (r *NostrRoot) CreateEntityDir( - parent fs.InodeEmbedder, - event *nostr.Event, -) *fs.Inode { - return parent.EmbeddedInode().NewPersistentInode( - r.ctx, - &EntityDir{root: r, event: event, publisher: debouncer.New(r.opts.AutoPublishArticlesTimeout)}, - fs.StableAttr{Mode: syscall.S_IFDIR}, - ) -} diff --git a/nostrfs/eventdir.go b/nostrfs/eventdir.go deleted file mode 100644 index 9cf875b..0000000 --- a/nostrfs/eventdir.go +++ /dev/null @@ -1,241 +0,0 @@ -package nostrfs - -import ( - "bytes" - "context" - "encoding/binary" - "encoding/json" - "fmt" - "io" - "net/http" - "path/filepath" - "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" -) - -type EventDir struct { - fs.Inode - ctx context.Context - wd string - evt *nostr.Event -} - -var _ = (fs.NodeGetattrer)((*EventDir)(nil)) - -func (e *EventDir) Getattr(_ context.Context, f fs.FileHandle, out *fuse.AttrOut) syscall.Errno { - out.Mtime = uint64(e.evt.CreatedAt) - return fs.OK -} - -func (r *NostrRoot) FetchAndCreateEventDir( - parent fs.InodeEmbedder, - pointer nostr.EventPointer, -) (*fs.Inode, error) { - event, _, err := r.sys.FetchSpecificEvent(r.ctx, pointer, sdk.FetchSpecificEventParameters{ - WithRelays: false, - }) - if err != nil { - return nil, fmt.Errorf("failed to fetch: %w", err) - } - - return r.CreateEventDir(parent, event), nil -} - -func (r *NostrRoot) CreateEventDir( - parent fs.InodeEmbedder, - event *nostr.Event, -) *fs.Inode { - h := parent.EmbeddedInode().NewPersistentInode( - r.ctx, - &EventDir{ctx: r.ctx, wd: r.wd, evt: event}, - fs.StableAttr{Mode: syscall.S_IFDIR, Ino: binary.BigEndian.Uint64(event.ID[8:16])}, - ) - - h.AddChild("@author", h.NewPersistentInode( - r.ctx, - &fs.MemSymlink{ - Data: []byte(r.wd + "/" + nip19.EncodeNpub(event.PubKey)), - }, - fs.StableAttr{Mode: syscall.S_IFLNK}, - ), true) - - eventj, _ := json.MarshalIndent(event, "", " ") - h.AddChild("event.json", h.NewPersistentInode( - r.ctx, - &fs.MemRegularFile{ - Data: eventj, - Attr: fuse.Attr{ - Mode: 0444, - Ctime: uint64(event.CreatedAt), - Mtime: uint64(event.CreatedAt), - Size: uint64(len(event.Content)), - }, - }, - fs.StableAttr{}, - ), true) - - h.AddChild("id", h.NewPersistentInode( - r.ctx, - &fs.MemRegularFile{ - Data: []byte(event.ID.Hex()), - Attr: fuse.Attr{ - Mode: 0444, - Ctime: uint64(event.CreatedAt), - Mtime: uint64(event.CreatedAt), - Size: uint64(64), - }, - }, - fs.StableAttr{}, - ), true) - - h.AddChild("content.txt", h.NewPersistentInode( - r.ctx, - &fs.MemRegularFile{ - Data: []byte(event.Content), - Attr: fuse.Attr{ - Mode: 0444, - Ctime: uint64(event.CreatedAt), - Mtime: uint64(event.CreatedAt), - Size: uint64(len(event.Content)), - }, - }, - fs.StableAttr{}, - ), true) - - var refsdir *fs.Inode - i := 0 - 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) - } - refsdir.AddChild(fmt.Sprintf("ref_%02d", i), refsdir.NewPersistentInode( - r.ctx, - &fs.MemSymlink{ - Data: []byte(r.wd + "/" + nip19.EncodePointer(ref.Pointer)), - }, - fs.StableAttr{Mode: syscall.S_IFLNK}, - ), true) - } - - var imagesdir *fs.Inode - images := nip92.ParseTags(event.Tags) - for _, imeta := range images { - if imeta.URL == "" { - continue - } - if imagesdir == nil { - in := &fs.Inode{} - imagesdir = h.NewPersistentInode(r.ctx, in, fs.StableAttr{Mode: syscall.S_IFDIR}) - h.AddChild("images", imagesdir, true) - } - imagesdir.AddChild(filepath.Base(imeta.URL), imagesdir.NewPersistentInode( - r.ctx, - &AsyncFile{ - ctx: r.ctx, - load: func() ([]byte, nostr.Timestamp) { - ctx, cancel := context.WithTimeout(r.ctx, time.Second*20) - defer cancel() - r, err := http.NewRequestWithContext(ctx, "GET", imeta.URL, nil) - if err != nil { - return nil, 0 - } - resp, err := http.DefaultClient.Do(r) - if err != nil { - return nil, 0 - } - defer resp.Body.Close() - if resp.StatusCode >= 300 { - return nil, 0 - } - w := &bytes.Buffer{} - io.Copy(w, resp.Body) - return w.Bytes(), 0 - }, - }, - fs.StableAttr{}, - ), true) - } - - if event.Kind == 1 { - if pointer := nip10.GetThreadRoot(event.Tags); pointer != nil { - nevent := nip19.EncodePointer(pointer) - h.AddChild("@root", h.NewPersistentInode( - r.ctx, - &fs.MemSymlink{ - Data: []byte(r.wd + "/" + nevent), - }, - fs.StableAttr{Mode: syscall.S_IFLNK}, - ), true) - } - if pointer := nip10.GetImmediateParent(event.Tags); pointer != nil { - nevent := nip19.EncodePointer(pointer) - h.AddChild("@parent", h.NewPersistentInode( - r.ctx, - &fs.MemSymlink{ - Data: []byte(r.wd + "/" + nevent), - }, - fs.StableAttr{Mode: syscall.S_IFLNK}, - ), true) - } - } else if event.Kind == 1111 { - if pointer := nip22.GetThreadRoot(event.Tags); pointer != nil { - if xp, ok := pointer.(nip73.ExternalPointer); ok { - h.AddChild("@root", h.NewPersistentInode( - r.ctx, - &fs.MemRegularFile{ - Data: []byte(``), - }, - fs.StableAttr{}, - ), true) - } else { - nevent := nip19.EncodePointer(pointer) - h.AddChild("@parent", h.NewPersistentInode( - r.ctx, - &fs.MemSymlink{ - Data: []byte(r.wd + "/" + nevent), - }, - fs.StableAttr{Mode: syscall.S_IFLNK}, - ), true) - } - } - if pointer := nip22.GetImmediateParent(event.Tags); pointer != nil { - if xp, ok := pointer.(nip73.ExternalPointer); ok { - h.AddChild("@parent", h.NewPersistentInode( - r.ctx, - &fs.MemRegularFile{ - Data: []byte(``), - }, - fs.StableAttr{}, - ), true) - } else { - nevent := nip19.EncodePointer(pointer) - h.AddChild("@parent", h.NewPersistentInode( - r.ctx, - &fs.MemSymlink{ - Data: []byte(r.wd + "/" + nevent), - }, - fs.StableAttr{Mode: syscall.S_IFLNK}, - ), true) - } - } - } - - return h -} diff --git a/nostrfs/npubdir.go b/nostrfs/npubdir.go deleted file mode 100644 index afce05c..0000000 --- a/nostrfs/npubdir.go +++ /dev/null @@ -1,261 +0,0 @@ -package nostrfs - -import ( - "bytes" - "context" - "encoding/binary" - "encoding/json" - "io" - "net/http" - "sync/atomic" - "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" -) - -type NpubDir struct { - fs.Inode - root *NostrRoot - pointer nostr.ProfilePointer - fetched atomic.Bool -} - -var _ = (fs.NodeOnAdder)((*NpubDir)(nil)) - -func (r *NostrRoot) CreateNpubDir( - parent fs.InodeEmbedder, - pointer nostr.ProfilePointer, - signer nostr.Signer, -) *fs.Inode { - npubdir := &NpubDir{root: r, pointer: pointer} - return parent.EmbeddedInode().NewPersistentInode( - r.ctx, - npubdir, - fs.StableAttr{Mode: syscall.S_IFDIR, Ino: binary.BigEndian.Uint64(pointer.PublicKey[8:16])}, - ) -} - -func (h *NpubDir) OnAdd(_ context.Context) { - log := h.root.ctx.Value("log").(func(msg string, args ...any)) - - relays := h.root.sys.FetchOutboxRelays(h.root.ctx, h.pointer.PublicKey, 2) - log("- adding folder for %s with relays %s\n", - color.HiYellowString(nip19.EncodePointer(h.pointer)), color.HiGreenString("%v", relays)) - - h.AddChild("pubkey", h.NewPersistentInode( - h.root.ctx, - &fs.MemRegularFile{Data: []byte(h.pointer.PublicKey.Hex() + "\n"), Attr: fuse.Attr{Mode: 0444}}, - fs.StableAttr{}, - ), true) - - go func() { - pm := h.root.sys.FetchProfileMetadata(h.root.ctx, h.pointer.PublicKey) - if pm.Event == nil { - return - } - - metadataj, _ := json.MarshalIndent(pm, "", " ") - h.AddChild( - "metadata.json", - h.NewPersistentInode( - h.root.ctx, - &fs.MemRegularFile{ - Data: metadataj, - Attr: fuse.Attr{ - Mtime: uint64(pm.Event.CreatedAt), - Mode: 0444, - }, - }, - fs.StableAttr{}, - ), - true, - ) - - ctx, cancel := context.WithTimeout(h.root.ctx, time.Second*20) - defer cancel() - req, err := http.NewRequestWithContext(ctx, "GET", pm.Picture, nil) - if err == nil { - resp, err := http.DefaultClient.Do(req) - if err == nil { - defer resp.Body.Close() - if resp.StatusCode < 300 { - b := &bytes.Buffer{} - io.Copy(b, resp.Body) - - ext := "png" - if ft, err := magic.Lookup(b.Bytes()); err == nil { - ext = ft.Extension - } - - h.AddChild("picture."+ext, h.NewPersistentInode( - ctx, - &fs.MemRegularFile{ - Data: b.Bytes(), - Attr: fuse.Attr{ - Mtime: uint64(pm.Event.CreatedAt), - Mode: 0444, - }, - }, - fs.StableAttr{}, - ), true) - } - } - } - }() - - if h.GetChild("notes") == nil { - h.AddChild( - "notes", - h.NewPersistentInode( - h.root.ctx, - &ViewDir{ - root: h.root, - filter: nostr.Filter{ - Kinds: []nostr.Kind{1}, - Authors: []nostr.PubKey{h.pointer.PublicKey}, - }, - paginate: true, - relays: relays, - replaceable: false, - createable: true, - }, - fs.StableAttr{Mode: syscall.S_IFDIR}, - ), - true, - ) - } - - if h.GetChild("comments") == nil { - h.AddChild( - "comments", - h.NewPersistentInode( - h.root.ctx, - &ViewDir{ - root: h.root, - filter: nostr.Filter{ - Kinds: []nostr.Kind{1111}, - Authors: []nostr.PubKey{h.pointer.PublicKey}, - }, - paginate: true, - relays: relays, - replaceable: false, - }, - fs.StableAttr{Mode: syscall.S_IFDIR}, - ), - true, - ) - } - - if h.GetChild("photos") == nil { - h.AddChild( - "photos", - h.NewPersistentInode( - h.root.ctx, - &ViewDir{ - root: h.root, - filter: nostr.Filter{ - Kinds: []nostr.Kind{20}, - Authors: []nostr.PubKey{h.pointer.PublicKey}, - }, - paginate: true, - relays: relays, - replaceable: false, - }, - fs.StableAttr{Mode: syscall.S_IFDIR}, - ), - true, - ) - } - - if h.GetChild("videos") == nil { - h.AddChild( - "videos", - h.NewPersistentInode( - h.root.ctx, - &ViewDir{ - root: h.root, - filter: nostr.Filter{ - Kinds: []nostr.Kind{21, 22}, - Authors: []nostr.PubKey{h.pointer.PublicKey}, - }, - paginate: false, - relays: relays, - replaceable: false, - }, - fs.StableAttr{Mode: syscall.S_IFDIR}, - ), - true, - ) - } - - if h.GetChild("highlights") == nil { - h.AddChild( - "highlights", - h.NewPersistentInode( - h.root.ctx, - &ViewDir{ - root: h.root, - filter: nostr.Filter{ - Kinds: []nostr.Kind{9802}, - Authors: []nostr.PubKey{h.pointer.PublicKey}, - }, - paginate: false, - relays: relays, - replaceable: false, - }, - fs.StableAttr{Mode: syscall.S_IFDIR}, - ), - true, - ) - } - - if h.GetChild("articles") == nil { - h.AddChild( - "articles", - h.NewPersistentInode( - h.root.ctx, - &ViewDir{ - root: h.root, - filter: nostr.Filter{ - Kinds: []nostr.Kind{30023}, - Authors: []nostr.PubKey{h.pointer.PublicKey}, - }, - paginate: false, - relays: relays, - replaceable: true, - createable: true, - }, - fs.StableAttr{Mode: syscall.S_IFDIR}, - ), - true, - ) - } - - if h.GetChild("wiki") == nil { - h.AddChild( - "wiki", - h.NewPersistentInode( - h.root.ctx, - &ViewDir{ - root: h.root, - filter: nostr.Filter{ - Kinds: []nostr.Kind{30818}, - Authors: []nostr.PubKey{h.pointer.PublicKey}, - }, - paginate: false, - relays: relays, - replaceable: true, - createable: true, - }, - fs.StableAttr{Mode: syscall.S_IFDIR}, - ), - true, - ) - } -} diff --git a/nostrfs/root.go b/nostrfs/root.go index 6c9fc2f..c59d401 100644 --- a/nostrfs/root.go +++ b/nostrfs/root.go @@ -2,16 +2,19 @@ package nostrfs import ( "context" + "encoding/json" + "fmt" + "net/http" "path/filepath" - "syscall" + "strings" + "sync" "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/winfsp/cgofuse/fuse" ) type Options struct { @@ -20,111 +23,1144 @@ type Options struct { } type NostrRoot struct { - fs.Inode - + fuse.FileSystemBase ctx context.Context - wd string sys *sdk.System rootPubKey nostr.PubKey signer nostr.Signer + opts Options + mountpoint string - opts Options + mu sync.RWMutex + nodes map[string]*Node // path -> node + nextIno uint64 + pendingNotes map[string]*time.Timer // path -> auto-publish timer } -var _ = (fs.NodeOnAdder)((*NostrRoot)(nil)) +type Node struct { + ino uint64 + path string + name string + isDir bool + size int64 + mode uint32 + mtime time.Time + data []byte + children map[string]*Node + loadFunc func() ([]byte, error) // for lazy loading + loaded bool +} + +var _ fuse.FileSystemInterface = (*NostrRoot)(nil) + +func NewNostrRoot(ctx context.Context, sys interface{}, user interface{}, mountpoint string, o Options) *NostrRoot { + var system *sdk.System + if sys != nil { + system = sys.(*sdk.System) + } + + var pubkey nostr.PubKey + var signer nostr.Signer + + if user != nil { + if u, ok := user.(nostr.User); ok { + pubkey, _ = u.GetPublicKey(ctx) + signer, _ = user.(nostr.Signer) + } + } -func NewNostrRoot(ctx context.Context, sys *sdk.System, user nostr.User, mountpoint string, o Options) *NostrRoot { - pubkey, _ := user.GetPublicKey(ctx) abs, _ := filepath.Abs(mountpoint) - var signer nostr.Signer - if user != nil { - signer, _ = user.(nostr.Signer) + root := &NostrRoot{ + ctx: ctx, + sys: system, + rootPubKey: pubkey, + signer: signer, + opts: o, + mountpoint: abs, + nodes: make(map[string]*Node), + nextIno: 2, // 1 is reserved for root + pendingNotes: make(map[string]*time.Timer), } - return &NostrRoot{ - ctx: ctx, - sys: sys, - rootPubKey: pubkey, - signer: signer, - wd: abs, - - opts: o, + // Initialize root directory + rootNode := &Node{ + ino: 1, + path: "/", + name: "", + isDir: true, + mode: fuse.S_IFDIR | 0755, + mtime: time.Now(), + children: make(map[string]*Node), } + root.nodes["/"] = rootNode + + // Start async initialization + go root.initialize() + + return root } -func (r *NostrRoot) OnAdd(_ context.Context) { +func (r *NostrRoot) initialize() { if r.rootPubKey == nostr.ZeroPK { return } - go func() { - time.Sleep(time.Millisecond * 100) + log := r.getLog() + time.Sleep(time.Millisecond * 100) - // add our contacts - fl := r.sys.FetchFollowList(r.ctx, r.rootPubKey) - for _, f := range fl.Items { - pointer := nostr.ProfilePointer{PublicKey: f.Pubkey, Relays: []string{f.Relay}} - r.AddChild( - nip19.EncodeNpub(f.Pubkey), - r.CreateNpubDir(r, pointer, nil), - true, - ) + // Fetch follow list + fl := r.sys.FetchFollowList(r.ctx, r.rootPubKey) + log("- fetched %d contacts\n", len(fl.Items)) + + r.mu.Lock() + defer r.mu.Unlock() + + // Add our contacts + for _, f := range fl.Items { + npub := nip19.EncodeNpub(f.Pubkey) + if _, exists := r.nodes["/"+npub]; !exists { + r.createNpubDirLocked(npub, f.Pubkey, nil) } + } - // add ourselves - npub := nip19.EncodeNpub(r.rootPubKey) - if r.GetChild(npub) == nil { - pointer := nostr.ProfilePointer{PublicKey: r.rootPubKey} + // Add ourselves + npub := nip19.EncodeNpub(r.rootPubKey) + if _, exists := r.nodes["/"+npub]; !exists { + r.createNpubDirLocked(npub, r.rootPubKey, r.signer) + } - r.AddChild( - npub, - r.CreateNpubDir(r, pointer, r.signer), - true, - ) - } - - // add a link to ourselves - r.AddChild("@me", r.NewPersistentInode( - r.ctx, - &fs.MemSymlink{Data: []byte(r.wd + "/" + npub)}, - fs.StableAttr{Mode: syscall.S_IFLNK}, - ), true) - }() + // Add @me symlink (for now, just create a text file pointing to our npub) + meNode := &Node{ + ino: r.nextIno, + path: "/@me", + name: "@me", + isDir: false, + mode: fuse.S_IFREG | 0444, + mtime: time.Now(), + data: []byte(npub + "\n"), + size: int64(len(npub) + 1), + } + r.nextIno++ + r.nodes["/@me"] = meNode + r.nodes["/"].children["@me"] = meNode } -func (r *NostrRoot) Lookup(_ context.Context, name string, out *fuse.EntryOut) (*fs.Inode, syscall.Errno) { - out.SetEntryTimeout(time.Minute * 5) - - child := r.GetChild(name) - if child != nil { - return child, fs.OK +func (r *NostrRoot) fetchMetadata(dirPath string, pubkey nostr.PubKey) { + pm := r.sys.FetchProfileMetadata(r.ctx, pubkey) + if pm.Event == nil { + return } - if pp, err := nip05.QueryIdentifier(r.ctx, name); err == nil { - return r.NewPersistentInode( - r.ctx, - &fs.MemSymlink{Data: []byte(r.wd + "/" + nip19.EncodePointer(*pp))}, - fs.StableAttr{Mode: syscall.S_IFLNK}, - ), fs.OK + // Use the content field which contains the actual profile JSON + metadataJ := []byte(pm.Event.Content) + + r.mu.Lock() + defer r.mu.Unlock() + + metadataNode := &Node{ + ino: r.nextIno, + path: dirPath + "/metadata.json", + name: "metadata.json", + isDir: false, + mode: fuse.S_IFREG | 0444, + mtime: time.Unix(int64(pm.Event.CreatedAt), 0), + data: metadataJ, + size: int64(len(metadataJ)), + } + r.nextIno++ + r.nodes[dirPath+"/metadata.json"] = metadataNode + if dir, ok := r.nodes[dirPath]; ok { + dir.children["metadata.json"] = metadataNode + } +} + +func (r *NostrRoot) fetchProfilePicture(dirPath string, pubkey nostr.PubKey) { + pm := r.sys.FetchProfileMetadata(r.ctx, pubkey) + if pm.Event == nil || pm.Picture == "" { + return } + // Download picture + ctx, cancel := context.WithTimeout(r.ctx, time.Second*20) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "GET", pm.Picture, nil) + if err != nil { + return + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return + } + defer resp.Body.Close() + + if resp.StatusCode >= 300 { + return + } + + // Read image data + imageData := make([]byte, 0, 1024*1024) // 1MB initial capacity + buf := make([]byte, 32*1024) + for { + n, err := resp.Body.Read(buf) + if n > 0 { + imageData = append(imageData, buf[:n]...) + } + if err != nil { + break + } + if len(imageData) > 10*1024*1024 { // 10MB max + break + } + } + + if len(imageData) == 0 { + return + } + + // Detect file extension from content-type or URL + ext := "png" + if ct := resp.Header.Get("Content-Type"); ct != "" { + switch ct { + case "image/jpeg": + ext = "jpg" + case "image/png": + ext = "png" + case "image/gif": + ext = "gif" + case "image/webp": + ext = "webp" + } + } + + r.mu.Lock() + defer r.mu.Unlock() + + picturePath := dirPath + "/picture." + ext + pictureNode := &Node{ + ino: r.nextIno, + path: picturePath, + name: "picture." + ext, + isDir: false, + mode: fuse.S_IFREG | 0444, + mtime: time.Unix(int64(pm.Event.CreatedAt), 0), + data: imageData, + size: int64(len(imageData)), + } + r.nextIno++ + r.nodes[picturePath] = pictureNode + if dir, ok := r.nodes[dirPath]; ok { + dir.children["picture."+ext] = pictureNode + } +} + +func (r *NostrRoot) fetchEvents(dirPath string, filter nostr.Filter) { + ctx, cancel := context.WithTimeout(r.ctx, time.Second*10) + defer cancel() + + // Get relays for authors + var relays []string + if len(filter.Authors) > 0 { + relays = r.sys.FetchOutboxRelays(ctx, filter.Authors[0], 3) + } + if len(relays) == 0 { + relays = []string{"wss://relay.damus.io", "wss://nos.lol"} + } + + log := r.getLog() + log("- fetching events for %s from %v\n", dirPath, relays) + + // Fetch events + events := make([]*nostr.Event, 0) + for ie := range r.sys.Pool.FetchMany(ctx, relays, filter, nostr.SubscriptionOptions{ + Label: "nak-fs", + }) { + // Make a copy to avoid pointer issues with loop variable + evt := ie.Event + events = append(events, &evt) + if len(events) >= int(filter.Limit) { + break + } + } + + log("- fetched %d events for %s\n", len(events), dirPath) + + r.mu.Lock() + defer r.mu.Unlock() + + dir, ok := r.nodes[dirPath] + if !ok { + return + } + + // Track oldest timestamp for pagination + var oldestTimestamp nostr.Timestamp + if len(events) > 0 { + oldestTimestamp = events[len(events)-1].CreatedAt + } + + for _, evt := range events { + // Create filename based on event + filename := r.eventToFilename(evt) + filePath := dirPath + "/" + filename + + if _, exists := r.nodes[filePath]; exists { + continue + } + + content := evt.Content + if len(content) == 0 { + content = "(empty)" + } + + fileNode := &Node{ + ino: r.nextIno, + path: filePath, + name: filename, + isDir: false, + mode: fuse.S_IFREG | 0644, + mtime: time.Unix(int64(evt.CreatedAt), 0), + data: []byte(content), + size: int64(len(content)), + } + r.nextIno++ + r.nodes[filePath] = fileNode + dir.children[filename] = fileNode + } + + // Add "more" file for pagination if we got a full page + if len(events) >= int(filter.Limit) { + moreFile := &Node{ + ino: r.nextIno, + path: dirPath + "/.more", + name: ".more", + isDir: false, + mode: fuse.S_IFREG | 0444, + mtime: time.Now(), + data: []byte(fmt.Sprintf("Read this file to load more events (until: %d)\n", oldestTimestamp)), + size: int64(len(fmt.Sprintf("Read this file to load more events (until: %d)\n", oldestTimestamp))), + loadFunc: func() ([]byte, error) { + // When .more is read, fetch next page + newFilter := filter + newFilter.Until = oldestTimestamp + go r.fetchEvents(dirPath, newFilter) + return []byte("Loading more events...\n"), nil + }, + } + r.nextIno++ + r.nodes[dirPath+"/.more"] = moreFile + dir.children[".more"] = moreFile + } +} + +func (r *NostrRoot) eventToFilename(evt *nostr.Event) string { + // Use event ID first 8 chars + extension based on kind + ext := kindToExtension(evt.Kind) + + // Get hex representation of event ID + // evt.ID.String() may return format like ":1234abcd" so use Hex() or remove colons + idHex := evt.ID.Hex() + if len(idHex) > 8 { + idHex = idHex[:8] + } + + // For articles, try to use title + if evt.Kind == 30023 || evt.Kind == 30818 { + for _, tag := range evt.Tags { + if len(tag) >= 2 && tag[0] == "title" { + titleStr := tag[1] + if titleStr != "" { + // Sanitize title for filename + name := strings.Map(func(r rune) rune { + if r == '/' || r == '\\' || r == ':' || r == '*' || r == '?' || r == '"' || r == '<' || r == '>' || r == '|' { + return '-' + } + return r + }, titleStr) + if len(name) > 50 { + name = name[:50] + } + return fmt.Sprintf("%s-%s.%s", name, idHex, ext) + } + } + } + } + + return fmt.Sprintf("%s.%s", idHex, ext) +} + +func (r *NostrRoot) getLog() func(string, ...interface{}) { + if log := r.ctx.Value("log"); log != nil { + return log.(func(string, ...interface{})) + } + return func(string, ...interface{}) {} +} + +func (r *NostrRoot) getNode(path string) *Node { + originalPath := path + + // Normalize path + if path == "" { + path = "/" + } + + // Convert Windows backslashes to forward slashes + path = strings.ReplaceAll(path, "\\", "/") + + // Ensure path starts with / + if !strings.HasPrefix(path, "/") { + path = "/" + path + } + + // Remove trailing slash except for root + if path != "/" && strings.HasSuffix(path, "/") { + path = strings.TrimSuffix(path, "/") + } + + // Debug logging + if r.ctx.Value("logverbose") != nil { + logv := r.ctx.Value("logverbose").(func(string, ...interface{})) + logv("getNode: original='%s' normalized='%s'\n", originalPath, path) + } + + r.mu.RLock() + defer r.mu.RUnlock() + + node := r.nodes[path] + + // Debug: if not found, show similar paths + if node == nil && r.ctx.Value("logverbose") != nil { + logv := r.ctx.Value("logverbose").(func(string, ...interface{})) + logv("getNode: NOT FOUND '%s'\n", path) + basename := filepath.Base(path) + logv("getNode: searching for similar (basename='%s'):\n", basename) + count := 0 + for p := range r.nodes { + if strings.Contains(p, basename) { + logv(" - '%s'\n", p) + count++ + if count >= 5 { + break + } + } + } + } + + return node +} + +func (r *NostrRoot) Getattr(path string, stat *fuse.Stat_t, fh uint64) int { + node := r.getNode(path) + + // If node doesn't exist, try dynamic lookup + // But skip for special files starting with @ or . + if node == nil { + basename := filepath.Base(path) + if !strings.HasPrefix(basename, "@") && !strings.HasPrefix(basename, ".") { + if r.dynamicLookup(path) { + node = r.getNode(path) + } + } + } + + if node == nil { + return -fuse.ENOENT + } + + stat.Ino = node.ino + stat.Mode = node.mode + stat.Size = node.size + stat.Mtim = fuse.NewTimespec(node.mtime) + stat.Atim = stat.Mtim + stat.Ctim = stat.Mtim + + return 0 +} + +// dynamicLookup tries to create nodes on-demand for npub/note/nevent paths +func (r *NostrRoot) dynamicLookup(path string) bool { + // Normalize path + path = strings.ReplaceAll(path, "\\", "/") + if !strings.HasPrefix(path, "/") { + path = "/" + path + } + + // Get the first component after root + parts := strings.Split(strings.TrimPrefix(path, "/"), "/") + if len(parts) == 0 { + return false + } + + name := parts[0] + + // Try to decode as nostr pointer pointer, err := nip19.ToPointer(name) if err != nil { - return nil, syscall.ENOENT + // Try NIP-05 + if strings.Contains(name, "@") && !strings.HasPrefix(name, "@") { + ctx, cancel := context.WithTimeout(r.ctx, time.Second*5) + defer cancel() + if pp, err := nip05.QueryIdentifier(ctx, name); err == nil { + pointer = pp + } else { + return false + } + } else { + return false + } + } + + r.mu.Lock() + defer r.mu.Unlock() + + // Check if already exists + if _, exists := r.nodes["/"+name]; exists { + return true } switch p := pointer.(type) { case nostr.ProfilePointer: - npubdir := r.CreateNpubDir(r, p, nil) - return npubdir, fs.OK + // Create npub directory dynamically + r.createNpubDirLocked(name, p.PublicKey, nil) + return true + case nostr.EventPointer: - eventdir, err := r.FetchAndCreateEventDir(r, p) - if err != nil { - return nil, syscall.ENOENT - } - return eventdir, fs.OK + // Create event directory dynamically + return r.createEventDirLocked(name, p) + default: - return nil, syscall.ENOENT + return false } } + +func (r *NostrRoot) createNpubDirLocked(npub string, pubkey nostr.PubKey, signer nostr.Signer) { + dirPath := "/" + npub + + // Check if already exists + if _, exists := r.nodes[dirPath]; exists { + return + } + + dirNode := &Node{ + ino: r.nextIno, + path: dirPath, + name: npub, + isDir: true, + mode: fuse.S_IFDIR | 0755, + mtime: time.Now(), + children: make(map[string]*Node), + } + r.nextIno++ + r.nodes[dirPath] = dirNode + r.nodes["/"].children[npub] = dirNode + + // Add pubkey file + pubkeyData := []byte(pubkey.Hex() + "\n") + pubkeyNode := &Node{ + ino: r.nextIno, + path: dirPath + "/pubkey", + name: "pubkey", + isDir: false, + mode: fuse.S_IFREG | 0444, + mtime: time.Now(), + data: pubkeyData, + size: int64(len(pubkeyData)), + } + r.nextIno++ + r.nodes[dirPath+"/pubkey"] = pubkeyNode + dirNode.children["pubkey"] = pubkeyNode + + // Fetch metadata asynchronously + go r.fetchMetadata(dirPath, pubkey) + + // Add notes directory + r.createViewDirLocked(dirPath, "notes", nostr.Filter{ + Kinds: []nostr.Kind{1}, + Authors: []nostr.PubKey{pubkey}, + Limit: 50, + }) + + // Add articles directory + r.createViewDirLocked(dirPath, "articles", nostr.Filter{ + Kinds: []nostr.Kind{30023}, + Authors: []nostr.PubKey{pubkey}, + Limit: 50, + }) + + // Add comments directory + r.createViewDirLocked(dirPath, "comments", nostr.Filter{ + Kinds: []nostr.Kind{1111}, + Authors: []nostr.PubKey{pubkey}, + Limit: 50, + }) + + // Add highlights directory + r.createViewDirLocked(dirPath, "highlights", nostr.Filter{ + Kinds: []nostr.Kind{9802}, + Authors: []nostr.PubKey{pubkey}, + Limit: 50, + }) + + // Add photos directory + r.createViewDirLocked(dirPath, "photos", nostr.Filter{ + Kinds: []nostr.Kind{20}, + Authors: []nostr.PubKey{pubkey}, + Limit: 50, + }) + + // Add videos directory + r.createViewDirLocked(dirPath, "videos", nostr.Filter{ + Kinds: []nostr.Kind{21, 22}, + Authors: []nostr.PubKey{pubkey}, + Limit: 50, + }) + + // Add wikis directory + r.createViewDirLocked(dirPath, "wikis", nostr.Filter{ + Kinds: []nostr.Kind{30818}, + Authors: []nostr.PubKey{pubkey}, + Limit: 50, + }) + + // Fetch profile picture asynchronously + go r.fetchProfilePicture(dirPath, pubkey) +} + +func (r *NostrRoot) createViewDirLocked(parentPath, name string, filter nostr.Filter) { + dirPath := parentPath + "/" + name + + // Check if already exists + if _, exists := r.nodes[dirPath]; exists { + return + } + + dirNode := &Node{ + ino: r.nextIno, + path: dirPath, + name: name, + isDir: true, + mode: fuse.S_IFDIR | 0755, + mtime: time.Now(), + children: make(map[string]*Node), + } + r.nextIno++ + + r.nodes[dirPath] = dirNode + if parent, ok := r.nodes[parentPath]; ok { + parent.children[name] = dirNode + } + + // Fetch events asynchronously + go r.fetchEvents(dirPath, filter) +} + +func (r *NostrRoot) createEventDirLocked(name string, pointer nostr.EventPointer) bool { + dirPath := "/" + name + + // Fetch the event + ctx, cancel := context.WithTimeout(r.ctx, time.Second*10) + defer cancel() + + var relays []string + if len(pointer.Relays) > 0 { + relays = pointer.Relays + } else { + relays = []string{"wss://relay.damus.io", "wss://nos.lol"} + } + + filter := nostr.Filter{IDs: []nostr.ID{pointer.ID}} + + var evt *nostr.Event + for ie := range r.sys.Pool.FetchMany(ctx, relays, filter, nostr.SubscriptionOptions{ + Label: "nak-fs-event", + }) { + // Make a copy to avoid pointer issues + evtCopy := ie.Event + evt = &evtCopy + break + } + + if evt == nil { + return false + } + + // Create event directory + dirNode := &Node{ + ino: r.nextIno, + path: dirPath, + name: name, + isDir: true, + mode: fuse.S_IFDIR | 0755, + mtime: time.Unix(int64(evt.CreatedAt), 0), + children: make(map[string]*Node), + } + r.nextIno++ + r.nodes[dirPath] = dirNode + r.nodes["/"].children[name] = dirNode + + // Add content file + ext := kindToExtension(evt.Kind) + contentPath := dirPath + "/content." + ext + contentNode := &Node{ + ino: r.nextIno, + path: contentPath, + name: "content." + ext, + isDir: false, + mode: fuse.S_IFREG | 0644, + mtime: time.Unix(int64(evt.CreatedAt), 0), + data: []byte(evt.Content), + size: int64(len(evt.Content)), + } + r.nextIno++ + r.nodes[contentPath] = contentNode + dirNode.children["content."+ext] = contentNode + + // Add event.json + eventJSON, _ := json.MarshalIndent(evt, "", " ") + eventJSONPath := dirPath + "/event.json" + eventJSONNode := &Node{ + ino: r.nextIno, + path: eventJSONPath, + name: "event.json", + isDir: false, + mode: fuse.S_IFREG | 0444, + mtime: time.Unix(int64(evt.CreatedAt), 0), + data: eventJSON, + size: int64(len(eventJSON)), + } + r.nextIno++ + r.nodes[eventJSONPath] = eventJSONNode + dirNode.children["event.json"] = eventJSONNode + + return true +} + +func (r *NostrRoot) Readdir(path string, + fill func(name string, stat *fuse.Stat_t, ofst int64) bool, + ofst int64, + fh uint64) int { + + node := r.getNode(path) + if node == nil || !node.isDir { + return -fuse.ENOENT + } + + fill(".", nil, 0) + fill("..", nil, 0) + + r.mu.RLock() + defer r.mu.RUnlock() + + for name, child := range node.children { + stat := &fuse.Stat_t{ + Ino: child.ino, + Mode: child.mode, + Size: child.size, + Mtim: fuse.NewTimespec(child.mtime), + } + if !fill(name, stat, 0) { + break + } + } + + return 0 +} + +func (r *NostrRoot) Open(path string, flags int) (int, uint64) { + // Log the open attempt + if r.ctx.Value("logverbose") != nil { + logv := r.ctx.Value("logverbose").(func(string, ...interface{})) + logv("Open: path='%s' flags=%d\n", path, flags) + } + + node := r.getNode(path) + if node == nil { + return -fuse.ENOENT, ^uint64(0) + } + if node.isDir { + return -fuse.EISDIR, ^uint64(0) + } + + // Load data if needed + if node.loadFunc != nil && !node.loaded { + r.mu.Lock() + if !node.loaded { + if data, err := node.loadFunc(); err == nil { + node.data = data + node.size = int64(len(data)) + node.loaded = true + } + } + r.mu.Unlock() + } + + return 0, node.ino +} + +func (r *NostrRoot) Read(path string, buff []byte, ofst int64, fh uint64) int { + node := r.getNode(path) + if node == nil || node.isDir { + return -fuse.ENOENT + } + + if ofst >= node.size { + return 0 + } + + endofst := ofst + int64(len(buff)) + if endofst > node.size { + endofst = node.size + } + + n := copy(buff, node.data[ofst:endofst]) + return n +} + +func (r *NostrRoot) Opendir(path string) (int, uint64) { + node := r.getNode(path) + if node == nil { + return -fuse.ENOENT, ^uint64(0) + } + if !node.isDir { + return -fuse.ENOTDIR, ^uint64(0) + } + return 0, node.ino +} + +func (r *NostrRoot) Release(path string, fh uint64) int { + return 0 +} + +func (r *NostrRoot) Releasedir(path string, fh uint64) int { + return 0 +} + +// Create creates a new file +func (r *NostrRoot) Create(path string, flags int, mode uint32) (int, uint64) { + // Parse path + path = strings.ReplaceAll(path, "\\", "/") + if !strings.HasPrefix(path, "/") { + path = "/" + path + } + + dir := filepath.Dir(path) + name := filepath.Base(path) + + r.mu.Lock() + defer r.mu.Unlock() + + // Check if parent directory exists + parent, ok := r.nodes[dir] + if !ok || !parent.isDir { + return -fuse.ENOENT, ^uint64(0) + } + + // Check if file already exists + if _, exists := r.nodes[path]; exists { + return -fuse.EEXIST, ^uint64(0) + } + + // Create new file node + fileNode := &Node{ + ino: r.nextIno, + path: path, + name: name, + isDir: false, + mode: fuse.S_IFREG | 0644, + mtime: time.Now(), + data: []byte{}, + size: 0, + } + r.nextIno++ + + r.nodes[path] = fileNode + parent.children[name] = fileNode + + return 0, fileNode.ino +} + +// Truncate truncates a file +func (r *NostrRoot) Truncate(path string, size int64, fh uint64) int { + node := r.getNode(path) + if node == nil { + return -fuse.ENOENT + } + if node.isDir { + return -fuse.EISDIR + } + + r.mu.Lock() + defer r.mu.Unlock() + + if size == 0 { + node.data = []byte{} + } else if size < int64(len(node.data)) { + node.data = node.data[:size] + } else { + // Extend with zeros + newData := make([]byte, size) + copy(newData, node.data) + node.data = newData + } + node.size = size + node.mtime = time.Now() + + return 0 +} + +// Write writes data to a file +func (r *NostrRoot) Write(path string, buff []byte, ofst int64, fh uint64) int { + node := r.getNode(path) + if node == nil { + return -fuse.ENOENT + } + if node.isDir { + return -fuse.EISDIR + } + + r.mu.Lock() + defer r.mu.Unlock() + + endofst := ofst + int64(len(buff)) + + // Extend data if necessary + if endofst > int64(len(node.data)) { + newData := make([]byte, endofst) + copy(newData, node.data) + node.data = newData + } + + n := copy(node.data[ofst:], buff) + node.size = int64(len(node.data)) + node.mtime = time.Now() + + // Check if this is a note that should be auto-published + if r.signer != nil && strings.Contains(path, "/notes/") && !strings.HasPrefix(filepath.Base(path), ".") { + // Cancel existing timer if any + if timer, exists := r.pendingNotes[path]; exists { + timer.Stop() + } + + // Schedule auto-publish + timeout := r.opts.AutoPublishNotesTimeout + if timeout > 0 && timeout < time.Hour*24*365 { + r.pendingNotes[path] = time.AfterFunc(timeout, func() { + r.publishNote(path) + }) + } + } + + return n +} + +func (r *NostrRoot) publishNote(path string) { + r.mu.Lock() + node, ok := r.nodes[path] + if !ok { + r.mu.Unlock() + return + } + + content := string(node.data) + r.mu.Unlock() + + if r.signer == nil { + return + } + + log := r.getLog() + log("- auto-publishing note from %s\n", path) + + // Create and sign event + evt := &nostr.Event{ + CreatedAt: nostr.Now(), + Kind: 1, + Tags: nostr.Tags{}, + Content: content, + } + + if err := r.signer.SignEvent(r.ctx, evt); err != nil { + log("- failed to sign note: %v\n", err) + return + } + + // Publish to relays + ctx, cancel := context.WithTimeout(r.ctx, time.Second*10) + defer cancel() + + relays := r.sys.FetchOutboxRelays(ctx, r.rootPubKey, 3) + if len(relays) == 0 { + relays = []string{"wss://relay.damus.io", "wss://nos.lol"} + } + + for _, url := range relays { + relay, err := r.sys.Pool.EnsureRelay(url) + if err != nil { + continue + } + relay.Publish(ctx, *evt) + } + + log("- published note %s to %d relays\n", evt.ID.Hex()[:8], len(relays)) + + // Update filename to include event ID + r.mu.Lock() + defer r.mu.Unlock() + + dir := filepath.Dir(path) + oldName := filepath.Base(path) + ext := filepath.Ext(oldName) + newName := evt.ID.Hex()[:8] + ext + newPath := dir + "/" + newName + + // Rename node + if _, exists := r.nodes[newPath]; !exists { + node.path = newPath + node.name = newName + r.nodes[newPath] = node + delete(r.nodes, path) + + if parent, ok := r.nodes[dir]; ok { + delete(parent.children, oldName) + parent.children[newName] = node + } + } + + delete(r.pendingNotes, path) +} + +// Unlink deletes a file +func (r *NostrRoot) Unlink(path string) int { + path = strings.ReplaceAll(path, "\\", "/") + if !strings.HasPrefix(path, "/") { + path = "/" + path + } + + dir := filepath.Dir(path) + name := filepath.Base(path) + + r.mu.Lock() + defer r.mu.Unlock() + + // Check if file exists + node, ok := r.nodes[path] + if !ok { + return -fuse.ENOENT + } + if node.isDir { + return -fuse.EISDIR + } + + // Remove from parent + if parent, ok := r.nodes[dir]; ok { + delete(parent.children, name) + } + + // Remove from nodes map + delete(r.nodes, path) + + return 0 +} + +// Mkdir creates a new directory +func (r *NostrRoot) Mkdir(path string, mode uint32) int { + path = strings.ReplaceAll(path, "\\", "/") + if !strings.HasPrefix(path, "/") { + path = "/" + path + } + + dir := filepath.Dir(path) + name := filepath.Base(path) + + r.mu.Lock() + defer r.mu.Unlock() + + // Check if parent directory exists + parent, ok := r.nodes[dir] + if !ok || !parent.isDir { + return -fuse.ENOENT + } + + // Check if directory already exists + if _, exists := r.nodes[path]; exists { + return -fuse.EEXIST + } + + // Create new directory node + dirNode := &Node{ + ino: r.nextIno, + path: path, + name: name, + isDir: true, + mode: fuse.S_IFDIR | 0755, + mtime: time.Now(), + children: make(map[string]*Node), + } + r.nextIno++ + + r.nodes[path] = dirNode + parent.children[name] = dirNode + + return 0 +} + +// Rmdir removes a directory +func (r *NostrRoot) Rmdir(path string) int { + path = strings.ReplaceAll(path, "\\", "/") + if !strings.HasPrefix(path, "/") { + path = "/" + path + } + + if path == "/" { + return -fuse.EACCES + } + + dir := filepath.Dir(path) + name := filepath.Base(path) + + r.mu.Lock() + defer r.mu.Unlock() + + // Check if directory exists + node, ok := r.nodes[path] + if !ok { + return -fuse.ENOENT + } + if !node.isDir { + return -fuse.ENOTDIR + } + + // Check if directory is empty + if len(node.children) > 0 { + return -fuse.ENOTEMPTY + } + + // Remove from parent + if parent, ok := r.nodes[dir]; ok { + delete(parent.children, name) + } + + // Remove from nodes map + delete(r.nodes, path) + + return 0 +} + +// Utimens updates file timestamps +func (r *NostrRoot) Utimens(path string, tmsp []fuse.Timespec) int { + node := r.getNode(path) + if node == nil { + return -fuse.ENOENT + } + + r.mu.Lock() + defer r.mu.Unlock() + + if len(tmsp) > 1 { + node.mtime = time.Unix(tmsp[1].Sec, int64(tmsp[1].Nsec)) + } + + return 0 +} diff --git a/nostrfs/viewdir.go b/nostrfs/viewdir.go deleted file mode 100644 index de3afba..0000000 --- a/nostrfs/viewdir.go +++ /dev/null @@ -1,267 +0,0 @@ -package nostrfs - -import ( - "context" - "strings" - "sync/atomic" - "syscall" - - "fiatjaf.com/lib/debouncer" - "fiatjaf.com/nostr" - "github.com/fatih/color" - "github.com/hanwen/go-fuse/v2/fs" - "github.com/hanwen/go-fuse/v2/fuse" -) - -type ViewDir struct { - fs.Inode - root *NostrRoot - fetched atomic.Bool - filter nostr.Filter - paginate bool - relays []string - replaceable bool - createable bool - publisher *debouncer.Debouncer - publishing struct { - note string - } -} - -var ( - _ = (fs.NodeOpendirer)((*ViewDir)(nil)) - _ = (fs.NodeGetattrer)((*ViewDir)(nil)) - _ = (fs.NodeMkdirer)((*ViewDir)(nil)) - _ = (fs.NodeSetattrer)((*ViewDir)(nil)) - _ = (fs.NodeCreater)((*ViewDir)(nil)) - _ = (fs.NodeUnlinker)((*ViewDir)(nil)) -) - -func (f *ViewDir) Setattr(_ context.Context, _ fs.FileHandle, _ *fuse.SetAttrIn, _ *fuse.AttrOut) syscall.Errno { - return fs.OK -} - -func (n *ViewDir) Create( - _ context.Context, - name string, - flags uint32, - mode uint32, - out *fuse.EntryOut, -) (node *fs.Inode, fh fs.FileHandle, fuseFlags uint32, errno syscall.Errno) { - if !n.createable || n.root.rootPubKey != n.filter.Authors[0] { - return nil, nil, 0, syscall.EPERM - } - if n.publisher == nil { - n.publisher = debouncer.New(n.root.opts.AutoPublishNotesTimeout) - } - if n.filter.Kinds[0] != 1 { - return nil, nil, 0, syscall.ENOTSUP - } - - switch name { - case "new": - log := n.root.ctx.Value("log").(func(msg string, args ...any)) - - if n.publisher.IsRunning() { - log("pending note updated, timer reset.") - } else { - log("new note detected") - if n.root.opts.AutoPublishNotesTimeout.Hours() < 24*365 { - log(", publishing it in %d seconds...\n", int(n.root.opts.AutoPublishNotesTimeout.Seconds())) - } else { - log(".\n") - } - log("- `touch publish` to publish immediately\n") - log("- `rm new` to erase and cancel the publication.\n") - } - - n.publisher.Call(n.publishNote) - - first := true - - return n.NewPersistentInode( - n.root.ctx, - n.root.NewWriteableFile(n.publishing.note, uint64(nostr.Now()), uint64(nostr.Now()), func(s string) { - if !first { - log("pending note updated, timer reset.\n") - } - first = false - n.publishing.note = strings.TrimSpace(s) - n.publisher.Call(n.publishNote) - }), - fs.StableAttr{}, - ), nil, 0, fs.OK - case "publish": - if n.publisher.IsRunning() { - // this causes the publish process to be triggered faster - log := n.root.ctx.Value("log").(func(msg string, args ...any)) - log("publishing now!\n") - n.publisher.Flush() - return nil, nil, 0, syscall.ENOTDIR - } - } - - return nil, nil, 0, syscall.ENOTSUP -} - -func (n *ViewDir) Unlink(ctx context.Context, name string) syscall.Errno { - if !n.createable || n.root.rootPubKey != n.filter.Authors[0] { - return syscall.EPERM - } - if n.publisher == nil { - n.publisher = debouncer.New(n.root.opts.AutoPublishNotesTimeout) - } - if n.filter.Kinds[0] != 1 { - return syscall.ENOTSUP - } - - switch name { - case "new": - log := n.root.ctx.Value("log").(func(msg string, args ...any)) - log("publishing canceled.\n") - n.publisher.Stop() - n.publishing.note = "" - return fs.OK - } - - return syscall.ENOTSUP -} - -func (n *ViewDir) publishNote() { - log := n.root.ctx.Value("log").(func(msg string, args ...any)) - - log("publishing note...\n") - evt := nostr.Event{ - Kind: 1, - CreatedAt: nostr.Now(), - Content: n.publishing.note, - Tags: make(nostr.Tags, 0, 2), - } - - // our write relays - 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) - } - - // massage and extract tags from raw text - targetRelays := n.root.sys.PrepareNoteEvent(n.root.ctx, &evt) - relays = nostr.AppendUnique(relays, targetRelays...) - - // sign and publish - if err := n.root.signer.SignEvent(n.root.ctx, &evt); err != nil { - log("failed to sign: %s\n", err) - return - } - log(evt.String() + "\n") - - log("publishing to %d relays... ", len(relays)) - success := false - first := true - for res := range n.root.sys.Pool.PublishMany(n.root.ctx, relays, evt) { - cleanUrl, _ := strings.CutPrefix(res.RelayURL, "wss://") - if !first { - log(", ") - } - first = false - - if res.Error != nil { - log("%s: %s", color.RedString(cleanUrl), res.Error) - } else { - success = true - log("%s: ok", color.GreenString(cleanUrl)) - } - } - log("\n") - - if success { - n.RmChild("new") - 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())) - } -} - -func (n *ViewDir) Getattr(_ context.Context, f fs.FileHandle, out *fuse.AttrOut) syscall.Errno { - now := nostr.Now() - if n.filter.Until != 0 { - now = n.filter.Until - } - aMonthAgo := now - 30*24*60*60 - out.Mtime = uint64(aMonthAgo) - - return fs.OK -} - -func (n *ViewDir) Opendir(ctx context.Context) syscall.Errno { - if n.fetched.CompareAndSwap(true, true) { - return fs.OK - } - - if n.paginate { - now := nostr.Now() - if n.filter.Until != 0 { - now = n.filter.Until - } - aMonthAgo := now - 30*24*60*60 - n.filter.Since = aMonthAgo - - filter := n.filter - filter.Until = aMonthAgo - - n.AddChild("@previous", n.NewPersistentInode( - n.root.ctx, - &ViewDir{ - root: n.root, - filter: filter, - relays: n.relays, - replaceable: n.replaceable, - }, - fs.StableAttr{Mode: syscall.S_IFDIR}, - ), true) - } - - if n.replaceable { - 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) - } - } - } else { - for ie := range n.root.sys.Pool.FetchMany(n.root.ctx, n.relays, n.filter, - 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) - } - } - } - - return fs.OK -} - -func (n *ViewDir) Mkdir(ctx context.Context, name string, mode uint32, out *fuse.EntryOut) (*fs.Inode, syscall.Errno) { - if !n.createable || n.root.signer == nil || n.root.rootPubKey != n.filter.Authors[0] { - return nil, syscall.ENOTSUP - } - - if n.replaceable { - // create a template event that can later be modified and published as new - return n.root.CreateEntityDir(n, &nostr.Event{ - PubKey: n.root.rootPubKey, - CreatedAt: 0, - Kind: n.filter.Kinds[0], - Tags: nostr.Tags{ - nostr.Tag{"d", name}, - }, - }), fs.OK - } - - return nil, syscall.ENOTSUP -} diff --git a/nostrfs/writeablefile.go b/nostrfs/writeablefile.go deleted file mode 100644 index b6ca0a9..0000000 --- a/nostrfs/writeablefile.go +++ /dev/null @@ -1,93 +0,0 @@ -package nostrfs - -import ( - "context" - "sync" - "syscall" - - "github.com/hanwen/go-fuse/v2/fs" - "github.com/hanwen/go-fuse/v2/fuse" -) - -type WriteableFile struct { - fs.Inode - root *NostrRoot - mu sync.Mutex - data []byte - attr fuse.Attr - onWrite func(string) -} - -var ( - _ = (fs.NodeOpener)((*WriteableFile)(nil)) - _ = (fs.NodeReader)((*WriteableFile)(nil)) - _ = (fs.NodeWriter)((*WriteableFile)(nil)) - _ = (fs.NodeGetattrer)((*WriteableFile)(nil)) - _ = (fs.NodeSetattrer)((*WriteableFile)(nil)) - _ = (fs.NodeFlusher)((*WriteableFile)(nil)) -) - -func (r *NostrRoot) NewWriteableFile(data string, ctime, mtime uint64, onWrite func(string)) *WriteableFile { - return &WriteableFile{ - root: r, - data: []byte(data), - attr: fuse.Attr{ - Mode: 0666, - Ctime: ctime, - Mtime: mtime, - Size: uint64(len(data)), - }, - onWrite: onWrite, - } -} - -func (f *WriteableFile) Open(ctx context.Context, flags uint32) (fh fs.FileHandle, fuseFlags uint32, errno syscall.Errno) { - return nil, fuse.FOPEN_KEEP_CACHE, fs.OK -} - -func (f *WriteableFile) Write(ctx context.Context, fh fs.FileHandle, data []byte, off int64) (uint32, syscall.Errno) { - f.mu.Lock() - defer f.mu.Unlock() - - offset := int(off) - end := offset + len(data) - if len(f.data) < end { - newData := make([]byte, offset+len(data)) - copy(newData, f.data) - f.data = newData - } - copy(f.data[offset:], data) - f.data = f.data[0:end] - - f.onWrite(string(f.data)) - return uint32(len(data)), fs.OK -} - -func (f *WriteableFile) Getattr(ctx context.Context, fh fs.FileHandle, out *fuse.AttrOut) syscall.Errno { - f.mu.Lock() - defer f.mu.Unlock() - out.Attr = f.attr - out.Attr.Size = uint64(len(f.data)) - return fs.OK -} - -func (f *WriteableFile) Setattr(_ context.Context, _ fs.FileHandle, in *fuse.SetAttrIn, _ *fuse.AttrOut) syscall.Errno { - f.attr.Mtime = in.Mtime - f.attr.Atime = in.Atime - f.attr.Ctime = in.Ctime - return fs.OK -} - -func (f *WriteableFile) Flush(ctx context.Context, fh fs.FileHandle) syscall.Errno { - return fs.OK -} - -func (f *WriteableFile) Read(ctx context.Context, fh fs.FileHandle, dest []byte, off int64) (fuse.ReadResult, syscall.Errno) { - f.mu.Lock() - defer f.mu.Unlock() - end := int(off) + len(dest) - if end > len(f.data) { - end = len(f.data) - } - return fuse.ReadResultData(f.data[off:end]), fs.OK -} From 0e283368ed1a1c0bafd43dc880442753df66e9a1 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sun, 11 Jan 2026 17:43:54 -0300 Subject: [PATCH 395/401] bunker: authorize preexisting keys first. --- bunker.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/bunker.go b/bunker.go index 1a5b2bd..fbab99b 100644 --- a/bunker.go +++ b/bunker.go @@ -329,6 +329,10 @@ var bunker = &cli.Command{ // asking user for authorization signer.AuthorizeRequest = func(harmless bool, from nostr.PubKey, secret string) bool { + if slices.Contains(config.AuthorizedKeys, from) || slices.Contains(authorizedSecrets, secret) { + return true + } + if secret == newSecret { // store this key config.AuthorizedKeys = appendUnique(config.AuthorizedKeys, from) @@ -343,9 +347,11 @@ var bunker = &cli.Command{ if persist != nil { persist() } + + return true } - return slices.Contains(config.AuthorizedKeys, from) || slices.Contains(authorizedSecrets, secret) + return false } for ie := range events { From 6dfbed4413f84f8b5f8e787e9ae448037f140ee6 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Fri, 16 Jan 2026 12:18:32 -0300 Subject: [PATCH 396/401] fs: just some renames. --- nostrfs/helpers.go | 16 --- nostrfs/root.go | 283 +++++++++++++++++++++++---------------------- 2 files changed, 147 insertions(+), 152 deletions(-) delete mode 100644 nostrfs/helpers.go diff --git a/nostrfs/helpers.go b/nostrfs/helpers.go deleted file mode 100644 index 79562b2..0000000 --- a/nostrfs/helpers.go +++ /dev/null @@ -1,16 +0,0 @@ -package nostrfs - -import ( - "fiatjaf.com/nostr" -) - -func kindToExtension(kind nostr.Kind) string { - switch kind { - case 30023: - return "md" - case 30818: - return "adoc" - default: - return "txt" - } -} diff --git a/nostrfs/root.go b/nostrfs/root.go index c59d401..a76f426 100644 --- a/nostrfs/root.go +++ b/nostrfs/root.go @@ -17,27 +17,27 @@ import ( "github.com/winfsp/cgofuse/fuse" ) -type Options struct { +type FSOptions struct { AutoPublishNotesTimeout time.Duration AutoPublishArticlesTimeout time.Duration } -type NostrRoot struct { +type FSRoot struct { fuse.FileSystemBase ctx context.Context sys *sdk.System rootPubKey nostr.PubKey signer nostr.Signer - opts Options + opts FSOptions mountpoint string mu sync.RWMutex - nodes map[string]*Node // path -> node + nodes map[string]*FSNode // path -> node nextIno uint64 pendingNotes map[string]*time.Timer // path -> auto-publish timer } -type Node struct { +type FSNode struct { ino uint64 path string name string @@ -46,14 +46,14 @@ type Node struct { mode uint32 mtime time.Time data []byte - children map[string]*Node + children map[string]*FSNode loadFunc func() ([]byte, error) // for lazy loading loaded bool } -var _ fuse.FileSystemInterface = (*NostrRoot)(nil) +var _ fuse.FileSystemInterface = (*FSRoot)(nil) -func NewNostrRoot(ctx context.Context, sys interface{}, user interface{}, mountpoint string, o Options) *NostrRoot { +func NewFSRoot(ctx context.Context, sys interface{}, user interface{}, mountpoint string, o FSOptions) *FSRoot { var system *sdk.System if sys != nil { system = sys.(*sdk.System) @@ -71,37 +71,37 @@ func NewNostrRoot(ctx context.Context, sys interface{}, user interface{}, mountp abs, _ := filepath.Abs(mountpoint) - root := &NostrRoot{ + root := &FSRoot{ ctx: ctx, sys: system, rootPubKey: pubkey, signer: signer, opts: o, mountpoint: abs, - nodes: make(map[string]*Node), + nodes: make(map[string]*FSNode), nextIno: 2, // 1 is reserved for root pendingNotes: make(map[string]*time.Timer), } - // Initialize root directory - rootNode := &Node{ + // initialize root directory + rootNode := &FSNode{ ino: 1, path: "/", name: "", isDir: true, mode: fuse.S_IFDIR | 0755, mtime: time.Now(), - children: make(map[string]*Node), + children: make(map[string]*FSNode), } root.nodes["/"] = rootNode - // Start async initialization + // start async initialization go root.initialize() return root } -func (r *NostrRoot) initialize() { +func (r *FSRoot) initialize() { if r.rootPubKey == nostr.ZeroPK { return } @@ -109,14 +109,14 @@ func (r *NostrRoot) initialize() { log := r.getLog() time.Sleep(time.Millisecond * 100) - // Fetch follow list + // fetch follow list fl := r.sys.FetchFollowList(r.ctx, r.rootPubKey) log("- fetched %d contacts\n", len(fl.Items)) r.mu.Lock() defer r.mu.Unlock() - // Add our contacts + // add our contacts for _, f := range fl.Items { npub := nip19.EncodeNpub(f.Pubkey) if _, exists := r.nodes["/"+npub]; !exists { @@ -124,14 +124,14 @@ func (r *NostrRoot) initialize() { } } - // Add ourselves + // add ourselves npub := nip19.EncodeNpub(r.rootPubKey) if _, exists := r.nodes["/"+npub]; !exists { r.createNpubDirLocked(npub, r.rootPubKey, r.signer) } - // Add @me symlink (for now, just create a text file pointing to our npub) - meNode := &Node{ + // add @me symlink (for now, just create a text file pointing to our npub) + meNode := &FSNode{ ino: r.nextIno, path: "/@me", name: "@me", @@ -146,19 +146,19 @@ func (r *NostrRoot) initialize() { r.nodes["/"].children["@me"] = meNode } -func (r *NostrRoot) fetchMetadata(dirPath string, pubkey nostr.PubKey) { +func (r *FSRoot) fetchMetadata(dirPath string, pubkey nostr.PubKey) { pm := r.sys.FetchProfileMetadata(r.ctx, pubkey) if pm.Event == nil { return } - // Use the content field which contains the actual profile JSON + // use the content field which contains the actual profile JSON metadataJ := []byte(pm.Event.Content) r.mu.Lock() defer r.mu.Unlock() - metadataNode := &Node{ + metadataNode := &FSNode{ ino: r.nextIno, path: dirPath + "/metadata.json", name: "metadata.json", @@ -175,13 +175,13 @@ func (r *NostrRoot) fetchMetadata(dirPath string, pubkey nostr.PubKey) { } } -func (r *NostrRoot) fetchProfilePicture(dirPath string, pubkey nostr.PubKey) { +func (r *FSRoot) fetchProfilePicture(dirPath string, pubkey nostr.PubKey) { pm := r.sys.FetchProfileMetadata(r.ctx, pubkey) if pm.Event == nil || pm.Picture == "" { return } - // Download picture + // download picture ctx, cancel := context.WithTimeout(r.ctx, time.Second*20) defer cancel() @@ -200,7 +200,7 @@ func (r *NostrRoot) fetchProfilePicture(dirPath string, pubkey nostr.PubKey) { return } - // Read image data + // read image data imageData := make([]byte, 0, 1024*1024) // 1MB initial capacity buf := make([]byte, 32*1024) for { @@ -220,7 +220,7 @@ func (r *NostrRoot) fetchProfilePicture(dirPath string, pubkey nostr.PubKey) { return } - // Detect file extension from content-type or URL + // detect file extension from content-type or URL ext := "png" if ct := resp.Header.Get("Content-Type"); ct != "" { switch ct { @@ -239,7 +239,7 @@ func (r *NostrRoot) fetchProfilePicture(dirPath string, pubkey nostr.PubKey) { defer r.mu.Unlock() picturePath := dirPath + "/picture." + ext - pictureNode := &Node{ + pictureNode := &FSNode{ ino: r.nextIno, path: picturePath, name: "picture." + ext, @@ -256,11 +256,11 @@ func (r *NostrRoot) fetchProfilePicture(dirPath string, pubkey nostr.PubKey) { } } -func (r *NostrRoot) fetchEvents(dirPath string, filter nostr.Filter) { +func (r *FSRoot) fetchEvents(dirPath string, filter nostr.Filter) { ctx, cancel := context.WithTimeout(r.ctx, time.Second*10) defer cancel() - // Get relays for authors + // get relays for authors var relays []string if len(filter.Authors) > 0 { relays = r.sys.FetchOutboxRelays(ctx, filter.Authors[0], 3) @@ -272,12 +272,12 @@ func (r *NostrRoot) fetchEvents(dirPath string, filter nostr.Filter) { log := r.getLog() log("- fetching events for %s from %v\n", dirPath, relays) - // Fetch events + // fetch events events := make([]*nostr.Event, 0) for ie := range r.sys.Pool.FetchMany(ctx, relays, filter, nostr.SubscriptionOptions{ Label: "nak-fs", }) { - // Make a copy to avoid pointer issues with loop variable + // make a copy to avoid pointer issues with loop variable evt := ie.Event events = append(events, &evt) if len(events) >= int(filter.Limit) { @@ -295,14 +295,14 @@ func (r *NostrRoot) fetchEvents(dirPath string, filter nostr.Filter) { return } - // Track oldest timestamp for pagination + // track oldest timestamp for pagination var oldestTimestamp nostr.Timestamp if len(events) > 0 { oldestTimestamp = events[len(events)-1].CreatedAt } for _, evt := range events { - // Create filename based on event + // create filename based on event filename := r.eventToFilename(evt) filePath := dirPath + "/" + filename @@ -315,7 +315,7 @@ func (r *NostrRoot) fetchEvents(dirPath string, filter nostr.Filter) { content = "(empty)" } - fileNode := &Node{ + fileNode := &FSNode{ ino: r.nextIno, path: filePath, name: filename, @@ -330,9 +330,9 @@ func (r *NostrRoot) fetchEvents(dirPath string, filter nostr.Filter) { dir.children[filename] = fileNode } - // Add "more" file for pagination if we got a full page + // add "more" file for pagination if we got a full page if len(events) >= int(filter.Limit) { - moreFile := &Node{ + moreFile := &FSNode{ ino: r.nextIno, path: dirPath + "/.more", name: ".more", @@ -342,7 +342,7 @@ func (r *NostrRoot) fetchEvents(dirPath string, filter nostr.Filter) { data: []byte(fmt.Sprintf("Read this file to load more events (until: %d)\n", oldestTimestamp)), size: int64(len(fmt.Sprintf("Read this file to load more events (until: %d)\n", oldestTimestamp))), loadFunc: func() ([]byte, error) { - // When .more is read, fetch next page + // when .more is read, fetch next page newFilter := filter newFilter.Until = oldestTimestamp go r.fetchEvents(dirPath, newFilter) @@ -355,24 +355,24 @@ func (r *NostrRoot) fetchEvents(dirPath string, filter nostr.Filter) { } } -func (r *NostrRoot) eventToFilename(evt *nostr.Event) string { - // Use event ID first 8 chars + extension based on kind +func (r *FSRoot) eventToFilename(evt *nostr.Event) string { + // use event ID first 8 chars + extension based on kind ext := kindToExtension(evt.Kind) - // Get hex representation of event ID + // get hex representation of event ID // evt.ID.String() may return format like ":1234abcd" so use Hex() or remove colons idHex := evt.ID.Hex() if len(idHex) > 8 { idHex = idHex[:8] } - // For articles, try to use title + // for articles, try to use title if evt.Kind == 30023 || evt.Kind == 30818 { for _, tag := range evt.Tags { if len(tag) >= 2 && tag[0] == "title" { titleStr := tag[1] if titleStr != "" { - // Sanitize title for filename + // sanitize title for filename name := strings.Map(func(r rune) rune { if r == '/' || r == '\\' || r == ':' || r == '*' || r == '?' || r == '"' || r == '<' || r == '>' || r == '|' { return '-' @@ -391,35 +391,35 @@ func (r *NostrRoot) eventToFilename(evt *nostr.Event) string { return fmt.Sprintf("%s.%s", idHex, ext) } -func (r *NostrRoot) getLog() func(string, ...interface{}) { +func (r *FSRoot) getLog() func(string, ...interface{}) { if log := r.ctx.Value("log"); log != nil { return log.(func(string, ...interface{})) } return func(string, ...interface{}) {} } -func (r *NostrRoot) getNode(path string) *Node { +func (r *FSRoot) getNode(path string) *FSNode { originalPath := path - // Normalize path + // normalize path if path == "" { path = "/" } - // Convert Windows backslashes to forward slashes + // convert Windows backslashes to forward slashes path = strings.ReplaceAll(path, "\\", "/") - // Ensure path starts with / + // ensure path starts with / if !strings.HasPrefix(path, "/") { path = "/" + path } - // Remove trailing slash except for root + // remove trailing slash except for root if path != "/" && strings.HasSuffix(path, "/") { path = strings.TrimSuffix(path, "/") } - // Debug logging + // debug logging if r.ctx.Value("logverbose") != nil { logv := r.ctx.Value("logverbose").(func(string, ...interface{})) logv("getNode: original='%s' normalized='%s'\n", originalPath, path) @@ -430,7 +430,7 @@ func (r *NostrRoot) getNode(path string) *Node { node := r.nodes[path] - // Debug: if not found, show similar paths + // debug: if not found, show similar paths if node == nil && r.ctx.Value("logverbose") != nil { logv := r.ctx.Value("logverbose").(func(string, ...interface{})) logv("getNode: NOT FOUND '%s'\n", path) @@ -451,11 +451,11 @@ func (r *NostrRoot) getNode(path string) *Node { return node } -func (r *NostrRoot) Getattr(path string, stat *fuse.Stat_t, fh uint64) int { +func (r *FSRoot) Getattr(path string, stat *fuse.Stat_t, fh uint64) int { node := r.getNode(path) - // If node doesn't exist, try dynamic lookup - // But skip for special files starting with @ or . + // if node doesn't exist, try dynamic lookup + // but skip for special files starting with @ or . if node == nil { basename := filepath.Base(path) if !strings.HasPrefix(basename, "@") && !strings.HasPrefix(basename, ".") { @@ -480,14 +480,14 @@ func (r *NostrRoot) Getattr(path string, stat *fuse.Stat_t, fh uint64) int { } // dynamicLookup tries to create nodes on-demand for npub/note/nevent paths -func (r *NostrRoot) dynamicLookup(path string) bool { - // Normalize path +func (r *FSRoot) dynamicLookup(path string) bool { + // normalize path path = strings.ReplaceAll(path, "\\", "/") if !strings.HasPrefix(path, "/") { path = "/" + path } - // Get the first component after root + // get the first component after root parts := strings.Split(strings.TrimPrefix(path, "/"), "/") if len(parts) == 0 { return false @@ -495,10 +495,10 @@ func (r *NostrRoot) dynamicLookup(path string) bool { name := parts[0] - // Try to decode as nostr pointer + // try to decode as nostr pointer pointer, err := nip19.ToPointer(name) if err != nil { - // Try NIP-05 + // try NIP-05 if strings.Contains(name, "@") && !strings.HasPrefix(name, "@") { ctx, cancel := context.WithTimeout(r.ctx, time.Second*5) defer cancel() @@ -515,19 +515,19 @@ func (r *NostrRoot) dynamicLookup(path string) bool { r.mu.Lock() defer r.mu.Unlock() - // Check if already exists + // check if already exists if _, exists := r.nodes["/"+name]; exists { return true } switch p := pointer.(type) { case nostr.ProfilePointer: - // Create npub directory dynamically + // create npub directory dynamically r.createNpubDirLocked(name, p.PublicKey, nil) return true case nostr.EventPointer: - // Create event directory dynamically + // create event directory dynamically return r.createEventDirLocked(name, p) default: @@ -535,30 +535,30 @@ func (r *NostrRoot) dynamicLookup(path string) bool { } } -func (r *NostrRoot) createNpubDirLocked(npub string, pubkey nostr.PubKey, signer nostr.Signer) { +func (r *FSRoot) createNpubDirLocked(npub string, pubkey nostr.PubKey, signer nostr.Signer) { dirPath := "/" + npub - // Check if already exists + // check if already exists if _, exists := r.nodes[dirPath]; exists { return } - dirNode := &Node{ + dirNode := &FSNode{ ino: r.nextIno, path: dirPath, name: npub, isDir: true, mode: fuse.S_IFDIR | 0755, mtime: time.Now(), - children: make(map[string]*Node), + children: make(map[string]*FSNode), } r.nextIno++ r.nodes[dirPath] = dirNode r.nodes["/"].children[npub] = dirNode - // Add pubkey file + // add pubkey file pubkeyData := []byte(pubkey.Hex() + "\n") - pubkeyNode := &Node{ + pubkeyNode := &FSNode{ ino: r.nextIno, path: dirPath + "/pubkey", name: "pubkey", @@ -572,78 +572,78 @@ func (r *NostrRoot) createNpubDirLocked(npub string, pubkey nostr.PubKey, signer r.nodes[dirPath+"/pubkey"] = pubkeyNode dirNode.children["pubkey"] = pubkeyNode - // Fetch metadata asynchronously + // fetch metadata asynchronously go r.fetchMetadata(dirPath, pubkey) - // Add notes directory + // add notes directory r.createViewDirLocked(dirPath, "notes", nostr.Filter{ Kinds: []nostr.Kind{1}, Authors: []nostr.PubKey{pubkey}, Limit: 50, }) - // Add articles directory + // add articles directory r.createViewDirLocked(dirPath, "articles", nostr.Filter{ Kinds: []nostr.Kind{30023}, Authors: []nostr.PubKey{pubkey}, Limit: 50, }) - // Add comments directory + // add comments directory r.createViewDirLocked(dirPath, "comments", nostr.Filter{ Kinds: []nostr.Kind{1111}, Authors: []nostr.PubKey{pubkey}, Limit: 50, }) - // Add highlights directory + // add highlights directory r.createViewDirLocked(dirPath, "highlights", nostr.Filter{ Kinds: []nostr.Kind{9802}, Authors: []nostr.PubKey{pubkey}, Limit: 50, }) - // Add photos directory + // add photos directory r.createViewDirLocked(dirPath, "photos", nostr.Filter{ Kinds: []nostr.Kind{20}, Authors: []nostr.PubKey{pubkey}, Limit: 50, }) - // Add videos directory + // add videos directory r.createViewDirLocked(dirPath, "videos", nostr.Filter{ Kinds: []nostr.Kind{21, 22}, Authors: []nostr.PubKey{pubkey}, Limit: 50, }) - // Add wikis directory + // add wikis directory r.createViewDirLocked(dirPath, "wikis", nostr.Filter{ Kinds: []nostr.Kind{30818}, Authors: []nostr.PubKey{pubkey}, Limit: 50, }) - // Fetch profile picture asynchronously + // fetch profile picture asynchronously go r.fetchProfilePicture(dirPath, pubkey) } -func (r *NostrRoot) createViewDirLocked(parentPath, name string, filter nostr.Filter) { +func (r *FSRoot) createViewDirLocked(parentPath, name string, filter nostr.Filter) { dirPath := parentPath + "/" + name - // Check if already exists + // check if already exists if _, exists := r.nodes[dirPath]; exists { return } - dirNode := &Node{ + dirNode := &FSNode{ ino: r.nextIno, path: dirPath, name: name, isDir: true, mode: fuse.S_IFDIR | 0755, mtime: time.Now(), - children: make(map[string]*Node), + children: make(map[string]*FSNode), } r.nextIno++ @@ -652,14 +652,14 @@ func (r *NostrRoot) createViewDirLocked(parentPath, name string, filter nostr.Fi parent.children[name] = dirNode } - // Fetch events asynchronously + // fetch events asynchronously go r.fetchEvents(dirPath, filter) } -func (r *NostrRoot) createEventDirLocked(name string, pointer nostr.EventPointer) bool { +func (r *FSRoot) createEventDirLocked(name string, pointer nostr.EventPointer) bool { dirPath := "/" + name - // Fetch the event + // fetch the event ctx, cancel := context.WithTimeout(r.ctx, time.Second*10) defer cancel() @@ -676,7 +676,7 @@ func (r *NostrRoot) createEventDirLocked(name string, pointer nostr.EventPointer for ie := range r.sys.Pool.FetchMany(ctx, relays, filter, nostr.SubscriptionOptions{ Label: "nak-fs-event", }) { - // Make a copy to avoid pointer issues + // make a copy to avoid pointer issues evtCopy := ie.Event evt = &evtCopy break @@ -686,24 +686,24 @@ func (r *NostrRoot) createEventDirLocked(name string, pointer nostr.EventPointer return false } - // Create event directory - dirNode := &Node{ + // create event directory + dirNode := &FSNode{ ino: r.nextIno, path: dirPath, name: name, isDir: true, mode: fuse.S_IFDIR | 0755, mtime: time.Unix(int64(evt.CreatedAt), 0), - children: make(map[string]*Node), + children: make(map[string]*FSNode), } r.nextIno++ r.nodes[dirPath] = dirNode r.nodes["/"].children[name] = dirNode - // Add content file + // add content file ext := kindToExtension(evt.Kind) contentPath := dirPath + "/content." + ext - contentNode := &Node{ + contentNode := &FSNode{ ino: r.nextIno, path: contentPath, name: "content." + ext, @@ -717,10 +717,10 @@ func (r *NostrRoot) createEventDirLocked(name string, pointer nostr.EventPointer r.nodes[contentPath] = contentNode dirNode.children["content."+ext] = contentNode - // Add event.json + // add event.json eventJSON, _ := json.MarshalIndent(evt, "", " ") eventJSONPath := dirPath + "/event.json" - eventJSONNode := &Node{ + eventJSONNode := &FSNode{ ino: r.nextIno, path: eventJSONPath, name: "event.json", @@ -737,11 +737,11 @@ func (r *NostrRoot) createEventDirLocked(name string, pointer nostr.EventPointer return true } -func (r *NostrRoot) Readdir(path string, +func (r *FSRoot) Readdir(path string, fill func(name string, stat *fuse.Stat_t, ofst int64) bool, ofst int64, - fh uint64) int { - + fh uint64, +) int { node := r.getNode(path) if node == nil || !node.isDir { return -fuse.ENOENT @@ -768,8 +768,8 @@ func (r *NostrRoot) Readdir(path string, return 0 } -func (r *NostrRoot) Open(path string, flags int) (int, uint64) { - // Log the open attempt +func (r *FSRoot) Open(path string, flags int) (int, uint64) { + // log the open attempt if r.ctx.Value("logverbose") != nil { logv := r.ctx.Value("logverbose").(func(string, ...interface{})) logv("Open: path='%s' flags=%d\n", path, flags) @@ -783,7 +783,7 @@ func (r *NostrRoot) Open(path string, flags int) (int, uint64) { return -fuse.EISDIR, ^uint64(0) } - // Load data if needed + // load data if needed if node.loadFunc != nil && !node.loaded { r.mu.Lock() if !node.loaded { @@ -799,7 +799,7 @@ func (r *NostrRoot) Open(path string, flags int) (int, uint64) { return 0, node.ino } -func (r *NostrRoot) Read(path string, buff []byte, ofst int64, fh uint64) int { +func (r *FSRoot) Read(path string, buff []byte, ofst int64, fh uint64) int { node := r.getNode(path) if node == nil || node.isDir { return -fuse.ENOENT @@ -818,7 +818,7 @@ func (r *NostrRoot) Read(path string, buff []byte, ofst int64, fh uint64) int { return n } -func (r *NostrRoot) Opendir(path string) (int, uint64) { +func (r *FSRoot) Opendir(path string) (int, uint64) { node := r.getNode(path) if node == nil { return -fuse.ENOENT, ^uint64(0) @@ -829,17 +829,17 @@ func (r *NostrRoot) Opendir(path string) (int, uint64) { return 0, node.ino } -func (r *NostrRoot) Release(path string, fh uint64) int { +func (r *FSRoot) Release(path string, fh uint64) int { return 0 } -func (r *NostrRoot) Releasedir(path string, fh uint64) int { +func (r *FSRoot) Releasedir(path string, fh uint64) int { return 0 } // Create creates a new file -func (r *NostrRoot) Create(path string, flags int, mode uint32) (int, uint64) { - // Parse path +func (r *FSRoot) Create(path string, flags int, mode uint32) (int, uint64) { + // parse path path = strings.ReplaceAll(path, "\\", "/") if !strings.HasPrefix(path, "/") { path = "/" + path @@ -851,19 +851,19 @@ func (r *NostrRoot) Create(path string, flags int, mode uint32) (int, uint64) { r.mu.Lock() defer r.mu.Unlock() - // Check if parent directory exists + // check if parent directory exists parent, ok := r.nodes[dir] if !ok || !parent.isDir { return -fuse.ENOENT, ^uint64(0) } - // Check if file already exists + // check if file already exists if _, exists := r.nodes[path]; exists { return -fuse.EEXIST, ^uint64(0) } - // Create new file node - fileNode := &Node{ + // create new file node + fileNode := &FSNode{ ino: r.nextIno, path: path, name: name, @@ -882,7 +882,7 @@ func (r *NostrRoot) Create(path string, flags int, mode uint32) (int, uint64) { } // Truncate truncates a file -func (r *NostrRoot) Truncate(path string, size int64, fh uint64) int { +func (r *FSRoot) Truncate(path string, size int64, fh uint64) int { node := r.getNode(path) if node == nil { return -fuse.ENOENT @@ -899,7 +899,7 @@ func (r *NostrRoot) Truncate(path string, size int64, fh uint64) int { } else if size < int64(len(node.data)) { node.data = node.data[:size] } else { - // Extend with zeros + // extend with zeros newData := make([]byte, size) copy(newData, node.data) node.data = newData @@ -911,7 +911,7 @@ func (r *NostrRoot) Truncate(path string, size int64, fh uint64) int { } // Write writes data to a file -func (r *NostrRoot) Write(path string, buff []byte, ofst int64, fh uint64) int { +func (r *FSRoot) Write(path string, buff []byte, ofst int64, fh uint64) int { node := r.getNode(path) if node == nil { return -fuse.ENOENT @@ -925,7 +925,7 @@ func (r *NostrRoot) Write(path string, buff []byte, ofst int64, fh uint64) int { endofst := ofst + int64(len(buff)) - // Extend data if necessary + // extend data if necessary if endofst > int64(len(node.data)) { newData := make([]byte, endofst) copy(newData, node.data) @@ -936,14 +936,14 @@ func (r *NostrRoot) Write(path string, buff []byte, ofst int64, fh uint64) int { node.size = int64(len(node.data)) node.mtime = time.Now() - // Check if this is a note that should be auto-published + // check if this is a note that should be auto-published if r.signer != nil && strings.Contains(path, "/notes/") && !strings.HasPrefix(filepath.Base(path), ".") { - // Cancel existing timer if any + // cancel existing timer if any if timer, exists := r.pendingNotes[path]; exists { timer.Stop() } - // Schedule auto-publish + // schedule auto-publish timeout := r.opts.AutoPublishNotesTimeout if timeout > 0 && timeout < time.Hour*24*365 { r.pendingNotes[path] = time.AfterFunc(timeout, func() { @@ -955,7 +955,7 @@ func (r *NostrRoot) Write(path string, buff []byte, ofst int64, fh uint64) int { return n } -func (r *NostrRoot) publishNote(path string) { +func (r *FSRoot) publishNote(path string) { r.mu.Lock() node, ok := r.nodes[path] if !ok { @@ -973,7 +973,7 @@ func (r *NostrRoot) publishNote(path string) { log := r.getLog() log("- auto-publishing note from %s\n", path) - // Create and sign event + // create and sign event evt := &nostr.Event{ CreatedAt: nostr.Now(), Kind: 1, @@ -986,7 +986,7 @@ func (r *NostrRoot) publishNote(path string) { return } - // Publish to relays + // publish to relays ctx, cancel := context.WithTimeout(r.ctx, time.Second*10) defer cancel() @@ -1005,7 +1005,7 @@ func (r *NostrRoot) publishNote(path string) { log("- published note %s to %d relays\n", evt.ID.Hex()[:8], len(relays)) - // Update filename to include event ID + // update filename to include event ID r.mu.Lock() defer r.mu.Unlock() @@ -1015,7 +1015,7 @@ func (r *NostrRoot) publishNote(path string) { newName := evt.ID.Hex()[:8] + ext newPath := dir + "/" + newName - // Rename node + // rename node if _, exists := r.nodes[newPath]; !exists { node.path = newPath node.name = newName @@ -1032,7 +1032,7 @@ func (r *NostrRoot) publishNote(path string) { } // Unlink deletes a file -func (r *NostrRoot) Unlink(path string) int { +func (r *FSRoot) Unlink(path string) int { path = strings.ReplaceAll(path, "\\", "/") if !strings.HasPrefix(path, "/") { path = "/" + path @@ -1044,7 +1044,7 @@ func (r *NostrRoot) Unlink(path string) int { r.mu.Lock() defer r.mu.Unlock() - // Check if file exists + // check if file exists node, ok := r.nodes[path] if !ok { return -fuse.ENOENT @@ -1053,19 +1053,19 @@ func (r *NostrRoot) Unlink(path string) int { return -fuse.EISDIR } - // Remove from parent + // remove from parent if parent, ok := r.nodes[dir]; ok { delete(parent.children, name) } - // Remove from nodes map + // remove from nodes map delete(r.nodes, path) return 0 } // Mkdir creates a new directory -func (r *NostrRoot) Mkdir(path string, mode uint32) int { +func (r *FSRoot) Mkdir(path string, mode uint32) int { path = strings.ReplaceAll(path, "\\", "/") if !strings.HasPrefix(path, "/") { path = "/" + path @@ -1077,26 +1077,26 @@ func (r *NostrRoot) Mkdir(path string, mode uint32) int { r.mu.Lock() defer r.mu.Unlock() - // Check if parent directory exists + // check if parent directory exists parent, ok := r.nodes[dir] if !ok || !parent.isDir { return -fuse.ENOENT } - // Check if directory already exists + // check if directory already exists if _, exists := r.nodes[path]; exists { return -fuse.EEXIST } - // Create new directory node - dirNode := &Node{ + // create new directory node + dirNode := &FSNode{ ino: r.nextIno, path: path, name: name, isDir: true, mode: fuse.S_IFDIR | 0755, mtime: time.Now(), - children: make(map[string]*Node), + children: make(map[string]*FSNode), } r.nextIno++ @@ -1107,7 +1107,7 @@ func (r *NostrRoot) Mkdir(path string, mode uint32) int { } // Rmdir removes a directory -func (r *NostrRoot) Rmdir(path string) int { +func (r *FSRoot) Rmdir(path string) int { path = strings.ReplaceAll(path, "\\", "/") if !strings.HasPrefix(path, "/") { path = "/" + path @@ -1123,7 +1123,7 @@ func (r *NostrRoot) Rmdir(path string) int { r.mu.Lock() defer r.mu.Unlock() - // Check if directory exists + // check if directory exists node, ok := r.nodes[path] if !ok { return -fuse.ENOENT @@ -1132,24 +1132,24 @@ func (r *NostrRoot) Rmdir(path string) int { return -fuse.ENOTDIR } - // Check if directory is empty + // check if directory is empty if len(node.children) > 0 { return -fuse.ENOTEMPTY } - // Remove from parent + // remove from parent if parent, ok := r.nodes[dir]; ok { delete(parent.children, name) } - // Remove from nodes map + // remove from nodes map delete(r.nodes, path) return 0 } // Utimens updates file timestamps -func (r *NostrRoot) Utimens(path string, tmsp []fuse.Timespec) int { +func (r *FSRoot) Utimens(path string, tmsp []fuse.Timespec) int { node := r.getNode(path) if node == nil { return -fuse.ENOENT @@ -1164,3 +1164,14 @@ func (r *NostrRoot) Utimens(path string, tmsp []fuse.Timespec) int { return 0 } + +func kindToExtension(kind nostr.Kind) string { + switch kind { + case 30023: + return "md" + case 30818: + return "djot" + default: + return "txt" + } +} From e838de9b72bf83bc8b4826032d0fbf6dc27cc9fe Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Fri, 16 Jan 2026 12:34:09 -0300 Subject: [PATCH 397/401] fs: move everything to the top-level directory. --- fs.go | 9 ++++----- fs_other.go | 2 +- nostrfs/root.go => fs_root.go | 6 +++--- fs_windows.go | 26 ++++++++++++-------------- 4 files changed, 20 insertions(+), 23 deletions(-) rename nostrfs/root.go => fs_root.go (99%) diff --git a/fs.go b/fs.go index 1f7f30c..ba80089 100644 --- a/fs.go +++ b/fs.go @@ -13,7 +13,6 @@ import ( "fiatjaf.com/nostr" "fiatjaf.com/nostr/keyer" "github.com/fatih/color" - "github.com/fiatjaf/nak/nostrfs" "github.com/urfave/cli/v3" "github.com/winfsp/cgofuse/fuse" ) @@ -63,7 +62,7 @@ var fsCmd = &cli.Command{ apat = time.Hour * 24 * 365 * 3 } - root := nostrfs.NewNostrRoot( + root := NewFSRoot( context.WithValue( context.WithValue( ctx, @@ -74,7 +73,7 @@ var fsCmd = &cli.Command{ sys, kr, mountpoint, - nostrfs.Options{ + FSOptions{ AutoPublishNotesTimeout: apnt, AutoPublishArticlesTimeout: apat, }, @@ -83,12 +82,12 @@ var fsCmd = &cli.Command{ // create the server log("- mounting at %s... ", color.HiCyanString(mountpoint)) - // Create cgofuse host + // create cgofuse host host := fuse.NewFileSystemHost(root) host.SetCapReaddirPlus(true) host.SetUseIno(true) - // Mount the filesystem + // mount the filesystem mountArgs := []string{"-s", mountpoint} if isVerbose { mountArgs = append([]string{"-d"}, mountArgs...) diff --git a/fs_other.go b/fs_other.go index ccc2894..fe9c244 100644 --- a/fs_other.go +++ b/fs_other.go @@ -15,6 +15,6 @@ var fsCmd = &cli.Command{ Description: `doesn't work on Windows and OpenBSD.`, DisableSliceFlagSeparator: true, Action: func(ctx context.Context, c *cli.Command) error { - return fmt.Errorf("this doesn't work on Windows and OpenBSD.") + return fmt.Errorf("this doesn't work on OpenBSD.") }, } diff --git a/nostrfs/root.go b/fs_root.go similarity index 99% rename from nostrfs/root.go rename to fs_root.go index a76f426..f61ee94 100644 --- a/nostrfs/root.go +++ b/fs_root.go @@ -1,8 +1,8 @@ -package nostrfs +package main import ( "context" - "encoding/json" + stdjson "encoding/json" "fmt" "net/http" "path/filepath" @@ -718,7 +718,7 @@ func (r *FSRoot) createEventDirLocked(name string, pointer nostr.EventPointer) b dirNode.children["content."+ext] = contentNode // add event.json - eventJSON, _ := json.MarshalIndent(evt, "", " ") + eventJSON, _ := stdjson.MarshalIndent(evt, "", " ") eventJSONPath := dirPath + "/event.json" eventJSONNode := &FSNode{ ino: r.nextIno, diff --git a/fs_windows.go b/fs_windows.go index 8dfcc15..d1dc1a3 100644 --- a/fs_windows.go +++ b/fs_windows.go @@ -12,7 +12,6 @@ import ( "fiatjaf.com/nostr" "fiatjaf.com/nostr/keyer" "github.com/fatih/color" - "github.com/fiatjaf/nak/nostrfs" "github.com/urfave/cli/v3" "github.com/winfsp/cgofuse/fuse" ) @@ -62,7 +61,7 @@ var fsCmd = &cli.Command{ apat = time.Hour * 24 * 365 * 3 } - root := nostrfs.NewNostrRoot( + root := NewFSRoot( context.WithValue( context.WithValue( ctx, @@ -73,7 +72,7 @@ var fsCmd = &cli.Command{ sys, kr, mountpoint, - nostrfs.Options{ + FSOptions{ AutoPublishNotesTimeout: apnt, AutoPublishArticlesTimeout: apat, }, @@ -82,37 +81,37 @@ var fsCmd = &cli.Command{ // create the server log("- mounting at %s... ", color.HiCyanString(mountpoint)) - // Create cgofuse host + // create cgofuse host host := fuse.NewFileSystemHost(root) host.SetCapReaddirPlus(true) host.SetUseIno(true) - // Mount the filesystem - Windows/WinFsp version - // Based on rclone cmount implementation + // mount the filesystem - Windows/WinFsp version + // based on rclone cmount implementation mountArgs := []string{ "-o", "uid=-1", "-o", "gid=-1", "--FileSystemName=nak", } - // Check if mountpoint is a drive letter or directory + // check if mountpoint is a drive letter or directory isDriveLetter := len(mountpoint) == 2 && mountpoint[1] == ':' if !isDriveLetter { - // WinFsp primarily supports drive letters on Windows - // Directory mounting may not work reliably + // winFsp primarily supports drive letters on Windows + // directory mounting may not work reliably log("WARNING: directory mounting may not work on Windows (WinFsp limitation)\n") log(" consider using a drive letter instead (e.g., 'nak fs Z:')\n") - // For directory mounts, follow rclone's approach: - // 1. Check that mountpoint doesn't already exist + // for directory mounts, follow rclone's approach: + // 1. check that mountpoint doesn't already exist if _, err := os.Stat(mountpoint); err == nil { return fmt.Errorf("mountpoint path already exists: %s (must not exist before mounting)", mountpoint) } else if !os.IsNotExist(err) { return fmt.Errorf("failed to check mountpoint: %w", err) } - // 2. Check that parent directory exists + // 2. check that parent directory exists parent := filepath.Join(mountpoint, "..") if _, err := os.Stat(parent); err != nil { if os.IsNotExist(err) { @@ -121,7 +120,7 @@ var fsCmd = &cli.Command{ return fmt.Errorf("failed to check parent directory: %w", err) } - // 3. Use network mode for directory mounts + // 3. use network mode for directory mounts mountArgs = append(mountArgs, "--VolumePrefix=\\nak\\"+filepath.Base(mountpoint)) } @@ -132,7 +131,6 @@ var fsCmd = &cli.Command{ log("ok.\n") - // Mount in main thread like hellofs if !host.Mount("", mountArgs) { return fmt.Errorf("failed to mount filesystem") } From 00fbda9af71addee71a2b8f48629e757d8673170 Mon Sep 17 00:00:00 2001 From: mattn Date: Sat, 17 Jan 2026 01:43:19 +0900 Subject: [PATCH 398/401] use native runner and install macfuse --- .github/workflows/release-cli.yml | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml index 620debb..49a20a8 100644 --- a/.github/workflows/release-cli.yml +++ b/.github/workflows/release-cli.yml @@ -24,15 +24,13 @@ jobs: - make-release strategy: matrix: - goos: [linux, freebsd, darwin, windows] + goos: [linux, freebsd, windows] goarch: [amd64, arm64, riscv64] exclude: - goarch: arm64 goos: windows - goarch: riscv64 goos: windows - - goarch: riscv64 - goos: darwin - goarch: arm64 goos: freebsd steps: @@ -42,6 +40,28 @@ jobs: github_token: ${{ secrets.GITHUB_TOKEN }} goos: ${{ matrix.goos }} goarch: ${{ matrix.goarch }} + ldflags: -X main.version=${{ github. ref_name }} + overwrite: true + md5sum: false + sha256sum: false + compress_assets: false + + build-darwin: + runs-on: macos-latest + needs: + - make-release + strategy: + matrix: + goarch: [amd64, arm64] + steps: + - uses: actions/checkout@v3 + - name: Install macFUSE + run: brew install --cask macfuse + - uses: wangyoucao577/go-release-action@v1.40 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + goos: darwin + goarch: ${{ matrix.goarch }} ldflags: -X main.version=${{ github.ref_name }} overwrite: true md5sum: false From acd6227dd06a7efac2843f5cc8da4b6e350d92d9 Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Sat, 17 Jan 2026 02:47:34 +0900 Subject: [PATCH 399/401] fix darwin build --- .github/workflows/release-cli.yml | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml index 49a20a8..715af36 100644 --- a/.github/workflows/release-cli.yml +++ b/.github/workflows/release-cli.yml @@ -55,18 +55,27 @@ jobs: goarch: [amd64, arm64] steps: - uses: actions/checkout@v3 + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: 'stable' - name: Install macFUSE run: brew install --cask macfuse - - uses: wangyoucao577/go-release-action@v1.40 + - name: Build binary + env: + GOOS: darwin + GOARCH: ${{ matrix.goarch }} + run: | + go build -ldflags "-X main.version=${{ github.ref_name }}" -o nak-${{ github.ref_name }}-darwin-${{ matrix.goarch }} + - name: Upload Release Asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets. GITHUB_TOKEN }} with: - github_token: ${{ secrets.GITHUB_TOKEN }} - goos: darwin - goarch: ${{ matrix.goarch }} - ldflags: -X main.version=${{ github.ref_name }} - overwrite: true - md5sum: false - sha256sum: false - compress_assets: false + upload_url: ${{ needs.make-release.outputs.upload_url }} + asset_path: ./nak-${{ github.ref_name }}-darwin-${{ matrix.goarch }} + asset_name: nak-${{ github.ref_name }}-darwin-${{ matrix.goarch }} + asset_content_type: application/octet-stream smoke-test-linux-amd64: runs-on: ubuntu-latest needs: From c6da13649de02f94cfbe67ba6f564d54d1d27d4a Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Fri, 16 Jan 2026 16:08:29 -0300 Subject: [PATCH 400/401] hopefully eliminate the weird case of cron and githubactions calling nak with an empty stdin and causing it to do nothing. closes https://github.com/fiatjaf/nak/issues/90 --- event.go | 1 + helpers.go | 12 +++++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/event.go b/event.go index 1f351ea..2a3a2b7 100644 --- a/event.go +++ b/event.go @@ -155,6 +155,7 @@ example: os.Exit(3) } } + kr, sec, err := gatherKeyerFromArguments(ctx, c) if err != nil { return err diff --git a/helpers.go b/helpers.go index cda3e96..6ae6e4f 100644 --- a/helpers.go +++ b/helpers.go @@ -46,8 +46,14 @@ var ( ) func isPiped() bool { - stat, _ := os.Stdin.Stat() - return stat.Mode()&os.ModeCharDevice == 0 + stat, err := os.Stdin.Stat() + if err != nil { + panic(err) + } + + mode := stat.Mode() + is := mode&os.ModeCharDevice == 0 + return is } func getJsonsOrBlank() iter.Seq[string] { @@ -76,7 +82,7 @@ func getJsonsOrBlank() iter.Seq[string] { return true }) - if !hasStdin && !isPiped() { + if !hasStdin { yield("{}") } From 120a92920efd5ba85293f85100d2efe32c9dab35 Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Sat, 17 Jan 2026 15:40:45 +0900 Subject: [PATCH 401/401] switch to softprops/action-gh-release --- .github/workflows/release-cli.yml | 29 ++++++----------------------- 1 file changed, 6 insertions(+), 23 deletions(-) diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml index 715af36..3d581b8 100644 --- a/.github/workflows/release-cli.yml +++ b/.github/workflows/release-cli.yml @@ -9,19 +9,8 @@ permissions: contents: write jobs: - make-release: - runs-on: ubuntu-latest - steps: - - uses: actions/create-release@latest - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag_name: ${{ github.ref }} - release_name: ${{ github.ref }} build-all-for-all: runs-on: ubuntu-latest - needs: - - make-release strategy: matrix: goos: [linux, freebsd, windows] @@ -40,16 +29,14 @@ jobs: github_token: ${{ secrets.GITHUB_TOKEN }} goos: ${{ matrix.goos }} goarch: ${{ matrix.goarch }} - ldflags: -X main.version=${{ github. ref_name }} + ldflags: -X main.version=${{ github.ref_name }} overwrite: true - md5sum: false + md5sum: false sha256sum: false compress_assets: false build-darwin: runs-on: macos-latest - needs: - - make-release strategy: matrix: goarch: [amd64, arm64] @@ -68,14 +55,10 @@ jobs: run: | go build -ldflags "-X main.version=${{ github.ref_name }}" -o nak-${{ github.ref_name }}-darwin-${{ matrix.goarch }} - name: Upload Release Asset - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets. GITHUB_TOKEN }} + uses: softprops/action-gh-release@v1 with: - upload_url: ${{ needs.make-release.outputs.upload_url }} - asset_path: ./nak-${{ github.ref_name }}-darwin-${{ matrix.goarch }} - asset_name: nak-${{ github.ref_name }}-darwin-${{ matrix.goarch }} - asset_content_type: application/octet-stream + files: ./nak-${{ github.ref_name }}-darwin-${{ matrix.goarch }} + smoke-test-linux-amd64: runs-on: ubuntu-latest needs: @@ -131,7 +114,7 @@ jobs: # test NIP-49 key encryption/decryption echo "testing NIP-49 key encryption/decryption..." ENCRYPTED_KEY=$(./nak key encrypt $SECRET_KEY "testpassword") - echo "encrypted key: ${ENCRYPTED_KEY:0:20}..." + echo "encrypted key: ${ENCRYPTED_KEY: 0:20}..." DECRYPTED_KEY=$(./nak key decrypt $ENCRYPTED_KEY "testpassword") if [ "$DECRYPTED_KEY" != "$SECRET_KEY" ]; then echo "nip-49 encryption/decryption test failed!"