Compare commits

...

13 Commits

Author SHA1 Message Date
fiatjaf
f9cf01b48b do target validation on a case-by-case basis and don't validate empty -author on nevent. 2023-07-08 20:52:50 -03:00
fiatjaf
fb7c49bb5c add nak encode to readme. 2023-07-05 15:05:09 -03:00
fiatjaf
4ad0769a62 add encode command with support for all the nip19 things. 2023-07-05 15:03:26 -03:00
fiatjaf
3ace11d7b2 support --nson flag on event. 2023-07-05 14:11:15 -03:00
fiatjaf
194e94ec9a update go-nostr for OK-related security fix. 2023-06-26 21:05:01 -03:00
fiatjaf
2b2018b742 allow extra tag elements on event creation, separated by ";" 2023-06-26 20:52:12 -03:00
fiatjaf
30c8eb83b2 rename editable to selectable. 2023-06-20 15:33:29 -03:00
fiatjaf
015cfd857c print naddr if parsed event is replaceable. 2023-06-20 15:31:53 -03:00
fiatjaf
4e5f7e6d21 print naddr when given an "a" tag. 2023-06-20 15:21:25 -03:00
fiatjaf
fb9faf24ae mention that the "d" tag is the identifier. 2023-06-20 14:52:03 -03:00
fiatjaf
76ca99a73b clicking on edit button to fill in the input. 2023-06-20 14:49:56 -03:00
fiatjaf
7890466783 removing relay hints. 2023-06-20 11:57:01 -03:00
fiatjaf
ba2d86ca33 each relay hint in a separate component. 2023-06-20 11:48:50 -03:00
11 changed files with 494 additions and 41 deletions

View File

