2023-05-03 17:14:06 -04:00
package main
import (
2023-05-30 12:43:57 -04:00
"context"
2023-05-03 17:14:06 -04:00
"encoding/json"
"fmt"
2023-05-23 22:24:55 -04:00
"os"
2023-05-03 17:14:06 -04:00
"strconv"
"strings"
"time"
2023-07-05 13:11:15 -04:00
"github.com/mailru/easyjson"
2023-05-03 17:14:06 -04:00
"github.com/nbd-wtf/go-nostr"
2024-01-24 20:43:23 -05:00
"github.com/nbd-wtf/go-nostr/nip19"
2024-06-25 21:18:26 -04:00
"github.com/urfave/cli/v3"
2023-11-13 08:34:09 -05:00
"golang.org/x/exp/slices"
2023-05-03 17:14:06 -04:00
)
const CATEGORY_EVENT_FIELDS = "EVENT FIELDS"
var event = & cli . Command {
Name : "event" ,
2023-05-23 22:24:55 -04:00
Usage : "generates an encoded event and either prints it or sends it to a set of relays" ,
2023-10-29 20:48:18 -04:00
Description : ` outputs an event built with the flags . if one or more relays are given as arguments , an attempt is also made to publish the event to these relays .
example :
nak event - c hello wss : //nos.lol
nak event - k 3 - p 3 bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d
if an event -- or a partial event -- is given on stdin , the flags can be used to optionally modify it . if it is modified it is rehashed and resigned , otherwise it is just returned as given , but that can be used to just publish to relays .
example :
echo ' { "id" : "a889df6a387419ff204305f4c2d296ee328c3cd4f8b62f205648a541b4554dfb" , "pubkey" : "c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5" , "created_at" : 1698623783 , "kind" : 1 , "tags" : [ ] , "content" : "hello from the nostr army knife" , "sig" : "84876e1ee3e726da84e5d195eb79358b2b3eaa4d9bd38456fde3e8a2af3f1cd4cda23f23fda454869975b3688797d4c66e12f4c51c1b43c6d2997c5e61865661" } ' | nak event wss : //offchain.pub
2023-11-20 13:01:51 -05:00
echo ' { "tags" : [ [ "t" , "spam" ] ] } ' | nak event - c ' this is spam ' ` ,
2023-05-03 17:14:06 -04:00
Flags : [ ] cli . Flag {
& cli . StringFlag {
Name : "sec" ,
2024-06-25 21:18:26 -04:00
Usage : "secret key to sign the event, as nsec, ncryptsec or hex" ,
2023-05-03 17:14:06 -04:00
DefaultText : "the key '1'" ,
Value : "0000000000000000000000000000000000000000000000000000000000000001" ,
} ,
2023-11-08 12:26:25 -05:00
& cli . BoolFlag {
Name : "prompt-sec" ,
Usage : "prompt the user to paste a hex or nsec with which to sign the event" ,
} ,
2024-02-05 22:58:26 -05:00
& cli . StringFlag {
Name : "connect" ,
Usage : "sign event using NIP-46, expects a bunker://... URL" ,
} ,
2024-02-06 10:47:46 -05:00
& cli . StringFlag {
Name : "connect-as" ,
Usage : "private key to when communicating with the bunker given on --connect" ,
DefaultText : "a random key" ,
} ,
2024-05-14 14:23:08 -04:00
// ~ these args are only for the convoluted musig2 signing process
// they will be generally copy-shared-pasted across some manual coordination method between participants
& cli . UintFlag {
2024-05-14 22:52:56 -04:00
Name : "musig" ,
2024-05-14 14:23:08 -04:00
Usage : "number of signers to use for musig2" ,
Value : 1 ,
DefaultText : "1 -- i.e. do not use musig2 at all" ,
} ,
& cli . StringSliceFlag {
2024-05-14 22:52:56 -04:00
Name : "musig-pubkey" ,
2024-05-14 14:23:08 -04:00
Hidden : true ,
} ,
& cli . StringFlag {
2024-05-14 22:52:56 -04:00
Name : "musig-nonce-secret" ,
2024-05-14 14:23:08 -04:00
Hidden : true ,
} ,
& cli . StringSliceFlag {
2024-05-14 22:52:56 -04:00
Name : "musig-nonce" ,
2024-05-14 14:23:08 -04:00
Hidden : true ,
} ,
& cli . StringSliceFlag {
2024-05-14 22:52:56 -04:00
Name : "musig-partial" ,
2024-05-14 14:23:08 -04:00
Hidden : true ,
} ,
// ~~~
2023-05-03 17:14:06 -04:00
& cli . BoolFlag {
Name : "envelope" ,
Usage : "print the event enveloped in a [\"EVENT\", ...] message ready to be sent to a relay" ,
} ,
2023-12-07 16:12:12 -05:00
& cli . BoolFlag {
Name : "auth" ,
Usage : "always perform NIP-42 \"AUTH\" when facing an \"auth-required: \" rejection and try again" ,
} ,
2024-01-24 20:43:23 -05:00
& cli . BoolFlag {
Name : "nevent" ,
Usage : "print the nevent code (to stderr) after the event is published" ,
} ,
2023-05-03 17:14:06 -04:00
& cli . IntFlag {
Name : "kind" ,
Aliases : [ ] string { "k" } ,
Usage : "event kind" ,
DefaultText : "1" ,
2023-10-29 20:48:18 -04:00
Value : 0 ,
2023-05-03 17:14:06 -04:00
Category : CATEGORY_EVENT_FIELDS ,
} ,
& cli . StringFlag {
Name : "content" ,
Aliases : [ ] string { "c" } ,
Usage : "event content" ,
DefaultText : "hello from the nostr army knife" ,
2023-10-29 20:48:18 -04:00
Value : "" ,
2023-05-03 17:14:06 -04:00
Category : CATEGORY_EVENT_FIELDS ,
} ,
& cli . StringSliceFlag {
Name : "tag" ,
Aliases : [ ] string { "t" } ,
Usage : "sets a tag field on the event, takes a value like -t e=<id>" ,
Category : CATEGORY_EVENT_FIELDS ,
} ,
& cli . StringSliceFlag {
Name : "e" ,
Usage : "shortcut for --tag e=<value>" ,
Category : CATEGORY_EVENT_FIELDS ,
} ,
& cli . StringSliceFlag {
Name : "p" ,
Usage : "shortcut for --tag p=<value>" ,
Category : CATEGORY_EVENT_FIELDS ,
} ,
2024-01-17 06:47:51 -05:00
& cli . StringSliceFlag {
Name : "d" ,
Usage : "shortcut for --tag d=<value>" ,
Category : CATEGORY_EVENT_FIELDS ,
} ,
2023-05-03 17:14:06 -04:00
& cli . StringFlag {
Name : "created-at" ,
Aliases : [ ] string { "time" , "ts" } ,
Usage : "unix timestamp value for the created_at field" ,
DefaultText : "now" ,
2023-10-29 20:48:18 -04:00
Value : "" ,
2023-05-03 17:14:06 -04:00
Category : CATEGORY_EVENT_FIELDS ,
} ,
} ,
2023-05-23 22:24:55 -04:00
ArgsUsage : "[relay...]" ,
2024-06-25 21:18:26 -04:00
Action : func ( ctx context . Context , c * cli . Command ) error {
2023-11-13 12:57:35 -05:00
// try to connect to the relays here
var relays [ ] * nostr . Relay
if relayUrls := c . Args ( ) . Slice ( ) ; len ( relayUrls ) > 0 {
2024-07-10 13:48:02 -04:00
_ , relays = connectToAllRelays ( ctx , relayUrls , false )
2023-11-13 12:57:35 -05:00
if len ( relays ) == 0 {
2023-11-13 13:03:27 -05:00
log ( "failed to connect to any of the given relays.\n" )
2023-11-13 12:57:35 -05:00
os . Exit ( 3 )
}
}
2024-01-11 19:29:39 -05:00
defer func ( ) {
for _ , relay := range relays {
relay . Close ( )
}
} ( )
2024-06-25 21:18:26 -04:00
sec , bunker , err := gatherSecretKeyOrBunkerFromArguments ( ctx , c )
2023-12-02 10:18:55 -05:00
if err != nil {
return err
2023-11-08 12:26:25 -05:00
}
2023-12-07 16:12:12 -05:00
doAuth := c . Bool ( "auth" )
2023-11-08 12:26:25 -05:00
// then process input and generate events
2023-11-07 15:57:43 -05:00
for stdinEvent := range getStdinLinesOrBlank ( ) {
evt := nostr . Event {
Tags : make ( nostr . Tags , 0 , 3 ) ,
}
2023-10-29 20:48:18 -04:00
2023-11-24 19:19:10 -05:00
kindWasSupplied := false
2023-11-07 15:57:43 -05:00
mustRehashAndResign := false
2023-12-07 16:12:12 -05:00
2023-11-07 15:57:43 -05:00
if stdinEvent != "" {
2023-11-28 13:18:43 -05:00
if err := easyjson . Unmarshal ( [ ] byte ( stdinEvent ) , & evt ) ; err != nil {
2024-07-11 14:33:19 -04:00
ctx = lineProcessingError ( ctx , "invalid event received from stdin: %s" , err )
2023-11-07 15:57:43 -05:00
continue
}
2023-11-13 08:34:09 -05:00
kindWasSupplied = strings . Contains ( stdinEvent , ` "kind" ` )
2023-10-29 20:48:18 -04:00
}
2023-11-24 19:08:13 -05:00
if kind := c . Int ( "kind" ) ; slices . Contains ( c . FlagNames ( ) , "kind" ) {
2024-06-25 21:18:26 -04:00
evt . Kind = int ( kind )
2023-11-07 15:57:43 -05:00
mustRehashAndResign = true
2023-11-24 19:08:13 -05:00
} else if ! kindWasSupplied {
evt . Kind = 1
mustRehashAndResign = true
2023-11-07 15:57:43 -05:00
}
2023-10-29 20:48:18 -04:00
2023-11-07 15:57:43 -05:00
if content := c . String ( "content" ) ; content != "" {
evt . Content = content
mustRehashAndResign = true
} else if evt . Content == "" && evt . Kind == 1 {
evt . Content = "hello from the nostr army knife"
mustRehashAndResign = true
}
2023-05-03 17:14:06 -04:00
2023-11-07 15:57:43 -05:00
tags := make ( nostr . Tags , 0 , 5 )
for _ , tagFlag := range c . StringSlice ( "tag" ) {
// tags are in the format key=value
2023-12-13 17:31:52 -05:00
tagName , tagValue , found := strings . Cut ( tagFlag , "=" )
tag := [ ] string { tagName }
if found {
2023-11-07 15:57:43 -05:00
// tags may also contain extra elements separated with a ";"
2023-12-13 17:31:52 -05:00
tagValues := strings . Split ( tagValue , ";" )
tag = append ( tag , tagValues ... )
2023-11-07 15:57:43 -05:00
}
2024-02-21 07:47:34 -05:00
tags = tags . AppendUnique ( tag )
2023-05-03 17:14:06 -04:00
}
2024-01-11 19:48:54 -05:00
2023-11-07 15:57:43 -05:00
for _ , etag := range c . StringSlice ( "e" ) {
2024-01-17 06:47:51 -05:00
tags = tags . AppendUnique ( [ ] string { "e" , etag } )
2023-11-07 15:57:43 -05:00
mustRehashAndResign = true
}
for _ , ptag := range c . StringSlice ( "p" ) {
2024-01-17 06:47:51 -05:00
tags = tags . AppendUnique ( [ ] string { "p" , ptag } )
mustRehashAndResign = true
}
for _ , dtag := range c . StringSlice ( "d" ) {
tags = tags . AppendUnique ( [ ] string { "d" , dtag } )
2023-11-07 15:57:43 -05:00
mustRehashAndResign = true
}
if len ( tags ) > 0 {
for _ , tag := range tags {
evt . Tags = append ( evt . Tags , tag )
}
mustRehashAndResign = true
2023-05-03 17:14:06 -04:00
}
2023-11-07 15:57:43 -05:00
if createdAt := c . String ( "created-at" ) ; createdAt != "" {
ts := time . Now ( )
if createdAt != "now" {
if v , err := strconv . ParseInt ( createdAt , 10 , 64 ) ; err != nil {
return fmt . Errorf ( "failed to parse timestamp '%s': %w" , createdAt , err )
} else {
ts = time . Unix ( v , 0 )
}
2023-10-29 20:48:18 -04:00
}
2023-11-07 15:57:43 -05:00
evt . CreatedAt = nostr . Timestamp ( ts . Unix ( ) )
mustRehashAndResign = true
} else if evt . CreatedAt == 0 {
evt . CreatedAt = nostr . Now ( )
mustRehashAndResign = true
2023-05-03 17:14:06 -04:00
}
2023-11-07 15:57:43 -05:00
if evt . Sig == "" || mustRehashAndResign {
2024-02-05 22:58:26 -05:00
if bunker != nil {
2024-06-25 21:18:26 -04:00
if err := bunker . SignEvent ( ctx , & evt ) ; err != nil {
2024-02-05 22:58:26 -05:00
return fmt . Errorf ( "failed to sign with bunker: %w" , err )
}
2024-05-14 22:52:56 -04:00
} else if numSigners := c . Uint ( "musig" ) ; numSigners > 1 && sec != "" {
pubkeys := c . StringSlice ( "musig-pubkey" )
secNonce := c . String ( "musig-nonce-secret" )
pubNonces := c . StringSlice ( "musig-nonce" )
partialSigs := c . StringSlice ( "musig-partial" )
2024-06-25 21:18:26 -04:00
signed , err := performMusig ( ctx ,
2024-05-14 14:23:08 -04:00
sec , & evt , int ( numSigners ) , pubkeys , pubNonces , secNonce , partialSigs )
if err != nil {
2024-05-14 22:52:56 -04:00
return fmt . Errorf ( "musig error: %w" , err )
2024-05-14 14:23:08 -04:00
}
if ! signed {
// we haven't finished signing the event, so the users still have to do more steps
// instructions for what to do should have been printed by the performMusig() function
return nil
}
2024-02-05 22:58:26 -05:00
} else if err := evt . Sign ( sec ) ; err != nil {
2023-11-07 15:57:43 -05:00
return fmt . Errorf ( "error signing with provided key: %w" , err )
}
2023-10-29 20:48:18 -04:00
}
2023-05-03 17:14:06 -04:00
2023-12-07 16:12:12 -05:00
// print event as json
var result string
if c . Bool ( "envelope" ) {
j , _ := json . Marshal ( nostr . EventEnvelope { Event : evt } )
result = string ( j )
} else {
j , _ := easyjson . Marshal ( & evt )
result = string ( j )
}
2024-01-24 20:38:51 -05:00
stdout ( result )
2023-12-07 16:12:12 -05:00
// publish to relays
2024-01-24 20:43:23 -05:00
successRelays := make ( [ ] string , 0 , len ( relays ) )
2023-11-07 15:57:43 -05:00
if len ( relays ) > 0 {
2023-11-13 12:57:35 -05:00
os . Stdout . Sync ( )
for _ , relay := range relays {
2023-12-09 07:14:45 -05:00
publish :
2023-11-13 13:03:27 -05:00
log ( "publishing to %s... " , relay . URL )
2024-06-25 21:18:26 -04:00
ctx , cancel := context . WithTimeout ( ctx , 10 * time . Second )
2023-12-09 07:14:45 -05:00
defer cancel ( )
2023-12-07 16:12:12 -05:00
2023-12-10 18:49:08 -05:00
err := relay . Publish ( ctx , evt )
if err == nil {
2023-12-09 14:32:04 -05:00
// published fine
log ( "success.\n" )
2024-01-24 20:43:23 -05:00
successRelays = append ( successRelays , relay . URL )
2024-01-16 07:16:56 -05:00
continue // continue to next relay
2023-12-09 07:14:45 -05:00
}
2023-12-07 16:12:12 -05:00
2023-12-09 07:14:45 -05:00
// error publishing
2024-02-05 22:58:26 -05:00
if strings . HasPrefix ( err . Error ( ) , "msg: auth-required:" ) && ( sec != "" || bunker != nil ) && doAuth {
2023-12-09 07:14:45 -05:00
// if the relay is requesting auth and we can auth, let's do it
2024-02-05 22:58:26 -05:00
var pk string
if bunker != nil {
2024-06-25 21:18:26 -04:00
pk , err = bunker . GetPublicKey ( ctx )
2024-02-05 22:58:26 -05:00
if err != nil {
return fmt . Errorf ( "failed to get public key from bunker: %w" , err )
}
} else {
pk , _ = nostr . GetPublicKey ( sec )
}
2023-12-09 14:32:04 -05:00
log ( "performing auth as %s... " , pk )
2024-06-25 21:18:26 -04:00
if err := relay . Auth ( ctx , func ( evt * nostr . Event ) error {
2024-02-05 22:58:26 -05:00
if bunker != nil {
2024-06-25 21:18:26 -04:00
return bunker . SignEvent ( ctx , evt )
2024-02-05 22:58:26 -05:00
}
return evt . Sign ( sec )
} ) ; err == nil {
2023-12-09 07:14:45 -05:00
// try to publish again, but this time don't try to auth again
doAuth = false
goto publish
} else {
log ( "auth error: %s. " , err )
2023-11-07 15:57:43 -05:00
}
2023-05-23 22:24:55 -04:00
}
2023-12-09 07:14:45 -05:00
log ( "failed: %s\n" , err )
2023-05-23 22:24:55 -04:00
}
2024-01-24 20:43:23 -05:00
if len ( successRelays ) > 0 && c . Bool ( "nevent" ) {
nevent , _ := nip19 . EncodeEvent ( evt . ID , successRelays , evt . PubKey )
log ( nevent + "\n" )
}
2023-05-23 22:24:55 -04:00
}
2023-05-03 17:14:06 -04:00
}
2024-06-25 21:18:26 -04:00
exitIfLineProcessingError ( ctx )
2023-05-03 17:14:06 -04:00
return nil
} ,
}