Compare commits

..
13 Commits
Author SHA1 Message Date
fiatjaf 160afb540c git: percent-decode relay and identifier segments in nostr://
copied from https://gitworkshop.dev/fiatjaf.com/nak/prs/nevent1qqswm2nmw68uqf9d66f3c4vspt0fna8g0vd24z5yyeq35a0p33kmpach2gch6/files
2026-07-23 09:25:02 -03:00
fiatjaf 5a82298d95 event: exit with error when all the publishes fail. 2026-07-22 12:12:27 -03:00
fiatjaf a3890c4976 bunker: don't conflict with stored if only default --sec exists, not explicit.
fixes nostr:nevent1qvzqqqqqqypzprqgrf7tvq6w3gzkmltk6n4s7hxa6xzc0dgn7mrgr77cjs87svqzqyd8wumn8ghj7cnpwdehq6tnw3hkctn0wfnj76twvfhhsqglwaehxw309ac8junpd45kgtnxd9shg6npvchxxmmd9a5kucn00qqzq570y8j7wet7fw6qz39z3kchqdxxw6wp7anxnxpy62x0qhqd6fhlz79p53
2026-07-22 08:31:18 -03:00
fiatjaf 1403c7d2cd fetch: use more outbox relays. 2026-07-21 20:42:52 -03:00
fiatjaf 6855722ba5 make the relay connection timeout configurable. 2026-07-21 20:41:45 -03:00
fiatjaf aac995714b timeout relay connections at 2s. 2026-07-21 18:51:41 -03:00
fiatjaf a9df9c6a21 groups: nip29 parenting. 2026-07-20 17:05:36 -03:00
fiatjaf 57be7627d4 git: allow pushing without updating the repository announcement. 2026-07-17 22:51:21 -03:00
fiatjaf 5b7f3eb4db git: 'patch pull' similar to 'pr pull'. 2026-07-16 17:08:02 -03:00
fiatjaf 6d4f4c03ec git: eliminate maintainer and web setup. 2026-07-16 15:36:59 -03:00
fiatjaf eb9afd0b54 pr: merge. 2026-07-16 15:32:42 -03:00
fiatjaf 9c35de2b02 nak git pr 2026-07-16 15:23:30 -03:00
fiatjaf b65683886b nak key public prints our key if given by --sec. 2026-07-15 16:30:58 -03:00
12 changed files with 1109 additions and 277 deletions
+3 -3
View File
@@ -154,10 +154,10 @@ var bunker = &cli.Command{
}
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
// we don't have any secret key stored, so just use whatever was given via flags (or defaults)
config.Secret = baseSecret
} else if baseSecret.Plain == nil && baseSecret.Encrypted == nil {
// we didn't provide any keys, so we just use the stored
} else if !c.IsSet("sec") && !c.IsSet("prompt-sec") {
// we didn't provide any keys explicitly, 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
+13 -5
View File
@@ -63,11 +63,19 @@ var count = &cli.Command{
hll = hyperloglog.New(offset)
}
for _, relayUrl := range relayUrls {
relay, err := sys.Pool.EnsureRelay(relayUrl)
if err != nil {
fmt.Fprintf(os.Stderr, "%s%s: ", strings.Repeat(" ", biggerUrlSize-len(relayUrl)), relayUrl)
fmt.Fprintf(os.Stderr, "error: %s\n", err)
continue
nm := nostr.NormalizeURL(relayUrl)
relay, ok := sys.Pool.Relays.Load(nm)
if !ok || relay == nil || !relay.IsConnected() {
ct, cancel := context.WithTimeout(context.Background(), connectTimeout)
var err error
relay, err = nostr.RelayConnect(ct, relayUrl, sys.Pool.RelayOptions)
cancel()
if err != nil {
fmt.Fprintf(os.Stderr, "%s%s: ", strings.Repeat(" ", biggerUrlSize-len(relayUrl)), relayUrl)
fmt.Fprintf(os.Stderr, "error: %s\n", err)
continue
}
sys.Pool.Relays.Store(nm, relay)
}
count, hllRegisters, err := relay.Count(ctx, filter, nostr.SubscriptionOptions{
+17 -3
View File
@@ -543,9 +543,19 @@ func publishFlow(ctx context.Context, c *cli.Command, kr nostr.Signer, evt nostr
defer cancel()
if !relay.IsConnected() {
if new_, err := sys.Pool.EnsureRelay(relay.URL); err == nil {
relays[i] = new_
relay = new_
nm := nostr.NormalizeURL(relay.URL)
if r, ok := sys.Pool.Relays.Load(nm); ok && r != nil && r.IsConnected() {
relays[i] = r
relay = r
} else {
ct, cancel := context.WithTimeout(context.Background(), connectTimeout)
new_, err := nostr.RelayConnect(ct, relay.URL, sys.Pool.RelayOptions)
cancel()
if err == nil {
sys.Pool.Relays.Store(nm, new_)
relays[i] = new_
relay = new_
}
}
}
@@ -578,6 +588,10 @@ func publishFlow(ctx context.Context, c *cli.Command, kr nostr.Signer, evt nostr
if len(successRelays) > 0 && c.Bool("nevent") {
log(nip19.EncodeNevent(evt.ID, successRelays, evt.PubKey) + "\n")
}
if len(successRelays) == 0 {
return fmt.Errorf("failed to publish to any of the relays")
}
}
return nil
+1 -1
View File
@@ -101,7 +101,7 @@ var fetch = &cli.Command{
sys.Hints.Save(authorHint, nostr.NormalizeURL(url), hints.LastInHint, nostr.Now())
}
for _, url := range sys.FetchOutboxRelays(ctx, authorHint, 3) {
for _, url := range sys.FetchOutboxRelays(ctx, authorHint, 6) {
relays = append(relays, url)
}
}
+895 -200
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -3,7 +3,7 @@ module github.com/fiatjaf/nak
go 1.25
require (
fiatjaf.com/nostr v0.0.0-20260625215907-7a78ad0c459f
fiatjaf.com/nostr v0.0.0-20260716191248-c205ed45b97e
github.com/AlecAivazis/survey/v2 v2.3.7
github.com/bep/debounce v1.2.1
github.com/btcsuite/btcd/btcec/v2 v2.3.6
+2 -2
View File
@@ -1,7 +1,7 @@
fiatjaf.com/lib v0.3.7 h1:mXZOn7NrUcjSdy4oNvwQyAmes7Ueb+Zr5hjqMIe2dxI=
fiatjaf.com/lib v0.3.7/go.mod h1:UlHaZvPHj25PtKLh9GjZkUHRmQ2xZ8Jkoa4VRaLeeQ8=
fiatjaf.com/nostr v0.0.0-20260625215907-7a78ad0c459f h1:n0G6XmIXS/j+AgDRFiZqK+l1R0ZahlPe/HwHlfxwj/c=
fiatjaf.com/nostr v0.0.0-20260625215907-7a78ad0c459f/go.mod h1:b1EIUDnd133Ie8Pg8O/biaKdFyCMz28aD4n64g1GqvM=
fiatjaf.com/nostr v0.0.0-20260716191248-c205ed45b97e h1:/vgoytiH4qQ28O/oX1hj656X69CnTy1pGj/17HFjH+A=
fiatjaf.com/nostr v0.0.0-20260716191248-c205ed45b97e/go.mod h1:b1EIUDnd133Ie8Pg8O/biaKdFyCMz28aD4n64g1GqvM=
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=
+86 -17
View File
@@ -29,7 +29,6 @@ var group = &cli.Command{
ArgsUsage: "<subcommand> <relay>'<identifier> [flags]",
Flags: combineFlags([][]cli.Flag{}),
Before: func(ctx context.Context, c *cli.Command) (context.Context, error) {
return ctx, nil
},
Commands: []*cli.Command{
@@ -244,9 +243,17 @@ var group = &cli.Command{
return err
}
r, err := sys.Pool.EnsureRelay(relay)
if err != nil {
return err
nm := nostr.NormalizeURL(relay)
r, ok := sys.Pool.Relays.Load(nm)
if !ok || r == nil || !r.IsConnected() {
ct, cancel := context.WithTimeout(context.Background(), connectTimeout)
var err error
r, err = nostr.RelayConnect(ct, relay, sys.Pool.RelayOptions)
cancel()
if err != nil {
return err
}
sys.Pool.Relays.Store(nm, r)
}
sub, err := r.Subscribe(ctx, nostr.Filter{
@@ -317,11 +324,19 @@ var group = &cli.Command{
return fmt.Errorf("failed to sign message: %w", err)
}
if r, err := sys.Pool.EnsureRelay(relay); err != nil {
return err
} else {
return r.Publish(ctx, msg)
nm := nostr.NormalizeURL(relay)
r, ok := sys.Pool.Relays.Load(nm)
if !ok || r == nil || !r.IsConnected() {
ct, cancel := context.WithTimeout(context.Background(), connectTimeout)
var err error
r, err = nostr.RelayConnect(ct, relay, sys.Pool.RelayOptions)
cancel()
if err != nil {
return err
}
sys.Pool.Relays.Store(nm, r)
}
return r.Publish(ctx, msg)
},
},
},
@@ -475,9 +490,17 @@ write your forum post
return fmt.Errorf("failed to sign forum topic event: %w", err)
}
r, err := sys.Pool.EnsureRelay(relay)
if err != nil {
return err
nm := nostr.NormalizeURL(relay)
r, ok := sys.Pool.Relays.Load(nm)
if !ok || r == nil || !r.IsConnected() {
ct, cancel := context.WithTimeout(context.Background(), connectTimeout)
var err error
r, err = nostr.RelayConnect(ct, relay, sys.Pool.RelayOptions)
cancel()
if err != nil {
return err
}
sys.Pool.Relays.Store(nm, r)
}
return r.Publish(ctx, evt)
@@ -582,9 +605,17 @@ write your forum post
return fmt.Errorf("failed to sign forum comment event: %w", err)
}
r, err := sys.Pool.EnsureRelay(relay)
if err != nil {
return err
nm := nostr.NormalizeURL(relay)
r, ok := sys.Pool.Relays.Load(nm)
if !ok || r == nil || !r.IsConnected() {
ct, cancel := context.WithTimeout(context.Background(), connectTimeout)
var err error
r, err = nostr.RelayConnect(ct, relay, sys.Pool.RelayOptions)
cancel()
if err != nil {
return err
}
sys.Pool.Relays.Store(nm, r)
}
return r.Publish(ctx, evt)
@@ -696,6 +727,18 @@ write your forum post
Name: "all-kinds",
Usage: "specify this to delete the supported_kinds property, meaning everything will be supported",
},
&cli.StringFlag{
Name: "parent",
Usage: "set parent group identifier",
},
&cli.StringFlag{
Name: "children",
Usage: "set child group identifiers (semicolon-separated)",
},
&cli.BoolFlag{
Name: "orphan",
Usage: "remove parent tag, making this group an orphan",
},
},
Action: func(ctx context.Context, c *cli.Command) error {
relay, identifier, err := parseGroupIdentifier(c)
@@ -760,6 +803,18 @@ write your forum post
} else if c.Bool("all-kinds") {
group.SupportedKinds = nil
}
if parent := c.String("parent"); parent != "" {
group.Parent = parent
}
if children := c.String("children"); children != "" {
group.Children = strings.Split(children, ";")
for i := range group.Children {
group.Children[i] = strings.TrimSpace(group.Children[i])
}
}
if c.Bool("orphan") {
group.Parent = ""
}
return publishModerationEvent(ctx, c, 9002, func(evt *nostr.Event, args []string) error {
evt.Tags = append(evt.Tags, nostr.Tag{"name", group.Name})
@@ -790,6 +845,12 @@ write your forum post
}
evt.Tags = append(evt.Tags, tag)
}
if group.Parent != "" {
evt.Tags = append(evt.Tags, nostr.Tag{"parent", group.Parent})
}
for _, child := range group.Children {
evt.Tags = append(evt.Tags, nostr.Tag{"child", child})
}
return nil
})
},
@@ -877,9 +938,17 @@ func publishModerationEvent(ctx context.Context, c *cli.Command, kind nostr.Kind
stdout(evt.String())
r, err := sys.Pool.EnsureRelay(relay)
if err != nil {
return err
nm := nostr.NormalizeURL(relay)
r, ok := sys.Pool.Relays.Load(nm)
if !ok || r == nil || !r.IsConnected() {
ct, cancel := context.WithTimeout(context.Background(), connectTimeout)
var err error
r, err = nostr.RelayConnect(ct, relay, sys.Pool.RelayOptions)
cancel()
if err != nil {
return err
}
sys.Pool.Relays.Store(nm, r)
}
return r.Publish(ctx, evt)
+57 -43
View File
@@ -48,6 +48,8 @@ var (
stdout = func(args ...any) { fmt.Fprintln(color.Output, args...) }
)
var connectTimeout = 2 * time.Second
func isPiped() bool {
stat, err := os.Stdin.Stat()
if err != nil {
@@ -268,63 +270,75 @@ func connectToSingleRelay(
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 c.Bool("force-pre-auth") {
if colorizepreamble != nil {
colorizepreamble(color.YellowString)
}
logthis("waiting for auth challenge... ")
time.Sleep(time.Millisecond * 200)
nm := nostr.NormalizeURL(url)
for range 5 {
if err := relay.Auth(ctx, func(ctx context.Context, authEvent *nostr.Event) error {
challengeTag := authEvent.Tags.Find("challenge")
if challengeTag == nil || len(challengeTag) < 2 || challengeTag[1] == "" {
return fmt.Errorf("auth not received yet *****") // what a giant hack
}
return authSigner(ctx, c, logthis, authEvent)
}); 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
}
}
}
relay, ok := sys.Pool.Relays.Load(nm)
if !ok || relay == nil || !relay.IsConnected() {
connectCtx, cancel := context.WithTimeout(context.Background(), connectTimeout)
defer cancel()
var err error
if relay, err = nostr.RelayConnect(connectCtx, url, sys.Pool.RelayOptions); err != nil {
if colorizepreamble != nil {
colorizepreamble(colors.errorf)
}
logthis("failed to get an AUTH challenge in enough time.")
logthis(clampError(err, len(url)+12))
return nil
}
preauthSuccess:
sys.Pool.Relays.Store(nm, relay)
go func(r *nostr.Relay, relayURL string) {
<-r.Context().Done()
if current, ok := sys.Pool.Relays.Load(relayURL); ok && current == r {
sys.Pool.Relays.Delete(relayURL)
}
}(relay, nm)
}
if c.Bool("force-pre-auth") {
if colorizepreamble != nil {
colorizepreamble(colors.successf)
colorizepreamble(color.YellowString)
}
logthis("waiting for auth challenge... ")
time.Sleep(time.Millisecond * 200)
for range 5 {
if err := relay.Auth(ctx, func(ctx context.Context, authEvent *nostr.Event) error {
challengeTag := authEvent.Tags.Find("challenge")
if challengeTag == nil || len(challengeTag) < 2 || challengeTag[1] == "" {
return fmt.Errorf("auth not received yet *****") // what a giant hack
}
return authSigner(ctx, c, logthis, authEvent)
}); err == nil {
// auth succeeded
goto preauthSuccess
} else {
// auth failed
if strings.HasSuffix(err.Error(), "auth not received yet *****") {
time.Sleep(time.Second)
continue
} else {
if colorizepreamble != nil {
colorizepreamble(colors.errorf)
}
logthis(err.Error())
return nil
}
}
}
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))
logthis("failed to get an AUTH challenge in enough time.")
return nil
}
preauthSuccess:
if colorizepreamble != nil {
colorizepreamble(colors.successf)
}
logthis("ok.")
return relay
}
func clearLines(lineCount int) {
+15 -1
View File
@@ -57,7 +57,21 @@ var public = &cli.Command{
},
},
Action: func(ctx context.Context, c *cli.Command) error {
for sk := range getSecretKeysFromStdinLinesOrSlice(ctx, c, c.Args().Slice()) {
args := c.Args().Slice()
if len(args) == 0 && !isPiped() {
kr, _, err := gatherKeyerFromArguments(ctx, c)
if err != nil {
return err
}
pk, err := kr.GetPublicKey(ctx)
if err != nil {
return fmt.Errorf("failed to get public key: %w", err)
}
stdout(pk.Hex())
return nil
}
for sk := range getSecretKeysFromStdinLinesOrSlice(ctx, c, args) {
_, pk := btcec.PrivKeyFromBytes(sk[:])
if c.Bool("with-parity") {
+15
View File
@@ -7,6 +7,7 @@ import (
"os"
"path/filepath"
"strings"
"time"
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/nip42"
@@ -42,6 +43,7 @@ var defaultKeyFlags = []cli.Flag{
Name: "prompt-sec",
Usage: "prompt the user to paste a hex or nsec with which to sign the event",
Category: CATEGORY_SIGNER,
Hidden: true,
},
&SecretKeyFlag{
Name: "connect-as",
@@ -50,6 +52,7 @@ var defaultKeyFlags = []cli.Flag{
Sources: cli.EnvVars("NOSTR_CLIENT_KEY"),
Value: defaultKey(),
DefaultText: "the default key (see `nak key default`)",
Hidden: true,
},
}
@@ -128,6 +131,7 @@ var app = &cli.Command{
}
return nil
},
Hidden: true,
},
&cli.BoolFlag{
Name: "verbose",
@@ -141,6 +145,17 @@ var app = &cli.Command{
}
return nil
},
Hidden: true,
},
&cli.DurationFlag{
Name: "connect-timeout",
Usage: "timeout for connecting to relays",
Value: connectTimeout,
Action: func(ctx context.Context, c *cli.Command, d time.Duration) error {
connectTimeout = d
return nil
},
Hidden: true,
},
},
defaultKeyFlags,
+4 -1
View File
@@ -996,10 +996,13 @@ func (r *NostrRoot) publishNote(path string) {
}
for _, url := range relays {
relay, err := r.sys.Pool.EnsureRelay(url)
connectCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
relay, err := nostr.RelayConnect(connectCtx, url, r.sys.Pool.RelayOptions)
cancel()
if err != nil {
continue
}
r.sys.Pool.Relays.Store(nostr.NormalizeURL(url), relay)
relay.Publish(ctx, *evt)
}