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
"strings"
"time"
2024-07-30 10:43:14 -04:00
"github.com/fiatjaf/cli/v3"
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-08-20 09:29:18 -04:00
"github.com/nbd-wtf/go-nostr/nip13"
2024-01-24 20:43:23 -05:00
"github.com/nbd-wtf/go-nostr/nip19"
2023-11-13 08:34:09 -05:00
"golang.org/x/exp/slices"
2023-05-03 17:14:06 -04:00
)
2024-08-20 09:29:18 -04:00
const (
CATEGORY_EVENT_FIELDS = "EVENT FIELDS"
CATEGORY_SIGNER = "SIGNER OPTIONS"
CATEGORY_EXTRAS = "EXTRAS"
)
2023-05-03 17:14:06 -04:00
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 ' ` ,
2024-08-07 10:46:07 -04:00
DisableSliceFlagSeparator : true ,
2024-09-17 10:27:59 -04:00
Flags : append ( defaultKeyFlags ,
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" ,
2024-08-20 09:29:18 -04:00
Category : CATEGORY_SIGNER ,
2024-05-14 14:23:08 -04:00
} ,
& 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 ,
} ,
// ~~~
2024-08-20 09:29:18 -04:00
& cli . UintFlag {
Name : "pow" ,
Usage : "NIP-13 difficulty to target when doing hash work on the event id" ,
Category : CATEGORY_EXTRAS ,
} ,
2023-05-03 17:14:06 -04:00
& cli . BoolFlag {
2024-08-20 09:29:18 -04:00
Name : "envelope" ,
Usage : "print the event enveloped in a [\"EVENT\", ...] message ready to be sent to a relay" ,
Category : CATEGORY_EXTRAS ,
2023-05-03 17:14:06 -04:00
} ,
2023-12-07 16:12:12 -05:00
& cli . BoolFlag {
2024-08-20 09:29:18 -04:00
Name : "auth" ,
Usage : "always perform NIP-42 \"AUTH\" when facing an \"auth-required: \" rejection and try again" ,
Category : CATEGORY_EXTRAS ,
2023-12-07 16:12:12 -05:00
} ,
2024-01-24 20:43:23 -05:00
& cli . BoolFlag {
2024-08-20 09:29:18 -04:00
Name : "nevent" ,
Usage : "print the nevent code (to stderr) after the event is published" ,
Category : CATEGORY_EXTRAS ,
2024-01-24 20:43:23 -05:00
} ,
2024-07-13 10:16:28 -04:00
& cli . UintFlag {
2023-05-03 17:14:06 -04:00
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" } ,
2024-09-10 16:44:33 -04:00
Usage : "sets a tag field on the event, takes a value like -t e=<id> or -t sometag=\"value one;value two;value three\"" ,
2023-05-03 17:14:06 -04:00
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 ,
} ,
2024-07-13 12:06:24 -04:00
& NaturalTimeFlag {
2023-05-03 17:14:06 -04:00
Name : "created-at" ,
Aliases : [ ] string { "time" , "ts" } ,
Usage : "unix timestamp value for the created_at field" ,
DefaultText : "now" ,
2024-07-13 12:06:24 -04:00
Value : nostr . Now ( ) ,
2023-05-03 17:14:06 -04:00
Category : CATEGORY_EVENT_FIELDS ,
} ,
2024-09-17 10:27:59 -04:00
) ,
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-09-17 07:09:20 -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-10-29 20:11:15 -04:00
kr , sec , err := gatherKeyerFromArguments ( 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
}
2024-07-13 10:16:28 -04:00
if kind := c . Uint ( "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
2024-08-26 14:48:06 -04:00
if c . IsSet ( "content" ) {
evt . Content = c . String ( "content" )
2023-11-07 15:57:43 -05:00
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
2024-08-20 09:29:18 -04:00
tagFlags := c . StringSlice ( "tag" )
tags := make ( nostr . Tags , 0 , len ( tagFlags ) + 2 )
for _ , tagFlag := range tagFlags {
2023-11-07 15:57:43 -05:00
// 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-08-20 09:29:18 -04:00
tags = append ( tags , 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
}
for _ , ptag := range c . StringSlice ( "p" ) {
2024-01-17 06:47:51 -05:00
tags = tags . AppendUnique ( [ ] string { "p" , ptag } )
}
for _ , dtag := range c . StringSlice ( "d" ) {
tags = tags . AppendUnique ( [ ] string { "d" , dtag } )
2023-11-07 15:57:43 -05:00
}
if len ( tags ) > 0 {
for _ , tag := range tags {
evt . Tags = append ( evt . Tags , tag )
}
mustRehashAndResign = true
2023-05-03 17:14:06 -04:00
}
2024-07-13 12:06:24 -04:00
if c . IsSet ( "created-at" ) {
evt . CreatedAt = getNaturalDate ( c , "created-at" )
2023-11-07 15:57:43 -05:00
mustRehashAndResign = true
} else if evt . CreatedAt == 0 {
evt . CreatedAt = nostr . Now ( )
mustRehashAndResign = true
2023-05-03 17:14:06 -04:00
}
2024-10-29 12:33:24 -04:00
if c . IsSet ( "musig" ) || c . IsSet ( "sec" ) || c . IsSet ( "prompt-sec" ) {
mustRehashAndResign = true
}
2024-08-20 09:29:18 -04:00
if difficulty := c . Uint ( "pow" ) ; difficulty > 0 {
// before doing pow we need the pubkey
2024-09-17 10:27:59 -04:00
if numSigners := c . Uint ( "musig" ) ; numSigners > 1 {
2024-08-20 09:29:18 -04:00
pubkeys := c . StringSlice ( "musig-pubkey" )
if int ( numSigners ) != len ( pubkeys ) {
return fmt . Errorf ( "when doing a pow with musig we must know all signer pubkeys upfront" )
}
evt . PubKey , err = getMusigAggregatedKey ( ctx , pubkeys )
if err != nil {
return err
}
} else {
2024-09-22 18:21:41 -04:00
evt . PubKey , _ = kr . GetPublicKey ( ctx )
2024-08-20 09:29:18 -04:00
}
2024-08-20 22:06:14 -04:00
// try to generate work with this difficulty -- runs forever
nonceTag , _ := nip13 . DoWork ( ctx , evt , int ( difficulty ) )
evt . Tags = append ( evt . Tags , nonceTag )
2024-08-20 09:29:18 -04:00
mustRehashAndResign = true
}
2023-11-07 15:57:43 -05:00
if evt . Sig == "" || mustRehashAndResign {
2024-10-29 20:11:15 -04:00
if numSigners := c . Uint ( "musig" ) ; numSigners > 1 {
// must do musig
2024-05-14 22:52:56 -04:00
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-09-17 10:27:59 -04:00
} else if err := kr . SignEvent ( ctx , & evt ) ; 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-10-29 20:11:15 -04:00
if strings . HasPrefix ( err . Error ( ) , "msg: auth-required:" ) && kr != 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-09-22 18:21:41 -04:00
pk , _ := kr . GetPublicKey ( ctx )
log ( "performing auth as %s... " , pk )
2024-09-17 10:27:59 -04:00
if err := relay . Auth ( ctx , func ( authEvent * nostr . Event ) error {
return kr . SignEvent ( ctx , authEvent )
2024-02-05 22:58:26 -05:00
} ) ; 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
} ,
}