Compare commits

...

7 Commits

14 changed files with 297 additions and 53 deletions

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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)
@@ -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"`)

View File

@@ -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",

View File

@@ -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
}

4
go.mod
View File

@@ -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
@@ -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

8
go.sum
View File

@@ -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=
@@ -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=

View File

@@ -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 {
@@ -133,9 +169,9 @@ func connectToAllRelays(
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) {
@@ -174,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)
@@ -248,3 +284,7 @@ func randString(n int) string {
}
return string(b)
}
func leftPadKey(k string) string {
return strings.Repeat("0", 64-len(k)) + k
}

11
key.go
View File

@@ -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,14 @@ 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)
}
sec = leftPadKey(sec)
if !nostr.IsValid32ByteHex(sec) {
lineProcessingError(ctx, "invalid hex key")
ctx = lineProcessingError(ctx, "invalid hex key")
continue
}
ch <- sec

View File

@@ -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,

193
relay.go
View File

@@ -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"
)
@@ -23,7 +32,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
}
@@ -32,4 +41,186 @@ 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"}},
{"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)
}
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' 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
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)
}
}

14
req.go
View File

@@ -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)
@@ -176,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
}
}

View File

@@ -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
}
}