nak publish

This commit is contained in:
fiatjaf 2025-05-06 00:52:49 -03:00
parent 83195d9a00
commit 67e291e80d
8 changed files with 308 additions and 151 deletions

View File

@ -82,12 +82,6 @@ var count = &cli.Command{
biggerUrlSize = len(relay.URL)
}
}
defer func() {
for _, relay := range relays {
relay.Close()
}
}()
}
filter := nostr.Filter{}

View File

@ -140,10 +140,6 @@ example:
// try to connect to the relays here
var relays []*nostr.Relay
// these are defaults, they will be replaced if we use the magic dynamic thing
logthis := func(relayUrl string, s string, args ...any) { log(s, args...) }
colorizethis := func(relayUrl string, colorize func(string, ...any) string) {}
if relayUrls := c.Args().Slice(); len(relayUrls) > 0 {
relays = connectToAllRelays(ctx, c, relayUrls, nil,
nostr.PoolOptions{
@ -157,19 +153,11 @@ example:
os.Exit(3)
}
}
defer func() {
for _, relay := range relays {
relay.Close()
}
}()
kr, sec, err := gatherKeyerFromArguments(ctx, c)
if err != nil {
return err
}
doAuth := c.Bool("auth")
// then process input and generate events:
// will reuse this
@ -314,6 +302,23 @@ example:
}
stdout(result)
return publishFlow(ctx, c, kr, evt, relays)
}
for stdinEvent := range getJsonsOrBlank() {
if err := handleEvent(stdinEvent); err != nil {
ctx = lineProcessingError(ctx, err.Error())
}
}
exitIfLineProcessingError(ctx)
return nil
},
}
func publishFlow(ctx context.Context, c *cli.Command, kr nostr.Signer, evt nostr.Event, relays []*nostr.Relay) error {
doAuth := c.Bool("auth")
// publish to relays
successRelays := make([]string, 0, len(relays))
if len(relays) > 0 {
@ -351,12 +356,12 @@ example:
}
flush()
logthis = func(relayUrl, s string, args ...any) {
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) {
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)))
@ -432,14 +437,3 @@ example:
return nil
}
for stdinEvent := range getJsonsOrBlank() {
if err := handleEvent(stdinEvent); err != nil {
ctx = lineProcessingError(ctx, err.Error())
}
}
exitIfLineProcessingError(ctx)
return nil
},
}

View File