@@ -56,6 +56,7 @@ COMMANDS:
req generates encoded REQ messages and optionally use them to talk to relays
event generates an encoded event and either prints it or sends it to a set of relays
decode decodes nip19, nip21, nip05 or hex entities
encode encodes notes and other stuff to nip19 entities
help, h Shows a list of commands or help for one command
GLOBAL OPTIONS:
@@ -154,6 +155,34 @@ OPTIONS:
--id, -e return just the event id, if applicable (default: false)
--pubkey, -p return just the pubkey, if applicable (default: false)
--help, -h show help
~> nak encode --help
NAME:
nak encode - encodes notes and other stuff to nip19 entities
USAGE:
nak encode command [command options] [arguments...]
DESCRIPTION:
example usage:
nak encode npub <pubkey-hex>
nak encode nprofile <pubkey-hex>
nak encode nprofile --relay <relay-url> <pubkey-hex>
nak encode nevent <event-id>
nak encode nevent --author <pubkey-hex> --relay <relay-url> --relay <other-relay> <event-id>
nak encode nsec <privkey-hex>
COMMANDS:
npub encode a hex private key into bech32 'npub' format
nsec encode a hex private key into bech32 'nsec' format
nprofile generate profile codes with attached relay information
nevent generate event codes with optionally attached relay information
naddr generate codes for NIP-33 parameterized replaceable events
help, h Shows a list of commands or help for one command
OPTIONS:
--help, -h show help
```
written in go using [go-nostr](https://github.com/nbd-wtf/go-nostr), heavily inspired by [nostril](http://git.jb55.com/nostril/).

7
edit.svg Normal file
View File

@@ -0,0 +1,7 @@
<?xml version="1.0" ?>
<svg width="800px" height="800px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<g>
<path d="M20,16v4a2,2,0,0,1-2,2H4a2,2,0,0,1-2-2V6A2,2,0,0,1,4,4H8" fill="none" stroke="#f9cc9d" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<polygon fill="none" points="12.5 15.8 22 6.2 17.8 2 8.3 11.5 8 16 12.5 15.8" stroke="#f9cc9d" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 463 B

224
encode.go Normal file
View File

@@ -0,0 +1,224 @@
package main
import (
"encoding/hex"
"fmt"
"net/url"
"strings"
"github.com/nbd-wtf/go-nostr/nip19"
"github.com/urfave/cli/v2"
)
var encode = &cli.Command{
Name: "encode",
Usage: "encodes notes and other stuff to nip19 entities",
Description: `example usage:
nak encode npub <pubkey-hex>
nak encode nprofile <pubkey-hex>
nak encode nprofile --relay <relay-url> <pubkey-hex>
nak encode nevent <event-id>
nak encode nevent --author <pubkey-hex> --relay <relay-url> --relay <other-relay> <event-id>
nak encode nsec <privkey-hex>`,
Before: func(c *cli.Context) error {
if c.Args().Len() < 2 {
return fmt.Errorf("expected more than 2 arguments.")
}
return nil
},
Subcommands: []*cli.Command{
{
Name: "npub",
Usage: "encode a hex private key into bech32 'npub' format",
Action: func(c *cli.Context) error {
target := c.Args().First()
if err := validate32BytesHex(target); err != nil {
return err
}
if npub, err := nip19.EncodePublicKey(target); err == nil {
fmt.Println(npub)
return nil
} else {
return err
}
},
},
{
Name: "nsec",
Usage: "encode a hex private key into bech32 'nsec' format",
Action: func(c *cli.Context) error {
target := c.Args().First()
if err := validate32BytesHex(target); err != nil {
return err
}
if npub, err := nip19.EncodePrivateKey(target); err == nil {
fmt.Println(npub)
return nil
} else {
return err
}
},
},
{
Name: "nprofile",
Usage: "generate profile codes with attached relay information",
Flags: []cli.Flag{
&cli.StringSliceFlag{
Name: "relay",
Aliases: []string{"r"},
Usage: "attach relay hints to nprofile code",
},
},
Action: func(c *cli.Context) error {
target := c.Args().First()
if err := validate32BytesHex(target); err != nil {
return err
}
relays := c.StringSlice("relay")
if err := validateRelayURLs(relays); err != nil {
return err
}
if npub, err := nip19.EncodeProfile(target, relays); err == nil {
fmt.Println(npub)
return nil
} else {
return err
}
},
},
{
Name: "nevent",
Usage: "generate event codes with optionally attached relay information",
Flags: []cli.Flag{
&cli.StringSliceFlag{
Name: "relay",
Aliases: []string{"r"},
Usage: "attach relay hints to nevent code",
},
&cli.StringFlag{
Name: "author",
Usage: "attach an author pubkey as a hint to the nevent code",
},
},
Action: func(c *cli.Context) error {
target := c.Args().First()
if err := validate32BytesHex(target); err != nil {
return err
}
author := c.String("author")
if author != "" {
if err := validate32BytesHex(author); err != nil {
return err
}
}
relays := c.StringSlice("relay")
if err := validateRelayURLs(relays); err != nil {
return err
}
if npub, err := nip19.EncodeEvent(target, relays, author); err == nil {
fmt.Println(npub)
return nil
} else {
return err
}
},
},
{
Name: "naddr",
Usage: "generate codes for NIP-33 parameterized replaceable events",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "identifier",
Aliases: []string{"d"},
Usage: "the \"d\" tag identifier of this replaceable event",
Required: true,
},
&cli.StringFlag{
Name: "pubkey",
Usage: "pubkey of the naddr author",
Aliases: []string{"p"},
Required: true,
},
&cli.Int64Flag{
Name: "kind",
Aliases: []string{"k"},
Usage: "kind of referred replaceable event",
Required: true,
},
&cli.StringSliceFlag{
Name: "relay",
Aliases: []string{"r"},
Usage: "attach relay hints to naddr code",
},
},
Action: func(c *cli.Context) error {
pubkey := c.String("pubkey")
if err := validate32BytesHex(pubkey); err != nil {
return err
}
kind := c.Int("kind")
if kind < 30000 || kind >= 40000 {
return fmt.Errorf("kind must be between 30000 and 39999, as per NIP-16, got %d", kind)
}
d := c.String("identifier")
if d == "" {
return fmt.Errorf("\"d\" tag identifier can't be empty")
}
relays := c.StringSlice("relay")
if err := validateRelayURLs(relays); err != nil {
return err
}
if npub, err := nip19.EncodeEntity(pubkey, kind, d, relays); err == nil {
fmt.Println(npub)
return nil
} else {
return err
}
},
},
},
}
func validate32BytesHex(target string) error {
if _, err := hex.DecodeString(target); err != nil {
return fmt.Errorf("target '%s' is not valid hex: %s", target, err)
}
if len(target) != 64 {
return fmt.Errorf("expected '%s' to be 64 characters (32 bytes), got %d", target, len(target))
}
if strings.ToLower(target) != target {
return fmt.Errorf("expected target to be all lowercase hex. try again with '%s'", strings.ToLower(target))
}
return nil
}
func validateRelayURLs(wsurls []string) error {
for _, wsurl := range wsurls {
u, err := url.Parse(wsurl)
if err != nil {
return fmt.Errorf("invalid relay url '%s': %s", wsurl, err)
}
if u.Scheme != "ws" && u.Scheme != "wss" {
return fmt.Errorf("relay url must use wss:// or ws:// schemes, got '%s'", wsurl)
}
if u.Host == "" {
return fmt.Errorf("relay url '%s' is missing the hostname", wsurl)
}
}
return nil
}

View File

@@ -9,7 +9,9 @@ import (
"strings"
"time"
"github.com/mailru/easyjson"
"github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nson"
"github.com/urfave/cli/v2"
)
@@ -33,6 +35,10 @@ standalone:
Name: "envelope",
Usage: "print the event enveloped in a [\"EVENT\", ...] message ready to be sent to a relay",
},
&cli.BoolFlag{
Name: "nson",
Usage: "encode the event using NSON",
},
&cli.IntFlag{
Name: "kind",
Aliases: []string{"k"},
@@ -82,11 +88,17 @@ standalone:
Tags: make(nostr.Tags, 0, 3),
}
tags := make([][]string, 0, 5)
tags := make(nostr.Tags, 0, 5)
for _, tagFlag := range c.StringSlice("tag") {
// tags are in the format key=value
spl := strings.Split(tagFlag, "=")
if len(spl) == 2 && len(spl[0]) > 0 {
tags = append(tags, spl)
tag := nostr.Tag{spl[0]}
// tags may also contain extra elements separated with a ";"
spl2 := strings.Split(spl[1], ";")
tag = append(tag, spl2...)
// ~
tags = append(tags, tag)
}
}
for _, etag := range c.StringSlice("e") {
@@ -138,8 +150,11 @@ standalone:
if c.Bool("envelope") {
j, _ := json.Marshal([]any{"EVENT", evt})
result = string(j)
} else if c.Bool("nson") {
result, _ = nson.Marshal(&evt)
} else {
result = evt.String()
j, _ := easyjson.Marshal(&evt)
result = string(j)
}
fmt.Println(result)
}

View File

@@ -1 +1 @@
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M17 13.5v6H5v-12h6m3-3h6v6m0-6-9 9" class="icon_svg-stroke" stroke="#666" stroke-width="1.5" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round"></path></svg>
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M17 13.5v6H5v-12h6m3-3h6v6m0-6-9 9" class="icon_svg-stroke" stroke="#3b82f6" stroke-width="1.5" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round"></path></svg>

Before

Width:  |  Height:  |  Size: 278 B

After

Width:  |  Height:  |  Size: 281 B

4
go.mod
View File

@@ -3,7 +3,8 @@ module github.com/fiatjaf/nak
go 1.20
require (
github.com/nbd-wtf/go-nostr v0.18.7
github.com/mailru/easyjson v0.7.7
github.com/nbd-wtf/go-nostr v0.19.2
github.com/urfave/cli/v2 v2.25.3
)
@@ -18,7 +19,6 @@ require (
github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.2.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/puzpuzpuz/xsync v1.5.2 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/tidwall/gjson v1.14.4 // indirect

4
go.sum
View File

@@ -61,8 +61,8 @@ github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlT
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/nbd-wtf/go-nostr v0.18.7 h1:UZBc5ewiosuTVEWi9rXRDEu83oH1W6Gf8n0QzyTP/UQ=
github.com/nbd-wtf/go-nostr v0.18.7/go.mod h1:F9y6+M8askJCjilLgMC3rD0moA6UtG1MCnyClNYXeys=
github.com/nbd-wtf/go-nostr v0.19.2 h1:Oofhe5+EKvf74fZQmYyX5G4RS74/na1aNabsB/cW9b4=
github.com/nbd-wtf/go-nostr v0.19.2/go.mod h1:F9y6+M8askJCjilLgMC3rD0moA6UtG1MCnyClNYXeys=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=

View File

@@ -15,6 +15,7 @@ func main() {
req,
event,
decode,
encode,
},
}

View File

@@ -16,7 +16,10 @@ import snow.*
import Utils.*
object Components {
def render32Bytes(bytes32: ByteVector32): Resource[IO, HtmlDivElement[IO]] =
def render32Bytes(
store: Store,
bytes32: ByteVector32
): Resource[IO, HtmlDivElement[IO]] =
div(
cls := "text-md",
entry("canonical hex", bytes32.toHex),
@@ -25,9 +28,16 @@ object Components {
cls := "mt-2 pl-2 mb-2",
entry(
"npub",
NIP19.encode(XOnlyPublicKey(bytes32)),
Some(
selectable(
store,
NIP19.encode(XOnlyPublicKey(bytes32))
)
)
),
nip19_21(
store,
"nprofile",
NIP19.encode(ProfilePointer(XOnlyPublicKey(bytes32)))
)
@@ -37,13 +47,26 @@ object Components {
cls := "pl-2 mb-2",
entry(
"nsec",
NIP19.encode(PrivateKey(bytes32)),
Some(
selectable(
store,
NIP19.encode(PrivateKey(bytes32))
)
)
),
entry(
"npub",
NIP19.encode(PrivateKey(bytes32).publicKey.xonly),
Some(
selectable(
store,
NIP19.encode(PrivateKey(bytes32).publicKey.xonly)
)
)
),
nip19_21(
store,
"nprofile",
NIP19.encode(ProfilePointer(PrivateKey(bytes32).publicKey.xonly))
)
@@ -52,6 +75,7 @@ object Components {
div(
cls := "pl-2 mb-2",
nip19_21(
store,
"nevent",
NIP19.encode(EventPointer(bytes32.toHex))
)
@@ -60,10 +84,16 @@ object Components {
cls := "pl-2 mb-2",
entry(
"note",
NIP19.encode(bytes32),
Some(
selectable(
store,
NIP19.encode(bytes32)
)
)
)
)
)
def renderEventPointer(
store: Store,
@@ -71,13 +101,21 @@ object Components {
): Resource[IO, HtmlDivElement[IO]] =
div(
cls := "text-md",
entry("event id (hex)", evp.id),
entry(
"event id (hex)",
evp.id,
Some(selectable(store, evp.id))
),
relayHints(store, evp.relays),
evp.author.map { pk =>
entry("author hint (pubkey hex)", pk.value.toHex)
},
nip19_21("nevent", NIP19.encode(evp)),
entry("note", NIP19.encode(ByteVector32.fromValidHex(evp.id)))
nip19_21(store, "nevent", NIP19.encode(evp)),
entry(
"note",
NIP19.encode(ByteVector32.fromValidHex(evp.id)),
Some(selectable(store, NIP19.encode(ByteVector32.fromValidHex(evp.id))))
)
)
def renderProfilePointer(
@@ -87,30 +125,55 @@ object Components {
): Resource[IO, HtmlDivElement[IO]] =
div(
cls := "text-md",
sk.map { k => entry("private key (hex)", k.value.toHex) },
sk.map { k => entry("nsec", NIP19.encode(k)) },
entry("public key (hex)", pp.pubkey.value.toHex),
sk.map { k =>
entry(
"private key (hex)",
k.value.toHex,
Some(selectable(store, k.value.toHex))
)
},
sk.map { k =>
entry(
"nsec",
NIP19.encode(k),
Some(selectable(store, NIP19.encode(k)))
)
},
entry(
"public key (hex)",
pp.pubkey.value.toHex,
Some(selectable(store, pp.pubkey.value.toHex))
),
relayHints(
store,
pp.relays,
dynamic = if sk.isDefined then false else true
),
entry("npub", NIP19.encode(pp.pubkey)),
nip19_21("nprofile", NIP19.encode(pp))
entry(
"npub",
NIP19.encode(pp.pubkey),
Some(selectable(store, NIP19.encode(pp.pubkey)))
),
nip19_21(store, "nprofile", NIP19.encode(pp))
)
def renderAddressPointer(
store: Store,
addr: snow.AddressPointer
): Resource[IO, HtmlDivElement[IO]] =
): Resource[IO, HtmlDivElement[IO]] = {
val nip33atag =
s"${addr.kind}:${addr.author.value.toHex}:${addr.d}"
div(
cls := "text-md",
entry("author (pubkey hex)", addr.author.value.toHex),
entry("identifier", addr.d),
entry("identifier (d tag)", addr.d),
entry("kind", addr.kind.toString),
relayHints(store, addr.relays),
nip19_21("naddr", NIP19.encode(addr))
nip19_21(store, "naddr", NIP19.encode(addr)),
entry("nip33 'a' tag", nip33atag, Some(selectable(store, nip33atag)))
)
}
def renderEvent(
store: Store,
@@ -205,35 +268,60 @@ object Components {
),
event.id.map(id =>
nip19_21(
store,
"nevent",
NIP19.encode(EventPointer(id, author = event.pubkey))
)
),
if event.kind >= 30000 && event.kind < 40000 then
event.pubkey
.map(author =>
nip19_21(
store,
"naddr",
NIP19.encode(
AddressPointer(
d = event.tags
.collectFirst { case "d" :: v :: _ => v }
.getOrElse(""),
kind = event.kind,
author = author,
relays = List.empty
)
)
)
)
else
event.id.map(id =>
entry(
"note",
NIP19.encode(ByteVector32.fromValidHex(id))
NIP19.encode(ByteVector32.fromValidHex(id)),
Some(selectable(store, NIP19.encode(ByteVector32.fromValidHex(id))))
)
)
)
private def entry(
key: String,
value: String
value: String,
selectLink: Option[Resource[IO, HtmlSpanElement[IO]]] = None
): Resource[IO, HtmlDivElement[IO]] =
div(
cls := "flex items-center space-x-3",
span(cls := "font-bold", key + " "),
span(Styles.mono, cls := "max-w-xl", value)
span(Styles.mono, cls := "max-w-xl break-all", value),
selectLink
)
private def nip19_21(
store: Store,
key: String,
code: String
): Resource[IO, HtmlDivElement[IO]] =
div(
span(cls := "font-bold", key + " "),
span(Styles.mono, cls := "break-all", code),
selectable(store, code),
a(
href := "nostr:" + code,
external
@@ -254,13 +342,65 @@ object Components {
div(
cls := "flex items-center space-x-3",
span(cls := "font-bold", "relay hints "),
span(Styles.mono, cls := "max-w-xl", value),
if relays.size == 0 then div("")
else
// displaying each relay hint
div(
cls := "flex flex-wrap max-w-xl",
relays
.map(url =>
div(
Styles.mono,
cls := "flex items-center rounded py-0.5 px-1 mr-1 mb-1 bg-orange-100",
url,
// removing a relay hint by clicking on the x
div(
cls := "cursor-pointer ml-1 text-rose-600 hover:text-rose-300",
onClick --> (_.foreach(_ => {
store.result.get.flatMap(result =>
store.input.set(
result
.map {
case a: AddressPointer =>
NIP19
.encode(
a.copy(relays =
relays.filterNot(_ == url)
)
)
case p: ProfilePointer =>
NIP19
.encode(
p.copy(relays =
relays.filterNot(_ == url)
)
)
case e: EventPointer =>
NIP19
.encode(
e.copy(relays =
relays.filterNot(_ == url)
)
)
case r => ""
}
.getOrElse("")
)
)
})),
"×"
)
)
)
)
,
active.map {
case true =>
div(
input.withSelf { self =>
(
onKeyPress --> (_.foreach(evt =>
// confirm adding a relay hint
evt.key match {
case "Enter" =>
self.value.get.flatMap(url =>
@@ -274,17 +414,17 @@ object Components {
case a: AddressPointer =>
NIP19
.encode(
a.copy(relays = url :: a.relays)
a.copy(relays = a.relays :+ url)
)
case p: ProfilePointer =>
NIP19
.encode(
p.copy(relays = url :: p.relays)
p.copy(relays = p.relays :+ url)
)
case e: EventPointer =>
NIP19
.encode(
e.copy(relays = url :: e.relays)
e.copy(relays = e.relays :+ url)
)
case r => ""
}
@@ -301,6 +441,7 @@ object Components {
}
)
case false if dynamic =>
// button to add a new relay hint
button(
Styles.buttonSmall,
"add relay hint",
@@ -311,5 +452,25 @@ object Components {
)
}
private def selectable(
store: Store,
code: String
): Resource[IO, HtmlSpanElement[IO]] =
span(
store.input.map(current =>
if current == code then a("")
else
a(
href := "#/" + code,
onClick --> (_.foreach(evt =>
evt.preventDefault >>
store.input.set(code)
)),
edit
)
)
)
private val edit = img(cls := "inline w-4 ml-2", src := "edit.svg")
private val external = img(cls := "inline w-4 ml-2", src := "ext.svg")
}

View File

@@ -18,7 +18,7 @@ object Main extends IOWebApp {
def render: Resource[IO, HtmlDivElement[IO]] = Store(window).flatMap {
store =>
div(
cls := "flex w-full h-full flex-col items-center justify-center",
cls := "flex w-full flex-col items-center justify-center",
div(
cls := "w-4/5",
h1(
@@ -131,7 +131,7 @@ object Main extends IOWebApp {
cls := "w-full flex my-5",
store.result.map {
case Left(msg) => div(msg)
case Right(bytes: ByteVector32) => render32Bytes(bytes)
case Right(bytes: ByteVector32) => render32Bytes(store, bytes)
case Right(event: Event) => renderEvent(store, event)
case Right(pp: ProfilePointer) => renderProfilePointer(store, pp)
case Right(evp: EventPointer) => renderEventPointer(store, evp)

View File

@@ -26,7 +26,23 @@ object Parser {
case Right(evp: EventPointer) => Right(evp)
case Right(sk: PrivateKey) => Right(sk)
case Right(addr: AddressPointer) => Right(addr)
case Left(_) if input.split(":").size == 3 =>
// parse "a" tag format, nip 33
val spl = input.split(":")
(
spl(0).toIntOption,
ByteVector.fromHex(spl(1)),
Some(spl(2))
).mapN((kind, author, identifier) =>
AddressPointer(
identifier,
kind,
scoin.XOnlyPublicKey(ByteVector32(author)),
relays = List.empty
)
).toRight("couldn't parse as a nip33 'a' tag")
case Left(_) =>
// parse event json
parse(input) match {
case Left(err: io.circe.ParsingFailure) =>
Left("not valid JSON or NIP-19 code")