Compare commits

...

17 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
fiatjaf
dff57c207e accept tags with keys of any length. 2023-06-07 07:02:38 -03:00
fiatjaf
c3777abd81 fix "if this is a private key" section. 2023-06-02 09:04:25 -03:00
fiatjaf
746a13861d update go-nostr so subscriptions can end. 2023-05-30 17:51:40 -03:00
fiatjaf
bd7b22c4ff cancel publish context after 10 seconds. 2023-05-30 13:43:57 -03:00
11 changed files with 503 additions and 47 deletions

View File

@@ -56,6 +56,7 @@ COMMANDS:
req generates encoded REQ messages and optionally use them to talk to relays 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 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 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 help, h Shows a list of commands or help for one command
GLOBAL OPTIONS: GLOBAL OPTIONS:
@@ -154,6 +155,34 @@ OPTIONS:
--id, -e return just the event id, if applicable (default: false) --id, -e return just the event id, if applicable (default: false)
--pubkey, -p return just the pubkey, if applicable (default: false) --pubkey, -p return just the pubkey, if applicable (default: false)
--help, -h show help --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/). 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

@@ -1,6 +1,7 @@
package main package main
import ( import (
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"os" "os"
@@ -8,7 +9,9 @@ import (
"strings" "strings"
"time" "time"
"github.com/mailru/easyjson"
"github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nson"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
@@ -32,6 +35,10 @@ standalone:
Name: "envelope", Name: "envelope",
Usage: "print the event enveloped in a [\"EVENT\", ...] message ready to be sent to a relay", 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{ &cli.IntFlag{
Name: "kind", Name: "kind",
Aliases: []string{"k"}, Aliases: []string{"k"},
@@ -81,11 +88,17 @@ standalone:
Tags: make(nostr.Tags, 0, 3), Tags: make(nostr.Tags, 0, 3),
} }
tags := make([][]string, 0, 5) tags := make(nostr.Tags, 0, 5)
for _, tagFlag := range c.StringSlice("tag") { for _, tagFlag := range c.StringSlice("tag") {
// tags are in the format key=value
spl := strings.Split(tagFlag, "=") spl := strings.Split(tagFlag, "=")
if len(spl) == 2 && len(spl[0]) == 1 { 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") { for _, etag := range c.StringSlice("e") {
@@ -123,7 +136,9 @@ standalone:
if relay, err := nostr.RelayConnect(c.Context, url); err != nil { if relay, err := nostr.RelayConnect(c.Context, url); err != nil {
fmt.Fprintf(os.Stderr, "failed to connect: %s\n", err) fmt.Fprintf(os.Stderr, "failed to connect: %s\n", err)
} else { } else {
if status, err := relay.Publish(c.Context, evt); err != nil { ctx, cancel := context.WithTimeout(c.Context, 10*time.Second)
defer cancel()
if status, err := relay.Publish(ctx, evt); err != nil {
fmt.Fprintf(os.Stderr, "failed: %s\n", err) fmt.Fprintf(os.Stderr, "failed: %s\n", err)
} else { } else {
fmt.Fprintf(os.Stderr, "%s.\n", status) fmt.Fprintf(os.Stderr, "%s.\n", status)
@@ -135,8 +150,11 @@ standalone:
if c.Bool("envelope") { if c.Bool("envelope") {
j, _ := json.Marshal([]any{"EVENT", evt}) j, _ := json.Marshal([]any{"EVENT", evt})
result = string(j) result = string(j)
} else if c.Bool("nson") {
result, _ = nson.Marshal(&evt)
} else { } else {
result = evt.String() j, _ := easyjson.Marshal(&evt)
result = string(j)
} }
fmt.Println(result) 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

6
go.mod
View File

@@ -3,12 +3,12 @@ module github.com/fiatjaf/nak
go 1.20 go 1.20
require ( require (
github.com/nbd-wtf/go-nostr v0.18.3 github.com/mailru/easyjson v0.7.7
github.com/nbd-wtf/go-nostr v0.19.2
github.com/urfave/cli/v2 v2.25.3 github.com/urfave/cli/v2 v2.25.3
) )
require ( require (
github.com/SaveTheRbtz/generic-sync-map-go v0.0.0-20220414055132-a37292614db8 // indirect
github.com/btcsuite/btcd/btcec/v2 v2.2.0 // indirect github.com/btcsuite/btcd/btcec/v2 v2.2.0 // indirect
github.com/btcsuite/btcd/btcutil v1.1.3 // indirect github.com/btcsuite/btcd/btcutil v1.1.3 // indirect
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 // indirect github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 // indirect
@@ -19,7 +19,7 @@ require (
github.com/gobwas/pool v0.2.1 // indirect github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.2.0 // indirect github.com/gobwas/ws v1.2.0 // indirect
github.com/josharian/intern v1.0.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/russross/blackfriday/v2 v2.1.0 // indirect
github.com/tidwall/gjson v1.14.4 // indirect github.com/tidwall/gjson v1.14.4 // indirect
github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/match v1.1.1 // indirect

8
go.sum
View File

@@ -1,5 +1,3 @@
github.com/SaveTheRbtz/generic-sync-map-go v0.0.0-20220414055132-a37292614db8 h1:Xa6tp8DPDhdV+k23uiTC/GrAYOe4IdyJVKtob4KW3GA=
github.com/SaveTheRbtz/generic-sync-map-go v0.0.0-20220414055132-a37292614db8/go.mod h1:ihkm1viTbO/LOsgdGoFPBSvzqvx7ibvkMzYp3CgtHik=
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M= github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M=
@@ -63,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/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 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/nbd-wtf/go-nostr v0.18.3 h1:ofMYxlFAptyoErlOGOCUk7zGHQNJ8/ZkIXXOsveFZ+c= github.com/nbd-wtf/go-nostr v0.19.2 h1:Oofhe5+EKvf74fZQmYyX5G4RS74/na1aNabsB/cW9b4=
github.com/nbd-wtf/go-nostr v0.18.3/go.mod h1:GPJOOK8US38kz+bfb9nWe873Xu0e6bXlThejOs1LTkc= 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/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.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
@@ -75,6 +73,8 @@ github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1Cpa
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/puzpuzpuz/xsync v1.5.2 h1:yRAP4wqSOZG+/4pxJ08fPTwrfL0IzE/LKQ/cw509qGY=
github.com/puzpuzpuz/xsync v1.5.2/go.mod h1:K98BYhX3k1dQ2M63t1YNVDanbwUPmBCAhNmVrrxfiGg=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=

View File

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

View File

@@ -16,7 +16,10 @@ import snow.*
import Utils.* import Utils.*
object Components { object Components {
def render32Bytes(bytes32: ByteVector32): Resource[IO, HtmlDivElement[IO]] = def render32Bytes(
store: Store,
bytes32: ByteVector32
): Resource[IO, HtmlDivElement[IO]] =
div( div(
cls := "text-md", cls := "text-md",
entry("canonical hex", bytes32.toHex), entry("canonical hex", bytes32.toHex),
@@ -25,9 +28,16 @@ object Components {
cls := "mt-2 pl-2 mb-2", cls := "mt-2 pl-2 mb-2",
entry( entry(
"npub", "npub",
NIP19.encode(XOnlyPublicKey(bytes32)) NIP19.encode(XOnlyPublicKey(bytes32)),
Some(
selectable(
store,
NIP19.encode(XOnlyPublicKey(bytes32))
)
)
), ),
nip19_21( nip19_21(
store,
"nprofile", "nprofile",
NIP19.encode(ProfilePointer(XOnlyPublicKey(bytes32))) NIP19.encode(ProfilePointer(XOnlyPublicKey(bytes32)))
) )
@@ -37,21 +47,35 @@ object Components {
cls := "pl-2 mb-2", cls := "pl-2 mb-2",
entry( entry(
"nsec", "nsec",
NIP19.encode(PrivateKey(bytes32)) NIP19.encode(PrivateKey(bytes32)),
Some(
selectable(
store,
NIP19.encode(PrivateKey(bytes32))
)
)
), ),
entry( entry(
"npub", "npub",
NIP19.encode(XOnlyPublicKey(bytes32)) NIP19.encode(PrivateKey(bytes32).publicKey.xonly),
Some(
selectable(
store,
NIP19.encode(PrivateKey(bytes32).publicKey.xonly)
)
)
), ),
nip19_21( nip19_21(
store,
"nprofile", "nprofile",
NIP19.encode(ProfilePointer(XOnlyPublicKey(bytes32))) NIP19.encode(ProfilePointer(PrivateKey(bytes32).publicKey.xonly))
) )
), ),
"if this is an event id:", "if this is an event id:",
div( div(
cls := "pl-2 mb-2", cls := "pl-2 mb-2",
nip19_21( nip19_21(
store,
"nevent", "nevent",
NIP19.encode(EventPointer(bytes32.toHex)) NIP19.encode(EventPointer(bytes32.toHex))
) )
@@ -60,7 +84,13 @@ object Components {
cls := "pl-2 mb-2", cls := "pl-2 mb-2",
entry( entry(
"note", "note",
NIP19.encode(bytes32) NIP19.encode(bytes32),
Some(
selectable(
store,
NIP19.encode(bytes32)
)
)
) )
) )
) )
@@ -71,13 +101,21 @@ object Components {
): Resource[IO, HtmlDivElement[IO]] = ): Resource[IO, HtmlDivElement[IO]] =
div( div(
cls := "text-md", cls := "text-md",
entry("event id (hex)", evp.id), entry(
"event id (hex)",
evp.id,
Some(selectable(store, evp.id))
),
relayHints(store, evp.relays), relayHints(store, evp.relays),
evp.author.map { pk => evp.author.map { pk =>
entry("author hint (pubkey hex)", pk.value.toHex) entry("author hint (pubkey hex)", pk.value.toHex)
}, },
nip19_21("nevent", NIP19.encode(evp)), nip19_21(store, "nevent", NIP19.encode(evp)),
entry("note", NIP19.encode(ByteVector32.fromValidHex(evp.id))) entry(
"note",
NIP19.encode(ByteVector32.fromValidHex(evp.id)),
Some(selectable(store, NIP19.encode(ByteVector32.fromValidHex(evp.id))))
)
) )
def renderProfilePointer( def renderProfilePointer(
@@ -87,30 +125,55 @@ object Components {
): Resource[IO, HtmlDivElement[IO]] = ): Resource[IO, HtmlDivElement[IO]] =
div( div(
cls := "text-md", cls := "text-md",
sk.map { k => entry("private key (hex)", k.value.toHex) }, sk.map { k =>
sk.map { k => entry("nsec", NIP19.encode(k)) }, entry(
entry("public key (hex)", pp.pubkey.value.toHex), "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( relayHints(
store, store,
pp.relays, pp.relays,
dynamic = if sk.isDefined then false else true dynamic = if sk.isDefined then false else true
), ),
entry("npub", NIP19.encode(pp.pubkey)), entry(
nip19_21("nprofile", NIP19.encode(pp)) "npub",
NIP19.encode(pp.pubkey),
Some(selectable(store, NIP19.encode(pp.pubkey)))
),
nip19_21(store, "nprofile", NIP19.encode(pp))
) )
def renderAddressPointer( def renderAddressPointer(
store: Store, store: Store,
addr: snow.AddressPointer addr: snow.AddressPointer
): Resource[IO, HtmlDivElement[IO]] = ): Resource[IO, HtmlDivElement[IO]] = {
val nip33atag =
s"${addr.kind}:${addr.author.value.toHex}:${addr.d}"
div( div(
cls := "text-md", cls := "text-md",
entry("author (pubkey hex)", addr.author.value.toHex), entry("author (pubkey hex)", addr.author.value.toHex),
entry("identifier", addr.d), entry("identifier (d tag)", addr.d),
entry("kind", addr.kind.toString), entry("kind", addr.kind.toString),
relayHints(store, addr.relays), 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( def renderEvent(
store: Store, store: Store,
@@ -205,35 +268,60 @@ object Components {
), ),
event.id.map(id => event.id.map(id =>
nip19_21( nip19_21(
store,
"nevent", "nevent",
NIP19.encode(EventPointer(id, author = event.pubkey)) NIP19.encode(EventPointer(id, author = event.pubkey))
) )
), ),
event.id.map(id => if event.kind >= 30000 && event.kind < 40000 then
entry( event.pubkey
"note", .map(author =>
NIP19.encode(ByteVector32.fromValidHex(id)) 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)),
Some(selectable(store, NIP19.encode(ByteVector32.fromValidHex(id))))
)
) )
)
) )
private def entry( private def entry(
key: String, key: String,
value: String value: String,
selectLink: Option[Resource[IO, HtmlSpanElement[IO]]] = None
): Resource[IO, HtmlDivElement[IO]] = ): Resource[IO, HtmlDivElement[IO]] =
div( div(
cls := "flex items-center space-x-3", cls := "flex items-center space-x-3",
span(cls := "font-bold", key + " "), 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( private def nip19_21(
store: Store,
key: String, key: String,
code: String code: String
): Resource[IO, HtmlDivElement[IO]] = ): Resource[IO, HtmlDivElement[IO]] =
div( div(
span(cls := "font-bold", key + " "), span(cls := "font-bold", key + " "),
span(Styles.mono, cls := "break-all", code), span(Styles.mono, cls := "break-all", code),
selectable(store, code),
a( a(
href := "nostr:" + code, href := "nostr:" + code,
external external
@@ -254,13 +342,65 @@ object Components {
div( div(
cls := "flex items-center space-x-3", cls := "flex items-center space-x-3",
span(cls := "font-bold", "relay hints "), 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 { active.map {
case true => case true =>
div( div(
input.withSelf { self => input.withSelf { self =>
( (
onKeyPress --> (_.foreach(evt => onKeyPress --> (_.foreach(evt =>
// confirm adding a relay hint
evt.key match { evt.key match {
case "Enter" => case "Enter" =>
self.value.get.flatMap(url => self.value.get.flatMap(url =>
@@ -274,17 +414,17 @@ object Components {
case a: AddressPointer => case a: AddressPointer =>
NIP19 NIP19
.encode( .encode(
a.copy(relays = url :: a.relays) a.copy(relays = a.relays :+ url)
) )
case p: ProfilePointer => case p: ProfilePointer =>
NIP19 NIP19
.encode( .encode(
p.copy(relays = url :: p.relays) p.copy(relays = p.relays :+ url)
) )
case e: EventPointer => case e: EventPointer =>
NIP19 NIP19
.encode( .encode(
e.copy(relays = url :: e.relays) e.copy(relays = e.relays :+ url)
) )
case r => "" case r => ""
} }
@@ -301,6 +441,7 @@ object Components {
} }
) )
case false if dynamic => case false if dynamic =>
// button to add a new relay hint
button( button(
Styles.buttonSmall, Styles.buttonSmall,
"add relay hint", "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") 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 { def render: Resource[IO, HtmlDivElement[IO]] = Store(window).flatMap {
store => store =>
div( div(
cls := "flex w-full h-full flex-col items-center justify-center", cls := "flex w-full flex-col items-center justify-center",
div( div(
cls := "w-4/5", cls := "w-4/5",
h1( h1(
@@ -131,7 +131,7 @@ object Main extends IOWebApp {
cls := "w-full flex my-5", cls := "w-full flex my-5",
store.result.map { store.result.map {
case Left(msg) => div(msg) 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(event: Event) => renderEvent(store, event)
case Right(pp: ProfilePointer) => renderProfilePointer(store, pp) case Right(pp: ProfilePointer) => renderProfilePointer(store, pp)
case Right(evp: EventPointer) => renderEventPointer(store, evp) case Right(evp: EventPointer) => renderEventPointer(store, evp)

View File

@@ -22,11 +22,27 @@ object Parser {
.flatMap(b => Try(Right(ByteVector32(b))).toOption) .flatMap(b => Try(Right(ByteVector32(b))).toOption)
.getOrElse( .getOrElse(
NIP19.decode(input) match { NIP19.decode(input) match {
case Right(pp: ProfilePointer) => Right(pp) case Right(pp: ProfilePointer) => Right(pp)
case Right(evp: EventPointer) => Right(evp) case Right(evp: EventPointer) => Right(evp)
case Right(sk: PrivateKey) => Right(sk) case Right(sk: PrivateKey) => Right(sk)
case Right(addr: AddressPointer) => Right(addr) 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(_) => case Left(_) =>
// parse event json
parse(input) match { parse(input) match {
case Left(err: io.circe.ParsingFailure) => case Left(err: io.circe.ParsingFailure) =>
Left("not valid JSON or NIP-19 code") Left("not valid JSON or NIP-19 code")