@ -27,13 +27,6 @@ var fetch = &cli.Command{
),
ArgsUsage: "[nip05_or_nip19_code]",
Action: func(ctx context.Context, c *cli.Command) error {
defer func() {
sys.Pool.Relays.Range(func(_ string, relay *nostr.Relay) bool {
relay.Close()
return true
})
}()
for code := range getStdinLinesOrArguments(c.Args()) {
filter := nostr.Filter{}
var authorHint nostr.PubKey

4
go.mod
View File

@ -4,7 +4,6 @@ go 1.24.1
require (
fiatjaf.com/lib v0.3.1
fiatjaf.com/nostr v0.0.1
github.com/bep/debounce v1.2.1
github.com/btcsuite/btcd/btcec/v2 v2.3.4
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e
@ -23,6 +22,7 @@ require (
)
require (
fiatjaf.com/nostr v0.0.0-20250506031545-0d99789a54e2 // indirect
github.com/FastFilter/xorfilter v0.2.1 // indirect
github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3 // indirect
github.com/andybalholm/brotli v1.1.1 // indirect
@ -86,5 +86,3 @@ require (
google.golang.org/protobuf v1.36.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
replace fiatjaf.com/nostr => ../nostrlib

2
go.sum
View File

@ -1,6 +1,8 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
fiatjaf.com/lib v0.3.1 h1:/oFQwNtFRfV+ukmOCxfBEAuayoLwXp4wu2/fz5iHpwA=
fiatjaf.com/lib v0.3.1/go.mod h1:Ycqq3+mJ9jAWu7XjbQI1cVr+OFgnHn79dQR5oTII47g=
fiatjaf.com/nostr v0.0.0-20250506031545-0d99789a54e2 h1:WDjFQ8hPUAvTDKderZ0NC6vaRBBxODPchKER4wuQdG8=
fiatjaf.com/nostr v0.0.0-20250506031545-0d99789a54e2/go.mod h1:VPs38Fc8J1XAErV750CXAmMUqIq3XEX9VZVj/LuQzzM=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/FastFilter/xorfilter v0.2.1 h1:lbdeLG9BdpquK64ZsleBS8B4xO/QW1IM0gMzF7KaBKc=
github.com/FastFilter/xorfilter v0.2.1/go.mod h1:aumvdkhscz6YBZF9ZA/6O4fIoNod4YR50kIVGGZ7l9I=

View File

@ -44,6 +44,7 @@ var app = &cli.Command{
mcpServer,
curl,
fsCmd,
publish,
},
Version: version,
Flags: []cli.Flag{

181
publish.go Normal file
View File

@ -0,0 +1,181 @@
package main
import (
"context"
"fmt"
"io"
"os"
"strings"
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/nip19"
"fiatjaf.com/nostr/sdk"
"github.com/urfave/cli/v3"
)
var publish = &cli.Command{
Name: "publish",
Usage: "publishes a note with content from stdin",
Description: `reads content from stdin and publishes it as a note, optionally as a reply to another note.
example:
echo "hello world" | nak publish
echo "I agree!" | nak publish --reply nevent1...
echo "tagged post" | nak publish -t t=mytag -t e=someeventid`,
DisableSliceFlagSeparator: true,
Flags: append(defaultKeyFlags,
&cli.StringFlag{
Name: "reply",
Usage: "event id, naddr1 or nevent1 code to reply to",
},
&cli.StringSliceFlag{
Name: "tag",
Aliases: []string{"t"},
Usage: "sets a tag field on the event, takes a value like -t e=<id> or -t sometag=\"value one;value two;value three\"",
},
&NaturalTimeFlag{
Name: "created-at",
Aliases: []string{"time", "ts"},
Usage: "unix timestamp value for the created_at field",
DefaultText: "now",
Value: nostr.Now(),
},
&cli.BoolFlag{
Name: "auth",
Usage: "always perform nip42 \"AUTH\" when facing an \"auth-required: \" rejection and try again",
Category: CATEGORY_EXTRAS,
},
&cli.BoolFlag{
Name: "nevent",
Usage: "print the nevent code (to stderr) after the event is published",
Category: CATEGORY_EXTRAS,
},
&cli.BoolFlag{
Name: "confirm",
Usage: "ask before publishing the event",
Category: CATEGORY_EXTRAS,
},
),
Action: func(ctx context.Context, c *cli.Command) error {
content, err := io.ReadAll(os.Stdin)
if err != nil {
return fmt.Errorf("failed to read from stdin: %w", err)
}
evt := nostr.Event{
Kind: 1,
Content: strings.TrimSpace(string(content)),
Tags: make(nostr.Tags, 0, 4),
CreatedAt: nostr.Now(),
}
// handle timestamp flag
if c.IsSet("created-at") {
evt.CreatedAt = getNaturalDate(c, "created-at")
}
// handle reply flag
var replyRelays []string
if replyTo := c.String("reply"); replyTo != "" {
var replyEvent *nostr.Event
// try to decode as nevent or naddr first
if strings.HasPrefix(replyTo, "nevent1") || strings.HasPrefix(replyTo, "naddr1") {
_, value, err := nip19.Decode(replyTo)
if err != nil {
return fmt.Errorf("invalid reply target: %w", err)
}
switch pointer := value.(type) {
case nostr.EventPointer:
replyEvent, _, err = sys.FetchSpecificEvent(ctx, pointer, sdk.FetchSpecificEventParameters{})
case nostr.EntityPointer:
replyEvent, _, err = sys.FetchSpecificEvent(ctx, pointer, sdk.FetchSpecificEventParameters{})
}
if err != nil {
return fmt.Errorf("failed to fetch reply target event: %w", err)
}
} else {
// try as raw event ID
id, err := nostr.IDFromHex(replyTo)
if err != nil {
return fmt.Errorf("invalid event id: %w", err)
}
replyEvent, _, err = sys.FetchSpecificEvent(ctx, nostr.EventPointer{ID: id}, sdk.FetchSpecificEventParameters{})
if err != nil {
return fmt.Errorf("failed to fetch reply target event: %w", err)
}
}
if replyEvent.Kind != 1 {
evt.Kind = 1111
}
// add reply tags
evt.Tags = append(evt.Tags,
nostr.Tag{"e", replyEvent.ID.Hex(), "", "reply"},
nostr.Tag{"p", replyEvent.PubKey.Hex()},
)
replyRelays = sys.FetchInboxRelays(ctx, replyEvent.PubKey, 3)
}
// handle other tags -- copied from event.go
tagFlags := c.StringSlice("tag")
for _, tagFlag := range tagFlags {
// tags are in the format key=value
tagName, tagValue, found := strings.Cut(tagFlag, "=")
tag := []string{tagName}
if found {
// tags may also contain extra elements separated with a ";"
tagValues := strings.Split(tagValue, ";")
tag = append(tag, tagValues...)
}
evt.Tags = append(evt.Tags, tag)
}
// process the content
targetRelays := sys.PrepareNoteEvent(ctx, &evt)
// connect to all the relays (like event.go)
kr, _, err := gatherKeyerFromArguments(ctx, c)
if err != nil {
return err
}
pk, err := kr.GetPublicKey(ctx)
if err != nil {
return fmt.Errorf("failed to get our public key: %w", err)
}
relayUrls := sys.FetchWriteRelays(ctx, pk)
relayUrls = nostr.AppendUnique(relayUrls, targetRelays...)
relayUrls = nostr.AppendUnique(relayUrls, replyRelays...)
relayUrls = nostr.AppendUnique(relayUrls, c.Args().Slice()...)
relays := connectToAllRelays(ctx, c, relayUrls, nil,
nostr.PoolOptions{
AuthHandler: func(ctx context.Context, authEvent *nostr.Event) error {
return authSigner(ctx, c, func(s string, args ...any) {}, authEvent)
},
},
)
if len(relays) == 0 {
if len(relayUrls) == 0 {
return fmt.Errorf("no relays to publish this note to.")
} else {
return fmt.Errorf("failed to connect to any of [ %v ].", relayUrls)
}
}
// sign the event
if err := kr.SignEvent(ctx, &evt); err != nil {
return fmt.Errorf("error signing event: %w", err)
}
// print
stdout(evt.String())
// publish (like event.go)
return publishFlow(ctx, c, kr, evt, relays)
},
}

6
req.go
View File

@ -113,12 +113,6 @@ example:
for i, relay := range relays {
relayUrls[i] = relay.URL
}
defer func() {
for _, relay := range relays {
relay.Close()
}
}()
}
// go line by line from stdin or run once with input from flags