mirror of
https://github.com/fiatjaf/nak.git
synced 2025-12-08 16:48:51 +00:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6daf0f4e5c | ||
|
|
bfeaf0710f | ||
|
|
e45b54ea62 | ||
|
|
35da063c30 | ||
|
|
15aefe3df4 | ||
|
|
55fd631787 | ||
|
|
6f48c29d0f | ||
|
|
703c186958 | ||
|
|
7ae2e686cb | ||
|
|
9547711e8d | ||
|
|
50119e21e6 | ||
|
|
33f4272dd0 | ||
|
|
7b6f387aad |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
nak
|
||||
mnt
|
||||
nak.exe
|
||||
|
||||
33
README.md
33
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`.
|
||||
|
||||
@@ -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, c, relayUrls, nil)
|
||||
if len(relays) == 0 {
|
||||
log("failed to connect to any of the given relays.\n")
|
||||
os.Exit(3)
|
||||
|
||||
7
count.go
7
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, c, relayUrls, nil)
|
||||
if len(relays) == 0 {
|
||||
log("failed to connect to any of the given relays.\n")
|
||||
os.Exit(3)
|
||||
|
||||
12
dvm.go
12
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, false)
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
141
event.go
141
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, false)
|
||||
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)
|
||||
@@ -209,13 +219,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 {
|
||||
@@ -295,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")
|
||||
|
||||
8
go.mod
8
go.mod
@@ -17,9 +17,10 @@ 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
|
||||
golang.org/x/term v0.30.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -28,13 +29,13 @@ 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 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
|
||||
|
||||
16
go.sum
16
go.sum
@@ -33,8 +33,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/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=
|
||||
@@ -49,8 +49,8 @@ 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=
|
||||
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 +102,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 +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.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=
|
||||
@@ -239,6 +237,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=
|
||||
|
||||
265
helpers.go
265
helpers.go
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"iter"
|
||||
"math/rand"
|
||||
@@ -10,15 +11,19 @@ import (
|
||||
"net/textproto"
|
||||
"net/url"
|
||||
"os"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
var sys *sdk.System
|
||||
@@ -148,10 +153,19 @@ func normalizeAndValidateRelayURLs(wsurls []string) error {
|
||||
|
||||
func connectToAllRelays(
|
||||
ctx context.Context,
|
||||
c *cli.Command,
|
||||
relayUrls []string,
|
||||
forcePreAuth bool,
|
||||
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),
|
||||
@@ -163,52 +177,176 @@ 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.GetFirst([]string{"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, c, 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... ", color.CyanString(url))
|
||||
relay := connectToSingleRelay(ctx, c, url, preAuthSigner, nil, log)
|
||||
if relay != nil {
|
||||
relays = append(relays, relay)
|
||||
}
|
||||
log("\n")
|
||||
}
|
||||
}
|
||||
|
||||
return relays
|
||||
}
|
||||
|
||||
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),
|
||||
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, c, 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(colors.errorf)
|
||||
}
|
||||
logthis(err.Error())
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
if colorizepreamble != nil {
|
||||
colorizepreamble(colors.errorf)
|
||||
}
|
||||
logthis("failed to get an AUTH challenge in enough time.")
|
||||
return nil
|
||||
}
|
||||
|
||||
preauthSuccess:
|
||||
if colorizepreamble != nil {
|
||||
colorizepreamble(colors.successf)
|
||||
}
|
||||
logthis("ok.")
|
||||
return relay
|
||||
} else {
|
||||
if colorizepreamble != nil {
|
||||
colorizepreamble(colors.errorf)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
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(int(os.Stderr.Fd()))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if width < 110 {
|
||||
return false
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -234,16 +372,51 @@ 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(int(os.Stderr.Fd()))
|
||||
|
||||
if len(msg) > termSize-prefixAlreadyPrinted && prefixAlreadyPrinted+1 < termSize {
|
||||
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,
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/nbd-wtf/go-nostr/nip19"
|
||||
"github.com/nbd-wtf/go-nostr/nip46"
|
||||
"github.com/nbd-wtf/go-nostr/nip49"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
var defaultKeyFlags = []cli.Flag{
|
||||
@@ -84,9 +85,6 @@ 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")
|
||||
}
|
||||
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)
|
||||
@@ -133,29 +131,59 @@ func promptDecrypt(ncryptsec string) (string, 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 := os.Open("/dev/tty")
|
||||
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()
|
||||
|
||||
rl, err := readline.NewEx(config)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for {
|
||||
// Print the prompt to stderr so it's visible to the user
|
||||
fmt.Fprintf(color.Error, color.YellowString(msg))
|
||||
|
||||
for {
|
||||
answer, err := rl.Readline()
|
||||
// Read password from TTY with masking
|
||||
password, err := term.ReadPassword(int(tty.Fd()))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Print newline after password input
|
||||
fmt.Fprintln(color.Error)
|
||||
|
||||
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: color.Error,
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
115
mcp.go
115
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
|
||||
}
|
||||
|
||||
@@ -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(", ")
|
||||
}
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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(", ")
|
||||
}
|
||||
|
||||
48
req.go
48
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/nip77"
|
||||
@@ -76,35 +77,29 @@ example:
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
relayUrls := c.Args().Slice()
|
||||
if len(relayUrls) > 0 {
|
||||
relays := connectToAllRelays(ctx,
|
||||
// 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
|
||||
if !c.Bool("force-pre-auth") {
|
||||
forcePreAuthSigner = nil
|
||||
}
|
||||
relays := connectToAllRelays(
|
||||
ctx,
|
||||
c,
|
||||
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.GetFirst([]string{"relay", ""}))[1],
|
||||
err,
|
||||
)
|
||||
}
|
||||
}()
|
||||
|
||||
if !c.Bool("auth") && !c.Bool("force-pre-auth") {
|
||||
return fmt.Errorf("auth not authorized")
|
||||
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"):]
|
||||
}
|
||||
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)
|
||||
},
|
||||
),
|
||||
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 +116,7 @@ example:
|
||||
}()
|
||||
}
|
||||
|
||||
// go line by line from stdin or run once with input from flags
|
||||
for stdinFilter := range getJsonsOrBlank() {
|
||||
filter := nostr.Filter{}
|
||||
if stdinFilter != "" {
|
||||
|
||||
30
wallet.go
30
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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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]
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user