diff --git a/.github/workflows/publish-webapp.yml b/.github/workflows/publish-webapp.yml deleted file mode 100644 index eb2d043..0000000 --- a/.github/workflows/publish-webapp.yml +++ /dev/null @@ -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: . diff --git a/build.sbt b/build.sbt deleted file mode 100644 index 36ebb0b..0000000 --- a/build.sbt +++ /dev/null @@ -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) } - ) diff --git a/edit.svg b/edit.svg deleted file mode 100644 index abfcd8b..0000000 --- a/edit.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/ext.svg b/ext.svg deleted file mode 100644 index 09048c0..0000000 --- a/ext.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/favicon.ico b/favicon.ico deleted file mode 100644 index 3736df3..0000000 Binary files a/favicon.ico and /dev/null differ diff --git a/index.html b/index.html deleted file mode 100644 index faebfe1..0000000 --- a/index.html +++ /dev/null @@ -1,7 +0,0 @@ - -nostr army knife - - -
- - diff --git a/justfile b/justfile deleted file mode 100644 index 50881fb..0000000 --- a/justfile +++ /dev/null @@ -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 diff --git a/project/build.properties b/project/build.properties deleted file mode 100644 index 22af262..0000000 --- a/project/build.properties +++ /dev/null @@ -1 +0,0 @@ -sbt.version=1.7.1 diff --git a/project/plugins.sbt b/project/plugins.sbt deleted file mode 100644 index dabefb2..0000000 --- a/project/plugins.sbt +++ /dev/null @@ -1,2 +0,0 @@ -addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.13.0") -addSbtPlugin("com.fiatjaf" % "sbt-esbuild" % "0.1.1") diff --git a/src/main/scala/Components.scala b/src/main/scala/Components.scala deleted file mode 100644 index 6137b14..0000000 --- a/src/main/scala/Components.scala +++ /dev/null @@ -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") -} diff --git a/src/main/scala/Main.scala b/src/main/scala/Main.scala deleted file mode 100644 index 7f3c914..0000000 --- a/src/main/scala/Main.scala +++ /dev/null @@ -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) - } - ) -} diff --git a/src/main/scala/Parser.scala b/src/main/scala/Parser.scala deleted file mode 100644 index 9033aaa..0000000 --- a/src/main/scala/Parser.scala +++ /dev/null @@ -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" - } - } - } - } - ) -} diff --git a/src/main/scala/Store.scala b/src/main/scala/Store.scala deleted file mode 100644 index 35dadd8..0000000 --- a/src/main/scala/Store.scala +++ /dev/null @@ -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) - } -} diff --git a/src/main/scala/Styles.scala b/src/main/scala/Styles.scala deleted file mode 100644 index 4a72181..0000000 --- a/src/main/scala/Styles.scala +++ /dev/null @@ -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" -} diff --git a/src/main/scala/Utils.scala b/src/main/scala/Utils.scala deleted file mode 100644 index 7a64810..0000000 --- a/src/main/scala/Utils.scala +++ /dev/null @@ -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 - ) -}