mirror of
https://github.com/fiatjaf/nak.git
synced 2024-10-30 09:09:08 -04:00
move nak-web into a separate repository.
This commit is contained in:
parent
85d658bdd4
commit
c5573410df
20
.github/workflows/publish-webapp.yml
vendored
20
.github/workflows/publish-webapp.yml
vendored
|
@ -1,20 +0,0 @@
|
||||||
name: build page and publish to cloudflare
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
with:
|
|
||||||
fetch-depth: 1
|
|
||||||
- uses: olafurpg/setup-scala@v11
|
|
||||||
- name: build page / compile scalajs
|
|
||||||
run: sbt fullLinkJS/esBuild
|
|
||||||
- name: publish to cloudflare
|
|
||||||
uses: cloudflare/pages-action@v1
|
|
||||||
with:
|
|
||||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
|
||||||
accountId: 60325047cc7d0811c6b337717918cbc1
|
|
||||||
projectName: nostr-army-knife
|
|
||||||
directory: .
|
|
14
build.sbt
14
build.sbt
|
@ -1,14 +0,0 @@
|
||||||
enablePlugins(ScalaJSPlugin, EsbuildPlugin)
|
|
||||||
|
|
||||||
name := "nostr-army-knife"
|
|
||||||
scalaVersion := "3.3.0-RC4"
|
|
||||||
|
|
||||||
lazy val root = (project in file("."))
|
|
||||||
.settings(
|
|
||||||
libraryDependencies ++= Seq(
|
|
||||||
"com.armanbilge" %%% "calico" % "0.2.0-RC2",
|
|
||||||
"com.fiatjaf" %%% "snow" % "0.0.1"
|
|
||||||
),
|
|
||||||
scalaJSUseMainModuleInitializer := true,
|
|
||||||
scalaJSLinkerConfig ~= { _.withModuleKind(ModuleKind.CommonJSModule) }
|
|
||||||
)
|
|
7
edit.svg
7
edit.svg
|
@ -1,7 +0,0 @@
|
||||||
<?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>
|
|
Before Width: | Height: | Size: 463 B |
1
ext.svg
1
ext.svg
|
@ -1 +0,0 @@
|
||||||
<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: 281 B |
BIN
favicon.ico
BIN
favicon.ico
Binary file not shown.
Before Width: | Height: | Size: 37 KiB |
|
@ -1,7 +0,0 @@
|
||||||
<meta charset=utf-8>
|
|
||||||
<title>nostr army knife</title>
|
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
|
||||||
<body class="bg-emerald-200 text-black m-0 w-full h-full">
|
|
||||||
<div id="app" class="w-full h-full"></div>
|
|
||||||
<script type="module" src="/target/esbuild/bundle.js"></script>
|
|
||||||
</body>
|
|
13
justfile
13
justfile
|
@ -1,13 +0,0 @@
|
||||||
build-prod:
|
|
||||||
sbt fullLinkJS/esBuild
|
|
||||||
|
|
||||||
cloudflare:
|
|
||||||
rm -fr cf
|
|
||||||
mkdir -p cf/target/esbuild
|
|
||||||
cp index.html cf/
|
|
||||||
cp favicon.ico cf/
|
|
||||||
cp target/esbuild/bundle.js cf/target/esbuild
|
|
||||||
wrangler pages publish cf --project-name nostr-army-knife --branch master
|
|
||||||
rm -fr cf
|
|
||||||
|
|
||||||
build-and-deploy: build-prod cloudflare
|
|
|
@ -1 +0,0 @@
|
||||||
sbt.version=1.7.1
|
|
|
@ -1,2 +0,0 @@
|
||||||
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.13.0")
|
|
||||||
addSbtPlugin("com.fiatjaf" % "sbt-esbuild" % "0.1.1")
|
|
|
@ -1,476 +0,0 @@
|
||||||
import cats.data.{Store => *, *}
|
|
||||||
import cats.effect.*
|
|
||||||
import cats.effect.syntax.all.*
|
|
||||||
import cats.syntax.all.*
|
|
||||||
import fs2.concurrent.*
|
|
||||||
import fs2.dom.{Event => _, *}
|
|
||||||
import io.circe.parser.*
|
|
||||||
import io.circe.syntax.*
|
|
||||||
import calico.*
|
|
||||||
import calico.html.io.{*, given}
|
|
||||||
import calico.syntax.*
|
|
||||||
import scodec.bits.ByteVector
|
|
||||||
import scoin.*
|
|
||||||
import snow.*
|
|
||||||
|
|
||||||
import Utils.*
|
|
||||||
|
|
||||||
object Components {
|
|
||||||
def render32Bytes(
|
|
||||||
store: Store,
|
|
||||||
bytes32: ByteVector32
|
|
||||||
): Resource[IO, HtmlDivElement[IO]] =
|
|
||||||
div(
|
|
||||||
cls := "text-md",
|
|
||||||
entry("canonical hex", bytes32.toHex),
|
|
||||||
"if this is a public key:",
|
|
||||||
div(
|
|
||||||
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)))
|
|
||||||
)
|
|
||||||
),
|
|
||||||
"if this is a private key:",
|
|
||||||
div(
|
|
||||||
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))
|
|
||||||
)
|
|
||||||
),
|
|
||||||
"if this is an event id:",
|
|
||||||
div(
|
|
||||||
cls := "pl-2 mb-2",
|
|
||||||
nip19_21(
|
|
||||||
store,
|
|
||||||
"nevent",
|
|
||||||
NIP19.encode(EventPointer(bytes32.toHex))
|
|
||||||
)
|
|
||||||
),
|
|
||||||
div(
|
|
||||||
cls := "pl-2 mb-2",
|
|
||||||
entry(
|
|
||||||
"note",
|
|
||||||
NIP19.encode(bytes32),
|
|
||||||
Some(
|
|
||||||
selectable(
|
|
||||||
store,
|
|
||||||
NIP19.encode(bytes32)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def renderEventPointer(
|
|
||||||
store: Store,
|
|
||||||
evp: snow.EventPointer
|
|
||||||
): Resource[IO, HtmlDivElement[IO]] =
|
|
||||||
div(
|
|
||||||
cls := "text-md",
|
|
||||||
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(store, "nevent", NIP19.encode(evp)),
|
|
||||||
entry(
|
|
||||||
"note",
|
|
||||||
NIP19.encode(ByteVector32.fromValidHex(evp.id)),
|
|
||||||
Some(selectable(store, NIP19.encode(ByteVector32.fromValidHex(evp.id))))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def renderProfilePointer(
|
|
||||||
store: Store,
|
|
||||||
pp: snow.ProfilePointer,
|
|
||||||
sk: Option[PrivateKey] = None
|
|
||||||
): Resource[IO, HtmlDivElement[IO]] =
|
|
||||||
div(
|
|
||||||
cls := "text-md",
|
|
||||||
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),
|
|
||||||
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]] = {
|
|
||||||
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 (d tag)", addr.d),
|
|
||||||
entry("kind", addr.kind.toString),
|
|
||||||
relayHints(store, addr.relays),
|
|
||||||
nip19_21(store, "naddr", NIP19.encode(addr)),
|
|
||||||
entry("nip33 'a' tag", nip33atag, Some(selectable(store, nip33atag)))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
def renderEvent(
|
|
||||||
store: Store,
|
|
||||||
event: Event
|
|
||||||
): Resource[IO, HtmlDivElement[IO]] =
|
|
||||||
div(
|
|
||||||
cls := "text-md",
|
|
||||||
if event.pubkey.isEmpty then
|
|
||||||
Some(
|
|
||||||
div(
|
|
||||||
cls := "flex items-center",
|
|
||||||
entry("missing", "pubkey"),
|
|
||||||
button(
|
|
||||||
Styles.buttonSmall,
|
|
||||||
"fill with a debugging key",
|
|
||||||
onClick --> (_.foreach { _ =>
|
|
||||||
store.input.set(
|
|
||||||
event
|
|
||||||
.copy(pubkey = Some(keyOne.publicKey.xonly))
|
|
||||||
.asJson
|
|
||||||
.printWith(jsonPrinter)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else None,
|
|
||||||
if event.id.isEmpty then
|
|
||||||
Some(
|
|
||||||
div(
|
|
||||||
cls := "flex items-center",
|
|
||||||
entry("missing", "id"),
|
|
||||||
if event.pubkey.isDefined then
|
|
||||||
Some(
|
|
||||||
button(
|
|
||||||
Styles.buttonSmall,
|
|
||||||
"fill id",
|
|
||||||
onClick --> (_.foreach(_ =>
|
|
||||||
store.input.set(
|
|
||||||
event
|
|
||||||
.copy(id = Some(event.hash.toHex))
|
|
||||||
.asJson
|
|
||||||
.printWith(jsonPrinter)
|
|
||||||
)
|
|
||||||
))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else None,
|
|
||||||
if event.sig.isEmpty then
|
|
||||||
Some(
|
|
||||||
div(
|
|
||||||
cls := "flex items-center",
|
|
||||||
entry("missing", "sig"),
|
|
||||||
if event.id.isDefined && event.pubkey == Some(
|
|
||||||
keyOne.publicKey.xonly
|
|
||||||
)
|
|
||||||
then
|
|
||||||
Some(
|
|
||||||
button(
|
|
||||||
Styles.buttonSmall,
|
|
||||||
"sign",
|
|
||||||
onClick --> (_.foreach(_ =>
|
|
||||||
store.input.set(
|
|
||||||
event
|
|
||||||
.sign(keyOne)
|
|
||||||
.asJson
|
|
||||||
.printWith(jsonPrinter)
|
|
||||||
)
|
|
||||||
))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else None,
|
|
||||||
entry("serialized event", event.serialized),
|
|
||||||
entry("implied event id", event.hash.toHex),
|
|
||||||
entry(
|
|
||||||
"does the implied event id match the given event id?",
|
|
||||||
event.id == Some(event.hash.toHex) match {
|
|
||||||
case true => "yes"; case false => "no"
|
|
||||||
}
|
|
||||||
),
|
|
||||||
entry(
|
|
||||||
"is signature valid?",
|
|
||||||
event.isValid match {
|
|
||||||
case true => "yes"; case false => "no"
|
|
||||||
}
|
|
||||||
),
|
|
||||||
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)),
|
|
||||||
Some(selectable(store, NIP19.encode(ByteVector32.fromValidHex(id))))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
private def entry(
|
|
||||||
key: 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 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
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
private def relayHints(
|
|
||||||
store: Store,
|
|
||||||
relays: List[String],
|
|
||||||
dynamic: Boolean = true
|
|
||||||
): Resource[IO, HtmlDivElement[IO]] =
|
|
||||||
if !dynamic && relays.isEmpty then div("")
|
|
||||||
else
|
|
||||||
SignallingRef[IO].of(false).toResource.flatMap { active =>
|
|
||||||
val value =
|
|
||||||
if relays.size > 0 then relays.reduce((a, b) => s"$a, $b") else ""
|
|
||||||
|
|
||||||
div(
|
|
||||||
cls := "flex items-center space-x-3",
|
|
||||||
span(cls := "font-bold", "relay hints "),
|
|
||||||
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 =>
|
|
||||||
if url.startsWith("wss://") || url
|
|
||||||
.startsWith("ws://")
|
|
||||||
then {
|
|
||||||
store.result.get.flatMap(result =>
|
|
||||||
store.input.set(
|
|
||||||
result
|
|
||||||
.map {
|
|
||||||
case a: AddressPointer =>
|
|
||||||
NIP19
|
|
||||||
.encode(
|
|
||||||
a.copy(relays = a.relays :+ url)
|
|
||||||
)
|
|
||||||
case p: ProfilePointer =>
|
|
||||||
NIP19
|
|
||||||
.encode(
|
|
||||||
p.copy(relays = p.relays :+ url)
|
|
||||||
)
|
|
||||||
case e: EventPointer =>
|
|
||||||
NIP19
|
|
||||||
.encode(
|
|
||||||
e.copy(relays = e.relays :+ url)
|
|
||||||
)
|
|
||||||
case r => ""
|
|
||||||
}
|
|
||||||
.getOrElse("")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
>> active.set(false)
|
|
||||||
} else IO.unit
|
|
||||||
)
|
|
||||||
case _ => IO.unit
|
|
||||||
}
|
|
||||||
))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
case false if dynamic =>
|
|
||||||
// button to add a new relay hint
|
|
||||||
button(
|
|
||||||
Styles.buttonSmall,
|
|
||||||
"add relay hint",
|
|
||||||
onClick --> (_.foreach(_ => active.set(true)))
|
|
||||||
)
|
|
||||||
case false => div("")
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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")
|
|
||||||
}
|
|
|
@ -1,147 +0,0 @@
|
||||||
import cats.effect.*
|
|
||||||
import cats.effect.syntax.all.*
|
|
||||||
import cats.syntax.all.*
|
|
||||||
import fs2.concurrent.*
|
|
||||||
import fs2.dom.{Event => _, *}
|
|
||||||
import io.circe.parser.*
|
|
||||||
import io.circe.syntax.*
|
|
||||||
import calico.*
|
|
||||||
import calico.html.io.{*, given}
|
|
||||||
import calico.syntax.*
|
|
||||||
import scoin.*
|
|
||||||
import snow.*
|
|
||||||
|
|
||||||
import Utils.*
|
|
||||||
import Components.*
|
|
||||||
|
|
||||||
object Main extends IOWebApp {
|
|
||||||
def render: Resource[IO, HtmlDivElement[IO]] = Store(window).flatMap {
|
|
||||||
store =>
|
|
||||||
div(
|
|
||||||
cls := "flex w-full flex-col items-center justify-center",
|
|
||||||
div(
|
|
||||||
cls := "w-4/5",
|
|
||||||
h1(
|
|
||||||
cls := "px-1 py-2 text-center text-xl",
|
|
||||||
img(
|
|
||||||
cls := "inline-block w-8 mr-2",
|
|
||||||
src := "/favicon.ico"
|
|
||||||
),
|
|
||||||
a(
|
|
||||||
href := "/",
|
|
||||||
"nostr army knife"
|
|
||||||
)
|
|
||||||
),
|
|
||||||
div(
|
|
||||||
cls := "flex my-3",
|
|
||||||
input(store),
|
|
||||||
actions(store)
|
|
||||||
),
|
|
||||||
result(store)
|
|
||||||
),
|
|
||||||
div(
|
|
||||||
cls := "flex justify-end mr-5 mt-10 text-xs w-4/5",
|
|
||||||
a(
|
|
||||||
href := "https://github.com/fiatjaf/nak",
|
|
||||||
"source code"
|
|
||||||
),
|
|
||||||
a(
|
|
||||||
cls := "ml-4",
|
|
||||||
href := "https://github.com/fiatjaf/nak",
|
|
||||||
"get the command-line tool"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
def actions(store: Store): Resource[IO, HtmlDivElement[IO]] =
|
|
||||||
div(
|
|
||||||
cls := "flex flex-col space-y-1 my-3",
|
|
||||||
store.input.map {
|
|
||||||
case "" => div("")
|
|
||||||
case _ =>
|
|
||||||
button(
|
|
||||||
Styles.button,
|
|
||||||
"clear",
|
|
||||||
onClick --> (_.foreach(_ => store.input.set("")))
|
|
||||||
)
|
|
||||||
},
|
|
||||||
store.result.map {
|
|
||||||
case Right(_: Event) =>
|
|
||||||
button(
|
|
||||||
Styles.button,
|
|
||||||
"format",
|
|
||||||
onClick --> (_.foreach(_ =>
|
|
||||||
store.input.update(original =>
|
|
||||||
parse(original).toOption
|
|
||||||
.map(_.printWith(jsonPrinter))
|
|
||||||
.getOrElse(original)
|
|
||||||
)
|
|
||||||
))
|
|
||||||
)
|
|
||||||
case _ => div("")
|
|
||||||
},
|
|
||||||
button(
|
|
||||||
Styles.button,
|
|
||||||
"generate event",
|
|
||||||
onClick --> (_.foreach(_ =>
|
|
||||||
store.input.set(
|
|
||||||
Event(
|
|
||||||
kind = 1,
|
|
||||||
content = "hello world"
|
|
||||||
).sign(keyOne)
|
|
||||||
.asJson
|
|
||||||
.printWith(jsonPrinter)
|
|
||||||
)
|
|
||||||
))
|
|
||||||
),
|
|
||||||
button(
|
|
||||||
Styles.button,
|
|
||||||
"generate keypair",
|
|
||||||
onClick --> (_.foreach(_ =>
|
|
||||||
store.input.set(
|
|
||||||
NIP19.encode(PrivateKey(randomBytes32()))
|
|
||||||
)
|
|
||||||
))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def input(store: Store): Resource[IO, HtmlDivElement[IO]] =
|
|
||||||
div(
|
|
||||||
cls := "w-full grow",
|
|
||||||
div(
|
|
||||||
cls := "w-full flex justify-center",
|
|
||||||
textArea.withSelf { self =>
|
|
||||||
(
|
|
||||||
cls := "w-full max-h-96 p-3 rounded",
|
|
||||||
styleAttr := "min-height: 280px; font-family: monospace",
|
|
||||||
spellCheck := false,
|
|
||||||
placeholder := "paste something nostric (event JSON, nprofile, npub, nevent etc or hex key or id)",
|
|
||||||
onInput --> (_.foreach(_ =>
|
|
||||||
self.value.get.flatMap(store.input.set)
|
|
||||||
)),
|
|
||||||
value <-- store.input
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def result(store: Store): Resource[IO, HtmlDivElement[IO]] =
|
|
||||||
div(
|
|
||||||
cls := "w-full flex my-5",
|
|
||||||
store.result.map {
|
|
||||||
case Left(msg) => div(msg)
|
|
||||||
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)
|
|
||||||
case Right(sk: PrivateKey) =>
|
|
||||||
renderProfilePointer(
|
|
||||||
store,
|
|
||||||
ProfilePointer(pubkey = sk.publicKey.xonly),
|
|
||||||
Some(sk)
|
|
||||||
)
|
|
||||||
case Right(addr: AddressPointer) => renderAddressPointer(store, addr)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,61 +0,0 @@
|
||||||
import scala.util.Try
|
|
||||||
import io.circe.parser.*
|
|
||||||
import cats.syntax.all.*
|
|
||||||
import scodec.bits.ByteVector
|
|
||||||
import scoin.*
|
|
||||||
import snow.*
|
|
||||||
|
|
||||||
type Result = Either[
|
|
||||||
String,
|
|
||||||
Event | PrivateKey | AddressPointer | EventPointer | ProfilePointer |
|
|
||||||
ByteVector32
|
|
||||||
]
|
|
||||||
|
|
||||||
object Parser {
|
|
||||||
val additions = raw" *\+ *".r
|
|
||||||
|
|
||||||
def parseInput(input: String): Result =
|
|
||||||
if input == "" then Left("")
|
|
||||||
else
|
|
||||||
ByteVector
|
|
||||||
.fromHex(input)
|
|
||||||
.flatMap(b => Try(Right(ByteVector32(b))).toOption)
|
|
||||||
.getOrElse(
|
|
||||||
NIP19.decode(input) match {
|
|
||||||
case Right(pp: ProfilePointer) => Right(pp)
|
|
||||||
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")
|
|
||||||
case Right(json) =>
|
|
||||||
json
|
|
||||||
.as[Event]
|
|
||||||
.leftMap { err =>
|
|
||||||
err.pathToRootString match {
|
|
||||||
case None => s"decoding ${err.pathToRootString}"
|
|
||||||
case Some(path) => s"field $path is missing or wrong"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,46 +0,0 @@
|
||||||
import cats.data.*
|
|
||||||
import cats.effect.*
|
|
||||||
import cats.effect.syntax.all.*
|
|
||||||
import cats.syntax.all.*
|
|
||||||
import fs2.concurrent.*
|
|
||||||
import fs2.dom.{Event => _, *}
|
|
||||||
import scoin.PrivateKey
|
|
||||||
|
|
||||||
case class Store(
|
|
||||||
input: SignallingRef[IO, String],
|
|
||||||
result: SignallingRef[IO, Result]
|
|
||||||
)
|
|
||||||
|
|
||||||
object Store {
|
|
||||||
def apply(window: Window[IO]): Resource[IO, Store] = {
|
|
||||||
val key = "nak-input"
|
|
||||||
|
|
||||||
for {
|
|
||||||
input <- SignallingRef[IO].of("").toResource
|
|
||||||
result <- SignallingRef[IO, Result](Left("")).toResource
|
|
||||||
|
|
||||||
_ <- Resource.eval {
|
|
||||||
OptionT(window.localStorage.getItem(key))
|
|
||||||
.foreachF(input.set(_))
|
|
||||||
}
|
|
||||||
|
|
||||||
_ <- window.localStorage
|
|
||||||
.events(window)
|
|
||||||
.foreach {
|
|
||||||
case Storage.Event.Updated(`key`, _, value, _) =>
|
|
||||||
input.set(value)
|
|
||||||
case _ => IO.unit
|
|
||||||
}
|
|
||||||
.compile
|
|
||||||
.drain
|
|
||||||
.background
|
|
||||||
|
|
||||||
_ <- input.discrete
|
|
||||||
.evalTap(input => IO.cede *> window.localStorage.setItem(key, input))
|
|
||||||
.evalTap(input => result.set(Parser.parseInput(input.trim())))
|
|
||||||
.compile
|
|
||||||
.drain
|
|
||||||
.background
|
|
||||||
} yield Store(input, result)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
import calico.html.io.*
|
|
||||||
|
|
||||||
object Styles {
|
|
||||||
val button = cls :=
|
|
||||||
"shrink bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 mx-2 px-4 rounded "
|
|
||||||
val buttonSmall = cls :=
|
|
||||||
"shrink text-sm bg-blue-500 hover:bg-blue-700 text-white font-bold mx-2 px-2 rounded "
|
|
||||||
val mono = styleAttr := "font-family: monospace"
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
import io.circe.Printer
|
|
||||||
import scodec.bits.ByteVector
|
|
||||||
import scoin.*
|
|
||||||
|
|
||||||
object Utils {
|
|
||||||
val keyOne = PrivateKey(ByteVector32(ByteVector(0x01).padLeft(32)))
|
|
||||||
|
|
||||||
val jsonPrinter = Printer(
|
|
||||||
dropNullValues = false,
|
|
||||||
indent = " ",
|
|
||||||
lbraceRight = "\n",
|
|
||||||
rbraceLeft = "\n",
|
|
||||||
lbracketRight = "\n",
|
|
||||||
rbracketLeft = "\n",
|
|
||||||
lrbracketsEmpty = "",
|
|
||||||
arrayCommaRight = "\n",
|
|
||||||
objectCommaRight = "\n",
|
|
||||||
colonLeft = "",
|
|
||||||
colonRight = " ",
|
|
||||||
sortKeys = true
|
|
||||||
)
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user